MediaWiki  master
PHPSessionHandler.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
26 use BagOStuff;
29 use Psr\Log\LoggerInterface;
30 use Psr\Log\NullLogger;
31 use Wikimedia\AtEase\AtEase;
32 
38 class PHPSessionHandler implements \SessionHandlerInterface {
40  protected static $instance = null;
41 
43  protected $enable = false;
44 
46  protected $warn = true;
47 
49  protected $manager;
50 
52  protected $store;
53 
55  protected $logger;
56 
58  protected $sessionFieldCache = [];
59 
60  protected function __construct( SessionManager $manager ) {
61  $this->setEnableFlags(
63  );
64  $manager->setupPHPSessionHandler( $this );
65  }
66 
75  private function setEnableFlags( $PHPSessionHandling ) {
76  switch ( $PHPSessionHandling ) {
77  case 'enable':
78  $this->enable = true;
79  $this->warn = false;
80  break;
81 
82  case 'warn':
83  $this->enable = true;
84  $this->warn = true;
85  break;
86 
87  case 'disable':
88  $this->enable = false;
89  $this->warn = false;
90  break;
91  }
92  }
93 
98  public static function isInstalled() {
99  return (bool)self::$instance;
100  }
101 
106  public static function isEnabled() {
107  return self::$instance && self::$instance->enable;
108  }
109 
114  public static function install( SessionManager $manager ) {
115  if ( self::$instance ) {
116  $manager->setupPHPSessionHandler( self::$instance );
117  return;
118  }
119 
120  // @codeCoverageIgnoreStart
121  if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
122  throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
123  }
124  // @codeCoverageIgnoreEnd
125 
126  self::$instance = new self( $manager );
127 
128  // Close any auto-started session, before we replace it
129  session_write_close();
130 
131  try {
132  AtEase::suppressWarnings();
133 
134  // Tell PHP not to mess with cookies itself
135  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal Scalar okay with php8.1
136  ini_set( 'session.use_cookies', 0 );
137  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal Scalar okay with php8.1
138  ini_set( 'session.use_trans_sid', 0 );
139 
140  // T124510: Disable automatic PHP session related cache headers.
141  // MediaWiki adds its own headers and the default PHP behavior may
142  // set headers such as 'Pragma: no-cache' that cause problems with
143  // some user agents.
144  session_cache_limiter( '' );
145 
146  // Also set a serialization handler
147  \Wikimedia\PhpSessionSerializer::setSerializeHandler();
148 
149  // Register this as the save handler, and register an appropriate
150  // shutdown function.
151  session_set_save_handler( self::$instance, true );
152  } finally {
153  AtEase::restoreWarnings();
154  }
155  }
156 
164  public function setManager(
166  ) {
167  if ( $this->manager !== $manager ) {
168  // Close any existing session before we change stores
169  if ( $this->manager ) {
170  session_write_close();
171  }
172  $this->manager = $manager;
173  $this->store = $store;
174  $this->logger = $logger;
175  \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
176  }
177  }
178 
186  #[\ReturnTypeWillChange]
187  public function open( $save_path, $session_name ) {
188  if ( self::$instance !== $this ) {
189  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
190  }
191  if ( !$this->enable ) {
192  throw new \BadMethodCallException( 'Attempt to use PHP session management' );
193  }
194  return true;
195  }
196 
202  #[\ReturnTypeWillChange]
203  public function close() {
204  if ( self::$instance !== $this ) {
205  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
206  }
207  $this->sessionFieldCache = [];
208  return true;
209  }
210 
217  #[\ReturnTypeWillChange]
218  public function read( $id ) {
219  if ( self::$instance !== $this ) {
220  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
221  }
222  if ( !$this->enable ) {
223  throw new \BadMethodCallException( 'Attempt to use PHP session management' );
224  }
225 
226  $session = $this->manager->getSessionById( $id, false );
227  if ( !$session ) {
228  return '';
229  }
230  $session->persist();
231 
232  $data = iterator_to_array( $session );
233  $this->sessionFieldCache[$id] = $data;
234  return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
235  }
236 
246  #[\ReturnTypeWillChange]
247  public function write( $id, $dataStr ) {
248  if ( self::$instance !== $this ) {
249  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
250  }
251  if ( !$this->enable ) {
252  throw new \BadMethodCallException( 'Attempt to use PHP session management' );
253  }
254 
255  $session = $this->manager->getSessionById( $id, true );
256  if ( !$session ) {
257  // This can happen under normal circumstances, if the session exists but is
258  // invalid. Let's emit a log warning instead of a PHP warning.
259  $this->logger->warning(
260  __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
261  [
262  'session' => $id,
263  ] );
264  return true;
265  }
266 
267  // First, decode the string PHP handed us
268  $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
269  if ( $data === null ) {
270  // @codeCoverageIgnoreStart
271  return false;
272  // @codeCoverageIgnoreEnd
273  }
274 
275  // Now merge the data into the Session object.
276  $changed = false;
277  $cache = $this->sessionFieldCache[$id] ?? [];
278  foreach ( $data as $key => $value ) {
279  if ( !array_key_exists( $key, $cache ) ) {
280  if ( $session->exists( $key ) ) {
281  // New in both, so ignore and log
282  $this->logger->warning(
283  __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
284  );
285  } else {
286  // New in $_SESSION, keep it
287  $session->set( $key, $value );
288  $changed = true;
289  }
290  } elseif ( $cache[$key] === $value ) {
291  // Unchanged in $_SESSION, so ignore it
292  } elseif ( !$session->exists( $key ) ) {
293  // Deleted in Session, keep but log
294  $this->logger->warning(
295  __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
296  );
297  $session->set( $key, $value );
298  $changed = true;
299  } elseif ( $cache[$key] === $session->get( $key ) ) {
300  // Unchanged in Session, so keep it
301  $session->set( $key, $value );
302  $changed = true;
303  } else {
304  // Changed in both, so ignore and log
305  $this->logger->warning(
306  __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
307  );
308  }
309  }
310  // Anything deleted in $_SESSION and unchanged in Session should be deleted too
311  // (but not if $_SESSION can't represent it at all)
312  \Wikimedia\PhpSessionSerializer::setLogger( new NullLogger() );
313  foreach ( $cache as $key => $value ) {
314  if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
315  \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
316  ) {
317  if ( $value === $session->get( $key ) ) {
318  // Unchanged in Session, delete it
319  $session->remove( $key );
320  $changed = true;
321  } else {
322  // Changed in Session, ignore deletion and log
323  $this->logger->warning(
324  __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
325  );
326  }
327  }
328  }
329  \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
330 
331  // Save and update cache if anything changed
332  if ( $changed ) {
333  if ( $this->warn ) {
334  wfDeprecated( '$_SESSION', '1.27' );
335  $this->logger->warning( 'Something wrote to $_SESSION!' );
336  }
337 
338  $session->save();
339  $this->sessionFieldCache[$id] = iterator_to_array( $session );
340  }
341 
342  $session->persist();
343 
344  return true;
345  }
346 
353  #[\ReturnTypeWillChange]
354  public function destroy( $id ) {
355  if ( self::$instance !== $this ) {
356  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
357  }
358  if ( !$this->enable ) {
359  throw new \BadMethodCallException( 'Attempt to use PHP session management' );
360  }
361  $session = $this->manager->getSessionById( $id, false );
362  if ( $session ) {
363  $session->clear();
364  }
365  return true;
366  }
367 
375  #[\ReturnTypeWillChange]
376  public function gc( $maxlifetime ) {
377  if ( self::$instance !== $this ) {
378  throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
379  }
380  $this->store->deleteObjectsExpiringBefore( wfTimestampNow() );
381  return true;
382  }
383 }
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.
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
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.
SessionManagerInterface null $manager
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.
__construct(SessionManager $manager)
bool $enable
Whether PHP session handling is enabled.
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
static PHPSessionHandler $instance
This serves as the entry point to the MediaWiki session handling system.
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...