<?php
/**
* The class responsible for storing Queue events in the `wp_options` table.
*
* Used by class Queue.
*
* @see \Automattic\Jetpack\Sync\Queue
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Queue;
/**
* `wp_options` storage backend for the Queue.
*/
class Queue_Storage_Options {
/**
* What queue is this instance responsible for.
*
* @var string
*/
public $queue_id = '';
/**
* Class constructor.
*
* @param string $queue_id The queue name this instance will be responsible for.
*
* @throws \Exception If queue name was not provided.
*/
public function __construct( $queue_id ) {
if ( empty( $queue_id ) ) {
// TODO what should we return here or throw an exception?
throw new \Exception( 'Invalid queue_id provided' );
}
// TODO validate the value maybe?
$this->queue_id = $queue_id;
}
/**
* Insert an item in the queue.
*
* @param string $item_id The item ID.
* @param string $item Serialized item data.
*
* @return bool If the item was added.
*/
public function insert_item( $item_id, $item ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
$rows_added = $wpdb->query(
$wpdb->prepare(
"INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
$item_id,
$item,
'no'
)
);
return ( 0 !== $rows_added );
}
/**
* Fetch items from the queue.
*
* @param int|null $item_count How many items to fetch from the queue.
* Null for no limit.
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
* Any other value will be treated as 'ASC'.
*
* @return array|object|null Array of result objects on success, or null on failure.
*/
public function fetch_items( $item_count, $order = 'ASC' ) {
global $wpdb;
$order = 'DESC' === $order ? 'DESC' : 'ASC';
$sql_order = "ORDER BY option_name {$order}";
$sql = "SELECT option_name AS id, option_value AS value
FROM $wpdb->options
WHERE option_name LIKE %s
{$sql_order}";
$params = array( "jpsq_{$this->queue_id}-%" );
if ( $item_count ) {
$sql .= ' LIMIT %d';
$params[] = $item_count;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$items = $wpdb->get_results(
$wpdb->prepare( $sql, $params ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
OBJECT
);
return $items;
}
/**
* Fetches items with specific IDs from the Queue.
*
* @param array $items_ids Items IDs to fetch from the queue.
*
* @return \stdClass[]|null
*/
public function fetch_items_by_ids( $items_ids ) {
global $wpdb;
// return early if $items_ids is empty or not an array.
if ( empty( $items_ids ) || ! is_array( $items_ids ) ) {
return array();
}
$ids_placeholders = implode( ', ', array_fill( 0, count( $items_ids ), '%s' ) );
$query_with_placeholders = "SELECT option_name AS id, option_value AS value
FROM $wpdb->options
WHERE option_name IN ( $ids_placeholders )";
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
$items = $wpdb->get_results(
$wpdb->prepare(
$query_with_placeholders, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$items_ids
),
OBJECT
);
return $items;
}
/**
* Clear out the queue.
*
* @return bool|int|\mysqli_result|resource|null
*/
public function clear_queue() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
return $wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
"jpsq_{$this->queue_id}-%"
)
);
}
/**
* Check how many items are in the queue.
*
* @return int
*/
public function get_item_count() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s",
"jpsq_{$this->queue_id}-%"
)
);
}
/**
* Return the lag amount for the queue.
*
* @param float|int|null $now A timestamp to use as starting point when calculating the lag.
*
* @return float|int The lag amount.
*/
public function get_lag( $now = null ) {
global $wpdb;
// TODO replace with peek and a flag to fetch only the name.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
$first_item_name = $wpdb->get_var(
$wpdb->prepare(
"SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
"jpsq_{$this->queue_id}-%"
)
);
if ( ! $first_item_name ) {
return 0;
}
if ( null === $now ) {
$now = microtime( true );
}
// Break apart the item name to get the timestamp.
$matches = null;
if ( preg_match( '/^jpsq_' . $this->queue_id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
return $now - (float) $matches[1];
} else {
return 0;
}
}
/**
* Add multiple items to the queue at once.
*
* @param array $items Array of items to add.
* @param string $id_prefix Prefix to use for all the items.
*
* @return bool|int|\mysqli_result|resource|null
*/
public function add_all( $items, $id_prefix ) {
global $wpdb;
$query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
$rows = array();
$count_items = count( $items );
for ( $i = 0; $i < $count_items; ++$i ) {
// skip empty items.
if ( empty( $items[ $i ] ) ) {
continue;
}
try {
$option_name = esc_sql( $id_prefix . '-' . $i );
$option_value = esc_sql( serialize( $items[ $i ] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$rows[] = "('$option_name', '$option_value', 'no')";
} catch ( \Exception $e ) {
// Item cannot be serialized so skip.
continue;
}
}
$rows_added = $wpdb->query( $query . implode( ',', $rows ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
return $rows_added;
}
/**
* Return $max_count items from the queue, including their value string length.
*
* @param int $max_count How many items to fetch from the queue.
*
* @return object[]|null
*/
public function get_items_ids_with_size( $max_count ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
return $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
"jpsq_{$this->queue_id}-%",
$max_count
),
OBJECT
);
}
/**
* Delete items with specific IDs from the queue.
*
* @param array $ids IDs of the items to remove from the queue.
*
* @return bool|int|\mysqli_result|resource|null
*/
public function delete_items_by_ids( $ids ) {
global $wpdb;
if ( ! is_array( $ids ) || empty( $ids ) ) {
return false;
}
// TODO check if it's working properly - no need to delete all options in the table if the params are not right
$ids_placeholders = implode( ', ', array_fill( 0, count( $ids ), '%s' ) );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
return $wpdb->query(
$wpdb->prepare(
/**
* Ignoring the linting warning, as there's still no placeholder replacement for DB field name,
* in this case this is `$ids_placeholders`, as we're preparing them above and are a dynamic count.
*/
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
"DELETE FROM {$wpdb->options} WHERE option_name IN ( $ids_placeholders )",
$ids
)
);
}
}