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
← All changes | classes/Bulk/Bulk.php +198 -11 2.2.9 → trunk View file →
@@ -8,9 +8,9 @@
8 8
9 9 /**
10 10 * Bulk optimization
11 11 */
12 -final class Bulk {
12 +final class Bulk implements BulkOptimizerInterface {
13 13 use InstanceGetterTrait;
14 14
15 15 /**
16 16 * Class init: launch hooks.
@@ -20,8 +20,9 @@
20 20 public function init() {
21 21 add_action( 'imagify_optimize_media', [ $this, 'optimize_media' ], 10, 3 );
22 22 add_action( 'imagify_convert_next_gen', [ $this, 'generate_nextgen_versions' ], 10, 2 ); // @phpstan-ignore-line
23 23 add_action( 'wp_ajax_imagify_bulk_optimize', [ $this, 'bulk_optimize_callback' ] );
24 + add_action( 'wp_ajax_imagify_bulk_stop', [ $this, 'bulk_stop_callback' ] );
24 25 add_action( 'wp_ajax_imagify_missing_nextgen_generation', [ $this, 'missing_nextgen_callback' ] );
25 26 add_action( 'wp_ajax_imagify_get_folder_type_data', [ $this, 'get_folder_type_data_callback' ] );
26 27 add_action( 'wp_ajax_imagify_bulk_info_seen', [ $this, 'bulk_info_seen_callback' ] );
27 28 add_action( 'wp_ajax_imagify_bulk_get_stats', [ $this, 'bulk_get_stats_callback' ] );
@@ -27,11 +28,40 @@
27 28 add_action( 'wp_ajax_imagify_bulk_get_stats', [ $this, 'bulk_get_stats_callback' ] );
28 29 add_action( 'imagify_after_optimize', [ $this, 'check_optimization_status' ], 10, 2 );
29 30 add_action( 'imagify_deactivation', [ $this, 'delete_transients_data' ] );
30 31 add_action( 'update_option_imagify_settings', [ $this, 'maybe_generate_missing_nextgen' ], 10, 2 );
32 + add_action( 'imagify_upgrade', [ $this, 'maybe_clear_stale_bulk_run' ], 10, 2 );
31 33 }
32 34
33 35 /**
36 + * Clear any bulk optimization progress left running from before 2.3.2, on upgrade.
37 + *
38 + * Queue items enqueued by a bulk run before 2.3.2 don't carry the `bulk` flag used by
39 + * self::check_optimization_status() to tell bulk completions from manual/auto ones (see
40 + * that method). If a bulk run straddles the upgrade, its remaining items complete without
41 + * that flag and are never counted, so the run's `remaining` counter never reaches zero and
42 + * the bulk page is stuck showing it as still running. Clearing the running transients lets
43 + * a fresh bulk run start clean instead.
44 + *
45 + * @since 2.3.2
46 + *
47 + * @param string $network_version Previous version stored on the network.
48 + * @param string $site_version Previous version stored on site level.
49 + *
50 + * @return void
51 + */
52 + public function maybe_clear_stale_bulk_run( $network_version, $site_version ) {
53 + if ( version_compare( $site_version, '2.3.2' ) >= 0 ) {
54 + return;
55 + }
56 +
57 + delete_transient( 'imagify_custom-folders_optimize_running' );
58 + delete_transient( 'imagify_wp_optimize_running' );
59 + delete_transient( 'imagify_bulk_optimization_result' );
60 + delete_transient( 'imagify_bulk_optimization_complete' );
61 + }
62 +
63 + /**
34 64 * Delete transients data on deactivation
35 65 *
36 66 * @return void
37 67 */
@@ -61,8 +91,15 @@
61 91 ) {
62 92 return;
63 93 }
64 94
95 + if ( empty( $item['data']['bulk'] ) ) {
96 + // This completion was not enqueued by Bulk::run_optimize() (e.g. a manual click or
97 + // an auto-optimize-on-upload that happens to complete while a bulk job is active):
98 + // do not let it affect the bulk job's progress counters.
99 + return;
100 + }
101 +
65 102 $data = $process->get_data();
66 103
67 104 if ( ! $data ) {
68 105 return;
@@ -172,9 +209,9 @@
172 209 * @param int $optimization_level Optimization level.
173 210 *
174 211 * @return array
175 212 */
176 - public function run_optimize( string $context, int $optimization_level ) {
213 + public function run_optimize( string $context, int $optimization_level ): array {
177 214 if ( ! $this->can_optimize() ) {
178 215 return [
179 216 'success' => false,
180 217 'message' => 'over-quota',
@@ -182,8 +219,30 @@
182 219 }
183 220
184 221 $media_ids = $this->get_bulk_instance( $context )->get_unoptimized_media_ids( $optimization_level );
185 222
223 + $media_ids = array_values(
224 + array_filter(
225 + $media_ids,
226 + function ( $media_id ) use ( $context, $optimization_level ) {
227 + /**
228 + * Filter the list of media to optimize during a bulk optimization, per media.
229 + *
230 + * Return false to exclude a given media from the bulk optimization run
231 + * (e.g. to skip a specific file type such as PDFs).
232 + *
233 + * @since 2.3
234 + *
235 + * @param bool $optimize True to optimize this media, false to exclude it.
236 + * @param int $media_id The media ID (attachment ID, or custom-folder file ID).
237 + * @param string $context The optimization context ('wp' or 'custom-folders').
238 + * @param int $optimization_level The optimization level.
239 + */
240 + return wpm_apply_filters_typed( 'boolean', 'imagify_bulk_optimize_media', true, $media_id, $context, $optimization_level );
241 + }
242 + )
243 + );
244 +
186 245 if ( empty( $media_ids ) ) {
187 246 return [
188 247 'success' => false,
189 248 'message' => 'no-images',
@@ -219,8 +278,112 @@
219 278 ];
220 279 }
221 280
222 281 /**
282 + * Stops a running bulk process for the given contexts.
283 + *
284 + * Cancels every media still waiting in the queue and clears the progress data. Media already
285 + * sent to the Imagify API cannot be cancelled: the one being processed when the stop is
286 + * requested still completes.
287 + *
288 + * @since 2.3
289 + *
290 + * @param array $contexts An array of contexts (WP/Custom folders).
291 + *
292 + * @return array {
293 + * @type bool $success Whether a running process was found.
294 + * @type string $message Status message.
295 + * @type int $cancelled Number of queued media actually cancelled.
296 + * }
297 + */
298 + public function run_stop( array $contexts ): array {
299 + $cancelled = 0;
300 + $was_running = false;
301 +
302 + foreach ( $contexts as $context ) {
303 + if ( false !== get_transient( "imagify_{$context}_optimize_running" ) ) {
304 + $was_running = true;
305 + }
306 +
307 + $cancelled += $this->cancel_pending_actions( 'imagify_optimize_media', "imagify-{$context}-optimize-media" );
308 + $cancelled += $this->cancel_pending_actions( 'imagify_convert_next_gen', "imagify-{$context}-convert-nextgen" );
309 +
310 + delete_transient( "imagify_{$context}_optimize_running" );
311 + }
312 +
313 + delete_transient( 'imagify_bulk_optimization_result' );
314 + delete_transient( 'imagify_missing_next_gen_total' );
315 +
316 + if ( ! $was_running && 0 === $cancelled ) {
317 + return [
318 + 'success' => false,
319 + 'message' => 'not-running',
320 + 'cancelled' => 0,
321 + ];
322 + }
323 +
324 + /**
325 + * Fires after a bulk process has been manually stopped.
326 + *
327 + * @since 2.3
328 + *
329 + * @param array $contexts The contexts that were stopped.
330 + * @param int $cancelled Number of queued media actually cancelled.
331 + */
332 + do_action( 'imagify_bulk_stopped', $contexts, $cancelled );
333 +
334 + return [
335 + 'success' => true,
336 + 'message' => 'success',
337 + 'cancelled' => $cancelled,
338 + ];
339 + }
340 +
341 + /**
342 + * Cancel all the pending actions for a given hook and group.
343 + *
344 + * Actions already running are left untouched: Action Scheduler cannot interrupt them.
345 + *
346 + * The pending actions are counted before being cancelled, so the caller reports what was
347 + * really removed from the queue. The `remaining` value of the progress transient cannot be
348 + * used for that: it is only decremented by the `imagify_after_optimize` hook, which never
349 + * fires when an optimization bails out early (unsupported media, already optimized, locked
350 + * process) or when the request dies, so it drifts above the real queue size.
351 + *
352 + * @since 2.3
353 + *
354 + * @param string $hook The action hook.
355 + * @param string $group The Action Scheduler group.
356 + *
357 + * @return int Number of pending actions that were cancelled.
358 + */
359 + private function cancel_pending_actions( string $hook, string $group ): int {
360 + if ( ! function_exists( 'as_unschedule_all_actions' ) || ! function_exists( 'as_get_scheduled_actions' ) ) {
361 + return 0;
362 + }
363 +
364 + try {
365 + $pending = as_get_scheduled_actions(
366 + [
367 + 'hook' => $hook,
368 + 'group' => $group,
369 + // ActionScheduler_Store::STATUS_PENDING, without depending on the class being loaded.
370 + 'status' => 'pending',
371 + 'per_page' => -1,
372 + 'orderby' => 'none',
373 + ],
374 + 'ids'
375 + );
376 +
377 + as_unschedule_all_actions( $hook, [], $group );
378 + } catch ( Exception $exception ) {
379 + return 0;
380 + }
381 +
382 + return is_array( $pending ) ? count( $pending ) : 0;
383 + }
384 +
385 + /**
223 386 * Runs the bulk restore for a given context.
224 387 *
225 388 * Restores all optimized media to their original state synchronously.
226 389 * Does not consume API quota since restore is a local file operation.
@@ -295,15 +458,10 @@
295 458
296 459 foreach ( $contexts as $context ) {
297 460 foreach ( $formats as $format ) {
298 461 $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format );
299 - if ( ! $media['ids'] && $media['errors']['no_backup'] ) {
300 - // No backup, no next-gen.
301 - return [
302 - 'success' => false,
303 - 'message' => 'no-backup',
304 - ];
305 - } elseif ( ! $media['ids'] && $media['errors']['no_file_path'] ) {
462 +
463 + if ( ! $media['ids'] && $media['errors']['no_file_path'] ) {
306 464 // Error.
307 465 return [
308 466 'success' => false,
309 467 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ),
@@ -309,9 +467,11 @@
309 467 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ),
310 468 ];
311 469 }
312 470
313 - $medias[ $context ] = $media['ids'];
471 + if ( $media['ids'] ) {
472 + $medias[ $context ] = $media['ids'];
473 + }
314 474 }
315 475 }
316 476
317 477 if ( empty( $medias ) ) {
@@ -432,9 +592,9 @@
432 592 return $result;
433 593 }
434 594 }
435 595
436 - return $process->optimize( $level );
596 + return $process->optimize( $level, [ 'bulk' => true ] );
437 597 }
438 598
439 599 /**
440 600 * Generate next-gen images if they are missing.
@@ -539,8 +699,35 @@
539 699 wp_send_json_error( [ 'message' => $data['message'] ] );
540 700 }
541 701
542 702 wp_send_json_success( [ 'total' => $data['message'] ] );
703 + }
704 +
705 + /**
706 + * Stop the running bulk optimization
707 + *
708 + * @since 2.3
709 + *
710 + * @return void
711 + */
712 + public function bulk_stop_callback() {
713 + imagify_check_nonce( 'imagify-bulk-optimize' );
714 +
715 + $contexts = $this->get_contexts();
716 +
717 + foreach ( $contexts as $context ) {
718 + if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
719 + imagify_die();
720 + }
721 + }
722 +
723 + $data = $this->run_stop( $contexts );
724 +
725 + if ( false === $data['success'] ) {
726 + wp_send_json_error( [ 'message' => $data['message'] ] );
727 + }
728 +
729 + wp_send_json_success( [ 'cancelled' => $data['cancelled'] ] );
543 730 }
544 731
545 732 /**
546 733 * Launch the missing Next-gen versions generation