MediaWiki master
PHPSessionHandler.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
28use Psr\Log\LoggerInterface;
29use Psr\Log\NullLogger;
30use SessionHandlerInterface;
31use Wikimedia\AtEase\AtEase;
33use Wikimedia\PhpSessionSerializer;
34
40class PHPSessionHandler implements SessionHandlerInterface {
42 protected static $instance = null;
43
45 protected $enable = false;
46
48 protected $warn = true;
49
51 protected ?BagOStuff $store = null;
52 protected LoggerInterface $logger;
53
55 protected $sessionFieldCache = [];
56
57 protected function __construct( SessionManager $manager ) {
58 $this->setEnableFlags(
60 );
61 $manager->setupPHPSessionHandler( $this );
62 }
63
72 private function setEnableFlags( $PHPSessionHandling ) {
73 switch ( $PHPSessionHandling ) {
74 case 'enable':
75 $this->enable = true;
76 $this->warn = false;
77 break;
78
79 case 'warn':
80 $this->enable = true;
81 $this->warn = true;
82 break;
83
84 case 'disable':
85 $this->enable = false;
86 $this->warn = false;
87 break;
88 }
89 }
90
95 public static function isInstalled() {
96 return (bool)self::$instance;
97 }
98
103 public static function isEnabled() {
104 return self::$instance && self::$instance->enable;
105 }
106
110 public static function install( SessionManager $manager ) {
111 if ( self::$instance ) {
112 $manager->setupPHPSessionHandler( self::$instance );
113 return;
114 }
115
116 // @codeCoverageIgnoreStart
117 if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
118 throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
119 }
120 // @codeCoverageIgnoreEnd
121
122 self::$instance = new self( $manager );
123
124 // Close any auto-started session, before we replace it
125 session_write_close();
126
127 try {
128 AtEase::suppressWarnings();
129
130 // Tell PHP not to mess with cookies itself
131 ini_set( 'session.use_cookies', 0 );
132
133 // T124510: Disable automatic PHP session related cache headers.
134 // MediaWiki adds its own headers and the default PHP behavior may
135 // set headers such as 'Pragma: no-cache' that cause problems with
136 // some user agents.
137 session_cache_limiter( '' );
138
139 // Also set a serialization handler
140 PhpSessionSerializer::setSerializeHandler();
141
142 // Register this as the save handler, and register an appropriate
143 // shutdown function.
144 session_set_save_handler( self::$instance, true );
145 } finally {
146 AtEase::restoreWarnings();
147 }
148 }
149
157 public function setManager(
159 ) {
160 if ( $this->manager !== $manager ) {
161 // Close any existing session before we change stores
162 if ( $this->manager ) {
163 session_write_close();
164 }
165 $this->manager = $manager;
166 $this->store = $store;
167 $this->logger = $logger;
168 PhpSessionSerializer::setLogger( $this->logger );
169 }
170 }
171
179 #[\ReturnTypeWillChange]
180 public function open( $save_path, $session_name ) {
181 if ( self::$instance !== $this ) {
182 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
183 }
184 if ( !$this->enable ) {
185 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
186 }
187 return true;
188 }
189
195 #[\ReturnTypeWillChange]
196 public function close() {
197 if ( self::$instance !== $this ) {
198 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
199 }
200 $this->sessionFieldCache = [];
201 return true;
202 }
203
210 #[\ReturnTypeWillChange]
211 public function read( $id ) {
212 if ( self::$instance !== $this ) {
213 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
214 }
215 if ( !$this->enable ) {
216 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
217 }
218
219 $session = $this->manager->getSessionById( $id, false );
220 if ( !$session ) {
221 return '';
222 }
223 $session->persist();
224
225 $data = iterator_to_array( $session );
226 $this->sessionFieldCache[$id] = $data;
227 return (string)PhpSessionSerializer::encode( $data );
228 }
229
239 #[\ReturnTypeWillChange]
240 public function write( $id, $dataStr ) {
241 if ( self::$instance !== $this ) {
242 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
243 }
244 if ( !$this->enable ) {
245 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
246 }
247
248 $session = $this->manager->getSessionById( $id, true );
249 if ( !$session ) {
250 // This can happen under normal circumstances, if the session exists but is
251 // invalid. Let's emit a log warning instead of a PHP warning.
252 $this->logger->warning(
253 __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
254 [
255 'session' => $id,
256 ] );
257 return true;
258 }
259
260 // First, decode the string PHP handed us
261 $data = PhpSessionSerializer::decode( $dataStr );
262 if ( $data === null ) {
263 // @codeCoverageIgnoreStart
264 return false;
265 // @codeCoverageIgnoreEnd
266 }
267
268 // Now merge the data into the Session object.
269 $changed = false;
270 $cache = $this->sessionFieldCache[$id] ?? [];
271 foreach ( $data as $key => $value ) {
272 if ( !array_key_exists( $key, $cache ) ) {
273 if ( $session->exists( $key ) ) {
274 // New in both, so ignore and log
275 $this->logger->warning(
276 __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
277 );
278 } else {
279 // New in $_SESSION, keep it
280 $session->set( $key, $value );
281 $changed = true;
282 }
283 } elseif ( $cache[$key] === $value ) {
284 // Unchanged in $_SESSION, so ignore it
285 } elseif ( !$session->exists( $key ) ) {
286 // Deleted in Session, keep but log
287 $this->logger->warning(
288 __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
289 );
290 $session->set( $key, $value );
291 $changed = true;
292 } elseif ( $cache[$key] === $session->get( $key ) ) {
293 // Unchanged in Session, so keep it
294 $session->set( $key, $value );
295 $changed = true;
296 } else {
297 // Changed in both, so ignore and log
298 $this->logger->warning(
299 __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
300 );
301 }
302 }
303 // Anything deleted in $_SESSION and unchanged in Session should be deleted too
304 // (but not if $_SESSION can't represent it at all)
305 PhpSessionSerializer::setLogger( new NullLogger() );
306 foreach ( $cache as $key => $value ) {
307 if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
308 PhpSessionSerializer::encode( [ $key => true ] )
309 ) {
310 if ( $value === $session->get( $key ) ) {
311 // Unchanged in Session, delete it
312 $session->remove( $key );
313 $changed = true;
314 } else {
315 // Changed in Session, ignore deletion and log
316 $this->logger->warning(
317 __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
318 );
319 }
320 }
321 }
322 PhpSessionSerializer::setLogger( $this->logger );
323
324 // Save and update cache if anything changed
325 if ( $changed ) {
326 if ( $this->warn ) {
327 wfDeprecated( '$_SESSION', '1.27' );
328 $this->logger->warning( 'Something wrote to $_SESSION!' );
329 }
330
331 $session->save();
332 $this->sessionFieldCache[$id] = iterator_to_array( $session );
333 }
334
335 $session->persist();
336
337 return true;
338 }
339
346 #[\ReturnTypeWillChange]
347 public function destroy( $id ) {
348 if ( self::$instance !== $this ) {
349 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
350 }
351 if ( !$this->enable ) {
352 throw new \BadMethodCallException( 'Attempt to use PHP session management' );
353 }
354 $session = $this->manager->getSessionById( $id, false );
355 if ( $session ) {
356 $session->clear();
357 }
358 return true;
359 }
360
368 #[\ReturnTypeWillChange]
369 public function gc( $maxlifetime ) {
370 if ( self::$instance !== $this ) {
371 throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
372 }
373 $this->store->deleteObjectsExpiringBefore( wfTimestampNow() );
374 return true;
375 }
376}
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
A class containing constants representing the names of configuration variables.
const PHPSessionHandling
Name constant for the PHPSessionHandling setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Adapter for PHP's session handling.
gc( $maxlifetime)
Execute garbage collection.
write( $id, $dataStr)
Write session data.
static isInstalled()
Test whether the handler is installed.
static isEnabled()
Test whether the handler is installed and enabled.
close()
Close the session (handler)
array $sessionFieldCache
Track original session fields for later modification check.
open( $save_path, $session_name)
Initialize the session (handler)
static install(SessionManager $manager)
Install a session handler for the current web request.
bool $enable
Whether PHP session handling is enabled.
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
This serves as the entry point to the MediaWiki session handling system.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:88
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...