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