'invalid_request', 'error_description' => 'POST only' ), array( 'Allow' => 'POST', 'Cache-Control' => 'no-store' ) ); } $c = AgentAdapt_Request_Auth::context( 'POST', 'protocol' ); if ( is_wp_error( $c ) ) { $data = (array) $c->get_error_data(); $headers = isset( $data['www_authenticate'] ) ? array( 'WWW-Authenticate' => (string) $data['www_authenticate'] ) : array(); AgentAdapt_Http::oauth_error( (int) ( $data['status'] ?? 401 ), $c->get_error_code(), $c->get_error_message(), $headers ); } self::limit( 'feedback-session:' . $c->session_id, self::PER_SESSION ); self::limit( 'feedback-agent:' . $c->client_hash, self::PER_AGENT ); $report = self::parse( (string) AgentAdapt_Http::header( 'Content-Type' ), self::read_body() ); if ( is_wp_error( $report ) ) { AgentAdapt_Http::oauth_error( 400, 'invalid_request', $report->get_error_message() ); } $id = AgentAdapt_Feedback_Store::insert( $c, $report['kind'], $report['task'], $report['description'] ); if ( null === $id ) { AgentAdapt_Http::oauth_error( 500, 'server_error', 'the report could not be stored; try again later' ); } if ( $id > 0 ) { $recognised = in_array( $report['kind'], self::KINDS, true ); AgentAdapt_Events::record( 'feedback.received', $c, array( 'object_id' => $id ) + ( $recognised ? array( 'meta' => array( 'kind' => $report['kind'] ) ) : array() ) ); } AgentAdapt_Http::no_content( 202, array( 'Cache-Control' => 'no-store' ) ); } /** * Check a report. Pure: the endpoint's whole notion of "malformed or too * long" (spec §8), so it can be tested on its own. * * @param string $content_type The request's Content-Type. * @param string $raw The request body. * @return array{kind: string, task: string, description: string}|WP_Error invalid_request with a fixed description (the report's text is never echoed). */ public static function parse( string $content_type, string $raw ) { if ( ! str_starts_with( strtolower( trim( $content_type ) ), 'application/json' ) ) { return new WP_Error( 'invalid_request', 'the body must be application/json' ); } if ( strlen( $raw ) > self::MAX_BODY ) { return new WP_Error( 'invalid_request', 'the request body is too large' ); } $in = json_decode( $raw, true ); if ( ! is_array( $in ) || ( $in && array_is_list( $in ) ) ) { return new WP_Error( 'invalid_request', 'the body must be a JSON object' ); } $out = array(); $fields = array( 'kind' => self::MAX_KIND, 'task' => self::MAX_TASK, 'description' => self::MAX_DESCRIPTION, ); foreach ( $fields as $name => $max ) { $value = $in[ $name ] ?? null; $value = is_string( $value ) ? trim( $value ) : null; if ( null === $value || '' === $value ) { return new WP_Error( 'invalid_request', sprintf( '%s is required, as a non-empty string', $name ) ); } if ( mb_strlen( $value, 'UTF-8' ) > $max ) { return new WP_Error( 'invalid_request', sprintf( '%s is longer than %d characters', $name, $max ) ); } $out[ $name ] = $value; } return $out; } /** * The request body, at most one byte more than parse() accepts. */ private static function read_body(): string { return (string) file_get_contents( 'php://input', false, null, 0, self::MAX_BODY + 1 ); } /** * Count one report against a bucket; over its limit, answer 429. * * @param string $bucket Bucket. * @param int $limit Reports per window. */ private static function limit( string $bucket, int $limit ): void { $r = AgentAdapt_Rate_Limit::hit( $bucket, $limit, self::WINDOW ); if ( ! $r['allowed'] ) { AgentAdapt_Http::rate_limited( $r['retry_after'] ); } } }