PluginProbe
AliNext – WooCommerce Dropshipping Plugin for AliExpress / trunk
AliNext – WooCommerce Dropshipping Plugin for AliExpress vtrunk
ali2woo-lite / includes / libs / wp-background-processing / classes / wp-background-process.php

wp-background-process.php in AliNext – WooCommerce Dropshipping Plugin for AliExpress trunk, at includes/libs/wp-background-processing/classes/wp-background-process.php

793 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Background Process
4 *
5 * @package WP-Background-Processing
6 */
7 // phpcs:ignoreFile WordPress.Security.EscapeOutput.OutputNotEscaped
8 namespace AliNext_Lite\Library\BackgroundProcessing;
9
10 use stdClass;
11 use WP_Error;
12
13 /**
14 * Abstract WP_Background_Process class.
15 *
16 * @abstract
17 * @extends WP_Async_Request
18 */
19 abstract class WP_Background_Process extends WP_Async_Request {
20
21 /**
22 * Action
23 *
24 * (default value: 'background_process')
25 *
26 * @var string
27 * @access protected
28 */
29 protected $action = 'background_process';
30
31 /**
32 * Start time of current process.
33 *
34 * (default value: 0)
35 *
36 * @var int
37 * @access protected
38 */
39 protected $start_time = 0;
40
41 /**
42 * Cron_hook_identifier
43 *
44 * @var string
45 * @access protected
46 */
47 protected $cron_hook_identifier;
48
49 /**
50 * Cron_interval_identifier
51 *
52 * @var string
53 * @access protected
54 */
55 protected $cron_interval_identifier;
56
57 /**
58 * Restrict object instantiation when using unserialize.
59 *
60 * @var bool|array
61 */
62 protected $allowed_batch_data_classes = true;
63
64 /**
65 * The status set when process is cancelling.
66 *
67 * @var int
68 */
69 const STATUS_CANCELLED = 1;
70
71 /**
72 * The status set when process is paused or pausing.
73 *
74 * @var int;
75 */
76 const STATUS_PAUSED = 2;
77
78 /**
79 * Initiate new background process.
80 *
81 * @param bool|array $allowed_batch_data_classes Optional. Array of class names that can be unserialized. Default true (any class).
82 */
83 public function __construct( $allowed_batch_data_classes = true ) {
84 parent::__construct();
85
86 if ( empty( $allowed_batch_data_classes ) && false !== $allowed_batch_data_classes ) {
87 $allowed_batch_data_classes = true;
88 }
89
90 if ( ! is_bool( $allowed_batch_data_classes ) && ! is_array( $allowed_batch_data_classes ) ) {
91 $allowed_batch_data_classes = true;
92 }
93
94 // If allowed_batch_data_classes property set in subclass,
95 // only apply override if not allowing any class.
96 if ( true === $this->allowed_batch_data_classes || true !== $allowed_batch_data_classes ) {
97 $this->allowed_batch_data_classes = $allowed_batch_data_classes;
98 }
99
100 $this->cron_hook_identifier = $this->identifier . '_cron';
101 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
102
103 add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
104 add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
105 }
106
107 /**
108 * Schedule the cron healthcheck and dispatch an async request to start processing the queue.
109 *
110 * @access public
111 * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted.
112 */
113 public function dispatch() {
114 if ( $this->is_processing() ) {
115 // Process already running.
116 return false;
117 }
118
119 // Schedule the cron healthcheck.
120 $this->schedule_event();
121
122 // Perform remote post.
123 return parent::dispatch();
124 }
125
126 /**
127 * Push to the queue.
128 *
129 * Note, save must be called in order to persist queued items to a batch for processing.
130 *
131 * @param mixed $data Data.
132 *
133 * @return $this
134 */
135 public function push_to_queue( $data ) {
136 $this->data[] = $data;
137
138 return $this;
139 }
140
141 /**
142 * Save the queued items for future processing.
143 *
144 * @return $this
145 */
146 public function save() {
147 $key = $this->generate_key();
148
149 if ( ! empty( $this->data ) ) {
150 update_site_option( $key, $this->data );
151 }
152
153 // Clean out data so that new data isn't prepended with closed session's data.
154 $this->data = array();
155
156 return $this;
157 }
158
159 /**
160 * Update a batch's queued items.
161 *
162 * @param string $key Key.
163 * @param array $data Data.
164 *
165 * @return $this
166 */
167 public function update( $key, $data ) {
168 if ( ! empty( $data ) ) {
169 update_site_option( $key, $data );
170 }
171
172 return $this;
173 }
174
175 /**
176 * Delete a batch of queued items.
177 *
178 * @param string $key Key.
179 *
180 * @return $this
181 */
182 public function delete( $key ) {
183 delete_site_option( $key );
184
185 return $this;
186 }
187
188 /**
189 * Delete entire job queue.
190 */
191 public function delete_all() {
192 $batches = $this->get_batches();
193
194 foreach ( $batches as $batch ) {
195 $this->delete( $batch->key );
196 }
197
198 delete_site_option( $this->get_status_key() );
199
200 $this->cancelled();
201 }
202
203 /**
204 * Cancel job on next batch.
205 */
206 public function cancel() {
207 update_site_option( $this->get_status_key(), self::STATUS_CANCELLED );
208
209 // Just in case the job was paused at the time.
210 $this->dispatch();
211 }
212
213 /**
214 * Has the process been cancelled?
215 *
216 * @return bool
217 */
218 public function is_cancelled() {
219 $status = get_site_option( $this->get_status_key(), 0 );
220
221 return absint( $status ) === self::STATUS_CANCELLED;
222 }
223
224 /**
225 * Called when background process has been cancelled.
226 */
227 protected function cancelled() {
228 do_action( $this->identifier . '_cancelled' );
229 }
230
231 /**
232 * Pause job on next batch.
233 */
234 public function pause() {
235 update_site_option( $this->get_status_key(), self::STATUS_PAUSED );
236 }
237
238 /**
239 * Is the job paused?
240 *
241 * @return bool
242 */
243 public function is_paused() {
244 $status = get_site_option( $this->get_status_key(), 0 );
245
246 return absint( $status ) === self::STATUS_PAUSED;
247 }
248
249 /**
250 * Called when background process has been paused.
251 */
252 protected function paused() {
253 do_action( $this->identifier . '_paused' );
254 }
255
256 /**
257 * Resume job.
258 */
259 public function resume() {
260 delete_site_option( $this->get_status_key() );
261
262 $this->schedule_event();
263 $this->dispatch();
264 $this->resumed();
265 }
266
267 /**
268 * Called when background process has been resumed.
269 */
270 protected function resumed() {
271 do_action( $this->identifier . '_resumed' );
272 }
273
274 /**
275 * Is queued?
276 *
277 * @return bool
278 */
279 public function is_queued() {
280 return ! $this->is_queue_empty();
281 }
282
283 /**
284 * Is the tool currently active, e.g. starting, working, paused or cleaning up?
285 *
286 * @return bool
287 */
288 public function is_active() {
289 return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled();
290 }
291
292 /**
293 * Generate key for a batch.
294 *
295 * Generates a unique key based on microtime. Queue items are
296 * given a unique key so that they can be merged upon save.
297 *
298 * @param int $length Optional max length to trim key to, defaults to 64 characters.
299 * @param string $key Optional string to append to identifier before hash, defaults to "batch".
300 *
301 * @return string
302 */
303 protected function generate_key( $length = 64, $key = 'batch' ) {
304 $unique = md5( microtime() . wp_rand() );
305 $prepend = $this->identifier . '_' . $key . '_';
306
307 return substr( $prepend . $unique, 0, $length );
308 }
309
310 /**
311 * Get the status key.
312 *
313 * @return string
314 */
315 protected function get_status_key() {
316 return $this->identifier . '_status';
317 }
318
319 /**
320 * Maybe process a batch of queued items.
321 *
322 * Checks whether data exists within the queue and that
323 * the process is not already running.
324 */
325 public function maybe_handle() {
326 // Don't lock up other requests while processing.
327 session_write_close();
328
329 if ( $this->is_processing() ) {
330 // Background process already running.
331 return $this->maybe_wp_die();
332 }
333
334 if ( $this->is_cancelled() ) {
335 $this->clear_scheduled_event();
336 $this->delete_all();
337
338 return $this->maybe_wp_die();
339 }
340
341 if ( $this->is_paused() ) {
342 $this->clear_scheduled_event();
343 $this->paused();
344
345 return $this->maybe_wp_die();
346 }
347
348 if ( $this->is_queue_empty() ) {
349 // No data to process.
350 return $this->maybe_wp_die();
351 }
352
353 check_ajax_referer( $this->identifier, 'nonce' );
354
355 $this->handle();
356
357 return $this->maybe_wp_die();
358 }
359
360 /**
361 * Is queue empty?
362 *
363 * @return bool
364 */
365 protected function is_queue_empty() {
366 return empty( $this->get_batch() );
367 }
368
369 /**
370 * Is process running?
371 *
372 * Check whether the current process is already running
373 * in a background process.
374 *
375 * @return bool
376 *
377 * @deprecated 1.1.0 Superseded.
378 * @see is_processing()
379 */
380 protected function is_process_running() {
381 return $this->is_processing();
382 }
383
384 /**
385 * Is the background process currently running?
386 *
387 * @return bool
388 */
389 public function is_processing() {
390 if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
391 // Process already running.
392 return true;
393 }
394
395 return false;
396 }
397
398 /**
399 * Lock process.
400 *
401 * Lock the process so that multiple instances can't run simultaneously.
402 * Override if applicable, but the duration should be greater than that
403 * defined in the time_exceeded() method.
404 */
405 protected function lock_process() {
406 $this->start_time = time(); // Set start time of current process.
407
408 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
409 $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
410
411 set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
412 }
413
414 /**
415 * Unlock process.
416 *
417 * Unlock the process so that other instances can spawn.
418 *
419 * @return $this
420 */
421 protected function unlock_process() {
422 delete_site_transient( $this->identifier . '_process_lock' );
423
424 return $this;
425 }
426
427 /**
428 * Get batch.
429 *
430 * @return stdClass Return the first batch of queued items.
431 */
432 protected function get_batch() {
433 return array_reduce(
434 $this->get_batches( 1 ),
435 static function ( $carry, $batch ) {
436 return $batch;
437 },
438 array()
439 );
440 }
441
442 /**
443 * Get batches.
444 *
445 * @param int $limit Number of batches to return, defaults to all.
446 *
447 * @return array of stdClass
448 */
449 public function get_batches( $limit = 0 ) {
450 global $wpdb;
451
452 if ( empty( $limit ) || ! is_int( $limit ) ) {
453 $limit = 0;
454 }
455
456 $table = $wpdb->options;
457 $column = 'option_name';
458 $key_column = 'option_id';
459 $value_column = 'option_value';
460
461 if ( is_multisite() ) {
462 $table = $wpdb->sitemeta;
463 $column = 'meta_key';
464 $key_column = 'meta_id';
465 $value_column = 'meta_value';
466 }
467
468 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
469
470 $sql = '
471 SELECT *
472 FROM ' . $table . '
473 WHERE ' . $column . ' LIKE %s
474 ORDER BY ' . $key_column . ' ASC
475 ';
476
477 $args = array( $key );
478
479 if ( ! empty( $limit ) ) {
480 $sql .= ' LIMIT %d';
481
482 $args[] = $limit;
483 }
484
485 $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
486
487 $batches = array();
488
489 if ( ! empty( $items ) ) {
490 $allowed_classes = $this->allowed_batch_data_classes;
491
492 $batches = array_map(
493 static function ( $item ) use ( $column, $value_column, $allowed_classes ) {
494 $batch = new stdClass();
495 $batch->key = $item->{$column};
496 $batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes );
497
498 return $batch;
499 },
500 $items
501 );
502 }
503
504 return $batches;
505 }
506
507 /**
508 * Handle a dispatched request.
509 *
510 * Pass each queue item to the task handler, while remaining
511 * within server memory and time limit constraints.
512 */
513 protected function handle() {
514 $this->lock_process();
515
516 /**
517 * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0.
518 *
519 * @param int $seconds
520 */
521 $throttle_seconds = max(
522 0,
523 apply_filters(
524 $this->identifier . '_seconds_between_batches',
525 apply_filters(
526 $this->prefix . '_seconds_between_batches',
527 0
528 )
529 )
530 );
531
532 do {
533 $batch = $this->get_batch();
534
535 foreach ( $batch->data as $key => $value ) {
536 $task = $this->task( $value );
537
538 if ( false !== $task ) {
539 $batch->data[ $key ] = $task;
540 } else {
541 unset( $batch->data[ $key ] );
542 }
543
544 // Keep the batch up to date while processing it.
545 if ( ! empty( $batch->data ) ) {
546 $this->update( $batch->key, $batch->data );
547 }
548
549 // Let the server breathe a little.
550 sleep( $throttle_seconds );
551
552 // Batch limits reached, or pause or cancel request.
553 if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ) {
554 break;
555 }
556 }
557
558 // Delete current batch if fully processed.
559 if ( empty( $batch->data ) ) {
560 $this->delete( $batch->key );
561 }
562 } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_paused() && ! $this->is_cancelled() );
563
564 $this->unlock_process();
565
566 // Start next batch or complete process.
567 if ( ! $this->is_queue_empty() ) {
568 $this->dispatch();
569 } else {
570 $this->complete();
571 }
572
573 return $this->maybe_wp_die();
574 }
575
576 /**
577 * Memory exceeded?
578 *
579 * Ensures the batch process never exceeds 90%
580 * of the maximum WordPress memory.
581 *
582 * @return bool
583 */
584 protected function memory_exceeded() {
585 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
586 $current_memory = memory_get_usage( true );
587 $return = false;
588
589 if ( $current_memory >= $memory_limit ) {
590 $return = true;
591 }
592
593 return apply_filters( $this->identifier . '_memory_exceeded', $return );
594 }
595
596 /**
597 * Get memory limit in bytes.
598 *
599 * @return int
600 */
601 protected function get_memory_limit() {
602 if ( function_exists( 'ini_get' ) ) {
603 $memory_limit = ini_get( 'memory_limit' );
604 } else {
605 // Sensible default.
606 $memory_limit = '128M';
607 }
608
609 if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
610 // Unlimited, set to 32GB.
611 $memory_limit = '32000M';
612 }
613
614 return wp_convert_hr_to_bytes( $memory_limit );
615 }
616
617 /**
618 * Time limit exceeded?
619 *
620 * Ensures the batch never exceeds a sensible time limit.
621 * A timeout limit of 30s is common on shared hosting.
622 *
623 * @return bool
624 */
625 protected function time_exceeded() {
626 $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
627 $return = false;
628
629 if ( time() >= $finish ) {
630 $return = true;
631 }
632
633 return apply_filters( $this->identifier . '_time_exceeded', $return );
634 }
635
636 /**
637 * Complete processing.
638 *
639 * Override if applicable, but ensure that the below actions are
640 * performed, or, call parent::complete().
641 */
642 protected function complete() {
643 delete_site_option( $this->get_status_key() );
644
645 // Remove the cron healthcheck job from the cron schedule.
646 $this->clear_scheduled_event();
647
648 $this->completed();
649 }
650
651 /**
652 * Called when background process has completed.
653 */
654 protected function completed() {
655 do_action( $this->identifier . '_completed' );
656 }
657
658 /**
659 * Get the cron healthcheck interval in minutes.
660 *
661 * Default is 5 minutes, minimum is 1 minute.
662 *
663 * @return int
664 */
665 public function get_cron_interval() {
666 $interval = 5;
667
668 if ( property_exists( $this, 'cron_interval' ) ) {
669 $interval = $this->cron_interval;
670 }
671
672 $interval = apply_filters( $this->cron_interval_identifier, $interval );
673
674 return is_int( $interval ) && 0 < $interval ? $interval : 5;
675 }
676
677 /**
678 * Schedule the cron healthcheck job.
679 *
680 * @access public
681 *
682 * @param mixed $schedules Schedules.
683 *
684 * @return mixed
685 */
686 public function schedule_cron_healthcheck( $schedules ) {
687 $interval = $this->get_cron_interval();
688
689 if ( 1 === $interval ) {
690 $display = esc_html__( 'Every Minute' );
691 } else {
692 $display = sprintf( esc_html__( 'Every %d Minutes' ), $interval );
693 }
694
695 // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules.
696 $schedules[ $this->cron_interval_identifier ] = array(
697 'interval' => MINUTE_IN_SECONDS * $interval,
698 'display' => $display,
699 );
700
701 return $schedules;
702 }
703
704 /**
705 * Handle cron healthcheck event.
706 *
707 * Restart the background process if not already running
708 * and data exists in the queue.
709 */
710 public function handle_cron_healthcheck() {
711 if ( $this->is_processing() ) {
712 // Background process already running.
713 exit;
714 }
715
716 if ( $this->is_queue_empty() ) {
717 // No data to process.
718 $this->clear_scheduled_event();
719 exit;
720 }
721
722 $this->dispatch();
723 }
724
725 /**
726 * Schedule the cron healthcheck event.
727 */
728 protected function schedule_event() {
729 if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
730 wp_schedule_event( time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ), $this->cron_interval_identifier, $this->cron_hook_identifier );
731 }
732 }
733
734 /**
735 * Clear scheduled cron healthcheck event.
736 */
737 protected function clear_scheduled_event() {
738 $timestamp = wp_next_scheduled( $this->cron_hook_identifier );
739
740 if ( $timestamp ) {
741 wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
742 }
743 }
744
745 /**
746 * Cancel the background process.
747 *
748 * Stop processing queue items, clear cron job and delete batch.
749 *
750 * @deprecated 1.1.0 Superseded.
751 * @see cancel()
752 */
753 public function cancel_process() {
754 $this->cancel();
755 }
756
757 /**
758 * Perform task with queued item.
759 *
760 * Override this method to perform any actions required on each
761 * queue item. Return the modified item for further processing
762 * in the next pass through. Or, return false to remove the
763 * item from the queue.
764 *
765 * @param mixed $item Queue item to iterate over.
766 *
767 * @return mixed
768 */
769 abstract protected function task( $item );
770
771 /**
772 * Maybe unserialize data, but not if an object.
773 *
774 * @param mixed $data Data to be unserialized.
775 * @param bool|array $allowed_classes Array of class names that can be unserialized.
776 *
777 * @return mixed
778 */
779 protected static function maybe_unserialize( $data, $allowed_classes ) {
780 if ( is_serialized( $data ) ) {
781 $options = array();
782 if ( is_bool( $allowed_classes ) || is_array( $allowed_classes ) ) {
783 $options['allowed_classes'] = $allowed_classes;
784 }
785
786 return @unserialize( $data, $options ); // @phpcs:ignore
787 }
788
789 return $data;
790 }
791 }
792
793