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
imagify / classes / Bulk / WP.php

WP.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at classes/Bulk/WP.php

474 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\Bulk;
3
4 use Imagify_DB;
5
6 /**
7 * Class to use for bulk for WP attachments.
8 *
9 * @since 1.9
10 */
11 class WP extends AbstractBulk {
12 /**
13 * Context "short name".
14 *
15 * @var string
16 * @since 1.9
17 */
18 protected $context = 'wp';
19
20 /**
21 * Get all unoptimized media ids.
22 *
23 * @since 1.9
24 *
25 * @param int $optimization_level The optimization level.
26 *
27 * @return array A list of unoptimized media IDs.
28 */
29 public function get_unoptimized_media_ids( $optimization_level ) {
30 global $wpdb;
31
32 $this->set_no_time_limit();
33
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 );
75
76 $wpdb->flush();
77 unset( $mime_types );
78 $ids = array_filter( array_map( 'absint', $ids ) );
79
80 if ( ! $ids ) {
81 return [];
82 }
83
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 );
99
100 // First run.
101 foreach ( $ids as $i => $id ) {
102 $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false;
103 $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false;
104 $attachment_error = '';
105
106 if ( isset( $metas['data'][ $id ]['sizes']['full']['error'] ) ) {
107 $attachment_error = $metas['data'][ $id ]['sizes']['full']['error'];
108 }
109
110 // Don't try to re-optimize if the optimization level is still the same.
111 if ( $optimization_level === $attachment_optimization_level && is_string( $attachment_error ) ) {
112 unset( $ids[ $i ] );
113 continue;
114 }
115
116 // Don't try to re-optimize images already compressed.
117 if ( 'already_optimized' === $attachment_status && $attachment_optimization_level >= $optimization_level ) {
118 unset( $ids[ $i ] );
119 continue;
120 }
121
122 $attachment_error = trim( $attachment_error );
123
124 // Don't try to re-optimize images with an empty error message.
125 if ( 'error' === $attachment_status && empty( $attachment_error ) ) {
126 unset( $ids[ $i ] );
127 }
128 }
129
130 if ( ! $ids ) {
131 return [];
132 }
133
134 $ids = array_values( $ids );
135
136 /**
137 * Fires before testing for file existence.
138 *
139 * @since 1.6.7
140 *
141 * @param array $ids An array of attachment IDs.
142 * @param array $metas An array of the data fetched from the database.
143 * @param int $optimization_level The optimization level that will be used for the optimization.
144 */
145 do_action( 'imagify_bulk_optimize_before_file_existence_tests', $ids, $metas, $optimization_level );
146
147 $data = [];
148
149 foreach ( $ids as $i => $id ) {
150 if ( empty( $metas['filenames'][ $id ] ) ) {
151 // Problem.
152 continue;
153 }
154
155 $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] );
156
157 if ( ! $file_path || ! $this->filesystem->exists( $file_path ) ) {
158 continue;
159 }
160
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;
164 $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false;
165
166 // Don't try to re-optimize if there is no backup file.
167 if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) {
168 continue;
169 }
170
171 $data[] = $id;
172 }
173
174 return $data;
175 }
176
177 /**
178 * Get all optimized media IDs that have a backup file available for restore.
179 *
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.
263 *
264 * @since 2.2
265 *
266 * @param string $format Format we are looking for. (webp|avif).
267 *
268 * @return array {
269 * @type array $ids A list of media IDs.
270 * @type array $errors {
271 * @type array $no_file_path A list of media IDs.
272 * @type array $no_backup A list of media IDs.
273 * }
274 * }
275 */
276 public function get_optimized_media_ids_without_format( $format ) {
277 global $wpdb;
278
279 $this->set_no_time_limit();
280
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 "
315 SELECT p.ID
316 FROM $wpdb->posts AS p
317 $nodata_join
318 LEFT JOIN $wpdb->postmeta AS mt1
319 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' )
320 LEFT JOIN $wpdb->postmeta AS mt2
321 ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' )
322 WHERE
323 p.post_mime_type IN ( $mime_types )
324 AND ( mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' )
325 AND mt2.meta_value NOT LIKE %s
326 AND mt2.meta_value NOT LIKE %s
327 AND p.post_type = 'attachment'
328 AND p.post_status IN ( $statuses )
329 $nodata_where
330 ORDER BY p.ID DESC
331 LIMIT 0, %d",
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 );
345
346 $wpdb->flush();
347 unset( $mime_types, $statuses, $nextgen_suffix, $mime );
348
349 $ids = array_filter( array_map( 'absint', $ids ) );
350
351 $data = [
352 'ids' => [],
353 'errors' => [
354 'no_file_path' => [],
355 'no_backup' => [],
356 ],
357 ];
358
359 if ( ! $ids ) {
360 return $data;
361 }
362
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 );
372
373 /**
374 * Fires before testing for file existence.
375 *
376 * @since 1.9
377 *
378 * @param array $ids An array of attachment IDs.
379 * @param array $metas An array of the data fetched from the database.
380 * @param string $context The context.
381 */
382 do_action( 'imagify_bulk_generate_nextgen_before_file_existence_tests', $ids, $metas, 'wp' );
383
384 foreach ( $ids as $i => $id ) {
385 if ( empty( $metas['filenames'][ $id ] ) ) {
386 // Problem. Should not happen, thanks to the wpdb query.
387 $data['errors']['no_file_path'][] = $id;
388 continue;
389 }
390
391 $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] );
392
393 if ( ! $file_path ) {
394 // Main file not found.
395 $data['errors']['no_file_path'][] = $id;
396 continue;
397 }
398
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 }
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
408 if ( ! $this->filesystem->exists( $backup_path ) ) {
409 // No backup, no WebP.
410 $data['errors']['no_backup'][] = $id;
411 continue;
412 }
413
414 $data['ids'][] = $id;
415 }
416
417 return $data;
418 }
419
420 /**
421 * Get the original (pre-scaling) file path from batched `_wp_attachment_metadata`.
422 *
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.
429 *
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.
438 */
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 }
443
444 return trailingslashit( dirname( $file_path ) ) . $metadata['original_image'];
445 }
446
447 /**
448 * Get the context data.
449 *
450 * @since 1.9
451 *
452 * @return array {
453 * The formated data.
454 *
455 * @type string $count-optimized Number of media optimized.
456 * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors.
457 * @type string $optimized-size Optimized filesize.
458 * @type string $original-size Original filesize.
459 * }
460 */
461 public function get_context_data() {
462 $total_saving_data = imagify_count_saving_data();
463 $data = [
464 'count-optimized' => imagify_count_optimized_attachments(),
465 'count-errors' => imagify_count_error_attachments(),
466 'optimized-size' => $total_saving_data['optimized_size'],
467 'original-size' => $total_saving_data['original_size'],
468 'errors_url' => get_imagify_admin_url( 'folder-errors', $this->context ),
469 ];
470
471 return $this->format_context_data( $data );
472 }
473 }
474