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

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

715 lines 21.1 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 if (!class_exists('WP_Optimize_WebP')) :
6
7 class WP_Optimize_WebP {
8
9 /**
10 * @var WP_Optimize_Htaccess
11 */
12 private $_htaccess;
13
14 /**
15 * Set to true when webp is enabled and vice versa
16 *
17 * @var boolean
18 */
19 private $_should_use_webp;
20
21 /**
22 * The logger for this instance
23 *
24 * @var Updraft_File_Logger
25 */
26 private $logger;
27
28 /**
29 * Constructor
30 */
31 private function __construct() {
32 $this->_should_use_webp = (bool) WP_Optimize()->get_options()->get_option('webp_conversion');
33
34 $this->logger = new Updraft_File_Logger($this->get_logfile_path());
35
36 add_action('wpo_reset_webp_conversion_test_result', array($this, 'reset_webp_serving_method'));
37 add_action('wpo_prune_webp_logs', array($this, 'prune_webp_logs'));
38 $task_manager = WPO_Webp_Task_Manager::get_instance();
39 add_action('wpo_webp_convert_compressed_images', array($task_manager, 'webp_convert_compressed_images'));
40 }
41
42 /**
43 * Returns singleton instance
44 *
45 * @return self
46 */
47 public static function get_instance(): self {
48 static $instance = null;
49 if (null === $instance) {
50 $instance = new self();
51 }
52 return $instance;
53 }
54
55 /**
56 * Evaluate WebP conversion capability.
57 *
58 * @return array{is_available: bool, reason: string, message: string, converter_status: array<string, array<string>>, test_ran: bool}
59 */
60 public function evaluate_webp_capability(): array {
61 $converter_status = array();
62 $test_ran = false;
63 if ($this->is_only_shell_converters_available() && !$this->shell_functions_available()) {
64 return array(
65 'is_available' => false,
66 'reason' => 'update_failed_no_shell_functions',
67 'message' => __('Required WebP shell functions are not available on the server.', 'wp-optimize'),
68 'converter_status' => $converter_status,
69 'test_ran' => $test_ran,
70 );
71 }
72
73 if ($this->should_run_webp_conversion_test()) {
74 $test_ran = true;
75 $converter_status = WPO_WebP_Test_Run::get_converter_status();
76 if (!$this->is_webp_conversion_successful()) {
77 return array(
78 'is_available' => false,
79 'reason' => 'update_failed_no_working_webp_converter',
80 'message' => __('No working WebP converter was found on the server.', 'wp-optimize'),
81 'converter_status' => $converter_status,
82 'test_ran' => $test_ran,
83 );
84 }
85 }
86
87 return array(
88 'is_available' => true,
89 'reason' => '',
90 'message' => '',
91 'converter_status' => $converter_status,
92 'test_ran' => $test_ran,
93 );
94 }
95
96 /**
97 * Try to enable webp conversion, upon failure disable webp conversion
98 *
99 * @param array<string, string> $data - Data webp_conversion (true|false)
100 * @return WP_Error|bool - information about the operation or WP_Error object on failure
101 */
102 private function configure_webp_conversion($data) {
103
104 $options = array();
105 $options['webp_conversion'] = isset($data['webp_conversion']) ? filter_var($data['webp_conversion'], FILTER_VALIDATE_BOOLEAN) : false;
106
107 if ($options['webp_conversion']) {
108 $capability = $this->evaluate_webp_capability();
109
110 if (!$capability['is_available']) {
111 $this->disable_webp_conversion();
112 $message = $capability['message'];
113 $reason = $capability['reason'];
114 $this->log($message);
115 return new WP_Error($reason, $message);
116 }
117
118 // Only update the test result and converters if the test actually ran
119 if ($capability['test_ran']) {
120 $options['webp_conversion_test'] = true;
121 $options['webp_converters'] = $capability['converter_status']['working_converters'] ?? array();
122 }
123
124 // Run serving methods tests and set necessary option values
125 // Not possible to test alter HTML since the test is browser-based
126 $this->save_htaccess_rules();
127 if (!$this->is_webp_redirection_possible()) {
128 $this->empty_htaccess_file();
129 $options['redirection_possible'] = 'false';
130 } else {
131 $options['redirection_possible'] = 'true';
132 }
133 }
134 $smush_manager = Updraft_Smush_Manager::instance();
135 $success = $smush_manager->update_smush_options($options);
136
137 if (!$success) {
138 $this->disable_webp_conversion();
139 $message = __('WebP options could not be updated.', 'wp-optimize');
140 $this->log($message);
141 return new WP_Error('update_failed', $message);
142 }
143
144 // Set up daily CRON only when enabling WebP and Delete daily CRON when disabling WebP
145 if ($options['webp_conversion']) {
146 $this->init_webp_cron_scheduler();
147 } else {
148 $this->remove_webp_cron_schedules();
149 $this->empty_htaccess_file();
150 }
151 return $success;
152 }
153
154 /**
155 * Save WebP settings and trigger related actions.
156 *
157 * @param array<string, string> $data WebP settings (expects 'webp_conversion').
158 * @return bool|WP_Error True on success, WP_Error on failure.
159 */
160 public function save_webp_settings($data) {
161
162 if (!is_array($data)) {
163 return new WP_Error('invalid_data', __('Invalid WebP settings data.', 'wp-optimize'));
164 }
165
166 $result = $this->configure_webp_conversion($data);
167
168 if (!is_wp_error($result)) {
169 do_action('wpo_save_images_settings');
170 }
171
172 return $result;
173 }
174
175 /**
176 * Returns the path to the logfile
177 *
178 * @return string - file path
179 */
180 private function get_logfile_path(): string {
181 return WP_Optimize_Utils::get_log_file_path('webp');
182 }
183
184 /**
185 * Logging of interesting messages related to WebP.
186 *
187 * @param string $message Log message
188 * @param string $level Log level (e.g., 'info', 'warning', 'error')
189 *
190 * @return void
191 */
192 public function log(string $message, string $level = 'info'): void {
193 $this->logger->log($message, $level);
194 }
195
196 /**
197 * Prunes the log file
198 *
199 * @return void
200 */
201 public function prune_webp_logs(): void {
202 $this->log("Pruning the WebP log file");
203 $this->logger->prune_logs();
204 }
205
206 /**
207 * Test Run and find converter status
208 *
209 * @return void
210 */
211 private function set_converter_status(): void {
212 $converter_status = WPO_WebP_Test_Run::get_converter_status();
213 if ($this->is_webp_conversion_successful()) {
214 WP_Optimize()->get_options()->update_option('webp_conversion_test', true);
215 WP_Optimize()->get_options()->update_option('webp_converters', $converter_status['working_converters']);
216 }
217 }
218
219 /**
220 * If .htaccess redirection is not possible, attempts to use the alter_html method.
221 *
222 * @param string $buffer Page HTML
223 *
224 * @return string
225 */
226 public function maybe_decide_webp_serve_method(string $buffer): string {
227 if (!$this->is_webp_redirection_possible()) {
228 $buffer = $this->maybe_use_alter_html($buffer);
229 }
230 return $buffer;
231 }
232
233 /**
234 * If alter HTML method is possible, apply it to the buffer.
235 *
236 * @param string $buffer Page HTML
237 *
238 * @return string
239 */
240 private function maybe_use_alter_html(string $buffer): string {
241 if ($this->is_alter_html_possible()) {
242 $this->empty_htaccess_file();
243 $buffer = WPO_WebP_Alter_HTML::get_instance()->alter_html($buffer);
244 }
245 return $buffer;
246 }
247
248 /**
249 * Even if the server supports .htaccess rewrite, sometimes it is not possible
250 * to serve webp images. This method determines whether webp redirection is possible.
251 *
252 * Also applies `wpo_force_webp_serve_using_altered_html` filter for users to be able to
253 * force the Altered HTML method.
254 *
255 * @return bool
256 */
257 public function is_webp_redirection_possible(): bool {
258 if (apply_filters('wpo_force_webp_serve_using_altered_html', false)) {
259 return false;
260 }
261
262 $redirection_possible = WP_Optimize()->get_options()->get_option('redirection_possible');
263
264 // If a previous test result exists, use it; otherwise, run the self test
265 if (!empty($redirection_possible)) {
266 return 'true' === $redirection_possible;
267 }
268
269 return $this->run_webp_serving_self_test();
270 }
271
272 /**
273 * Detect whether using alter HTML method is possible or not
274 *
275 * @return bool
276 */
277 private function is_alter_html_possible(): bool {
278 return WPO_WebP_Utils::is_browser_accepting_webp();
279 }
280
281 /**
282 * Initialize .htaccess
283 *
284 * @return void
285 */
286 private function setup_htaccess_file(): void {
287 if (null !== $this->_htaccess) return;
288 $wp_uploads = wp_get_upload_dir();
289 $htaccess_file = $wp_uploads['basedir'] . '/.htaccess';
290 if (!file_exists($htaccess_file)) {
291 file_put_contents($htaccess_file, '');
292 }
293 $this->_htaccess = new WP_Optimize_Htaccess($htaccess_file);
294 }
295
296 /**
297 * Save .htaccess rules
298 *
299 * @return void
300 */
301 private function save_htaccess_rules(): void {
302 $this->setup_htaccess_file();
303 $this->add_webp_mime_type();
304 $htaccess_comment_section = 'WP-Optimize WebP Rules';
305 if ($this->_htaccess->is_commented_section_exists($htaccess_comment_section)) return;
306 $this->_htaccess->update_commented_section($this->prepare_webp_htaccess_rules(), $htaccess_comment_section);
307 $this->_htaccess->write_file();
308 WP_Optimize()->get_options()->update_option('htaccess_has_webp_rules', true);
309 }
310
311 /**
312 * Empty .htaccess file
313 *
314 * @return void
315 */
316 private function empty_htaccess_file(): void {
317 // Setting default to true, so on initial run (when option is not yet present in the DB) we don't break the function here
318 if (!WP_Optimize()->get_options()->get_option('htaccess_has_webp_rules', true)) return;
319 $this->setup_htaccess_file();
320 $htaccess_comment_sections = array(
321 'WP-Optimize WebP Rules',
322 'Register webp mime type',
323 );
324 foreach ($htaccess_comment_sections as $htaccess_comment_section) {
325 if (!$this->_htaccess->is_commented_section_exists($htaccess_comment_section)) continue;
326 $this->_htaccess->remove_commented_section($htaccess_comment_section);
327 $this->_htaccess->write_file();
328 }
329 WP_Optimize()->get_options()->update_option('htaccess_has_webp_rules', false);
330 }
331
332 /**
333 * Prepare array of htaccess rules to use webp images.
334 *
335 * @return array<int, array<array<array<string>|string>|string>>
336 */
337 private function prepare_webp_htaccess_rules() {
338 return array(
339 array(
340 '<IfModule mod_rewrite.c>',
341 'RewriteEngine On',
342 '',
343 '# Redirect to existing converted image in same dir (if browser supports webp)',
344 'RewriteCond %{HTTP_ACCEPT} image/webp',
345 'RewriteCond %{REQUEST_FILENAME} (?i)(.*)(\.jpe?g|\.png)$',
346 'RewriteCond %1%2\.webp -f',
347 'RewriteRule (?i)(.*)(\.jpe?g|\.png)$ %1%2\.webp [T=image/webp,E=EXISTING:1,E=ADDVARY:1,L]',
348 '',
349 '# Make sure that browsers which does not support webp also gets the Vary:Accept header',
350 '# when requesting images that would be redirected to webp on browsers that does.',
351 array(
352 '<IfModule mod_headers.c>',
353 array(
354 '<FilesMatch "(?i)\.(jpe?g|png)$">',
355 'Header append "Vary" "Accept"',
356 '</FilesMatch>',
357 ),
358 '</IfModule>',
359 ),
360 '',
361 '</IfModule>',
362 '',
363 ),
364 array(
365 '# Rules for handling requests for webp images',
366 '# ---------------------------------------------',
367 '',
368 '# Set Vary:Accept header if we came here by way of our redirect, which set the ADDVARY environment variable',
369 '# The purpose is to make proxies and CDNs aware that the response varies with the Accept header',
370 '<IfModule mod_headers.c>',
371 array(
372 '<IfModule mod_setenvif.c>',
373 '# Apache appends "REDIRECT_" in front of the environment variables defined in mod_rewrite, but LiteSpeed does not',
374 '# So, the next lines are for Apache, in order to set environment variables without "REDIRECT_"',
375 'SetEnvIf REDIRECT_EXISTING 1 EXISTING=1',
376 'SetEnvIf REDIRECT_ADDVARY 1 ADDVARY=1',
377 '',
378 'Header append "Vary" "Accept" env=ADDVARY',
379 '',
380 '# Set X-WPO-WebP header for diagnose purposes',
381 'Header set "X-WPO-WebP" "Redirected directly to existing webp" env=EXISTING',
382 '</IfModule>',
383 ),
384 '</IfModule>',
385 ),
386 );
387 }
388
389 /**
390 * Add webp mime type to htaccess rules.
391 *
392 * @return void
393 */
394 private function add_webp_mime_type(): void {
395 $htaccess_comment_section = 'Register webp mime type';
396 if ($this->_htaccess->is_exists() && !$this->_htaccess->is_commented_section_exists($htaccess_comment_section)) {
397 $webp_mime_type = array(
398 array(
399 '<IfModule mod_mime.c>',
400 'AddType image/webp .webp',
401 '</IfModule>',
402 ),
403 );
404 $this->_htaccess->update_commented_section($webp_mime_type, $htaccess_comment_section);
405 $this->_htaccess->write_file();
406 }
407 }
408
409 /**
410 * Returns the path to the WebP test image file.
411 *
412 * @return string
413 */
414 private function get_webp_test_image_path(): string {
415 $upload_dir = wp_upload_dir();
416 return $upload_dir['basedir'] . '/wpo/images/wpo_logo_small.png.webp';
417 }
418
419 /**
420 * Checks whether a webp conversion test is successful or not
421 *
422 * @return bool
423 */
424 private function is_webp_conversion_successful(): bool {
425 return file_exists($this->get_webp_test_image_path());
426 }
427
428 /**
429 * Checks whether a sample webp conversion test should be run or not
430 *
431 * @return bool Returns true if the sample test should be run, false otherwise
432 */
433 private function should_run_webp_conversion_test(): bool {
434 return !$this->get_webp_conversion_test_result();
435 }
436
437 /**
438 * Returns webp conversion test result
439 *
440 * @return bool Returns the value of the webp_conversion_test saved in the options table
441 */
442 public function get_webp_conversion_test_result(): bool {
443 return (bool) WP_Optimize()->get_options()->get_option('webp_conversion_test');
444 }
445
446 /**
447 * Checks whether the webp redirection is possible or not and sets flag
448 *
449 * @return bool Returns true if webp is served successfully, false otherwise
450 */
451 private function run_webp_serving_self_test(): bool {
452 $self_test = WPO_WebP_Self_Test::get_instance();
453
454 if ($self_test->is_webp_served()) {
455 WP_Optimize()->get_options()->update_option('redirection_possible', 'true');
456 return true;
457 }
458 WP_Optimize()->get_options()->update_option('redirection_possible', 'false');
459 $this->empty_htaccess_file();
460 return false;
461 }
462
463 /**
464 * Resets webp serving method by running self test, if needed purges cache and empties `uploads/.htaccess` file
465 *
466 * @return void
467 */
468 public function reset_webp_serving_method(): void {
469 if ($this->_should_use_webp) {
470 $this->reset_webp_options();
471 $this->run_self_test();
472 list($old_redirection_possible, $new_redirection_possible) = $this->get_old_and_new_redirection_possibility();
473 $this->maybe_purge_cache($old_redirection_possible, $new_redirection_possible);
474 $this->maybe_empty_htaccess_file($new_redirection_possible);
475 }
476 }
477
478 /**
479 * Resets WebP related options
480 *
481 * @return void
482 */
483 private function reset_webp_options(): void {
484 $options = WP_Optimize()->get_options();
485 $options->update_option('old_redirection_possible', $options->get_option('redirection_possible'));
486 $options->update_option('webp_conversion_test', false);
487 $options->update_option('webp_converters', false);
488 $options->update_option('redirection_possible', false);
489 $this->remove_webp_test_image_file();
490 }
491
492 /**
493 * Running self test to find available converters and possibility of serving webp using redirection method
494 *
495 * @return void
496 */
497 private function run_self_test(): void {
498 $this->set_converter_status();
499 if ($this->get_webp_conversion_test_result()) {
500 $this->save_htaccess_rules();
501 $this->run_webp_serving_self_test();
502 } else {
503 $this->disable_webp_conversion();
504 $this->log("No working WebP converter was found on the server when running self-test, disabling WebP conversion");
505 }
506 }
507
508 /**
509 * Gets old and new redirection possibility values
510 *
511 * @return array<string>
512 */
513 private function get_old_and_new_redirection_possibility() {
514 $options = WP_Optimize()->get_options();
515 return array(
516 $options->get_option('old_redirection_possible'),
517 $options->get_option('redirection_possible'),
518 );
519 }
520
521 /**
522 * Cache is cleared when there is a change in the potential for serving WebP using redirection.
523 *
524 * @param string $old_redirection_possible Previous redirection possibility value
525 * @param string $new_redirection_possible Current redirection possibility value
526 * @return void
527 */
528 private function maybe_purge_cache($old_redirection_possible, $new_redirection_possible): void {
529 if ($old_redirection_possible === $new_redirection_possible) {
530 return;
531 }
532
533 $is_cache_purged = WP_Optimize()->get_page_cache()->purge();
534 $log_old_value = empty($old_redirection_possible) ? 'null' : $old_redirection_possible;
535 $log_new_value = empty($new_redirection_possible) ? 'null' : $new_redirection_possible;
536 $this->log("Purging cache because redirection_possible value changed from: {$log_old_value} to {$log_new_value}");
537
538 if ($is_cache_purged) {
539 WP_Optimize()->get_page_cache()->file_log('Full Cache Purge due to change in the value of WebP redirection');
540 }
541 }
542
543 /**
544 * Remove redirection rules from `uploads/.htaccess` file if redirection is not possible
545 *
546 * @param string $new_redirection_possible
547 * @return void
548 */
549 private function maybe_empty_htaccess_file($new_redirection_possible): void {
550 if ('false' === $new_redirection_possible) {
551 $this->empty_htaccess_file();
552 }
553 }
554
555 /**
556 * Initialize cron scheduler
557 *
558 * @return void
559 */
560 public function init_webp_cron_scheduler(): void {
561 if (!wp_next_scheduled('wpo_reset_webp_conversion_test_result')) {
562 wp_schedule_event(time(), 'wpo_daily', 'wpo_reset_webp_conversion_test_result');
563 }
564 if (!wp_next_scheduled('wpo_prune_webp_logs')) {
565 wp_schedule_event(time(), 'weekly', 'wpo_prune_webp_logs');
566 }
567 if (!wp_next_scheduled('wpo_webp_convert_compressed_images')) {
568 wp_schedule_event(strtotime('midnight'), 'daily', 'wpo_webp_convert_compressed_images');
569 }
570 }
571
572 /**
573 * Remove all cron schedules
574 *
575 * @return void
576 */
577 public function remove_webp_cron_schedules(): void {
578 wp_clear_scheduled_hook('wpo_reset_webp_conversion_test_result');
579 wp_clear_scheduled_hook('wpo_prune_webp_logs');
580 wp_clear_scheduled_hook('wpo_webp_convert_compressed_images');
581 }
582
583 /**
584 * Return the true if webp conversion is enabled and vice versa
585 *
586 * @return bool
587 */
588 public function is_webp_conversion_enabled(): bool {
589 return $this->_should_use_webp;
590 }
591
592 /**
593 * Set the webp_conversion option value to false and remove webp cron schedules
594 *
595 * @return void
596 */
597 public function disable_webp_conversion(): void {
598 $this->empty_htaccess_file();
599 WP_Optimize()->get_options()->update_option("webp_conversion", false);
600 $this->remove_webp_cron_schedules();
601 $message = __('Disabling the webp conversion.', 'wp-optimize');
602 $this->log($message);
603 }
604
605 /**
606 * Remove webp converted test image file
607 *
608 * @return void
609 */
610 private function remove_webp_test_image_file(): void {
611 $destination = $this->get_webp_test_image_path();
612 wp_delete_file($destination);
613 }
614
615 /**
616 * Run during plugin deactivation
617 *
618 * @return void
619 */
620 public function plugin_deactivate(): void {
621 $this->empty_htaccess_file();
622 $this->remove_webp_test_image_file();
623 }
624
625 /**
626 * Determines whether one of the PHP shell functions required for WebP conversion is available or not.
627 *
628 * @return bool
629 */
630 public function shell_functions_available(): bool {
631 $has_escapeshellarg = function_exists('escapeshellarg');
632 $has_exec_or_passthru = $this->any_function_exists(array('exec', 'passthru'));
633 $has_proc_functions = $this->all_functions_exist(array('proc_open', 'proc_close'));
634 $has_popen_functions = $this->all_functions_exist(array('popen', 'pclose'));
635
636 return $has_escapeshellarg && ($has_exec_or_passthru || $has_proc_functions || $has_popen_functions);
637 }
638
639 /**
640 * Checks if only shell converters available for WebP conversion.
641 *
642 * @return boolean
643 */
644 private function is_only_shell_converters_available(): bool {
645 $available_converters = WP_Optimize()->get_options()->get_option('webp_converters');
646 /** @var array<string> $available_converters */
647 $available_converters = is_array($available_converters) ? $available_converters : array();
648 $converters_with_shell = WPO_WebP_Test_Run::get_converters_with_shell();
649 $available_with_shell = array_intersect($available_converters, $converters_with_shell);
650
651 return count($available_converters) > 0 && count($available_converters) === count($available_with_shell);
652 }
653
654 /**
655 * Determines whether one of the PHP shell functions required for WebP conversion is available or not.
656 *
657 * @deprecated 3.6.0
658 * @return bool
659 */
660 public static function is_shell_functions_available(): bool {
661 _deprecated_function(__METHOD__, '3.6.0', 'WP_Optimize_WebP::shell_functions_available');
662 return WP_Optimize_WebP::get_instance()->shell_functions_available();
663 }
664
665 /**
666 * Check if all the functions from the list is available.
667 *
668 * @param array<string> $functions
669 * @return bool
670 */
671 private function all_functions_exist($functions): bool {
672 foreach ($functions as $function) {
673 if (!function_exists($function)) return false;
674 }
675 return true;
676 }
677
678 /**
679 * Check if one of the functions from the list is available.
680 *
681 * @param array<string> $functions
682 * @return bool
683 */
684 private function any_function_exists($functions): bool {
685 foreach ($functions as $function) {
686 if (function_exists($function)) return true;
687 }
688 return false;
689 }
690
691 /**
692 * Return true if webp conversion is enabled and vice versa.
693 *
694 * @return bool
695 */
696 public function is_webp_enabled(): bool {
697 _deprecated_function(__METHOD__, '4.5.4', 'WP_Optimize_WebP::is_webp_conversion_enabled');
698 return $this->is_webp_conversion_enabled();
699 }
700
701 /**
702 * Actions to be performed upon plugin activation
703 *
704 * @return void
705 */
706 public function plugin_activate(): void {
707 if ($this->is_webp_conversion_enabled()) {
708 $this->init_webp_cron_scheduler();
709 $this->reset_webp_serving_method();
710 }
711 }
712 }
713
714 endif;
715