PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.2
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
imagify / classes / Abilities / OptimizeMedia.php

OptimizeMedia.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.2, at classes/Abilities/OptimizeMedia.php

337 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare(strict_types=1);
3
4 namespace Imagify\Abilities;
5
6 /**
7 * MCP ability: optimize a media on-demand.
8 *
9 * Registers itself with the WP Abilities API under the slug
10 * `imagify/optimize-media` and delegates to the existing
11 * `Imagify\Optimization\Process\WP` class.
12 *
13 * @since 2.3.0
14 */
15 class OptimizeMedia extends AbstractAbility implements CreditConsumingAbilityInterface {
16
17 const ABILITY_ID = 'imagify/optimize-media';
18 const ABILITY_NAME = 'Optimize media';
19
20 /**
21 * Returns the ability slug.
22 *
23 * @return string
24 */
25 public function get_id(): string {
26 return self::ABILITY_ID;
27 }
28
29 /**
30 * Returns the human-readable ability label.
31 *
32 * @return string
33 */
34 public function get_name(): string {
35 return self::ABILITY_NAME;
36 }
37
38 /**
39 * Register the ability with the WP Abilities API.
40 *
41 * No-ops gracefully when the API is not available (WP < 6.9).
42 *
43 * @return void
44 */
45 public function register(): void {
46 if ( ! function_exists( 'wp_register_ability' ) ) {
47 return;
48 }
49
50 wp_register_ability(
51 'imagify/optimize-media',
52 [
53 'label' => __( 'Optimize media', 'imagify' ),
54 'description' => __( 'Optimizes a specific media on-demand using Imagify.', 'imagify' ),
55 'category' => 'imagify',
56 'input_schema' => [
57 'type' => 'object',
58 'properties' => [
59 'media_id' => [
60 'type' => 'integer',
61 'description' => __( 'The WordPress attachment ID to optimize.', 'imagify' ),
62 ],
63 'optimization_level' => [
64 'type' => 'integer',
65 'description' => __( 'Optimization level: 0 (normal), 1 (aggressive), or 2 (ultra). If omitted, uses the global setting.', 'imagify' ),
66 'minimum' => 0,
67 'maximum' => 2,
68 ],
69 'confirm' => [
70 'type' => 'boolean',
71 'description' => __( 'Set to true to execute after reviewing the credit-consumption preview returned by a prior call without this flag.', 'imagify' ),
72 'default' => false,
73 ],
74 ],
75 'required' => [ 'media_id' ],
76 ],
77 'output_schema' => [
78 'type' => 'object',
79 'properties' => [
80 'status' => [
81 'type' => 'string',
82 'description' => __( 'Result status: "success", "error", "confirmation_required", "insufficient_quota", or "invalid_api_key".', 'imagify' ),
83 'enum' => [ 'success', 'error', 'confirmation_required', 'insufficient_quota', 'invalid_api_key' ],
84 ],
85 'original_size' => [
86 'type' => [ 'integer', 'null' ],
87 'description' => __( 'Original file size in bytes before optimization, or null on error.', 'imagify' ),
88 ],
89 'optimized_size' => [
90 'type' => [ 'integer', 'null' ],
91 'description' => __( 'Optimized file size in bytes after optimization, or null on error or if not yet available.', 'imagify' ),
92 ],
93 'savings_percent' => [
94 'type' => [ 'number', 'null' ],
95 'description' => __( 'Percentage savings, or null on error.', 'imagify' ),
96 ],
97 'error_message' => [
98 'type' => [ 'string', 'null' ],
99 'description' => __( 'Human-readable error message on failure, or null on success.', 'imagify' ),
100 ],
101 ],
102 ],
103 'execute_callback' => [ $this, 'execute' ],
104 'permission_callback' => [ $this, 'check_permissions' ],
105 'meta' => [
106 'show_in_rest' => true,
107 'mcp' => [
108 'public' => true,
109 ],
110 'annotations' => [
111 'readonly' => false,
112 'destructive' => true,
113 'idempotent' => false,
114 ],
115 ],
116 ]
117 );
118 }
119
120 /**
121 * Check if the current user has permission to execute this ability.
122 *
123 * Routes through Imagify's capability abstraction so the `imagify_capacity`
124 * filter and multisite network-admin logic are honoured.
125 *
126 * Note: `manual-optimize` is not used here because `check_permissions()` is
127 * called before `execute()` and receives no `media_id`, making the underlying
128 * `edit_post` check ambiguous. The `manage` descriptor is the correct top-level
129 * gate consistent with existing AJAX equivalents.
130 *
131 * @return bool True when the current user has the Imagify `manage` capability.
132 */
133 protected function has_permission(): bool {
134 return imagify_get_context( 'wp' )->current_user_can( 'manage' );
135 }
136
137 /**
138 * Returns the credit-consumption impact estimate for a single media optimization.
139 *
140 * @param array $args Input arguments (unused: optimizing a single media always costs 1 unit).
141 * @return array{unit: string, count: int, label: string}
142 */
143 public function get_impact_estimate( array $args ): array {
144 return [
145 'unit' => 'image',
146 'count' => 1,
147 'label' => 'this media',
148 ];
149 }
150
151 /**
152 * Execute the ability: optimize the media.
153 *
154 * Wraps the real execution behind `guard_credit_confirmation()` so the
155 * caller must pass `confirm: true` once quota is confirmed (and is not
156 * over quota, and the API key is valid). Fires `imagify_mcp_ability_executed`
157 * after the ability resolves so that tracking and other subscribers can
158 * react to every outcome (previews included).
159 *
160 * @param array $args Input arguments. Expects `media_id` (int) and optionally `optimization_level` (int), `confirm` (bool).
161 * @return array<string, mixed> Guard response (invalid_api_key/insufficient_quota/confirmation_required) or the do_execute() result shape.
162 */
163 public function execute( array $args = [] ): array {
164 $start_time = microtime( true );
165 $result = $this->guard_credit_confirmation(
166 $args,
167 function ( array $a ) {
168 return $this->do_execute( $a );
169 }
170 );
171
172 $this->fire_executed( $result, $start_time, $args );
173
174 return $result;
175 }
176
177 /**
178 * Internal execution logic for the ability.
179 *
180 * Separated from execute() so that the do_action hook fires for every
181 * outcome (success and all error paths) with a single call site.
182 *
183 * @param array $args Input arguments.
184 * @return array{status: string, original_size: int|null, optimized_size: int|null, savings_percent: float|null, error_message: string|null}
185 */
186 private function do_execute( array $args ): array {
187 $media_id = isset( $args['media_id'] ) ? (int) $args['media_id'] : 0;
188
189 if ( $media_id <= 0 ) {
190 return $this->error_response( 'Invalid or missing media_id.' );
191 }
192
193 // Verify the attachment exists.
194 $post = get_post( $media_id );
195 if ( ! $post ) {
196 return $this->error_response( 'Invalid media.' );
197 }
198
199 // Verify the post is an attachment.
200 if ( 'attachment' !== get_post_type( $post ) ) {
201 return $this->error_response( 'The provided ID is not a media attachment.' );
202 }
203
204 // Determine optimization level.
205 $optimization_level = null;
206 if ( isset( $args['optimization_level'] ) ) {
207 $optimization_level = (int) $args['optimization_level'];
208 }
209
210 // Get the process for this media.
211 $process = imagify_get_optimization_process( $media_id, 'wp' );
212
213 if ( ! $process ) {
214 return $this->error_response( 'Could not initialize optimization process.' );
215 }
216
217 // Capture the original size before optimization.
218 $original_size = $this->get_media_original_size( $process );
219
220 // Determine whether to optimize or reoptimize.
221 $data = $process->get_data();
222
223 if ( $data->is_optimized() ) {
224 // Re-optimize the media.
225 $result = $process->reoptimize( $optimization_level );
226 } else {
227 // First-time optimization.
228 $result = $process->optimize( $optimization_level );
229 }
230
231 // Handle errors from the process.
232 if ( is_wp_error( $result ) ) {
233 return $this->error_response( $result->get_error_message() );
234 }
235
236 // Capture the optimized size after optimization.
237 // Note: The process queues a background job, so optimized_size may be 0 until job completes.
238 $optimized_size = $this->get_media_optimized_size( $process );
239
240 // Calculate savings percentage.
241 $savings_percent = null;
242 if ( $original_size > 0 && null !== $optimized_size ) {
243 $savings_percent = (float) round( ( ( $original_size - $optimized_size ) / $original_size ) * 100, 1 );
244 }
245
246 return [
247 'status' => 'success',
248 'original_size' => $original_size,
249 'optimized_size' => $optimized_size,
250 'savings_percent' => $savings_percent,
251 'error_message' => null,
252 ];
253 }
254
255 /**
256 * Build an error response array.
257 *
258 * @param string $error_message The error message.
259 * @return array{status: string, original_size: null, optimized_size: null, savings_percent: null, error_message: string}
260 */
261 private function error_response( string $error_message ): array {
262 return [
263 'status' => 'error',
264 'original_size' => null,
265 'optimized_size' => null,
266 'savings_percent' => null,
267 'error_message' => $error_message,
268 ];
269 }
270
271 /**
272 * Get the original size of the media before optimization.
273 *
274 * Extracted into a protected method so unit tests can override.
275 *
276 * @param \Imagify\Optimization\Process\ProcessInterface $process The optimization process.
277 * @return int Original file size in bytes, or 0 if unavailable.
278 */
279 protected function get_media_original_size( $process ): int {
280 $data = $process->get_data();
281
282 if ( ! $data ) {
283 return 0;
284 }
285
286 // If already optimized, use the original_size from optimization stats.
287 if ( $data->is_optimized() ) {
288 $optimization_data = $data->get_optimization_data();
289 if ( isset( $optimization_data['stats']['original_size'] ) ) {
290 return (int) $optimization_data['stats']['original_size'];
291 }
292 }
293
294 // Otherwise, get the original file size from the media object.
295 $media = $process->get_media();
296
297 if ( ! $media ) {
298 return 0;
299 }
300
301 $path = $media->get_raw_original_path();
302
303 if ( ! $path || ! file_exists( $path ) ) {
304 return 0;
305 }
306
307 return (int) filesize( $path );
308 }
309
310 /**
311 * Get the optimized size of the media after optimization.
312 *
313 * For newly-queued jobs, this may return 0 until the background job completes.
314 * Clients should poll imagify_get_media_status to track final results.
315 *
316 * Extracted into a protected method so unit tests can override.
317 *
318 * @param \Imagify\Optimization\Process\ProcessInterface $process The optimization process.
319 * @return int|null Optimized file size in bytes, or null if unavailable.
320 */
321 protected function get_media_optimized_size( $process ): ?int {
322 $data = $process->get_data();
323
324 if ( ! $data ) {
325 return null;
326 }
327
328 $optimization_data = $data->get_optimization_data();
329
330 if ( isset( $optimization_data['stats']['optimized_size'] ) ) {
331 return (int) $optimization_data['stats']['optimized_size'];
332 }
333
334 return null;
335 }
336 }
337