PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 All 129 releases
wdesignkit / includes / abilities / class-wdk-ability-lifecycle.php

class-wdk-ability-lifecycle.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/abilities/class-wdk-ability-lifecycle.php

644 lines 25.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WDesignKit Abilities — execution lifecycle integration (WordPress 7.1).
4 *
5 * WordPress 7.1 added a set of filters and one action around WP_Ability::execute(), so a
6 * plugin can hook the shared parts of every ability's run instead of repeating them in 95
7 * separate execute callbacks:
8 *
9 * wp_pre_execute_ability short-circuit the whole pipeline
10 * wp_ability_normalize_input transform normalized input before validation
11 * wp_ability_validate_input supplement JSON Schema input validation
12 * wp_ability_permission_result modify/override the permission_callback result
13 * wp_ability_execute_result transform or recover the execute callback's result
14 * wp_ability_validate_output supplement JSON Schema output validation
15 * wp_ability_invoked (action) audit hook, fires before anything else
16 *
17 * Every handler here is scoped to the `wdesignkit/` namespace and returns its incoming
18 * value untouched for anything else, so a third-party ability is never affected.
19 *
20 * VERSION BEHAVIOUR: the plugin supports WordPress 6.0+ (readme.txt "Requires at least"),
21 * and the Abilities API itself only landed in 7.0. None of the seven hook names above exist
22 * before 7.1, so on an older site every add_filter()/add_action() for them simply never
23 * fires — this file is inert rather than fatal, and needs no version_compare() guard.
24 *
25 * That inertness is exactly why capability enforcement must NOT live only in those hooks.
26 * `wp_ability_permission_result` is 7.1-only, so on 7.0.x — the only shipping release with
27 * an Abilities API — a `meta.required_capability` enforced solely there was not enforced at
28 * all, and `wdesignkit/rollback` and `wdesignkit/remove-database` were gated by nothing but
29 * the shared manage_options floor. The declaration is therefore ALSO applied at registration
30 * time, in wdk_enforce_required_capability(), via `wp_register_ability_args` — a filter that
31 * has existed since the API shipped. See that method for the reproduction and the reasoning.
32 *
33 * So the baseline on every supported version is: the manage_options floor in
34 * wdesignkit_mcp_permission_callback(), plus wdk_enforce_required_capability() for any
35 * ability declaring a capability, plus each ability's own in-callback checks
36 * (wdesignkit_widget_path_guard(), and the current_user_can() call in
37 * wdesignkit-dependencies.php). The 7.1 handlers below are additional layers on top of
38 * that, never the only thing standing between a caller and a destructive operation.
39 *
40 * @link https://make.wordpress.org/core/2026/07/29/new-execution-lifecycle-filters-for-the-abilities-api-in-wordpress-7-1/
41 * @link https://make.wordpress.org/core/2026/07/31/abilities-api-improvements-in-wordpress-7-1/
42 * @since 2.6.5
43 *
44 * @package Wdesignkit
45 * @subpackage Wdesignkit/includes/abilities
46 */
47
48 if ( ! defined( 'ABSPATH' ) ) {
49 exit;
50 }
51
52 if ( ! class_exists( 'Wdk_Ability_Lifecycle' ) ) {
53
54 /**
55 * Binds WDesignKit behaviour onto the WordPress 7.1 ability execution lifecycle.
56 *
57 * @since 2.6.5
58 */
59 class Wdk_Ability_Lifecycle {
60
61 /**
62 * Ability-name prefix this class acts on.
63 *
64 * @since 2.6.5
65 * @var string
66 */
67 const NAMESPACE_PREFIX = 'wdesignkit/';
68
69 /**
70 * Input keys treated as filesystem-ish and therefore traversal-checked.
71 *
72 * Matched case-insensitively against the *leaf* key name anywhere in the input
73 * tree, so `folder`, `widget_folder` and `files[0][file_name]` are all covered.
74 *
75 * @since 2.6.5
76 * @var string[]
77 */
78 const PATH_KEY_SUFFIXES = array( 'folder', 'file', 'file_name', 'filename', 'path', 'dir', 'directory' );
79
80 /**
81 * Singleton instance.
82 *
83 * @since 2.6.5
84 * @var Wdk_Ability_Lifecycle|null
85 */
86 private static $instance = null;
87
88 /**
89 * Instantiate once.
90 *
91 * @since 2.6.5
92 * @return Wdk_Ability_Lifecycle
93 */
94 public static function instance() {
95 if ( is_null( self::$instance ) ) {
96 self::$instance = new self();
97 }
98
99 return self::$instance;
100 }
101
102 /**
103 * Register the lifecycle hooks.
104 *
105 * @since 2.6.5
106 */
107 public function __construct() {
108 add_filter( 'wp_register_ability_args', array( $this, 'wdk_register_ability_args' ), 10, 2 );
109 add_action( 'wp_ability_invoked', array( $this, 'wdk_on_ability_invoked' ), 10, 3 );
110 add_filter( 'wp_pre_execute_ability', array( $this, 'wdk_pre_execute_ability' ), 10, 4 );
111 add_filter( 'wp_ability_normalize_input', array( $this, 'wdk_normalize_input' ), 10, 3 );
112 add_filter( 'wp_ability_validate_input', array( $this, 'wdk_validate_input' ), 10, 3 );
113 add_filter( 'wp_ability_permission_result', array( $this, 'wdk_permission_result' ), 10, 4 );
114 add_filter( 'wp_ability_execute_result', array( $this, 'wdk_execute_result' ), 10, 4 );
115 }
116
117 /**
118 * Whether a given ability name belongs to WDesignKit.
119 *
120 * @since 2.6.5
121 *
122 * @param mixed $ability_name Ability name as passed by core.
123 * @return bool
124 */
125 private function is_wdk_ability( $ability_name ) {
126 return is_string( $ability_name ) && 0 === strpos( $ability_name, self::NAMESPACE_PREFIX );
127 }
128
129 /**
130 * Safety net so a new ability is never accidentally hidden from AI/MCP clients.
131 *
132 * All 95 abilities declare `meta.public` explicitly, which is where the flag belongs —
133 * exposure is security-relevant, so it should be readable next to the ability rather
134 * than injected from somewhere else. This filter therefore never overrides a declared
135 * value; it exists purely for the ability nobody has written yet.
136 *
137 * The trap it closes: in 7.1 core resolves an absent `meta.public` to **false**. A new
138 * WDesignKit ability added later that follows the existing house pattern (`show_in_rest`
139 * plus `mcp.public`) but forgets the new top-level flag would register fine, work over
140 * REST, and be silently invisible to any client reading the unified flag — a failure
141 * with no error to notice. Deriving the default from `mcp.public`, which every ability
142 * file already carries, makes the two agree by construction.
143 *
144 * Derives ONLY from `mcp.public`, deliberately not from `show_in_rest`. `mcp.public` is
145 * a statement about agent-facing exposure, which is what `public` means; `show_in_rest`
146 * is REST-only, and the 7.1 guidance is to keep using it directly when exposure is
147 * meant to stay limited to REST. Inferring `public` from it would silently widen
148 * exposure, so an ability declaring neither is left alone and core's `false` stands.
149 *
150 * @since 2.6.5
151 *
152 * @param mixed $args Registration args being filtered.
153 * @param string $ability_name Ability being registered.
154 * @return mixed $args, with meta.public defaulted where it was absent.
155 */
156 public function wdk_register_ability_args( $args, $ability_name ) {
157 if ( ! is_array( $args ) || ! $this->is_wdk_ability( $ability_name ) ) {
158 return $args;
159 }
160
161 $args = $this->wdk_enforce_required_capability( $args, $ability_name );
162
163 // An explicit declaration always wins — this is only a default.
164 if ( isset( $args['meta']['public'] ) ) {
165 return $args;
166 }
167
168 if ( isset( $args['meta']['mcp']['public'] ) ) {
169 $args['meta']['public'] = (bool) $args['meta']['mcp']['public'];
170 }
171
172 return $args;
173 }
174
175 /**
176 * Makes `meta.required_capability` binding on every version that has an Abilities API.
177 *
178 * wdk_permission_result() below enforces the same declaration, but it hangs off
179 * `wp_ability_permission_result`, which does not exist before WordPress 7.1. That left
180 * the declaration completely unenforced on 7.0.x — the only shipping release with the
181 * Abilities API — where the sole gate was the shared manage_options floor in
182 * wdesignkit_mcp_permission_callback(). Two destructive abilities relied on it:
183 * `wdesignkit/rollback` (update_plugins) and `wdesignkit/remove-database`
184 * (delete_plugins). Reproduced on WordPress 7.0.4 with a role holding manage_options but
185 * neither of those caps: both callbacks ran to completion, remove-database on its real
186 * `action=execute, confirm=true` branch — nothing was deleted only because that site's
187 * cleanup config happened to be empty. The gap is widest on multisite, where a site
188 * administrator legitimately has manage_options while update_plugins / delete_plugins
189 * are reserved to the network administrator, which is the separation the declaration
190 * exists to express.
191 *
192 * `wp_register_ability_args` is the right place to close it: it has existed since the
193 * API shipped (6.9/7.0), it runs before core validates the args and instantiates the
194 * ability, and wrapping the callback here means ONE enforcement point covers every
195 * ability that declares the meta — including ones nobody has written yet. Adding
196 * `current_user_can()` inside the two callbacks would fix these two and leave the next
197 * one to repeat the mistake.
198 *
199 * The wrapper only ever tightens: it delegates to the declared callback first and
200 * returns that result verbatim on failure, so an existing denial and its message stand.
201 * On success it returns the original truthy value rather than a literal `true`, because
202 * WP_Ability::execute() compares with `true !==` and a callback returning some other
203 * truthy value must keep failing that comparison exactly as it does today.
204 *
205 * On 7.1 this runs first and wdk_permission_result() re-checks the same caps afterwards.
206 * That is intentional redundancy, not a bug: the filter stays the second layer it is
207 * documented as. Only one `wdesignkit_ability_permission_denied` event fires per denial,
208 * because wdk_permission_result() returns early on an already-false permission.
209 *
210 * @since 2.6.5
211 *
212 * @param array $args Registration args being filtered.
213 * @param string $ability_name Ability being registered.
214 * @return array $args, with permission_callback wrapped when a capability is declared.
215 */
216 private function wdk_enforce_required_capability( $args, $ability_name ) {
217 $caps = isset( $args['meta']['required_capability'] ) ? $args['meta']['required_capability'] : array();
218 $caps = is_array( $caps ) ? $caps : array( $caps );
219
220 $caps = array_values(
221 array_filter(
222 $caps,
223 static function ( $cap ) {
224 return is_string( $cap ) && '' !== $cap;
225 }
226 )
227 );
228
229 if ( empty( $caps ) || empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) {
230 return $args;
231 }
232
233 $inner = $args['permission_callback'];
234
235 /*
236 * $input carries a default because core omits the argument entirely for an ability with
237 * no input schema — see WP_Ability::invoke_callback(), which only appends $input when
238 * get_input_schema() is non-empty. A closure requiring one parameter would fatal there.
239 */
240 $args['permission_callback'] = static function ( $input = null ) use ( $inner, $caps, $ability_name ) {
241 $permission = call_user_func( $inner, $input );
242
243 if ( is_wp_error( $permission ) || ! $permission ) {
244 return $permission;
245 }
246
247 foreach ( $caps as $cap ) {
248 if ( ! current_user_can( $cap ) ) {
249 /** This action is documented in includes/abilities/class-wdk-ability-lifecycle.php */
250 do_action( 'wdesignkit_ability_permission_denied', $ability_name, $cap, get_current_user_id() );
251
252 return false;
253 }
254 }
255
256 return $permission;
257 };
258
259 return $args;
260 }
261
262 /**
263 * Audit hook: fires at the very top of WP_Ability::execute().
264 *
265 * Re-exposed as `wdesignkit_ability_invoked` so the plugin's own tracking layer (and
266 * site owners) can observe ability traffic from one place instead of instrumenting
267 * every callback. Deliberately does no work of its own and sends nothing anywhere.
268 *
269 * IMPORTANT for listeners: `$input` here is the RAW argument passed to execute() —
270 * core has not yet applied schema defaults, normalized it, validated it, or run the
271 * permission check. Treat it as untrusted, never log it verbatim (ability input
272 * carries cloud tokens, widget source and template payloads), and do not assume the
273 * ability actually ran — an invoked ability can still fail permissions or validation.
274 *
275 * @since 2.6.5
276 *
277 * @param string $ability_name Ability being executed.
278 * @param mixed $input Raw, unnormalized input.
279 * @param mixed $ability WP_Ability instance.
280 * @return void
281 */
282 public function wdk_on_ability_invoked( $ability_name, $input, $ability = null ) {
283 if ( ! $this->is_wdk_ability( $ability_name ) ) {
284 return;
285 }
286
287 /**
288 * Fires when a WDesignKit ability is invoked, before any validation runs.
289 *
290 * @since 2.6.5
291 *
292 * @param string $ability_name Ability name.
293 * @param mixed $input Raw, unvalidated input. Do not log verbatim.
294 * @param mixed $ability WP_Ability instance.
295 */
296 do_action( 'wdesignkit_ability_invoked', $ability_name, $input, $ability );
297 }
298
299 /**
300 * Operator kill switch for the whole WDesignKit ability surface.
301 *
302 * Some site owners want the plugin's UI without exposing 95 admin-level abilities to
303 * REST/MCP clients at all. Before 7.1 the only way to get that was to unregister each
304 * ability; `wp_pre_execute_ability` gives one enforcement point that stops execution
305 * before normalization, validation and the permission check:
306 *
307 * add_filter( 'wdesignkit_abilities_enabled', '__return_false' );
308 *
309 * This blocks *execution*, not registration — a disabled ability is still listed, and
310 * still answers wdesignkit/list-abilities, so a client gets an explicit WP_Error
311 * rather than a mysteriously absent tool.
312 *
313 * @since 2.6.5
314 *
315 * @param mixed $pre Precomputed result; returned unchanged to continue.
316 * @param string $ability_name Ability being executed.
317 * @param mixed $input Raw input.
318 * @param mixed $ability WP_Ability instance.
319 * @return mixed Unchanged $pre, or WP_Error to short-circuit.
320 */
321 public function wdk_pre_execute_ability( $pre, $ability_name, $input = null, $ability = null ) {
322 if ( ! $this->is_wdk_ability( $ability_name ) ) {
323 return $pre;
324 }
325
326 /**
327 * Filters whether WDesignKit abilities may execute at all.
328 *
329 * @since 2.6.5
330 *
331 * @param bool $enabled Whether execution is allowed. Default true.
332 * @param string $ability_name Ability being executed.
333 */
334 $enabled = (bool) apply_filters( 'wdesignkit_abilities_enabled', true, $ability_name );
335
336 if ( ! $enabled ) {
337 return new \WP_Error(
338 'wdkit_abilities_disabled',
339 __( 'WDesignKit abilities are disabled on this site.', 'wdesignkit' ),
340 array( 'status' => 403 )
341 );
342 }
343
344 return $pre;
345 }
346
347 /**
348 * Strips NUL and C0 control bytes from every string in the input tree.
349 *
350 * Runs after core applies schema defaults and before validation, so each callback
351 * sees clean strings. JSON Schema has no way to express "no control characters", and
352 * these bytes are the classic way to smuggle a payload past a later check: a NUL
353 * truncates the string for C-level filesystem calls, so `widget.php\0.txt` passes an
354 * extension check and then opens `widget.php`. Handled centrally here because the
355 * callbacks receive names, slugs and paths in dozens of shapes.
356 *
357 * Tabs, newlines and carriage returns are preserved — widget/snippet source code and
358 * template markup travel through these inputs and legitimately contain them.
359 *
360 * @since 2.6.5
361 *
362 * @param mixed $input Normalized input.
363 * @param string $ability_name Ability being executed.
364 * @param mixed $ability WP_Ability instance.
365 * @return mixed Input with control bytes removed from all strings.
366 */
367 public function wdk_normalize_input( $input, $ability_name, $ability = null ) {
368 if ( ! $this->is_wdk_ability( $ability_name ) ) {
369 return $input;
370 }
371
372 return $this->strip_control_bytes( $input );
373 }
374
375 /**
376 * Recursively removes NUL/C0 control bytes from strings, preserving \t \n \r.
377 *
378 * @since 2.6.5
379 *
380 * @param mixed $value Value to clean.
381 * @return mixed Cleaned value, same shape as the input.
382 */
383 private function strip_control_bytes( $value ) {
384 if ( is_string( $value ) ) {
385 return preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value );
386 }
387
388 if ( is_array( $value ) ) {
389 foreach ( $value as $key => $item ) {
390 $value[ $key ] = $this->strip_control_bytes( $item );
391 }
392 }
393
394 return $value;
395 }
396
397 /**
398 * Supplementary input validation: rejects path traversal in filesystem-ish params.
399 *
400 * Defense in depth, not a replacement for anything. Widget/template/snippet abilities
401 * already resolve every path through wdesignkit_widget_path_guard() and
402 * wdesignkit_path_inside_builder_dir(), which stay the authoritative check and are the
403 * only check on WordPress < 7.1 (see the version note in this file's header). What
404 * 7.1 adds is the chance to refuse a hostile value once, up front, before any callback
405 * has a chance to concatenate it into a path — so a future ability that forgets to
406 * call the guard is not automatically a traversal bug.
407 *
408 * Only keys whose leaf name looks like a path are checked, so a widget's PHP source
409 * (which contains `../` in comments and require paths perfectly legitimately) is left
410 * alone.
411 *
412 * @since 2.6.5
413 *
414 * @param true|\WP_Error $is_valid Current validity from core/earlier filters.
415 * @param mixed $value Input being validated.
416 * @param string $ability_name Ability being executed.
417 * @return true|\WP_Error True when valid, WP_Error describing the rejection.
418 */
419 public function wdk_validate_input( $is_valid, $value, $ability_name ) {
420 // Never override an existing failure — core's message is the more specific one.
421 if ( is_wp_error( $is_valid ) || ! $this->is_wdk_ability( $ability_name ) ) {
422 return $is_valid;
423 }
424
425 $offender = $this->find_unsafe_path_key( $value );
426
427 if ( null !== $offender ) {
428 return new \WP_Error(
429 'wdkit_unsafe_path_input',
430 sprintf(
431 /* translators: %s: input parameter name */
432 __( 'The "%s" parameter contains a path segment that is not allowed. Pass a plain name without directory separators or "..".', 'wdesignkit' ),
433 $offender
434 ),
435 array( 'status' => 400 )
436 );
437 }
438
439 return true;
440 }
441
442 /**
443 * Walks the input tree and returns the first path-ish key holding an unsafe value.
444 *
445 * Unsafe means: a `..` segment, a forward or back slash, or a NUL byte (which
446 * wdk_normalize_input() should already have removed — checked again here because the
447 * two filters are independent and a site could reorder or remove either).
448 *
449 * @since 2.6.5
450 *
451 * @param mixed $value Value to inspect.
452 * @param string $key_name Leaf key this value arrived under.
453 * @return string|null Offending key name, or null when everything is safe.
454 */
455 private function find_unsafe_path_key( $value, $key_name = '' ) {
456 if ( is_array( $value ) ) {
457 foreach ( $value as $key => $item ) {
458 // Numeric keys are list indexes — keep the parent's key name for context.
459 $child_key = is_int( $key ) ? $key_name : (string) $key;
460
461 $offender = $this->find_unsafe_path_key( $item, $child_key );
462 if ( null !== $offender ) {
463 return $offender;
464 }
465 }
466
467 return null;
468 }
469
470 if ( ! is_string( $value ) || '' === $value || ! $this->is_path_key( $key_name ) ) {
471 return null;
472 }
473
474 if ( false !== strpos( $value, "\0" )
475 || false !== strpos( $value, '/' )
476 || false !== strpos( $value, '\\' )
477 || preg_match( '/(^|[\/\\\\])\.\.($|[\/\\\\])/', $value )
478 ) {
479 return $key_name;
480 }
481
482 return null;
483 }
484
485 /**
486 * Whether an input key name designates a filesystem path/name.
487 *
488 * @since 2.6.5
489 *
490 * @param string $key_name Leaf key name.
491 * @return bool
492 */
493 private function is_path_key( $key_name ) {
494 if ( ! is_string( $key_name ) || '' === $key_name ) {
495 return false;
496 }
497
498 $key_name = strtolower( $key_name );
499
500 foreach ( self::PATH_KEY_SUFFIXES as $suffix ) {
501 if ( $key_name === $suffix || substr( $key_name, - ( strlen( $suffix ) + 1 ) ) === '_' . $suffix ) {
502 return true;
503 }
504 }
505
506 return false;
507 }
508
509 /**
510 * Enforces a declarative `meta.required_capability` on top of the permission callback.
511 *
512 * Every WDesignKit ability shares wdesignkit_mcp_permission_callback(), a single
513 * manage_options gate — correct as a floor, but too coarse for the handful of
514 * abilities that install, roll back or delete. Those now declare the capability they
515 * actually need in their own meta, and this filter enforces it after the shared
516 * callback has passed:
517 *
518 * 'meta' => array( 'required_capability' => 'update_plugins' ),
519 *
520 * The distinction is real on multisite, where a site administrator has manage_options
521 * but install_plugins/update_plugins/delete_plugins are reserved to the network
522 * administrator. Declaring it in meta (rather than hard-coding another callback) also
523 * makes the requirement introspectable by REST/MCP clients.
524 *
525 * This raises the bar on 7.1 only; on older versions the manage_options floor is
526 * unchanged, which is the behaviour those versions have always had.
527 *
528 * DENIES WITH `false`, NOT A WP_Error, deliberately. WP_Ability::execute() treats a
529 * WP_Error from this filter as a developer mistake — it will not "leak the permission
530 * check error to someone without the correct perms", so it discards the message,
531 * routes it through _doing_it_wrong(), and returns a generic
532 * `ability_invalid_permissions` to the caller regardless. Returning a WP_Error would
533 * therefore buy nothing for the client and emit a PHP notice on every ordinary
534 * authorization denial. The reason is published on `wdesignkit_ability_permission_denied`
535 * instead, where a site can log it without exposing it.
536 *
537 * @since 2.6.5
538 *
539 * @param bool|\WP_Error $permission Result from the ability's permission_callback.
540 * @param string $ability_name Ability being executed.
541 * @param mixed $input Input used for the permission check.
542 * @param mixed $ability WP_Ability instance.
543 * @return bool|\WP_Error Unchanged result, or false when the extra cap is missing.
544 */
545 public function wdk_permission_result( $permission, $ability_name, $input = null, $ability = null ) {
546 // Only ever tighten: an existing denial stands, and its message is more specific.
547 if ( ! $this->is_wdk_ability( $ability_name ) || is_wp_error( $permission ) || ! $permission ) {
548 return $permission;
549 }
550
551 if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_meta' ) ) {
552 return $permission;
553 }
554
555 $meta = $ability->get_meta();
556 $caps = isset( $meta['required_capability'] ) ? $meta['required_capability'] : '';
557 $caps = is_array( $caps ) ? $caps : array( $caps );
558
559 foreach ( $caps as $cap ) {
560 if ( ! is_string( $cap ) || '' === $cap ) {
561 continue;
562 }
563
564 if ( ! current_user_can( $cap ) ) {
565 /**
566 * Fires when a WDesignKit ability is denied for a missing capability.
567 *
568 * The reason cannot be returned to the caller (see this method's docblock),
569 * so it is published here for logging and auditing.
570 *
571 * @since 2.6.5
572 *
573 * @param string $ability_name Ability that was denied.
574 * @param string $cap Capability the current user is missing.
575 * @param int $user_id Current user ID, 0 when not logged in.
576 */
577 do_action( 'wdesignkit_ability_permission_denied', $ability_name, $cap, get_current_user_id() );
578
579 return false;
580 }
581 }
582
583 return $permission;
584 }
585
586 /**
587 * Observability for failed ability runs, before output validation.
588 *
589 * Returns `$result` untouched — clients depend on the existing shapes (a WP_Error, or
590 * an array with `success => false` and a `message`), so nothing is rewritten here.
591 * The value of the hook is that the two failure shapes can finally be noticed in one
592 * place instead of per callback.
593 *
594 * @since 2.6.5
595 *
596 * @param mixed $result Result from the execute callback, or WP_Error.
597 * @param string $ability_name Ability being executed.
598 * @param mixed $input Normalized input.
599 * @param mixed $ability WP_Ability instance.
600 * @return mixed $result, unchanged.
601 */
602 public function wdk_execute_result( $result, $ability_name, $input = null, $ability = null ) {
603 if ( ! $this->is_wdk_ability( $ability_name ) ) {
604 return $result;
605 }
606
607 $failed = false;
608 $code = '';
609 $message = '';
610
611 if ( is_wp_error( $result ) ) {
612 $failed = true;
613 $code = $result->get_error_code();
614 $message = $result->get_error_message();
615 } elseif ( is_array( $result ) && isset( $result['success'] ) && ! $result['success'] ) {
616 $failed = true;
617 $code = 'wdkit_ability_unsuccessful';
618 $message = isset( $result['message'] ) && is_string( $result['message'] ) ? $result['message'] : '';
619 }
620
621 if ( $failed ) {
622 /**
623 * Fires when a WDesignKit ability reports a failure.
624 *
625 * Covers both failure shapes the abilities use: a returned WP_Error, and an
626 * array with `success => false`.
627 *
628 * @since 2.6.5
629 *
630 * @param string $ability_name Ability name.
631 * @param string $code Error code, or 'wdkit_ability_unsuccessful'.
632 * @param string $message Human-readable failure message, may be empty.
633 * @param mixed $ability WP_Ability instance.
634 */
635 do_action( 'wdesignkit_ability_failed', $ability_name, $code, $message, $ability );
636 }
637
638 return $result;
639 }
640 }
641
642 Wdk_Ability_Lifecycle::instance();
643 }
644