first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<?php
/**
* Abilities Categories class file.
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities;
defined( 'ABSPATH' ) || exit;
/**
* Abilities Categories class for WooCommerce.
*
* Registers categories for WooCommerce abilities to improve organization
* and discoverability in the WordPress Abilities API v0.3.0+.
*/
class AbilitiesCategories {
/**
* Initialize category registration.
*
* @internal
*/
final public static function init(): void {
/*
* Register categories when Abilities API categories are ready.
* Support both old (pre-6.9) and new (6.9+) action names.
*/
add_action( 'abilities_api_categories_init', array( __CLASS__, 'register_categories' ) );
add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'register_categories' ) );
}
/**
* Register WooCommerce ability categories.
*/
public static function register_categories(): void {
// Only register if the function exists.
if ( ! function_exists( 'wp_register_ability_category' ) ) {
return;
}
wp_register_ability_category(
'woocommerce-rest',
array(
'label' => __( 'WooCommerce REST API', 'woocommerce' ),
'description' => __( 'REST API operations for WooCommerce resources including products, orders, and other store data.', 'woocommerce' ),
)
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Abilities Registry class file.
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities;
defined( 'ABSPATH' ) || exit;
/**
* Abilities Registry class for WooCommerce.
*
* Centralized registry that initializes all WooCommerce abilities.
* These abilities can be consumed by MCP, REST API, or other tools.
*/
class AbilitiesRegistry {
/**
* Initialize the registry.
*/
public function __construct() {
$this->init_abilities();
}
/**
* Initialize all WooCommerce abilities.
*/
private function init_abilities(): void {
AbilitiesCategories::init();
AbilitiesRestBridge::init();
}
/**
* Get all ability IDs from the WordPress Abilities API.
*
* @return array Array of all ability IDs.
*/
public function get_abilities_ids(): array {
// Check if the abilities API is available.
if ( ! function_exists( 'wp_get_abilities' ) ) {
return array();
}
$all_abilities = wp_get_abilities();
return array_keys( $all_abilities );
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Abilities REST Bridge class file.
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities;
use Automattic\WooCommerce\Internal\Abilities\REST\RestAbilityFactory;
use Automattic\WooCommerce\Internal\MCP\MCPAdapterProvider;
defined( 'ABSPATH' ) || exit;
/**
* Abilities REST Bridge class for WooCommerce.
*
* Configuration-driven registry that exposes REST endpoints as WordPress abilities.
* Each ability is explicitly configured with ID, label, description, and operation.
*/
class AbilitiesRestBridge {
/**
* Get REST controller configurations with explicit IDs, labels, and descriptions.
*
* @return array Controller configurations.
*/
private static function get_configurations(): array {
return array(
array(
'controller' => \WC_REST_Products_Controller::class,
'route' => '/wc/v3/products',
'abilities' => array(
array(
'id' => 'woocommerce/products-list',
'operation' => 'list',
'label' => __( 'List Products', 'woocommerce' ),
'description' => __( 'Retrieve a paginated list of products with optional filters for status, category, price range, and other attributes.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/products-get',
'operation' => 'get',
'label' => __( 'Get Product', 'woocommerce' ),
'description' => __( 'Retrieve detailed information about a single product by ID, including price, description, images, and metadata.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/products-create',
'operation' => 'create',
'label' => __( 'Create Product', 'woocommerce' ),
'description' => __( 'Create a new product in WooCommerce with name, price, description, and other product attributes.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/products-update',
'operation' => 'update',
'label' => __( 'Update Product', 'woocommerce' ),
'description' => __( 'Update an existing product by modifying its attributes such as price, stock, description, or metadata.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/products-delete',
'operation' => 'delete',
'label' => __( 'Delete Product', 'woocommerce' ),
'description' => __( 'Permanently delete a product from the store. This action cannot be undone.', 'woocommerce' ),
),
),
),
array(
'controller' => \WC_REST_Orders_Controller::class,
'route' => '/wc/v3/orders',
'abilities' => array(
array(
'id' => 'woocommerce/orders-list',
'operation' => 'list',
'label' => __( 'List Orders', 'woocommerce' ),
'description' => __( 'Retrieve a paginated list of orders with optional filters for status, customer, date range, and other criteria.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/orders-get',
'operation' => 'get',
'label' => __( 'Get Order', 'woocommerce' ),
'description' => __( 'Retrieve detailed information about a single order by ID, including line items, customer details, and payment information.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/orders-create',
'operation' => 'create',
'label' => __( 'Create Order', 'woocommerce' ),
'description' => __( 'Create a new order with customer information, line items, shipping details, and payment information.', 'woocommerce' ),
),
array(
'id' => 'woocommerce/orders-update',
'operation' => 'update',
'label' => __( 'Update Order', 'woocommerce' ),
'description' => __( 'Update an existing order by modifying status, customer information, line items, or other order details.', 'woocommerce' ),
),
),
),
);
}
/**
* Initialize the ability registration.
*
* @internal
*/
final public static function init(): void {
/*
* Register abilities when Abilities API is ready.
* Support both old (pre-6.9) and new (6.9+) action names.
*/
add_action( 'abilities_api_init', array( __CLASS__, 'register_abilities' ) );
add_action( 'wp_abilities_api_init', array( __CLASS__, 'register_abilities' ) );
}
/**
* Register all configured abilities.
*/
public static function register_abilities(): void {
// Only register abilities if this is an MCP endpoint request.
// We check here (on abilities_api_init action) rather than earlier
// because REST request detection requires the WordPress REST infrastructure
// to be fully initialized.
if ( ! MCPAdapterProvider::is_mcp_request() ) {
return;
}
foreach ( self::get_configurations() as $config ) {
RestAbilityFactory::register_controller_abilities( $config );
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* REST Ability class file.
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities\REST;
defined( 'ABSPATH' ) || exit;
/**
* Custom WP_Ability subclass for REST API-based abilities.
*
* This class extends the base WP_Ability class but skips output validation
* to handle the discrepancies between WooCommerce REST API schemas and
* actual output. This is necessary because WooCommerce schemas are often
* incomplete or inaccurate regarding nullable fields and type variations.
*/
class RestAbility extends \WP_Ability {
/**
* Skip output validation for REST abilities.
*
* WooCommerce REST API schemas often don't accurately reflect the actual
* output, particularly for nullable fields and type variations. Rather than
* trying to fix all schema inconsistencies, we skip output validation for
* REST-based abilities while maintaining input validation and permissions.
*
* @param mixed $output The output to validate.
* @return true Always returns true (no validation).
*/
protected function validate_output( $output ) {
// Skip validation - trust that REST controllers return valid data.
return true;
}
}

View File

@@ -0,0 +1,353 @@
<?php
/**
* REST Ability Factory class file.
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities\REST;
use Automattic\WooCommerce\Internal\MCP\Transport\WooCommerceRestTransport;
defined( 'ABSPATH' ) || exit;
/**
* Factory class for creating abilities from REST controllers.
*
* Handles the conversion of WooCommerce REST API endpoints into WordPress abilities
* that can be consumed by MCP or other systems.
*/
class RestAbilityFactory {
/**
* Register abilities for a REST controller based on configuration.
*
* @param array $config Controller configuration containing controller class and abilities array.
*/
public static function register_controller_abilities( array $config ): void {
$controller_class = $config['controller'];
if ( ! class_exists( $controller_class ) ) {
return;
}
$controller = new $controller_class();
foreach ( $config['abilities'] as $ability_config ) {
self::register_single_ability( $controller, $ability_config, $config['route'] );
}
}
/**
* Register a single ability.
*
* @param object $controller REST controller instance.
* @param array $ability_config Ability configuration array.
* @param string $route REST route for this controller.
*/
private static function register_single_ability( $controller, array $ability_config, string $route ): void {
// Only proceed if wp_register_ability function exists.
if ( ! function_exists( 'wp_register_ability' ) ) {
return;
}
try {
$ability_args = array(
'label' => $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,
),
);
// 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' );
}
/**
* 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
*
* @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.
if ( isset( $arg['type'] ) ) {
$property['type'] = $arg['type'];
}
if ( isset( $arg['description'] ) ) {
$property['description'] = $arg['description'];
}
if ( isset( $arg['default'] ) ) {
$property['default'] = $arg['default'];
}
if ( isset( $arg['enum'] ) ) {
$property['enum'] = array_values( $arg['enum'] );
}
if ( isset( $arg['items'] ) ) {
$property['items'] = $arg['items'];
}
if ( isset( $arg['minimum'] ) ) {
$property['minimum'] = $arg['minimum'];
}
if ( isset( $arg['maximum'] ) ) {
$property['maximum'] = $arg['maximum'];
}
if ( isset( $arg['format'] ) ) {
$property['format'] = $arg['format'];
}
if ( isset( $arg['properties'] ) ) {
$property['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;
}
/**
* 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 = $controller->get_item_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 );
}
}