Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题: P0 (CRITICAL): - TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁 - REST 路由: __return_true → check_bearer_present 轻量鉴权 - Schema 验证: messages/tools/tool_choice 添加完整 validate_callback - SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义 - SQL 表名插值: 统一使用 %i 占位符 P1 (HIGH): - IP hash 加盐 (HMAC) + IP 解析统一 (context 共享) - Header 注入防护 (X-Request-Id/响应头 \r\n 过滤) - 数值参数范围校验 (temperature/top_p/penalties/max_tokens) - insert_key 错误处理 + revoke_key 审计日志 - Pipeline finally 保护 + RateLimiter rollback 双 store - wp_unslash 补全 + strtotime 返回值检查 - RedisRateStore $rid 冒号验证 - UpstreamStreamClient catch \Throwable P2 (MEDIUM): - AuditLogRepository DRY (提取 build_where_clause) - Token 估算 CJK 优化 (mb_strlen) - ResponseTransformMiddleware null 检查 - 嵌入响应 usage 数据修复 - SchemaManager autoload 优化 25 files changed, ~530 insertions, ~190 deletions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
350 lines
9 KiB
PHP
350 lines
9 KiB
PHP
<?php
|
|
/**
|
|
* Gateway Request Schema
|
|
*
|
|
* Defines WordPress REST API argument schemas for each endpoint.
|
|
*
|
|
* @package WPMind\Modules\ApiGateway
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WPMind\Modules\ApiGateway;
|
|
|
|
/**
|
|
* Class GatewayRequestSchema
|
|
*
|
|
* Static methods returning WP REST API args arrays
|
|
* for request validation and sanitization.
|
|
*/
|
|
final class GatewayRequestSchema {
|
|
|
|
/**
|
|
* Schema for chat completions endpoint.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public static function chat_completions(): array {
|
|
return [
|
|
'model' => [
|
|
'type' => 'string',
|
|
'required' => true,
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
],
|
|
'messages' => [
|
|
'type' => 'array',
|
|
'required' => true,
|
|
'maxItems' => 256,
|
|
'items' => [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'role' => [
|
|
'type' => 'string',
|
|
'enum' => [ 'system', 'user', 'assistant', 'tool' ],
|
|
],
|
|
'content' => [
|
|
'type' => [ 'string', 'array' ],
|
|
],
|
|
],
|
|
],
|
|
'validate_callback' => [ __CLASS__, 'validate_messages' ],
|
|
],
|
|
'temperature' => [
|
|
'type' => 'number',
|
|
'default' => 1.0,
|
|
'minimum' => 0,
|
|
'maximum' => 2,
|
|
],
|
|
'max_tokens' => [
|
|
'type' => 'integer',
|
|
'default' => null,
|
|
'minimum' => 1,
|
|
],
|
|
'top_p' => [
|
|
'type' => 'number',
|
|
'default' => 1.0,
|
|
'minimum' => 0,
|
|
'maximum' => 1,
|
|
],
|
|
'frequency_penalty' => [
|
|
'type' => 'number',
|
|
'default' => 0,
|
|
'minimum' => -2,
|
|
'maximum' => 2,
|
|
],
|
|
'presence_penalty' => [
|
|
'type' => 'number',
|
|
'default' => 0,
|
|
'minimum' => -2,
|
|
'maximum' => 2,
|
|
],
|
|
'stream' => [
|
|
'type' => 'boolean',
|
|
'default' => false,
|
|
],
|
|
'stop' => [
|
|
'type' => [ 'string', 'array' ],
|
|
'default' => null,
|
|
'validate_callback' => [ __CLASS__, 'validate_stop' ],
|
|
],
|
|
'n' => [
|
|
'type' => 'integer',
|
|
'default' => 1,
|
|
'minimum' => 1,
|
|
'validate_callback' => [ __CLASS__, 'validate_n' ],
|
|
],
|
|
'tools' => [
|
|
'type' => 'array',
|
|
'default' => null,
|
|
'validate_callback' => [ __CLASS__, 'validate_tools' ],
|
|
],
|
|
'tool_choice' => [
|
|
'type' => [ 'string', 'object' ],
|
|
'default' => null,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Schema for embeddings endpoint.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public static function embeddings(): array {
|
|
return [
|
|
'model' => [
|
|
'type' => 'string',
|
|
'required' => true,
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
],
|
|
'input' => [
|
|
'type' => [ 'string', 'array' ],
|
|
'required' => true,
|
|
'validate_callback' => [ __CLASS__, 'validate_input' ],
|
|
],
|
|
'encoding_format' => [
|
|
'type' => 'string',
|
|
'default' => 'float',
|
|
'enum' => [ 'float', 'base64' ],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Schema for responses endpoint.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public static function responses(): array {
|
|
return [
|
|
'model' => [
|
|
'type' => 'string',
|
|
'required' => true,
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
],
|
|
'input' => [
|
|
'type' => [ 'string', 'array' ],
|
|
'required' => true,
|
|
],
|
|
'instructions' => [
|
|
'type' => 'string',
|
|
'default' => null,
|
|
'sanitize_callback' => 'sanitize_textarea_field',
|
|
],
|
|
'temperature' => [
|
|
'type' => 'number',
|
|
'default' => 1.0,
|
|
'minimum' => 0,
|
|
'maximum' => 2,
|
|
],
|
|
'max_tokens' => [
|
|
'type' => 'integer',
|
|
'default' => null,
|
|
'minimum' => 1,
|
|
],
|
|
'tools' => [
|
|
'type' => 'array',
|
|
'default' => null,
|
|
'validate_callback' => [ __CLASS__, 'validate_tools' ],
|
|
],
|
|
'stream' => [
|
|
'type' => 'boolean',
|
|
'default' => false,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Schema for models endpoint.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public static function models(): array {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Validate messages array.
|
|
*
|
|
* @param mixed $value Parameter value.
|
|
* @param \WP_REST_Request $request REST request.
|
|
* @param string $param Parameter name.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate_messages( $value, \WP_REST_Request $request, string $param ): true|\WP_Error {
|
|
if ( ! is_array( $value ) ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'messages must be an array.', [ 'status' => 400 ] );
|
|
}
|
|
|
|
if ( count( $value ) === 0 ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'messages must not be empty.', [ 'status' => 400 ] );
|
|
}
|
|
|
|
if ( count( $value ) > 256 ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'messages must not exceed 256 items.', [ 'status' => 400 ] );
|
|
}
|
|
|
|
$valid_roles = [ 'system', 'user', 'assistant', 'tool' ];
|
|
|
|
foreach ( $value as $i => $msg ) {
|
|
if ( ! is_array( $msg ) || ! isset( $msg['role'], $msg['content'] ) ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_param',
|
|
sprintf( 'messages[%d] must have role and content.', $i ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
if ( ! in_array( $msg['role'], $valid_roles, true ) ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_param',
|
|
sprintf( 'messages[%d].role must be one of: %s.', $i, implode( ', ', $valid_roles ) ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
|
|
if ( ! is_string( $msg['content'] ) && ! is_array( $msg['content'] ) ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_param',
|
|
sprintf( 'messages[%d].content must be a string or array.', $i ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate stop parameter.
|
|
*
|
|
* @param mixed $value Parameter value.
|
|
* @param \WP_REST_Request $request REST request.
|
|
* @param string $param Parameter name.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate_stop( $value, \WP_REST_Request $request, string $param ): true|\WP_Error {
|
|
if ( $value === null ) {
|
|
return true;
|
|
}
|
|
|
|
if ( is_string( $value ) ) {
|
|
if ( $value === '' ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'stop string must not be empty.', [ 'status' => 400 ] );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if ( is_array( $value ) ) {
|
|
if ( count( $value ) > 4 ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'stop array must not exceed 4 items.', [ 'status' => 400 ] );
|
|
}
|
|
foreach ( $value as $item ) {
|
|
if ( ! is_string( $item ) ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'Each stop item must be a string.', [ 'status' => 400 ] );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return new \WP_Error( 'rest_invalid_param', 'stop must be a string or array.', [ 'status' => 400 ] );
|
|
}
|
|
|
|
/**
|
|
* Validate n parameter (must be 1).
|
|
*
|
|
* @param mixed $value Parameter value.
|
|
* @param \WP_REST_Request $request REST request.
|
|
* @param string $param Parameter name.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate_n( $value, \WP_REST_Request $request, string $param ): true|\WP_Error {
|
|
if ( (int) $value !== 1 ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_param',
|
|
'Only n=1 is supported. Multiple completions (n>1) are not available.',
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate tools array.
|
|
*
|
|
* @param mixed $value Parameter value.
|
|
* @param \WP_REST_Request $request REST request.
|
|
* @param string $param Parameter name.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate_tools( $value, \WP_REST_Request $request, string $param ): true|\WP_Error {
|
|
if ( $value === null ) {
|
|
return true;
|
|
}
|
|
|
|
if ( ! is_array( $value ) ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'tools must be an array.', [ 'status' => 400 ] );
|
|
}
|
|
|
|
foreach ( $value as $i => $tool ) {
|
|
if ( ! is_array( $tool ) || ! isset( $tool['type'], $tool['function'] ) ) {
|
|
return new \WP_Error(
|
|
'rest_invalid_param',
|
|
sprintf( 'tools[%d] must have type and function keys.', $i ),
|
|
[ 'status' => 400 ]
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate input parameter for embeddings.
|
|
*
|
|
* @param mixed $value Parameter value.
|
|
* @param \WP_REST_Request $request REST request.
|
|
* @param string $param Parameter name.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate_input( $value, \WP_REST_Request $request, string $param ): true|\WP_Error {
|
|
if ( is_string( $value ) ) {
|
|
if ( $value === '' ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'input string must not be empty.', [ 'status' => 400 ] );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if ( is_array( $value ) ) {
|
|
foreach ( $value as $item ) {
|
|
if ( ! is_string( $item ) ) {
|
|
return new \WP_Error( 'rest_invalid_param', 'Each input item must be a string.', [ 'status' => 400 ] );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return new \WP_Error( 'rest_invalid_param', 'input must be a string or array of strings.', [ 'status' => 400 ] );
|
|
}
|
|
}
|