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 / 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.5.2, at includes/class-wp-optimize-preloader.php

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