PluginProbe ʕ •ᴥ•ʔ
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.7.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.7.1
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.11.1 1.11.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.13.0 2.14.0 2.14.1 2.15.0 2.15.1 2.16.0 2.16.1 2.16.2 2.16.3 2.17.0 2.17.1 2.17.2 2.18.0 2.19.0 2.19.2 2.19.3 2.19.4 2.2.0 2.2.1 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.22.0 2.22.1 2.23.0 2.24.0 2.25.0 2.25.1 2.25.2 2.26.0 2.27.0 2.27.1 2.28.0 2.29.0 2.29.1 2.29.2 2.29.3 2.29.4 2.3.0 2.3.1 2.30.0 2.31.0 2.31.1 2.31.2 2.31.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.40.0 2.40.1 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.9.0 3.0.0 3.0.0-RC1 3.0.0-RC2 3.0.0-beta1 3.0.0-beta2 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.10.0 3.10.1 3.11.0 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.13.4 3.14.0 3.15.0 3.15.1 3.15.2 3.15.3 3.15.4 3.15.5 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.16.6 3.16.7 3.16.8 3.17.0 3.17.1 3.17.2 3.17.3 3.17.4 3.17.5 3.17.6 3.18.0 3.19.0 3.19.1 3.19.2 3.2.0 3.2.1 3.2.2 3.20.0 3.20.1 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 trunk 4.1.0 0.2.19.1 4.1.1 1.0.0 4.2.0 1.0.1 1.0.2
surecart / app / src / Abilities / Abilities / AbstractAbility.php
surecart / app / src / Abilities / Abilities Last commit date
AbstractAbility.php 2 weeks ago ArchiveProduct.php 4 months ago CancelSubscription.php 4 months ago CreateCoupon.php 4 months ago CreateCustomer.php 4 months ago CreateFulfillment.php 4 months ago CreateInvoice.php 2 months ago CreatePrice.php 2 months ago CreateProduct.php 4 months ago CreatePromotion.php 4 months ago CreateRefund.php 2 months ago DeleteCoupon.php 4 months ago DeleteCustomer.php 4 months ago DeleteFulfillment.php 4 months ago DeletePromotion.php 4 months ago DuplicateProduct.php 4 months ago FilterCustomers.php 2 months ago FilterOrders.php 2 months ago FilterSubscriptions.php 2 months ago GetAbandonedCheckout.php 3 months ago GetCoupon.php 4 months ago GetCustomer.php 4 months ago GetCustomersFilterSchema.php 2 months ago GetFulfillment.php 4 months ago GetFulfillmentItem.php 4 months ago GetLicense.php 4 months ago GetOrder.php 4 months ago GetOrderStatistics.php 4 months ago GetOrdersFilterSchema.php 2 months ago GetProduct.php 4 months ago GetPromotion.php 4 months ago GetRefund.php 4 months ago GetStoreDashboard.php 4 months ago GetStoreInfo.php 4 months ago GetSubscription.php 4 months ago GetSubscriptionsFilterSchema.php 2 months ago ListAbandonedCheckouts.php 3 months ago ListCoupons.php 4 months ago ListCustomers.php 4 months ago ListFulfillments.php 4 months ago ListLicenses.php 4 months ago ListOrders.php 4 months ago ListPrices.php 4 months ago ListProducts.php 4 months ago ListPromotions.php 4 months ago ListRefunds.php 4 months ago ListSubscriptions.php 4 months ago UpdateCoupon.php 4 months ago UpdateCustomer.php 4 months ago UpdateFulfillment.php 4 months ago UpdateInvoice.php 4 months ago UpdatePrice.php 4 months ago UpdateProduct.php 4 months ago UpdatePromotion.php 4 months ago UpdateSubscriptionRenewalDate.php 4 months ago
AbstractAbility.php
328 lines
1 <?php
2
3 namespace SureCart\Abilities\Abilities;
4
5 /**
6 * Base class for all SureCart abilities.
7 */
8 abstract class AbstractAbility {
9
10 /**
11 * Get the ability name (e.g., 'surecart/list-products').
12 *
13 * @return string
14 */
15 abstract public function get_name(): string;
16
17 /**
18 * Get the ability label.
19 *
20 * @return string
21 */
22 abstract public function get_label(): string;
23
24 /**
25 * Get the ability description.
26 *
27 * @return string
28 */
29 abstract public function get_description(): string;
30
31 /**
32 * Get the ability annotations.
33 *
34 * Returns an array with three boolean keys:
35 * - 'readonly' — true if the ability only reads data (maps to GET).
36 * - 'destructive' — true if the ability deletes or irreversibly modifies data.
37 * - 'idempotent' — true if calling multiple times produces the same result.
38 *
39 * @return array{readonly: bool, destructive: bool, idempotent: bool}
40 */
41 abstract public function get_annotations(): array;
42
43 /**
44 * Get AI-facing instructions for when and how to use this ability.
45 *
46 * Override in subclasses to provide guidance on edge cases,
47 * confirmation requirements, and usage patterns.
48 *
49 * @return string
50 */
51 public function get_instructions(): string {
52 return '';
53 }
54
55 /**
56 * Get the JSON Schema for the input.
57 *
58 * @return array
59 */
60 abstract public function get_input_schema(): array;
61
62 /**
63 * Get the JSON Schema for the output.
64 *
65 * @return array
66 */
67 abstract public function get_output_schema(): array;
68
69 /**
70 * Execute the ability.
71 *
72 * @param array $input The input data.
73 *
74 * @return array|\WP_Error The result array on success, or WP_Error on failure.
75 */
76 abstract public function execute( array $input );
77
78 /**
79 * Check if the current user has permission to execute this ability.
80 *
81 * @return bool
82 */
83 public function check_permission(): bool {
84 return false;
85 }
86
87 /**
88 * Get the full configuration array for wp_register_ability().
89 *
90 * @return array
91 */
92 public function get_config(): array {
93 return array(
94 'label' => $this->get_label(),
95 'description' => $this->get_description(),
96 'category' => 'surecart-ecommerce',
97 'permission_callback' => array( $this, 'check_permission' ),
98 'input_schema' => $this->get_input_schema(),
99 'output_schema' => $this->get_output_schema(),
100 'execute_callback' => function ( array $input ) {
101 $result = $this->execute( $input );
102 return is_wp_error( $result ) ? $this->extract_detailed_error( $result ) : $result;
103 },
104 'meta' => array(
105 'public' => true, // Unified exposure flag (WP 7.1+).
106 'show_in_rest' => true,
107 'annotations' => $this->get_annotations(),
108 'instructions' => $this->get_instructions(),
109 'mcp' => array(
110 'public' => true,
111 ),
112 ),
113 );
114 }
115
116 /**
117 * Return a success response.
118 *
119 * @param array $data The response data.
120 *
121 * @return array
122 */
123 protected function success( array $data = array() ): array {
124 return array_merge( array( 'success' => true ), $data );
125 }
126
127 /**
128 * Return a WP_Error for a validation or logic error.
129 *
130 * @param string $code The error code.
131 * @param string $message The error message.
132 *
133 * @return \WP_Error
134 */
135 protected function error( string $code, string $message ): \WP_Error {
136 return new \WP_Error( $code, $message );
137 }
138
139 /**
140 * Extract a detailed error message from a WP_Error, including validation errors.
141 *
142 * The SureCart API often returns a generic top-level message (e.g. "There were some
143 * validation errors") with field-level details in additional error entries. This method
144 * combines all error messages into a single, actionable WP_Error.
145 *
146 * @param \WP_Error $wp_error The WP_Error to process.
147 *
148 * @return \WP_Error A WP_Error with a detailed, combined message.
149 */
150 private function extract_detailed_error( \WP_Error $wp_error ): \WP_Error {
151 $messages = $wp_error->get_error_messages();
152
153 // If there's only one message, return as-is.
154 if ( count( $messages ) <= 1 ) {
155 return $wp_error;
156 }
157
158 // Combine all messages: skip the generic first message if there are field-level details.
159 // Deduplicate to avoid repeated messages (e.g. "Amount is invalid. Amount is invalid.").
160 $detail_messages = array_unique( array_slice( $messages, 1 ) );
161 $combined = implode( ' ', $detail_messages );
162
163 return new \WP_Error(
164 $wp_error->get_error_code(),
165 $combined,
166 $wp_error->get_error_data()
167 );
168 }
169
170 /**
171 * Normalize ability input that may be a JSON string or an array (e.g. from form fields).
172 *
173 * @param mixed $value Raw input.
174 *
175 * @return array|null The decoded or original array, or null if not a valid array.
176 */
177 protected function parse_json_or_array( $value ): ?array {
178 if ( is_string( $value ) ) {
179 $value = json_decode( $value, true );
180 }
181 return is_array( $value ) ? $value : null;
182 }
183
184 /**
185 * Recursively sanitize and structurally validate a filter rule tree.
186 *
187 * Rebuilds a clean tree from known keys only, dropping any unexpected keys,
188 * whitelisting type and combinator, and sanitizing the scalar leaves.
189 *
190 * @param mixed $rules The raw rule tree (condition or group).
191 * @param int $depth Current recursion depth, used to bound deeply nested input.
192 *
193 * @return array|\WP_Error Clean rule tree, or WP_Error if structurally invalid.
194 */
195 protected function sanitize_rule_tree( $rules, $depth = 0 ) {
196 if ( $depth > 10 ) {
197 return new \WP_Error( 'invalid_filter', __( 'Filter rule tree is too deeply nested.', 'surecart' ) );
198 }
199
200 if ( ! is_array( $rules ) || empty( $rules ) ) {
201 return new \WP_Error( 'invalid_filter', __( 'The filter must be a condition or group object.', 'surecart' ) );
202 }
203
204 $type = sanitize_text_field( $rules['type'] ?? '' );
205
206 if ( 'group' === $type ) {
207 $combinator = sanitize_text_field( $rules['combinator'] ?? '' );
208 if ( ! in_array( $combinator, array( 'and', 'or' ), true ) ) {
209 return new \WP_Error( 'invalid_filter', __( 'A group combinator must be "and" or "or".', 'surecart' ) );
210 }
211
212 $conditions = $rules['conditions'] ?? array();
213 if ( ! is_array( $conditions ) || empty( $conditions ) ) {
214 return new \WP_Error( 'invalid_filter', __( 'A group must have at least one condition.', 'surecart' ) );
215 }
216
217 if ( count( $conditions ) > 50 ) {
218 return new \WP_Error( 'invalid_filter', __( 'A group has too many conditions (max 50).', 'surecart' ) );
219 }
220
221 $clean = array();
222 foreach ( $conditions as $condition ) {
223 $sanitized = $this->sanitize_rule_tree( $condition, $depth + 1 );
224 if ( is_wp_error( $sanitized ) ) {
225 return $sanitized;
226 }
227 $clean[] = $sanitized;
228 }
229
230 return array(
231 'type' => 'group',
232 'combinator' => $combinator,
233 'conditions' => $clean,
234 );
235 }
236
237 if ( 'condition' === $type ) {
238 $value = $rules['comparison_value'] ?? '';
239 return array(
240 'type' => 'condition',
241 'attribute_name' => sanitize_text_field( $rules['attribute_name'] ?? '' ),
242 'operator_label' => sanitize_text_field( $rules['operator_label'] ?? '' ),
243 'comparison_value' => is_scalar( $value ) ? sanitize_text_field( (string) $value ) : '',
244 );
245 }
246
247 return new \WP_Error( 'invalid_filter', __( 'Each rule must have a type of "condition" or "group".', 'surecart' ) );
248 }
249
250 /**
251 * Validate a date string is in YYYY-MM-DD format.
252 *
253 * @param string $date The date string to validate.
254 *
255 * @return bool
256 */
257 protected function is_valid_date( string $date ): bool {
258 $d = \DateTime::createFromFormat( 'Y-m-d', $date );
259 return $d && $d->format( 'Y-m-d' ) === $date;
260 }
261
262 /**
263 * Validate and normalize stats query args from ability input.
264 *
265 * Accepts optional YYYY-MM-DD date strings and passes them directly
266 * to the SureCart statistics API.
267 *
268 * @param array $input The raw ability input.
269 *
270 * @return array|\WP_Error Normalized args array, or WP_Error on validation failure.
271 */
272 protected function resolve_stats_args( array $input ) {
273 $allowed_intervals = array( 'hour', 'day', 'week', 'month', 'year' );
274 $interval = sanitize_text_field( $input['interval'] ?? 'day' );
275 if ( ! in_array( $interval, $allowed_intervals, true ) ) {
276 return new \WP_Error(
277 'invalid_interval',
278 /* translators: %s: comma-separated list of valid interval values */
279 sprintf( __( 'Invalid interval. Allowed values: %s', 'surecart' ), implode( ', ', $allowed_intervals ) )
280 );
281 }
282
283 $args = array( 'interval' => $interval );
284
285 if ( ! empty( $input['start_date'] ) ) {
286 $start_date = sanitize_text_field( $input['start_date'] );
287 if ( ! $this->is_valid_date( $start_date ) ) {
288 return new \WP_Error( 'invalid_date', __( 'start_date must be in YYYY-MM-DD format.', 'surecart' ) );
289 }
290 $args['start_at'] = $start_date;
291 }
292
293 if ( ! empty( $input['end_date'] ) ) {
294 $end_date = sanitize_text_field( $input['end_date'] );
295 if ( ! $this->is_valid_date( $end_date ) ) {
296 return new \WP_Error( 'invalid_date', __( 'end_date must be in YYYY-MM-DD format.', 'surecart' ) );
297 }
298 $args['end_at'] = $end_date;
299 }
300
301 return $args;
302 }
303
304 /**
305 * Convert a model object to an array, handling nested objects.
306 *
307 * @param mixed $model The model or data to convert.
308 *
309 * @return array
310 */
311 protected function model_to_array( $model ): array {
312 if ( is_array( $model ) ) {
313 return $model;
314 }
315
316 if ( $model instanceof \JsonSerializable ) {
317 $data = $model->jsonSerialize();
318 return is_array( $data ) ? $data : array();
319 }
320
321 if ( is_object( $model ) ) {
322 return (array) $model;
323 }
324
325 return array();
326 }
327 }
328