* @param int|string $job_id API job_id. * @param callable $wait_time_callback Callback to get the wait time. * * @return bool */ public function make_status_pending( $id, $job_id, callable $wait_time_callback ) { if ( ! $this->table_exists() ) { return false; } $item = $this->find( $id ); if ( empty( $item ) ) { return false; } $update_data = [ 'job_id' => $job_id, 'retries' => (int) $item->retries + 1, 'status' => self::STATUS_PENDING, 'error_code' => null, 'error_message' => null, ]; // Determine the postpone time based on number of retries. $wait_time = $wait_time_callback( $update_data['retries'] ); $postpone_until = time() + $wait_time; $update_data['postponed_until'] = gmdate( 'Y-m-d H:i:s', $postpone_until ); return $this->update_item( $item->id, $update_data ); } /** * Postpone item. * * @param int $id Item ID. * @param callable $wait_time_callback Callback to get the wait time. * * @return bool */ public function postpone_item( $id, callable $wait_time_callback ) { if ( ! $this->table_exists() ) { return false; } $item = $this->find( $id ); if ( empty( $item ) ) { return false; } // Determine the postpone time based on number of retries. $wait_time = $wait_time_callback( $item->retries + 1 ); $postpone_until = time() + $wait_time; $update_data = [ 'retries' => (int) $item->retries + 1, 'postponed_until' => gmdate( 'Y-m-d H:i:s', $postpone_until ), ]; return $this->update_item( $item->id, $update_data ); } /** * Set "to download" status. * * @param int $id row. * * @return bool */ public function make_status_to_download( int $id ) { return $this->make_status( $id, self::STATUS_TO_DOWNLOAD ); } /** * Set downloading status. * * @param int $id row. * * @return bool */ public function make_status_downloading( int $id ) { return $this->make_status( $id, self::STATUS_DOWNLOADING ); } /** * Set failed status. * * @param int $id row. * @param string $error_code code. * @param ?string $error_message message. * * @return bool */ public function make_status_failed( int $id, string $error_code, ?string $error_message = '' ) { return $this->make_status( $id, self::STATUS_FAILED, $error_code, $error_message ); } /** * Set status. * * @param int $id row. * @param string $status status. * @param string $error_code code. * @param string $error_message message. * * @return bool */ private function make_status( int $id, string $status, $error_code = null, $error_message = null ) { if ( ! $this->table_exists() ) { return false; } $item = $this->find( $id ); if ( empty( $item ) ) { return false; } $update_data = [ 'status' => $status, 'error_code' => $error_code, 'error_message' => $error_message, ]; return $this->update_item( $item->id, $update_data ); } /** * Get job ready to download. * * @return ?Row Job ready to download. */ public function get_job_ready_to_download(): ?Row { if ( ! $this->table_exists() ) { return null; } $items = $this->query( [ 'status' => self::STATUS_TO_DOWNLOAD, 'orderby' => 'modified_at', 'order' => 'ASC', 'number' => 1, ], false ); return $this->extract_first_item( $items ); } /** * Get job ready to upload. * * @return ?Row Job ready to upload. */ public function get_job_ready_to_upload(): ?Row { if ( ! $this->table_exists() ) { return null; } $items = $this->query( [ 'status' => self::STATUS_NEW, 'orderby' => [ 'DESC' => 'priority', 'ASC' => 'created_at', ], 'number' => 1, ], false ); return $this->extract_first_item( $items ); } /** * Get total number of pending jobs. * * @return int Total number of pending jobs. */ public function get_pending_jobs_count(): int { if ( ! $this->table_exists() ) { return 0; } return (int) $this->query( [ 'count' => true, 'status' => self::STATUS_PENDING, ], false ); } /** * Get the size of the image queue. Failed jobs are excluded. * * This is intended to show the remaining number of images to be optimized in a progress widget. * * @return int the size of the image queue */ public function get_queue_original_size(): int { if ( ! $this->table_exists() ) { return 0; } return (int) $this->query( [ 'format' => 'original', 'count' => true, 'status__not_in' => [ self::STATUS_FAILED, ], ], false ); } /** * Check if there are any more items in the queue that can be processed at this time. * * These could be more items ready for download or more items waiting to be uploaded. Also pending images with * postponed date in the past. * * @since 3.12.6.1_1.1-1 * @since latest Added $exclude_new parameter to be able to exclude new items from the check. * * @param bool $exclude_new Exclude new items from the check. * * @return bool */ public function has_more_items_to_process( $exclude_new ): bool { if ( ! $this->table_exists() ) { return false; } $statuses = [ self::STATUS_TO_DOWNLOAD, ]; if ( ! $exclude_new ) { $statuses[] = self::STATUS_NEW; } $count = (int) $this->query( [ 'count' => true, 'status__in' => $statuses, 'postponed_until' => '0000-00-00 00:00:00', ], false ); if ( $count > 0 ) { return true; } $count = (int) $this->query( [ 'count' => true, 'status' => [ self::STATUS_PENDING, ], 'date_query' => [ [ 'column' => 'postponed_until', 'before' => current_time( 'mysql', true ), ], ], ], false ); return $count > 0; } /** * Get earliest postponed item. * * @since 3.12.6.1_1.1-1 * @since latest Changed the return from the count of items to the earliest postponed item. * * @return ?Row Earliest postponed item. */ public function get_earliest_postponed_item(): ?Row { if ( ! $this->table_exists() ) { return null; } $items = $this->query( [ 'status' => self::STATUS_PENDING, 'order' => 'ASC', 'orderby' => 'postponed_until', 'number' => 1, 'date_query' => [ [ 'column' => 'postponed_until', 'compare' => '!=', 'value' => '0000-00-00 00:00:00', ], ], ], false ); return $this->extract_first_item( $items ); } /** * Get job pending and not postponed. * * @return ?Row Job pending and not postponed. */ public function get_job_pending_not_postponed(): ?Row { if ( ! $this->table_exists() ) { return null; } $items = $this->query( [ 'status' => self::STATUS_PENDING, 'order' => 'ASC', 'orderby' => 'postponed_until', 'date_query' => [ [ 'column' => 'postponed_until', 'before' => current_time( 'mysql', true ), ], ], 'number' => 1, ], false ); return $this->extract_first_item( $items ); } /** * Extract the first items from raw output of query() function. * * @param array $items Items to extract first item from. Output from query(). * * @return ?Row * * @see query() */ private function extract_first_item( $items ): ?Row { if ( ! $this->table_exists() ) { return null; } if ( empty( $items ) || ! is_array( $items ) ) { // @phpstan-ignore-line return null; } $count = count( $items ); for ( $i = 0; $i < $count; $i ++ ) { if ( $items[ $i ] instanceof Row ) { return $items[ $i ]; } } return null; } /** * Deletes all entries from the table. * * @return int|bool Boolean true in case of success. Boolean false on error. */ public function delete_all() { if ( ! $this->table_exists() ) { return false; } $table_name = $this->get_db()->prefix . $this->table_name; return $this->get_db()->query( 'TRUNCATE ' . $table_name . ';' ); } /** * Make status new for all. * * @return int|bool Boolean true in case of success. Boolean false on error. */ public function make_status_new_for_all() { if ( ! $this->table_exists_strict() ) { // CL. return false; } $table_name = $this->get_db()->prefix . $this->table_name; $sql = "UPDATE `$table_name` SET `status` = %s, `retries` = 0, `error_code` = null, `error_message` = null, `postponed_until` = '0000-00-00 00:00:00';"; return $this->get_db()->query( $this->get_db()->prepare( $sql, [ 'status' => self::STATUS_NEW, ] ) ); } /** * Returns the current status of `wpr_image_optimization` table; true if it exists, false otherwise. * * @return boolean */ private function table_exists(): bool { if ( self::$table_exists ) { return true; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( empty( $db ) ) { return false; } // Query statement. $query = 'SHOW TABLES LIKE %s'; $like = $db->esc_like( $db->{$this->table_name} ); $prepared = $db->prepare( $query, $like ); $result = $db->get_var( $prepared ); // Does the table exist? $exists = $this->is_success( $result ); if ( $exists ) { self::$table_exists = $exists; } return $exists; } /** * Strict physical table existence check, bypassing the static cache. * * This is used by destructive helpers like {@see delete_all()} and * {@see make_status_new_for_all()} to avoid running TRUNCATE/UPDATE * on a table that has been dropped between test runs. * * @return bool */ private function table_exists_strict(): bool { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( empty( $db ) ) { return false; } // CL: Use SHOW CREATE TABLE to detect both regular and TEMPORARY tables. // This works for temporary tables which are not visible in SHOW TABLES or information_schema. $table_name = isset( $db->{$this->table_name} ) ? $db->{$this->table_name} : ( $db->prefix . $this->table_name ); $db->suppress_errors(); $result = $db->get_var( "SHOW CREATE TABLE `{$table_name}`" ); $db->show_errors(); $exists = $this->is_success( $result ); // Keep the public cache consistent for non-test code paths. self::$table_exists = $exists; return $exists; } }