seconds). */ abstract class BotBaseController { /** * Authenticate the request and return the ApiKeyBot model. * On failure, logs the attempt and returns a WP_REST_Response that the * concrete endpoint should return directly. * * @param \WP_REST_Request $request * @param string $action Action name for logging * @return ApiKeyBot|\WP_REST_Response */ protected function authenticate(\WP_REST_Request $request, $action) { $result = BotApiKeyAuth::authenticate($request); if (is_wp_error($result)) { $data = $result->get_error_data(); $status = isset($data['status']) ? $data['status'] : 401; $code = isset($data['code']) ? $data['code'] : 'AUTHENTICATION_FAILED'; $response = $this->error($code, $result->get_error_message()); $this->log(null, $action, $request, $response, $status); return new \WP_REST_Response($response, $status); } return $result; } /** * Build a success envelope. * * @param array|null $data * @return array */ protected function success($data = null) { return [ 'success' => true, 'data' => $data, ]; } /** * Build an error envelope with a machine-readable code. * * @param string $code Machine readable code (e.g. SLOT_UNAVAILABLE) * @param string $message Human readable message * @param array $details Optional extra fields (validation errors, suggestions...) * @return array */ protected function error($code, $message, $details = []) { $error = [ 'code' => $code, 'message' => $message, ]; if (!empty($details)) { $error['details'] = $details; } return [ 'success' => false, 'error' => $error, ]; } /** * Send and log an error response. * * @param ApiKeyBot|null $apiKey * @param string $action * @param \WP_REST_Request $request * @param string $code * @param string $message * @param int $status * @param array $details * @return \WP_REST_Response */ protected function errorResponse($apiKey, $action, $request, $code, $message, $status, $details = []) { $response = $this->error($code, $message, $details); $this->log($apiKey, $action, $request, $response, $status); return new \WP_REST_Response($response, $status); } /** * Send and log a success response. * * @param ApiKeyBot|null $apiKey * @param string $action * @param \WP_REST_Request $request * @param array|null $data * @param int $status * @param string|null $idempotencyKey * @return \WP_REST_Response */ protected function successResponse($apiKey, $action, $request, $data, $status = 200, $idempotencyKey = null) { $response = $this->success($data); $this->log($apiKey, $action, $request, $response, $status, $idempotencyKey); return new \WP_REST_Response($response, $status); } /** * Resolve the widget_id to use for this request. * Priority: explicit param > API key default. * Returns null if neither is available. * * @param \WP_REST_Request $request * @param ApiKeyBot $apiKey * @return int|null */ protected function resolveWidgetId(\WP_REST_Request $request, $apiKey) { $param = $request->get_param('widget_id'); if (!empty($param)) { return intval($param); } return $apiKey->widget_id ? intval($apiKey->widget_id) : null; } /** * Parse a date string in YYYY-MM-DD format. * * @param string $date * @return string|false Normalized date or false if invalid */ protected function parseDate($date) { $date = sanitize_text_field($date); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return false; } // Validate it's a real date $parts = explode('-', $date); if (!checkdate(intval($parts[1]), intval($parts[2]), intval($parts[0]))) { return false; } return $date; } /** * Parse a time string "HH:MM" to seconds since midnight. * * @param string $time * @return int|false */ protected function parseTime($time) { $time = trim($time); if (!preg_match('/^(\d{1,2}):(\d{2})$/', $time, $matches)) { return false; } $hours = intval($matches[1]); $minutes = intval($matches[2]); if ($hours > 23 || $minutes > 59) { return false; } return ($hours * 3600) + ($minutes * 60); } /** * Format seconds since midnight to "HH:MM" string. * * @param int $seconds * @return string */ protected function formatTime($seconds) { return evavel_seconds_to_Hm(intval($seconds)); } /** * Get request params (JSON body preferred, then form/query). * * @param \WP_REST_Request $request * @return array */ protected function getParams(\WP_REST_Request $request) { $params = $request->get_json_params(); if (empty($params)) { $params = $request->get_params(); } return is_array($params) ? $params : []; } /** * Persist a request/response in api_logs_bot. * * @param ApiKeyBot|null $apiKey * @param string $action * @param \WP_REST_Request $request * @param mixed $response * @param int|null $statusCode * @param string|null $idempotencyKey */ protected function log($apiKey, $action, $request, $response, $statusCode = null, $idempotencyKey = null) { $requestBody = $request->get_json_params(); if (empty($requestBody)) { $requestBody = $request->get_params(); } // Strip sensitive data if (is_array($requestBody)) { unset($requestBody['api_key']); } ApiLogBot::log($apiKey, $action, $requestBody, $response, $statusCode, $idempotencyKey); } /** * Decode a base64-encoded HTML field (e.g. public_notes) and return plain text * suitable for a conversational bot. The dashboard stores HTML produced by * the rich-text editor and base64-encodes it before serialising. Here we: * - base64_decode (fall back to the raw value if not actually base64) * - convert block-level closing tags and
to newlines * - strip the remaining tags * - decode HTML entities * - collapse extra blank lines * * @param mixed $value * @return string|null */ protected function decodeRichText($value) { if ($value === null || $value === '' || $value === 'null') { return null; } // Only attempt base64_decode if the value actually looks like base64 // (avoids false positives on plain text that happens to be base64-safe). if (is_string($value) && preg_match('#^[A-Za-z0-9+/]+={0,2}$#', $value)) { $decoded = base64_decode($value, true); if ($decoded !== false && $decoded !== '') { $value = $decoded; } } $text = preg_replace('/<\s*(\/p|br\s*\/?|\/li|\/h[1-6]|\/div)\s*>/i', "\n", $value); $text = strip_tags($text); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace("/\n{3,}/", "\n\n", trim($text)); return $text === '' ? null : $text; } /** * Build a synthetic email for customers that contact through a chat platform * without providing one. Mirrors the convention used by the widget for * WhatsApp-created customers ("whatsapp+{phone}@fake"). * * @param string $platform Platform name from the API key (instagram, whatsapp_bot...) * @param string $phone Raw phone number * @param string $dialCode Optional dial code prefix * @return string */ protected function buildSyntheticEmail($platform, $phone, $dialCode = '') { $platform = preg_replace('/[^a-z0-9_]/i', '', strtolower($platform ?: 'bot')); $clean = preg_replace('/[^0-9]/', '', $dialCode . $phone); if (empty($clean)) { $clean = bin2hex(random_bytes(4)); } return $platform . '+' . $clean . '@fake'; } }