PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.7.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.7.0
3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 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 1 month ago message-builder.php 1 month ago model-environment.php 8 months ago response-id-manager.php 1 month ago session.php 1 week ago usage-stats.php 2 months ago
session.php
262 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 * 3.6.4 still accepted legacy uniqid-shaped cookies so that guests mid-session kept
90 * their files. That branch is gone: it left the original hole open to anyone who simply
91 * sent a uniqid-shaped value, which is the whole attack. Unsigned cookies are refused.
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 // Unsigned values are refused, including the legacy uniqid() shape (13 hex chars)
116 // that 3.6.4 still accepted for migration. Accepting them kept the original defect
117 // alive: the id doubles as an ownership key, so anyone could invent or enumerate a
118 // uniqid-shaped value and claim another guest's files. Re-signing such a cookie on
119 // first sight would not have helped, since a forged one is indistinguishable from a
120 // genuine one at that point. The cost of dropping it is that a guest still holding a
121 // pre-3.6.4 cookie starts a new session and loses the files uploaded in the old one.
122 // The cookie has no expiry, so it dies with the browser and that window is short.
123 return null;
124 }
125
126 public function get_session_id() {
127 // Check if we have the session cookie
128 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
129 $sessionId = $this->parse_session_cookie( $_COOKIE['mwai_session_id'] );
130 if ( $sessionId !== null ) {
131 return $sessionId;
132 }
133 // An unusable cookie falls through so a fresh, signed one is issued below.
134 }
135
136 // Already generated one earlier in this request? Reuse it so repeated calls
137 // stay consistent (setcookie above does not populate $_COOKIE mid-request).
138 if ( $this->sessionId !== null ) {
139 return $this->sessionId;
140 }
141
142 // If no cookie exists and we can set one, create it now (lazy initialization)
143 if ( !headers_sent() && !wp_doing_cron() ) {
144 $this->sessionId = bin2hex( random_bytes( 16 ) );
145 @setcookie( 'mwai_session_id', $this->sessionId . '.' . $this->sign_session_id( $this->sessionId ), [
146 'expires' => 0,
147 'path' => '/',
148 'secure' => is_ssl(),
149 'httponly' => true,
150 ] );
151 return $this->sessionId;
152 }
153
154 // For cron jobs or when headers are sent, return a temporary session ID
155 return wp_doing_cron() ? 'wp-cron' : 'N/A';
156 }
157
158 public function get_ip_address( $force = false ) {
159 // Get the actual IP address
160 $ip_keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR',
161 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_FORWARDED_FOR',
162 'HTTP_FORWARDED', 'REMOTE_ADDR' ];
163 $actual_ip = null;
164 foreach ( $ip_keys as $key ) {
165 if ( array_key_exists( $key, $_SERVER ) === true ) {
166 $ips = explode( ',', $_SERVER[$key] );
167 foreach ( $ips as $ip ) {
168 $ip = trim( $ip );
169 if ( $this->validate_ip( $ip ) ) {
170 $actual_ip = $ip;
171 break 2;
172 }
173 }
174 }
175 }
176 if ( !$actual_ip ) {
177 $actual_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
178 }
179
180 // If privacy_first is enabled and not forced, return hashed IP
181 if ( !$force && $this->core->get_option( 'privacy_first' ) ) {
182 // Use a salt that's unique per site but consistent
183 $salt = wp_salt( 'auth' );
184 // Create a hash that's consistent for the same IP but anonymized
185 return 'hashed_' . substr( hash( 'sha256', $actual_ip . $salt ), 0, 16 );
186 }
187
188 return $actual_ip;
189 }
190
191 public function get_user_data() {
192 $user = wp_get_current_user();
193 if ( empty( $user ) || empty( $user->ID ) ) {
194 return null;
195 }
196
197 // Return both the new format (for frontend) and placeholder format (for do_placeholders)
198 $userData = [
199 'ID' => $user->ID,
200 'name' => $user->display_name,
201 'email' => $user->user_email,
202 'avatar' => get_avatar_url( $user->ID ),
203 'type' => 'logged-in',
204 // Add placeholder keys for do_placeholders function
205 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
206 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
207 'USER_LOGIN' => isset( $user->data ) && isset( $user->data->user_login ) ?
208 $user->data->user_login : null,
209 'DISPLAY_NAME' => isset( $user->data ) && isset( $user->data->display_name ) ?
210 $user->data->display_name : null,
211 'AVATAR_URL' => get_avatar_url( $user->ID ),
212 ];
213
214 return $userData;
215 }
216
217 public function get_user_id() {
218 // This function has to be re-thinked for all other API endpoints
219 $userId = null;
220 // If there is a current session, we probably know the current user
221 if ( is_user_logged_in() ) {
222 $userId = get_current_user_id();
223 }
224 // For guest users, return null instead of generating a string ID
225 // This allows the database to store NULL for guests, which displays as "Guest" in the UI
226 return $userId;
227 }
228
229 /**
230 * Get session-based user ID for guest users
231 * This creates a unique identifier based on session ID for tracking guest uploads
232 *
233 * @return string|null Session-based user ID or null if no session
234 */
235 public function get_session_user_id() {
236 $sessionId = $this->get_session_id();
237 if ( !$sessionId || $sessionId === 'N/A' ) {
238 return null;
239 }
240 // Create a consistent user ID based on session
241 // Prefix with 'session_' to distinguish from real user IDs
242 return 'session_' . $sessionId;
243 }
244
245 public function get_admin_user() {
246 $users = get_users( [ 'role' => 'administrator' ] );
247 if ( !empty( $users ) ) {
248 return $users[0];
249 }
250 return null;
251 }
252
253 // Private helper methods
254 private function validate_ip( $ip ) {
255 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
256 return false;
257 }
258 return true;
259 }
260
261 }
262