balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

wp-background-process.php (11021B)


      1 <?php
      2 namespace Elementor\Core\Base\BackgroundProcess;
      3 
      4 if ( ! defined( 'ABSPATH' ) ) {
      5 	exit;
      6 }
      7 
      8 /**
      9  * 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, 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 array|\WP_Error
     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 		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    207 		// Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
    208 		$count = $wpdb->get_var( $wpdb->prepare( "
    209 			SELECT COUNT(*)
    210 			FROM {$table}
    211 			WHERE {$column} LIKE %s
    212 		", $key ) );
    213 		// phpcs:enable
    214 
    215 		return ( $count > 0 ) ? false : true;
    216 	}
    217 
    218 	/**
    219 	 * Is process running
    220 	 *
    221 	 * Check whether the current process is already running
    222 	 * in a background process.
    223 	 */
    224 	protected function is_process_running() {
    225 		if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
    226 			// Process already running.
    227 			return true;
    228 		}
    229 
    230 		return false;
    231 	}
    232 
    233 	/**
    234 	 * Lock process
    235 	 *
    236 	 * Lock the process so that multiple instances can't run simultaneously.
    237 	 * Override if applicable, but the duration should be greater than that
    238 	 * defined in the time_exceeded() method.
    239 	 */
    240 	protected function lock_process() {
    241 		$this->start_time = time(); // Set start time of current process.
    242 
    243 		$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
    244 		$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
    245 
    246 		set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
    247 	}
    248 
    249 	/**
    250 	 * Unlock process
    251 	 *
    252 	 * Unlock the process so that other instances can spawn.
    253 	 *
    254 	 * @return $this
    255 	 */
    256 	protected function unlock_process() {
    257 		delete_site_transient( $this->identifier . '_process_lock' );
    258 
    259 		return $this;
    260 	}
    261 
    262 	/**
    263 	 * Get batch
    264 	 *
    265 	 * @return \stdClass Return the first batch from the queue
    266 	 */
    267 	protected function get_batch() {
    268 		global $wpdb;
    269 
    270 		$table        = $wpdb->options;
    271 		$column       = 'option_name';
    272 		$key_column   = 'option_id';
    273 		$value_column = 'option_value';
    274 
    275 		if ( is_multisite() ) {
    276 			$table        = $wpdb->sitemeta;
    277 			$column       = 'meta_key';
    278 			$key_column   = 'meta_id';
    279 			$value_column = 'meta_value';
    280 		}
    281 
    282 		$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
    283 
    284 		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    285 		// Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
    286 		$query = $wpdb->get_row( $wpdb->prepare( "
    287 			SELECT *
    288 			FROM {$table}
    289 			WHERE {$column} LIKE %s
    290 			ORDER BY {$key_column} ASC
    291 			LIMIT 1
    292 		", $key ) );
    293 		// phpcs:enable
    294 
    295 		$batch       = new \stdClass();
    296 		$batch->key  = $query->$column;
    297 		$batch->data = maybe_unserialize( $query->$value_column );
    298 
    299 		return $batch;
    300 	}
    301 
    302 	/**
    303 	 * Handle
    304 	 *
    305 	 * Pass each queue item to the task handler, while remaining
    306 	 * within server memory and time limit constraints.
    307 	 */
    308 	protected function handle() {
    309 		$this->lock_process();
    310 
    311 		do {
    312 			$batch = $this->get_batch();
    313 
    314 			foreach ( $batch->data as $key => $value ) {
    315 				$task = $this->task( $value );
    316 
    317 				if ( false !== $task ) {
    318 					$batch->data[ $key ] = $task;
    319 				} else {
    320 					unset( $batch->data[ $key ] );
    321 				}
    322 
    323 				if ( $this->time_exceeded() || $this->memory_exceeded() ) {
    324 					// Batch limits reached.
    325 					break;
    326 				}
    327 			}
    328 
    329 			// Update or delete current batch.
    330 			if ( ! empty( $batch->data ) ) {
    331 				$this->update( $batch->key, $batch->data );
    332 			} else {
    333 				$this->delete( $batch->key );
    334 			}
    335 		} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
    336 
    337 		$this->unlock_process();
    338 
    339 		// Start next batch or complete process.
    340 		if ( ! $this->is_queue_empty() ) {
    341 			$this->dispatch();
    342 		} else {
    343 			$this->complete();
    344 		}
    345 
    346 		wp_die();
    347 	}
    348 
    349 	/**
    350 	 * Memory exceeded
    351 	 *
    352 	 * Ensures the batch process never exceeds 90%
    353 	 * of the maximum WordPress memory.
    354 	 *
    355 	 * @return bool
    356 	 */
    357 	protected function memory_exceeded() {
    358 		$memory_limit   = $this->get_memory_limit() * 0.9; // 90% of max memory
    359 		$current_memory = memory_get_usage( true );
    360 		$return         = false;
    361 
    362 		if ( $current_memory >= $memory_limit ) {
    363 			$return = true;
    364 		}
    365 
    366 		return apply_filters( $this->identifier . '_memory_exceeded', $return );
    367 	}
    368 
    369 	/**
    370 	 * Get memory limit
    371 	 *
    372 	 * @return int
    373 	 */
    374 	protected function get_memory_limit() {
    375 		if ( function_exists( 'ini_get' ) ) {
    376 			$memory_limit = ini_get( 'memory_limit' );
    377 		} else {
    378 			// Sensible default.
    379 			$memory_limit = '128M';
    380 		}
    381 
    382 		if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
    383 			// Unlimited, set to 32GB.
    384 			$memory_limit = '32000M';
    385 		}
    386 
    387 		return intval( $memory_limit ) * 1024 * 1024;
    388 	}
    389 
    390 	/**
    391 	 * Time exceeded.
    392 	 *
    393 	 * Ensures the batch never exceeds a sensible time limit.
    394 	 * A timeout limit of 30s is common on shared hosting.
    395 	 *
    396 	 * @return bool
    397 	 */
    398 	protected function time_exceeded() {
    399 		$finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
    400 		$return = false;
    401 
    402 		if ( time() >= $finish ) {
    403 			$return = true;
    404 		}
    405 
    406 		return apply_filters( $this->identifier . '_time_exceeded', $return );
    407 	}
    408 
    409 	/**
    410 	 * Complete.
    411 	 *
    412 	 * Override if applicable, but ensure that the below actions are
    413 	 * performed, or, call parent::complete().
    414 	 */
    415 	protected function complete() {
    416 		// Unschedule the cron healthcheck.
    417 		$this->clear_scheduled_event();
    418 	}
    419 
    420 	/**
    421 	 * Schedule cron healthcheck
    422 	 *
    423 	 * @access public
    424 	 * @param mixed $schedules Schedules.
    425 	 * @return mixed
    426 	 */
    427 	public function schedule_cron_healthcheck( $schedules ) {
    428 		$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
    429 
    430 		if ( property_exists( $this, 'cron_interval' ) ) {
    431 			$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
    432 		}
    433 
    434 		// Adds every 5 minutes to the existing schedules.
    435 		$schedules[ $this->identifier . '_cron_interval' ] = array(
    436 			'interval' => MINUTE_IN_SECONDS * $interval,
    437 			'display'  => sprintf( esc_html__( 'Every %d Minutes', 'elementor' ), $interval ),
    438 		);
    439 
    440 		return $schedules;
    441 	}
    442 
    443 	/**
    444 	 * Handle cron healthcheck
    445 	 *
    446 	 * Restart the background process if not already running
    447 	 * and data exists in the queue.
    448 	 */
    449 	public function handle_cron_healthcheck() {
    450 		if ( $this->is_process_running() ) {
    451 			// Background process already running.
    452 			exit;
    453 		}
    454 
    455 		if ( $this->is_queue_empty() ) {
    456 			// No data to process.
    457 			$this->clear_scheduled_event();
    458 			exit;
    459 		}
    460 
    461 		$this->handle();
    462 
    463 		exit;
    464 	}
    465 
    466 	/**
    467 	 * Schedule event
    468 	 */
    469 	protected function schedule_event() {
    470 		if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
    471 			wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
    472 		}
    473 	}
    474 
    475 	/**
    476 	 * Clear scheduled event
    477 	 */
    478 	protected function clear_scheduled_event() {
    479 		$timestamp = wp_next_scheduled( $this->cron_hook_identifier );
    480 
    481 		if ( $timestamp ) {
    482 			wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
    483 		}
    484 	}
    485 
    486 	/**
    487 	 * Cancel Process
    488 	 *
    489 	 * Stop processing queue items, clear cronjob and delete batch.
    490 	 *
    491 	 */
    492 	public function cancel_process() {
    493 		if ( ! $this->is_queue_empty() ) {
    494 			$batch = $this->get_batch();
    495 
    496 			$this->delete( $batch->key );
    497 
    498 			wp_clear_scheduled_hook( $this->cron_hook_identifier );
    499 		}
    500 
    501 	}
    502 
    503 	/**
    504 	 * Task
    505 	 *
    506 	 * Override this method to perform any actions required on each
    507 	 * queue item. Return the modified item for further processing
    508 	 * in the next pass through. Or, return false to remove the
    509 	 * item from the queue.
    510 	 *
    511 	 * @param mixed $item Queue item to iterate over.
    512 	 *
    513 	 * @return mixed
    514 	 */
    515 	abstract protected function task( $item );
    516 
    517 }