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 / GenerateMissingNextgen.php

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

257 lines 7.9 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 use Imagify\Bulk\Bulk;
7 use Imagify\Stats\StatInterface;
8
9 /**
10 * MCP ability: generate missing next-gen (WebP/AVIF) versions.
11 *
12 * Queues generation of missing next-gen versions for all optimized media
13 * by delegating to `Bulk::run_generate_nextgen()`, exactly as the WP-CLI
14 * `GenerateMissingNextgenCommand` and the AJAX `missing_nextgen_callback` do.
15 *
16 * @since 2.3.0
17 */
18 class GenerateMissingNextgen extends AbstractAbility implements CreditConsumingAbilityInterface {
19
20 const ABILITY_ID = 'imagify/generate-missing-nextgen';
21 const ABILITY_NAME = 'Generate missing next-gen versions';
22
23 /**
24 * Bulk instance for queueing next-gen generation jobs.
25 *
26 * Typed via docblock (not property declaration) so that test doubles can be
27 * injected via reflection without triggering PHP 8+ typed-property enforcement.
28 *
29 * @var Bulk
30 */
31 private $bulk;
32
33 /**
34 * Stat service used to compute the missing-nextgen count for the
35 * credit-consumption impact estimate. Typed via docblock for the same
36 * reflection-injection reason as `$bulk` above.
37 *
38 * @var StatInterface
39 */
40 private $stat;
41
42 /**
43 * Constructor.
44 *
45 * @param Bulk $bulk Bulk instance injected by the DI container.
46 * @param StatInterface $stat Stat service (`OptimizedMediaWithoutNextGen`) injected by the DI container.
47 */
48 public function __construct( Bulk $bulk, StatInterface $stat ) {
49 $this->bulk = $bulk;
50 $this->stat = $stat;
51 }
52
53 /**
54 * Returns the ability slug.
55 *
56 * @return string
57 */
58 public function get_id(): string {
59 return self::ABILITY_ID;
60 }
61
62 /**
63 * Returns the human-readable ability label.
64 *
65 * @return string
66 */
67 public function get_name(): string {
68 return self::ABILITY_NAME;
69 }
70
71 /**
72 * Register the ability with the WP Abilities API.
73 *
74 * No-ops gracefully when the WP Abilities API is not available (WP < 6.9).
75 *
76 * @return void
77 */
78 public function register(): void {
79 if ( ! function_exists( 'wp_register_ability' ) ) {
80 return;
81 }
82
83 wp_register_ability(
84 'imagify/generate-missing-nextgen',
85 [
86 'label' => __( 'Generate missing next-gen versions', 'imagify' ),
87 'description' => __( 'Queues generation of missing next-gen (WebP/AVIF) versions for all optimized media. Runs asynchronously via Action Scheduler.', 'imagify' ),
88 'category' => 'imagify',
89 'input_schema' => [
90 'type' => 'object',
91 'properties' => [
92 'confirm' => [
93 'type' => 'boolean',
94 'description' => __( 'Set to true to execute after reviewing the credit-consumption preview returned by a prior call without this flag.', 'imagify' ),
95 'default' => false,
96 ],
97 ],
98 ],
99 'output_schema' => [
100 'type' => 'object',
101 'properties' => [
102 'status' => [
103 'type' => 'string',
104 'enum' => [ 'scheduled', 'error', 'confirmation_required', 'insufficient_quota', 'invalid_api_key' ],
105 ],
106 'queued_count' => [ 'type' => 'integer' ],
107 'error_message' => [ 'type' => [ 'string', 'null' ] ],
108 ],
109 ],
110 'execute_callback' => [ $this, 'execute' ],
111 'permission_callback' => [ $this, 'check_permissions' ],
112 'meta' => [
113 'show_in_rest' => true,
114 'mcp' => [ 'public' => true ],
115 'annotations' => [
116 'readonly' => false,
117 'destructive' => true,
118 'idempotent' => false,
119 ],
120 ],
121 ]
122 );
123 }
124
125 /**
126 * Check if the current user has permission to execute this ability.
127 *
128 * @return bool True if the current user has the `manage_options` capability.
129 */
130 protected function has_permission(): bool {
131 return (bool) current_user_can( 'manage_options' );
132 }
133
134 /**
135 * Returns the capability required to execute this ability.
136 *
137 * @return string
138 */
139 protected function get_required_capability(): string {
140 return 'manage_options';
141 }
142
143 /**
144 * Returns the credit-consumption impact estimate for generating missing next-gen versions.
145 *
146 * `count` is the number of optimized media still missing a next-gen version,
147 * computed live via `OptimizedMediaWithoutNextGen::get_stat()` (not the
148 * 2-day cached `get_cached_stat()`) because this preview drives an AI
149 * credit-spend decision and must not go stale relative to `total`. `total`
150 * is the number of optimized media across both contexts, summed the same
151 * way `OptimizedMediaWithoutNextGen::get_stat()` sums its own per-context
152 * loop. `count` is clamped to `total` as a defensive safeguard.
153 *
154 * @param array $args Input arguments (unused: the estimate does not depend on input).
155 * @return array{unit: string, count: int, total: int, label: string}
156 */
157 public function get_impact_estimate( array $args ): array {
158 $count = (int) $this->stat->get_stat();
159 $total = imagify_count_optimized_attachments() + \Imagify_Files_Stats::count_optimized_files();
160 $count = min( $count, (int) $total );
161
162 return [
163 'unit' => 'image',
164 'count' => $count,
165 'total' => (int) $total,
166 'label' => __( 'optimized images missing a next-gen version', 'imagify' ),
167 ];
168 }
169
170 /**
171 * Execute the ability: queue generation of missing next-gen versions.
172 *
173 * Wraps the real execution behind `guard_credit_confirmation()` so the AI
174 * never even attempts the call when quota is already exhausted.
175 *
176 * @param array $args Input arguments. Expects optionally `confirm` (bool).
177 * @return array<string, mixed> Guard response (invalid_api_key/insufficient_quota/confirmation_required) or the do_execute() result shape.
178 */
179 public function execute( array $args = [] ): array {
180 $start_time = microtime( true );
181 $result = $this->guard_credit_confirmation(
182 $args,
183 function ( array $a ) {
184 return $this->do_execute( $a );
185 }
186 );
187
188 $this->fire_executed( $result, $start_time, $args );
189
190 return $result;
191 }
192
193 /**
194 * Internal execution logic for the ability.
195 *
196 * Delegates to `Bulk::run_generate_nextgen()` and maps its return value to
197 * the MCP contract shape:
198 * - `success=true` → `status=scheduled, queued_count=N`
199 * - `success=false, no-images` → `status=scheduled, queued_count=0` (AC #3 no-op)
200 * - `success=false, other` → `status=error, queued_count=0, error_message=...`
201 *
202 * Separated from execute() so guard_credit_confirmation() can invoke it via
203 * a closure without a `[$this, 'method']` callable-array visibility problem
204 * (this method is private, and the guard lives on AbstractAbility).
205 *
206 * @param array $args Input arguments (unused by the underlying Bulk call).
207 * @return array{status: string, queued_count: int, error_message: string|null}
208 */
209 private function do_execute( array $args ): array {
210 $contexts = $this->bulk->get_contexts();
211 $formats = imagify_nextgen_images_formats();
212 $result = $this->bulk->run_generate_nextgen( $contexts, $formats );
213
214 if ( true === $result['success'] ) {
215 return [
216 'status' => 'scheduled',
217 'queued_count' => (int) $result['message'],
218 'error_message' => null,
219 ];
220 }
221
222 if ( 'no-images' === $result['message'] ) {
223 // Nothing to generate — not an error (AC #3).
224 return [
225 'status' => 'scheduled',
226 'queued_count' => 0,
227 'error_message' => null,
228 ];
229 }
230
231 return $this->error_response( (string) $result['message'] );
232 }
233
234 /**
235 * Build an error response array from a Bulk error message string.
236 *
237 * @param string $message The raw message returned by `Bulk::run_generate_nextgen()`.
238 *
239 * @return array{status: string, queued_count: int, error_message: string}
240 */
241 private function error_response( string $message ): array {
242 if ( 'over-quota' === $message ) {
243 $readable = __( 'Imagify account is over quota or the API key is invalid.', 'imagify' );
244 } elseif ( 'no-backup' === $message ) {
245 $readable = __( 'No backup available; next-gen versions cannot be generated.', 'imagify' );
246 } else {
247 $readable = $message;
248 }
249
250 return [
251 'status' => 'error',
252 'queued_count' => 0,
253 'error_message' => $readable,
254 ];
255 }
256 }
257