PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.0
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.0
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.0, at classes/Abilities/OptimizeMedia.php

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