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-preloader.php

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

665 lines 19.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('No direct access allowed');
4
5 if (!class_exists('Updraft_Task_Manager_1_4')) require_once(WPO_PLUGIN_MAIN_PATH . 'vendor/team-updraft/common-libs/src/updraft-tasks/class-updraft-task-manager.php');
6
7 abstract class WP_Optimize_Preloader extends Updraft_Task_Manager_1_4 {
8
9 protected $options;
10
11 /**
12 * If set to true, the preloader will not close the browser connection when running via AJAX.
13 *
14 * @var bool
15 */
16 protected $dont_close_browser_connection = false;
17
18 /**
19 * WP_Optimize_Page_Cache_Preloader constructor.
20 */
21 public function __construct() {
22 parent::__construct();
23
24 $this->options = WP_Optimize()->get_options();
25 // setup loggers
26 if (did_action('init')) {
27 $this->setup_loggers();
28 } else {
29 add_action('init', array($this, 'setup_loggers'));
30 }
31
32 add_action('wpo_' . $this->preload_type . '_preload_continue', array($this, 'process_tasks_queue'));
33 add_filter('updraft_interrupt_tasks_queue_'.$this->task_type, array($this, 'maybe_interrupt_queue'), 20);
34 }
35
36 /**
37 * Setup loggers
38 *
39 * @return void
40 */
41 public function setup_loggers() {
42 $this->set_loggers(WP_Optimize()->wpo_loggers());
43 }
44
45 /**
46 * Get a schedule interval
47 *
48 * @param string $schedule_key The schedule to check
49 * @return integer
50 */
51 protected function get_schedule_interval($schedule_key) {
52 $schedules = wp_get_schedules();
53 if (!isset($schedules[$schedule_key])) {
54 $this->log('Could not get interval for event of type '.$schedule_key);
55 return 0;
56 }
57 return (int) $schedules[$schedule_key]['interval'] ?? 0;
58 }
59
60 /**
61 * Get the interval to continuing a preload task
62 *
63 * @return integer
64 */
65 public function get_continue_preload_cron_interval() {
66 /**
67 * Filters the interval between each preload attempt, in seconds.
68 */
69 $filtered_interval = apply_filters('wpo_' . $this->preload_type . '_preload_continue_interval', 600);
70 return is_int($filtered_interval) ? $filtered_interval : 600;
71 }
72
73 /**
74 * Schedule action for continuously preload.
75 */
76 public function schedule_preload_continue_action() {
77 $continue_in = wp_next_scheduled('wpo_' . $this->preload_type .'_preload_continue');
78
79 // Action is still scheduled
80 if ($continue_in && $continue_in > 0) return;
81 // Action is overdue, delete it and re-schedule it
82 if ($continue_in && $continue_in < 0) $this->delete_preload_continue_action();
83
84 wp_schedule_event(time() + $this->get_schedule_interval('wpo_' . $this->preload_type . '_preload_continue_interval'), 'wpo_' . $this->preload_type . '_preload_continue_interval', 'wpo_' . $this->preload_type . '_preload_continue');
85 }
86
87 /**
88 * Delete scheduled action for continuously preload.
89 */
90 public function delete_preload_continue_action() {
91 wp_clear_scheduled_hook('wpo_' . $this->preload_type . '_preload_continue');
92 }
93
94 /**
95 * Run preload. If task queue is empty it creates tasks for site urls.
96 *
97 * @param string $type - The preload type (schedule | manual)
98 * @param ?array $response - Specific response for echo into output thread when browser connection closing.
99 * @param bool $silent - If DOING_AJAX, close the connection without sending any additional data (default: false)
100 * @return array|void - Void when closing the browser connection
101 */
102 public function run($type = 'scheduled', $response = null, $silent = false) {
103 if (!$this->is_option_active()) {
104 return $this->get_option_disabled_error();
105 }
106
107 do_action('wpo_before_' . $this->preload_type . '_preload');
108
109 if (empty($response)) {
110 $response = array('success' => true);
111 }
112
113 $this->delete_cancel_flag();
114
115 // trying to lock semaphore.
116
117 $creating_tasks_semaphore = new Updraft_Semaphore_3_0('wpo_' . $this->preload_type . '_preloader_creating_tasks');
118 $lock = $creating_tasks_semaphore->lock();
119
120 // if semaphore haven't locked then just return response.
121 if (!$lock) {
122 return $this->get_preload_already_running_error();
123 }
124
125 $is_wp_cli = defined('WP_CLI') && WP_CLI;
126
127 // close browser connection and continue work for ajax actions.
128 if (defined('DOING_AJAX') && DOING_AJAX && !$this->dont_close_browser_connection) {
129 if (true === $silent) {
130 $output = '';
131 } else {
132 $output = wp_json_encode($response);
133 }
134
135 WP_Optimize()->close_browser_connection($output);
136 }
137
138 // Reset the flag to not close the browser connection for the next runs.
139 $this->dont_close_browser_connection = false;
140
141 // trying to change time limit.
142 WP_Optimize()->change_time_limit();
143
144 $status = $this->get_status($this->task_type);
145
146 if (0 === (int) $status['all_tasks'] && $lock) {
147 if (is_multisite()) {
148 $sites = WP_Optimize()->get_sites();
149
150 foreach ($sites as $site) {
151 switch_to_blog($site->blog_id);
152 $this->create_tasks_for_preload_site_urls($type);
153 restore_current_blog();
154 }
155 } else {
156 $this->create_tasks_for_preload_site_urls($type);
157 }
158 }
159
160 if ($lock) $creating_tasks_semaphore->release();
161
162 $this->process_tasks_queue();
163
164 // return $response in WP-CLI mode
165 if ($is_wp_cli) {
166 return $response;
167 }
168 }
169
170 /**
171 * Set the flag to not close the browser connection when running via AJAX.
172 *
173 * @return void
174 */
175 public function maybe_dont_close_browser_connection() {
176
177 if ($this->dont_close_browser_connection) return;
178
179 $dont_close_browser_connection = apply_filters('wpo_' . $this->preload_type . '_preloader_dont_close_browser_connection', $this->dont_close_browser_connection);
180
181 if ($dont_close_browser_connection) {
182 $this->dont_close_browser_connection = true;
183 }
184 }
185
186 /**
187 * Process tasks queue.
188 */
189 public function process_tasks_queue() {
190 // schedule continue preload action.
191 $this->schedule_preload_continue_action();
192
193 if (!$this->process_queue($this->task_type)) {
194 return;
195 }
196
197 // delete scheduled continue preload action.
198 $this->delete_preload_continue_action();
199
200 // update last preload time only if processing any tasks, else process was cancelled.
201 if ($this->is_running()) {
202 $this->options->update_option('wpo_last_' . $this->preload_type . '_preload', time());
203 }
204
205 $this->clean_up_old_tasks($this->task_type);
206 }
207
208 /**
209 * Find out if the current queue should be interrupted
210 *
211 * @param boolean $interrupt
212 * @return boolean
213 */
214 public function maybe_interrupt_queue($interrupt) {
215
216 if ($interrupt) return $interrupt;
217
218 static $memory_threshold = null;
219 if (null === $memory_threshold) {
220 /**
221 * Filters the minimum memory required before stopping a queue. Default: 10MB
222 */
223 $memory_threshold = apply_filters('wpo_' . $this->preload_type . '_preload_memory_threshold', 10485760);
224 }
225
226 return WP_Optimize()->get_free_memory() < $memory_threshold;
227 }
228
229 /**
230 * Delete all preload tasks from queue.
231 */
232 public function cancel_preload() {
233 $this->set_cancel_flag();
234 $this->delete_tasks($this->task_type);
235 $this->delete_preload_continue_action();
236 }
237
238 /**
239 * Set 'cancel' option to true.
240 */
241 public function set_cancel_flag() {
242 $this->options->update_option("last_{$this->preload_type}_preload_cancel", true);
243 }
244
245 /**
246 * Delete 'cancel' option.
247 */
248 public function delete_cancel_flag() {
249 $this->options->delete_option('last_' . $this->preload_type . '_preload_cancel');
250 }
251
252 /**
253 * Check if the last preload is cancelled.
254 *
255 * @return bool
256 */
257 public function is_cancelled() {
258 return (bool) $this->options->get_option("last_{$this->preload_type}_preload_cancel", false);
259 }
260
261 /**
262 * Check if preloading queue is processing.
263 *
264 * @return bool
265 */
266 public function is_busy() {
267 return $this->is_semaphore_locked($this->task_type) || $this->is_semaphore_locked('wpo_' . $this->preload_type . '_preloader_creating_tasks');
268 }
269
270 /**
271 * Get current status of preloading urls.
272 *
273 * @return mixed
274 */
275 public function get_status_info() {
276
277 $status = $this->get_status($this->task_type);
278 $preload_data = $this->get_preload_data();
279
280 if ($this->is_semaphore_locked('wpo_' . $this->preload_type . '_preloader_creating_tasks') && !$this->is_cancelled()) {
281 // we are still creating tasks.
282 return $this->get_preloading_message($preload_data);
283 } elseif ((int) $status['complete_tasks'] === (int) $status['all_tasks']) {
284 $gmt_offset = (int) (3600 * get_option('gmt_offset'));
285
286 $last_preload_time = $this->options->get_option('wpo_last_' . $this->preload_type . '_preload');
287
288 if ($last_preload_time) {
289 $last_preload_time_str = date_i18n(get_option('time_format').', '.get_option('date_format'), $last_preload_time + $gmt_offset);
290 return $this->get_last_preload_message($preload_data, $last_preload_time_str);
291 } else {
292 return $this->get_preload_success_message($preload_data);
293 }
294 } else {
295 $preload_resuming_time = wp_next_scheduled('wpo_' . $this->preload_type . '_preload_continue');
296 $preload_resuming_in = $preload_resuming_time ? $preload_resuming_time - time() : 0;
297 // translators: %1$s: number of preloaded urls, %2$s: total number of urls
298 $preloaded_message = sprintf(_n('%1$s out of %2$s URL preloaded', '%1$s out of %2$s URLs preloaded', $status['all_tasks'], 'wp-optimize'), $status['complete_tasks'], $status['all_tasks']);
299 if ('sitemap' === $this->options->get_option('wpo_last_' . $this->preload_type . '_preload_type', '')) {
300 $preloaded_message = __('Preloading posts found in sitemap:', 'wp-optimize') .' '. $preloaded_message;
301 }
302 $return = $this->get_preload_progress_message($preload_data, $preloaded_message, $preload_resuming_in);
303 if (defined('DOING_AJAX') && DOING_AJAX) {
304 // if no cron was found or cron is overdue more than 20s, trigger it
305 if (!$preload_resuming_time || $preload_resuming_in < -20) {
306 $this->run('scheduled', $return);
307 }
308 }
309 return $return;
310 }
311 }
312
313 /**
314 * Check if preload action in process.
315 *
316 * @return bool
317 */
318 public function is_running() {
319 $status = $this->get_status($this->task_type);
320
321 return $status['all_tasks'] > 0;
322 }
323
324 /**
325 * Preload desktop version from url.
326 *
327 * @param string $url
328 *
329 * @return void
330 */
331 public function preload_desktop($url) {
332 $desktop_args = array(
333 'httpversion' => '1.1',
334 'user-agent' => WP_Optimize_Utils::get_user_agent(),
335 'timeout' => 10,
336 'headers' => apply_filters('wpo_preload_headers', array()),
337 );
338
339 $desktop_args = apply_filters('wpo_' . $this->preload_type . '_preloader_desktop_args', $desktop_args, $url);
340
341 $this->log('preload_desktop - '. $url);
342
343 wp_remote_get($url, $desktop_args);
344 }
345
346 /**
347 * Preload amp version from $url.
348 *
349 * @param string $url
350 *
351 * @return void
352 */
353 public function preload_amp($url) {
354 if (!apply_filters('wpo_should_preload_amp', false, $url)) return;
355
356 $amp_args = array(
357 'httpversion' => '1.1',
358 'user-agent' => WP_Optimize_Utils::get_user_agent(),
359 'timeout' => 10,
360 'headers' => apply_filters('wpo_preload_headers', array()),
361 );
362
363 $url = untrailingslashit($url) . '/amp/';
364
365 $amp_args = apply_filters('wpo_' . $this->preload_type . '_preloader_amp_args', $amp_args, $url);
366
367 $this->log('preload_amp - ' . $url);
368
369 wp_remote_get($url, $amp_args);
370 }
371
372 /**
373 * Get sitemap filename.
374 *
375 * @return string
376 */
377 protected function get_sitemap_filename() {
378 $sitemap_filename = 'sitemap.xml';
379 /**
380 * Filter the sitemap file used to collect the URLs to preload
381 *
382 * @param string $filename - The sitemap name
383 * @default sitemap.xml
384 */
385 $filtered_sitemap_filename = apply_filters('wpo_cache_preload_sitemap_filename', $sitemap_filename);
386 return is_string($filtered_sitemap_filename) ? $filtered_sitemap_filename : $sitemap_filename;
387 }
388
389 /**
390 * Check if sitemap exists then returns list of urls from sitemap file otherwise returns all posts urls.
391 *
392 * @return array
393 */
394 public function get_site_urls() {
395
396 $urls = $this->get_sitemap_urls();
397
398 if (!empty($urls)) {
399 $this->options->update_option('wpo_last_' . $this->preload_type . '_preload_type', 'sitemap');
400 } else {
401 $urls = $this->get_post_urls();
402 $this->options->update_option('wpo_last_' . $this->preload_type . '_preload_type', 'posts');
403 }
404
405 // translators: %d: number of urls
406 $this->log(sprintf(_n('%d url found.', '%d urls found.', count($urls), 'wp-optimize'), count($urls)));
407
408 /**
409 * Filter the URLs which will be preloaded
410 *
411 * @param array $urls
412 * @return array
413 */
414 $filtered_url = apply_filters('wpo_preload_get_site_urls', $urls);
415 return is_array($filtered_url) ? $filtered_url : array();
416 }
417
418 /**
419 * Loads sitemap file and returns list of urls.
420 *
421 * @param string $sitemap_url
422 *
423 * @return mixed
424 */
425 public function get_sitemap_urls($sitemap_url = '') {
426
427 $urls = array();
428
429 // if sitemap url is empty then use main sitemap file name.
430 $sitemap_url = ('' === $sitemap_url) ? site_url('/'.$this->get_sitemap_filename()) : $sitemap_url;
431
432 // if simplexml_load_string not available then we don't load sitemap.
433 if (!function_exists('simplexml_load_string')) {
434 return $urls;
435 }
436
437 // load sitemap file.
438 $response = wp_remote_get($sitemap_url, array('timeout' => 30));
439
440 // if we get error then
441 if (is_wp_error($response)) {
442 $response = file_get_contents($sitemap_url);
443
444 // if response is empty then try load from file.
445 if (empty($response) && '' === $sitemap_url) {
446 $sitemap_file = $this->get_local_sitemap_file();
447
448 $response = file_get_contents($sitemap_file);
449 }
450
451 if (empty($response)) return $urls;
452
453 $xml = @simplexml_load_string($response); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress warnings when error found in XML data
454 } else {
455 // parse xml answer.
456 $xml = @simplexml_load_string(wp_remote_retrieve_body($response)); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress warnings when error found in XML data
457 }
458
459 // xml file has not valid xml content then return false.
460 if (false === $xml) return array();
461
462 // if exists urls then return them.
463 if (isset($xml->url)) {
464 foreach ($xml->url as $element) {
465 if (!isset($element->loc)) continue;
466 $urls[] = (string) $element->loc;
467 }
468 } elseif (isset($xml->sitemap)) {
469 // if it has links to other sitemap files then get urls from them.
470 foreach ($xml->sitemap as $element) {
471 if (!isset($element->loc)) continue;
472
473 $sitemap_urls = $this->get_sitemap_urls($element->loc);
474 $urls = array_merge($urls, $sitemap_urls);
475 }
476 }
477
478 return $urls;
479 }
480
481 /**
482 * Get the path to a local sitemap file
483 *
484 * @return string
485 */
486 protected function get_local_sitemap_file() {
487 global $wp_file_descriptions; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Make the scope of $wp_file_descriptions global, so that when wp-admin/includes/file.php assigns to it, it is adjusting the global variable as intended
488
489 if (!function_exists('get_home_path')) {
490 include_once ABSPATH . '/wp-admin/includes/file.php';
491 }
492 return trailingslashit(get_home_path()) . $this->get_sitemap_filename();
493 }
494
495 /**
496 * Get all posts of any post type and returns urls for them.
497 *
498 * @return array
499 */
500 public function get_post_urls() {
501 global $post;
502
503 $offset = 0;
504 $posts_per_page = 1000;
505 $urls = array();
506
507 $urls[] = site_url('/');
508
509 do {
510 $query = new WP_Query(array(
511 'post_type' => 'any',
512 'post_status' => 'publish',
513 'posts_per_page' => $posts_per_page,
514 'offset' => $offset,
515 'orderby' => 'ID',
516 'order' => 'ASC',
517 'cache_results' => false, // disable cache to avoid memory error.
518 ));
519
520 $posts_loaded = $query->post_count;
521
522 while ($query->have_posts()) {
523 $query->the_post();
524 $permalink = get_permalink();
525 $urls[] = $permalink;
526
527 // check page separators in the post content
528 preg_match_all('/\<\!--nextpage--\>/', $post->post_content, $matches);
529 // if there are any separators add urls for each page
530 if (count($matches[0])) {
531 $prefix = strpos($permalink, '?') ? '&page=' : '';
532 for ($page = 0; $page < count($matches[0]); $page++) {
533 if ('' != $prefix) {
534 $urls[] = $permalink . $prefix . ($page+2);
535 } else {
536 $urls[] = trailingslashit($permalink) . ($page+2);
537 }
538 }
539 }
540 }
541
542 $offset += $posts_loaded;
543 } while ($posts_loaded > 0);
544
545 /**
546 * If domain mapping enabled then replace domains in urls.
547 */
548 if ($this->is_domain_mapping_enabled()) {
549 $blog_id = get_current_blog_id();
550
551 $mapped_domain = $this->get_mapped_domain($blog_id);
552 $blog_details = get_blog_details($blog_id);
553
554 if ($mapped_domain) {
555 foreach ($urls as $i => $url) {
556 $urls[$i] = preg_replace('/'.$blog_details->domain.'/i', $mapped_domain, $url, 1);
557 }
558 }
559 }
560
561 wp_reset_postdata();
562
563 return $urls;
564 }
565
566 /**
567 * Check if domain mapping enabled.
568 *
569 * @return bool
570 */
571 public function is_domain_mapping_enabled() {
572 // SUNRISE constant is defined with installation WordPress MU Domain Mapping plugin.
573 $enabled = is_multisite() && defined('SUNRISE') && 'on' === strtolower(SUNRISE);
574
575 /**
576 * Filters if Multisite Domain mapping is enabled.
577 * Currently, we can only detect if the WordPress MU Domain Mapping plugin is in use.
578 * Using the WP Core functionality should not require this, unless if the domain name is set somewhere else but in the site url option.
579 */
580 return (bool) apply_filters('wpo_is_domain_mapping_enabled', $enabled);
581 }
582
583 /**
584 * Return mapped domain by $blog_id.
585 *
586 * @param int $blog_id
587 *
588 * @return string
589 */
590 public function get_mapped_domain($blog_id) {
591 global $wpdb;
592
593 $domain = '';
594 $multisite_plugin_table_name = $wpdb->base_prefix.'domain_mapping';
595 // Check if table exists
596 if ($wpdb->get_var("SHOW TABLES LIKE '" . esc_sql($multisite_plugin_table_name) . "'") != $multisite_plugin_table_name) {
597 // This table created in WordPress MU Domain Mapping plugin.
598 $row = $wpdb->get_row($wpdb->prepare("SELECT `domain` FROM " . esc_sql($multisite_plugin_table_name) . " WHERE `blog_id` = %d AND `active` = %d", $blog_id, 1), ARRAY_A);
599 if (!empty($row)) {
600 $domain = $row['domain'];
601 }
602 } else {
603 // When using the WP Core method, the site url option contains the mapped domain.
604 $domain = get_site_url($blog_id);
605 }
606
607 /**
608 * Filters the mapped domain name
609 *
610 * @param string $domain The domain name
611 * @param integer $blog_id The blog ID
612 */
613 $filtered_domain = apply_filters('wpo_get_mapped_domain', $domain, $blog_id);
614 return is_string($filtered_domain) ? $filtered_domain : $domain;
615 }
616
617 /**
618 * Captures and logs any interesting messages
619 *
620 * @param string $message - the error message
621 * @param string $error_type - the error type
622 */
623 public function log($message, $error_type = 'info') {
624
625 if (isset($this->loggers)) {
626 foreach ($this->loggers as $logger) {
627 $logger->log($message, $error_type);
628 }
629 }
630 }
631
632 /**
633 * Check if semaphore is locked.
634 *
635 * @param string $semaphore
636 * @return bool
637 */
638 protected function is_semaphore_locked($semaphore) {
639 $semaphore = new Updraft_Semaphore_3_0($semaphore);
640 if ($semaphore->lock()) {
641 $semaphore->release();
642 return false;
643 }
644 return true;
645 }
646
647 abstract protected function is_option_active();
648
649 abstract protected function get_option_disabled_error();
650
651 abstract protected function get_preload_already_running_error();
652
653 abstract protected function create_tasks_for_preload_site_urls($type);
654
655 abstract protected function get_preload_data();
656
657 abstract protected function get_preloading_message($data);
658
659 abstract protected function get_last_preload_message($data, $last_preload_time_str);
660
661 abstract protected function get_preload_success_message($data);
662
663 abstract protected function get_preload_progress_message($data, $preloaded_message, $preload_resuming_in);
664 }
665