PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 1.7.2
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v1.7.2
2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 1.8.1 All 42 releases
sellkit / includes / core / libraries / wp-background-process.php

wp-background-process.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster 1.7.2, at includes/core/libraries/wp-background-process.php

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