$arguments Raw arguments. * @param array $schema JSON Schema (object type). * @return array|\WP_Error Coerced arguments or an error. */ public function validate( array $arguments, array $schema ) { $properties = isset( $schema['properties'] ) && is_array( $schema['properties'] ) ? $schema['properties'] : array(); $required = isset( $schema['required'] ) && is_array( $schema['required'] ) ? $schema['required'] : array(); $additional = ! array_key_exists( 'additionalProperties', $schema ) || false !== $schema['additionalProperties']; $errors = array(); $out = array(); // Reject unexpected properties when additionalProperties is false. if ( ! $additional ) { foreach ( array_keys( $arguments ) as $key ) { if ( ! isset( $properties[ $key ] ) ) { $errors[] = sprintf( /* translators: %s: property name. */ __( 'Unexpected property "%s".', 'gripmcp' ), $key ); } } } else { // Pass through unknown properties untouched. foreach ( $arguments as $key => $value ) { if ( ! isset( $properties[ $key ] ) ) { $out[ $key ] = $value; } } } foreach ( $properties as $name => $spec ) { $has = array_key_exists( $name, $arguments ); if ( ! $has ) { if ( in_array( $name, $required, true ) ) { $errors[] = sprintf( /* translators: %s: property name. */ __( 'Missing required property "%s".', 'gripmcp' ), $name ); } elseif ( is_array( $spec ) && array_key_exists( 'default', $spec ) ) { $out[ $name ] = $spec['default']; } continue; } $value = $arguments[ $name ]; $result = $this->validate_value( $name, $value, is_array( $spec ) ? $spec : array() ); if ( is_wp_error( $result ) ) { $errors[] = $result->get_error_message(); continue; } $out[ $name ] = $result; } if ( ! empty( $errors ) ) { return new \WP_Error( 'gripmcp_invalid_arguments', __( 'Invalid arguments: ', 'gripmcp' ) . implode( ' ', $errors ), array( 'errors' => $errors ) ); } return $out; } /** * Validate/coerce a single value against a property spec. * * @param string $name Property name. * @param mixed $value Value. * @param array $spec Property schema. * @return mixed|\WP_Error */ private function validate_value( string $name, $value, array $spec ) { $type = isset( $spec['type'] ) ? (string) $spec['type'] : ''; // Enum. if ( isset( $spec['enum'] ) && is_array( $spec['enum'] ) && ! in_array( $value, $spec['enum'], true ) ) { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: allowed values. */ __( 'Property "%1$s" must be one of: %2$s.', 'gripmcp' ), $name, implode( ', ', array_map( 'strval', $spec['enum'] ) ) ) ); } switch ( $type ) { case 'string': if ( ! is_string( $value ) ) { return $this->type_error( $name, 'string' ); } if ( isset( $spec['maxLength'] ) && strlen( $value ) > (int) $spec['maxLength'] ) { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: max. */ __( '"%1$s" exceeds max length %2$d.', 'gripmcp' ), $name, (int) $spec['maxLength'] ) ); } if ( isset( $spec['minLength'] ) && strlen( $value ) < (int) $spec['minLength'] ) { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: min. */ __( '"%1$s" is shorter than min length %2$d.', 'gripmcp' ), $name, (int) $spec['minLength'] ) ); } return $value; case 'integer': if ( is_bool( $value ) || ! is_numeric( $value ) || (int) $value != $value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual return $this->type_error( $name, 'integer' ); } return $this->range_check( $name, (int) $value, $spec ); case 'number': if ( is_bool( $value ) || ! is_numeric( $value ) ) { return $this->type_error( $name, 'number' ); } return $this->range_check( $name, (float) $value, $spec ); case 'boolean': if ( is_bool( $value ) ) { return $value; } // Coerce common representations from lenient clients. if ( in_array( $value, array( 'true', '1', 1 ), true ) ) { return true; } if ( in_array( $value, array( 'false', '0', 0 ), true ) ) { return false; } return $this->type_error( $name, 'boolean' ); case 'array': if ( ! is_array( $value ) || ( ! empty( $value ) && array_keys( $value ) !== range( 0, count( $value ) - 1 ) ) ) { return $this->type_error( $name, 'array' ); } if ( isset( $spec['items']['type'] ) ) { foreach ( $value as $i => $item ) { $check = $this->validate_value( $name . "[$i]", $item, $spec['items'] ); if ( is_wp_error( $check ) ) { return $check; } } } return $value; case 'object': if ( ! is_array( $value ) ) { return $this->type_error( $name, 'object' ); } return $value; default: // Unknown/absent type: accept as-is. return $value; } } /** * Numeric range checking. * * @param string $name Property name. * @param int|float $value Value. * @param array $spec Spec. * @return int|float|\WP_Error */ private function range_check( string $name, $value, array $spec ) { if ( isset( $spec['minimum'] ) && $value < $spec['minimum'] ) { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: min. */ __( '"%1$s" must be >= %2$s.', 'gripmcp' ), $name, (string) $spec['minimum'] ) ); } if ( isset( $spec['maximum'] ) && $value > $spec['maximum'] ) { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: max. */ __( '"%1$s" must be <= %2$s.', 'gripmcp' ), $name, (string) $spec['maximum'] ) ); } return $value; } /** * Build a type error. * * @param string $name Property name. * @param string $expected Expected type. */ private function type_error( string $name, string $expected ): \WP_Error { return new \WP_Error( 'invalid', sprintf( /* translators: 1: property, 2: type. */ __( 'Property "%1$s" must be of type %2$s.', 'gripmcp' ), $name, $expected ) ); } }