PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / lib / mcp-adapter / includes / Transport / Infrastructure / SessionManager.php

SessionManager.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at lib/mcp-adapter/includes/Transport/Infrastructure/SessionManager.php

341 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Session Manager using User Meta
4 *
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Transport\Infrastructure;
12
13 use WP_Error;
14
15 /**
16 * MCP Session Manager
17 *
18 * Handles session creation, validation, and cleanup using user meta storage.
19 * Sessions are tied to authenticated users to prevent anonymous session flooding.
20 */
21 final class SessionManager {
22
23 /**
24 * User meta key for storing sessions
25 *
26 * @var string
27 */
28 private const SESSION_META_KEY = 'mcp_adapter_sessions';
29
30 /**
31 * Maximum sessions per user.
32 *
33 * @var int
34 */
35 private const DEFAULT_MAX_SESSIONS = 32;
36
37 /**
38 * Session inactivity timeout in seconds (24 hours).
39 *
40 * @var int
41 */
42 private const DEFAULT_INACTIVITY_TIMEOUT = DAY_IN_SECONDS;
43
44 /**
45 * Minimum interval between last_activity writes in seconds.
46 *
47 * @var int
48 */
49 private const DEFAULT_ACTIVITY_UPDATE_INTERVAL = 60;
50
51 /**
52 * Create a new session for a user
53 *
54 * @param int $user_id The user ID.
55 * @param array $params Client parameters from initialize request.
56 *
57 * @return string|false The session ID on success, false on failure.
58 */
59 public static function create_session( int $user_id, array $params = array() ) {
60 if ( ! $user_id || ! get_user_by( 'id', $user_id ) ) {
61 return false;
62 }
63
64 // Cleanup inactive sessions first
65 self::cleanup_expired_sessions( $user_id );
66
67 // Get current sessions
68 $sessions = self::get_all_user_sessions( $user_id );
69
70 // Check session limit - remove oldest if over limit
71 $config = self::get_config();
72 $max_sessions = $config['max_sessions'];
73 if ( count( $sessions ) >= $max_sessions ) {
74 // Remove oldest session (FIFO) - sort by created_at and remove first
75 uasort(
76 $sessions,
77 static function ( $a, $b ) {
78 return $a['created_at'] <=> $b['created_at'];
79 }
80 );
81
82 array_shift( $sessions );
83 }
84
85 // Create a new session
86 $session_id = wp_generate_uuid4();
87 $now = time();
88
89 $sessions[ $session_id ] = array(
90 'created_at' => $now,
91 'last_activity' => $now,
92 'client_params' => $params,
93 );
94
95 // Save sessions
96 update_user_meta( $user_id, self::SESSION_META_KEY, $sessions );
97
98 return $session_id;
99 }
100
101 /**
102 * Cleanup inactive sessions for a user
103 *
104 * @param int $user_id The user ID.
105 *
106 * @return int Number of sessions removed.
107 */
108 public static function cleanup_expired_sessions( int $user_id ): int {
109 if ( ! $user_id ) {
110 return 0;
111 }
112
113 $sessions = self::get_all_user_sessions( $user_id );
114 $now = time();
115 $removed = 0;
116
117 $config = self::get_config();
118 $inactivity_timeout = $config['inactivity_timeout'];
119
120 foreach ( $sessions as $session_id => $session ) {
121 // Check if still active - skip if valid
122 if ( $session['last_activity'] + $inactivity_timeout >= $now ) {
123 continue;
124 }
125
126 // Session is inactive - remove it
127 unset( $sessions[ $session_id ] );
128 ++$removed;
129 }
130
131 if ( $removed > 0 ) {
132 if ( empty( $sessions ) ) {
133 delete_user_meta( $user_id, self::SESSION_META_KEY );
134 } else {
135 update_user_meta( $user_id, self::SESSION_META_KEY, $sessions );
136 }
137 }
138
139 return $removed;
140 }
141
142 /**
143 * Get all sessions for a user
144 *
145 * @param int $user_id The user ID.
146 *
147 * @return array Array of sessions.
148 */
149 public static function get_all_user_sessions( int $user_id ): array {
150 if ( ! $user_id ) {
151 return array();
152 }
153
154 $sessions = get_user_meta( $user_id, self::SESSION_META_KEY, true );
155
156 if ( ! is_array( $sessions ) ) {
157 return array();
158 }
159
160 return $sessions;
161 }
162
163 /**
164 * Get configuration values.
165 *
166 * @return array{max_sessions: int, inactivity_timeout: int, activity_update_interval: int} Configuration array.
167 */
168 private static function get_config(): array {
169 /**
170 * Filters the maximum number of MCP sessions allowed per user.
171 *
172 * When a user exceeds this limit, the oldest inactive session is
173 * automatically removed to make room for new sessions.
174 *
175 * @since 0.3.0
176 *
177 * @param int $max_sessions Maximum sessions per user. Default 32.
178 */
179 $max_sessions = (int) apply_filters( 'mcp_adapter_session_max_per_user', self::DEFAULT_MAX_SESSIONS );
180
181 /**
182 * Filters the session inactivity timeout in seconds.
183 *
184 * Sessions that have been inactive longer than this duration are
185 * considered expired and may be cleaned up automatically.
186 *
187 * @since 0.3.0
188 *
189 * @param int $timeout Inactivity timeout in seconds. Default DAY_IN_SECONDS (86400 / 24 hours).
190 */
191 $inactivity_timeout = (int) apply_filters( 'mcp_adapter_session_inactivity_timeout', self::DEFAULT_INACTIVITY_TIMEOUT );
192
193 /**
194 * Filters the minimum interval between session last_activity writes.
195 *
196 * To reduce write amplification, the session manager only updates
197 * `last_activity` if at least this many seconds have elapsed since
198 * the last write.
199 *
200 * @since 0.5.0
201 *
202 * @param int $interval Minimum seconds between writes. Default 60.
203 */
204 $activity_update_interval = (int) apply_filters( 'mcp_adapter_session_activity_update_interval', self::DEFAULT_ACTIVITY_UPDATE_INTERVAL );
205
206 // Clamp: interval must be less than inactivity timeout to prevent
207 // sessions from expiring despite active use.
208 if ( $activity_update_interval >= $inactivity_timeout ) {
209 $activity_update_interval = (int) ( $inactivity_timeout / 2 );
210 }
211
212 return array(
213 'max_sessions' => $max_sessions,
214 'inactivity_timeout' => $inactivity_timeout,
215 'activity_update_interval' => max( 0, $activity_update_interval ),
216 );
217 }
218
219 /**
220 * Get a specific session for a user
221 *
222 * @param int $user_id The user ID.
223 * @param string $session_id The session ID.
224 *
225 * @return array|\WP_Error|false Session data on success, WP_Error on invalid input, false if not found or inactive.
226 */
227 public static function get_session( int $user_id, string $session_id ) {
228 if ( ! $user_id || ! $session_id ) {
229 return new WP_Error( 'mcp_session_invalid_input', 'Invalid user ID or session ID.' );
230 }
231
232 $sessions = self::get_all_user_sessions( $user_id );
233
234 if ( ! isset( $sessions[ $session_id ] ) ) {
235 return false;
236 }
237
238 $session = $sessions[ $session_id ];
239
240 // Check inactivity timeout
241 $config = self::get_config();
242 $inactivity_timeout = $config['inactivity_timeout'];
243 if ( $session['last_activity'] + $inactivity_timeout < time() ) {
244 self::clear_session( $user_id, $session_id );
245
246 return false;
247 }
248
249 return $session;
250 }
251
252 /**
253 * Clear an inactive session (internal cleanup).
254 *
255 * @param int $user_id The user ID.
256 * @param string $session_id The session ID to clear.
257 *
258 * @return void
259 */
260 private static function clear_session( int $user_id, string $session_id ): void {
261 $sessions = self::get_all_user_sessions( $user_id );
262
263 if ( ! isset( $sessions[ $session_id ] ) ) {
264 return;
265 }
266
267 unset( $sessions[ $session_id ] );
268 update_user_meta( $user_id, self::SESSION_META_KEY, $sessions );
269 }
270
271 /**
272 * Validate a session and update last activity
273 *
274 * @param int $user_id The user ID.
275 * @param string $session_id The session ID.
276 *
277 * @return bool True if valid, false otherwise.
278 */
279 public static function validate_session( int $user_id, string $session_id ): bool {
280 if ( ! $user_id || ! $session_id ) {
281 return false;
282 }
283
284 $sessions = self::get_all_user_sessions( $user_id );
285
286 if ( ! isset( $sessions[ $session_id ] ) ) {
287 return false;
288 }
289
290 $session = $sessions[ $session_id ];
291
292 // Check inactivity timeout
293 $config = self::get_config();
294 $inactivity_timeout = $config['inactivity_timeout'];
295 if ( $session['last_activity'] + $inactivity_timeout < time() ) {
296 self::clear_session( $user_id, $session_id );
297
298 return false;
299 }
300
301 // Throttle last_activity writes to reduce write amplification
302 $activity_update_interval = $config['activity_update_interval'];
303 if ( time() - $session['last_activity'] >= $activity_update_interval ) {
304 $sessions[ $session_id ]['last_activity'] = time();
305 update_user_meta( $user_id, self::SESSION_META_KEY, $sessions );
306 }
307
308 return true;
309 }
310
311 /**
312 * Delete a specific session
313 *
314 * @param int $user_id The user ID.
315 * @param string $session_id The session ID.
316 *
317 * @return bool True on success, false on failure.
318 */
319 public static function delete_session( int $user_id, string $session_id ): bool {
320 if ( ! $user_id || ! $session_id ) {
321 return false;
322 }
323
324 $sessions = self::get_all_user_sessions( $user_id );
325
326 if ( ! isset( $sessions[ $session_id ] ) ) {
327 return false;
328 }
329
330 unset( $sessions[ $session_id ] );
331
332 if ( empty( $sessions ) ) {
333 delete_user_meta( $user_id, self::SESSION_META_KEY );
334 } else {
335 update_user_meta( $user_id, self::SESSION_META_KEY, $sessions );
336 }
337
338 return true;
339 }
340 }
341