PluginProbe
Image Optimization – Compress Images and Convert to WebP or AVIF / 1.6.6
Image Optimization – Compress Images and Convert to WebP or AVIF v1.6.6
1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 All 32 releases
image-optimization / modules / optimization / classes / bulk-optimization-controller.php

bulk-optimization-controller.php in Image Optimization – Compress Images and Convert to WebP or AVIF 1.6.6, at modules/optimization/classes/bulk-optimization-controller.php

451 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimization\Modules\Optimization\Classes;
4
5 use ImageOptimization\Classes\Async_Operation\{
6 Async_Operation,
7 Async_Operation_Hook,
8 Async_Operation_Queue,
9 Exceptions\Async_Operation_Exception,
10 Queries\Image_Optimization_Operation_Query
11 };
12 use ImageOptimization\Classes\Image\{
13 Exceptions\Invalid_Image_Exception,
14 Image,
15 Image_Meta,
16 Image_Optimization_Error_Type,
17 Image_Query_Builder,
18 Image_Status,
19 WP_Image_Meta
20 };
21 use ImageOptimization\Classes\File_System\Exceptions\File_System_Operation_Error;
22 use ImageOptimization\Classes\File_System\File_System;
23 use ImageOptimization\Classes\Logger;
24 use ImageOptimization\Classes\Utils;
25 use ImageOptimization\Modules\Oauth\Classes\Data;
26 use ImageOptimization\Classes\Exceptions\Quota_Exceeded_Error;
27 use ImageOptimization\Modules\Optimization\Classes\Exceptions\Bulk_Token_Obtaining_Error;
28 use ImageOptimization\Modules\Optimization\Components\Exceptions\Bulk_Optimization_Token_Not_Found_Error;
29 use ImageOptimization\Modules\Stats\Classes\Optimization_Stats;
30
31 use ImageOptimization\Plugin;
32
33 use Throwable;
34
35 if ( ! defined( 'ABSPATH' ) ) {
36 exit; // Exit if accessed directly.
37 }
38
39 class Bulk_Optimization_Controller {
40 private const OBTAIN_TOKEN_ENDPOINT = 'image/bulk-token';
41
42 public static function reschedule_bulk_optimization() {
43 self::delete_bulk_optimization();
44 self::find_images_and_schedule_optimization();
45 }
46
47 public static function reschedule_bulk_reoptimization() {
48 self::delete_bulk_reoptimization();
49 self::find_optimized_images_and_schedule_reoptimization();
50 }
51
52 /**
53 * Cancels pending bulk optimization operations.
54 *
55 * @return void
56 * @throws Async_Operation_Exception
57 */
58 public static function delete_bulk_optimization(): void {
59 $query = ( new Image_Optimization_Operation_Query() )
60 ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
61 // It's risky to cancel in-progress operations at that point, so we cancel only the pending ones.
62 ->set_status( Async_Operation::OPERATION_STATUS_PENDING )
63 ->set_limit( -1 );
64
65 $operations = Async_Operation::get( $query );
66
67 foreach ( $operations as $operation ) {
68 $image_id = $operation->get_args()['attachment_id'];
69
70 Async_Operation::remove( [ $operation->get_id() ] );
71
72 ( new Image_Meta( $image_id ) )->delete();
73 }
74 }
75
76 /**
77 * Cancels pending bulk re-optimization operations.
78 *
79 * @return void
80 * @throws Async_Operation_Exception
81 */
82 public static function delete_bulk_reoptimization(): void {
83 $query = ( new Image_Optimization_Operation_Query() )
84 ->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK )
85 // It's risky to cancel in-progress operations at that point, so we cancel only the pending ones.
86 ->set_status( Async_Operation::OPERATION_STATUS_PENDING )
87 ->set_limit( -1 );
88
89 $operations = Async_Operation::get( $query );
90
91 foreach ( $operations as $operation ) {
92 $image_id = $operation->get_args()['attachment_id'];
93
94 Async_Operation::remove( [ $operation->get_id() ] );
95
96 ( new Image_Meta( $image_id ) )->delete();
97 }
98 }
99
100 /**
101 * Looks for all non-optimized images and creates a bulk operation for each of them.
102 * Also, obtains bulk token and passes it to a newly created operation.
103 *
104 * @return void
105 *
106 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
107 */
108 public static function find_images_and_schedule_optimization(): void {
109 $images = self::find_images(
110 ( new Image_Query_Builder() )
111 ->return_not_optimized_images(),
112 true
113 );
114
115 if ( ! $images['total_images_count'] ) {
116 return;
117 }
118
119 $operation_id = wp_generate_password( 10, false );
120
121 try {
122 $bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
123 self::set_bulk_operation_token( $operation_id, $bulk_token );
124 } catch ( Bulk_Token_Obtaining_Error $e ) {
125 $bulk_token = null;
126 }
127
128 foreach ( $images['attachments_in_quota'] as $attachment_id ) {
129 $meta = new Image_Meta( $attachment_id );
130
131 if ( null === $bulk_token ) {
132 $meta
133 ->set_status( Image_Status::OPTIMIZATION_FAILED )
134 ->save();
135
136 continue;
137 }
138
139 try {
140 Async_Operation::create(
141 Async_Operation_Hook::OPTIMIZE_BULK,
142 [
143 'attachment_id' => $attachment_id,
144 'operation_id' => $operation_id,
145 ],
146 Async_Operation_Queue::OPTIMIZE
147 );
148
149 $meta
150 ->set_status( Image_Status::OPTIMIZATION_IN_PROGRESS )
151 ->save();
152 } catch ( Async_Operation_Exception $aoe ) {
153 $meta
154 ->set_status( Image_Status::OPTIMIZATION_FAILED )
155 ->save();
156
157 continue;
158 }
159 }
160 }
161
162 /**
163 * Looks for already optimized images with backups and creates a bulk operation for each of them.
164 * Also, obtains bulk token and passes it to a newly created operation.
165 *
166 * @return void
167 *
168 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
169 */
170 public static function find_optimized_images_and_schedule_reoptimization(): void {
171 $images = self::find_images(
172 ( new Image_Query_Builder() )
173 ->return_optimized_images()
174 );
175
176 if ( ! $images['total_images_count'] ) {
177 return;
178 }
179
180 $operation_id = wp_generate_password( 10, false );
181
182 try {
183 $bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
184 self::set_bulk_operation_token( $operation_id, $bulk_token );
185 } catch ( Bulk_Token_Obtaining_Error $e ) {
186 $bulk_token = null;
187 }
188
189 foreach ( $images['attachments_in_quota'] as $attachment_id ) {
190 $meta = new Image_Meta( $attachment_id );
191
192 if ( null === $bulk_token ) {
193 $meta
194 ->set_status( Image_Status::REOPTIMIZING_FAILED )
195 ->save();
196
197 continue;
198 }
199
200 try {
201 Async_Operation::create(
202 Async_Operation_Hook::REOPTIMIZE_BULK,
203 [
204 'attachment_id' => $attachment_id,
205 'operation_id' => $operation_id,
206 ],
207 Async_Operation_Queue::OPTIMIZE
208 );
209
210 $meta
211 ->set_status( Image_Status::REOPTIMIZING_IN_PROGRESS )
212 ->save();
213 } catch ( Async_Operation_Exception $aoe ) {
214 $meta
215 ->set_status( Image_Status::REOPTIMIZING_FAILED )
216 ->save();
217
218 continue;
219 }
220 }
221
222 foreach ( $images['attachments_out_of_quota'] as $attachment_id ) {
223 ( new Image_Meta( $attachment_id ) )
224 ->set_status( Image_Status::REOPTIMIZING_FAILED )
225 ->set_error_type( Image_Optimization_Error_Type::QUOTA_EXCEEDED )
226 ->save();
227 }
228 }
229
230 /**
231 * Looks for images for bulk optimization operations based on a query passed and the quota left.
232 *
233 * @param Image_Query_Builder $query Image query to execute.
234 * @param bool $limit_to_quota If true, it limits image query to the quota left.
235 * @return array{total_images_count: int, attachments_in_quota: array, attachments_out_of_quota: array}
236 *
237 * @throws Invalid_Image_Exception
238 * @throws Quota_Exceeded_Error
239 */
240 private static function find_images( Image_Query_Builder $query, bool $limit_to_quota = false ): array {
241 $output = [
242 'total_images_count' => 0,
243 'attachments_in_quota' => [],
244 'attachments_out_of_quota' => [],
245 ];
246
247 $images_left = Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->images_left();
248
249 if ( ! $images_left ) {
250 throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimization' ) );
251 }
252
253 if ( $limit_to_quota ) {
254 $query->set_paging_size( $images_left );
255 }
256
257 $wp_query = $query->execute();
258
259 if ( ! $wp_query->post_count ) {
260 return $output;
261 }
262
263 foreach ( $wp_query->posts as $attachment_id ) {
264 try {
265 Validate_Image::is_valid( $attachment_id );
266 $wp_meta = new WP_Image_Meta( $attachment_id );
267 } catch ( Invalid_Image_Exception |Exceptions\Image_Validation_Error $ie ) {
268 continue;
269 }
270
271 $sizes_count = count( $wp_meta->get_size_keys() );
272
273 if ( $output['total_images_count'] + $sizes_count <= $images_left ) {
274 $output['total_images_count'] += $sizes_count;
275 $output['attachments_in_quota'][] = $attachment_id;
276 } else {
277 break;
278 }
279 }
280
281 $output['attachments_out_of_quota'] = array_diff( $wp_query->posts, $output['attachments_in_quota'] );
282
283 return $output;
284 }
285
286 /**
287 * Looks for the bulk token in transients.
288 *
289 * @param string $operation_id Bulk optimization operation id
290 *
291 * @return string|null Bulk token.
292 *
293 * @throws Bulk_Optimization_Token_Not_Found_Error
294 */
295 public static function get_bulk_operation_token( string $operation_id ): ?string {
296 $bulk_token = get_transient( "image_optimizer_bulk_token_$operation_id" );
297
298 if ( ! $bulk_token ) {
299 throw new Bulk_Optimization_Token_Not_Found_Error( "There is no token found for the operation $operation_id" );
300 }
301
302 return $bulk_token;
303 }
304
305 /**
306 * Saves bulk optimization token to transients for a day.
307 *
308 * @param string $operation_id Bulk optimization operation id
309 * @param string $bulk_token Bulk optimization token
310 * @return void
311 */
312 public static function set_bulk_operation_token( string $operation_id, string $bulk_token ): void {
313 set_transient( "image_optimizer_bulk_token_$operation_id", $bulk_token, HOUR_IN_SECONDS );
314 }
315
316 /**
317 * Sends a request to the BE to obtain bulk optimization token.
318 * It prevents obtaining a token for each and every optimization operation.
319 *
320 * @return string
321 *
322 * @throws Bulk_Token_Obtaining_Error
323 */
324 private static function obtain_bulk_token( int $images_count ): ?string {
325 try {
326 $response = Utils::get_api_client()->make_request(
327 'POST',
328 self::OBTAIN_TOKEN_ENDPOINT,
329 [
330 'images_count' => $images_count,
331 ]
332 );
333 } catch ( Throwable $t ) {
334 Logger::log( Logger::LEVEL_ERROR, 'Error while sending bulk token request: ' . $t->getMessage() );
335
336 throw new Bulk_Token_Obtaining_Error( $t->getMessage() );
337 }
338
339 return $response->token ?? null;
340 }
341
342 /**
343 * Checks if there is a bulk optimization operation in progress.
344 * If there is at least a single active bulk optimization operation it returns true, otherwise false.
345 *
346 * @return bool
347 * @throws Async_Operation_Exception
348 */
349 public static function is_optimization_in_progress(): bool {
350 $query = ( new Image_Optimization_Operation_Query() )
351 ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
352 ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
353 ->set_limit( 1 )
354 ->return_ids();
355
356 return ! empty( Async_Operation::get( $query ) );
357 }
358
359 /**
360 * Checks if there is a bulk re-optimization operation in progress.
361 * If there is at least a single active bulk re-optimization operation it returns true, otherwise false.
362 *
363 * @return bool
364 * @throws Async_Operation_Exception
365 */
366 public static function is_reoptimization_in_progress(): bool {
367 $query = ( new Image_Optimization_Operation_Query() )
368 ->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK )
369 ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
370 ->set_limit( 1 )
371 ->return_ids();
372
373 return ! empty( Async_Operation::get( $query ) );
374 }
375
376 /**
377 * Retrieves the bulk optimization process status.
378 *
379 * @return array{status: string, stats: array}
380 * @throws Async_Operation_Exception
381 */
382 public static function get_status(): array {
383 $stats = Optimization_Stats::get_image_stats();
384
385 $output = [
386 'status' => 'not-started',
387 'percentage' => round( $stats['optimized_image_count'] / $stats['total_image_count'] * 100 ),
388 ];
389
390 $active_query = ( new Image_Optimization_Operation_Query() )
391 ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
392 ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
393 ->set_limit( -1 );
394
395 if ( empty( Async_Operation::get( $active_query ) ) ) {
396 return $output;
397 }
398
399 $output['status'] = 'in-progress';
400
401 return $output;
402 }
403
404 /**
405 * Returns latest operations for the bulk optimization screen.
406 *
407 * @param string|null $operation_id
408 *
409 * @return array
410 * @throws Async_Operation_Exception
411 */
412 public static function get_processed_images( string $operation_id ): array {
413 $output = [];
414
415 $query = ( new Image_Optimization_Operation_Query() )
416 ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
417 ->set_bulk_operation_id( $operation_id )
418 ->set_limit( 50 );
419
420 $operations = Async_Operation::get( $query );
421
422 foreach ( $operations as $operation ) {
423 $image_id = $operation->get_args()['attachment_id'];
424 $image = new Image( $image_id );
425
426 try {
427 $stats = Optimization_Stats::get_image_stats( $image_id );
428 } catch ( Invalid_Image_Exception $iie ) {
429 continue;
430 } catch ( Throwable $t ) {
431 $original_file_size = 0;
432 $current_file_size = 0;
433 }
434
435 $output[] = [
436 'id' => $operation->get_id(),
437 'status' => $operation->get_status() === Async_Operation::OPERATION_STATUS_COMPLETE
438 ? ( new Image_Meta( $image_id ) )->get_status()
439 : $operation->get_status(),
440 'image_name' => $image->get_attachment_object()->post_title,
441 'image_id' => $image_id,
442 'thumbnail_url' => $image->get_url( 'thumbnail' ),
443 'original_file_size' => $stats['initial_image_size'],
444 'current_file_size' => $stats['current_image_size'],
445 ];
446 }
447
448 return $output;
449 }
450 }
451