�PNG  IHDR22?�� pHYs  �� OiCCPPhotoshop ICC profilexڝSgTS�=���BK���KoR RB���&*! J�!��Q�EEȠ�����Q, � ��!���������{�kּ������>���� �H3Q5� �B�������.@� $p�d!s�#�~<<+"��x� �M��0���B�\���t�8K�@z�B�@F���&S�`�cb�P-`'������{[�!�� e�Dh;��V�EX0fK�9�-0IWfH���� � 0Q��){`�##x��F�W<�+��*x��<�$9E�[-qWW.(�I+6aa�@.�y�2�4���������x����6��_-��"bb���ϫp@�t~��,/��;�m��%�h^ �u��f�@����W�p�~<�5�j>{�-�]c�K'Xt����o��(�h���w��?�G�%�fI�q^D$.Tʳ?�D��*�A��,���� �`6�B$��BB d�r`)��B(�Ͱ*`/�@4�Qh��p.�U�=p�a��(�� A�a!ڈb�X#����!�H�$ ɈQ"K�5H1R�T UH�=r9�\F��;�2����G1���Q=� �C��7�F� �dt1�����r�=�6��Ыhڏ>C�0��3�l0.��B�8, �c˱"� ���V����cϱw�E� 6wB aAHXLXN�H� $4� 7 �Q�'"��K�&���b21�XH,#��/{�C�7$�C2'��I��T��F�nR#�,��4H#���dk�9�, +ȅ����3��!�[ �b@q��S�(R�jJ��4�e�2AU��Rݨ�T5�ZB���R�Q��4u�9̓IK�����hh�i��t�ݕN��W���G���w ��Ljg(�gw��L�Ӌ�T071���oUX*�*|�� �J�&�*/T����ު U�U�T��^S}�FU3S� Ԗ�U��P�SSg�;���g�oT?�~Y��Y�L�OC�Q��_�� c�x,!k ��u�5�&���|v*�����=���9C3J3W�R�f?�q��tN �(���~���)�)�4L�1e\k����X�H�Q�G�6������E�Y��A�J'\'Gg����S�Sݧ �M=:��.�k���Dw�n��^��Lo��y���}/�T�m���G X� $� �<�5qo</���QC]�@C�a�a�ᄑ��<��F�F�i�\�$�m�mƣ&&!&KM�M�RM��)�;L;L���͢�֙5�=1�2��כ߷`ZxZ,����eI��Z�Yn�Z9Y�XUZ]�F���%ֻ�����N�N���gð�ɶ�����ۮ�m�}agbg�Ů��}�}��= ���Z~s�r:V:ޚΜ�?}����/gX���3��)�i�S��Ggg�s�󈋉K��.�>.���Ƚ�Jt�q]�z���������ۯ�6�i�ܟ�4�)�Y3s���C�Q��? ��0k߬~OCO�g��#/c/�W�װ��w��a�>�>r��>�<7�2�Y_�7��ȷ�O�o�_��C#�d�z����%g��A�[��z|!��?:�e����A���AA�������!h�쐭!��Α�i�P~���a�a��~ '���W�?�p�X�1�5w��Cs�D�D�Dޛg1O9�-J5*>�.j<�7�4�?�.fY��X�XIlK9.*�6nl������� �{�/�]py�����.,:�@L�N8��A*��%�w%� y��g"/�6ш�C\*N�H*Mz�쑼5y$�3�,幄'���L Lݛ:��v m2=:�1����qB�!M��g�g�fvˬe����n��/��k���Y- �B��TZ(�*�geWf�͉�9���+��̳�ې7�����ᒶ��KW-X潬j9�������(�x��oʿ�ܔ���Ĺd�f�f���-�[����n �ڴ �V����E�/��(ۻ��C���<��e����;?T�T�T�T6��ݵa��n��{��4���[���>ɾ�UUM�f�e�I���?�������m]�Nmq����#�׹���=TR��+�G�����w- 6 U����#pDy��� �� :�v�{���vg/jB��F�S��[b[�O�>����z�G��499�?r����C�d�&����ˮ/~�����јѡ�򗓿m|������������x31^�V���w�w��O�| (�h���SЧ��������c3-� cHRMz%������u0�`:�o�_�F5IDATx��ytUս�?�;���fN$$��@B�!�k��� -*�gE���j�O�kQ�ʠ`U�B_ �AÐ0%�Ȕ�@r3ޛ;߳��$�@��v����^�s����������$��ߢ !�W��5�̱'-٘������4�>�Rƫ$�G��0U�H}�_ _� #��k�MJ/=�9Du *ڲ�k�PQ;0��j|�*�$�^컂脛y���>�z×� B��� ��(�:$�ʔ_�6��C!B����MVx��a��ζyY����j��ƋY2�9�F�{r��r�딟F��Q��hY6��� x��[l��RC��������nt@��;P��3��� F�w��7��:�P���ɋ'���mj���Bƪ�$�Q�L��y��߷g��\���z����v5�p�v$�v櫻�`6~K �Y�X����,�:��"�o�e0II"(��,������l_�C���/4�+f�E�!q��:����u����c��樼.UE�@� �]K�W���$-�h]�JG���Dz)�`��.�A/ ���mᏣc�3jK� �!�>���$�Q�!�_�? \�C2(E �M$Z I{�U���s�ǣ���=��@Բe ޻��W�����@ �w����}�9v,)��.}*|"�s��di���,���G�]W�AC�������|<����[��ߕ������K�*�G nߴ!��‹��J�!��6(L�a)��Am��������;adf��:�xm��nh�;֦I��(���o�C~ډa?�=��s����h��E������xp����)���_���t��W�����´;8���`dz=(��@@�+,�;�`�fhl��XȌ�3,uI�~z(J����� �ZX�A����H'�À�㜦�82|��a�n�z2��I���� ���EY�h�ε<�0�q�{��C��v�˅��?"@��χ�x�WS+��N�~�KQ�J�8ل�� _.W��� !TU{�����,��"�� �;�������HI S� ��))�o�1�H�D� ���� (� ;����"Ҕ��,���� �F=���O��&�Ph>�`���y��߲ &S��P�<��'��1s�1cĂ�N����2)����@�u��tv��<�+�-&�����h��[!FSG���' � z z0��<����8d&�����E0�,��<��"!P�b4��� �{S�����X����.�囤���T��hEF��X�q0"-<����h���]�Py*����F⩍��@v�˂�Y08�� .q��a��s����(!<̫1�3�L�H��7sYW�`��qtu�\7��?�@�Y�� %Ka�x�GZ�*W�P��y�����N0r9��Z�c�%���q,��k�����!!,��>���IB��� �x1�Xu�Y���v-���ֺ��N������JP��a�2`�e5��w��u;�6�i���̃��(K���ťa��(l51��JL��O'%���]���ٖ뿉!��{*�p+f ���i8��D�ȑ���ܩ�M��V��ӟz��x�7Z�1i}�^͜���6e����x��)u�d�ˏ�V�eG�\�X1�����=n�U'�~Y����L�\,�(d!x ,�:�TW8� M`r���@��r+v�Q0��>^;˴�VbF��('����� �*O��i��j�Ӆ����^�S3&� ���{������.ی�� N;��NU%d0�Ce\r"�C�b��&t7N�7��� �ȪD�[%l�۪�$��'m?�A9tI�9>��NVL ��� 6v~M�����ъ�B���0����9$&=ώm��W�0���L���&��E���f�f��'+�9��hv��7,���M��6�D�&�^� �����8 D֮�����o��k^z��O��hP�8q�5d*��YVq3�{��������#LX4�?��jP=3�~�؅c�ݰ�̱�ұ��8̲��2�G�����j��έ�9��w7���U�X�w�85C��?!I&���o&����L���I#�^�eV�,�^t5�k� �xs˛\��U�8��."���{���ǵ�l4��ݼ�;��r"���G�/~���gJV�N糦��ZZ^k+NN�D��I$�=OE�z����|*�*{��%k���G��>�`9��b;�s���8,�qs��c��.b��q�%�>r��n�R�Ƨ�%�,�A��le#�8���}; 21�0�b�&�QX�f���q�00����$>�h�N�'�a3��A���/�r ��L5�8�GԲ'�����w;��c#�q1���h$*��V^�k��QĽ�b:i������� ����H F#�_���,��]g����Ը����`RUU���� ��"�qD���>-��ϤI�~��3�̴�����NZ[[555�eeek7�m����YF#[��Đ��R��i��L�H�r ��@��J-�Pŝ��_|�-��6y��ks�V�U~��6���w.����(��0A� 4��f��� /���C�xŊ�ٳ����z=6�����rg���mKo��o�/����a�g���?'9%9��P��斓��~���L{�� �H��{AW��B��$����v�1b�&Mb�ر��ر#�0`�`xMBp! )Z�Nmo�mZ�g�}�eذaWTVV ���'99��C5��&�&+5�bT>|8��PUUETT����&���t��~TE%��'������b�tEGG;RSS�S�N�v��ѯ��t����u1@�ޚ"f��͈���r'L�Ph��m��ƌ�74o��`�x�q�P���NRSS�X,�����l&&&��MFFA!-��`��ҷ�n��Yuu�����x<I�LB%����KԒ50�!�3�bz�����ʸKL���;---�l6[��`��������Ȏ;�����egg4��!D�T��jWujyZ��������K����%����p�^���X,6��sV=�ܖ����6�3�X ���4ҿ��A2�&����_k�;d�� �VfOIEND�B`�
Warning: session_start(): Session cannot be started after headers have already been sent in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 124

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 130

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 131

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 132

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 133

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 134

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 135

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 547

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 548

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 549

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 550

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 551
$ability_config['label'], 'description' => $ability_config['description'], 'category' => 'woocommerce-rest', 'input_schema' => self::get_schema_for_operation( $controller, $ability_config['operation'] ), 'output_schema' => self::get_output_schema( $controller, $ability_config['operation'] ), 'execute_callback' => function ( $input ) use ( $controller, $ability_config, $route ) { return self::execute_operation( $controller, $ability_config['operation'], $input, $route ); }, 'permission_callback' => function () use ( $controller, $ability_config ) { return self::check_permission( $controller, $ability_config['operation'] ); }, 'ability_class' => RestAbility::class, 'meta' => array( 'show_in_rest' => true, self::EXPOSE_IN_DEPRECATED_MCP_META_KEY => true, ), ); // Add readonly annotation for GET operations (list and get). if ( in_array( $ability_config['operation'], array( 'list', 'get' ), true ) ) { $ability_args['meta']['annotations'] = array( 'readonly' => true, ); } wp_register_ability( $ability_config['id'], $ability_args ); } catch ( \Throwable $e ) { // Log the error for debugging but don't break the registration of other abilities. if ( function_exists( 'wc_get_logger' ) ) { wc_get_logger()->error( "Failed to register ability {$ability_config['id']}: " . $e->getMessage(), array( 'source' => 'woocommerce-rest-abilities' ) ); } } } /** * Get input schema based on operation type. * * @param object $controller REST controller instance. * @param string $operation Operation type (list, get, create, update, delete). * @return array Input schema array. */ private static function get_schema_for_operation( $controller, string $operation ): array { switch ( $operation ) { case 'list': // Use controller's collection parameters. if ( method_exists( $controller, 'get_collection_params' ) ) { return self::sanitize_args_to_schema( $controller->get_collection_params() ); } break; case 'create': // Use controller's creatable schema. if ( method_exists( $controller, 'get_endpoint_args_for_item_schema' ) ) { $args = $controller->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ); return self::sanitize_args_to_schema( $args ); } break; case 'update': // Use controller's editable schema + ID. if ( method_exists( $controller, 'get_endpoint_args_for_item_schema' ) ) { $args = $controller->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ); $schema = self::sanitize_args_to_schema( $args ); // Add ID field for update operations. $schema['properties']['id'] = array( 'type' => 'integer', 'description' => __( 'Unique identifier for the resource', 'woocommerce' ), ); // Ensure ID is required. if ( ! isset( $schema['required'] ) ) { $schema['required'] = array(); } if ( ! in_array( 'id', $schema['required'], true ) ) { $schema['required'][] = 'id'; } return $schema; } break; case 'get': case 'delete': // Only need ID. return array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'integer', 'description' => __( 'Unique identifier for the resource', 'woocommerce' ), ), ), 'required' => array( 'id' ), ); } // Fallback. return array( 'type' => 'object' ); } /** * Valid JSON Schema types. * * @var array */ private static $valid_types = array( 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null' ); /** * Subset of {@see self::$valid_types} considered scalar for output relaxation. * * When a field is declared as one of these in the source REST schema, output * validation widens it to {@see self::OUTPUT_SCALAR_UNION}. */ private const SCALAR_TYPES = array( 'string', 'integer', 'number', 'boolean' ); /** * Union we emit on output for any field originally declared as a single scalar. * * Covers three failure modes seen in the wild on WooCommerce REST responses: * 1. The field may legitimately be unset / null (e.g. `low_stock_amount`). * 2. The declared scalar disagrees with the scalar actually returned (e.g. * `shipping_class_id` declared `string`, returned as `int`). * 3. The declared scalar is returned as a non-scalar — most notably * `meta_data[].display_value`, declared `string` but routinely an array * when the underlying meta value is itself an array (variation * attributes, serialized custom data, etc.). * * The union is effectively "any JSON type." That makes the type constraint * a no-op for declared scalars, but it remains explicit (so validators that * require a `type` key are still satisfied) and contained to the MCP output * schema path. The alternative is per-controller schema fixes scattered * across legacy REST code. */ private const OUTPUT_SCALAR_UNION = array( 'string', 'integer', 'number', 'boolean', 'array', 'object', 'null' ); /** * Sanitize WordPress REST args to valid JSON Schema format. * * Converts WordPress REST API argument arrays to JSON Schema by: * - Removing PHP callbacks (sanitize_callback, validate_callback) * - Converting 'required' from boolean-per-field to array-of-names * - Removing WordPress-specific non-schema fields * - Preserving valid JSON Schema properties * - Converting invalid types (date-time, mixed, action) to valid JSON Schema * - Recursively sanitizing nested properties and items * - Deduplicating enum values * * @param array $args WordPress REST API arguments array. * @return array Valid JSON Schema object. */ private static function sanitize_args_to_schema( array $args ): array { $properties = array(); $required = array(); foreach ( $args as $key => $arg ) { $property = array(); // Copy valid JSON Schema fields, normalizing types. if ( isset( $arg['type'] ) ) { $property = self::normalize_type( $property, $arg['type'] ); } if ( isset( $arg['description'] ) ) { $property['description'] = $arg['description']; } if ( isset( $arg['default'] ) ) { $property['default'] = $arg['default']; } if ( isset( $arg['enum'] ) ) { $property['enum'] = self::dedupe_enum( $arg['enum'] ); } if ( isset( $arg['items'] ) ) { $property['items'] = self::sanitize_schema( $arg['items'] ); } if ( isset( $arg['minimum'] ) ) { $property['minimum'] = $arg['minimum']; } if ( isset( $arg['maximum'] ) ) { $property['maximum'] = $arg['maximum']; } if ( isset( $arg['format'] ) && ! isset( $property['format'] ) ) { $property['format'] = $arg['format']; } if ( isset( $arg['properties'] ) ) { $property['properties'] = self::sanitize_schema_properties( $arg['properties'] ); } // Convert readonly to readOnly (JSON Schema format). if ( isset( $arg['readonly'] ) && $arg['readonly'] ) { $property['readOnly'] = true; } // Collect required fields. if ( isset( $arg['required'] ) && true === $arg['required'] ) { $required[] = $key; } $properties[ $key ] = $property; } $schema = array( 'type' => 'object', 'properties' => $properties, ); if ( ! empty( $required ) ) { $schema['required'] = array_unique( $required ); } return $schema; } /** * Recursively sanitize a JSON Schema node. * * Fixes invalid types, deduplicates enums, and recurses into * nested properties and items. * * @param array $schema A JSON Schema node. * @return array Sanitized schema node. */ private static function sanitize_schema( array $schema ): array { if ( isset( $schema['type'] ) ) { $schema = self::normalize_type( $schema, $schema['type'] ); } if ( isset( $schema['enum'] ) ) { $schema['enum'] = self::dedupe_enum( $schema['enum'] ); } // Remove WordPress-style boolean 'required' — JSON Schema requires an array. if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) { unset( $schema['required'] ); } if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { // Collect required fields from nested boolean 'required' before sanitizing. $required = array(); foreach ( $schema['properties'] as $key => $property ) { if ( is_array( $property ) && isset( $property['required'] ) && true === $property['required'] ) { $required[] = $key; } } if ( ! empty( $required ) ) { $schema['required'] = isset( $schema['required'] ) && is_array( $schema['required'] ) ? array_values( array_unique( array_merge( $schema['required'], $required ) ) ) : $required; } $schema['properties'] = self::sanitize_schema_properties( $schema['properties'] ); } if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) { if ( isset( $schema['items'][0] ) ) { // Tuple form: sanitize each positional entry. foreach ( $schema['items'] as $index => $entry ) { if ( is_array( $entry ) ) { $schema['items'][ $index ] = self::sanitize_schema( $entry ); } } } else { $schema['items'] = self::sanitize_schema( $schema['items'] ); } } return $schema; } /** * Sanitize a map of JSON Schema properties. * * @param array $properties Map of property name to schema. * @return array Sanitized properties map. */ private static function sanitize_schema_properties( array $properties ): array { foreach ( $properties as $key => $property ) { if ( is_array( $property ) ) { $properties[ $key ] = self::sanitize_schema( $property ); } } return $properties; } /** * Normalize a schema type value. * * Handles both string types ('string', 'date-time', etc.) and * array types (['string', 'null']) used for nullable fields. * * @param array $schema The schema node being built. * @param string|array $type The type value to normalize. * @return array Schema with normalized type (or type removed if all invalid). */ private static function normalize_type( array $schema, $type ): array { if ( is_string( $type ) ) { if ( 'date-time' === $type ) { $schema['type'] = 'string'; if ( ! isset( $schema['format'] ) ) { $schema['format'] = 'date-time'; } } elseif ( 'action' === $type ) { $schema['type'] = 'object'; } elseif ( in_array( $type, self::$valid_types, true ) ) { $schema['type'] = $type; } else { unset( $schema['type'] ); } return $schema; } if ( is_array( $type ) ) { $normalized = array(); foreach ( $type as $single ) { if ( ! is_string( $single ) ) { continue; } if ( 'date-time' === $single ) { $single = 'string'; if ( ! isset( $schema['format'] ) ) { $schema['format'] = 'date-time'; } } elseif ( 'action' === $single ) { $single = 'object'; } elseif ( ! in_array( $single, self::$valid_types, true ) ) { continue; } $normalized[] = $single; } $normalized = array_values( array_unique( $normalized ) ); if ( empty( $normalized ) ) { unset( $schema['type'] ); } elseif ( 1 === count( $normalized ) ) { $schema['type'] = $normalized[0]; } else { $schema['type'] = $normalized; } return $schema; } // Non-string, non-array type — remove it. unset( $schema['type'] ); return $schema; } /** * Remove duplicate enum values while preserving order. * * Uses JSON encoding for fingerprinting to correctly handle * mixed scalar types (1 vs '1'), nulls, and complex values (arrays). * * @param array $values Enum values. * @return array Deduplicated enum values. */ private static function dedupe_enum( array $values ): array { $seen = array(); $unique = array(); foreach ( $values as $value ) { $fingerprint = wp_json_encode( $value ); if ( isset( $seen[ $fingerprint ] ) ) { continue; } $seen[ $fingerprint ] = true; $unique[] = $value; } return $unique; } /** * Recursively relax an output schema so it accepts the shapes WooCommerce REST * controllers actually return. * * Used on output schemas only — input schemas keep their tighter constraints so * MCP clients still get useful hints when formatting tool calls. Must run AFTER * {@see self::sanitize_schema()}, which converts the `date-time` pseudo-type to * `type: "string"` + `format: "date-time"` — this method then strips the format. * * Relaxations: * * 1. `format: "date-time"` and `format: "uri"` are stripped. WooCommerce REST * date strings (e.g. `2025-11-24T16:31:43`) omit the timezone suffix RFC 3339 * requires, and `format: "uri"` fields routinely return empty strings. * 2. Any `type` whose declared members are all scalars and/or `null` is * widened to {@see self::OUTPUT_SCALAR_UNION} — every JSON type plus * `null`. Applies to single scalars (`string`, `integer`, `number`, * `boolean`) and to pre-existing unions like `[integer, null]`. Fields * that declare any compound type (`object`, `array`) are left alone. * This is a deliberate accuracy tradeoff: many WooCommerce REST * controllers declare types that disagree with what they actually return * (e.g. `shipping_class_id` declared `string` but returned as `int`; * `low_stock_amount` declared `[integer, null]` but returned as `""` * when unset; `meta_data[].display_value` declared `string` but * routinely an array for variation attributes and serialized custom * meta). The alternative is per-controller schema fixes across legacy * code. Skipped inside `anyOf` / `oneOf` / `allOf` branches: widening * every branch breaks the "exactly one" rule for `oneOf`, and for * `anyOf` / `allOf` the schema author was explicit about admissible * shapes. * * Recurses into `properties`, `items` (single schema and tuple form), * `additionalProperties`, and the `anyOf` / `oneOf` / `allOf` combiners. * * @param array $schema A JSON Schema node. * @param bool $apply_null_union Whether to apply the scalar-to-nullable widening at this node. * False when recursing into combiner branches. * @return array Relaxed schema node. */ private static function relax_output_schema_for_wc_quirks( array $schema, bool $apply_null_union = true ): array { if ( isset( $schema['format'] ) && in_array( $schema['format'], array( 'date-time', 'uri' ), true ) ) { unset( $schema['format'] ); } if ( $apply_null_union && isset( $schema['type'] ) && self::should_widen_to_output_union( $schema['type'] ) ) { $schema['type'] = self::OUTPUT_SCALAR_UNION; } if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { foreach ( $schema['properties'] as $key => $property ) { if ( is_array( $property ) ) { $schema['properties'][ $key ] = self::relax_output_schema_for_wc_quirks( $property, $apply_null_union ); } } } if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) { if ( isset( $schema['items'][0] ) ) { // Tuple form: each numerically-indexed entry validates the array element at that position. foreach ( $schema['items'] as $index => $entry ) { if ( is_array( $entry ) ) { $schema['items'][ $index ] = self::relax_output_schema_for_wc_quirks( $entry, $apply_null_union ); } } } else { $schema['items'] = self::relax_output_schema_for_wc_quirks( $schema['items'], $apply_null_union ); } } if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { $schema['additionalProperties'] = self::relax_output_schema_for_wc_quirks( $schema['additionalProperties'], $apply_null_union ); } foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $combiner ) { if ( isset( $schema[ $combiner ] ) && is_array( $schema[ $combiner ] ) ) { foreach ( $schema[ $combiner ] as $index => $branch ) { if ( is_array( $branch ) ) { // Entering a combiner from anywhere disables null-union for the entire subtree. $schema[ $combiner ][ $index ] = self::relax_output_schema_for_wc_quirks( $branch, false ); } } } } return $schema; } /** * Decide whether an output-schema `type` should be widened to the full union. * * Widens when the declared type is either a single scalar (`integer`, `string`, etc.) * or an array union whose members are all scalars and/or `null`. Leaves the * declaration alone if any compound type (`object`, `array`) appears, since the * schema author was explicit about admitting a structured value. * * Handles the WC quirk where fields declared as `[integer, null]` * (e.g. `low_stock_amount`) are returned as empty strings when unset, which * neither member of the declared union admits. * * @param mixed $type Schema `type` value (string, array, or other). * @return bool True if the type should be replaced with {@see self::OUTPUT_SCALAR_UNION}. */ private static function should_widen_to_output_union( $type ): bool { if ( is_string( $type ) ) { return in_array( $type, self::SCALAR_TYPES, true ); } if ( ! is_array( $type ) || empty( $type ) ) { return false; } $widenable = array_merge( self::SCALAR_TYPES, array( 'null' ) ); foreach ( $type as $member ) { if ( ! is_string( $member ) || ! in_array( $member, $widenable, true ) ) { return false; } } return true; } /** * Get output schema for operation. * * @param object $controller REST controller instance. * @param string $operation Operation type. * @return array Output schema array. */ private static function get_output_schema( $controller, string $operation ): array { if ( method_exists( $controller, 'get_item_schema' ) ) { $schema = self::sanitize_schema( $controller->get_item_schema() ); $schema = self::relax_output_schema_for_wc_quirks( $schema ); if ( 'list' === $operation ) { // For list operations, return object wrapping array of items. // This ensures MCP compatibility while maintaining REST structure. return array( 'type' => 'object', 'properties' => array( 'data' => array( 'type' => 'array', 'items' => $schema, ), ), ); } elseif ( 'delete' === $operation ) { // For delete operations, return simple confirmation. return array( 'type' => 'object', 'properties' => array( 'deleted' => array( 'type' => 'boolean' ), 'previous' => $schema, ), ); } // For get, create, update operations. return $schema; } return array( 'type' => 'object' ); } /** * Execute the REST operation. * * @param object $controller REST controller instance. * @param string $operation Operation type. * @param array $input Input parameters. * @param string $route REST route for this controller. * @return mixed Operation result. */ private static function execute_operation( $controller, string $operation, array $input, string $route ) { $method = self::get_http_method_for_operation( $operation ); // Build final route - add ID for single item operations. $request_route = $route; if ( isset( $input['id'] ) && in_array( $operation, array( 'get', 'update', 'delete' ), true ) ) { $request_route .= '/' . intval( $input['id'] ); unset( $input['id'] ); } // Create REST request. $request = new \WP_REST_Request( $method, $request_route ); foreach ( $input as $key => $value ) { $request->set_param( $key, $value ); } // Dispatch through REST API for proper validation and permissions. $response = rest_do_request( $request ); if ( is_wp_error( $response ) ) { return $response; } $data = $response instanceof \WP_REST_Response ? $response->get_data() : $response; // For list operations, wrap in data object to match schema. if ( 'list' === $operation ) { return array( 'data' => $data ); } return $data; } /** * Get HTTP method for a given operation type. * * @param string $operation Operation type (list, get, create, update, delete). * @return string HTTP method (GET, POST, PUT, DELETE). */ private static function get_http_method_for_operation( string $operation ): string { $method_map = array( 'list' => 'GET', 'get' => 'GET', 'create' => 'POST', 'update' => 'PUT', 'delete' => 'DELETE', ); return $method_map[ $operation ] ?? 'GET'; } /** * Check permissions for MCP operations. * * @param object $controller REST controller instance. * @param string $operation Operation type. * @return bool Whether permission is granted. */ private static function check_permission( $controller, string $operation ): bool { // Get HTTP method for the operation. $method = self::get_http_method_for_operation( $operation ); /** * Filter to check REST ability permissions for HTTP method. * * @since 10.3.0 * @param bool $allowed Whether the operation is allowed. Default false. * @param string $method HTTP method (GET, POST, PUT, DELETE). * @param object $controller REST controller instance. */ return apply_filters( 'woocommerce_check_rest_ability_permissions_for_method', false, $method, $controller ); } }