session_id = $session_id; $data = get_transient( 'agentorie_chat_session_' . $this->session_id ); if ( false !== $data && is_array( $data ) ) { $this->data = $data; } else { $this->data = $this->get_default_data(); } return $this; } /** * Generate a unique session ID. * * @since 1.0.0 * @return string Generated session ID. */ public static function generate_session_id() { return time() . '000_' . wp_generate_password( 9, false, false ); } /** * Validate session ID format. * * @since 1.0.0 * @param string $session_id Session ID to validate. * @return bool True if valid. */ public static function validate_session_id( $session_id ) { if ( preg_match( '/^[0-9]{13}_[a-z0-9]{9}$/i', $session_id ) ) { return true; } if ( preg_match( '/^[a-f0-9]{32}$/i', $session_id ) ) { return true; } if ( preg_match( '/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $session_id ) ) { return true; } return false; } /** * Get default session data. * * @since 1.0.0 * @return array Default data array. */ private function get_default_data() { return array( 'session_id' => $this->session_id, 'visitor_id' => '', 'visitor_ip' => self::get_client_ip(), 'visitor_country' => '', 'current_url' => '', 'page_title' => '', 'referrer' => '', 'user_agent' => self::get_user_agent(), 'is_active' => true, 'admin_takeover' => false, 'admin_user_id' => null, 'human_requested' => false, 'negotiate_detected' => false, 'unread_count' => 0, 'messages_count' => 0, 'last_message' => '', 'last_activity' => current_time( 'mysql' ), 'created_at' => current_time( 'mysql' ), ); } /** * Save session data to transient. * * @since 1.0.0 * @return bool True on success. */ public function save() { $this->data['last_activity'] = current_time( 'mysql' ); $retention_hours = agentorie_chat_get_setting( 'conversation_retention_hours', 6 ); $expiration = $retention_hours * HOUR_IN_SECONDS; return set_transient( 'agentorie_chat_session_' . $this->session_id, $this->data, $expiration ); } /** * Get current session ID. * * @since 1.0.0 * @return string Session ID. */ public function get_session_id() { return $this->session_id; } /** * Get a session data value. * * @since 1.0.0 * @param string $key Data key. * @param mixed $default_value Default value. * @return mixed Value or default. */ public function get( $key, $default_value = null ) { return isset( $this->data[ $key ] ) ? $this->data[ $key ] : $default_value; } /** * Set a session data value. * * @since 1.0.0 * @param string $key Data key. * @param mixed $value Value to set. * @return void */ public function set( $key, $value ) { $this->data[ $key ] = $value; } /** * Get all session data. * * @since 1.0.0 * @return array Session data. */ public function get_all() { return $this->data; } /** * Update multiple session data values. * * @since 1.0.0 * @param array $data Data to update. * @return void */ public function update( $data ) { foreach ( $data as $key => $value ) { $this->data[ $key ] = $value; } } /** * Update visitor information. * * @since 1.0.0 * @param string|null $visitor_id Visitor ID. * @param string|null $current_url Current page URL. * @param string|null $page_title Page title. * @return void */ public function update_visitor_info( $visitor_id, $current_url, $page_title ) { if ( null !== $visitor_id && self::validate_visitor_id( $visitor_id ) ) { $this->data['visitor_id'] = sanitize_text_field( $visitor_id ); } if ( null !== $current_url ) { $this->data['current_url'] = esc_url_raw( substr( $current_url, 0, 500 ) ); } if ( null !== $page_title ) { $this->data['page_title'] = sanitize_text_field( substr( $page_title, 0, 255 ) ); } } /** * Set admin takeover status. * * @since 1.0.0 * @param bool $takeover Whether admin has taken over. * @param int|null $admin_id Admin user ID. * @return void */ public function set_admin_takeover( $takeover, $admin_id = null ) { $this->data['admin_takeover'] = $takeover; $this->data['admin_user_id'] = $takeover ? $admin_id : null; if ( $takeover ) { $this->data['human_requested'] = false; $this->data['negotiate_detected'] = false; } } /** * Check if admin has taken over. * * @since 1.0.0 * @return bool True if admin takeover is active. */ public function is_admin_takeover() { return (bool) ( isset( $this->data['admin_takeover'] ) ? $this->data['admin_takeover'] : false ); } /** * Get admin user ID. * * @since 1.0.0 * @return int|null Admin ID or null. */ public function get_admin_id() { return isset( $this->data['admin_user_id'] ) ? $this->data['admin_user_id'] : null; } /** * Set human requested flag. * * @since 1.0.0 * @param bool $requested Whether human was requested. * @return void */ public function set_human_requested( $requested ) { $this->data['human_requested'] = $requested; } /** * Set negotiate detected flag. * * @since 1.0.0 * @param bool $detected Whether negotiation was detected. * @return void */ public function set_negotiate_detected( $detected ) { $this->data['negotiate_detected'] = $detected; } /** * Increment message count. * * @since 1.0.0 * @return void */ public function increment_messages() { $current = isset( $this->data['messages_count'] ) ? $this->data['messages_count'] : 0; $this->data['messages_count'] = $current + 1; } /** * Set last message preview. * * @since 1.0.0 * @param string $message Message content. * @return void */ public function set_last_message( $message ) { $this->data['last_message'] = sanitize_text_field( substr( $message, 0, 255 ) ); } /** * Destroy the session. * * @since 1.0.0 * @return bool True on success. */ public function destroy() { return delete_transient( 'agentorie_chat_session_' . $this->session_id ); } /** * Sync session data to database. * * @since 1.0.0 * @return bool True on success. */ public function sync_to_database() { return Agentorie_Database::update_session( $this->session_id, $this->data ); } /** * Get or create a session. * * @since 1.0.0 * @param string $session_id Session ID. * @return Agentorie_Session Session instance. */ public static function get_or_create( $session_id ) { if ( ! self::validate_session_id( $session_id ) ) { $session_id = self::generate_session_id(); } $instance = self::get_instance()->load( $session_id ); $db_session = Agentorie_Database::get_or_create_session( $session_id, $instance->get_all() ); // Synchronize critical flags from database to transient. // Database is source of truth for admin_takeover (set by LiveChat admin). if ( $db_session && is_array( $db_session ) ) { $sync_fields = array( 'admin_takeover', 'admin_user_id', 'human_requested', 'negotiate_detected' ); $needs_save = false; foreach ( $sync_fields as $field ) { if ( isset( $db_session[ $field ] ) ) { $db_value = $db_session[ $field ]; $current_value = $instance->get( $field ); // Convert to comparable types. if ( is_numeric( $db_value ) ) { $db_value = (bool) intval( $db_value ); } if ( is_numeric( $current_value ) ) { $current_value = (bool) intval( $current_value ); } // If DB has true and transient has false, sync from DB. if ( $db_value && ! $current_value ) { $instance->set( $field, $db_value ); $needs_save = true; } } } // Save transient if we synced any fields. if ( $needs_save ) { $instance->save(); } } return $instance; } /** * Validate visitor ID format. * * @since 1.0.0 * @param string $visitor_id Visitor ID to validate. * @return bool True if valid. */ public static function validate_visitor_id( $visitor_id ) { return (bool) preg_match( '/^[A-Za-z0-9]{8,12}$/i', $visitor_id ); } /** * Generate a visitor ID. * * Note: The visitor ID is managed client-side via JavaScript localStorage * and sent with each request. This method provides a server-side fallback. * * @since 1.0.0 * @return string Visitor ID. */ public static function get_or_create_visitor_id() { return agentorie_chat_generate_visitor_id(); } /** * Get license data. * * @since 1.0.0 * @param bool $force_refresh Force refresh from API. * @return array License data. */ public static function get_license( $force_refresh = false ) { if ( ! $force_refresh ) { $license = get_transient( self::LICENSE_CACHE_KEY ); if ( false !== $license && is_array( $license ) ) { return $license; } } $license_key = agentorie_chat_get_setting( 'license_key', '' ); if ( empty( $license_key ) ) { $license = self::get_free_license_data(); set_transient( self::LICENSE_CACHE_KEY, $license, HOUR_IN_SECONDS ); return $license; } if ( class_exists( 'Agentorie_API' ) ) { $response = Agentorie_API::check_license( $license_key ); if ( ! is_wp_error( $response ) && isset( $response['license'] ) ) { set_transient( self::LICENSE_CACHE_KEY, $response['license'], HOUR_IN_SECONDS ); update_option( 'agentorie_chat_license_backup', $response['license'] ); return $response['license']; } } $cached = get_option( 'agentorie_chat_license_backup', null ); if ( $cached ) { return $cached; } return self::get_free_license_data(); } /** * Get free license default data. * * @since 1.0.0 * @return array Free license data. */ private static function get_free_license_data() { return array( 'plan' => 'free', 'status' => 'active', 'features' => array( 'base_chat' ), 'limits' => array( 'conversations_month' => 100, 'faq_characters' => 5000, ), 'expires_at' => null, ); } /** * Get license status. * * @since 1.0.0 * @return string License status. */ public static function get_license_status() { $license = self::get_license(); return isset( $license['status'] ) ? $license['status'] : 'active'; } /** * Check if license is expired. * * @since 1.0.0 * @return bool True if expired. */ public static function is_license_expired() { $license = self::get_license(); if ( empty( $license['expires_at'] ) ) { return false; } return strtotime( $license['expires_at'] ) < time(); } /** * Clear license cache. * * @since 1.0.0 * @return void */ public static function clear_license_cache() { delete_transient( self::LICENSE_CACHE_KEY ); } /** * Get client IP address. * * @since 1.0.0 * @return string IP address or empty string. */ public static function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' ); foreach ( $ip_keys as $key ) { if ( ! empty( $_SERVER[ $key ] ) ) { $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); if ( false !== strpos( $ip, ',' ) ) { $ips = explode( ',', $ip ); $ip = trim( $ips[0] ); } if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } } return ''; } /** * Get user agent string. * * @since 1.0.0 * @return string User agent or empty string. */ public static function get_user_agent() { if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); return substr( $user_agent, 0, 255 ); } return ''; } }