PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.0.1
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.0.1
1.7.7 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 All 33 releases
image-optimization / modules / optimization / classes / bulk-optimization.php

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

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