|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* MediaWiki\Session entry point |
|
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 MWException; |
|
27
|
|
|
use Psr\Log\LoggerInterface; |
|
28
|
|
|
use BagOStuff; |
|
29
|
|
|
use CachedBagOStuff; |
|
30
|
|
|
use Config; |
|
31
|
|
|
use FauxRequest; |
|
32
|
|
|
use User; |
|
33
|
|
|
use WebRequest; |
|
34
|
|
|
|
|
35
|
|
|
/** |
|
36
|
|
|
* This serves as the entry point to the MediaWiki session handling system. |
|
37
|
|
|
* |
|
38
|
|
|
* @ingroup Session |
|
39
|
|
|
* @since 1.27 |
|
40
|
|
|
*/ |
|
41
|
|
|
final class SessionManager implements SessionManagerInterface { |
|
42
|
|
|
/** @var SessionManager|null */ |
|
43
|
|
|
private static $instance = null; |
|
44
|
|
|
|
|
45
|
|
|
/** @var Session|null */ |
|
46
|
|
|
private static $globalSession = null; |
|
47
|
|
|
|
|
48
|
|
|
/** @var WebRequest|null */ |
|
49
|
|
|
private static $globalSessionRequest = null; |
|
50
|
|
|
|
|
51
|
|
|
/** @var LoggerInterface */ |
|
52
|
|
|
private $logger; |
|
53
|
|
|
|
|
54
|
|
|
/** @var Config */ |
|
55
|
|
|
private $config; |
|
56
|
|
|
|
|
57
|
|
|
/** @var CachedBagOStuff|null */ |
|
58
|
|
|
private $store; |
|
59
|
|
|
|
|
60
|
|
|
/** @var SessionProvider[] */ |
|
61
|
|
|
private $sessionProviders = null; |
|
62
|
|
|
|
|
63
|
|
|
/** @var string[] */ |
|
64
|
|
|
private $varyCookies = null; |
|
65
|
|
|
|
|
66
|
|
|
/** @var array */ |
|
67
|
|
|
private $varyHeaders = null; |
|
68
|
|
|
|
|
69
|
|
|
/** @var SessionBackend[] */ |
|
70
|
|
|
private $allSessionBackends = []; |
|
71
|
|
|
|
|
72
|
|
|
/** @var SessionId[] */ |
|
73
|
|
|
private $allSessionIds = []; |
|
74
|
|
|
|
|
75
|
|
|
/** @var string[] */ |
|
76
|
|
|
private $preventUsers = []; |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* Get the global SessionManager |
|
80
|
|
|
* @return SessionManagerInterface |
|
81
|
|
|
* (really a SessionManager, but this is to make IDEs less confused) |
|
82
|
|
|
*/ |
|
83
|
|
|
public static function singleton() { |
|
84
|
|
|
if ( self::$instance === null ) { |
|
85
|
|
|
self::$instance = new self(); |
|
86
|
|
|
} |
|
87
|
|
|
return self::$instance; |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* Get the "global" session |
|
92
|
|
|
* |
|
93
|
|
|
* If PHP's session_id() has been set, returns that session. Otherwise |
|
94
|
|
|
* returns the session for RequestContext::getMain()->getRequest(). |
|
95
|
|
|
* |
|
96
|
|
|
* @return Session |
|
97
|
|
|
*/ |
|
98
|
|
|
public static function getGlobalSession() { |
|
99
|
|
|
if ( !PHPSessionHandler::isEnabled() ) { |
|
100
|
|
|
$id = ''; |
|
101
|
|
|
} else { |
|
102
|
|
|
$id = session_id(); |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
$request = \RequestContext::getMain()->getRequest(); |
|
106
|
|
|
if ( |
|
107
|
|
|
!self::$globalSession // No global session is set up yet |
|
108
|
|
|
|| self::$globalSessionRequest !== $request // The global WebRequest changed |
|
109
|
|
|
|| $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id() |
|
110
|
|
|
) { |
|
111
|
|
|
self::$globalSessionRequest = $request; |
|
112
|
|
|
if ( $id === '' ) { |
|
113
|
|
|
// session_id() wasn't used, so fetch the Session from the WebRequest. |
|
114
|
|
|
// We use $request->getSession() instead of $singleton->getSessionForRequest() |
|
115
|
|
|
// because doing the latter would require a public |
|
116
|
|
|
// "$request->getSessionId()" method that would confuse end |
|
117
|
|
|
// users by returning SessionId|null where they'd expect it to |
|
118
|
|
|
// be short for $request->getSession()->getId(), and would |
|
119
|
|
|
// wind up being a duplicate of the code in |
|
120
|
|
|
// $request->getSession() anyway. |
|
121
|
|
|
self::$globalSession = $request->getSession(); |
|
122
|
|
|
} else { |
|
123
|
|
|
// Someone used session_id(), so we need to follow suit. |
|
124
|
|
|
// Note this overwrites whatever session might already be |
|
125
|
|
|
// associated with $request with the one for $id. |
|
126
|
|
|
self::$globalSession = self::singleton()->getSessionById( $id, true, $request ) |
|
127
|
|
|
?: $request->getSession(); |
|
128
|
|
|
} |
|
129
|
|
|
} |
|
130
|
|
|
return self::$globalSession; |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* @param array $options |
|
135
|
|
|
* - config: Config to fetch configuration from. Defaults to the default 'main' config. |
|
136
|
|
|
* - logger: LoggerInterface to use for logging. Defaults to the 'session' channel. |
|
137
|
|
|
* - store: BagOStuff to store session data in. |
|
138
|
|
|
*/ |
|
139
|
|
|
public function __construct( $options = [] ) { |
|
140
|
|
|
if ( isset( $options['config'] ) ) { |
|
141
|
|
|
$this->config = $options['config']; |
|
142
|
|
|
if ( !$this->config instanceof Config ) { |
|
143
|
|
|
throw new \InvalidArgumentException( |
|
144
|
|
|
'$options[\'config\'] must be an instance of Config' |
|
145
|
|
|
); |
|
146
|
|
|
} |
|
147
|
|
|
} else { |
|
148
|
|
|
$this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); |
|
|
|
|
|
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
if ( isset( $options['logger'] ) ) { |
|
152
|
|
|
if ( !$options['logger'] instanceof LoggerInterface ) { |
|
153
|
|
|
throw new \InvalidArgumentException( |
|
154
|
|
|
'$options[\'logger\'] must be an instance of LoggerInterface' |
|
155
|
|
|
); |
|
156
|
|
|
} |
|
157
|
|
|
$this->setLogger( $options['logger'] ); |
|
158
|
|
|
} else { |
|
159
|
|
|
$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) ); |
|
160
|
|
|
} |
|
161
|
|
|
|
|
162
|
|
|
if ( isset( $options['store'] ) ) { |
|
163
|
|
|
if ( !$options['store'] instanceof BagOStuff ) { |
|
164
|
|
|
throw new \InvalidArgumentException( |
|
165
|
|
|
'$options[\'store\'] must be an instance of BagOStuff' |
|
166
|
|
|
); |
|
167
|
|
|
} |
|
168
|
|
|
$store = $options['store']; |
|
169
|
|
|
} else { |
|
170
|
|
|
$store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) ); |
|
171
|
|
|
} |
|
172
|
|
|
$this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store ); |
|
173
|
|
|
|
|
174
|
|
|
register_shutdown_function( [ $this, 'shutdown' ] ); |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
public function setLogger( LoggerInterface $logger ) { |
|
178
|
|
|
$this->logger = $logger; |
|
179
|
|
|
} |
|
180
|
|
|
|
|
181
|
|
|
public function getSessionForRequest( WebRequest $request ) { |
|
182
|
|
|
$info = $this->getSessionInfoForRequest( $request ); |
|
183
|
|
|
|
|
184
|
|
|
if ( !$info ) { |
|
185
|
|
|
$session = $this->getEmptySession( $request ); |
|
186
|
|
|
} else { |
|
187
|
|
|
$session = $this->getSessionFromInfo( $info, $request ); |
|
188
|
|
|
} |
|
189
|
|
|
return $session; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
public function getSessionById( $id, $create = false, WebRequest $request = null ) { |
|
193
|
|
|
if ( !self::validateSessionId( $id ) ) { |
|
194
|
|
|
throw new \InvalidArgumentException( 'Invalid session ID' ); |
|
195
|
|
|
} |
|
196
|
|
|
if ( !$request ) { |
|
197
|
|
|
$request = new FauxRequest; |
|
198
|
|
|
} |
|
199
|
|
|
|
|
200
|
|
|
$session = null; |
|
201
|
|
|
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] ); |
|
202
|
|
|
|
|
203
|
|
|
// If we already have the backend loaded, use it directly |
|
204
|
|
|
if ( isset( $this->allSessionBackends[$id] ) ) { |
|
205
|
|
|
return $this->getSessionFromInfo( $info, $request ); |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
// Test if the session is in storage, and if so try to load it. |
|
209
|
|
|
$key = wfMemcKey( 'MWSession', $id ); |
|
210
|
|
|
if ( is_array( $this->store->get( $key ) ) ) { |
|
211
|
|
|
$create = false; // If loading fails, don't bother creating because it probably will fail too. |
|
212
|
|
|
if ( $this->loadSessionInfoFromStore( $info, $request ) ) { |
|
213
|
|
|
$session = $this->getSessionFromInfo( $info, $request ); |
|
214
|
|
|
} |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
if ( $create && $session === null ) { |
|
218
|
|
|
$ex = null; |
|
219
|
|
|
try { |
|
220
|
|
|
$session = $this->getEmptySessionInternal( $request, $id ); |
|
221
|
|
|
} catch ( \Exception $ex ) { |
|
222
|
|
|
$this->logger->error( 'Failed to create empty session: {exception}', |
|
223
|
|
|
[ |
|
224
|
|
|
'method' => __METHOD__, |
|
225
|
|
|
'exception' => $ex, |
|
226
|
|
|
] ); |
|
227
|
|
|
$session = null; |
|
228
|
|
|
} |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
return $session; |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
public function getEmptySession( WebRequest $request = null ) { |
|
235
|
|
|
return $this->getEmptySessionInternal( $request ); |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
/** |
|
239
|
|
|
* @see SessionManagerInterface::getEmptySession |
|
240
|
|
|
* @param WebRequest|null $request |
|
241
|
|
|
* @param string|null $id ID to force on the new session |
|
242
|
|
|
* @return Session |
|
243
|
|
|
*/ |
|
244
|
|
|
private function getEmptySessionInternal( WebRequest $request = null, $id = null ) { |
|
245
|
|
|
if ( $id !== null ) { |
|
246
|
|
|
if ( !self::validateSessionId( $id ) ) { |
|
247
|
|
|
throw new \InvalidArgumentException( 'Invalid session ID' ); |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
$key = wfMemcKey( 'MWSession', $id ); |
|
251
|
|
|
if ( is_array( $this->store->get( $key ) ) ) { |
|
252
|
|
|
throw new \InvalidArgumentException( 'Session ID already exists' ); |
|
253
|
|
|
} |
|
254
|
|
|
} |
|
255
|
|
|
if ( !$request ) { |
|
256
|
|
|
$request = new FauxRequest; |
|
257
|
|
|
} |
|
258
|
|
|
|
|
259
|
|
|
$infos = []; |
|
260
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
261
|
|
|
$info = $provider->newSessionInfo( $id ); |
|
|
|
|
|
|
262
|
|
|
if ( !$info ) { |
|
263
|
|
|
continue; |
|
264
|
|
|
} |
|
265
|
|
|
if ( $info->getProvider() !== $provider ) { |
|
266
|
|
|
throw new \UnexpectedValueException( |
|
267
|
|
|
"$provider returned an empty session info for a different provider: $info" |
|
268
|
|
|
); |
|
269
|
|
|
} |
|
270
|
|
|
if ( $id !== null && $info->getId() !== $id ) { |
|
271
|
|
|
throw new \UnexpectedValueException( |
|
272
|
|
|
"$provider returned empty session info with a wrong id: " . |
|
273
|
|
|
$info->getId() . ' != ' . $id |
|
274
|
|
|
); |
|
275
|
|
|
} |
|
276
|
|
|
if ( !$info->isIdSafe() ) { |
|
277
|
|
|
throw new \UnexpectedValueException( |
|
278
|
|
|
"$provider returned empty session info with id flagged unsafe" |
|
279
|
|
|
); |
|
280
|
|
|
} |
|
281
|
|
|
$compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1; |
|
282
|
|
|
if ( $compare > 0 ) { |
|
283
|
|
|
continue; |
|
284
|
|
|
} |
|
285
|
|
|
if ( $compare === 0 ) { |
|
286
|
|
|
$infos[] = $info; |
|
287
|
|
|
} else { |
|
288
|
|
|
$infos = [ $info ]; |
|
289
|
|
|
} |
|
290
|
|
|
} |
|
291
|
|
|
|
|
292
|
|
|
// Make sure there's exactly one |
|
293
|
|
|
if ( count( $infos ) > 1 ) { |
|
294
|
|
|
throw new \UnexpectedValueException( |
|
295
|
|
|
'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos ) |
|
296
|
|
|
); |
|
297
|
|
|
} elseif ( count( $infos ) < 1 ) { |
|
298
|
|
|
throw new \UnexpectedValueException( 'No provider could provide an empty session!' ); |
|
299
|
|
|
} |
|
300
|
|
|
|
|
301
|
|
|
return $this->getSessionFromInfo( $infos[0], $request ); |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
public function invalidateSessionsForUser( User $user ) { |
|
305
|
|
|
$user->setToken(); |
|
306
|
|
|
$user->saveSettings(); |
|
307
|
|
|
|
|
308
|
|
|
$authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] ); |
|
|
|
|
|
|
309
|
|
|
if ( $authUser ) { |
|
310
|
|
|
$authUser->resetAuthToken(); |
|
311
|
|
|
} |
|
312
|
|
|
|
|
313
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
314
|
|
|
$provider->invalidateSessionsForUser( $user ); |
|
315
|
|
|
} |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
public function getVaryHeaders() { |
|
319
|
|
|
// @codeCoverageIgnoreStart |
|
320
|
|
|
if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) { |
|
|
|
|
|
|
321
|
|
|
return []; |
|
322
|
|
|
} |
|
323
|
|
|
// @codeCoverageIgnoreEnd |
|
324
|
|
|
if ( $this->varyHeaders === null ) { |
|
325
|
|
|
$headers = []; |
|
326
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
327
|
|
|
foreach ( $provider->getVaryHeaders() as $header => $options ) { |
|
328
|
|
|
if ( !isset( $headers[$header] ) ) { |
|
329
|
|
|
$headers[$header] = []; |
|
330
|
|
|
} |
|
331
|
|
|
if ( is_array( $options ) ) { |
|
332
|
|
|
$headers[$header] = array_unique( array_merge( $headers[$header], $options ) ); |
|
333
|
|
|
} |
|
334
|
|
|
} |
|
335
|
|
|
} |
|
336
|
|
|
$this->varyHeaders = $headers; |
|
337
|
|
|
} |
|
338
|
|
|
return $this->varyHeaders; |
|
339
|
|
|
} |
|
340
|
|
|
|
|
341
|
|
|
public function getVaryCookies() { |
|
342
|
|
|
// @codeCoverageIgnoreStart |
|
343
|
|
|
if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) { |
|
|
|
|
|
|
344
|
|
|
return []; |
|
345
|
|
|
} |
|
346
|
|
|
// @codeCoverageIgnoreEnd |
|
347
|
|
|
if ( $this->varyCookies === null ) { |
|
348
|
|
|
$cookies = []; |
|
349
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
350
|
|
|
$cookies = array_merge( $cookies, $provider->getVaryCookies() ); |
|
351
|
|
|
} |
|
352
|
|
|
$this->varyCookies = array_values( array_unique( $cookies ) ); |
|
|
|
|
|
|
353
|
|
|
} |
|
354
|
|
|
return $this->varyCookies; |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
/** |
|
358
|
|
|
* Validate a session ID |
|
359
|
|
|
* @param string $id |
|
360
|
|
|
* @return bool |
|
361
|
|
|
*/ |
|
362
|
|
|
public static function validateSessionId( $id ) { |
|
363
|
|
|
return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id ); |
|
364
|
|
|
} |
|
365
|
|
|
|
|
366
|
|
|
/** |
|
367
|
|
|
* @name Internal methods |
|
368
|
|
|
* @{ |
|
369
|
|
|
*/ |
|
370
|
|
|
|
|
371
|
|
|
/** |
|
372
|
|
|
* Auto-create the given user, if necessary |
|
373
|
|
|
* @private Don't call this yourself. Let Setup.php do it for you at the right time. |
|
374
|
|
|
* @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead |
|
375
|
|
|
* @param User $user User to auto-create |
|
376
|
|
|
* @return bool Success |
|
377
|
|
|
*/ |
|
378
|
|
|
public static function autoCreateUser( User $user ) { |
|
|
|
|
|
|
379
|
|
|
global $wgAuth, $wgDisableAuthManager; |
|
380
|
|
|
|
|
381
|
|
|
// @codeCoverageIgnoreStart |
|
382
|
|
|
if ( !$wgDisableAuthManager ) { |
|
383
|
|
|
wfDeprecated( __METHOD__, '1.27' ); |
|
384
|
|
|
return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser( |
|
385
|
|
|
$user, |
|
386
|
|
|
\MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION, |
|
387
|
|
|
false |
|
388
|
|
|
)->isGood(); |
|
389
|
|
|
} |
|
390
|
|
|
// @codeCoverageIgnoreEnd |
|
391
|
|
|
|
|
392
|
|
|
$logger = self::singleton()->logger; |
|
393
|
|
|
|
|
394
|
|
|
// Much of this code is based on that in CentralAuth |
|
395
|
|
|
|
|
396
|
|
|
// Try the local user from the slave DB |
|
397
|
|
|
$localId = User::idFromName( $user->getName() ); |
|
398
|
|
|
$flags = 0; |
|
399
|
|
|
|
|
400
|
|
|
// Fetch the user ID from the master, so that we don't try to create the user |
|
401
|
|
|
// when they already exist, due to replication lag |
|
402
|
|
|
// @codeCoverageIgnoreStart |
|
403
|
|
View Code Duplication |
if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) { |
|
|
|
|
|
|
404
|
|
|
$localId = User::idFromName( $user->getName(), User::READ_LATEST ); |
|
405
|
|
|
$flags = User::READ_LATEST; |
|
406
|
|
|
} |
|
407
|
|
|
// @codeCoverageIgnoreEnd |
|
408
|
|
|
|
|
409
|
|
|
if ( $localId ) { |
|
|
|
|
|
|
410
|
|
|
// User exists after all. |
|
411
|
|
|
$user->setId( $localId ); |
|
412
|
|
|
$user->loadFromId( $flags ); |
|
413
|
|
|
return false; |
|
414
|
|
|
} |
|
415
|
|
|
|
|
416
|
|
|
// Denied by AuthPlugin? But ignore AuthPlugin itself. |
|
417
|
|
|
if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) { |
|
418
|
|
|
$logger->debug( __METHOD__ . ': denied by AuthPlugin' ); |
|
419
|
|
|
$user->setId( 0 ); |
|
420
|
|
|
$user->loadFromId(); |
|
421
|
|
|
return false; |
|
422
|
|
|
} |
|
423
|
|
|
|
|
424
|
|
|
// Wiki is read-only? |
|
425
|
|
View Code Duplication |
if ( wfReadOnly() ) { |
|
426
|
|
|
$logger->debug( __METHOD__ . ': denied by wfReadOnly()' ); |
|
427
|
|
|
$user->setId( 0 ); |
|
428
|
|
|
$user->loadFromId(); |
|
429
|
|
|
return false; |
|
430
|
|
|
} |
|
431
|
|
|
|
|
432
|
|
|
$userName = $user->getName(); |
|
433
|
|
|
|
|
434
|
|
|
// Check the session, if we tried to create this user already there's |
|
435
|
|
|
// no point in retrying. |
|
436
|
|
|
$session = self::getGlobalSession(); |
|
437
|
|
|
$reason = $session->get( 'MWSession::AutoCreateBlacklist' ); |
|
438
|
|
View Code Duplication |
if ( $reason ) { |
|
439
|
|
|
$logger->debug( __METHOD__ . ": blacklisted in session ($reason)" ); |
|
440
|
|
|
$user->setId( 0 ); |
|
441
|
|
|
$user->loadFromId(); |
|
442
|
|
|
return false; |
|
443
|
|
|
} |
|
444
|
|
|
|
|
445
|
|
|
// Is the IP user able to create accounts? |
|
446
|
|
|
$anon = new User; |
|
447
|
|
|
if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) |
|
448
|
|
|
|| $anon->isBlockedFromCreateAccount() |
|
449
|
|
|
) { |
|
450
|
|
|
// Blacklist the user to avoid repeated DB queries subsequently |
|
451
|
|
|
$logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' ); |
|
452
|
|
|
$session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 ); |
|
453
|
|
|
$session->persist(); |
|
454
|
|
|
$user->setId( 0 ); |
|
455
|
|
|
$user->loadFromId(); |
|
456
|
|
|
return false; |
|
457
|
|
|
} |
|
458
|
|
|
|
|
459
|
|
|
// Check for validity of username |
|
460
|
|
|
if ( !User::isCreatableName( $userName ) ) { |
|
461
|
|
|
$logger->debug( __METHOD__ . ': Invalid username, blacklisting' ); |
|
462
|
|
|
$session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 ); |
|
463
|
|
|
$session->persist(); |
|
464
|
|
|
$user->setId( 0 ); |
|
465
|
|
|
$user->loadFromId(); |
|
466
|
|
|
return false; |
|
467
|
|
|
} |
|
468
|
|
|
|
|
469
|
|
|
// Give other extensions a chance to stop auto creation. |
|
470
|
|
|
$user->loadDefaults( $userName ); |
|
471
|
|
|
$abortMessage = ''; |
|
472
|
|
|
if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) { |
|
473
|
|
|
// In this case we have no way to return the message to the user, |
|
474
|
|
|
// but we can log it. |
|
475
|
|
|
$logger->debug( __METHOD__ . ": denied by hook: $abortMessage" ); |
|
476
|
|
|
$session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 ); |
|
477
|
|
|
$session->persist(); |
|
478
|
|
|
$user->setId( 0 ); |
|
479
|
|
|
$user->loadFromId(); |
|
480
|
|
|
return false; |
|
481
|
|
|
} |
|
482
|
|
|
|
|
483
|
|
|
// Make sure the name has not been changed |
|
484
|
|
|
if ( $user->getName() !== $userName ) { |
|
485
|
|
|
$user->setId( 0 ); |
|
486
|
|
|
$user->loadFromId(); |
|
487
|
|
|
throw new \UnexpectedValueException( |
|
488
|
|
|
'AbortAutoAccount hook tried to change the user name' |
|
489
|
|
|
); |
|
490
|
|
|
} |
|
491
|
|
|
|
|
492
|
|
|
// Ignore warnings about master connections/writes...hard to avoid here |
|
493
|
|
|
\Profiler::instance()->getTransactionProfiler()->resetExpectations(); |
|
494
|
|
|
|
|
495
|
|
|
$cache = \ObjectCache::getLocalClusterInstance(); |
|
496
|
|
|
$backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) ); |
|
497
|
|
|
if ( $cache->get( $backoffKey ) ) { |
|
498
|
|
|
$logger->debug( __METHOD__ . ': denied by prior creation attempt failures' ); |
|
499
|
|
|
$user->setId( 0 ); |
|
500
|
|
|
$user->loadFromId(); |
|
501
|
|
|
return false; |
|
502
|
|
|
} |
|
503
|
|
|
|
|
504
|
|
|
// Checks passed, create the user... |
|
505
|
|
|
$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI'; |
|
506
|
|
|
$logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}', |
|
507
|
|
|
[ |
|
508
|
|
|
'username' => $userName, |
|
509
|
|
|
'url' => $from, |
|
510
|
|
|
] ); |
|
511
|
|
|
|
|
512
|
|
|
try { |
|
513
|
|
|
// Insert the user into the local DB master |
|
514
|
|
|
$status = $user->addToDatabase(); |
|
515
|
|
|
if ( !$status->isOK() ) { |
|
516
|
|
|
// @codeCoverageIgnoreStart |
|
517
|
|
|
// double-check for a race condition (T70012) |
|
518
|
|
|
$id = User::idFromName( $user->getName(), User::READ_LATEST ); |
|
519
|
|
|
if ( $id ) { |
|
|
|
|
|
|
520
|
|
|
$logger->info( __METHOD__ . ': tried to autocreate existing user', |
|
521
|
|
|
[ |
|
522
|
|
|
'username' => $userName, |
|
523
|
|
|
] ); |
|
524
|
|
|
} else { |
|
525
|
|
|
$logger->error( |
|
526
|
|
|
__METHOD__ . ': failed with message ' . $status->getWikiText( false, false, 'en' ), |
|
527
|
|
|
[ |
|
528
|
|
|
'username' => $userName, |
|
529
|
|
|
] |
|
530
|
|
|
); |
|
531
|
|
|
} |
|
532
|
|
|
$user->setId( $id ); |
|
533
|
|
|
$user->loadFromId( User::READ_LATEST ); |
|
534
|
|
|
return false; |
|
535
|
|
|
// @codeCoverageIgnoreEnd |
|
536
|
|
|
} |
|
537
|
|
|
} catch ( \Exception $ex ) { |
|
538
|
|
|
// @codeCoverageIgnoreStart |
|
539
|
|
|
$logger->error( __METHOD__ . ': failed with exception {exception}', [ |
|
540
|
|
|
'exception' => $ex, |
|
541
|
|
|
'username' => $userName, |
|
542
|
|
|
] ); |
|
543
|
|
|
// Do not keep throwing errors for a while |
|
544
|
|
|
$cache->set( $backoffKey, 1, 600 ); |
|
545
|
|
|
// Bubble up error; which should normally trigger DB rollbacks |
|
546
|
|
|
throw $ex; |
|
547
|
|
|
// @codeCoverageIgnoreEnd |
|
548
|
|
|
} |
|
549
|
|
|
|
|
550
|
|
|
# Notify AuthPlugin |
|
551
|
|
|
// @codeCoverageIgnoreStart |
|
552
|
|
|
$tmpUser = $user; |
|
553
|
|
|
$wgAuth->initUser( $tmpUser, true ); |
|
554
|
|
|
if ( $tmpUser !== $user ) { |
|
555
|
|
|
$logger->warning( __METHOD__ . ': ' . |
|
556
|
|
|
get_class( $wgAuth ) . '::initUser() replaced the user object' ); |
|
557
|
|
|
} |
|
558
|
|
|
// @codeCoverageIgnoreEnd |
|
559
|
|
|
|
|
560
|
|
|
# Notify hooks (e.g. Newuserlog) |
|
561
|
|
|
\Hooks::run( 'AuthPluginAutoCreate', [ $user ] ); |
|
562
|
|
|
\Hooks::run( 'LocalUserCreated', [ $user, true ] ); |
|
563
|
|
|
|
|
564
|
|
|
$user->saveSettings(); |
|
565
|
|
|
|
|
566
|
|
|
# Update user count |
|
567
|
|
|
\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) ); |
|
568
|
|
|
|
|
569
|
|
|
# Watch user's userpage and talk page |
|
570
|
|
|
$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS ); |
|
571
|
|
|
|
|
572
|
|
|
return true; |
|
573
|
|
|
} |
|
574
|
|
|
|
|
575
|
|
|
/** |
|
576
|
|
|
* Prevent future sessions for the user |
|
577
|
|
|
* |
|
578
|
|
|
* The intention is that the named account will never again be usable for |
|
579
|
|
|
* normal login (i.e. there is no way to undo the prevention of access). |
|
580
|
|
|
* |
|
581
|
|
|
* @private For use from \User::newSystemUser only |
|
582
|
|
|
* @param string $username |
|
583
|
|
|
*/ |
|
584
|
|
|
public function preventSessionsForUser( $username ) { |
|
585
|
|
|
$this->preventUsers[$username] = true; |
|
586
|
|
|
|
|
587
|
|
|
// Instruct the session providers to kill any other sessions too. |
|
588
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
589
|
|
|
$provider->preventSessionsForUser( $username ); |
|
590
|
|
|
} |
|
591
|
|
|
} |
|
592
|
|
|
|
|
593
|
|
|
/** |
|
594
|
|
|
* Test if a user is prevented |
|
595
|
|
|
* @private For use from SessionBackend only |
|
596
|
|
|
* @param string $username |
|
597
|
|
|
* @return bool |
|
598
|
|
|
*/ |
|
599
|
|
|
public function isUserSessionPrevented( $username ) { |
|
600
|
|
|
return !empty( $this->preventUsers[$username] ); |
|
601
|
|
|
} |
|
602
|
|
|
|
|
603
|
|
|
/** |
|
604
|
|
|
* Get the available SessionProviders |
|
605
|
|
|
* @return SessionProvider[] |
|
606
|
|
|
*/ |
|
607
|
|
|
protected function getProviders() { |
|
608
|
|
|
if ( $this->sessionProviders === null ) { |
|
609
|
|
|
$this->sessionProviders = []; |
|
610
|
|
|
foreach ( $this->config->get( 'SessionProviders' ) as $spec ) { |
|
611
|
|
|
$provider = \ObjectFactory::getObjectFromSpec( $spec ); |
|
612
|
|
|
$provider->setLogger( $this->logger ); |
|
613
|
|
|
$provider->setConfig( $this->config ); |
|
614
|
|
|
$provider->setManager( $this ); |
|
615
|
|
|
if ( isset( $this->sessionProviders[(string)$provider] ) ) { |
|
616
|
|
|
throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" ); |
|
617
|
|
|
} |
|
618
|
|
|
$this->sessionProviders[(string)$provider] = $provider; |
|
619
|
|
|
} |
|
620
|
|
|
} |
|
621
|
|
|
return $this->sessionProviders; |
|
622
|
|
|
} |
|
623
|
|
|
|
|
624
|
|
|
/** |
|
625
|
|
|
* Get a session provider by name |
|
626
|
|
|
* |
|
627
|
|
|
* Generally, this will only be used by internal implementation of some |
|
628
|
|
|
* special session-providing mechanism. General purpose code, if it needs |
|
629
|
|
|
* to access a SessionProvider at all, will use Session::getProvider(). |
|
630
|
|
|
* |
|
631
|
|
|
* @param string $name |
|
632
|
|
|
* @return SessionProvider|null |
|
633
|
|
|
*/ |
|
634
|
|
|
public function getProvider( $name ) { |
|
635
|
|
|
$providers = $this->getProviders(); |
|
636
|
|
|
return isset( $providers[$name] ) ? $providers[$name] : null; |
|
637
|
|
|
} |
|
638
|
|
|
|
|
639
|
|
|
/** |
|
640
|
|
|
* Save all active sessions on shutdown |
|
641
|
|
|
* @private For internal use with register_shutdown_function() |
|
642
|
|
|
*/ |
|
643
|
|
|
public function shutdown() { |
|
644
|
|
|
if ( $this->allSessionBackends ) { |
|
645
|
|
|
$this->logger->debug( 'Saving all sessions on shutdown' ); |
|
646
|
|
|
if ( session_id() !== '' ) { |
|
647
|
|
|
// @codeCoverageIgnoreStart |
|
648
|
|
|
session_write_close(); |
|
649
|
|
|
} |
|
650
|
|
|
// @codeCoverageIgnoreEnd |
|
651
|
|
|
foreach ( $this->allSessionBackends as $backend ) { |
|
652
|
|
|
$backend->shutdown(); |
|
653
|
|
|
} |
|
654
|
|
|
} |
|
655
|
|
|
} |
|
656
|
|
|
|
|
657
|
|
|
/** |
|
658
|
|
|
* Fetch the SessionInfo(s) for a request |
|
659
|
|
|
* @param WebRequest $request |
|
660
|
|
|
* @return SessionInfo|null |
|
661
|
|
|
*/ |
|
662
|
|
|
private function getSessionInfoForRequest( WebRequest $request ) { |
|
663
|
|
|
// Call all providers to fetch "the" session |
|
664
|
|
|
$infos = []; |
|
665
|
|
|
foreach ( $this->getProviders() as $provider ) { |
|
666
|
|
|
$info = $provider->provideSessionInfo( $request ); |
|
|
|
|
|
|
667
|
|
|
if ( !$info ) { |
|
668
|
|
|
continue; |
|
669
|
|
|
} |
|
670
|
|
|
if ( $info->getProvider() !== $provider ) { |
|
671
|
|
|
throw new \UnexpectedValueException( |
|
672
|
|
|
"$provider returned session info for a different provider: $info" |
|
673
|
|
|
); |
|
674
|
|
|
} |
|
675
|
|
|
$infos[] = $info; |
|
676
|
|
|
} |
|
677
|
|
|
|
|
678
|
|
|
// Sort the SessionInfos. Then find the first one that can be |
|
679
|
|
|
// successfully loaded, and then all the ones after it with the same |
|
680
|
|
|
// priority. |
|
681
|
|
|
usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' ); |
|
682
|
|
|
$retInfos = []; |
|
683
|
|
|
while ( $infos ) { |
|
684
|
|
|
$info = array_pop( $infos ); |
|
685
|
|
|
if ( $this->loadSessionInfoFromStore( $info, $request ) ) { |
|
686
|
|
|
$retInfos[] = $info; |
|
687
|
|
|
while ( $infos ) { |
|
688
|
|
|
$info = array_pop( $infos ); |
|
689
|
|
|
if ( SessionInfo::compare( $retInfos[0], $info ) ) { |
|
690
|
|
|
// We hit a lower priority, stop checking. |
|
691
|
|
|
break; |
|
692
|
|
|
} |
|
693
|
|
|
if ( $this->loadSessionInfoFromStore( $info, $request ) ) { |
|
694
|
|
|
// This is going to error out below, but we want to |
|
695
|
|
|
// provide a complete list. |
|
696
|
|
|
$retInfos[] = $info; |
|
697
|
|
|
} else { |
|
698
|
|
|
// Session load failed, so unpersist it from this request |
|
699
|
|
|
$info->getProvider()->unpersistSession( $request ); |
|
700
|
|
|
} |
|
701
|
|
|
} |
|
702
|
|
|
} else { |
|
703
|
|
|
// Session load failed, so unpersist it from this request |
|
704
|
|
|
$info->getProvider()->unpersistSession( $request ); |
|
705
|
|
|
} |
|
706
|
|
|
} |
|
707
|
|
|
|
|
708
|
|
|
if ( count( $retInfos ) > 1 ) { |
|
709
|
|
|
$ex = new \OverflowException( |
|
710
|
|
|
'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos ) |
|
711
|
|
|
); |
|
712
|
|
|
$ex->sessionInfos = $retInfos; |
|
|
|
|
|
|
713
|
|
|
throw $ex; |
|
714
|
|
|
} |
|
715
|
|
|
|
|
716
|
|
|
return $retInfos ? $retInfos[0] : null; |
|
717
|
|
|
} |
|
718
|
|
|
|
|
719
|
|
|
/** |
|
720
|
|
|
* Load and verify the session info against the store |
|
721
|
|
|
* |
|
722
|
|
|
* @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance |
|
723
|
|
|
* @param WebRequest $request |
|
724
|
|
|
* @return bool Whether the session info matches the stored data (if any) |
|
725
|
|
|
*/ |
|
726
|
|
|
private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) { |
|
727
|
|
|
$key = wfMemcKey( 'MWSession', $info->getId() ); |
|
728
|
|
|
$blob = $this->store->get( $key ); |
|
729
|
|
|
|
|
730
|
|
|
// If we got data from the store and the SessionInfo says to force use, |
|
731
|
|
|
// "fail" means to delete the data from the store and retry. Otherwise, |
|
732
|
|
|
// "fail" is just return false. |
|
733
|
|
|
if ( $info->forceUse() && $blob !== false ) { |
|
734
|
|
|
$failHandler = function () use ( $key, &$info, $request ) { |
|
735
|
|
|
$this->store->delete( $key ); |
|
736
|
|
|
return $this->loadSessionInfoFromStore( $info, $request ); |
|
737
|
|
|
}; |
|
738
|
|
|
} else { |
|
739
|
|
|
$failHandler = function () { |
|
740
|
|
|
return false; |
|
741
|
|
|
}; |
|
742
|
|
|
} |
|
743
|
|
|
|
|
744
|
|
|
$newParams = []; |
|
745
|
|
|
|
|
746
|
|
|
if ( $blob !== false ) { |
|
747
|
|
|
// Sanity check: blob must be an array, if it's saved at all |
|
748
|
|
|
if ( !is_array( $blob ) ) { |
|
749
|
|
|
$this->logger->warning( 'Session "{session}": Bad data', [ |
|
750
|
|
|
'session' => $info, |
|
751
|
|
|
] ); |
|
752
|
|
|
$this->store->delete( $key ); |
|
753
|
|
|
return $failHandler(); |
|
754
|
|
|
} |
|
755
|
|
|
|
|
756
|
|
|
// Sanity check: blob has data and metadata arrays |
|
757
|
|
|
if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) || |
|
758
|
|
|
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) |
|
759
|
|
|
) { |
|
760
|
|
|
$this->logger->warning( 'Session "{session}": Bad data structure', [ |
|
761
|
|
|
'session' => $info, |
|
762
|
|
|
] ); |
|
763
|
|
|
$this->store->delete( $key ); |
|
764
|
|
|
return $failHandler(); |
|
765
|
|
|
} |
|
766
|
|
|
|
|
767
|
|
|
$data = $blob['data']; |
|
768
|
|
|
$metadata = $blob['metadata']; |
|
769
|
|
|
|
|
770
|
|
|
// Sanity check: metadata must be an array and must contain certain |
|
771
|
|
|
// keys, if it's saved at all |
|
772
|
|
|
if ( !array_key_exists( 'userId', $metadata ) || |
|
773
|
|
|
!array_key_exists( 'userName', $metadata ) || |
|
774
|
|
|
!array_key_exists( 'userToken', $metadata ) || |
|
775
|
|
|
!array_key_exists( 'provider', $metadata ) |
|
776
|
|
|
) { |
|
777
|
|
|
$this->logger->warning( 'Session "{session}": Bad metadata', [ |
|
778
|
|
|
'session' => $info, |
|
779
|
|
|
] ); |
|
780
|
|
|
$this->store->delete( $key ); |
|
781
|
|
|
return $failHandler(); |
|
782
|
|
|
} |
|
783
|
|
|
|
|
784
|
|
|
// First, load the provider from metadata, or validate it against the metadata. |
|
785
|
|
|
$provider = $info->getProvider(); |
|
786
|
|
|
if ( $provider === null ) { |
|
787
|
|
|
$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] ); |
|
788
|
|
|
if ( !$provider ) { |
|
789
|
|
|
$this->logger->warning( |
|
790
|
|
|
'Session "{session}": Unknown provider ' . $metadata['provider'], |
|
791
|
|
|
[ |
|
792
|
|
|
'session' => $info, |
|
793
|
|
|
] |
|
794
|
|
|
); |
|
795
|
|
|
$this->store->delete( $key ); |
|
796
|
|
|
return $failHandler(); |
|
797
|
|
|
} |
|
798
|
|
|
} elseif ( $metadata['provider'] !== (string)$provider ) { |
|
799
|
|
|
$this->logger->warning( 'Session "{session}": Wrong provider ' . |
|
800
|
|
|
$metadata['provider'] . ' !== ' . $provider, |
|
801
|
|
|
[ |
|
802
|
|
|
'session' => $info, |
|
803
|
|
|
] ); |
|
804
|
|
|
return $failHandler(); |
|
805
|
|
|
} |
|
806
|
|
|
|
|
807
|
|
|
// Load provider metadata from metadata, or validate it against the metadata |
|
808
|
|
|
$providerMetadata = $info->getProviderMetadata(); |
|
809
|
|
|
if ( isset( $metadata['providerMetadata'] ) ) { |
|
810
|
|
|
if ( $providerMetadata === null ) { |
|
811
|
|
|
$newParams['metadata'] = $metadata['providerMetadata']; |
|
812
|
|
|
} else { |
|
813
|
|
|
try { |
|
814
|
|
|
$newProviderMetadata = $provider->mergeMetadata( |
|
815
|
|
|
$metadata['providerMetadata'], $providerMetadata |
|
816
|
|
|
); |
|
817
|
|
|
if ( $newProviderMetadata !== $providerMetadata ) { |
|
818
|
|
|
$newParams['metadata'] = $newProviderMetadata; |
|
819
|
|
|
} |
|
820
|
|
|
} catch ( MetadataMergeException $ex ) { |
|
821
|
|
|
$this->logger->warning( |
|
822
|
|
|
'Session "{session}": Metadata merge failed: {exception}', |
|
823
|
|
|
[ |
|
824
|
|
|
'session' => $info, |
|
825
|
|
|
'exception' => $ex, |
|
826
|
|
|
] + $ex->getContext() |
|
827
|
|
|
); |
|
828
|
|
|
return $failHandler(); |
|
829
|
|
|
} |
|
830
|
|
|
} |
|
831
|
|
|
} |
|
832
|
|
|
|
|
833
|
|
|
// Next, load the user from metadata, or validate it against the metadata. |
|
834
|
|
|
$userInfo = $info->getUserInfo(); |
|
835
|
|
|
if ( !$userInfo ) { |
|
836
|
|
|
// For loading, id is preferred to name. |
|
837
|
|
|
try { |
|
838
|
|
|
if ( $metadata['userId'] ) { |
|
839
|
|
|
$userInfo = UserInfo::newFromId( $metadata['userId'] ); |
|
840
|
|
|
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case |
|
841
|
|
|
$userInfo = UserInfo::newFromName( $metadata['userName'] ); |
|
842
|
|
|
} else { |
|
843
|
|
|
$userInfo = UserInfo::newAnonymous(); |
|
844
|
|
|
} |
|
845
|
|
|
} catch ( \InvalidArgumentException $ex ) { |
|
846
|
|
|
$this->logger->error( 'Session "{session}": {exception}', [ |
|
847
|
|
|
'session' => $info, |
|
848
|
|
|
'exception' => $ex, |
|
849
|
|
|
] ); |
|
850
|
|
|
return $failHandler(); |
|
851
|
|
|
} |
|
852
|
|
|
$newParams['userInfo'] = $userInfo; |
|
853
|
|
|
} else { |
|
854
|
|
|
// User validation passes if user ID matches, or if there |
|
855
|
|
|
// is no saved ID and the names match. |
|
856
|
|
|
if ( $metadata['userId'] ) { |
|
857
|
|
|
if ( $metadata['userId'] !== $userInfo->getId() ) { |
|
858
|
|
|
$this->logger->warning( |
|
859
|
|
|
'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}', |
|
860
|
|
|
[ |
|
861
|
|
|
'session' => $info, |
|
862
|
|
|
'uid_a' => $metadata['userId'], |
|
863
|
|
|
'uid_b' => $userInfo->getId(), |
|
864
|
|
|
] ); |
|
865
|
|
|
return $failHandler(); |
|
866
|
|
|
} |
|
867
|
|
|
|
|
868
|
|
|
// If the user was renamed, probably best to fail here. |
|
869
|
|
View Code Duplication |
if ( $metadata['userName'] !== null && |
|
870
|
|
|
$userInfo->getName() !== $metadata['userName'] |
|
871
|
|
|
) { |
|
872
|
|
|
$this->logger->warning( |
|
873
|
|
|
'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}', |
|
874
|
|
|
[ |
|
875
|
|
|
'session' => $info, |
|
876
|
|
|
'uname_a' => $metadata['userName'], |
|
877
|
|
|
'uname_b' => $userInfo->getName(), |
|
878
|
|
|
] ); |
|
879
|
|
|
return $failHandler(); |
|
880
|
|
|
} |
|
881
|
|
|
|
|
882
|
|
View Code Duplication |
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case |
|
883
|
|
|
if ( $metadata['userName'] !== $userInfo->getName() ) { |
|
884
|
|
|
$this->logger->warning( |
|
885
|
|
|
'Session "{session}": User name mismatch, {uname_a} !== {uname_b}', |
|
886
|
|
|
[ |
|
887
|
|
|
'session' => $info, |
|
888
|
|
|
'uname_a' => $metadata['userName'], |
|
889
|
|
|
'uname_b' => $userInfo->getName(), |
|
890
|
|
|
] ); |
|
891
|
|
|
return $failHandler(); |
|
892
|
|
|
} |
|
893
|
|
|
} elseif ( !$userInfo->isAnon() ) { |
|
894
|
|
|
// Metadata specifies an anonymous user, but the passed-in |
|
895
|
|
|
// user isn't anonymous. |
|
896
|
|
|
$this->logger->warning( |
|
897
|
|
|
'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided', |
|
898
|
|
|
[ |
|
899
|
|
|
'session' => $info, |
|
900
|
|
|
] ); |
|
901
|
|
|
return $failHandler(); |
|
902
|
|
|
} |
|
903
|
|
|
} |
|
904
|
|
|
|
|
905
|
|
|
// And if we have a token in the metadata, it must match the loaded/provided user. |
|
906
|
|
|
if ( $metadata['userToken'] !== null && |
|
907
|
|
|
$userInfo->getToken() !== $metadata['userToken'] |
|
908
|
|
|
) { |
|
909
|
|
|
$this->logger->warning( 'Session "{session}": User token mismatch', [ |
|
910
|
|
|
'session' => $info, |
|
911
|
|
|
] ); |
|
912
|
|
|
return $failHandler(); |
|
913
|
|
|
} |
|
914
|
|
|
if ( !$userInfo->isVerified() ) { |
|
915
|
|
|
$newParams['userInfo'] = $userInfo->verified(); |
|
916
|
|
|
} |
|
917
|
|
|
|
|
918
|
|
|
if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) { |
|
919
|
|
|
$newParams['remembered'] = true; |
|
920
|
|
|
} |
|
921
|
|
|
if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) { |
|
922
|
|
|
$newParams['forceHTTPS'] = true; |
|
923
|
|
|
} |
|
924
|
|
|
if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) { |
|
925
|
|
|
$newParams['persisted'] = true; |
|
926
|
|
|
} |
|
927
|
|
|
|
|
928
|
|
|
if ( !$info->isIdSafe() ) { |
|
929
|
|
|
$newParams['idIsSafe'] = true; |
|
930
|
|
|
} |
|
931
|
|
|
} else { |
|
932
|
|
|
// No metadata, so we can't load the provider if one wasn't given. |
|
933
|
|
|
if ( $info->getProvider() === null ) { |
|
934
|
|
|
$this->logger->warning( |
|
935
|
|
|
'Session "{session}": Null provider and no metadata', |
|
936
|
|
|
[ |
|
937
|
|
|
'session' => $info, |
|
938
|
|
|
] ); |
|
939
|
|
|
return $failHandler(); |
|
940
|
|
|
} |
|
941
|
|
|
|
|
942
|
|
|
// If no user was provided and no metadata, it must be anon. |
|
943
|
|
|
if ( !$info->getUserInfo() ) { |
|
944
|
|
|
if ( $info->getProvider()->canChangeUser() ) { |
|
945
|
|
|
$newParams['userInfo'] = UserInfo::newAnonymous(); |
|
946
|
|
|
} else { |
|
947
|
|
|
$this->logger->info( |
|
948
|
|
|
'Session "{session}": No user provided and provider cannot set user', |
|
949
|
|
|
[ |
|
950
|
|
|
'session' => $info, |
|
951
|
|
|
] ); |
|
952
|
|
|
return $failHandler(); |
|
953
|
|
|
} |
|
954
|
|
|
} elseif ( !$info->getUserInfo()->isVerified() ) { |
|
955
|
|
|
$this->logger->warning( |
|
956
|
|
|
'Session "{session}": Unverified user provided and no metadata to auth it', |
|
957
|
|
|
[ |
|
958
|
|
|
'session' => $info, |
|
959
|
|
|
] ); |
|
960
|
|
|
return $failHandler(); |
|
961
|
|
|
} |
|
962
|
|
|
|
|
963
|
|
|
$data = false; |
|
964
|
|
|
$metadata = false; |
|
965
|
|
|
|
|
966
|
|
|
if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) { |
|
967
|
|
|
// The ID doesn't come from the user, so it should be safe |
|
968
|
|
|
// (and if not, nothing we can do about it anyway) |
|
969
|
|
|
$newParams['idIsSafe'] = true; |
|
970
|
|
|
} |
|
971
|
|
|
} |
|
972
|
|
|
|
|
973
|
|
|
// Construct the replacement SessionInfo, if necessary |
|
974
|
|
|
if ( $newParams ) { |
|
975
|
|
|
$newParams['copyFrom'] = $info; |
|
976
|
|
|
$info = new SessionInfo( $info->getPriority(), $newParams ); |
|
977
|
|
|
} |
|
978
|
|
|
|
|
979
|
|
|
// Allow the provider to check the loaded SessionInfo |
|
980
|
|
|
$providerMetadata = $info->getProviderMetadata(); |
|
981
|
|
|
if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) { |
|
982
|
|
|
return $failHandler(); |
|
983
|
|
|
} |
|
984
|
|
|
if ( $providerMetadata !== $info->getProviderMetadata() ) { |
|
985
|
|
|
$info = new SessionInfo( $info->getPriority(), [ |
|
986
|
|
|
'metadata' => $providerMetadata, |
|
987
|
|
|
'copyFrom' => $info, |
|
988
|
|
|
] ); |
|
989
|
|
|
} |
|
990
|
|
|
|
|
991
|
|
|
// Give hooks a chance to abort. Combined with the SessionMetadata |
|
992
|
|
|
// hook, this can allow for tying a session to an IP address or the |
|
993
|
|
|
// like. |
|
994
|
|
|
$reason = 'Hook aborted'; |
|
995
|
|
|
if ( !\Hooks::run( |
|
996
|
|
|
'SessionCheckInfo', |
|
997
|
|
|
[ &$reason, $info, $request, $metadata, $data ] |
|
998
|
|
|
) ) { |
|
999
|
|
|
$this->logger->warning( 'Session "{session}": ' . $reason, [ |
|
1000
|
|
|
'session' => $info, |
|
1001
|
|
|
] ); |
|
1002
|
|
|
return $failHandler(); |
|
1003
|
|
|
} |
|
1004
|
|
|
|
|
1005
|
|
|
return true; |
|
1006
|
|
|
} |
|
1007
|
|
|
|
|
1008
|
|
|
/** |
|
1009
|
|
|
* Create a session corresponding to the passed SessionInfo |
|
1010
|
|
|
* @private For use by a SessionProvider that needs to specially create its |
|
1011
|
|
|
* own session. |
|
1012
|
|
|
* @param SessionInfo $info |
|
1013
|
|
|
* @param WebRequest $request |
|
1014
|
|
|
* @return Session |
|
1015
|
|
|
*/ |
|
1016
|
|
|
public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) { |
|
1017
|
|
|
// @codeCoverageIgnoreStart |
|
1018
|
|
|
if ( defined( 'MW_NO_SESSION' ) ) { |
|
1019
|
|
|
if ( MW_NO_SESSION === 'warn' ) { |
|
|
|
|
|
|
1020
|
|
|
// Undocumented safety case for converting existing entry points |
|
1021
|
|
|
$this->logger->error( 'Sessions are supposed to be disabled for this entry point', [ |
|
1022
|
|
|
'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ), |
|
1023
|
|
|
] ); |
|
1024
|
|
|
} else { |
|
1025
|
|
|
throw new \BadMethodCallException( 'Sessions are disabled for this entry point' ); |
|
1026
|
|
|
} |
|
1027
|
|
|
} |
|
1028
|
|
|
// @codeCoverageIgnoreEnd |
|
1029
|
|
|
|
|
1030
|
|
|
$id = $info->getId(); |
|
1031
|
|
|
|
|
1032
|
|
|
if ( !isset( $this->allSessionBackends[$id] ) ) { |
|
1033
|
|
|
if ( !isset( $this->allSessionIds[$id] ) ) { |
|
1034
|
|
|
$this->allSessionIds[$id] = new SessionId( $id ); |
|
1035
|
|
|
} |
|
1036
|
|
|
$backend = new SessionBackend( |
|
1037
|
|
|
$this->allSessionIds[$id], |
|
1038
|
|
|
$info, |
|
1039
|
|
|
$this->store, |
|
|
|
|
|
|
1040
|
|
|
$this->logger, |
|
1041
|
|
|
$this->config->get( 'ObjectCacheSessionExpiry' ) |
|
1042
|
|
|
); |
|
1043
|
|
|
$this->allSessionBackends[$id] = $backend; |
|
1044
|
|
|
$delay = $backend->delaySave(); |
|
1045
|
|
|
} else { |
|
1046
|
|
|
$backend = $this->allSessionBackends[$id]; |
|
1047
|
|
|
$delay = $backend->delaySave(); |
|
1048
|
|
|
if ( $info->wasPersisted() ) { |
|
1049
|
|
|
$backend->persist(); |
|
1050
|
|
|
} |
|
1051
|
|
|
if ( $info->wasRemembered() ) { |
|
1052
|
|
|
$backend->setRememberUser( true ); |
|
1053
|
|
|
} |
|
1054
|
|
|
} |
|
1055
|
|
|
|
|
1056
|
|
|
$request->setSessionId( $backend->getSessionId() ); |
|
1057
|
|
|
$session = $backend->getSession( $request ); |
|
1058
|
|
|
|
|
1059
|
|
|
if ( !$info->isIdSafe() ) { |
|
1060
|
|
|
$session->resetId(); |
|
1061
|
|
|
} |
|
1062
|
|
|
|
|
1063
|
|
|
\ScopedCallback::consume( $delay ); |
|
1064
|
|
|
return $session; |
|
1065
|
|
|
} |
|
1066
|
|
|
|
|
1067
|
|
|
/** |
|
1068
|
|
|
* Deregister a SessionBackend |
|
1069
|
|
|
* @private For use from \MediaWiki\Session\SessionBackend only |
|
1070
|
|
|
* @param SessionBackend $backend |
|
1071
|
|
|
*/ |
|
1072
|
|
|
public function deregisterSessionBackend( SessionBackend $backend ) { |
|
1073
|
|
|
$id = $backend->getId(); |
|
1074
|
|
View Code Duplication |
if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) || |
|
1075
|
|
|
$this->allSessionBackends[$id] !== $backend || |
|
1076
|
|
|
$this->allSessionIds[$id] !== $backend->getSessionId() |
|
1077
|
|
|
) { |
|
1078
|
|
|
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' ); |
|
1079
|
|
|
} |
|
1080
|
|
|
|
|
1081
|
|
|
unset( $this->allSessionBackends[$id] ); |
|
1082
|
|
|
// Explicitly do not unset $this->allSessionIds[$id] |
|
1083
|
|
|
} |
|
1084
|
|
|
|
|
1085
|
|
|
/** |
|
1086
|
|
|
* Change a SessionBackend's ID |
|
1087
|
|
|
* @private For use from \MediaWiki\Session\SessionBackend only |
|
1088
|
|
|
* @param SessionBackend $backend |
|
1089
|
|
|
*/ |
|
1090
|
|
|
public function changeBackendId( SessionBackend $backend ) { |
|
1091
|
|
|
$sessionId = $backend->getSessionId(); |
|
1092
|
|
|
$oldId = (string)$sessionId; |
|
1093
|
|
View Code Duplication |
if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) || |
|
1094
|
|
|
$this->allSessionBackends[$oldId] !== $backend || |
|
1095
|
|
|
$this->allSessionIds[$oldId] !== $sessionId |
|
1096
|
|
|
) { |
|
1097
|
|
|
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' ); |
|
1098
|
|
|
} |
|
1099
|
|
|
|
|
1100
|
|
|
$newId = $this->generateSessionId(); |
|
1101
|
|
|
|
|
1102
|
|
|
unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] ); |
|
1103
|
|
|
$sessionId->setId( $newId ); |
|
|
|
|
|
|
1104
|
|
|
$this->allSessionBackends[$newId] = $backend; |
|
1105
|
|
|
$this->allSessionIds[$newId] = $sessionId; |
|
1106
|
|
|
} |
|
1107
|
|
|
|
|
1108
|
|
|
/** |
|
1109
|
|
|
* Generate a new random session ID |
|
1110
|
|
|
* @return string |
|
1111
|
|
|
*/ |
|
1112
|
|
|
public function generateSessionId() { |
|
1113
|
|
|
do { |
|
1114
|
|
|
$id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 ); |
|
1115
|
|
|
$key = wfMemcKey( 'MWSession', $id ); |
|
1116
|
|
|
} while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) ); |
|
1117
|
|
|
return $id; |
|
1118
|
|
|
} |
|
1119
|
|
|
|
|
1120
|
|
|
/** |
|
1121
|
|
|
* Call setters on a PHPSessionHandler |
|
1122
|
|
|
* @private Use PhpSessionHandler::install() |
|
1123
|
|
|
* @param PHPSessionHandler $handler |
|
1124
|
|
|
*/ |
|
1125
|
|
|
public function setupPHPSessionHandler( PHPSessionHandler $handler ) { |
|
1126
|
|
|
$handler->setManager( $this, $this->store, $this->logger ); |
|
|
|
|
|
|
1127
|
|
|
} |
|
1128
|
|
|
|
|
1129
|
|
|
/** |
|
1130
|
|
|
* Reset the internal caching for unit testing |
|
1131
|
|
|
*/ |
|
1132
|
|
|
public static function resetCache() { |
|
1133
|
|
|
if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
|
1134
|
|
|
// @codeCoverageIgnoreStart |
|
1135
|
|
|
throw new MWException( __METHOD__ . ' may only be called from unit tests!' ); |
|
1136
|
|
|
// @codeCoverageIgnoreEnd |
|
1137
|
|
|
} |
|
1138
|
|
|
|
|
1139
|
|
|
self::$globalSession = null; |
|
1140
|
|
|
self::$globalSessionRequest = null; |
|
1141
|
|
|
} |
|
1142
|
|
|
|
|
1143
|
|
|
/**@}*/ |
|
1144
|
|
|
|
|
1145
|
|
|
} |
|
1146
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.