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

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