PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.2.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.2.2
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 2.2.2, at inc/classes/class-imagify-db.php

581 lines 17.8 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 * @param string $id_field An ID field to match the metadata ID against in the JOIN clause.
163 * Default is the posts table `ID` field, using the `p` alias: `p.ID`.
164 * In case of "false" value or PEBKAC, fallback to the same field without alias.
165 * @param bool $matching Set to false to get a query to fetch metas NOT matching the file extensions.
166 * @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.
167 * @param string $special_join_conditions Special conditions to apply on the join.
168 *
169 * @return string
170 * @author Grégory Viguier
171 *
172 * @since 1.7
173 * @access public
174 */
175 public static function get_required_wp_metadata_join_clause( $id_field = 'p.ID', $matching = true, $test = true, $special_join_conditions = '' ) {
176 global $wpdb;
177
178 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
179 return '';
180 }
181
182 self::unlimit_joins();
183 $clause = '';
184
185 if ( ! $id_field || ! is_string( $id_field ) ) {
186 $id_field = "$wpdb->posts.ID";
187 }
188
189 $join = $matching ? 'INNER' : 'LEFT';
190
191 $first = true;
192
193 foreach ( self::get_required_wp_metadata_aliases() as $meta_name => $alias ) {
194 if ( $first ) {
195 $first = false;
196 $clause .= "
197 $join JOIN $wpdb->postmeta AS $alias
198 ON ( $id_field = $alias.post_id AND $alias.meta_key = '$meta_name' $special_join_conditions )";
199 continue;
200 }
201 $clause .= "
202 $join JOIN $wpdb->postmeta AS $alias
203 ON ( $id_field = $alias.post_id AND $alias.meta_key = '$meta_name' )";
204 }
205
206 return $clause;
207 }
208
209 /**
210 * 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.
211 * It returns an empty string if the database has no attachments without the required metadada.
212 *
213 * @since 1.7
214 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter.
215 * @access public
216 * @author Grégory Viguier
217 *
218 * @param array $args {
219 * Optional. An array of arguments.
220 *
221 * string $aliases The aliases to use for the meta values.
222 * bool $matching Set to false to get a query to fetch invalid metas.
223 * 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.
224 * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare().
225 * }.
226 * @return string A query.
227 */
228 public static function get_required_wp_metadata_where_clause( $args = array() ) {
229 static $query = array();
230
231 $args = imagify_merge_intersect( $args, array(
232 'aliases' => array(),
233 'matching' => true,
234 'test' => true,
235 'prepared' => false,
236 ) );
237
238 list( $aliases, $matching, $test, $prepared ) = array_values( $args );
239
240 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
241 return '';
242 }
243
244 if ( $aliases && is_string( $aliases ) ) {
245 $aliases = array(
246 '_wp_attached_file' => $aliases,
247 );
248 } elseif ( ! is_array( $aliases ) ) {
249 $aliases = array();
250 }
251
252 $aliases = imagify_merge_intersect( $aliases, self::get_required_wp_metadata_aliases() );
253 $key = implode( '|', $aliases ) . '|' . (int) $matching;
254
255 if ( isset( $query[ $key ] ) ) {
256 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
257 }
258
259 unset( $args['prepared'] );
260 $alias_1 = $aliases['_wp_attached_file'];
261 $alias_2 = $aliases['_wp_attachment_metadata'];
262 $extensions = self::get_extensions_where_clause( $args );
263
264 if ( $matching ) {
265 $query[ $key ] = "AND $alias_1.meta_value NOT LIKE '%://%' AND $alias_1.meta_value NOT LIKE '_:\\\\\%' $extensions";
266 } else {
267 $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 )";
268 }
269
270 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
271 }
272
273 /**
274 * Get the SQL part to be used in a WHERE clause, to get only attachments that have a valid file extensions.
275 * It returns an empty string if the database has no attachments without the required metadada.
276 *
277 * @since 1.7
278 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter.
279 * @access public
280 * @author Grégory Viguier
281 *
282 * @param array $args {
283 * Optional. An array of arguments.
284 *
285 * string $alias The alias to use for the meta value.
286 * bool $matching Set to false to get a query to fetch metas NOT matching the file extensions.
287 * 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.
288 * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare().
289 * }.
290 * @return string A query.
291 */
292 public static function get_extensions_where_clause( $args = false ) {
293 static $extensions;
294 static $query = array();
295
296 $args = imagify_merge_intersect( $args, array(
297 'alias' => array(),
298 'matching' => true,
299 'test' => true,
300 'prepared' => false,
301 ) );
302
303 list( $alias, $matching, $test, $prepared ) = array_values( $args );
304
305 if ( $test && ! imagify_has_attachments_without_required_metadata() ) {
306 return '';
307 }
308
309 if ( ! isset( $extensions ) ) {
310 $extensions = array_keys( imagify_get_mime_types() );
311 $extensions = implode( '|', $extensions );
312 $extensions = explode( '|', $extensions );
313 $extensions = array_map(function ( $ex ) {
314 return strrev( $ex );
315 }, $extensions);
316 }
317
318 if ( ! $alias ) {
319 $alias = self::get_required_wp_metadata_aliases();
320 $alias = $alias['_wp_attached_file'];
321 }
322
323 $key = $alias . '|' . (int) $matching;
324
325 if ( isset( $query[ $key ] ) ) {
326 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
327 }
328
329 $regex = '^' . implode( '\..*|^', $extensions ) . '\..*';
330
331 if ( $matching ) {
332 $query[ $key ] = "AND REVERSE (LOWER( $alias.meta_value )) REGEXP '$regex'";
333 } else {
334 $query[ $key ] = "AND REVERSE (LOWER( $alias.meta_value )) NOT REGEXP '$regex'";
335 }
336
337 return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ];
338 }
339
340 /**
341 * 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().
342 *
343 * @since 1.7
344 * @access public
345 * @author Grégory Viguier
346 *
347 * @return array An array with the meta name as key and its alias as value.
348 */
349 public static function get_required_wp_metadata_aliases() {
350 return array(
351 '_wp_attached_file' => 'imrwpmt1',
352 '_wp_attachment_metadata' => 'imrwpmt2',
353 );
354 }
355
356 /**
357 * Combine two arrays with some specific keys.
358 * We use this function to combine the result of 2 SQL queries.
359 *
360 * @since 1.6.13
361 * @access public
362 * @author Grégory Viguier
363 *
364 * @param array $keys An array of keys.
365 * @param array $values An array of arrays like array( 'id' => id, 'value' => value ).
366 * @param int $keep_keys_order Set to true to return an array ordered like $keys instead of $values.
367 * @return array The combined arrays.
368 */
369 public static function combine_query_results( $keys, $values, $keep_keys_order = false ) {
370 if ( ! $keys || ! $values ) {
371 return array();
372 }
373
374 $result = array();
375 $keys = array_flip( $keys );
376
377 foreach ( $values as $v ) {
378 if ( isset( $keys[ $v['id'] ] ) ) {
379 $result[ $v['id'] ] = $v['value'];
380 }
381 }
382
383 if ( $keep_keys_order ) {
384 $keys = array_intersect_key( $keys, $result );
385 return array_replace( $keys, $result );
386 }
387
388 return $result;
389 }
390
391 /**
392 * A helper to retrieve all values from one or several post metas, given a list of post IDs.
393 * The $wpdb cache is flushed to save memory.
394 *
395 * @since 1.6.13
396 * @access public
397 * @author Grégory Viguier
398 *
399 * @param array $metas An array of meta names like:
400 * array(
401 * 'key1' => 'meta_name_1',
402 * 'key2' => 'meta_name_2',
403 * 'key3' => 'meta_name_3',
404 * )
405 * If a key contains 'data', the results will be unserialized.
406 * @param array $ids An array of post IDs.
407 * @return array An array of arrays of results like:
408 * array(
409 * 'key1' => array( post_id_1 => 'result_1', post_id_2 => 'result_2', post_id_3 => 'result_3' ),
410 * 'key2' => array( post_id_1 => 'result_4', post_id_3 => 'result_5' ),
411 * 'key3' => array( post_id_1 => 'result_6', post_id_2 => 'result_7' ),
412 * )
413 */
414 public static function get_metas( $metas, $ids ) {
415 global $wpdb;
416
417 if ( ! $ids ) {
418 return array_fill_keys( array_keys( $metas ), array() );
419 }
420
421 $sql_ids = implode( ',', $ids );
422
423 foreach ( $metas as $result_name => $meta_name ) {
424 $metas[ $result_name ] = $wpdb->get_results( // WPCS: unprepared SQL ok.
425 "SELECT pm.post_id as id, pm.meta_value as value
426 FROM $wpdb->postmeta as pm
427 WHERE pm.meta_key = '$meta_name'
428 AND pm.post_id IN ( $sql_ids )
429 ORDER BY pm.post_id DESC",
430 ARRAY_A
431 );
432
433 $wpdb->flush();
434 $metas[ $result_name ] = self::combine_query_results( $ids, $metas[ $result_name ], true );
435
436 if ( strpos( $result_name, 'data' ) !== false ) {
437 $metas[ $result_name ] = array_map( 'maybe_unserialize', $metas[ $result_name ] );
438 }
439 }
440
441 return $metas;
442 }
443
444 /**
445 * Create/Upgrade the table in the database.
446 *
447 * @since 1.7
448 * @access public
449 * @author Grégory Viguier
450 *
451 * @param string $table_name The (prefixed) table name.
452 * @param string $schema_query Query representing the table schema.
453 * @return bool True on success. False otherwise.
454 */
455 public static function create_table( $table_name, $schema_query ) {
456 global $wpdb;
457
458 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
459
460 $wpdb->hide_errors();
461
462 $schema_query = trim( $schema_query );
463 $charset_collate = $wpdb->get_charset_collate();
464
465 dbDelta( "CREATE TABLE $table_name ($schema_query) $charset_collate;" );
466
467 return empty( $wpdb->last_error ) && self::table_exists( $table_name );
468 }
469
470 /**
471 * Tell if the given table exists.
472 *
473 * @since 1.7
474 * @access public
475 * @author Grégory Viguier
476 *
477 * @param string $table_name Full name of the table (with DB prefix).
478 * @return bool
479 */
480 public static function table_exists( $table_name ) {
481 global $wpdb;
482
483 $escaped_table = self::esc_like( $table_name );
484 $result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $escaped_table ) );
485
486 return $result === $table_name;
487 }
488
489 /**
490 * Cache transients used for optimization process locks.
491 *
492 * @since 1.9
493 * @access public
494 * @author Grégory Viguier
495 *
496 * @param string $context The context.
497 * @param array $media_ids The media IDs.
498 */
499 public static function cache_process_locks( $context, $media_ids ) {
500 global $wpdb;
501
502 if ( ! $context || ! $media_ids || wp_using_ext_object_cache() ) {
503 return;
504 }
505
506 // Sanitize the IDs.
507 $media_ids = array_filter( $media_ids );
508 $media_ids = array_unique( $media_ids );
509
510 if ( ! $media_ids ) {
511 return;
512 }
513
514 $context_instance = imagify_get_context( $context );
515 $context = $context_instance->get_name();
516 $process_class_name = imagify_get_optimization_process_class_name( $context );
517 $transient_name = sprintf( $process_class_name::LOCK_NAME, $context, '%' );
518 $is_network_wide = $context_instance->is_network_wide();
519
520 // Do 1 DB query per context (and cache results) before doing 1 get_transient() (2 DB queries) per media ID.
521 $prefix = $is_network_wide ? '_site_transient_' : '_transient_';
522
523 if ( $is_network_wide && is_multisite() ) {
524 $network_id = function_exists( 'get_current_network_id' ) ? get_current_network_id() : (int) $wpdb->siteid;
525 $cache_prefix = "$network_id:";
526 $notoptions_key = "$network_id:notoptions";
527 $cache_group = 'site-options';
528 $results = $wpdb->get_results(
529 $wpdb->prepare(
530 "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",
531 $prefix . $transient_name,
532 $prefix . 'timeout_' . $transient_name,
533 $network_id
534 ),
535 OBJECT_K
536 ); // WPCS: unprepared SQL ok.
537 } else {
538 $cache_prefix = '';
539 $notoptions_key = 'notoptions';
540 $cache_group = 'options';
541 $results = $wpdb->get_results(
542 $wpdb->prepare(
543 "SELECT option_name as name, option_value as value FROM $wpdb->options WHERE ( option_name LIKE %s OR option_name LIKE %s )",
544 $prefix . $transient_name,
545 $prefix . 'timeout_' . $transient_name
546 ),
547 OBJECT_K
548 ); // WPCS: unprepared SQL ok.
549 }
550
551 $not_exist = [];
552
553 foreach ( [ '', 'timeout_' ] as $maybe_timeout ) {
554 foreach ( $media_ids as $id ) {
555 $option_name = $prefix . $maybe_timeout . str_replace( '%', $id, $transient_name );
556
557 if ( isset( $results[ $option_name ] ) ) {
558 // Cache the value.
559 $value = $results[ $option_name ]->value;
560 $value = maybe_unserialize( $value );
561 wp_cache_set( "$cache_prefix$option_name", $value, $cache_group );
562 } else {
563 // No value.
564 $not_exist[ $option_name ] = true;
565 }
566 }
567 }
568
569 if ( ! $not_exist ) {
570 return;
571 }
572
573 // Cache the options that don't exist in the DB.
574 $notoptions = wp_cache_get( $notoptions_key, $cache_group );
575 $notoptions = is_array( $notoptions ) ? $notoptions : [];
576 $notoptions = array_merge( $notoptions, $not_exist );
577
578 wp_cache_set( $notoptions_key, $notoptions, $cache_group );
579 }
580 }
581