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
← All changes | classes/Abilities/AbstractAbility.php +190 -4 2.3.0 → trunk View file →
@@ -2,15 +2,19 @@
2 2 declare(strict_types=1);
3 3
4 4 namespace Imagify\Abilities;
5 5
6 +use Imagify\User\User;
7 +
6 8 /**
7 9 * Base class for all Imagify MCP abilities.
8 10 *
9 11 * Provides the `check_permissions()` template method (fires the
10 - * `imagify_mcp_permission_denied` action on denial) and the
11 - * `fire_executed()` helper used by concrete `execute()` implementations
12 - * to fire `imagify_mcp_ability_executed` after every invocation.
12 + * `imagify_mcp_permission_denied` action on denial), the `fire_executed()`
13 + * helper used by concrete `execute()` implementations to fire
14 + * `imagify_mcp_ability_executed` after every invocation, and the
15 + * `guard_credit_confirmation()` template method reused by every
16 + * credit-consuming ability.
13 17 *
14 18 * @since 2.3.0
15 19 */
16 20 abstract class AbstractAbility implements AbilitiesInterface {
@@ -36,8 +40,22 @@
36 40 */
37 41 abstract protected function has_permission(): bool;
38 42
39 43 /**
44 + * Returns the capability name reported to `imagify_mcp_permission_denied`
45 + * when `has_permission()` denies access.
46 + *
47 + * Overridable so abilities whose `has_permission()` checks a capability
48 + * other than the Imagify `manage` capability (e.g. `manage_options`)
49 + * report the real required capability in tracking/analytics.
50 + *
51 + * @return string
52 + */
53 + protected function get_required_capability(): string {
54 + return 'manage';
55 + }
56 +
57 + /**
40 58 * Check if the current user has permission to execute this ability.
41 59 *
42 60 * Delegates the capability check to has_permission() and fires
43 61 * `imagify_mcp_permission_denied` when access is denied so that
@@ -48,9 +66,9 @@
48 66 public function check_permissions(): bool {
49 67 $allowed = $this->has_permission();
50 68
51 69 if ( ! $allowed ) {
52 - do_action( 'imagify_mcp_permission_denied', $this->get_id(), $this->get_name(), 'manage' );
70 + do_action( 'imagify_mcp_permission_denied', $this->get_id(), $this->get_name(), $this->get_required_capability() );
53 71 }
54 72
55 73 return $allowed;
56 74 }
@@ -67,6 +85,174 @@
67 85 * @return void
68 86 */
69 87 protected function fire_executed( $result, float $start_time, array $args = [] ): void {
70 88 do_action( 'imagify_mcp_ability_executed', $this->get_id(), $this->get_name(), $result, $start_time, $args );
89 + }
90 +
91 + /**
92 + * Fetch an initialized Imagify User instance.
93 + *
94 + * Extracted into a protected method so that unit tests can override
95 + * this call without needing to bootstrap the full Imagify API layer.
96 + *
97 + * @return User
98 + */
99 + protected function fetch_user(): User {
100 + $user = new User();
101 + $user->init_user();
102 + return $user;
103 + }
104 +
105 + /**
106 + * Shared pre-flight guard for credit-consuming abilities.
107 + *
108 + * Implements a 4-step flow, in this exact order:
109 + * 1. If the Imagify API key is invalid, returns an `invalid_api_key`
110 + * response — `$run` is never invoked.
111 + * 2. If the account is over quota, returns an `insufficient_quota`
112 + * response — `$run` is never invoked.
113 + * 3. If `$args['confirm']` is not strictly `true`, returns a
114 + * `confirmation_required` response built from `get_impact_estimate()` —
115 + * `$run` is never invoked.
116 + * 4. Otherwise calls `$run( $args )` and returns its result unchanged.
117 + *
118 + * Callers MUST pass a closure created inside the defining ability class
119 + * (e.g. `function ( array $a ) { return $this->do_execute( $a ); }`),
120 + * never a `[ $this, 'method' ]` callable-array: a private target method's
121 + * visibility is resolved against the scope that invokes it, which is this
122 + * method on `AbstractAbility` — not the ability class where the private
123 + * method is declared.
124 + *
125 + * @param array $args Raw input arguments passed to execute().
126 + * @param callable $run Closure invoked with `$args` once confirmed and
127 + * quota/API-key checks pass.
128 + * @return array
129 + */
130 + protected function guard_credit_confirmation( array $args, callable $run ): array {
131 + if ( ! \Imagify_Requirements::is_api_key_valid() ) {
132 + return $this->invalid_api_key_response();
133 + }
134 +
135 + if ( \Imagify_Requirements::is_over_quota() ) {
136 + return $this->insufficient_quota_response();
137 + }
138 +
139 + if ( true !== ( $args['confirm'] ?? null ) ) {
140 + return $this->confirmation_required_response( $args );
141 + }
142 +
143 + return $run( $args );
144 + }
145 +
146 + /**
147 + * Builds the `invalid_api_key` guard response.
148 + *
149 + * @return array{status: string, message: string}
150 + */
151 + private function invalid_api_key_response(): array {
152 + return [
153 + 'status' => 'invalid_api_key',
154 + 'message' => __( 'Your Imagify API key is invalid or missing. Update it in the Imagify settings before retrying.', 'imagify' ),
155 + ];
156 + }
157 +
158 + /**
159 + * Builds the `insufficient_quota` guard response.
160 + *
161 + * @return array{status: string, message: string, next_date_update: string, upgrade_url: string}
162 + */
163 + private function insufficient_quota_response(): array {
164 + $user = $this->fetch_user();
165 +
166 + return [
167 + 'status' => 'insufficient_quota',
168 + 'message' => __( 'Your Imagify quota is exhausted. Wait for the next reset date or upgrade your plan to continue.', 'imagify' ),
169 + 'next_date_update' => $user->next_date_update ? (string) $user->next_date_update : '',
170 + 'upgrade_url' => imagify_get_external_url(
171 + 'subscription',
172 + [
173 + 'utm_source' => 'plugin',
174 + 'utm_medium' => 'imagify-wp',
175 + 'utm_content' => 'over-quota',
176 + ]
177 + ),
178 + ];
179 + }
180 +
181 + /**
182 + * Builds the `confirmation_required` guard response.
183 + *
184 + * The confirmation step is kept for every account, but the messaging adapts
185 + * to the plan: quota-limited accounts get the credit-consumption wording plus
186 + * a `quota_remaining` figure, while Infinite accounts (whose plans have no
187 + * per-image quota to consume) get operation-focused wording and no
188 + * `quota_remaining` key.
189 + *
190 + * @param array $args Raw input arguments passed to execute().
191 + * @return array{status: string, message: string, impact: array, quota_remaining?: float, confirm_with: array}
192 + */
193 + private function confirmation_required_response( array $args ): array {
194 + $impact = $this instanceof CreditConsumingAbilityInterface ? $this->get_impact_estimate( $args ) : [];
195 +
196 + $unit = isset( $impact['unit'] ) ? (string) $impact['unit'] : 'image';
197 + $count = isset( $impact['count'] ) ? (int) $impact['count'] : 0;
198 + $label = isset( $impact['label'] ) ? (string) $impact['label'] : $unit;
199 +
200 + $impact_response = [
201 + 'unit' => $unit,
202 + 'count' => $count,
203 + ];
204 +
205 + if ( isset( $impact['total'] ) ) {
206 + $impact_response['total'] = (int) $impact['total'];
207 + }
208 +
209 + $user = $this->fetch_user();
210 + $is_infinite = $user->is_infinite();
211 + $has_total = isset( $impact['total'] );
212 +
213 + if ( $is_infinite ) {
214 + $message = $has_total
215 + ? sprintf(
216 + /* translators: 1: number of units about to be processed, 2: total number of units, 3: unit label */
217 + __( 'This action will process %1$d of %2$d %3$s. Add "confirm": true to the same call to proceed.', 'imagify' ),
218 + $count,
219 + (int) $impact['total'],
220 + $label
221 + )
222 + : sprintf(
223 + /* translators: 1: number of units about to be processed, 2: unit label */
224 + __( 'This action will process %1$d %2$s. Add "confirm": true to the same call to proceed.', 'imagify' ),
225 + $count,
226 + $label
227 + );
228 + } else {
229 + $message = $has_total
230 + ? sprintf(
231 + /* translators: 1: number of units about to be consumed, 2: total number of units, 3: unit label */
232 + __( 'This action will consume Imagify quota: %1$d of %2$d %3$s. Add "confirm": true to the same call to proceed.', 'imagify' ),
233 + $count,
234 + (int) $impact['total'],
235 + $label
236 + )
237 + : sprintf(
238 + /* translators: 1: number of units about to be consumed, 2: unit label */
239 + __( 'This action will consume Imagify quota: %1$d %2$s. Add "confirm": true to the same call to proceed.', 'imagify' ),
240 + $count,
241 + $label
242 + );
243 + }
244 +
245 + $response = [
246 + 'status' => 'confirmation_required',
247 + 'message' => $message,
248 + 'impact' => $impact_response,
249 + 'confirm_with' => [ 'confirm' => true ],
250 + ];
251 +
252 + if ( ! $is_infinite ) {
253 + $response['quota_remaining'] = (float) $user->get_percent_unconsumed_quota();
254 + }
255 +
256 + return $response;
71 257 }
72 258 }