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

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

259 lines 8.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 use Imagify\User\User;
7
8 /**
9 * Base class for all Imagify MCP abilities.
10 *
11 * Provides the `check_permissions()` template method (fires the
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.
17 *
18 * @since 2.3.0
19 */
20 abstract class AbstractAbility implements AbilitiesInterface {
21
22 /**
23 * Returns the ability slug used to identify this ability in hooks and tracking.
24 *
25 * @return string
26 */
27 abstract public function get_id(): string;
28
29 /**
30 * Returns the human-readable ability label used in hooks and tracking.
31 *
32 * @return string
33 */
34 abstract public function get_name(): string;
35
36 /**
37 * Internal permission check delegated by check_permissions().
38 *
39 * @return bool True when the current user may execute the ability.
40 */
41 abstract protected function has_permission(): bool;
42
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 /**
58 * Check if the current user has permission to execute this ability.
59 *
60 * Delegates the capability check to has_permission() and fires
61 * `imagify_mcp_permission_denied` when access is denied so that
62 * tracking and logging subscribers can react.
63 *
64 * @return bool True when the current user may execute the ability.
65 */
66 public function check_permissions(): bool {
67 $allowed = $this->has_permission();
68
69 if ( ! $allowed ) {
70 do_action( 'imagify_mcp_permission_denied', $this->get_id(), $this->get_name(), $this->get_required_capability() );
71 }
72
73 return $allowed;
74 }
75
76 /**
77 * Fire the `imagify_mcp_ability_executed` action after execute() resolves.
78 *
79 * Called by every concrete execute() so that tracking and other subscribers
80 * receive the result for both success and failure outcomes.
81 *
82 * @param mixed $result Return value of the ability's do_execute().
83 * @param float $start_time microtime(true) captured before do_execute() ran.
84 * @param array $args Raw input args forwarded from execute().
85 * @return void
86 */
87 protected function fire_executed( $result, float $start_time, array $args = [] ): void {
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;
257 }
258 }
259