PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.4
3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / services / session.php
ai-engine / classes / services Last commit date
image.php 4 weeks ago message-builder.php 1 month ago model-environment.php 7 months ago response-id-manager.php 4 weeks ago session.php 1 day ago usage-stats.php 2 months ago
session.php
258 lines
1 <?php
2
3 class Meow_MWAI_Services_Session {
4 private $core;
5 private $nonce = null;
6 // Memoized guest session id for this request. Without it, a guest whose first
7 // request has no cookie yet gets a fresh uniqid() on every call (setcookie
8 // does not populate $_COOKIE mid-request), so two calls in the same request
9 // disagree and guest-scoped checks (e.g. file ownership) mismatch.
10 private $sessionId = null;
11
12 public function __construct( $core ) {
13 $this->core = $core;
14 }
15
16 public function can_start_session() {
17 // Check if session already started
18 if ( session_status() !== PHP_SESSION_NONE ) {
19 return false;
20 }
21
22 // Check if we're in a context where sessions shouldn't be started
23 if ( wp_doing_cron() || defined( 'DOING_AUTOSAVE' ) ) {
24 return false;
25 }
26
27 // For AI Engine REST endpoints only - check if it's actually our endpoint
28 if ( $this->core->is_rest ) {
29 $request_uri = $_SERVER['REQUEST_URI'] ?? '';
30 // Only start sessions for actual AI Engine endpoints
31 if ( strpos( $request_uri, '/mwai/' ) === false && strpos( $request_uri, 'rest_route=/mwai/' ) === false ) {
32 return false;
33 }
34 }
35
36 // Off by default. Nothing in the plugin reads $_SESSION any more: the guest id moved
37 // to the mwai_session_id cookie and the news flag was dropped. Starting one anyway
38 // had two costs. It set a PHPSESSID cookie, so once a visitor used a chatbot every
39 // later page request carried a session cookie and Varnish (and most page caches)
40 // stopped serving them from cache. And PHP holds an exclusive write lock on the
41 // session file for the whole request, which serializes concurrent calls: that is the
42 // max_execution_time hang the MCP path had to work around with release_session_lock().
43 //
44 // Set this filter to true if your own code (a function-calling handler, for example)
45 // needs $_SESSION on AI Engine's REST endpoints.
46 return apply_filters( 'mwai_allow_session', false );
47 }
48
49 public function get_nonce( $force = false ) {
50 // NONCE GENERATION LOGIC:
51 // - For logged-out users (unless forced): Return null - they must use /start_session endpoint
52 // - For logged-in users: Create user-specific nonce tied to their WP session
53 // - With $force=true: Always create nonce (used by /start_session endpoint)
54 //
55 // This ensures logged-in users get a nonce matching their auth context on page load,
56 // preventing rest_cookie_invalid_nonce errors when cookies are present.
57 if ( !$force && !is_user_logged_in() ) {
58 return null;
59 }
60 if ( isset( $this->nonce ) ) {
61 return $this->nonce;
62 }
63 $this->nonce = wp_create_nonce( 'wp_rest' );
64 return $this->nonce;
65 }
66
67 // ChatID
68 public function fix_chat_id( $query, $params ) {
69 if ( isset( $query->chatId ) && $query->chatId !== 'N/A' ) {
70 return $query->chatId;
71 }
72 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : $query->session;
73 if ( $chatId === 'N/A' ) {
74 $chatId = $this->core->get_random_id( 8 );
75 }
76 $query->set_chat_id( $chatId );
77 return $chatId;
78 }
79
80 /**
81 * The guest session id doubles as an ownership key ("session_<id>" owns uploaded
82 * files), so it has to be unguessable AND unforgeable.
83 *
84 * It used to be uniqid(), which is microtime: knowing roughly when a session started
85 * left about a million candidates, so another guest's id could be enumerated rather
86 * than stolen. New ids are random_bytes() and carry an HMAC, so a guessed or invented
87 * id is rejected before it can claim anything.
88 *
89 * Legacy uniqid-shaped cookies are still accepted, otherwise every guest with an open
90 * browser would lose the files and conversations attached to their current session.
91 * They expire on their own: the cookie has no expiry, so it dies with the browser.
92 *
93 * Scope: this stops an id being guessed or invented. It does not stop someone who
94 * already holds a visitor's cookie from acting as that visitor, which is true of any
95 * session cookie and is why this one is HttpOnly, Secure and expires with the browser.
96 * Guessability was the actual defect here, since uniqid() is microtime.
97 */
98 private function sign_session_id( $id ) {
99 return hash_hmac( 'sha256', 'mwai_session|' . $id, wp_salt( 'auth' ) );
100 }
101
102 private function parse_session_cookie( $cookie ) {
103 if ( !is_string( $cookie ) || $cookie === '' ) {
104 return null;
105 }
106 // Signed format: "<id>.<signature>".
107 if ( strpos( $cookie, '.' ) !== false ) {
108 list( $id, $signature ) = array_pad( explode( '.', $cookie, 2 ), 2, '' );
109 if ( $id !== '' && hash_equals( $this->sign_session_id( $id ), $signature ) ) {
110 return $id;
111 }
112 // Claims to be signed but is not: refuse it rather than trust the raw value.
113 return null;
114 }
115 // Legacy uniqid() value (13 hex chars), issued before the signed format.
116 if ( preg_match( '/^[0-9a-f]{13}$/', $cookie ) ) {
117 return $cookie;
118 }
119 return null;
120 }
121
122 public function get_session_id() {
123 // Check if we have the session cookie
124 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
125 $sessionId = $this->parse_session_cookie( $_COOKIE['mwai_session_id'] );
126 if ( $sessionId !== null ) {
127 return $sessionId;
128 }
129 // An unusable cookie falls through so a fresh, signed one is issued below.
130 }
131
132 // Already generated one earlier in this request? Reuse it so repeated calls
133 // stay consistent (setcookie above does not populate $_COOKIE mid-request).
134 if ( $this->sessionId !== null ) {
135 return $this->sessionId;
136 }
137
138 // If no cookie exists and we can set one, create it now (lazy initialization)
139 if ( !headers_sent() && !wp_doing_cron() ) {
140 $this->sessionId = bin2hex( random_bytes( 16 ) );
141 @setcookie( 'mwai_session_id', $this->sessionId . '.' . $this->sign_session_id( $this->sessionId ), [
142 'expires' => 0,
143 'path' => '/',
144 'secure' => is_ssl(),
145 'httponly' => true,
146 ] );
147 return $this->sessionId;
148 }
149
150 // For cron jobs or when headers are sent, return a temporary session ID
151 return wp_doing_cron() ? 'wp-cron' : 'N/A';
152 }
153
154 public function get_ip_address( $force = false ) {
155 // Get the actual IP address
156 $ip_keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR',
157 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_FORWARDED_FOR',
158 'HTTP_FORWARDED', 'REMOTE_ADDR' ];
159 $actual_ip = null;
160 foreach ( $ip_keys as $key ) {
161 if ( array_key_exists( $key, $_SERVER ) === true ) {
162 $ips = explode( ',', $_SERVER[$key] );
163 foreach ( $ips as $ip ) {
164 $ip = trim( $ip );
165 if ( $this->validate_ip( $ip ) ) {
166 $actual_ip = $ip;
167 break 2;
168 }
169 }
170 }
171 }
172 if ( !$actual_ip ) {
173 $actual_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
174 }
175
176 // If privacy_first is enabled and not forced, return hashed IP
177 if ( !$force && $this->core->get_option( 'privacy_first' ) ) {
178 // Use a salt that's unique per site but consistent
179 $salt = wp_salt( 'auth' );
180 // Create a hash that's consistent for the same IP but anonymized
181 return 'hashed_' . substr( hash( 'sha256', $actual_ip . $salt ), 0, 16 );
182 }
183
184 return $actual_ip;
185 }
186
187 public function get_user_data() {
188 $user = wp_get_current_user();
189 if ( empty( $user ) || empty( $user->ID ) ) {
190 return null;
191 }
192
193 // Return both the new format (for frontend) and placeholder format (for do_placeholders)
194 $userData = [
195 'ID' => $user->ID,
196 'name' => $user->display_name,
197 'email' => $user->user_email,
198 'avatar' => get_avatar_url( $user->ID ),
199 'type' => 'logged-in',
200 // Add placeholder keys for do_placeholders function
201 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
202 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
203 'USER_LOGIN' => isset( $user->data ) && isset( $user->data->user_login ) ?
204 $user->data->user_login : null,
205 'DISPLAY_NAME' => isset( $user->data ) && isset( $user->data->display_name ) ?
206 $user->data->display_name : null,
207 'AVATAR_URL' => get_avatar_url( $user->ID ),
208 ];
209
210 return $userData;
211 }
212
213 public function get_user_id() {
214 // This function has to be re-thinked for all other API endpoints
215 $userId = null;
216 // If there is a current session, we probably know the current user
217 if ( is_user_logged_in() ) {
218 $userId = get_current_user_id();
219 }
220 // For guest users, return null instead of generating a string ID
221 // This allows the database to store NULL for guests, which displays as "Guest" in the UI
222 return $userId;
223 }
224
225 /**
226 * Get session-based user ID for guest users
227 * This creates a unique identifier based on session ID for tracking guest uploads
228 *
229 * @return string|null Session-based user ID or null if no session
230 */
231 public function get_session_user_id() {
232 $sessionId = $this->get_session_id();
233 if ( !$sessionId || $sessionId === 'N/A' ) {
234 return null;
235 }
236 // Create a consistent user ID based on session
237 // Prefix with 'session_' to distinguish from real user IDs
238 return 'session_' . $sessionId;
239 }
240
241 public function get_admin_user() {
242 $users = get_users( [ 'role' => 'administrator' ] );
243 if ( !empty( $users ) ) {
244 return $users[0];
245 }
246 return null;
247 }
248
249 // Private helper methods
250 private function validate_ip( $ip ) {
251 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
252 return false;
253 }
254 return true;
255 }
256
257 }
258