oont-contents/plugins/wp-rocket/inc/Engine/Preload/Database/Queries/Cache.php
2025-02-08 15:10:23 +01:00

588 lines
13 KiB
PHP

<?php
namespace WP_Rocket\Engine\Preload\Database\Queries;
use WP_Rocket\Logger\Logger;
use WP_Rocket\Dependencies\Database\Query;
use WP_Rocket\Engine\Preload\Database\Rows\CacheRow;
use WP_Rocket\Engine\Preload\Database\Schemas\Cache as Schema;
class Cache extends Query {
/**
* Logger instance.
*
* @var Logger
*/
protected $logger;
/**
* Name of the database table to query.
*
* @var string
*/
protected $table_name = 'wpr_rocket_cache';
/**
* String used to alias the database table in MySQL statement.
*
* Keep this short, but descriptive. I.E. "tr" for term relationships.
*
* This is used to avoid collisions with JOINs.
*
* @var string
*/
protected $table_alias = 'wpr_cache';
/**
* Name of class used to setup the database schema.
*
* @var string
*/
protected $table_schema = Schema::class;
/** Item ******************************************************************/
/**
* Name for a single item.
*
* Use underscores between words. I.E. "term_relationship"
*
* This is used to automatically generate action hooks.
*
* @var string
*/
protected $item_name = 'cache';
/**
* Plural version for a group of items.
*
* Use underscores between words. I.E. "term_relationships"
*
* This is used to automatically generate action hooks.
*
* @var string
*/
protected $item_name_plural = 'caches';
/**
* Name of class used to turn IDs into first-class objects.
*
* This is used when looping through return values to guarantee their shape.
*
* @var mixed
*/
protected $item_shape = CacheRow::class;
/**
* Instantiate query.
*
* @param Logger $logger logger instance.
*
* @param string|array $query {
* Optional. Array or query string of item query parameters.
* Default empty.
*
* @type string $fields Site fields to return. Accepts 'ids' (returns an array of item IDs)
* or empty (returns an array of complete item objects). Default empty.
* To do a date query against a field, append the field name with _query
* @type bool $count Whether to return a item count (true) or array of item objects.
* Default false.
* @type int $number Limit number of items to retrieve. Use 0 for no limit.
* Default 100.
* @type int $offset Number of items to offset the query. Used to build LIMIT clause.
* Default 0.
* @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query.
* Default true.
* @type string|array $orderby Accepts false, an empty array, or 'none' to disable `ORDER BY` clause.
* Default 'id'.
* @type string $item How to item retrieved items. Accepts 'ASC', 'DESC'.
* Default 'DESC'.
* @type string $search Search term(s) to retrieve matching items for.
* Default empty.
* @type array $search_columns Array of column names to be searched.
* Default empty array.
* @type bool $update_item_cache Whether to prime the cache for found items.
* Default false.
* @type bool $update_meta_cache Whether to prime the meta cache for found items.
* Default false.
* }
*/
public function __construct( Logger $logger, $query = [] ) {
parent::__construct( $query );
$this->logger = $logger;
}
/**
* Create new resource row or update its contents if not created before.
*
* @since 3.9
*
* @param array $resource Resource array.
*
* @return bool
*/
public function create_or_update( array $resource ) {
$url = untrailingslashit( strtok( $resource['url'], '?' ) );
if ( $this->is_rejected( $resource['url'] ) ) {
return false;
}
// check the database if those resources added before.
$rows = $this->query(
[
'url' => $url,
],
false
);
if ( count( $rows ) === 0 ) {
// Create this new row in DB.
$resource_id = $this->add_item(
[
'url' => $url,
'status' => key_exists( 'status', $resource ) ? $resource['status'] : 'pending',
'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : false,
'last_accessed' => current_time( 'mysql', true ),
]
);
if ( $resource_id ) {
return $resource_id;
}
$this->logger->error( "Cannot insert {$resource['url']} into {$this->table_name}" );
return false;
}
$db_row = array_pop( $rows );
$data = [
'url' => $url,
'status' => key_exists( 'status', $resource ) ? $resource['status'] : $db_row->status,
'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : $db_row->is_locked,
'modified' => current_time( 'mysql', true ),
];
if ( key_exists( 'last_accessed', $resource ) && (bool) $resource['last_accessed'] ) {
$data['last_accessed'] = current_time( 'mysql', true );
}
// Update this row with the new content.
$this->update_item(
$db_row->id,
$data
);
return $db_row->id;
}
/**
* Create new resource row or update its contents if not created before.
*
* @since 3.9
*
* @param array $resource Resource array.
*
* @return bool
*/
public function create_or_nothing( array $resource ) {
if ( $this->is_rejected( $resource['url'] ) ) {
return false;
}
$url = strtok( $resource['url'], '?' );
// check the database if those resources added before.
$rows = $this->query(
[
'url' => untrailingslashit( $url ),
],
false
);
if ( count( $rows ) > 0 ) {
return false;
}
// Create this new row in DB.
$resource_id = $this->add_item(
[
'url' => untrailingslashit( $url ),
'status' => key_exists( 'status', $resource ) ? $resource['status'] : 'pending',
'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : false,
'last_accessed' => current_time( 'mysql', true ),
]
);
if ( $resource_id ) {
return $resource_id;
}
$this->logger->error( "Cannot insert {$resource['url']} into {$this->table_name}" );
return false;
}
/**
* 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 ) {
$url = strtok( $url, '?' );
$query = $this->query(
[
'url' => untrailingslashit( $url ),
]
);
if ( empty( $query ) ) {
return false;
}
return $query;
}
/**
* 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 ( ! $items ) {
return false;
}
$deleted = true;
foreach ( $items as $item ) {
if ( ! is_bool( $item ) ) {
$deleted = $deleted && $this->delete_item( $item->id );
}
}
return $deleted;
}
/**
* Get all preload caches which were not accessed in the last month.
*
* @return array
*/
public function get_old_cache() : array {
// Get the database interface.
$db = $this->get_db();
// Bail if no database interface is available.
if ( empty( $db ) ) {
return [];
}
$prefixed_table_name = $db->prefix . $this->table_name;
$query = "SELECT id FROM `$prefixed_table_name` WHERE `last_accessed` <= date_sub(now(), interval 1 month)";
$rows_affected = $db->get_results( $query );
return $rows_affected;
}
/**
* Remove all completed rows one by one.
*
* @return void
*/
public function remove_all_not_accessed_rows() {
$rows = $this->get_old_cache();
foreach ( $rows as $row ) {
if ( ! is_bool( $row ) ) {
$this->delete_item( $row->id );
}
}
}
/**
* Fetch pending jobs.
*
* @param int $total total of jobs to fetch.
* @return array
*/
public function get_pending_jobs( int $total = 45 ) {
$inprogress_count = $this->query(
[
'count' => true,
'status' => 'in-progress',
'is_locked' => false,
],
false
);
if ( $inprogress_count >= $total ) {
return [];
}
$orderby = 'modified';
/**
* Filter order for preloading pending urls.
*
* @param bool $orderby order for preloading pending urls.
*
* @returns bool
*/
if ( apply_filters( 'rocket_preload_order', false ) ) {
$orderby = 'id';
}
return $this->query(
[
'number' => ( $total - $inprogress_count ),
'status' => 'pending',
'fields' => [
'id',
'url',
],
'job_id__not_in' => [
'not_in' => '',
],
'is_locked' => false,
'orderby' => $orderby,
'order' => 'asc',
]
);
}
/**
* Change the status from the task to inprogress.
*
* @param int $id id from the task.
* @return bool
*/
public function make_status_inprogress( int $id ) {
return $this->update_item(
$id,
[
'status' => 'in-progress',
]
);
}
/**
* Make the status from the task to complete.
*
* @param string $url url from the task.
* @return bool
*/
public function make_status_complete( string $url ) {
$tasks = $this->query(
[
'url' => $url,
]
);
if ( count( $tasks ) === 0 ) {
return false;
}
$task = array_pop( $tasks );
return $this->update_item(
$task->id,
[
'status' => 'completed',
]
);
}
/**
* Check if pending jobs are remaining.
*
* @return bool
*/
public function has_pending_jobs() {
$pending_count = $this->query(
[
'count' => true,
'status' => 'pending',
]
);
return 0 !== $pending_count;
}
/**
* Revert in-progress urls.
*
* @return void
*/
public function revert_in_progress() {
$in_progress_list = $this->query(
[
'status' => 'in-progress',
]
);
foreach ( $in_progress_list as $in_progress ) {
$this->update_item(
$in_progress->id,
[
'status' => 'pending',
]
);
}
}
/**
* Revert old in-progress rows
*/
public function revert_old_in_progress() {
// Get the database interface.
$db = $this->get_db();
// Bail if no database interface is available.
if ( empty( $db ) ) {
return false;
}
$prefixed_table_name = $db->prefix . $this->table_name;
$db->query( "UPDATE `$prefixed_table_name` SET status = 'pending' WHERE status = 'in-progress' AND `modified` <= date_sub(now(), interval 12 hour)" );
}
/**
* Set all rows to pending.
*/
public function set_all_to_pending() {
// Get the database interface.
$db = $this->get_db();
// Bail if no database interface is available.
if ( empty( $db ) ) {
return false;
}
$prefixed_table_name = $db->prefix . $this->table_name;
/**
* Filter condition for cleaning URLS in the database.
*
* @param string $condition condition for cleaning URLS in the database.
* @returns string
*/
$condition = apply_filters( 'rocket_preload_all_to_pending_condition', ' WHERE 1 = 1' );
$db->query( "UPDATE `$prefixed_table_name` SET status = 'pending'$condition" );
}
/**
* Check if the page is preloaded.
*
* @param string $url url from the page to check.
* @return bool
*/
public function is_preloaded( string $url ): bool {
$pending_count = $this->query(
[
'count' => true,
'status' => 'in-progress',
'url' => untrailingslashit( $url ),
]
);
return 0 !== $pending_count;
}
/**
* Check if the page is pending.
*
* @param string $url url from the page to check.
* @return bool
*/
public function is_pending( string $url ): bool {
$pending_count = $this->query(
[
'count' => true,
'status' => 'pending',
'url' => untrailingslashit( $url ),
]
);
return 0 !== $pending_count;
}
/**
* Remove all entries from the table.
*
* @return false|void
*/
public function remove_all() {
// Get the database interface.
$db = $this->get_db();
// Bail if no database interface is available.
if ( empty( $db ) ) {
return false;
}
$prefixed_table_name = $db->prefix . $this->table_name;
$db->query( "DELETE FROM `$prefixed_table_name` WHERE 1 = 1" );
}
/**
* Lock a URL.
*
* @param string $url URL to lock.
*
* @return void
*/
public function lock( string $url ) {
$this->create_or_update(
[
'url' => $url,
'is_locked' => true,
]
);
}
/**
* Unlock a URL.
*
* @param string $url URL to unlock.
*
* @return void
*/
public function unlock( string $url ) {
$this->create_or_update(
[
'url' => $url,
'is_locked' => false,
]
);
}
/**
* Check if the url is rejected.
*
* @param string $url url to check.
* @return bool
*/
protected function is_rejected( string $url ): bool {
$extensions = [
'php' => 1,
'xml' => 1,
'xsl' => 1,
'kml' => 1,
];
$extension = pathinfo( $url, PATHINFO_EXTENSION );
return $extension && isset( $extensions[ $extension ] );
}
}