PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.0
Elementor Website Builder – more than just a page builder v4.2.0
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / core / base / background-process / wp-background-process.php

wp-background-process.php in Elementor Website Builder – more than just a page builder 4.2.0, at core/base/background-process/wp-background-process.php

559 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Base\BackgroundProcess;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Link https://github.com/A5hleyRich/wp-background-processing GPL v2.0.
10 *
11 * WP Background Process
12 *
13 * @package WP-Background-Processing
14 */
15
16 /**
17 * Abstract WP_Background_Process class.
18 *
19 * @abstract
20 * @extends WP_Async_Request
21 */
22 abstract class WP_Background_Process extends WP_Async_Request {
23
24 /**
25 * Action
26 *
27 * (default value: 'background_process')
28 *
29 * @var string
30 * @access protected
31 */
32 protected $action = 'background_process';
33
34 /**
35 * Start time of current process.
36 *
37 * (default value: 0)
38 *
39 * @var int
40 * @access protected
41 */
42 protected $start_time = 0;
43
44 /**
45 * Cron_hook_identifier
46 *
47 * @var mixed
48 * @access protected
49 */
50 protected $cron_hook_identifier;
51
52 /**
53 * Cron_interval_identifier
54 *
55 * @var mixed
56 * @access protected
57 */
58 protected $cron_interval_identifier;
59
60 /**
61 * Initiate new background process
62 */
63 public function __construct() {
64 parent::__construct();
65
66 $this->cron_hook_identifier = $this->identifier . '_cron';
67 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
68
69 add_action( $this->cron_hook_identifier, [ $this, 'handle_cron_healthcheck' ] );
70 add_filter( 'cron_schedules', [ $this, 'schedule_cron_healthcheck' ] );
71 }
72
73 /**
74 * Dispatch
75 *
76 * @access public
77 * @return array|\WP_Error
78 */
79 public function dispatch() {
80 // Schedule the cron healthcheck.
81 $this->schedule_event();
82
83 // On admin page requests (not AJAX/cron), also process on shutdown as fallback.
84 // This ensures background tasks run even if HTTP loopback requests are blocked.
85 if ( is_admin() && ! wp_doing_ajax() && ! wp_doing_cron() ) {
86 add_action( 'shutdown', [ $this, 'maybe_handle_on_shutdown' ], 0 );
87 }
88
89 // Perform remote post.
90 return parent::dispatch();
91 }
92
93 /**
94 * Maybe handle on shutdown
95 *
96 * Fallback handler for when HTTP loopback requests are blocked.
97 * Flushes output to browser first, then processes the queue directly.
98 *
99 * @access public
100 */
101 public function maybe_handle_on_shutdown() {
102 // Don't run if already processed via loopback or if queue is empty.
103 if ( $this->is_process_running() ) {
104 return;
105 }
106
107 if ( $this->is_queue_empty() ) {
108 return;
109 }
110
111 // Flush output to browser so page loads immediately.
112 if ( ob_get_level() ) {
113 wp_ob_end_flush_all();
114 }
115
116 // Finish the request - browser gets response, PHP continues.
117 if ( function_exists( 'fastcgi_finish_request' ) ) {
118 fastcgi_finish_request();
119 } elseif ( function_exists( 'litespeed_finish_request' ) ) {
120 litespeed_finish_request();
121 }
122
123 // Process the queue directly.
124 $this->handle();
125 }
126
127 /**
128 * Push to queue
129 *
130 * @param mixed $data Data.
131 *
132 * @return $this
133 */
134 public function push_to_queue( $data ) {
135 $this->data[] = $data;
136
137 return $this;
138 }
139
140 /**
141 * Save queue
142 *
143 * @return $this
144 */
145 public function save() {
146 $key = $this->generate_key();
147
148 if ( ! empty( $this->data ) ) {
149 update_site_option( $key, $this->data );
150 }
151
152 return $this;
153 }
154
155 /**
156 * Update queue
157 *
158 * @param string $key Key.
159 * @param array $data Data.
160 *
161 * @return $this
162 */
163 public function update( $key, $data ) {
164 if ( ! empty( $data ) ) {
165 update_site_option( $key, $data );
166 }
167
168 return $this;
169 }
170
171 /**
172 * Delete queue
173 *
174 * @param string $key Key.
175 *
176 * @return $this
177 */
178 public function delete( $key ) {
179 delete_site_option( $key );
180
181 return $this;
182 }
183
184 /**
185 * Generate key
186 *
187 * Generates a unique key based on microtime. Queue items are
188 * given a unique key so that they can be merged upon save.
189 *
190 * @param int $length Length.
191 *
192 * @return string
193 */
194 protected function generate_key( $length = 64 ) {
195 $unique = md5( microtime() . wp_rand() );
196 $prepend = $this->identifier . '_batch_';
197
198 return substr( $prepend . $unique, 0, $length );
199 }
200
201 /**
202 * Maybe process queue
203 *
204 * Checks whether data exists within the queue and that
205 * the process is not already running.
206 */
207 public function maybe_handle() {
208 // Don't lock up other requests while processing
209 session_write_close();
210
211 if ( $this->is_process_running() ) {
212 // Background process already running.
213 wp_die();
214 }
215
216 if ( $this->is_queue_empty() ) {
217 // No data to process.
218 wp_die();
219 }
220
221 check_ajax_referer( $this->identifier, 'nonce' );
222
223 $this->handle();
224
225 wp_die();
226 }
227
228 /**
229 * Is queue empty
230 *
231 * @return bool
232 */
233 protected function is_queue_empty() {
234 global $wpdb;
235
236 $table = $wpdb->options;
237 $column = 'option_name';
238
239 if ( is_multisite() ) {
240 $table = $wpdb->sitemeta;
241 $column = 'meta_key';
242 }
243
244 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
245
246 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
247 // Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
248 $count = $wpdb->get_var( $wpdb->prepare( "
249 SELECT COUNT(*)
250 FROM {$table}
251 WHERE {$column} LIKE %s
252 ", $key ) );
253 // phpcs:enable
254
255 return ( $count > 0 ) ? false : true;
256 }
257
258 /**
259 * Is process running
260 *
261 * Check whether the current process is already running
262 * in a background process.
263 */
264 protected function is_process_running() {
265 if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
266 // Process already running.
267 return true;
268 }
269
270 return false;
271 }
272
273 /**
274 * Lock process
275 *
276 * Lock the process so that multiple instances can't run simultaneously.
277 * Override if applicable, but the duration should be greater than that
278 * defined in the time_exceeded() method.
279 */
280 protected function lock_process() {
281 $this->start_time = time(); // Set start time of current process.
282
283 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
284 $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
285
286 set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
287 }
288
289 /**
290 * Unlock process
291 *
292 * Unlock the process so that other instances can spawn.
293 *
294 * @return $this
295 */
296 protected function unlock_process() {
297 delete_site_transient( $this->identifier . '_process_lock' );
298
299 return $this;
300 }
301
302 /**
303 * Get batch
304 *
305 * @return \stdClass Return the first batch from the queue
306 */
307 protected function get_batch() {
308 global $wpdb;
309
310 $table = $wpdb->options;
311 $column = 'option_name';
312 $key_column = 'option_id';
313 $value_column = 'option_value';
314
315 if ( is_multisite() ) {
316 $table = $wpdb->sitemeta;
317 $column = 'meta_key';
318 $key_column = 'meta_id';
319 $value_column = 'meta_value';
320 }
321
322 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
323
324 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
325 // Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
326 $query = $wpdb->get_row( $wpdb->prepare( "
327 SELECT *
328 FROM {$table}
329 WHERE {$column} LIKE %s
330 ORDER BY {$key_column} ASC
331 LIMIT 1
332 ", $key ) );
333 // phpcs:enable
334
335 $batch = new \stdClass();
336 $batch->key = $query->$column;
337 $batch->data = maybe_unserialize( $query->$value_column );
338
339 return $batch;
340 }
341
342 /**
343 * Handle
344 *
345 * Pass each queue item to the task handler, while remaining
346 * within server memory and time limit constraints.
347 */
348 protected function handle() {
349 $this->lock_process();
350
351 do {
352 $batch = $this->get_batch();
353
354 foreach ( $batch->data as $key => $value ) {
355 $task = $this->task( $value );
356
357 if ( false !== $task ) {
358 $batch->data[ $key ] = $task;
359 } else {
360 unset( $batch->data[ $key ] );
361 }
362
363 if ( $this->time_exceeded() || $this->memory_exceeded() ) {
364 // Batch limits reached.
365 break;
366 }
367 }
368
369 // Update or delete current batch.
370 if ( ! empty( $batch->data ) ) {
371 $this->update( $batch->key, $batch->data );
372 } else {
373 $this->delete( $batch->key );
374 }
375 } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
376
377 $this->unlock_process();
378
379 // Start next batch or complete process.
380 if ( ! $this->is_queue_empty() ) {
381 $this->dispatch();
382 } else {
383 $this->complete();
384 }
385
386 wp_die();
387 }
388
389 /**
390 * Memory exceeded
391 *
392 * Ensures the batch process never exceeds 90%
393 * of the maximum WordPress memory.
394 *
395 * @return bool
396 */
397 protected function memory_exceeded() {
398 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
399 $current_memory = memory_get_usage( true );
400 $return = false;
401
402 if ( $current_memory >= $memory_limit ) {
403 $return = true;
404 }
405
406 return apply_filters( $this->identifier . '_memory_exceeded', $return );
407 }
408
409 /**
410 * Get memory limit
411 *
412 * @return int
413 */
414 protected function get_memory_limit() {
415 if ( function_exists( 'ini_get' ) ) {
416 $memory_limit = ini_get( 'memory_limit' );
417 } else {
418 // Sensible default.
419 $memory_limit = '128M';
420 }
421
422 if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
423 // Unlimited, set to 32GB.
424 $memory_limit = '32000M';
425 }
426
427 return intval( $memory_limit ) * 1024 * 1024;
428 }
429
430 /**
431 * Time exceeded.
432 *
433 * Ensures the batch never exceeds a sensible time limit.
434 * A timeout limit of 30s is common on shared hosting.
435 *
436 * @return bool
437 */
438 protected function time_exceeded() {
439 $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
440 $return = false;
441
442 if ( time() >= $finish ) {
443 $return = true;
444 }
445
446 return apply_filters( $this->identifier . '_time_exceeded', $return );
447 }
448
449 /**
450 * Complete.
451 *
452 * Override if applicable, but ensure that the below actions are
453 * performed, or, call parent::complete().
454 */
455 protected function complete() {
456 // Unschedule the cron healthcheck.
457 $this->clear_scheduled_event();
458 }
459
460 /**
461 * Schedule cron healthcheck
462 *
463 * @access public
464 * @param mixed $schedules Schedules.
465 * @return mixed
466 */
467 public function schedule_cron_healthcheck( $schedules ) {
468 $interval = apply_filters( $this->identifier . '_cron_interval', 5 );
469
470 if ( property_exists( $this, 'cron_interval' ) ) {
471 $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
472 }
473
474 // Adds every 5 minutes to the existing schedules.
475 $schedules[ $this->identifier . '_cron_interval' ] = [
476 'interval' => MINUTE_IN_SECONDS * $interval,
477 'display' => sprintf(
478 /* translators: %d: Interval in minutes. */
479 esc_html__( 'Every %d minutes', 'elementor' ),
480 $interval,
481 ),
482 ];
483
484 return $schedules;
485 }
486
487 /**
488 * Handle cron healthcheck
489 *
490 * Restart the background process if not already running
491 * and data exists in the queue.
492 */
493 public function handle_cron_healthcheck() {
494 if ( $this->is_process_running() ) {
495 // Background process already running.
496 exit;
497 }
498
499 if ( $this->is_queue_empty() ) {
500 // No data to process.
501 $this->clear_scheduled_event();
502 exit;
503 }
504
505 $this->handle();
506
507 exit;
508 }
509
510 /**
511 * Schedule event
512 */
513 protected function schedule_event() {
514 if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
515 wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
516 }
517 }
518
519 /**
520 * Clear scheduled event
521 */
522 protected function clear_scheduled_event() {
523 $timestamp = wp_next_scheduled( $this->cron_hook_identifier );
524
525 if ( $timestamp ) {
526 wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
527 }
528 }
529
530 /**
531 * Cancel Process
532 *
533 * Stop processing queue items, clear cronjob and delete batch.
534 */
535 public function cancel_process() {
536 if ( ! $this->is_queue_empty() ) {
537 $batch = $this->get_batch();
538
539 $this->delete( $batch->key );
540
541 wp_clear_scheduled_hook( $this->cron_hook_identifier );
542 }
543 }
544
545 /**
546 * Task
547 *
548 * Override this method to perform any actions required on each
549 * queue item. Return the modified item for further processing
550 * in the next pass through. Or, return false to remove the
551 * item from the queue.
552 *
553 * @param mixed $item Queue item to iterate over.
554 *
555 * @return mixed
556 */
557 abstract protected function task( $item );
558 }
559