PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.10
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.10
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / inc / classes / class-imagify-db.php

class-imagify-db.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 1.10, at inc/classes/class-imagify-db.php

565 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
3
4 /**
5 * Imagify DB class. It reunites tools to work with the DB.
6 *
7 * @since 1.6.13
8 * @author Grégory Viguier
9 */
10 class Imagify_DB {
11
12 /**
13 * Class version.
14 *
15 * @var string
16 */
17 const VERSION = '1.0.1';
18
19 /**
20 * Some hosts limit the number of JOINs in SQL queries, but we need them.
21 *
22 * @since 1.6.13
23 * @access public
24 * @author Grégory Viguier
25 */
26 public static function unlimit_joins() {
27 global $wpdb;
28 static $done = false;
29
30 if ( $done ) {
31 return;
32 }
33
34 $done = true;
35 $query = 'SET SQL_BIG_SELECTS=1';
36
37 /**
38 * Filter the SQL query allowing to remove the limit on JOINs.
39 *
40 * @since 1.6.13
41 * @author Grégory Viguier
42 *
43 * @param string|bool $query The query. False to prevent any query.
44 */
45 $query = apply_filters( 'imagify_db_unlimit_joins_query', $query );
46
47 if ( $query && is_string( $query ) ) {
48 $wpdb->query( $query ); // WPCS: unprepared SQL ok.
49 }
50 }
51
52 /**
53 * Change an array of values into a comma separated list, ready to be used in a `IN ()` clause.
54 *
55 * @since 1.6.13
56 * @access public
57 * @author Grégory Viguier
58 *
59 * @param array $values An array of values.
60 * @return string A comma separated list of values.
61 */
62 public static function prepare_values_list( $values ) {
63 $values = esc_sql( (array) $values );
64 $values = array_map( array( __CLASS__, 'quote_string' ), $values );
65 return implode( ',', $values );
66 }
67
68 /**
69 * Wrap a value in quotes, unless it's an integer.
70 *
71 * @since 1.6.13
72 * @access public
73 * @author Grégory Viguier
74 *
75 * @param int|string $value A value.
76 * @return int|string
77 */
78 public static function quote_string( $value ) {
79 return is_numeric( $value ) ? $value : "'" . addcslashes( $value, "'" ) . "'";
80 }
81
82 /**
83 * First half of escaping for LIKE special characters % and _ before preparing for MySQL.
84 * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security.
85 *
86 * Example Prepared Statement:
87 * $wild = '%';
88 * $find = 'only 43% of planets';
89 * $like = $wild . $wpdb->esc_like( $find ) . $wild;
90 * $sql = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_content LIKE %s", $like );
91 *
92 * Example Escape Chain:
93 * $sql = esc_sql( $wpdb->esc_like( $input ) );
94 *
95 * @since 1.7
96 * @access public
97 * @author Grégory Viguier
98 *
99 * @param string $text The raw text to be escaped. The input typed by the user should have no extra or deleted slashes.
100 * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() or real_escape next.
101 */
102 public static function esc_like( $text ) {
103 global $wpdb;
104
105 if ( method_exists( $wpdb, 'esc_like' ) ) {
106 // Introduced in WP 4.0.0.
107 return $wpdb->esc_like( $text );
108 }
109
110 return addcslashes( $text, '_%\\' );
111 }
112
113 /**
114 * Get Imagify mime types, ready to be used in a `IN ()` clause.
115 *
116 * @since 1.6.13
117 * @since 1.9 Added $type parameter.
118 * @access public
119 * @author Grégory Viguier
120 *
121 * @param string $type One of 'image', 'not-image'. Any other value will return all mime types.
122 * @return string A comma separated list of mime types.
123 */
124 public static function get_mime_types( $type = null ) {
125 static $mime_types = [];
126
127 if ( empty( $type ) ) {
128 $type = 'all';
129 }
130
131 if ( ! isset( $mime_types[ $type ] ) ) {
132 $mime_types[ $type ] = self::prepare_values_list( imagify_get_mime_types( $type ) );
133 }
134
135 return $mime_types[ $type ];
136 }
137
138 /**
139 * Get post statuses related to attachments, ready to be used in a `IN ()` clause.
140 *
141 * @since 1.7
142 * @access public
143 * @author Grégory Viguier
144 *
145 * @return string A comma separated list of post statuses.
146 */
147 public static function get_post_statuses() {
148 static $statuses;
149
150 if ( ! isset( $statuses ) ) {
151 $statuses = self::prepare_values_list( imagify_get_post_statuses() );
152 }
153
154 return $statuses;
155 }
156
157 /**
158 * Get the SQL JOIN clause to use to get only attachments that have the required WP metadata.
159 * It returns an empty string if the database has no attachments without the required metadada.
160 * It also triggers Imagify_DB::unlimit_joins().
161 *
162 * @since 1.7
163 * @access public
164 * @author Grégory Viguier
165 *
166 * @param string $id_field An ID field to match the metadata ID against in the JOIN clause.
167 * Default is the posts table `ID` field, using the `p` alias: `p.ID`.
168 * In case of "false" value or PEBKAC, fallback to the same field without alias.
169 * @param bool $matching Set to false to get a query to fetch metas NOT matching the file extensions.
170 * @param bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway.
171 * @return string
172 */
173 public static function get_required_wp_metadata_join_clause( $id_field = 'p.ID', $matching = true, $test = true ) {
174 global $wpdb;
175
176 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
177 return '';
178 }
179
180 self::unlimit_joins();
181 $clause = '';
182
183 if ( ! $id_field || ! is_string( $id_field ) ) {
184 $id_field = "$wpdb->posts.ID";
185 }
186
187 $join = $matching ? 'INNER' : 'LEFT';
188
189 foreach ( self::get_required_wp_metadata_aliases() as $meta_name => $alias ) {
190 $clause .= "
191 $join JOIN $wpdb->postmeta AS $alias
192 ON ( $id_field = $alias.post_id AND $alias.meta_key = '$meta_name' )";
193 }
194
195 return $clause;
196 }
197
198 /**
199 * Get the SQL part to be used in a WHERE clause, to get only attachments that have (in)valid '_wp_attached_file' and '_wp_attachment_metadata' metadatas.
200 * It returns an empty string if the database has no attachments without the required metadada.
201 *
202 * @since 1.7
203 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter.
204 * @access public
205 * @author Grégory Viguier
206 *
207 * @param array $args {
208 * Optional. An array of arguments.
209 *
210 * string $aliases The aliases to use for the meta values.
211 * bool $matching Set to false to get a query to fetch invalid metas.
212 * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway.
213 * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare().
214 * }.
215 * @return string A query.
216 */
217 public static function get_required_wp_metadata_where_clause( $args = array() ) {
218 static $query = array();
219
220 $args = imagify_merge_intersect( $args, array(
221 'aliases' => array(),
222 'matching' => true,
223 'test' => true,
224 'prepared' => false,
225 ) );
226
227 list( $aliases, $matching, $test, $prepared ) = array_values( $args );
228
229 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
230 return '';
231 }
232
233 if ( $aliases && is_string( $aliases ) ) {
234 $aliases = array(
235 '_wp_attached_file' => $aliases,
236 );
237 } elseif ( ! is_array( $aliases ) ) {
238 $aliases = array();
239 }
240
241 $aliases = imagify_merge_intersect( $aliases, self::get_required_wp_metadata_aliases() );
242 $key = implode( '|', $aliases ) . '|' . (int) $matching;
243
244 if ( isset( $query[ $key ] ) ) {
245 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
246 }
247
248 unset( $args['prepared'] );
249 $alias_1 = $aliases['_wp_attached_file'];
250 $alias_2 = $aliases['_wp_attachment_metadata'];
251 $extensions = self::get_extensions_where_clause( $args );
252
253 if ( $matching ) {
254 $query[ $key ] = "AND $alias_1.meta_value NOT LIKE '%://%' AND $alias_1.meta_value NOT LIKE '_:\\\\\%' $extensions";
255 } else {
256 $query[ $key ] = "AND ( $alias_2.meta_value IS NULL OR $alias_1.meta_value IS NULL OR $alias_1.meta_value LIKE '%://%' OR $alias_1.meta_value LIKE '_:\\\\\%' $extensions )";
257 }
258
259 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
260 }
261
262 /**
263 * Get the SQL part to be used in a WHERE clause, to get only attachments that have a valid file extensions.
264 * It returns an empty string if the database has no attachments without the required metadada.
265 *
266 * @since 1.7
267 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter.
268 * @access public
269 * @author Grégory Viguier
270 *
271 * @param array $args {
272 * Optional. An array of arguments.
273 *
274 * string $alias The alias to use for the meta value.
275 * bool $matching Set to false to get a query to fetch metas NOT matching the file extensions.
276 * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway.
277 * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare().
278 * }.
279 * @return string A query.
280 */
281 public static function get_extensions_where_clause( $args = false ) {
282 static $extensions;
283 static $query = array();
284
285 $args = imagify_merge_intersect( $args, array(
286 'alias' => array(),
287 'matching' => true,
288 'test' => true,
289 'prepared' => false,
290 ) );
291
292 list( $alias, $matching, $test, $prepared ) = array_values( $args );
293
294 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
295 return '';
296 }
297
298 if ( ! isset( $extensions ) ) {
299 $extensions = array_keys( imagify_get_mime_types() );
300 $extensions = implode( '|', $extensions );
301 $extensions = explode( '|', $extensions );
302 }
303
304 if ( ! $alias ) {
305 $alias = self::get_required_wp_metadata_aliases();
306 $alias = $alias['_wp_attached_file'];
307 }
308
309 $key = $alias . '|' . (int) $matching;
310
311 if ( isset( $query[ $key ] ) ) {
312 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
313 }
314
315 if ( $matching ) {
316 $query[ $key ] = "AND ( LOWER( $alias.meta_value ) LIKE '%." . implode( "' OR LOWER( $alias.meta_value ) LIKE '%.", $extensions ) . "' )";
317 } else {
318 $query[ $key ] = "OR ( LOWER( $alias.meta_value ) NOT LIKE '%." . implode( "' AND LOWER( $alias.meta_value ) NOT LIKE '%.", $extensions ) . "' )";
319 }
320
321 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
322 }
323
324 /**
325 * Get the aliases used for the metas in self::get_required_wp_metadata_join_clause(), self::get_required_wp_metadata_where_clause(), and self::get_extensions_where_clause().
326 *
327 * @since 1.7
328 * @access public
329 * @author Grégory Viguier
330 *
331 * @return array An array with the meta name as key and its alias as value.
332 */
333 public static function get_required_wp_metadata_aliases() {
334 return array(
335 '_wp_attached_file' => 'imrwpmt1',
336 '_wp_attachment_metadata' => 'imrwpmt2',
337 );
338 }
339
340 /**
341 * Combine two arrays with some specific keys.
342 * We use this function to combine the result of 2 SQL queries.
343 *
344 * @since 1.6.13
345 * @access public
346 * @author Grégory Viguier
347 *
348 * @param array $keys An array of keys.
349 * @param array $values An array of arrays like array( 'id' => id, 'value' => value ).
350 * @param int $keep_keys_order Set to true to return an array ordered like $keys instead of $values.
351 * @return array The combined arrays.
352 */
353 public static function combine_query_results( $keys, $values, $keep_keys_order = false ) {
354 if ( ! $keys || ! $values ) {
355 return array();
356 }
357
358 $result = array();
359 $keys = array_flip( $keys );
360
361 foreach ( $values as $v ) {
362 if ( isset( $keys[ $v['id'] ] ) ) {
363 $result[ $v['id'] ] = $v['value'];
364 }
365 }
366
367 if ( $keep_keys_order ) {
368 $keys = array_intersect_key( $keys, $result );
369 return array_replace( $keys, $result );
370 }
371
372 return $result;
373 }
374
375 /**
376 * A helper to retrieve all values from one or several post metas, given a list of post IDs.
377 * The $wpdb cache is flushed to save memory.
378 *
379 * @since 1.6.13
380 * @access public
381 * @author Grégory Viguier
382 *
383 * @param array $metas An array of meta names like:
384 * array(
385 * 'key1' => 'meta_name_1',
386 * 'key2' => 'meta_name_2',
387 * 'key3' => 'meta_name_3',
388 * )
389 * If a key contains 'data', the results will be unserialized.
390 * @param array $ids An array of post IDs.
391 * @return array An array of arrays of results like:
392 * array(
393 * 'key1' => array( post_id_1 => 'result_1', post_id_2 => 'result_2', post_id_3 => 'result_3' ),
394 * 'key2' => array( post_id_1 => 'result_4', post_id_3 => 'result_5' ),
395 * 'key3' => array( post_id_1 => 'result_6', post_id_2 => 'result_7' ),
396 * )
397 */
398 public static function get_metas( $metas, $ids ) {
399 global $wpdb;
400
401 if ( ! $ids ) {
402 return array_fill_keys( array_keys( $metas ), array() );
403 }
404
405 $sql_ids = implode( ',', $ids );
406
407 foreach ( $metas as $result_name => $meta_name ) {
408 $metas[ $result_name ] = $wpdb->get_results( // WPCS: unprepared SQL ok.
409 "SELECT pm.post_id as id, pm.meta_value as value
410 FROM $wpdb->postmeta as pm
411 WHERE pm.meta_key = '$meta_name'
412 AND pm.post_id IN ( $sql_ids )
413 ORDER BY pm.post_id DESC",
414 ARRAY_A
415 );
416
417 $wpdb->flush();
418 $metas[ $result_name ] = self::combine_query_results( $ids, $metas[ $result_name ], true );
419
420 if ( strpos( $result_name, 'data' ) !== false ) {
421 $metas[ $result_name ] = array_map( 'maybe_unserialize', $metas[ $result_name ] );
422 }
423 }
424
425 return $metas;
426 }
427
428 /**
429 * Create/Upgrade the table in the database.
430 *
431 * @since 1.7
432 * @access public
433 * @author Grégory Viguier
434 *
435 * @param string $table_name The (prefixed) table name.
436 * @param string $schema_query Query representing the table schema.
437 * @return bool True on success. False otherwise.
438 */
439 public static function create_table( $table_name, $schema_query ) {
440 global $wpdb;
441
442 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
443
444 $wpdb->hide_errors();
445
446 $schema_query = trim( $schema_query );
447 $charset_collate = $wpdb->get_charset_collate();
448
449 dbDelta( "CREATE TABLE $table_name ($schema_query) $charset_collate;" );
450
451 return empty( $wpdb->last_error ) && self::table_exists( $table_name );
452 }
453
454 /**
455 * Tell if the given table exists.
456 *
457 * @since 1.7
458 * @access public
459 * @author Grégory Viguier
460 *
461 * @param string $table_name Full name of the table (with DB prefix).
462 * @return bool
463 */
464 public static function table_exists( $table_name ) {
465 global $wpdb;
466
467 $escaped_table = self::esc_like( $table_name );
468 $result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $escaped_table ) );
469
470 return $result === $table_name;
471 }
472
473 /**
474 * Cache transients used for optimization process locks.
475 *
476 * @since 1.9
477 * @access public
478 * @author Grégory Viguier
479 *
480 * @param string $context The context.
481 * @param array $media_ids The media IDs.
482 */
483 public static function cache_process_locks( $context, $media_ids ) {
484 global $wpdb;
485
486 if ( ! $context || ! $media_ids || wp_using_ext_object_cache() ) {
487 return;
488 }
489
490 // Sanitize the IDs.
491 $media_ids = array_filter( $media_ids );
492 $media_ids = array_unique( $media_ids );
493
494 if ( ! $media_ids ) {
495 return;
496 }
497
498 $context_instance = imagify_get_context( $context );
499 $context = $context_instance->get_name();
500 $process_class_name = imagify_get_optimization_process_class_name( $context );
501 $transient_name = sprintf( $process_class_name::LOCK_NAME, $context, '%' );
502 $is_network_wide = $context_instance->is_network_wide();
503
504 // Do 1 DB query per context (and cache results) before doing 1 get_transient() (2 DB queries) per media ID.
505 $prefix = $is_network_wide ? '_site_transient_' : '_transient_';
506
507 if ( $is_network_wide && is_multisite() ) {
508 $network_id = function_exists( 'get_current_network_id' ) ? get_current_network_id() : (int) $wpdb->siteid;
509 $cache_prefix = "$network_id:";
510 $notoptions_key = "$network_id:notoptions";
511 $cache_group = 'site-options';
512 $results = $wpdb->get_results(
513 $wpdb->prepare(
514 "SELECT meta_key as name, meta_value as value FROM $wpdb->sitemeta WHERE ( meta_key LIKE %s OR meta_key LIKE %s ) AND site_id = %d",
515 $prefix . $transient_name,
516 $prefix . 'timeout_' . $transient_name,
517 $network_id
518 ),
519 OBJECT_K
520 ); // WPCS: unprepared SQL ok.
521 } else {
522 $cache_prefix = '';
523 $notoptions_key = 'notoptions';
524 $cache_group = 'options';
525 $results = $wpdb->get_results(
526 $wpdb->prepare(
527 "SELECT option_name as name, option_value as value FROM $wpdb->options WHERE ( option_name LIKE %s OR option_name LIKE %s )",
528 $prefix . $transient_name,
529 $prefix . 'timeout_' . $transient_name
530 ),
531 OBJECT_K
532 ); // WPCS: unprepared SQL ok.
533 }
534
535 $not_exist = [];
536
537 foreach ( [ '', 'timeout_' ] as $maybe_timeout ) {
538 foreach ( $media_ids as $id ) {
539 $option_name = $prefix . $maybe_timeout . str_replace( '%', $id, $transient_name );
540
541 if ( isset( $results[ $option_name ] ) ) {
542 // Cache the value.
543 $value = $results[ $option_name ]->value;
544 $value = maybe_unserialize( $value );
545 wp_cache_set( "$cache_prefix$option_name", $value, $cache_group );
546 } else {
547 // No value.
548 $not_exist[ $option_name ] = true;
549 }
550 }
551 }
552
553 if ( ! $not_exist ) {
554 return;
555 }
556
557 // Cache the options that don't exist in the DB.
558 $notoptions = wp_cache_get( $notoptions_key, $cache_group );
559 $notoptions = is_array( $notoptions ) ? $notoptions : [];
560 $notoptions = array_merge( $notoptions, $not_exist );
561
562 wp_cache_set( $notoptions_key, $notoptions, $cache_group );
563 }
564 }
565