PluginProbe
Image Optimization – Compress Images and Convert to WebP or AVIF / 1.1.0
Image Optimization – Compress Images and Convert to WebP or AVIF v1.1.0
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.1.0, at modules/optimization/classes/bulk-optimization-controller.php

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