Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
121 / 121 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
PHPSessionHandler | |
100.00% |
121 / 121 |
|
100.00% |
11 / 11 |
47 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setEnableFlags | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
isInstalled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
install | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
setManager | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
open | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
close | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
read | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
write | |
100.00% |
56 / 56 |
|
100.00% |
1 / 1 |
18 | |||
destroy | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
gc | n/a |
0 / 0 |
n/a |
0 / 0 |
2 |
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 BagOStuff; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\MediaWikiServices; |
29 | use Psr\Log\LoggerInterface; |
30 | use Psr\Log\NullLogger; |
31 | use SessionHandlerInterface; |
32 | use Wikimedia\AtEase\AtEase; |
33 | use Wikimedia\PhpSessionSerializer; |
34 | |
35 | /** |
36 | * Adapter for PHP's session handling |
37 | * @ingroup Session |
38 | * @since 1.27 |
39 | */ |
40 | class PHPSessionHandler implements SessionHandlerInterface { |
41 | /** @var PHPSessionHandler */ |
42 | protected static $instance = null; |
43 | |
44 | /** @var bool Whether PHP session handling is enabled */ |
45 | protected $enable = false; |
46 | |
47 | /** @var bool */ |
48 | protected $warn = true; |
49 | |
50 | /** @var SessionManagerInterface|null */ |
51 | protected $manager; |
52 | |
53 | /** @var BagOStuff|null */ |
54 | protected $store; |
55 | |
56 | /** @var LoggerInterface */ |
57 | protected $logger; |
58 | |
59 | /** @var array Track original session fields for later modification check */ |
60 | protected $sessionFieldCache = []; |
61 | |
62 | protected function __construct( SessionManager $manager ) { |
63 | $this->setEnableFlags( |
64 | MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::PHPSessionHandling ) |
65 | ); |
66 | $manager->setupPHPSessionHandler( $this ); |
67 | } |
68 | |
69 | /** |
70 | * Set $this->enable and $this->warn |
71 | * |
72 | * Separate just because there doesn't seem to be a good way to test it |
73 | * otherwise. |
74 | * |
75 | * @param string $PHPSessionHandling See $wgPHPSessionHandling |
76 | */ |
77 | private function setEnableFlags( $PHPSessionHandling ) { |
78 | switch ( $PHPSessionHandling ) { |
79 | case 'enable': |
80 | $this->enable = true; |
81 | $this->warn = false; |
82 | break; |
83 | |
84 | case 'warn': |
85 | $this->enable = true; |
86 | $this->warn = true; |
87 | break; |
88 | |
89 | case 'disable': |
90 | $this->enable = false; |
91 | $this->warn = false; |
92 | break; |
93 | } |
94 | } |
95 | |
96 | /** |
97 | * Test whether the handler is installed |
98 | * @return bool |
99 | */ |
100 | public static function isInstalled() { |
101 | return (bool)self::$instance; |
102 | } |
103 | |
104 | /** |
105 | * Test whether the handler is installed and enabled |
106 | * @return bool |
107 | */ |
108 | public static function isEnabled() { |
109 | return self::$instance && self::$instance->enable; |
110 | } |
111 | |
112 | /** |
113 | * Install a session handler for the current web request |
114 | * @param SessionManager $manager |
115 | */ |
116 | public static function install( SessionManager $manager ) { |
117 | if ( self::$instance ) { |
118 | $manager->setupPHPSessionHandler( self::$instance ); |
119 | return; |
120 | } |
121 | |
122 | // @codeCoverageIgnoreStart |
123 | if ( defined( 'MW_NO_SESSION_HANDLER' ) ) { |
124 | throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' ); |
125 | } |
126 | // @codeCoverageIgnoreEnd |
127 | |
128 | self::$instance = new self( $manager ); |
129 | |
130 | // Close any auto-started session, before we replace it |
131 | session_write_close(); |
132 | |
133 | try { |
134 | AtEase::suppressWarnings(); |
135 | |
136 | // Tell PHP not to mess with cookies itself |
137 | ini_set( 'session.use_cookies', 0 ); |
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 | 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 | |
157 | /** |
158 | * Set the manager, store, and logger |
159 | * @internal Use self::install(). |
160 | * @param SessionManagerInterface $manager |
161 | * @param BagOStuff $store |
162 | * @param LoggerInterface $logger |
163 | */ |
164 | public function setManager( |
165 | SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger |
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 | PhpSessionSerializer::setLogger( $this->logger ); |
176 | } |
177 | } |
178 | |
179 | /** |
180 | * Initialize the session (handler) |
181 | * @internal For internal use only |
182 | * @param string $save_path Path used to store session files (ignored) |
183 | * @param string $session_name Session name (ignored) |
184 | * @return true |
185 | */ |
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 | |
197 | /** |
198 | * Close the session (handler) |
199 | * @internal For internal use only |
200 | * @return true |
201 | */ |
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 | |
211 | /** |
212 | * Read session data |
213 | * @internal For internal use only |
214 | * @param string $id Session id |
215 | * @return string Session data |
216 | */ |
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)PhpSessionSerializer::encode( $data ); |
235 | } |
236 | |
237 | /** |
238 | * Write session data |
239 | * @internal For internal use only |
240 | * @param string $id Session id |
241 | * @param string $dataStr Session data. Not that you should ever call this |
242 | * directly, but note that this has the same issues with code injection |
243 | * via user-controlled data as does PHP's unserialize function. |
244 | * @return bool |
245 | */ |
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 = 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 | PhpSessionSerializer::setLogger( new NullLogger() ); |
313 | foreach ( $cache as $key => $value ) { |
314 | if ( !array_key_exists( $key, $data ) && $session->exists( $key ) && |
315 | 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 | 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 | |
347 | /** |
348 | * Destroy a session |
349 | * @internal For internal use only |
350 | * @param string $id Session id |
351 | * @return true |
352 | */ |
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 | |
368 | /** |
369 | * Execute garbage collection. |
370 | * @internal For internal use only |
371 | * @param int $maxlifetime Maximum session life time (ignored) |
372 | * @return true |
373 | * @codeCoverageIgnore See T135576 |
374 | */ |
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 | } |