File: /home/pclast/www/wp-content/plugins/wp-rocket/inc/Engine/Common/Database/Queries/AbstractQuery.php
<?php
declare(strict_types=1);
namespace WP_Rocket\Engine\Common\Database\Queries;
use WP_Rocket\Dependencies\BerlinDB\Database\Query;
use WP_Rocket\Engine\Common\Database\QueryInterface;
class AbstractQuery extends Query implements QueryInterface {
/**
* Cleanup interval in months.
* Default is 3 months.
*
* @var int
*/
public $cleanup_interval = 3;
/**
* Queries the database and retrieves items or counts.
* We extend this method to check if the query returns 'Table not exists' error to delete the transient and force creating table.
*
* @param string|array $query Array or URL query string of parameters.
* @param bool $use_cache Use DB cache or not. (custom parameter added by us!).
* @return array|int List of items, or number of items when 'count' is passed as a query var.
*/
public function query( $query = [], bool $use_cache = true ) {
$db = $this->get_db();
if ( empty( $db ) ) {
return ! empty( $query['count'] ) ? 0 : [];
}
$query_result = parent::query( $query, $use_cache );
$last_error = $db->last_error;
if ( empty( $last_error ) ) {
return $query_result;
}
$db_name = rocket_get_constant( 'DB_NAME', '' );
$prefixed_table = $db->prefix . $this->table_name;
$pattern = "/Table [`'\"]?" . preg_quote( $db_name, '/' ) . '\.' . preg_quote( $prefixed_table, '/' ) . "[`'\"]? doesn't exist/i";
if ( preg_match( $pattern, $last_error ) ) {
delete_transient( $this->table_name . '_exists' );
}
return ! empty( $query['count'] ) ? 0 : [];
}
/**
* Get row for specific url.
*
* @param string $url Page Url.
* @param bool $is_mobile if the request is for mobile page.
*
* @return false|mixed
*/
public function get_row( string $url, bool $is_mobile = false ) {
$query = $this->query(
[
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
]
);
if ( empty( $query[0] ) ) {
return false;
}
return $query[0];
}
/**
* Get single row by ID.
*
* @param int $row_id DB Row ID.
*
* @return object|array|false false if no row found, array or object if row found.
*/
public function get_row_by_id( int $row_id ) {
$query = $this->query(
[
'id' => $row_id,
]
);
if ( is_array( $query ) ) {
$query = array_pop( $query );
}
if ( empty( $query ) ) {
return false;
}
return $query;
}
/**
* Get all rows with the same url (desktop and mobile versions).
*
* @param string $url Page url.
*
* @return array|false
*/
public function get_rows_by_url( string $url ) {
$query = $this->query(
[
'url' => untrailingslashit( $url ),
]
);
if ( empty( $query ) ) {
return false;
}
return $query;
}
/**
* Fetch on submit jobs.
*
* @param int $count Number of jobs to fetch.
* @return array|int
*/
public function get_on_submit_jobs( int $count = 100 ) {
$in_progress_count = (int) $this->query(
[
'count' => true,
'status' => [ 'in-progress' ],
]
);
$pending_count = (int) $this->query(
[
'count' => true,
'status' => [ 'pending' ],
]
);
$processing_count = $in_progress_count + $pending_count;
if ( 0 !== $count && $processing_count >= $count ) {
return [];
}
$query_params = [
'status' => 'to-submit',
'orderby' => 'modified',
'order' => 'asc',
];
if ( 0 !== $count ) {
$query_params['number'] = ( $count - $processing_count );
}
return $this->query( $query_params );
}
/**
* Create new DB row for specific url.
*
* @param string $url Current page url.
* @param string $job_id API job_id.
* @param string $queue_name API Queue name.
* @param bool $is_mobile if the request is for mobile page.
* @param array $additional_details Additional details to be saved into DB.
*
* @return bool
*/
public function create_new_job( string $url, string $job_id = '', string $queue_name = '', bool $is_mobile = false, array $additional_details = [] ) {
$item = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
'job_id' => $job_id,
'queue_name' => $queue_name,
'status' => 'to-submit',
'retries' => 0,
'last_accessed' => current_time( 'mysql', true ),
];
if ( ! empty( $additional_details ) ) {
$item = array_merge( $item, $additional_details );
}
$result = $this->add_item( $item );
/**
* Fires after a new job has been added.
*
* @param mixed $is_success New job status: ID of inserted row if successfully added; false otherwise.
* @param string $timestamp Current timestamp.
*/
rocket_do_action_and_deprecated(
'rocket_last_saas_job_added_time',
[ $result, current_time( 'mysql', true ) ],
'3.16',
'rocket_last_rucss_job_added_time'
);
return $result;
}
/**
* Get pending jobs.
*
* @param int $count Number of rows.
*
* @return array
*/
public function get_pending_jobs( int $count = 100 ) {
$inprogress_count = (int) $this->query(
[
'count' => true,
'status' => 'in-progress',
]
);
if ( $inprogress_count >= $count ) {
return [];
}
return $this->query(
[
'number' => ( $count - $inprogress_count ),
'status' => 'pending',
'job_id__not_in' => [
'not_in' => '',
],
'orderby' => 'modified',
'order' => 'asc',
]
);
}
/**
* Increment retries number and change status back to pending.
*
* @param string $url Url from DB row.
* @param boolean $is_mobile Is mobile from DB row.
* @param string $error_code error code.
* @param string $error_message error message.
*
* @return bool|int
*/
public function increment_retries( string $url, bool $is_mobile, string $error_code, string $error_message ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
$old = $this->get_row( $url, $is_mobile );
$retries = 0;
$previous_message = '';
if ( $old ) {
$retries = $old->retries;
$previous_message = $old->error_message;
}
$data = [
'retries' => $retries + 1,
'status' => 'pending',
'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$error_code}: {$error_message}",
];
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
return $db->update( $prefixed_table_name, $data, $where );
}
/**
* Update Job ID.
*
* @param int $id DB row ID.
* @param int $new_job_id new job id.
*
* @return bool
*/
public function update_job_id( $id, $new_job_id ) {
$update_data['job_id'] = $new_job_id;
return $this->update_item( $id, $update_data );
}
/**
* Change the status to be in-progress.
*
* @param string $url Url from DB row.
* @param boolean $is_mobile Is mobile from DB row.
* @param array $additional_update_fields Additional fields to update in the database.
* @return bool|int
*/
public function make_status_inprogress( string $url, bool $is_mobile, $additional_update_fields = [] ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
$additional_update_fields['status'] = 'in-progress';
return $db->update( $prefixed_table_name, $additional_update_fields, $where );
}
/**
* Reset the job and add new job_id pending.
*
* @param int $id DB row ID.
* @param string $job_id API job_id.
* @param array $additional_details Additional details to be saved into DB.
*
* @return bool
*/
public function reset_job( int $id, string $job_id = '', array $additional_details = [] ) {
$updates = [
'job_id' => $job_id,
'status' => 'to-submit',
'error_code' => '',
'error_message' => '',
'retries' => 0,
'modified' => current_time( 'mysql', true ),
'submitted_at' => current_time( 'mysql', true ),
];
if ( ! empty( $additional_details ) ) {
$updates = wp_parse_args(
$additional_details,
$updates
);
}
return $this->update_item(
$id,
$updates
);
}
/**
* Change the status to be failed.
*
* @param string $url Url from DB row.
* @param boolean $is_mobile Is mobile from DB row.
* @param string $error_code error code.
* @param string $error_message error message.
*
* @return bool|int
*/
public function make_status_failed( string $url, bool $is_mobile, string $error_code, string $error_message ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
$old = $this->get_row( $url, $is_mobile );
$previous_message = $old ? $old->error_message : '';
$data = [
'status' => 'failed',
'error_code' => $error_code,
'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$error_code}: {$error_message}",
];
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
return $db->update( $prefixed_table_name, $data, $where );
}
/**
* Update row last_accessed date to current date.
*
* @param int $id Used CSS id.
*
* @return bool
*/
public function update_last_accessed( int $id ): bool {
return (bool) $this->update_item(
$id,
[
'last_accessed' => current_time( 'mysql', true ),
]
);
}
/**
* Delete DB row by url.
*
* @param string $url Page url to be deleted.
*
* @return bool
*/
public function delete_by_url( string $url ) {
$items = $this->get_rows_by_url( $url );
if ( empty( $items ) ) {
return false;
}
$deleted = true;
foreach ( $items as $item ) {
if ( empty( $item->id ) ) {
continue;
}
$deleted = $deleted && $this->delete_item( $item->id );
}
return $deleted;
}
/**
* Get the count of not completed rows.
*
* @return int
*/
public function get_not_completed_count() {
return $this->query(
[
'count' => true,
'status__in' => [ 'pending', 'in-progress' ],
]
);
}
/**
* Get the count of completed rows.
*
* @return int
*/
public function get_completed_count() {
return $this->query(
[
'count' => true,
'status' => 'completed',
]
);
}
/**
* Check if the completed row is the first one by checking if there is any other completed row except itself.
*
* @param int $id DB row ID.
* @return bool
*/
public function is_first_completed( $id ): bool {
$count = $this->query(
[
'count' => true,
'status' => 'completed',
'id__not_in' => [ $id ],
]
);
return 0 === $count;
}
/**
* Get completed rows IDs.
*
* @return int[]|array
*/
public function get_completed_ids() {
return $this->query(
[
'fields' => 'ids',
'status' => 'completed',
]
);
}
/**
* Get all failed rows.
*
* @param float $delay delay before the urls are deleted.
* @param string $unit unit from the delay.
* @return array|false
*/
public function get_failed_rows( float $delay = 3, string $unit = 'days' ) {
$query = $this->query(
[
'status' => 'failed',
'date_query' => [
[
'column' => 'modified',
'before' => "$delay $unit ago",
'inclusive' => true,
],
],
],
false
);
if ( empty( $query ) ) {
return false;
}
return $query;
}
/**
* Revert status to pending.
*
* @param integer $id Used CSS id.
* @param array $additional_details Additional details to be saved into DB.
*
* @return boolean
*/
public function revert_to_pending( int $id, array $additional_details = [] ): bool {
$item_details = [
'error_code' => '',
'error_message' => '',
'retries' => 0,
'status' => 'pending',
'modified' => current_time( 'mysql', true ),
];
if ( ! empty( $additional_details ) ) {
$item_details['data'] = wp_json_encode( $additional_details );
}
return (bool) $this->update_item(
$id,
$item_details
);
}
/**
* Change the status to be pending.
*
* @param string $url DB row url.
* @param string $job_id API job_id.
* @param string $queue_name API Queue name.
* @param bool $is_mobile if the request is for mobile page.
* @return bool|int
*/
public function make_status_pending( string $url, string $job_id = '', string $queue_name = '', bool $is_mobile = false ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
$data = [
'job_id' => $job_id,
'queue_name' => $queue_name,
'status' => 'pending',
'is_mobile' => $is_mobile,
'submitted_at' => current_time( 'mysql', true ),
];
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
return $db->update( $prefixed_table_name, $data, $where );
}
/**
* Update the error message.
*
* @param string $url DB row url.
* @param boolean $is_mobile Is mobile from DB row.
* @param int $code Response code.
* @param string $message Response message.
* @param string $previous_message Previous saved message.
*
* @return bool|int
*/
public function update_message( string $url, bool $is_mobile, int $code, string $message, string $previous_message = '' ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
$data = [ 'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$code}: {$message}" ];
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
return $db->update( $prefixed_table_name, $data, $where );
}
/**
* Updates the next_retry_time field
*
* @param string $url DB row url.
* @param boolean $is_mobile Is mobile from DB row.
* @param string|int $next_retry_time timestamp or mysql format date.
*
* @return bool|int either it is saved or not.
*/
public function update_next_retry_time( string $url, bool $is_mobile, $next_retry_time ) {
if ( ! $this->is_allowed() ) {
return false;
}
$db = $this->get_db();
$prefixed_table_name = $db->prefix . $this->table_name;
if ( is_string( $next_retry_time ) && strtotime( $next_retry_time ) ) {
// If $next_retry_time is a valid date string, convert it to a timestamp.
$next_retry_time = strtotime( $next_retry_time );
} elseif ( ! is_numeric( $next_retry_time ) ) {
// If it's not numeric and not a valid date string, return false.
return false;
}
$data = [ 'next_retry_time' => gmdate( 'Y-m-d H:i:s', $next_retry_time ) ];
$where = [
'url' => untrailingslashit( $url ),
'is_mobile' => $is_mobile,
];
return $db->update( $prefixed_table_name, $data, $where );
}
/**
* Check if db action can be processed.
*
* @return boolean
*/
private function is_allowed() {
// Bail if no database interface is available.
if ( empty( $this->get_db() ) ) {
return false;
}
return true;
}
/**
* Get total number of rows.
*
* @return int
*/
public function get_total_count() {
return (int) $this->query(
[
'count' => true,
]
);
}
/**
* Set cleanup interval
*
* @param int $interval The interval duration, usually default to 1.
*
* @return object
*/
public function set_cleanup_interval( int $interval ): object {
$this->cleanup_interval = $interval;
return $this;
}
}