'WP Cafe plugin is not active.' ]; } $all_capabilities = array_merge( [ $capability ], $extra_capabilities ); foreach ( $all_capabilities as $cap ) { if ( current_user_can( $cap ) ) { return null; } } return [ 'error' => 'You do not have permission to manage WP Cafe data.' ]; } /** * Unwrap a WpCafe REST controller response. * * Returns the inner data array on success, or ['error' => '...'] when * the HTTP status is 4xx/5xx, the body is not an array, or the response * lacks a 'data' key. Logs a warning/error in the latter two cases. * * @param \WP_HTTP_Response $response Controller response. * @param string $fallback_error Human-readable fallback error message. * @return array */ protected function extract_response( \WP_HTTP_Response $response, string $fallback_error ): array { $body = $response->get_data(); if ( $response->get_status() >= 400 ) { $message = is_array( $body ) ? ( $body['message'] ?? $fallback_error ) : $fallback_error; return [ 'error' => $message ]; } if ( ! is_array( $body ) ) { Log::error( 'WpCafe controller returned non-array body', [ 'status' => $response->get_status(), 'type' => gettype( $body ), ] ); return [ 'error' => $fallback_error ]; } if ( ! array_key_exists( 'data', $body ) ) { Log::warning( 'WpCafe controller response missing data key', [ 'body_keys' => array_keys( $body ) ] ); return [ 'error' => $fallback_error ]; } if ( ! is_array( $body['data'] ) ) { if ( is_object( $body['data'] ) && method_exists( $body['data'], 'to_array' ) ) { try { return $body['data']->to_array(); } catch ( \Throwable $e ) { Log::error( 'WpCafe resource to_array() threw during extract_response', [ 'class' => get_class( $body['data'] ), 'error' => $e->getMessage(), 'status' => $response->get_status(), ] ); return [ 'error' => $fallback_error ]; } } Log::warning( 'WpCafe controller returned non-array data value', [ 'status' => $response->get_status(), 'type' => gettype( $body['data'] ), ] ); return [ 'error' => $fallback_error ]; } return $body['data']; } /** * Unwrap a WpCafe write (create/update) response. * * Some write controllers (e.g. Location_Controller) return a raw Model with * no to_array(), which {@see extract_response()} can't unwrap and would * misreport as a failed write. This treats any non-4xx/5xx as success and * best-effort reads the new id (term_id or id). * * @param \WP_HTTP_Response $response Controller response. * @param string $fallback_error Human-readable fallback error message. * @return array [ 'id' => int ] on success, or [ 'error' => string ] on a real 4xx/5xx. */ protected function extract_write_result( \WP_HTTP_Response $response, string $fallback_error ): array { $status = $response->get_status(); $body = $response->get_data(); if ( $status >= 400 ) { $message = is_array( $body ) ? ( $body['message'] ?? $fallback_error ) : $fallback_error; return [ 'error' => $message ]; } // The write already succeeded; the id is best-effort only. $payload = ( is_array( $body ) && array_key_exists( 'data', $body ) ) ? $body['data'] : $body; $id = 0; if ( is_array( $payload ) ) { $id = (int) ( $payload['term_id'] ?? $payload['id'] ?? 0 ); } elseif ( is_object( $payload ) ) { $id = (int) ( $payload->term_id ?? $payload->id ?? 0 ); } return [ 'id' => $id ]; } /** * Return only the whitelisted keys from an associative array. * * Keys absent from the source are skipped. Used to curate verbose * REST/WooCommerce records down to the fields the LLM actually needs, * keeping tool results small. * * @param array $item Source associative array. * @param array $keys Keys to keep, in order. * @return array */ protected function pluck( array $item, array $keys ): array { $out = []; foreach ( $keys as $key ) { if ( array_key_exists( $key, $item ) ) { $out[ $key ] = $item[ $key ]; } } return $out; } /** * Map every row of a list through a shaper callback. * * Non-array rows are dropped and keys are re-indexed so the result is a * clean sequential list. * * @param array $rows List of records. * @param callable $shape Receives one record, returns the curated record. * @return array */ protected function shape_rows( array $rows, callable $shape ): array { $arrays = array_filter( $rows, 'is_array' ); return array_values( array_map( $shape, $arrays ) ); } /** * Shape a paginated WpCafe response that uses an 'items' key. * * Each item is curated through $shape; pagination metadata (total, * total_pages, paged, per_page) is preserved when present so the LLM can * still page through results. * * @param array $data Decoded controller data (expects an 'items' key). * @param callable $shape Receives one item, returns the curated item. * @return array */ protected function shape_paginated( array $data, callable $shape ): array { $pagination_keys = [ 'total', 'total_pages', 'paged', 'per_page' ]; $has_items = isset( $data['items'] ) && is_array( $data['items'] ); $items = $has_items ? $data['items'] : []; $shaped = [ 'items' => $this->shape_rows( $items, $shape ) ]; foreach ( $pagination_keys as $key ) { if ( isset( $data[ $key ] ) ) { $shaped[ $key ] = $data[ $key ]; } } return $shaped; } /** * Strip HTML and truncate free-text to a token-friendly length. * * @param string|null $text Raw text, possibly containing HTML. * @param int $length Maximum length in characters. * @return string */ protected function truncate( ?string $text, int $length = 300 ): string { $length = max( 1, $length ); $text = trim( wp_strip_all_tags( (string) $text ) ); if ( '' === $text ) { return ''; } return wp_html_excerpt( $text, $length, '…' ); } }