PluginProbe
ZIP AI – AI Website Builder & AI Agent / trunk
ZIP AI – AI Website Builder & AI Agent vtrunk
0.0.13 0.0.12 0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / core / abilities-input-normalizer.php

abilities-input-normalizer.php in ZIP AI – AI Website Builder & AI Agent trunk, at inc/core/abilities-input-normalizer.php

85 lines 3.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abilities input normalizer.
4 *
5 * Makes a no-input GET to a readonly WP Ability validate as an empty object.
6 *
7 * @package zip-ai
8 */
9
10 namespace ZipAI\MCP\Classes\Core;
11
12 defined( 'ABSPATH' ) || exit;
13
14 class Abilities_Input_Normalizer {
15
16 /**
17 * Hook the core `wp_ability_normalize_input` filter.
18 *
19 * @return void
20 */
21 public static function init(): void {
22 add_filter( 'wp_ability_normalize_input', array( __CLASS__, 'default_empty_object' ), 10, 3 );
23 }
24
25 /**
26 * Supply an empty object as the input when a readonly ability is called with
27 * none.
28 *
29 * WHY: core `WP_Ability::normalize_input()` only substitutes an input when the
30 * ability's `input_schema` declares a top-level `default`. An ability whose
31 * schema is an OBJECT with no `default` and no `required` fields omits it —
32 * notably Astra's `astra/list-font-family` (an object schema, every field
33 * optional). The browser Abilities run-route can't carry a JSON object on a GET
34 * (query params are strings; `?input={}` arrives as the string "{}"), so with no
35 * `input` param the ability receives `null` and the route rejects it 400 "input
36 * is not of type object" — even though `{}` is perfectly valid. The ZIP AI chat
37 * calls such abilities over exactly that GET route, and we can't patch a stock
38 * theme's schema, so we supply the empty object here. (Abilities with an EMPTY
39 * schema — `astra/get-font-heading` / `get-font-body` return `array()` — need no
40 * help: they fall out at the `empty( $schema )` guard below, no object to satisfy.)
41 *
42 * SCOPE: this feature needs it only for `zipai/*` and `astra/*` abilities, so it
43 * is gated on that namespace prefix — the filter fires for every ability on the
44 * site, but we no-op for anyone else's rather than widen their input handling.
45 * Beyond that: only `null` input, and only schemas where `{}` is genuinely valid —
46 * `type: object`, no `default` already (core handled those), and no `required`
47 * fields. An ability that truly needs input keeps its `required[]` and is still
48 * rejected by `validate_input()`, so this loosens nothing real; it only makes
49 * currently-uncallable no-input GET abilities callable.
50 *
51 * @param mixed $input The (possibly null) normalized input.
52 * @param string $name The ability name.
53 * @param mixed $ability The WP_Ability instance.
54 * @return mixed The input, or an empty array when an empty object is the right default.
55 */
56 public static function default_empty_object( $input, $name, $ability ) {
57 if ( null !== $input || ! $ability instanceof \WP_Ability ) {
58 return $input;
59 }
60
61 // Only widen the abilities this feature actually calls over the run-route —
62 // never every plugin's abilities site-wide.
63 if ( 0 !== strpos( $name, 'zipai/' ) && 0 !== strpos( $name, 'astra/' ) ) {
64 return $input;
65 }
66
67 $schema = $ability->get_input_schema();
68 if ( empty( $schema ) ) {
69 return $input;
70 }
71
72 // Only when `{}` is a valid input: object-typed, no default already
73 // substituted by core, and no required fields.
74 $is_object = isset( $schema['type'] ) && 'object' === $schema['type'];
75 $has_default = array_key_exists( 'default', $schema );
76 $requires = ! empty( $schema['required'] );
77
78 if ( $is_object && ! $has_default && ! $requires ) {
79 return array();
80 }
81
82 return $input;
83 }
84 }
85