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 / Job / MediaOptimization.php

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

504 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\Job;
3
4 use Imagify\Optimization\Process\ProcessInterface;
5 use Imagify\Traits\InstanceGetterTrait;
6 use WP_Error;
7
8 /**
9 * Job class for media optimization.
10 *
11 * @since 1.9
12 */
13 final class MediaOptimization extends \Imagify_Abstract_Background_Process {
14 use InstanceGetterTrait;
15
16 /**
17 * Batch-key segment used to mark a batch as "urgent" (high priority).
18 * Shared by generate_key() (write path) and get_batches() (read path) so both
19 * always agree on the exact same string shape.
20 *
21 * @var string
22 * @since 2.3.2
23 */
24 private const URGENT_KEY_SEGMENT = 'batch_urgent';
25
26 /**
27 * Background process: the action to perform.
28 *
29 * @var string
30 * @since 1.9
31 */
32 protected $action = 'optimize_media';
33
34 /**
35 * The optimization process instance.
36 *
37 * @var ?ProcessInterface
38 * @since 1.9
39 */
40 protected $optimization_process;
41
42 /**
43 * Handle job logic.
44 *
45 * @since 1.9
46 *
47 * @param array $item {
48 * The data to use for this job.
49 *
50 * @type string $task The task to perform. Optional: set it only if you know what you’re doing.
51 * @type int $id The media ID.
52 * @type array $sizes An array of media sizes (strings). Use "full" for the size of the main file.
53 * @type array $sizes_done Used internally to store the media sizes that have been processed.
54 * @type int $optimization_level The optimization level. Null for the level set in the settings.
55 * @type string $process_class The name of the process class. The class must implement ProcessInterface.
56 * @type array $data {
57 * Can be used to pass any data. Keep it short, don’t forget it will be stored in the database.
58 * It should contain the following though:
59 *
60 * @type string $hook_suffix Suffix used to trigger hooks before and after optimization. Should be always provided.
61 * @type bool $delete_backup True to delete the backup file after the optimization process. This is used when a temporary backup of the original file has been created, but backup option is disabled. Default is false.
62 * @type bool $bulk True when this optimization was triggered by a bulk run (see Bulk::force_optimize()). Forwarded verbatim to the 'imagify_before_*'/'imagify_after_*' hook callbacks below. Default is false.
63 * }
64 * }
65 * @return array|bool The modified item to put back in the queue. False to remove the item from the queue.
66 */
67 protected function task( $item ) {
68 $item = $this->validate_item( $item );
69
70 if ( ! $item ) {
71 // Not valid.
72 return false;
73 }
74
75 // Launch the task.
76 $method = 'task_' . $item['task'];
77 $item = $this->$method( $item );
78
79 if ( $item['task'] ) {
80 // Next task.
81 return $item;
82 }
83
84 // End of the queue.
85 $this->optimization_process->unlock();
86 return false;
87 }
88
89 /**
90 * Generate the batch key.
91 * Overrides the vendored method to give priority ("urgent") batches a distinct
92 * key shape, so get_batches() can order them first.
93 *
94 * @since 2.3.2
95 *
96 * @param int $length Optional, length of the string to return. Default 64.
97 * @param string $key Optional, string with a prefix key. Default 'batch'.
98 * @return string
99 */
100 protected function generate_key( $length = 64, $key = 'batch' ) {
101 if ( 'batch' === $key && $this->has_priority_item_queued() ) {
102 return parent::generate_key( $length, self::URGENT_KEY_SEGMENT );
103 }
104
105 return parent::generate_key( $length, $key );
106 }
107
108 /**
109 * Tell if any of the not-yet-persisted queued items carries a truthy 'priority' key.
110 *
111 * A single truthy item marks the whole batch urgent, which only makes sense because
112 * a batch is always a singleton at this point: generate_key() is called from
113 * push_to_queue(), immediately followed by save(), one item at a time. If several
114 * items were ever pushed to $this->data before save() is called, they would all end
115 * up sharing the same "urgent" key as soon as one of them requests priority.
116 *
117 * @since 2.3.2
118 *
119 * @return bool
120 */
121 private function has_priority_item_queued(): bool {
122 if ( empty( $this->data ) || ! is_array( $this->data ) ) {
123 return false;
124 }
125
126 foreach ( $this->data as $item ) {
127 if ( is_array( $item ) && ! empty( $item['priority'] ) ) {
128 return true;
129 }
130 }
131
132 return false;
133 }
134
135 /**
136 * Get batches.
137 * Overrides the vendored method to return "urgent" (priority) batches before
138 * normal ones, each tier still ordered FIFO (by option/meta ID) internally.
139 *
140 * @since 2.3.2
141 *
142 * @param int $limit Number of batches to return, defaults to all.
143 * @return array of stdClass
144 */
145 public function get_batches( $limit = 0 ) {
146 global $wpdb;
147
148 if ( empty( $limit ) || ! is_int( $limit ) ) {
149 $limit = 0;
150 }
151
152 $table = $wpdb->options;
153 $column = 'option_name';
154 $key_column = 'option_id';
155 $value_column = 'option_value';
156
157 if ( is_multisite() ) {
158 $table = $wpdb->sitemeta;
159 $column = 'meta_key';
160 $key_column = 'meta_id';
161 $value_column = 'meta_value';
162 }
163
164 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
165 $urgent_key = $wpdb->esc_like( $this->identifier . '_' . self::URGENT_KEY_SEGMENT . '_' ) . '%';
166
167 $sql = '
168 SELECT *
169 FROM ' . $table . '
170 WHERE ' . $column . ' LIKE %s
171 ORDER BY ( ' . $column . ' LIKE %s ) DESC, ' . $key_column . ' ASC
172 ';
173
174 $args = [ $key, $urgent_key ];
175
176 if ( ! empty( $limit ) ) {
177 $sql .= ' LIMIT %d';
178 $args[] = $limit;
179 }
180
181 $items = $wpdb->get_results(
182 $wpdb->prepare(
183 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
184 $args
185 )
186 );
187
188 $batches = [];
189
190 if ( ! empty( $items ) ) {
191 $allowed_classes = $this->allowed_batch_data_classes;
192
193 $batches = array_map(
194 static function ( $item ) use ( $column, $value_column, $allowed_classes ) {
195 $batch = new \stdClass();
196 $batch->key = $item->{$column};
197 $batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes );
198
199 return $batch;
200 },
201 $items
202 );
203 }
204
205 return $batches;
206 }
207
208 /**
209 * Trigger hooks before the optimization job.
210 *
211 * @since 1.9
212 *
213 * @param array $item See $this->task().
214 * @return array The item.
215 */
216 private function task_before( $item ) {
217 if ( ! empty( $item['error'] ) && is_wp_error( $item['error'] ) ) {
218 $wp_error = $item['error'];
219 } else {
220 $wp_error = new WP_Error();
221 }
222
223 /**
224 * Fires before optimizing a media.
225 * Any number of files can be optimized, not necessarily all of the media files.
226 * If you want to return a WP_Error, use the existing $wp_error object.
227 *
228 * @since 1.9
229 *
230 * @param array|WP_Error $data New data to pass along the item. A WP_Error object to stop the process.
231 * @param WP_Error $wp_error Add errors to this object and return it to stop the process.
232 * @param ProcessInterface $process The optimization process.
233 * @param array $item The item being processed. See $this->task().
234 */
235 $data = apply_filters( 'imagify_before_optimize', [], $wp_error, $this->optimization_process, $item ); // @phpstan-ignore-line
236
237 if ( is_wp_error( $data ) ) {
238 $wp_error = $data;
239 } elseif ( $data && is_array( $data ) ) {
240 $item['data'] = array_merge( $data, $item['data'] );
241 }
242
243 if ( $wp_error->get_error_codes() ) {
244 // Don't optimize if there is an error.
245 $item['task'] = 'after';
246 $item['error'] = $wp_error;
247 return $item;
248 }
249
250 if ( empty( $item['data']['hook_suffix'] ) ) {
251 // Next task.
252 $item['task'] = 'optimize';
253 return $item;
254 }
255
256 $hook_suffix = $item['data']['hook_suffix'];
257
258 /**
259 * Fires before optimizing a media.
260 * Any number of files can be optimized, not necessarily all of the media files.
261 * If you want to return a WP_Error, use the existing $wp_error object.
262 *
263 * @since 1.9
264 *
265 * @param array|WP_Error $data New data to pass along the item. A WP_Error object to stop the process.
266 * @param WP_Error $wp_error Add errors to this object and return it to stop the process.
267 * @param ProcessInterface $process The optimization process.
268 * @param array $item The item being processed. See $this->task().
269 */
270 $data = apply_filters( "imagify_before_{$hook_suffix}", [], $wp_error, $this->optimization_process, $item ); // @phpstan-ignore-line
271
272 if ( is_wp_error( $data ) ) {
273 $wp_error = $data;
274 } elseif ( $data && is_array( $data ) ) {
275 $item['data'] = array_merge( $data, $item['data'] );
276 }
277
278 if ( $wp_error->get_error_codes() ) {
279 // Don't optimize if there is an error.
280 $item['task'] = 'after';
281 $item['error'] = $wp_error;
282 return $item;
283 }
284
285 // Next task.
286 $item['task'] = 'optimize';
287
288 return $item;
289 }
290
291 /**
292 * Start the optimization job.
293 *
294 * @since 1.9
295 *
296 * @param array $item See $this->task().
297 * @return array The item.
298 */
299 private function task_optimize( $item ) {
300 // Determine which size we're going to optimize. The 'full' size must be optimized before any other.
301 if ( in_array( 'full', $item['sizes'], true ) ) {
302 $current_size = 'full';
303 $item['sizes'] = array_diff( $item['sizes'], [ 'full' ] );
304 } else {
305 $current_size = array_shift( $item['sizes'] );
306 }
307
308 $item['sizes_done'][] = $current_size;
309
310 // Optimize the file.
311 $data = $this->optimization_process->optimize_size( $current_size, $item['optimization_level'] );
312
313 if ( 'full' === $current_size ) {
314 if ( is_wp_error( $data ) ) {
315 // Don't go further if there is an error.
316 $item['sizes'] = [];
317 $item['error'] = $data;
318
319 } elseif ( 'already_optimized' === $data['status'] ) {
320 // Status is "already_optimized", try to create next-gen versions only.
321 $item['sizes'] = array_filter( $item['sizes'], [ $this->optimization_process, 'is_size_next_gen' ] );
322
323 } elseif ( 'success' !== $data['status'] ) {
324 // Don't go further if the full size has not the "success" status.
325 $item['sizes'] = [];
326 }
327 }
328
329 if ( ! $item['sizes'] ) {
330 // No more files to optimize.
331 $item['task'] = 'after';
332 }
333
334 // Optimize the next file or go to the next task.
335 return $item;
336 }
337
338 /**
339 * Trigger hooks after the optimization job.
340 *
341 * @since 1.9
342 *
343 * @param array $item See $this->task().
344 * @return array The item.
345 */
346 private function task_after( $item ) {
347 if ( ! empty( $item['data']['delete_backup'] ) ) {
348 $this->optimization_process->delete_backup();
349 }
350
351 /**
352 * Fires after optimizing a media.
353 * Any number of files can be optimized, not necessarily all of the media files.
354 *
355 * @since 1.9
356 *
357 * @param ProcessInterface $process The optimization process.
358 * @param array $item The item being processed. See $this->task().
359 */
360 do_action( 'imagify_after_optimize', $this->optimization_process, $item );
361
362 if ( empty( $item['data']['hook_suffix'] ) ) {
363 $item['task'] = false;
364 return $item;
365 }
366
367 $hook_suffix = $item['data']['hook_suffix'];
368
369 /**
370 * Fires after optimizing a media.
371 * Any number of files can be optimized, not necessarily all of the media files.
372 *
373 * @since 1.9
374 *
375 * @param ProcessInterface $process The optimization process.
376 * @param array $item The item being processed. See $this->task().
377 */
378 do_action( "imagify_after_{$hook_suffix}", $this->optimization_process, $item );
379
380 $item['task'] = false;
381 return $item;
382 }
383
384 /**
385 * Validate an item.
386 * On success, the property $this->optimization_process is set.
387 *
388 * @since 1.9
389 *
390 * @param array $item See $this->task().
391 * @return array|bool The item. False if invalid.
392 */
393 protected function validate_item( $item ) {
394 $this->optimization_process = null;
395
396 $default = [
397 'task' => '',
398 'id' => 0,
399 'sizes' => [],
400 'sizes_done' => [],
401 'optimization_level' => null,
402 'process_class' => '',
403 'data' => [],
404 ];
405
406 $item = imagify_merge_intersect( $item, $default );
407
408 // Validate some types first.
409 if ( ! is_array( $item['sizes'] ) ) {
410 return false;
411 }
412
413 if ( isset( $item['error'] ) && ! is_wp_error( $item['error'] ) ) {
414 unset( $item['error'] );
415 }
416
417 if ( isset( $item['data']['hook_suffix'] ) && ! is_string( $item['data']['hook_suffix'] ) ) {
418 unset( $item['data']['hook_suffix'] );
419 }
420
421 $item['id'] = (int) $item['id'];
422 $item['optimization_level'] = $this->sanitize_optimization_level( $item['optimization_level'] );
423
424 if ( ! $item['id'] || ! $item['process_class'] ) {
425 return false;
426 }
427
428 // Process.
429 $item['process_class'] = '\\' . ltrim( $item['process_class'], '\\' );
430
431 if ( ! class_exists( $item['process_class'] ) ) {
432 return false;
433 }
434
435 $process = $this->get_process( $item );
436
437 if ( ! $process ) {
438 return false;
439 }
440
441 $this->optimization_process = $process;
442
443 // Validate the current task.
444 if ( empty( $item['task'] ) ) {
445 $item['task'] = 'before';
446 }
447
448 if ( ! method_exists( $this, 'task_' . $item['task'] ) ) {
449 return false;
450 }
451
452 if ( ! $item['sizes'] && 'after' !== $item['task'] ) {
453 // Allow to have no sizes, but only after the optimize task is complete.
454 return false;
455 }
456
457 if ( ! isset( $item['sizes_done'] ) || ! is_array( $item['sizes_done'] ) ) {
458 $item['sizes_done'] = [];
459 }
460
461 return $item;
462 }
463
464 /**
465 * Get the process instance.
466 *
467 * @since 1.9
468 *
469 * @param array $item See $this->task().
470 * @return ProcessInterface|bool The instance object on success. False on failure.
471 */
472 protected function get_process( $item ) {
473 $process_class = $item['process_class'];
474 $process = new $process_class( $item['id'] );
475
476 if ( ! $process instanceof ProcessInterface || ! $process->is_valid() ) {
477 return false;
478 }
479
480 return $process;
481 }
482
483 /**
484 * Sanitize and validate an optimization level.
485 * If not provided (false, null), fallback to the level set in the plugin's settings.
486 *
487 * @since 1.9
488 *
489 * @param mixed $optimization_level The optimization level.
490 * @return int
491 */
492 protected function sanitize_optimization_level( $optimization_level ) {
493 if ( ! is_numeric( $optimization_level ) ) {
494 if ( get_imagify_option( 'lossless' ) ) {
495 return 0;
496 }
497
498 return get_imagify_option( 'optimization_level' );
499 }
500
501 return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level );
502 }
503 }
504