]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/session/PHPSessionHandler.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / session / PHPSessionHandler.php
1 <?php
2 /**
3  * Session storage in object cache.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Session
22  */
23
24 namespace MediaWiki\Session;
25
26 use Psr\Log\LoggerInterface;
27 use BagOStuff;
28
29 /**
30  * Adapter for PHP's session handling
31  * @ingroup Session
32  * @since 1.27
33  */
34 class PHPSessionHandler implements \SessionHandlerInterface {
35         /** @var PHPSessionHandler */
36         protected static $instance = null;
37
38         /** @var bool Whether PHP session handling is enabled */
39         protected $enable = false;
40         protected $warn = true;
41
42         /** @var SessionManager|null */
43         protected $manager;
44
45         /** @var BagOStuff|null */
46         protected $store;
47
48         /** @var LoggerInterface */
49         protected $logger;
50
51         /** @var array Track original session fields for later modification check */
52         protected $sessionFieldCache = [];
53
54         protected function __construct( SessionManager $manager ) {
55                 $this->setEnableFlags(
56                         \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
57                 );
58                 $manager->setupPHPSessionHandler( $this );
59         }
60
61         /**
62          * Set $this->enable and $this->warn
63          *
64          * Separate just because there doesn't seem to be a good way to test it
65          * otherwise.
66          *
67          * @param string $PHPSessionHandling See $wgPHPSessionHandling
68          */
69         private function setEnableFlags( $PHPSessionHandling ) {
70                 switch ( $PHPSessionHandling ) {
71                         case 'enable':
72                                 $this->enable = true;
73                                 $this->warn = false;
74                                 break;
75
76                         case 'warn':
77                                 $this->enable = true;
78                                 $this->warn = true;
79                                 break;
80
81                         case 'disable':
82                                 $this->enable = false;
83                                 $this->warn = false;
84                                 break;
85                 }
86         }
87
88         /**
89          * Test whether the handler is installed
90          * @return bool
91          */
92         public static function isInstalled() {
93                 return (bool)self::$instance;
94         }
95
96         /**
97          * Test whether the handler is installed and enabled
98          * @return bool
99          */
100         public static function isEnabled() {
101                 return self::$instance && self::$instance->enable;
102         }
103
104         /**
105          * Install a session handler for the current web request
106          * @param SessionManager $manager
107          */
108         public static function install( SessionManager $manager ) {
109                 if ( self::$instance ) {
110                         $manager->setupPHPSessionHandler( self::$instance );
111                         return;
112                 }
113
114                 // @codeCoverageIgnoreStart
115                 if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
116                         throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
117                 }
118                 // @codeCoverageIgnoreEnd
119
120                 self::$instance = new self( $manager );
121
122                 // Close any auto-started session, before we replace it
123                 session_write_close();
124
125                 // Tell PHP not to mess with cookies itself
126                 ini_set( 'session.use_cookies', 0 );
127                 ini_set( 'session.use_trans_sid', 0 );
128
129                 // T124510: Disable automatic PHP session related cache headers.
130                 // MediaWiki adds it's own headers and the default PHP behavior may
131                 // set headers such as 'Pragma: no-cache' that cause problems with
132                 // some user agents.
133                 session_cache_limiter( '' );
134
135                 // Also set a sane serialization handler
136                 \Wikimedia\PhpSessionSerializer::setSerializeHandler();
137
138                 // Register this as the save handler, and register an appropriate
139                 // shutdown function.
140                 session_set_save_handler( self::$instance, true );
141         }
142
143         /**
144          * Set the manager, store, and logger
145          * @private Use self::install().
146          * @param SessionManager $manager
147          * @param BagOStuff $store
148          * @param LoggerInterface $logger
149          */
150         public function setManager(
151                 SessionManager $manager, BagOStuff $store, LoggerInterface $logger
152         ) {
153                 if ( $this->manager !== $manager ) {
154                         // Close any existing session before we change stores
155                         if ( $this->manager ) {
156                                 session_write_close();
157                         }
158                         $this->manager = $manager;
159                         $this->store = $store;
160                         $this->logger = $logger;
161                         \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
162                 }
163         }
164
165         /**
166          * Workaround for PHP5 bug
167          *
168          * PHP5 has a bug in handling boolean return values for
169          * SessionHandlerInterface methods, it expects 0 or -1 instead of true or
170          * false. See <https://wiki.php.net/rfc/session.user.return-value>.
171          *
172          * PHP7 and HHVM are not affected.
173          *
174          * @todo When we drop support for Zend PHP 5, this can be removed.
175          * @return bool|int
176          * @codeCoverageIgnore
177          */
178         protected static function returnSuccess() {
179                 return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? true : 0;
180         }
181
182         /**
183          * Workaround for PHP5 bug
184          * @see self::returnSuccess()
185          * @return bool|int
186          * @codeCoverageIgnore
187          */
188         protected static function returnFailure() {
189                 return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? false : -1;
190         }
191
192         /**
193          * Initialize the session (handler)
194          * @private For internal use only
195          * @param string $save_path Path used to store session files (ignored)
196          * @param string $session_name Session name (ignored)
197          * @return bool|int Success (see self::returnSuccess())
198          */
199         public function open( $save_path, $session_name ) {
200                 if ( self::$instance !== $this ) {
201                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
202                 }
203                 if ( !$this->enable ) {
204                         throw new \BadMethodCallException( 'Attempt to use PHP session management' );
205                 }
206                 return self::returnSuccess();
207         }
208
209         /**
210          * Close the session (handler)
211          * @private For internal use only
212          * @return bool|int Success (see self::returnSuccess())
213          */
214         public function close() {
215                 if ( self::$instance !== $this ) {
216                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
217                 }
218                 $this->sessionFieldCache = [];
219                 return self::returnSuccess();
220         }
221
222         /**
223          * Read session data
224          * @private For internal use only
225          * @param string $id Session id
226          * @return string Session data
227          */
228         public function read( $id ) {
229                 if ( self::$instance !== $this ) {
230                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
231                 }
232                 if ( !$this->enable ) {
233                         throw new \BadMethodCallException( 'Attempt to use PHP session management' );
234                 }
235
236                 $session = $this->manager->getSessionById( $id, false );
237                 if ( !$session ) {
238                         return '';
239                 }
240                 $session->persist();
241
242                 $data = iterator_to_array( $session );
243                 $this->sessionFieldCache[$id] = $data;
244                 return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
245         }
246
247         /**
248          * Write session data
249          * @private For internal use only
250          * @param string $id Session id
251          * @param string $dataStr Session data. Not that you should ever call this
252          *   directly, but note that this has the same issues with code injection
253          *   via user-controlled data as does PHP's unserialize function.
254          * @return bool|int Success (see self::returnSuccess())
255          */
256         public function write( $id, $dataStr ) {
257                 if ( self::$instance !== $this ) {
258                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
259                 }
260                 if ( !$this->enable ) {
261                         throw new \BadMethodCallException( 'Attempt to use PHP session management' );
262                 }
263
264                 $session = $this->manager->getSessionById( $id, true );
265                 if ( !$session ) {
266                         // This can happen under normal circumstances, if the session exists but is
267                         // invalid. Let's emit a log warning instead of a PHP warning.
268                         $this->logger->warning(
269                                 __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
270                                 [
271                                         'session' => $id,
272                         ] );
273                         return self::returnSuccess();
274                 }
275
276                 // First, decode the string PHP handed us
277                 $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
278                 if ( $data === null ) {
279                         // @codeCoverageIgnoreStart
280                         return self::returnFailure();
281                         // @codeCoverageIgnoreEnd
282                 }
283
284                 // Now merge the data into the Session object.
285                 $changed = false;
286                 $cache = isset( $this->sessionFieldCache[$id] ) ? $this->sessionFieldCache[$id] : [];
287                 foreach ( $data as $key => $value ) {
288                         if ( !array_key_exists( $key, $cache ) ) {
289                                 if ( $session->exists( $key ) ) {
290                                         // New in both, so ignore and log
291                                         $this->logger->warning(
292                                                 __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
293                                         );
294                                 } else {
295                                         // New in $_SESSION, keep it
296                                         $session->set( $key, $value );
297                                         $changed = true;
298                                 }
299                         } elseif ( $cache[$key] === $value ) {
300                                 // Unchanged in $_SESSION, so ignore it
301                         } elseif ( !$session->exists( $key ) ) {
302                                 // Deleted in Session, keep but log
303                                 $this->logger->warning(
304                                         __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
305                                 );
306                                 $session->set( $key, $value );
307                                 $changed = true;
308                         } elseif ( $cache[$key] === $session->get( $key ) ) {
309                                 // Unchanged in Session, so keep it
310                                 $session->set( $key, $value );
311                                 $changed = true;
312                         } else {
313                                 // Changed in both, so ignore and log
314                                 $this->logger->warning(
315                                         __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
316                                 );
317                         }
318                 }
319                 // Anything deleted in $_SESSION and unchanged in Session should be deleted too
320                 // (but not if $_SESSION can't represent it at all)
321                 \Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
322                 foreach ( $cache as $key => $value ) {
323                         if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
324                                 \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
325                         ) {
326                                 if ( $cache[$key] === $session->get( $key ) ) {
327                                         // Unchanged in Session, delete it
328                                         $session->remove( $key );
329                                         $changed = true;
330                                 } else {
331                                         // Changed in Session, ignore deletion and log
332                                         $this->logger->warning(
333                                                 __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
334                                         );
335                                 }
336                         }
337                 }
338                 \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
339
340                 // Save and update cache if anything changed
341                 if ( $changed ) {
342                         if ( $this->warn ) {
343                                 wfDeprecated( '$_SESSION', '1.27' );
344                                 $this->logger->warning( 'Something wrote to $_SESSION!' );
345                         }
346
347                         $session->save();
348                         $this->sessionFieldCache[$id] = iterator_to_array( $session );
349                 }
350
351                 $session->persist();
352
353                 return self::returnSuccess();
354         }
355
356         /**
357          * Destroy a session
358          * @private For internal use only
359          * @param string $id Session id
360          * @return bool|int Success (see self::returnSuccess())
361          */
362         public function destroy( $id ) {
363                 if ( self::$instance !== $this ) {
364                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
365                 }
366                 if ( !$this->enable ) {
367                         throw new \BadMethodCallException( 'Attempt to use PHP session management' );
368                 }
369                 $session = $this->manager->getSessionById( $id, false );
370                 if ( $session ) {
371                         $session->clear();
372                 }
373                 return self::returnSuccess();
374         }
375
376         /**
377          * Execute garbage collection.
378          * @private For internal use only
379          * @param int $maxlifetime Maximum session life time (ignored)
380          * @return bool|int Success (see self::returnSuccess())
381          * @codeCoverageIgnore See T135576
382          */
383         public function gc( $maxlifetime ) {
384                 if ( self::$instance !== $this ) {
385                         throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
386                 }
387                 $before = date( 'YmdHis', time() );
388                 $this->store->deleteObjectsExpiringBefore( $before );
389                 return self::returnSuccess();
390         }
391 }