set_hook( Async_Operation_Hook::OPTIMIZE_BULK ) // It's risky to cancel in-progress operations at that point, so we cancel only the pending ones. ->set_status( Async_Operation::OPERATION_STATUS_PENDING ) ->set_limit( -1 ); $operations = Async_Operation::get( $query ); foreach ( $operations as $operation ) { $image_id = $operation->get_args()['attachment_id']; Async_Operation::cancel( $operation->get_id() ); ( new Image_Meta( $image_id ) )->delete(); } } /** * Looks for all non-optimized images and creates a bulk operation for each of them. * Also, obtains bulk token and passes it to a newly created operation. * @return void * @throws Quota_Exceeded_Error|Invalid_Image_Exception */ public static function find_images_and_schedule_optimization(): void { $images_left = Data::images_left(); if ( ! $images_left ) { throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimizer' ) ); } $query = ( new Image_Query_Builder() ) ->return_not_optimized_images() ->set_paging_size( $images_left ) ->execute(); if ( ! $query->post_count ) { return; } $operation_id = wp_generate_password( 10, false ); $total_images_count = 0; foreach ( $query->posts as $attachment_id ) { $wp_meta = new WP_Image_Meta( $attachment_id ); $sizes_count = count( $wp_meta->get_size_keys() ); if ( $total_images_count + $sizes_count <= $images_left ) { $total_images_count += $sizes_count; } else { break; } } try { $bulk_token = self::obtain_bulk_token( $total_images_count ); self::set_bulk_operation_token( $operation_id, $bulk_token ); } catch ( Bulk_Token_Obtaining_Error $e ) { $bulk_token = null; } foreach ( $query->posts as $attachment_id ) { $meta = new Image_Meta( $attachment_id ); if ( null === $bulk_token ) { $meta ->set_status( Image_Status::OPTIMIZATION_FAILED ) ->save(); continue; } try { Async_Operation::create( Async_Operation_Hook::OPTIMIZE_BULK, [ 'attachment_id' => $attachment_id, 'operation_id' => $operation_id, ], Async_Operation_Queue::OPTIMIZE ); $meta ->set_status( Image_Status::OPTIMIZATION_IN_PROGRESS ) ->save(); } catch ( Async_Operation_Exception $aoe ) { $meta ->set_status( Image_Status::OPTIMIZATION_FAILED ) ->save(); continue; } } } /** * Looks for already optimized images with backups and creates a bulk operation for each of them. * Also, obtains bulk token and passes it to a newly created operation. * * @return void * @throws Quota_Exceeded_Error */ public static function find_optimized_images_and_schedule_reoptimization(): void { $images_left = Data::images_left(); if ( ! $images_left ) { throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimizer' ) ); } $query = ( new Image_Query_Builder() ) ->return_images_only_with_backups() ->return_optimized_images() ->set_paging_size( $images_left ) ->execute(); if ( ! $query->post_count ) { return; } $operation_id = wp_generate_password( 10, false ); try { $bulk_token = self::obtain_bulk_token( $query->post_count ); self::set_bulk_operation_token( $operation_id, $bulk_token ); } catch ( Bulk_Token_Obtaining_Error $e ) { $bulk_token = null; } foreach ( $query->posts as $image_id ) { $meta = new Image_Meta( $image_id ); if ( null === $bulk_token ) { $meta ->set_status( Image_Status::OPTIMIZATION_FAILED ) ->save(); continue; } try { Async_Operation::create( Async_Operation_Hook::REOPTIMIZE_BULK, [ 'attachment_id' => $image_id, 'operation_id' => $operation_id, ], Async_Operation_Queue::OPTIMIZE ); $meta ->set_status( Image_Status::REOPTIMIZING_IN_PROGRESS ) ->save(); } catch ( Async_Operation_Exception $aoe ) { $meta ->set_status( Image_Status::REOPTIMIZING_FAILED ) ->save(); continue; } } } /** * Looks for the bulk token in transients. * * @param string $operation_id Bulk optimization operation id * * @return string|null Bulk token. * @throws Bulk_Optimization_Exception */ public static function get_bulk_operation_token( string $operation_id ): ?string { $bulk_token = get_transient( "image_optimizer_bulk_token_$operation_id" ); if ( ! $bulk_token ) { throw new Bulk_Optimization_Exception( "There is no token found for the operation $operation_id" ); } return $bulk_token; } /** * Saves bulk optimization token to transients for a day. * * @param string $operation_id Bulk optimization operation id * @param string $bulk_token Bulk optimization token * @return void */ public static function set_bulk_operation_token( string $operation_id, string $bulk_token ): void { set_transient( "image_optimizer_bulk_token_$operation_id", $bulk_token, DAY_IN_SECONDS ); } /** * Sends a request to the BE to obtain bulk optimization token. * It prevents obtaining a token for each and every optimization operation. * * @return string * @throws Bulk_Token_Obtaining_Error */ private static function obtain_bulk_token( int $images_count ): ?string { $response = Utils::get_api_client()->make_request( 'POST', self::OBTAIN_TOKEN_ENDPOINT, [ 'images_count' => $images_count, ] ); if ( is_wp_error( $response ) ) { throw new Bulk_Token_Obtaining_Error( $response->get_error_message() ); } return $response->token ?? null; } /** * Checks if there is a bulk optimization operation in progress. * If there is at least a single active bulk optimization operation it returns true, otherwise false. * * @return bool * @throws Async_Operation_Exception */ public static function is_optimization_in_progress(): bool { $query = ( new Image_Optimization_Operation_Query() ) ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK ) ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] ) ->set_limit( 1 ) ->return_ids(); return ! empty( Async_Operation::get( $query ) ); } /** * Checks if there is a bulk re-optimization operation in progress. * If there is at least a single active bulk re-optimization operation it returns true, otherwise false. * * @return bool * @throws Async_Operation_Exception */ public static function is_reoptimization_in_progress(): bool { $query = ( new Image_Optimization_Operation_Query() ) ->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK ) ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] ) ->set_limit( 1 ) ->return_ids(); return ! empty( Async_Operation::get( $query ) ); } /** * Retrieves the bulk optimization process status. * * @return array{status: string, stats: array} * @throws Async_Operation_Exception */ public static function get_status(): array { $stats = Optimization_Stats::get_image_stats(); $output = [ 'status' => 'not-started', 'percentage' => round( $stats['optimized_image_count'] / $stats['total_image_count'] * 100 ), ]; $active_query = ( new Image_Optimization_Operation_Query() ) ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK ) ->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] ) ->set_limit( -1 ); if ( empty( Async_Operation::get( $active_query ) ) ) { return $output; } $output['status'] = 'in-progress'; return $output; } /** * Returns latest operations for the bulk optimization screen. * * @return array * @throws Async_Operation_Exception */ public static function get_processed_images(): array { $output = []; $first_operation_query = ( new Image_Optimization_Operation_Query() ) ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK ) ->set_limit( 1 ); $first_operation = Async_Operation::get( $first_operation_query )[0] ?? null; if ( empty( $first_operation ) ) { return []; } $query = ( new Image_Optimization_Operation_Query() ) ->set_hook( Async_Operation_Hook::OPTIMIZE_BULK ) ->set_status([ Async_Operation::OPERATION_STATUS_NOT_STARTED, Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING, Async_Operation::OPERATION_STATUS_COMPLETE, Async_Operation::OPERATION_STATUS_FAILED, Async_Operation::OPERATION_STATUS_CANCELED, ]) ->set_bulk_operation_id( $first_operation->get_args()['operation_id'] ) ->set_limit( 50 ); $operations = Async_Operation::get( $query ); if ( empty( $operations ) ) { return []; } foreach ( $operations as $operation ) { $image_id = $operation->get_args()['attachment_id']; try { $image = new Image( $image_id ); $meta = new Image_Meta( $image_id ); $wp_meta = new WP_Image_Meta( $image_id ); } catch ( Invalid_Image_Exception $e ) { continue; } $output[] = [ 'operation_id' => $operation->get_id(), 'status' => $operation->get_status() === Async_Operation::OPERATION_STATUS_COMPLETE ? ( new Image_Meta( $image_id ) )->get_status() : $operation->get_status(), 'image_name' => $image->get_attachment_object()->post_title, 'image_id' => $image_id, 'thumbnail_url' => $image->get_url( 'thumbnail' ), 'original_file_size' => $meta->get_original_file_size( Image::SIZE_FULL ), 'current_file_size' => $wp_meta->get_file_size( Image::SIZE_FULL ), ]; } return $output; } }