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
imagify / classes / Abilities / OptimizeMedia.php

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

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