PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
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
← All changes | classes/Bulk/WP.php +256 -136 1.10 → trunk View file →
@@ -1,24 +1,20 @@
1 1 <?php
2 2 namespace Imagify\Bulk;
3 3
4 -defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
4 +use Imagify_DB;
5 5
6 6 /**
7 7 * Class to use for bulk for WP attachments.
8 8 *
9 - * @since 1.9
10 - * @author Grégory Viguier
9 + * @since 1.9
11 10 */
12 11 class WP extends AbstractBulk {
13 -
14 12 /**
15 13 * Context "short name".
16 14 *
17 - * @var string
15 + * @var string
18 16 * @since 1.9
19 - * @access protected
20 - * @author Grégory Viguier
21 17 */
22 18 protected $context = 'wp';
23 19
24 20 /**
@@ -23,57 +19,60 @@
23 19
24 20 /**
25 21 * Get all unoptimized media ids.
26 22 *
27 - * @since 1.9
28 - * @access public
29 - * @author Grégory Viguier
23 + * @since 1.9
30 24 *
31 - * @param int $optimization_level The optimization level.
32 - * @return array A list of unoptimized media. Array keys are media IDs prefixed with an underscore character, array values are the main file’s URL.
25 + * @param int $optimization_level The optimization level.
26 + *
27 + * @return array A list of unoptimized media IDs.
33 28 */
34 29 public function get_unoptimized_media_ids( $optimization_level ) {
35 30 global $wpdb;
36 31
37 - @set_time_limit( 0 );
32 + $this->set_no_time_limit();
38 33
39 - $mime_types = \Imagify_DB::get_mime_types();
40 - $statuses = \Imagify_DB::get_post_statuses();
41 - $nodata_join = \Imagify_DB::get_required_wp_metadata_join_clause();
42 - $nodata_where = \Imagify_DB::get_required_wp_metadata_where_clause( [
43 - 'prepared' => true,
44 - ] );
45 - $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok.
46 - "
47 - SELECT DISTINCT p.ID
48 - FROM $wpdb->posts AS p
49 - $nodata_join
50 - LEFT JOIN $wpdb->postmeta AS mt1
51 - ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
52 - LEFT JOIN $wpdb->postmeta AS mt2
53 - ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_optimization_level' )
54 - WHERE
55 - p.post_mime_type IN ( $mime_types )
56 - AND (
57 - mt1.meta_value = 'error'
58 - OR
59 - mt2.meta_value != %d
60 - OR
61 - mt2.post_id IS NULL
62 - )
63 - AND p.post_type = 'attachment'
64 - AND p.post_status IN ( $statuses )
65 - $nodata_where
66 - ORDER BY
67 - CASE mt1.meta_value
68 - WHEN 'already_optimized' THEN 2
69 - ELSE 1
70 - END ASC,
71 - p.ID DESC
72 - LIMIT 0, %d",
73 - $optimization_level,
74 - imagify_get_unoptimized_attachment_limit()
75 - ) );
34 + $mime_types = Imagify_DB::get_mime_types();
35 + $statuses = Imagify_DB::get_post_statuses();
36 + $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause();
37 + $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(
38 + [
39 + 'prepared' => true,
40 + ]
41 + );
42 + $ids = $wpdb->get_col(
43 + $wpdb->prepare( // WPCS: unprepared SQL ok.
44 + "
45 + SELECT DISTINCT p.ID
46 + FROM $wpdb->posts AS p
47 + $nodata_join
48 + LEFT JOIN $wpdb->postmeta AS mt1
49 + ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
50 + LEFT JOIN $wpdb->postmeta AS mt2
51 + ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_optimization_level' )
52 + WHERE
53 + p.post_mime_type IN ( $mime_types )
54 + AND (
55 + mt1.meta_value = 'error'
56 + OR
57 + mt2.meta_value != %d
58 + OR
59 + mt2.post_id IS NULL
60 + )
61 + AND p.post_type = 'attachment'
62 + AND p.post_status IN ( $statuses )
63 + $nodata_where
64 + ORDER BY
65 + CASE mt1.meta_value
66 + WHEN 'already_optimized' THEN 2
67 + ELSE 1
68 + END ASC,
69 + p.ID DESC
70 + LIMIT 0, %d",
71 + $optimization_level,
72 + imagify_get_unoptimized_attachment_limit()
73 + )
74 + );
76 75
77 76 $wpdb->flush();
78 77 unset( $mime_types );
79 78 $ids = array_filter( array_map( 'absint', $ids ) );
@@ -81,22 +80,27 @@
81 80 if ( ! $ids ) {
82 81 return [];
83 82 }
84 83
85 - $metas = \Imagify_DB::get_metas( [
86 - // Get attachments filename.
87 - 'filenames' => '_wp_attached_file',
88 - // Get attachments data.
89 - 'data' => '_imagify_data',
90 - // Get attachments optimization level.
91 - 'optimization_levels' => '_imagify_optimization_level',
92 - // Get attachments status.
93 - 'statuses' => '_imagify_status',
94 - ], $ids );
84 + $metas = Imagify_DB::get_metas(
85 + [
86 + // Get attachments filename.
87 + 'filenames' => '_wp_attached_file',
88 + // Get attachments data.
89 + 'data' => '_imagify_data',
90 + // Get attachments optimization level.
91 + 'optimization_levels' => '_imagify_optimization_level',
92 + // Get attachments status.
93 + 'statuses' => '_imagify_status',
94 + // Get attachments metadata, to detect a WP-scaled original.
95 + 'metadata' => '_wp_attachment_metadata',
96 + ],
97 + $ids
98 + );
95 99
96 100 // First run.
97 101 foreach ( $ids as $i => $id ) {
98 - $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false;
102 + $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false;
99 103 $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false;
100 104 $attachment_error = '';
101 105
102 106 if ( isset( $metas['data'][ $id ]['sizes']['full']['error'] ) ) {
@@ -129,12 +133,11 @@
129 133
130 134 $ids = array_values( $ids );
131 135
132 136 /**
133 - * Triggered before testing for file existence.
137 + * Fires before testing for file existence.
134 138 *
135 - * @since 1.6.7
136 - * @author Grégory Viguier
139 + * @since 1.6.7
137 140 *
138 141 * @param array $ids An array of attachment IDs.
139 142 * @param array $metas An array of the data fetched from the database.
140 143 * @param int $optimization_level The optimization level that will be used for the optimization.
@@ -154,10 +157,11 @@
154 157 if ( ! $file_path || ! $this->filesystem->exists( $file_path ) ) {
155 158 continue;
156 159 }
157 160
158 - $attachment_backup_path = get_imagify_attachment_backup_path( $file_path );
159 - $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false;
161 + $original_path = $this->get_original_file_path_from_metadata( $file_path, isset( $metas['metadata'][ $id ] ) ? $metas['metadata'][ $id ] : null );
162 + $attachment_backup_path = get_imagify_attachment_backup_path( $original_path );
163 + $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false;
160 164 $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false;
161 165
162 166 // Don't try to re-optimize if there is no backup file.
163 167 if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) {
@@ -163,22 +167,105 @@
163 167 if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) {
164 168 continue;
165 169 }
166 170
167 - $data[ '_' . $id ] = esc_url( get_imagify_attachment_url( $metas['filenames'][ $id ] ) );
168 - } // End foreach().
171 + $data[] = $id;
172 + }
169 173
170 174 return $data;
171 175 }
172 176
173 177 /**
174 - * Get ids of all optimized media without WebP versions.
178 + * Get all optimized media IDs that have a backup file available for restore.
175 179 *
176 - * @since 1.9
177 - * @since 1.9.5 The method doesn't return the IDs directly anymore.
178 - * @access public
179 - * @author Grégory Viguier
180 + * @return array A list of media IDs.
181 + */
182 + public function get_optimized_media_ids(): array {
183 + global $wpdb;
184 +
185 + $this->set_no_time_limit();
186 +
187 + $mime_types = Imagify_DB::get_mime_types();
188 + $statuses = Imagify_DB::get_post_statuses();
189 + $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause();
190 + $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(
191 + [
192 + 'prepared' => true,
193 + ]
194 + );
195 + $ids = $wpdb->get_col(
196 + $wpdb->prepare( // WPCS: unprepared SQL ok.
197 + "
198 + SELECT DISTINCT p.ID
199 + FROM $wpdb->posts AS p
200 + $nodata_join
201 + INNER JOIN $wpdb->postmeta AS mt1
202 + ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
203 + WHERE
204 + p.post_mime_type IN ( $mime_types )
205 + AND mt1.meta_value IN ( 'success', 'already_optimized' )
206 + AND p.post_type = 'attachment'
207 + AND p.post_status IN ( $statuses )
208 + $nodata_where
209 + ORDER BY p.ID DESC
210 + LIMIT 0, %d",
211 + imagify_get_unoptimized_attachment_limit()
212 + )
213 + );
214 +
215 + $wpdb->flush();
216 + unset( $mime_types, $statuses );
217 + $ids = array_filter( array_map( 'absint', $ids ) );
218 +
219 + if ( ! $ids ) {
220 + return [];
221 + }
222 +
223 + $metas = Imagify_DB::get_metas(
224 + [
225 + // Get attachments filename.
226 + 'filenames' => '_wp_attached_file',
227 + // Get attachments metadata, to detect a WP-scaled original.
228 + 'metadata' => '_wp_attachment_metadata',
229 + ],
230 + $ids
231 + );
232 +
233 + $data = [];
234 +
235 + foreach ( $ids as $id ) {
236 + if ( empty( $metas['filenames'][ $id ] ) ) {
237 + // Problem.
238 + continue;
239 + }
240 +
241 + $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] );
242 +
243 + if ( ! $file_path ) {
244 + continue;
245 + }
246 +
247 + $original_path = $this->get_original_file_path_from_metadata( $file_path, isset( $metas['metadata'][ $id ] ) ? $metas['metadata'][ $id ] : null );
248 + $attachment_backup_path = get_imagify_attachment_backup_path( $original_path );
249 +
250 + if ( ! $this->filesystem->exists( $attachment_backup_path ) ) {
251 + // No backup, cannot restore.
252 + continue;
253 + }
254 +
255 + $data[] = $id;
256 + }
257 +
258 + return $data;
259 + }
260 +
261 + /**
262 + * Get ids of all optimized media without Next gen versions.
180 263 *
264 + * @since 2.2
265 + *
266 + * @param string $format Format we are looking for. (webp|avif).
267 + *
181 268 * @return array {
182 269 * @type array $ids A list of media IDs.
183 270 * @type array $errors {
184 271 * @type array $no_file_path A list of media IDs.
@@ -185,22 +272,47 @@
185 272 * @type array $no_backup A list of media IDs.
186 273 * }
187 274 * }
188 275 */
189 - public function get_optimized_media_ids_without_webp() {
276 + public function get_optimized_media_ids_without_format( $format ) {
190 277 global $wpdb;
191 278
192 - @set_time_limit( 0 );
279 + $this->set_no_time_limit();
193 280
194 - $mime_types = \Imagify_DB::get_mime_types( 'image' );
195 - $statuses = \Imagify_DB::get_post_statuses();
196 - $nodata_join = \Imagify_DB::get_required_wp_metadata_join_clause();
197 - $nodata_where = \Imagify_DB::get_required_wp_metadata_where_clause( [
198 - 'prepared' => true,
199 - ] );
200 - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' );
201 - $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok.
202 - "
281 + $mime_types = Imagify_DB::get_mime_types( 'image' );
282 +
283 + // Remove single quotes and explode string into array.
284 + $mime_types_array = explode( ',', str_replace( "'", '', $mime_types ) );
285 +
286 + // Iterate over array and check if string contains input.
287 + foreach ( $mime_types_array as $item ) {
288 + if ( strpos( $item, $format ) !== false ) {
289 + $mime = $item;
290 + break;
291 + }
292 + }
293 + if ( ! isset( $mime ) && empty( $mime ) ) {
294 + $mime = 'image/webp';
295 + }
296 + $mime = trim( $mime );
297 + $mime_types = str_replace( [ ", '" . $mime . "'", ",'" . $mime . "'" ], '', $mime_types );
298 + $statuses = Imagify_DB::get_post_statuses();
299 + $nodata_join = '';
300 + $nodata_where = '';
301 + if ( ! imagify_has_attachments_without_required_metadata() ) {
302 + $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause();
303 + $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(
304 + [
305 + 'prepared' => true,
306 + ]
307 + );
308 + }
309 +
310 + $nextgen_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::' . strtoupper( $format ) . '_SUFFIX' );
311 +
312 + $ids = $wpdb->get_col(
313 + $wpdb->prepare( // WPCS: unprepared SQL ok.
314 + "
203 315 SELECT p.ID
204 316 FROM $wpdb->posts AS p
205 317 $nodata_join
206 318 LEFT JOIN $wpdb->postmeta AS mt1
@@ -205,26 +317,38 @@
205 317 $nodata_join
206 318 LEFT JOIN $wpdb->postmeta AS mt1
207 319 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
208 320 LEFT JOIN $wpdb->postmeta AS mt2
209 - ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' )
321 + ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' )
210 322 WHERE
211 323 p.post_mime_type IN ( $mime_types )
212 324 AND ( mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' )
213 325 AND mt2.meta_value NOT LIKE %s
326 + AND mt2.meta_value NOT LIKE %s
214 327 AND p.post_type = 'attachment'
215 328 AND p.post_status IN ( $statuses )
216 329 $nodata_where
217 330 ORDER BY p.ID DESC
218 331 LIMIT 0, %d",
219 - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%',
220 - imagify_get_unoptimized_attachment_limit()
221 - ) );
332 + '%' . $wpdb->esc_like( $nextgen_suffix . '";a:4:{s:7:"success";b:1;' ) . '%',
333 + /**
334 + * Second predicate: skip media the API permanently refused to convert (the next-gen file
335 + * would be heavier than the original, or the file is already compressed). Those are stored
336 + * as `<size><suffix>";a:3:{s:15:"permanent_error";b:1;s:7:"success";b:0;s:5:"error";…`.
337 + * Transient failures (network, quota, timeout) serialize as a 2-element array without the
338 + * `permanent_error` key, so they keep being retried. Matching serialized data with LIKE is
339 + * fragile: the key order written in Optimization\Data\WP must not change.
340 + */
341 + '%' . $wpdb->esc_like( $nextgen_suffix . '";a:3:{s:15:"permanent_error";b:1;' ) . '%',
342 + imagify_get_unoptimized_attachment_limit()
343 + )
344 + );
222 345
223 346 $wpdb->flush();
224 - unset( $mime_types, $statuses, $webp_suffix );
347 + unset( $mime_types, $statuses, $nextgen_suffix, $mime );
225 348
226 - $ids = array_filter( array_map( 'absint', $ids ) );
349 + $ids = array_filter( array_map( 'absint', $ids ) );
350 +
227 351 $data = [
228 352 'ids' => [],
229 353 'errors' => [
230 354 'no_file_path' => [],
@@ -235,24 +359,28 @@
235 359 if ( ! $ids ) {
236 360 return $data;
237 361 }
238 362
239 - $metas = \Imagify_DB::get_metas( [
240 - // Get attachments filename.
241 - 'filenames' => '_wp_attached_file',
242 - ], $ids );
363 + $metas = Imagify_DB::get_metas(
364 + [
365 + // Get attachments filename.
366 + 'filenames' => '_wp_attached_file',
367 + // Get attachments metadata, to detect a WP-scaled original.
368 + 'metadata' => '_wp_attachment_metadata',
369 + ],
370 + $ids
371 + );
243 372
244 373 /**
245 - * Triggered before testing for file existence.
374 + * Fires before testing for file existence.
246 375 *
247 - * @since 1.9
248 - * @author Grégory Viguier
376 + * @since 1.9
249 377 *
250 378 * @param array $ids An array of attachment IDs.
251 379 * @param array $metas An array of the data fetched from the database.
252 380 * @param string $context The context.
253 381 */
254 - do_action( 'imagify_bulk_generate_webp_before_file_existence_tests', $ids, $metas, 'wp' );
382 + do_action( 'imagify_bulk_generate_nextgen_before_file_existence_tests', $ids, $metas, 'wp' );
255 383
256 384 foreach ( $ids as $i => $id ) {
257 385 if ( empty( $metas['filenames'][ $id ] ) ) {
258 386 // Problem. Should not happen, thanks to the wpdb query.
@@ -267,10 +395,17 @@
267 395 $data['errors']['no_file_path'][] = $id;
268 396 continue;
269 397 }
270 398
271 - $backup_path = get_imagify_attachment_backup_path( $file_path );
399 + // Skip files whose extension already matches the target format
400 + // (e.g. a .webp file stored with incorrect post_mime_type).
401 + if ( strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) ) === $format ) {
402 + continue;
403 + }
272 404
405 + $original_path = $this->get_original_file_path_from_metadata( $file_path, isset( $metas['metadata'][ $id ] ) ? $metas['metadata'][ $id ] : null );
406 + $backup_path = get_imagify_attachment_backup_path( $original_path );
407 +
273 408 if ( ! $this->filesystem->exists( $backup_path ) ) {
274 409 // No backup, no WebP.
275 410 $data['errors']['no_backup'][] = $id;
276 411 continue;
@@ -276,59 +411,44 @@
276 411 continue;
277 412 }
278 413
279 414 $data['ids'][] = $id;
280 - } // End foreach().
415 + }
281 416
282 417 return $data;
283 418 }
284 419
285 420 /**
286 - * Tell if there are optimized media without WebP versions.
421 + * Get the original (pre-scaling) file path from batched `_wp_attachment_metadata`.
287 422 *
288 - * @since 1.9
289 - * @access public
290 - * @author Grégory Viguier
423 + * When WordPress scales down an image on upload, `_wp_attached_file` (and therefore
424 + * `$file_path`) points to the scaled copy, but Imagify backups are always stored at the
425 + * path derived from the true original file (see `Imagify\Media\WP::get_raw_backup_path()`,
426 + * which relies on `wp_get_original_image_path()`). This mirrors that derivation from data
427 + * already fetched in a single batched query, instead of instantiating a media object (and
428 + * firing its own uncached meta reads) for every attachment in the loop.
291 429 *
292 - * @return int The number of media.
430 + * @since 2.4
431 + *
432 + * @param string $file_path Path derived from `_wp_attached_file`.
433 + * @param mixed $metadata The unserialized `_wp_attachment_metadata` value for this attachment,
434 + * or null/garbage if it couldn't be fetched or decoded.
435 + *
436 + * @return string The original-derived file path, or $file_path unchanged when there is no
437 + * scaled original to account for.
293 438 */
294 - public function has_optimized_media_without_webp() {
295 - global $wpdb;
439 + private function get_original_file_path_from_metadata( $file_path, $metadata ) {
440 + if ( ! is_array( $metadata ) || empty( $metadata['original_image'] ) || ! is_string( $metadata['original_image'] ) ) {
441 + return $file_path;
442 + }
296 443
297 - $mime_types = \Imagify_DB::get_mime_types( 'image' );
298 - $statuses = \Imagify_DB::get_post_statuses();
299 - $nodata_join = \Imagify_DB::get_required_wp_metadata_join_clause();
300 - $nodata_where = \Imagify_DB::get_required_wp_metadata_where_clause( [
301 - 'prepared' => true,
302 - ] );
303 - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' );
304 -
305 - return (int) $wpdb->get_var( $wpdb->prepare( // WPCS: unprepared SQL ok.
306 - "
307 - SELECT COUNT(p.ID)
308 - FROM $wpdb->posts AS p
309 - $nodata_join
310 - LEFT JOIN $wpdb->postmeta AS mt1
311 - ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
312 - LEFT JOIN $wpdb->postmeta AS mt2
313 - ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' )
314 - WHERE
315 - p.post_mime_type IN ( $mime_types )
316 - AND ( mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' )
317 - AND mt2.meta_value NOT LIKE %s
318 - AND p.post_type = 'attachment'
319 - AND p.post_status IN ( $statuses )
320 - $nodata_where",
321 - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%'
322 - ) );
444 + return trailingslashit( dirname( $file_path ) ) . $metadata['original_image'];
323 445 }
324 446
325 447 /**
326 448 * Get the context data.
327 449 *
328 - * @since 1.9
329 - * @access public
330 - * @author Grégory Viguier
450 + * @since 1.9
331 451 *
332 452 * @return array {
333 453 * The formated data.
334 454 *