PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.3
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.3
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / deliciousbrains / wp-background-processing / classes / wp-background-process.php

wp-background-process.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.3, at vendor/deliciousbrains/wp-background-processing/classes/wp-background-process.php

999 lines 22.8 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
8 /**
9 * Abstract Imagify_WP_Background_Process class.
10 *
11 * @abstract
12 * @extends Imagify_WP_Async_Request
13 */
14 abstract class Imagify_WP_Background_Process extends Imagify_WP_Async_Request {
15 /**
16 * The default query arg name used for passing the chain ID to new processes.
17 */
18 const CHAIN_ID_ARG_NAME = 'chain_id';
19
20 /**
21 * Unique background process chain ID.
22 *
23 * @var string
24 */
25 private $chain_id;
26
27 /**
28 * Action
29 *
30 * (default value: 'background_process')
31 *
32 * @var string
33 * @access protected
34 */
35 protected $action = 'background_process';
36
37 /**
38 * Start time of current process.
39 *
40 * (default value: 0)
41 *
42 * @var int
43 * @access protected
44 */
45 protected $start_time = 0;
46
47 /**
48 * Cron_hook_identifier
49 *
50 * @var string
51 * @access protected
52 */
53 protected $cron_hook_identifier;
54
55 /**
56 * Cron_interval_identifier
57 *
58 * @var string
59 * @access protected
60 */
61 protected $cron_interval_identifier;
62
63 /**
64 * Restrict object instantiation when using unserialize.
65 *
66 * @var bool|array
67 */
68 protected $allowed_batch_data_classes = true;
69
70 /**
71 * The status set when process is cancelling.
72 *
73 * @var int
74 */
75 const STATUS_CANCELLED = 1;
76
77 /**
78 * The status set when process is paused or pausing.
79 *
80 * @var int;
81 */
82 const STATUS_PAUSED = 2;
83
84 /**
85 * Initiate new background process.
86 *
87 * @param bool|array $allowed_batch_data_classes Optional. Array of class names that can be unserialized. Default true (any class).
88 */
89 public function __construct( $allowed_batch_data_classes = true ) {
90 parent::__construct();
91
92 if ( empty( $allowed_batch_data_classes ) && false !== $allowed_batch_data_classes ) {
93 $allowed_batch_data_classes = true;
94 }
95
96 if ( ! is_bool( $allowed_batch_data_classes ) && ! is_array( $allowed_batch_data_classes ) ) {
97 $allowed_batch_data_classes = true;
98 }
99
100 // If allowed_batch_data_classes property set in subclass,
101 // only apply override if not allowing any class.
102 if ( true === $this->allowed_batch_data_classes || true !== $allowed_batch_data_classes ) {
103 $this->allowed_batch_data_classes = $allowed_batch_data_classes;
104 }
105
106 $this->cron_hook_identifier = $this->identifier . '_cron';
107 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
108
109 add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
110 add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
111
112 // Ensure dispatch query args included extra data.
113 add_filter( $this->identifier . '_query_args', array( $this, 'filter_dispatch_query_args' ) );
114 }
115
116 /**
117 * Schedule the cron healthcheck and dispatch an async request to start processing the queue.
118 *
119 * @access public
120 * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted.
121 */
122 public function dispatch() {
123 if ( $this->is_processing() ) {
124 // Process already running.
125 return false;
126 }
127
128 /**
129 * Filter fired before background process dispatches its next process.
130 *
131 * @param bool $cancel Should the dispatch be cancelled? Default false.
132 * @param string $chain_id The background process chain ID.
133 */
134 $cancel = apply_filters( $this->identifier . '_pre_dispatch', false, $this->get_chain_id() );
135
136 if ( $cancel ) {
137 return false;
138 }
139
140 // Schedule the cron healthcheck.
141 $this->schedule_event();
142
143 // Perform remote post.
144 return parent::dispatch();
145 }
146
147 /**
148 * Push to the queue.
149 *
150 * Note, save must be called in order to persist queued items to a batch for processing.
151 *
152 * @param mixed $data Data.
153 *
154 * @return $this
155 */
156 public function push_to_queue( $data ) {
157 $this->data[] = $data;
158
159 return $this;
160 }
161
162 /**
163 * Save the queued items for future processing.
164 *
165 * @return $this
166 */
167 public function save() {
168 $key = $this->generate_key();
169
170 if ( ! empty( $this->data ) ) {
171 update_site_option( $key, $this->data );
172 }
173
174 // Clean out data so that new data isn't prepended with closed session's data.
175 $this->data = array();
176
177 return $this;
178 }
179
180 /**
181 * Update a batch's queued items.
182 *
183 * @param string $key Key.
184 * @param array $data Data.
185 *
186 * @return $this
187 */
188 public function update( $key, $data ) {
189 if ( ! empty( $data ) ) {
190 update_site_option( $key, $data );
191 }
192
193 return $this;
194 }
195
196 /**
197 * Delete a batch of queued items.
198 *
199 * @param string $key Key.
200 *
201 * @return $this
202 */
203 public function delete( $key ) {
204 delete_site_option( $key );
205
206 return $this;
207 }
208
209 /**
210 * Delete entire job queue.
211 */
212 public function delete_all() {
213 $batches = $this->get_batches();
214
215 foreach ( $batches as $batch ) {
216 $this->delete( $batch->key );
217 }
218
219 delete_site_option( $this->get_status_key() );
220
221 $this->cancelled();
222 }
223
224 /**
225 * Cancel job on next batch.
226 */
227 public function cancel() {
228 update_site_option( $this->get_status_key(), self::STATUS_CANCELLED );
229
230 // Just in case the job was paused at the time.
231 $this->dispatch();
232 }
233
234 /**
235 * Has the process been cancelled?
236 *
237 * @return bool
238 */
239 public function is_cancelled() {
240 return $this->get_status() === self::STATUS_CANCELLED;
241 }
242
243 /**
244 * Called when background process has been cancelled.
245 */
246 protected function cancelled() {
247 do_action( $this->identifier . '_cancelled', $this->get_chain_id() );
248 }
249
250 /**
251 * Pause job on next batch.
252 */
253 public function pause() {
254 update_site_option( $this->get_status_key(), self::STATUS_PAUSED );
255 }
256
257 /**
258 * Has the process been paused?
259 *
260 * @return bool
261 */
262 public function is_paused() {
263 return $this->get_status() === self::STATUS_PAUSED;
264 }
265
266 /**
267 * Called when background process has been paused.
268 */
269 protected function paused() {
270 do_action( $this->identifier . '_paused', $this->get_chain_id() );
271 }
272
273 /**
274 * Resume job.
275 */
276 public function resume() {
277 delete_site_option( $this->get_status_key() );
278
279 $this->schedule_event();
280 $this->dispatch();
281 $this->resumed();
282 }
283
284 /**
285 * Called when background process has been resumed.
286 */
287 protected function resumed() {
288 do_action( $this->identifier . '_resumed', $this->get_chain_id() );
289 }
290
291 /**
292 * Is queued?
293 *
294 * @return bool
295 */
296 public function is_queued() {
297 return ! $this->is_queue_empty();
298 }
299
300 /**
301 * Is the tool currently active, e.g. starting, working, paused or cleaning up?
302 *
303 * @return bool
304 */
305 public function is_active() {
306 return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled();
307 }
308
309 /**
310 * Generate key for a batch.
311 *
312 * Generates a unique key based on microtime. Queue items are
313 * given a unique key so that they can be merged upon save.
314 *
315 * @param int $length Optional max length to trim key to, defaults to 64 characters.
316 * @param string $key Optional string to append to identifier before hash, defaults to "batch".
317 *
318 * @return string
319 */
320 protected function generate_key( $length = 64, $key = 'batch' ) {
321 $unique = md5( microtime() . wp_rand() );
322 $prepend = $this->identifier . '_' . $key . '_';
323
324 return substr( $prepend . $unique, 0, $length );
325 }
326
327 /**
328 * Get the status key.
329 *
330 * @return string
331 */
332 protected function get_status_key() {
333 return $this->identifier . '_status';
334 }
335
336 /**
337 * Get the status value for the process.
338 *
339 * @return int
340 */
341 protected function get_status() {
342 global $wpdb;
343
344 if ( is_multisite() ) {
345 $status = $wpdb->get_var(
346 $wpdb->prepare(
347 "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = %s AND site_id = %d LIMIT 1",
348 $this->get_status_key(),
349 get_current_network_id()
350 )
351 );
352 } else {
353 $status = $wpdb->get_var(
354 $wpdb->prepare(
355 "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
356 $this->get_status_key()
357 )
358 );
359 }
360
361 return absint( $status );
362 }
363
364 /**
365 * Maybe process a batch of queued items.
366 *
367 * Checks whether data exists within the queue and that
368 * the process is not already running.
369 */
370 public function maybe_handle() {
371 // Don't lock up other requests while processing.
372 session_write_close();
373
374 check_ajax_referer( $this->identifier, 'nonce' );
375
376 // Background process already running.
377 if ( $this->is_processing() ) {
378 return $this->maybe_wp_die();
379 }
380
381 // Cancel requested.
382 if ( $this->is_cancelled() ) {
383 $this->clear_scheduled_event();
384 $this->delete_all();
385
386 return $this->maybe_wp_die();
387 }
388
389 // Pause requested.
390 if ( $this->is_paused() ) {
391 $this->clear_scheduled_event();
392 $this->paused();
393
394 return $this->maybe_wp_die();
395 }
396
397 // No data to process.
398 if ( $this->is_queue_empty() ) {
399 return $this->maybe_wp_die();
400 }
401
402 $this->handle();
403
404 return $this->maybe_wp_die();
405 }
406
407 /**
408 * Is queue empty?
409 *
410 * @return bool
411 */
412 protected function is_queue_empty() {
413 return empty( $this->get_batch() );
414 }
415
416 /**
417 * Is process running?
418 *
419 * Check whether the current process is already running
420 * in a background process.
421 *
422 * @return bool
423 *
424 * @deprecated 1.1.0 Superseded.
425 * @see is_processing()
426 */
427 protected function is_process_running() {
428 return $this->is_processing();
429 }
430
431 /**
432 * Is the background process currently running?
433 *
434 * @return bool
435 */
436 public function is_processing() {
437 if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
438 // Process already running.
439 return true;
440 }
441
442 return false;
443 }
444
445 /**
446 * Lock process.
447 *
448 * Lock the process so that multiple instances can't run simultaneously.
449 * Override if applicable, but the duration should be greater than that
450 * defined in the time_exceeded() method.
451 *
452 * @param bool $reset_start_time Optional, default true.
453 */
454 public function lock_process( $reset_start_time = true ) {
455 if ( $reset_start_time ) {
456 $this->start_time = time(); // Set start time of current process.
457 }
458
459 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
460 $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
461
462 $microtime = microtime();
463 $locked = set_site_transient( $this->identifier . '_process_lock', $microtime, $lock_duration );
464
465 /**
466 * Action to note whether the background process managed to create its lock.
467 *
468 * The lock is used to signify that a process is running a task and no other
469 * process should be allowed to run the same task until the lock is released.
470 *
471 * @param bool $locked Whether the lock was successfully created.
472 * @param string $microtime Microtime string value used for the lock.
473 * @param int $lock_duration Max number of seconds that the lock will live for.
474 * @param string $chain_id Current background process chain ID.
475 */
476 do_action(
477 $this->identifier . '_process_locked',
478 $locked,
479 $microtime,
480 $lock_duration,
481 $this->get_chain_id()
482 );
483 }
484
485 /**
486 * Unlock process.
487 *
488 * Unlock the process so that other instances can spawn.
489 *
490 * @return $this
491 */
492 protected function unlock_process() {
493 $unlocked = delete_site_transient( $this->identifier . '_process_lock' );
494
495 /**
496 * Action to note whether the background process managed to release its lock.
497 *
498 * The lock is used to signify that a process is running a task and no other
499 * process should be allowed to run the same task until the lock is released.
500 *
501 * @param bool $unlocked Whether the lock was released.
502 * @param string $chain_id Current background process chain ID.
503 */
504 do_action( $this->identifier . '_process_unlocked', $unlocked, $this->get_chain_id() );
505
506 return $this;
507 }
508
509 /**
510 * Get batch.
511 *
512 * @return stdClass Return the first batch of queued items.
513 */
514 protected function get_batch() {
515 return array_reduce(
516 $this->get_batches( 1 ),
517 static function ( $carry, $batch ) {
518 return $batch;
519 },
520 array()
521 );
522 }
523
524 /**
525 * Get batches.
526 *
527 * @param int $limit Number of batches to return, defaults to all.
528 *
529 * @return array of stdClass
530 */
531 public function get_batches( $limit = 0 ) {
532 global $wpdb;
533
534 if ( empty( $limit ) || ! is_int( $limit ) ) {
535 $limit = 0;
536 }
537
538 $table = $wpdb->options;
539 $column = 'option_name';
540 $key_column = 'option_id';
541 $value_column = 'option_value';
542
543 if ( is_multisite() ) {
544 $table = $wpdb->sitemeta;
545 $column = 'meta_key';
546 $key_column = 'meta_id';
547 $value_column = 'meta_value';
548 }
549
550 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
551
552 $sql = '
553 SELECT *
554 FROM ' . $table . '
555 WHERE ' . $column . ' LIKE %s
556 ORDER BY ' . $key_column . ' ASC
557 ';
558
559 $args = array( $key );
560
561 if ( ! empty( $limit ) ) {
562 $sql .= ' LIMIT %d';
563
564 $args[] = $limit;
565 }
566
567 $items = $wpdb->get_results(
568 $wpdb->prepare(
569 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
570 $args
571 )
572 );
573
574 $batches = array();
575
576 if ( ! empty( $items ) ) {
577 $allowed_classes = $this->allowed_batch_data_classes;
578
579 $batches = array_map(
580 static function ( $item ) use ( $column, $value_column, $allowed_classes ) {
581 $batch = new stdClass();
582 $batch->key = $item->{$column};
583 $batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes );
584
585 return $batch;
586 },
587 $items
588 );
589 }
590
591 return $batches;
592 }
593
594 /**
595 * Handle a dispatched request.
596 *
597 * Pass each queue item to the task handler, while remaining
598 * within server memory and time limit constraints.
599 */
600 protected function handle() {
601 $this->lock_process();
602
603 /**
604 * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0.
605 *
606 * @param int $seconds
607 */
608 $throttle_seconds = max(
609 0,
610 apply_filters(
611 $this->identifier . '_seconds_between_batches',
612 apply_filters(
613 $this->prefix . '_seconds_between_batches',
614 0
615 )
616 )
617 );
618
619 do {
620 $batch = $this->get_batch();
621
622 foreach ( $batch->data as $key => $value ) {
623 $task = $this->task( $value );
624
625 if ( false !== $task ) {
626 $batch->data[ $key ] = $task;
627 } else {
628 unset( $batch->data[ $key ] );
629 }
630
631 // Keep the batch up to date while processing it.
632 if ( ! empty( $batch->data ) ) {
633 $this->update( $batch->key, $batch->data );
634 }
635
636 // Let the server breathe a little.
637 sleep( $throttle_seconds );
638
639 // Batch limits reached, or pause or cancel requested.
640 if ( ! $this->should_continue() ) {
641 break;
642 }
643 }
644
645 // Delete current batch if fully processed.
646 if ( empty( $batch->data ) ) {
647 $this->delete( $batch->key );
648 }
649 } while ( ! $this->is_queue_empty() && $this->should_continue() );
650
651 $this->unlock_process();
652
653 // Start next batch or complete process.
654 if ( ! $this->is_queue_empty() ) {
655 $this->dispatch();
656 } else {
657 $this->complete();
658 }
659
660 return $this->maybe_wp_die();
661 }
662
663 /**
664 * Memory exceeded?
665 *
666 * Ensures the batch process never exceeds 90%
667 * of the maximum WordPress memory.
668 *
669 * @return bool
670 */
671 protected function memory_exceeded() {
672 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
673 $current_memory = memory_get_usage( true );
674 $return = false;
675
676 if ( $current_memory >= $memory_limit ) {
677 $return = true;
678 }
679
680 return apply_filters( $this->identifier . '_memory_exceeded', $return );
681 }
682
683 /**
684 * Get memory limit in bytes.
685 *
686 * @return int
687 */
688 protected function get_memory_limit() {
689 if ( function_exists( 'ini_get' ) ) {
690 $memory_limit = ini_get( 'memory_limit' );
691 } else {
692 // Sensible default.
693 $memory_limit = '128M';
694 }
695
696 if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
697 // Unlimited, set to 32GB.
698 $memory_limit = '32000M';
699 }
700
701 return wp_convert_hr_to_bytes( $memory_limit );
702 }
703
704 /**
705 * Time limit exceeded?
706 *
707 * Ensures the batch never exceeds a sensible time limit.
708 * A timeout limit of 30s is common on shared hosting.
709 *
710 * @return bool
711 */
712 protected function time_exceeded() {
713 $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
714 $return = false;
715
716 if ( time() >= $finish ) {
717 $return = true;
718 }
719
720 return apply_filters( $this->identifier . '_time_exceeded', $return );
721 }
722
723 /**
724 * Complete processing.
725 *
726 * Override if applicable, but ensure that the below actions are
727 * performed, or, call parent::complete().
728 */
729 protected function complete() {
730 delete_site_option( $this->get_status_key() );
731
732 // Remove the cron healthcheck job from the cron schedule.
733 $this->clear_scheduled_event();
734
735 $this->completed();
736 }
737
738 /**
739 * Called when background process has completed.
740 */
741 protected function completed() {
742 do_action( $this->identifier . '_completed', $this->get_chain_id() );
743 }
744
745 /**
746 * Get the cron healthcheck interval in minutes.
747 *
748 * Default is 5 minutes, minimum is 1 minute.
749 *
750 * @return int
751 */
752 public function get_cron_interval() {
753 $interval = 5;
754
755 if ( property_exists( $this, 'cron_interval' ) ) {
756 $interval = $this->cron_interval;
757 }
758
759 $interval = apply_filters( $this->cron_interval_identifier, $interval );
760
761 return is_int( $interval ) && 0 < $interval ? $interval : 5;
762 }
763
764 /**
765 * Schedule the cron healthcheck job.
766 *
767 * @access public
768 *
769 * @param mixed $schedules Schedules.
770 *
771 * @return mixed
772 */
773 public function schedule_cron_healthcheck( $schedules ) {
774 $interval = $this->get_cron_interval();
775
776 if ( 1 === $interval ) {
777 $display = __( 'Every Minute' );
778 } else {
779 $display = sprintf( __( 'Every %d Minutes' ), $interval );
780 }
781
782 // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules.
783 $schedules[ $this->cron_interval_identifier ] = array(
784 'interval' => MINUTE_IN_SECONDS * $interval,
785 'display' => $display,
786 );
787
788 return $schedules;
789 }
790
791 /**
792 * Handle cron healthcheck event.
793 *
794 * Restart the background process if not already running
795 * and data exists in the queue.
796 */
797 public function handle_cron_healthcheck() {
798 if ( $this->is_processing() ) {
799 // Background process already running.
800 exit;
801 }
802
803 if ( $this->is_queue_empty() ) {
804 // No data to process.
805 $this->clear_scheduled_event();
806 exit;
807 }
808
809 $this->dispatch();
810 }
811
812 /**
813 * Schedule the cron healthcheck event.
814 */
815 protected function schedule_event() {
816 if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
817 wp_schedule_event(
818 time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ),
819 $this->cron_interval_identifier,
820 $this->cron_hook_identifier
821 );
822 }
823 }
824
825 /**
826 * Clear scheduled cron healthcheck event.
827 */
828 protected function clear_scheduled_event() {
829 $timestamp = wp_next_scheduled( $this->cron_hook_identifier );
830
831 if ( $timestamp ) {
832 wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
833 }
834 }
835
836 /**
837 * Cancel the background process.
838 *
839 * Stop processing queue items, clear cron job and delete batch.
840 *
841 * @deprecated 1.1.0 Superseded.
842 * @see cancel()
843 */
844 public function cancel_process() {
845 $this->cancel();
846 }
847
848 /**
849 * Perform task with queued item.
850 *
851 * Override this method to perform any actions required on each
852 * queue item. Return the modified item for further processing
853 * in the next pass through. Or, return false to remove the
854 * item from the queue.
855 *
856 * @param mixed $item Queue item to iterate over.
857 *
858 * @return mixed
859 */
860 abstract protected function task( $item );
861
862 /**
863 * Maybe unserialize data, but not if an object.
864 *
865 * @param mixed $data Data to be unserialized.
866 * @param bool|array $allowed_classes Array of class names that can be unserialized.
867 *
868 * @return mixed
869 */
870 protected static function maybe_unserialize( $data, $allowed_classes ) {
871 if ( is_serialized( $data ) ) {
872 $options = array();
873 if ( is_bool( $allowed_classes ) || is_array( $allowed_classes ) ) {
874 $options['allowed_classes'] = $allowed_classes;
875 }
876
877 return @unserialize( $data, $options ); // @phpcs:ignore
878 }
879
880 return $data;
881 }
882
883 /**
884 * Should any processing continue?
885 *
886 * @return bool
887 */
888 public function should_continue() {
889 /**
890 * Filter whether the current background process should continue running the task
891 * if there is data to be processed.
892 *
893 * If the processing time or memory limits have been exceeded, the value will be false.
894 * If pause or cancel have been requested, the value will be false.
895 *
896 * It is very unlikely that you would want to override a false value with true.
897 *
898 * If false is returned here, it does not necessarily mean background processing is
899 * complete. If there is batch data still to be processed and pause or cancel have not
900 * been requested, it simply means this background process should spawn a new process
901 * for the chain to continue processing and then close itself down.
902 *
903 * @param bool $continue Should the current process continue processing the task?
904 * @param string $chain_id The current background process chain's ID.
905 *
906 * @return bool
907 */
908 return apply_filters(
909 $this->identifier . '_should_continue',
910 ! ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ),
911 $this->get_chain_id()
912 );
913 }
914
915 /**
916 * Get the string used to identify this type of background process.
917 *
918 * @return string
919 */
920 public function get_identifier() {
921 return $this->identifier;
922 }
923
924 /**
925 * Return the current background process chain's ID.
926 *
927 * If the chain's ID hasn't been set before this function is first used,
928 * and hasn't been passed as a query arg during dispatch,
929 * the chain ID will be generated before being returned.
930 *
931 * @return string
932 */
933 public function get_chain_id() {
934 if ( empty( $this->chain_id ) && wp_doing_ajax() && isset( $_REQUEST['action'] ) && $_REQUEST['action'] === $this->identifier ) {
935 check_ajax_referer( $this->identifier, 'nonce' );
936
937 if ( ! empty( $_GET[ $this->get_chain_id_arg_name() ] ) ) {
938 $chain_id = sanitize_key( $_GET[ $this->get_chain_id_arg_name() ] );
939
940 if ( wp_is_uuid( $chain_id ) ) {
941 $this->chain_id = $chain_id;
942
943 return $this->chain_id;
944 }
945 }
946 }
947
948 if ( empty( $this->chain_id ) ) {
949 $this->chain_id = wp_generate_uuid4();
950 }
951
952 return $this->chain_id;
953 }
954
955 /**
956 * Filters the query arguments used during an async request.
957 *
958 * @param array $args Current query args.
959 *
960 * @return array
961 */
962 public function filter_dispatch_query_args( $args ) {
963 $args[ $this->get_chain_id_arg_name() ] = $this->get_chain_id();
964
965 return $args;
966 }
967
968 /**
969 * Get the query arg name used for passing the chain ID to new processes.
970 *
971 * @return string
972 */
973 private function get_chain_id_arg_name() {
974 static $chain_id_arg_name;
975
976 if ( ! empty( $chain_id_arg_name ) ) {
977 return $chain_id_arg_name;
978 }
979
980 /**
981 * Filter the query arg name used for passing the chain ID to new processes.
982 *
983 * If you encounter problems with using the default query arg name, you can
984 * change it with this filter.
985 *
986 * @param string $chain_id_arg_name Default "chain_id".
987 *
988 * @return string
989 */
990 $chain_id_arg_name = apply_filters( $this->identifier . '_chain_id_arg_name', self::CHAIN_ID_ARG_NAME );
991
992 if ( ! is_string( $chain_id_arg_name ) || empty( $chain_id_arg_name ) ) {
993 $chain_id_arg_name = self::CHAIN_ID_ARG_NAME;
994 }
995
996 return $chain_id_arg_name;
997 }
998 }
999