Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.25% covered (danger)
31.25%
30 / 96
20.00% covered (danger)
20.00%
2 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Queue_Storage_Options
31.25% covered (danger)
31.25%
30 / 96
20.00% covered (danger)
20.00%
2 / 10
194.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 insert_item
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 fetch_items
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 fetch_items_by_ids
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 clear_queue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_item_count
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_lag
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
5.26
 add_all
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 get_items_ids_with_size
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 delete_items_by_ids
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * The class responsible for storing Queue events in the `wp_options` table.
4 *
5 * Used by class Queue.
6 *
7 * @see \Automattic\Jetpack\Sync\Queue
8 *
9 * @package automattic/jetpack-sync
10 */
11
12namespace Automattic\Jetpack\Sync\Queue;
13
14/**
15 * `wp_options` storage backend for the Queue.
16 */
17class Queue_Storage_Options {
18    /**
19     * What queue is this instance responsible for.
20     *
21     * @var string
22     */
23    public $queue_id = '';
24
25    /**
26     * Class constructor.
27     *
28     * @param string $queue_id The queue name this instance will be responsible for.
29     *
30     * @throws \Exception If queue name was not provided.
31     */
32    public function __construct( $queue_id ) {
33        if ( empty( $queue_id ) ) {
34            // TODO what should we return here or throw an exception?
35            throw new \Exception( 'Invalid queue_id provided' );
36        }
37
38        // TODO validate the value maybe?
39        $this->queue_id = $queue_id;
40    }
41
42    /**
43     * Insert an item in the queue.
44     *
45     * @param string $item_id The item ID.
46     * @param string $item Serialized item data.
47     *
48     * @return bool If the item was added.
49     */
50    public function insert_item( $item_id, $item ) {
51        global $wpdb;
52
53        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
54        $rows_added = $wpdb->query(
55            $wpdb->prepare(
56                "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
57                $item_id,
58                $item,
59                'no'
60            )
61        );
62
63        return ( 0 !== $rows_added );
64    }
65
66    /**
67     * Fetch items from the queue.
68     *
69     * @param int|null $item_count How many items to fetch from the queue.
70     *                             Null for no limit.
71     * @param string   $order      Sort direction for the items. Accepts 'ASC' or 'DESC'.
72     *                             Any other value will be treated as 'ASC'.
73     *
74     * @return array|object|null Array of result objects on success, or null on failure.
75     */
76    public function fetch_items( $item_count, $order = 'ASC' ) {
77        global $wpdb;
78
79        $order = 'DESC' === $order ? 'DESC' : 'ASC';
80
81        $sql_order = "ORDER BY option_name {$order}";
82
83        $sql = "SELECT option_name AS id, option_value AS value
84                FROM $wpdb->options
85                WHERE option_name LIKE %s
86                {$sql_order}";
87
88        $params = array( "jpsq_{$this->queue_id}-%" );
89
90        if ( $item_count ) {
91            $sql     .= ' LIMIT %d';
92            $params[] = $item_count;
93        }
94
95        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
96        $items = $wpdb->get_results(
97            $wpdb->prepare( $sql, $params ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
98            OBJECT
99        );
100
101        return $items;
102    }
103
104    /**
105     * Fetches items with specific IDs from the Queue.
106     *
107     * @param array $items_ids Items IDs to fetch from the queue.
108     *
109     * @return \stdClass[]|null
110     */
111    public function fetch_items_by_ids( $items_ids ) {
112        global $wpdb;
113
114        // return early if $items_ids is empty or not an array.
115        if ( empty( $items_ids ) || ! is_array( $items_ids ) ) {
116            return array();
117        }
118
119        $ids_placeholders = implode( ', ', array_fill( 0, count( $items_ids ), '%s' ) );
120
121        $query_with_placeholders = "SELECT option_name AS id, option_value AS value
122                FROM $wpdb->options
123                WHERE option_name IN ( $ids_placeholders )";
124
125        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
126        $items = $wpdb->get_results(
127            $wpdb->prepare(
128                $query_with_placeholders, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
129                $items_ids
130            ),
131            OBJECT
132        );
133
134        return $items;
135    }
136
137    /**
138     * Clear out the queue.
139     *
140     * @return bool|int|\mysqli_result|resource|null
141     */
142    public function clear_queue() {
143        global $wpdb;
144
145        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
146        return $wpdb->query(
147            $wpdb->prepare(
148                "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
149                "jpsq_{$this->queue_id}-%"
150            )
151        );
152    }
153
154    /**
155     * Check how many items are in the queue.
156     *
157     * @return int
158     */
159    public function get_item_count() {
160        global $wpdb;
161
162        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
163        return (int) $wpdb->get_var(
164            $wpdb->prepare(
165                "SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s",
166                "jpsq_{$this->queue_id}-%"
167            )
168        );
169    }
170
171    /**
172     * Return the lag amount for the queue.
173     *
174     * @param float|int|null $now A timestamp to use as starting point when calculating the lag.
175     *
176     * @return float|int The lag amount.
177     */
178    public function get_lag( $now = null ) {
179        global $wpdb;
180
181        // TODO replace with peek and a flag to fetch only the name.
182        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
183        $first_item_name = $wpdb->get_var(
184            $wpdb->prepare(
185                "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
186                "jpsq_{$this->queue_id}-%"
187            )
188        );
189
190        if ( ! $first_item_name ) {
191            return 0;
192        }
193
194        if ( null === $now ) {
195            $now = microtime( true );
196        }
197
198        // Break apart the item name to get the timestamp.
199        $matches = null;
200        if ( preg_match( '/^jpsq_' . $this->queue_id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
201            return $now - (float) $matches[1];
202        } else {
203            return 0;
204        }
205    }
206
207    /**
208     * Add multiple items to the queue at once.
209     *
210     * @param array  $items Array of items to add.
211     * @param string $id_prefix Prefix to use for all the items.
212     *
213     * @return bool|int|\mysqli_result|resource|null
214     */
215    public function add_all( $items, $id_prefix ) {
216        global $wpdb;
217
218        $query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
219
220        $rows        = array();
221        $count_items = count( $items );
222        for ( $i = 0; $i < $count_items; ++$i ) {
223            // skip empty items.
224            if ( empty( $items[ $i ] ) ) {
225                continue;
226            }
227            try {
228                $option_name  = esc_sql( $id_prefix . '-' . $i );
229                $option_value = esc_sql( serialize( $items[ $i ] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
230                $rows[]       = "('$option_name', '$option_value', 'no')";
231            } catch ( \Exception $e ) {
232                // Item cannot be serialized so skip.
233                continue;
234            }
235        }
236
237        $rows_added = $wpdb->query( $query . implode( ',', $rows ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
238
239        return $rows_added;
240    }
241
242    /**
243     * Return $max_count items from the queue, including their value string length.
244     *
245     * @param int $max_count How many items to fetch from the queue.
246     *
247     * @return object[]|null
248     */
249    public function get_items_ids_with_size( $max_count ) {
250        global $wpdb;
251
252        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
253        return $wpdb->get_results(
254            $wpdb->prepare(
255                "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",
256                "jpsq_{$this->queue_id}-%",
257                $max_count
258            ),
259            OBJECT
260        );
261    }
262
263    /**
264     * Delete items with specific IDs from the queue.
265     *
266     * @param array $ids IDs of the items to remove from the queue.
267     *
268     * @return bool|int|\mysqli_result|resource|null
269     */
270    public function delete_items_by_ids( $ids ) {
271        global $wpdb;
272
273        if ( ! is_array( $ids ) || empty( $ids ) ) {
274            return false;
275        }
276
277        // TODO check if it's working properly - no need to delete all options in the table if the params are not right
278        $ids_placeholders = implode( ', ', array_fill( 0, count( $ids ), '%s' ) );
279
280        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
281        return $wpdb->query(
282            $wpdb->prepare(
283            /**
284             * Ignoring the linting warning, as there's still no placeholder replacement for DB field name,
285             * in this case this is `$ids_placeholders`, as we're preparing them above and are a dynamic count.
286             */
287                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
288                "DELETE FROM {$wpdb->options} WHERE option_name IN ( $ids_placeholders )",
289                $ids
290            )
291        );
292    }
293}