Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.37% |
76 / 108 |
|
86.67% |
26 / 30 |
CRAP | |
0.00% |
0 / 1 |
SessionProvider | |
70.37% |
76 / 108 |
|
86.67% |
26 / 30 |
104.46 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
postInitSetup | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLogger | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setManager | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getManager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setHookContainer | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getHookContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHookRunner | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
provideSessionInfo | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
newSessionInfo | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
mergeMetadata | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
refreshSessionInfo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
persistsSessionId | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
canChangeUser | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getRememberUserDuration | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sessionIdWasReset | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
persistSession | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
unpersistSession | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
preventSessionsForUser | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
invalidateSessionsForUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVaryHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVaryCookies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
suggestLoginUsername | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedUserRights | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRestrictions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
describeMessage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
describe | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
whyNoSession | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
safeAgainstCsrf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canAlwaysAutocreate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hashToSessionId | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
makeException | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * MediaWiki session provider base class |
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 ApiUsageException; |
27 | use ErrorPageError; |
28 | use Language; |
29 | use MediaWiki\Config\Config; |
30 | use MediaWiki\Context\RequestContext; |
31 | use MediaWiki\HookContainer\HookContainer; |
32 | use MediaWiki\HookContainer\HookRunner; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\Request\WebRequest; |
35 | use MediaWiki\User\User; |
36 | use MediaWiki\User\UserNameUtils; |
37 | use MWRestrictions; |
38 | use Psr\Log\LoggerInterface; |
39 | |
40 | /** |
41 | * A SessionProvider provides SessionInfo and support for Session |
42 | * |
43 | * A SessionProvider is responsible for taking a WebRequest and determining |
44 | * the authenticated session that it's a part of. It does this by returning an |
45 | * SessionInfo object with basic information about the session it thinks is |
46 | * associated with the request, namely the session ID and possibly the |
47 | * authenticated user the session belongs to. |
48 | * |
49 | * The SessionProvider also provides for updating the WebResponse with |
50 | * information necessary to provide the client with data that the client will |
51 | * send with later requests, and for populating the Vary and Key headers with |
52 | * the data necessary to correctly vary the cache on these client requests. |
53 | * |
54 | * An important part of the latter is indicating whether it even *can* tell the |
55 | * client to include such data in future requests, via the persistsSessionId() |
56 | * and canChangeUser() methods. The cases are (in order of decreasing |
57 | * commonness): |
58 | * - Cannot persist ID, no changing User: The request identifies and |
59 | * authenticates a particular local user, and the client cannot be |
60 | * instructed to include an arbitrary session ID with future requests. For |
61 | * example, OAuth or SSL certificate auth. |
62 | * - Can persist ID and can change User: The client can be instructed to |
63 | * return at least one piece of arbitrary data, that being the session ID. |
64 | * The user identity might also be given to the client, otherwise it's saved |
65 | * in the session data. For example, cookie-based sessions. |
66 | * - Can persist ID but no changing User: The request uniquely identifies and |
67 | * authenticates a local user, and the client can be instructed to return an |
68 | * arbitrary session ID with future requests. For example, HTTP Digest |
69 | * authentication might somehow use the 'opaque' field as a session ID |
70 | * (although getting MediaWiki to return 401 responses without breaking |
71 | * other stuff might be a challenge). |
72 | * - Cannot persist ID but can change User: I can't think of a way this |
73 | * would make sense. |
74 | * |
75 | * Note that many methods that are technically "cannot persist ID" could be |
76 | * turned into "can persist ID but not change User" using a session cookie, |
77 | * as implemented by ImmutableSessionProviderWithCookie. If doing so, different |
78 | * session cookie names should be used for different providers to avoid |
79 | * collisions. |
80 | * |
81 | * @stable to extend |
82 | * @ingroup Session |
83 | * @since 1.27 |
84 | * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager |
85 | */ |
86 | abstract class SessionProvider implements SessionProviderInterface { |
87 | |
88 | /** @var LoggerInterface */ |
89 | protected $logger; |
90 | |
91 | /** @var Config */ |
92 | protected $config; |
93 | |
94 | /** @var SessionManager */ |
95 | protected $manager; |
96 | |
97 | /** @var HookContainer */ |
98 | private $hookContainer; |
99 | |
100 | /** @var HookRunner */ |
101 | private $hookRunner; |
102 | |
103 | /** @var UserNameUtils */ |
104 | protected $userNameUtils; |
105 | |
106 | /** @var int Session priority. Used for the default newSessionInfo(), but |
107 | * could be used by subclasses too. |
108 | */ |
109 | protected $priority; |
110 | |
111 | /** |
112 | * @stable to call |
113 | */ |
114 | public function __construct() { |
115 | $this->priority = SessionInfo::MIN_PRIORITY + 10; |
116 | } |
117 | |
118 | /** |
119 | * Initialise with dependencies of a SessionProvider |
120 | * |
121 | * @since 1.37 |
122 | * @internal In production code SessionManager will initialize the |
123 | * SessionProvider, in tests SessionProviderTestTrait must be used. |
124 | * |
125 | * @param LoggerInterface $logger |
126 | * @param Config $config |
127 | * @param SessionManager $manager |
128 | * @param HookContainer $hookContainer |
129 | * @param UserNameUtils $userNameUtils |
130 | */ |
131 | public function init( |
132 | LoggerInterface $logger, |
133 | Config $config, |
134 | SessionManager $manager, |
135 | HookContainer $hookContainer, |
136 | UserNameUtils $userNameUtils |
137 | ) { |
138 | $this->logger = $logger; |
139 | $this->config = $config; |
140 | $this->manager = $manager; |
141 | $this->hookContainer = $hookContainer; |
142 | $this->hookRunner = new HookRunner( $hookContainer ); |
143 | $this->userNameUtils = $userNameUtils; |
144 | $this->postInitSetup(); |
145 | } |
146 | |
147 | /** |
148 | * A provider can override this to do any necessary setup after init() |
149 | * is called. |
150 | * |
151 | * @since 1.37 |
152 | * @stable to override |
153 | */ |
154 | protected function postInitSetup() { |
155 | } |
156 | |
157 | /** |
158 | * Sets a logger instance on the object. |
159 | * |
160 | * @deprecated since 1.37. For extension-defined session providers |
161 | * that were using this method to trigger other work, please override |
162 | * SessionProvider::postInitSetup instead. If your extension |
163 | * was using this to explicitly change the logger of an existing |
164 | * SessionProvider object, please file a report on phabricator |
165 | * - there is no non-deprecated way to do this anymore. |
166 | * @param LoggerInterface $logger |
167 | */ |
168 | public function setLogger( LoggerInterface $logger ) { |
169 | wfDeprecated( __METHOD__, '1.37' ); |
170 | $this->logger = $logger; |
171 | } |
172 | |
173 | /** |
174 | * Set configuration |
175 | * |
176 | * @deprecated since 1.37. For extension-defined session providers |
177 | * that were using this method to trigger other work, please override |
178 | * SessionProvider::postInitSetup instead. If your extension |
179 | * was using this to explicitly change the Config of an existing |
180 | * SessionProvider object, please file a report on phabricator |
181 | * - there is no non-deprecated way to do this anymore. |
182 | * @param Config $config |
183 | */ |
184 | public function setConfig( Config $config ) { |
185 | wfDeprecated( __METHOD__, '1.37' ); |
186 | $this->config = $config; |
187 | } |
188 | |
189 | /** |
190 | * Get the config |
191 | * |
192 | * @since 1.37 |
193 | * @return Config |
194 | */ |
195 | protected function getConfig() { |
196 | return $this->config; |
197 | } |
198 | |
199 | /** |
200 | * Set the session manager |
201 | * |
202 | * @deprecated since 1.37. For extension-defined session providers |
203 | * that were using this method to trigger other work, please override |
204 | * SessionProvider::postInitSetup instead. If your extension |
205 | * was using this to explicitly change the SessionManager of an existing |
206 | * SessionProvider object, please file a report on phabricator |
207 | * - there is no non-deprecated way to do this anymore. |
208 | * @param SessionManager $manager |
209 | */ |
210 | public function setManager( SessionManager $manager ) { |
211 | wfDeprecated( __METHOD__, '1.37' ); |
212 | $this->manager = $manager; |
213 | } |
214 | |
215 | /** |
216 | * Get the session manager |
217 | * @return SessionManager |
218 | */ |
219 | public function getManager() { |
220 | return $this->manager; |
221 | } |
222 | |
223 | /** |
224 | * @internal |
225 | * @deprecated since 1.37. For extension-defined session providers |
226 | * that were using this method to trigger other work, please override |
227 | * SessionProvider::postInitSetup instead. If your extension |
228 | * was using this to explicitly change the HookContainer of an existing |
229 | * SessionProvider object, please file a report on phabricator |
230 | * - there is no non-deprecated way to do this anymore. |
231 | * @param HookContainer $hookContainer |
232 | */ |
233 | public function setHookContainer( $hookContainer ) { |
234 | wfDeprecated( __METHOD__, '1.37' ); |
235 | $this->hookContainer = $hookContainer; |
236 | $this->hookRunner = new HookRunner( $hookContainer ); |
237 | } |
238 | |
239 | /** |
240 | * Get the HookContainer |
241 | * |
242 | * @return HookContainer |
243 | */ |
244 | protected function getHookContainer(): HookContainer { |
245 | return $this->hookContainer; |
246 | } |
247 | |
248 | /** |
249 | * Get the HookRunner |
250 | * |
251 | * @internal This is for use by core only. Hook interfaces may be removed |
252 | * without notice. |
253 | * @since 1.35 |
254 | * @return HookRunner |
255 | */ |
256 | protected function getHookRunner(): HookRunner { |
257 | return $this->hookRunner; |
258 | } |
259 | |
260 | /** |
261 | * Provide session info for a request |
262 | * |
263 | * If no session exists for the request, return null. Otherwise return an |
264 | * SessionInfo object identifying the session. |
265 | * |
266 | * If multiple SessionProviders provide sessions, the one with highest |
267 | * priority wins. In case of a tie, an exception is thrown. |
268 | * SessionProviders are encouraged to make priorities user-configurable |
269 | * unless only max-priority makes sense. |
270 | * |
271 | * @warning This will be called early in the MediaWiki setup process, |
272 | * before $wgUser, $wgLang, $wgOut, $wgTitle, the global parser, and |
273 | * corresponding pieces of the main RequestContext are set up! If you try |
274 | * to use these, things *will* break. |
275 | * @note The SessionProvider must not attempt to auto-create users. |
276 | * MediaWiki will do this later (when it's safe) if the chosen session has |
277 | * a user with a valid name but no ID. |
278 | * @note For use by \MediaWiki\Session\SessionManager only |
279 | * @param WebRequest $request |
280 | * @return SessionInfo|null |
281 | */ |
282 | abstract public function provideSessionInfo( WebRequest $request ); |
283 | |
284 | /** |
285 | * Provide session info for a new, empty session |
286 | * |
287 | * Return null if such a session cannot be created. This base |
288 | * implementation assumes that it only makes sense if a session ID can be |
289 | * persisted and changing users is allowed. |
290 | * @stable to override |
291 | * |
292 | * @note For use by \MediaWiki\Session\SessionManager only |
293 | * @param string|null $id ID to force for the new session |
294 | * @return SessionInfo|null |
295 | * If non-null, must return true for $info->isIdSafe(); pass true for |
296 | * $data['idIsSafe'] to ensure this. |
297 | */ |
298 | public function newSessionInfo( $id = null ) { |
299 | if ( $this->canChangeUser() && $this->persistsSessionId() ) { |
300 | return new SessionInfo( $this->priority, [ |
301 | 'id' => $id, |
302 | 'provider' => $this, |
303 | 'persisted' => false, |
304 | 'idIsSafe' => true, |
305 | ] ); |
306 | } |
307 | return null; |
308 | } |
309 | |
310 | /** |
311 | * Merge saved session provider metadata |
312 | * |
313 | * This method will be used to compare the metadata returned by |
314 | * provideSessionInfo() with the saved metadata (which has been returned by |
315 | * provideSessionInfo() the last time the session was saved), and merge the two |
316 | * into the new saved metadata, or abort if the current request is not a valid |
317 | * continuation of the session. |
318 | * |
319 | * The default implementation checks that anything in both arrays is |
320 | * identical, then returns $providedMetadata. |
321 | * @stable to override |
322 | * |
323 | * @note For use by \MediaWiki\Session\SessionManager only |
324 | * @param array $savedMetadata Saved provider metadata |
325 | * @param array $providedMetadata Provided provider metadata (from the SessionInfo) |
326 | * @return array Resulting metadata |
327 | * @throws MetadataMergeException If the metadata cannot be merged. |
328 | * Such exceptions will be handled by SessionManager and are a safe way of rejecting |
329 | * a suspicious or incompatible session. The provider is expected to write an |
330 | * appropriate message to its logger. |
331 | */ |
332 | public function mergeMetadata( array $savedMetadata, array $providedMetadata ) { |
333 | foreach ( $providedMetadata as $k => $v ) { |
334 | if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) { |
335 | $e = new MetadataMergeException( "Key \"$k\" changed" ); |
336 | $e->setContext( [ |
337 | 'old_value' => $savedMetadata[$k], |
338 | 'new_value' => $v, |
339 | ] ); |
340 | throw $e; |
341 | } |
342 | } |
343 | return $providedMetadata; |
344 | } |
345 | |
346 | /** |
347 | * Validate a loaded SessionInfo and refresh provider metadata |
348 | * |
349 | * This is similar in purpose to the 'SessionCheckInfo' hook, and also |
350 | * allows for updating the provider metadata. On failure, the provider is |
351 | * expected to write an appropriate message to its logger. |
352 | * @stable to override |
353 | * |
354 | * @note For use by \MediaWiki\Session\SessionManager only |
355 | * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here. |
356 | * @param WebRequest $request |
357 | * @param array|null &$metadata Provider metadata, may be altered. |
358 | * @return bool Return false to reject the SessionInfo after all. |
359 | */ |
360 | public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) { |
361 | return true; |
362 | } |
363 | |
364 | /** |
365 | * Indicate whether self::persistSession() can save arbitrary session IDs |
366 | * |
367 | * If false, any session passed to self::persistSession() will have an ID |
368 | * that was originally provided by self::provideSessionInfo(). |
369 | * |
370 | * If true, the provider may be passed sessions with arbitrary session IDs, |
371 | * and will be expected to manipulate the request in such a way that future |
372 | * requests will cause self::provideSessionInfo() to provide a SessionInfo |
373 | * with that ID. |
374 | * |
375 | * For example, a session provider for OAuth would function by matching the |
376 | * OAuth headers to a particular user, and then would use self::hashToSessionId() |
377 | * to turn the user and OAuth client ID (and maybe also the user token and |
378 | * client secret) into a session ID, and therefore can't easily assign that |
379 | * user+client a different ID. Similarly, a session provider for SSL client |
380 | * certificates would function by matching the certificate to a particular |
381 | * user, and then would use self::hashToSessionId() to turn the user and |
382 | * certificate fingerprint into a session ID, and therefore can't easily |
383 | * assign a different ID either. On the other hand, a provider that saves |
384 | * the session ID into a cookie can easily just set the cookie to a |
385 | * different value. |
386 | * |
387 | * @note For use by \MediaWiki\Session\SessionBackend only |
388 | * @return bool |
389 | */ |
390 | abstract public function persistsSessionId(); |
391 | |
392 | /** |
393 | * Indicate whether the user associated with the request can be changed |
394 | * |
395 | * If false, any session passed to self::persistSession() will have a user |
396 | * that was originally provided by self::provideSessionInfo(). Further, |
397 | * self::provideSessionInfo() may only provide sessions that have a user |
398 | * already set. |
399 | * |
400 | * If true, the provider may be passed sessions with arbitrary users, and |
401 | * will be expected to manipulate the request in such a way that future |
402 | * requests will cause self::provideSessionInfo() to provide a SessionInfo |
403 | * with that ID. This can be as simple as not passing any 'userInfo' into |
404 | * SessionInfo's constructor, in which case SessionInfo will load the user |
405 | * from the saved session's metadata. |
406 | * |
407 | * For example, a session provider for OAuth or SSL client certificates |
408 | * would function by matching the OAuth headers or certificate to a |
409 | * particular user, and thus would return false here since it can't |
410 | * arbitrarily assign those OAuth credentials or that certificate to a |
411 | * different user. A session provider that shoves information into cookies, |
412 | * on the other hand, could easily do so. |
413 | * |
414 | * @note For use by \MediaWiki\Session\SessionBackend only |
415 | * @return bool |
416 | */ |
417 | abstract public function canChangeUser(); |
418 | |
419 | /** |
420 | * Returns the duration (in seconds) for which users will be remembered when |
421 | * Session::setRememberUser() is set. Null means setting the remember flag will |
422 | * have no effect (and endpoints should not offer that option). |
423 | * @stable to override |
424 | * @return int|null |
425 | */ |
426 | public function getRememberUserDuration() { |
427 | return null; |
428 | } |
429 | |
430 | /** |
431 | * Notification that the session ID was reset |
432 | * |
433 | * No need to persist here, persistSession() will be called if appropriate. |
434 | * @stable to override |
435 | * |
436 | * @note For use by \MediaWiki\Session\SessionBackend only |
437 | * @param SessionBackend $session Session to persist |
438 | * @param string $oldId Old session ID |
439 | * @codeCoverageIgnore |
440 | */ |
441 | public function sessionIdWasReset( SessionBackend $session, $oldId ) { |
442 | } |
443 | |
444 | /** |
445 | * Persist a session into a request/response |
446 | * |
447 | * For example, you might set cookies for the session's ID, user ID, user |
448 | * name, and user token on the passed request. |
449 | * |
450 | * To correctly persist a user independently of the session ID, the |
451 | * provider should persist both the user ID (or name, but preferably the |
452 | * ID) and the user token. When reading the data from the request, it |
453 | * should construct a User object from the ID/name and then verify that the |
454 | * User object's token matches the token included in the request. Should |
455 | * the tokens not match, an anonymous user *must* be passed to |
456 | * SessionInfo::__construct(). |
457 | * |
458 | * When persisting a user independently of the session ID, |
459 | * $session->shouldRememberUser() should be checked first. If this returns |
460 | * false, the user token *must not* be saved to cookies. The user name |
461 | * and/or ID may be persisted, and should be used to construct an |
462 | * unverified UserInfo to pass to SessionInfo::__construct(). |
463 | * |
464 | * A backend that cannot persist session ID or user info should implement |
465 | * this as a no-op. |
466 | * |
467 | * @note For use by \MediaWiki\Session\SessionBackend only |
468 | * @param SessionBackend $session Session to persist |
469 | * @param WebRequest $request Request into which to persist the session |
470 | */ |
471 | abstract public function persistSession( SessionBackend $session, WebRequest $request ); |
472 | |
473 | /** |
474 | * Remove any persisted session from a request/response |
475 | * |
476 | * For example, blank and expire any cookies set by self::persistSession(). |
477 | * |
478 | * A backend that cannot persist session ID or user info should implement |
479 | * this as a no-op. |
480 | * |
481 | * @note For use by \MediaWiki\Session\SessionManager only |
482 | * @param WebRequest $request Request from which to remove any session data |
483 | */ |
484 | abstract public function unpersistSession( WebRequest $request ); |
485 | |
486 | /** |
487 | * Prevent future sessions for the user |
488 | * |
489 | * If the provider is capable of returning a SessionInfo with a verified |
490 | * UserInfo for the named user in some manner other than by validating |
491 | * against $user->getToken(), steps must be taken to prevent that from |
492 | * occurring in the future. This might add the username to a list, or |
493 | * it might just delete whatever authentication credentials would allow |
494 | * such a session in the first place (e.g. remove all OAuth grants or |
495 | * delete record of the SSL client certificate). |
496 | * |
497 | * The intention is that the named account will never again be usable for |
498 | * normal login (i.e. there is no way to undo the prevention of access). |
499 | * |
500 | * Note that the passed user name might not exist locally (i.e. |
501 | * UserIdentity::getId() === 0); the name should still be |
502 | * prevented, if applicable. |
503 | * |
504 | * @stable to override |
505 | * @note For use by \MediaWiki\Session\SessionManager only |
506 | * @param string $username |
507 | */ |
508 | public function preventSessionsForUser( $username ) { |
509 | if ( !$this->canChangeUser() ) { |
510 | throw new \BadMethodCallException( |
511 | __METHOD__ . ' must be implemented when canChangeUser() is false' |
512 | ); |
513 | } |
514 | } |
515 | |
516 | /** |
517 | * Invalidate existing sessions for a user |
518 | * |
519 | * If the provider has its own equivalent of CookieSessionProvider's Token |
520 | * cookie (and doesn't use User::getToken() to implement it), it should |
521 | * reset whatever token it does use here. |
522 | * |
523 | * @stable to override |
524 | * @note For use by \MediaWiki\Session\SessionManager only |
525 | * @param User $user |
526 | */ |
527 | public function invalidateSessionsForUser( User $user ) { |
528 | } |
529 | |
530 | /** |
531 | * Return the HTTP headers that need varying on. |
532 | * |
533 | * The return value is such that someone could theoretically do this: |
534 | * @code |
535 | * foreach ( $provider->getVaryHeaders() as $header => $_ ) { |
536 | * $outputPage->addVaryHeader( $header ); |
537 | * } |
538 | * @endcode |
539 | * |
540 | * @stable to override |
541 | * @note For use by \MediaWiki\Session\SessionManager only |
542 | * @return array<string,null> |
543 | */ |
544 | public function getVaryHeaders() { |
545 | return []; |
546 | } |
547 | |
548 | /** |
549 | * Return the list of cookies that need varying on. |
550 | * @stable to override |
551 | * @note For use by \MediaWiki\Session\SessionManager only |
552 | * @return string[] |
553 | */ |
554 | public function getVaryCookies() { |
555 | return []; |
556 | } |
557 | |
558 | /** |
559 | * Get a suggested username for the login form |
560 | * @stable to override |
561 | * @note For use by \MediaWiki\Session\SessionBackend only |
562 | * @param WebRequest $request |
563 | * @return string|null |
564 | */ |
565 | public function suggestLoginUsername( WebRequest $request ) { |
566 | return null; |
567 | } |
568 | |
569 | /** |
570 | * Fetch the rights allowed the user when the specified session is active. |
571 | * |
572 | * This is mainly meant for allowing the user to restrict access to the account |
573 | * by certain methods; you probably want to use this with GrantsInfo. The returned |
574 | * rights will be intersected with the user's actual rights. |
575 | * |
576 | * @stable to override |
577 | * @param SessionBackend $backend |
578 | * @return null|string[] Allowed user rights, or null to allow all. |
579 | */ |
580 | public function getAllowedUserRights( SessionBackend $backend ) { |
581 | if ( $backend->getProvider() !== $this ) { |
582 | // Not that this should ever happen... |
583 | throw new \InvalidArgumentException( 'Backend\'s provider isn\'t $this' ); |
584 | } |
585 | |
586 | return null; |
587 | } |
588 | |
589 | /** |
590 | * Fetch any restrictions imposed on logins or actions when this |
591 | * session is active. |
592 | * |
593 | * @since 1.42 |
594 | * @stable to override |
595 | * @return MWRestrictions|null |
596 | */ |
597 | public function getRestrictions( ?array $providerMetadata ): ?MWRestrictions { |
598 | return null; |
599 | } |
600 | |
601 | /** |
602 | * @note Only override this if it makes sense to instantiate multiple |
603 | * instances of the provider. Value returned must be unique across |
604 | * configured providers. If you override this, you'll likely need to |
605 | * override self::describeMessage() as well. |
606 | * @return string |
607 | */ |
608 | public function __toString() { |
609 | return static::class; |
610 | } |
611 | |
612 | /** |
613 | * Return a Message identifying this session type |
614 | * |
615 | * This default implementation takes the class name, lowercases it, |
616 | * replaces backslashes with dashes, and prefixes 'sessionprovider-' to |
617 | * determine the message key. For example, MediaWiki\Session\CookieSessionProvider |
618 | * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'. |
619 | * |
620 | * @stable to override |
621 | * @note If self::__toString() is overridden, this will likely need to be |
622 | * overridden as well. |
623 | * @warning This will be called early during MediaWiki startup. Do not |
624 | * use $wgUser, $wgLang, $wgOut, the global Parser, or their equivalents via |
625 | * RequestContext from this method! |
626 | * @return \Message |
627 | */ |
628 | protected function describeMessage() { |
629 | return wfMessage( |
630 | 'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) ) |
631 | ); |
632 | } |
633 | |
634 | /** |
635 | * @inheritDoc |
636 | * @stable to override |
637 | */ |
638 | public function describe( Language $lang ) { |
639 | $msg = $this->describeMessage(); |
640 | $msg->inLanguage( $lang ); |
641 | if ( $msg->isDisabled() ) { |
642 | $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang ); |
643 | } |
644 | return $msg->plain(); |
645 | } |
646 | |
647 | /** |
648 | * @inheritDoc |
649 | * @stable to override |
650 | */ |
651 | public function whyNoSession() { |
652 | return null; |
653 | } |
654 | |
655 | /** |
656 | * Most session providers require protection against CSRF attacks (usually via CSRF tokens) |
657 | * |
658 | * @stable to override |
659 | * @return bool false |
660 | */ |
661 | public function safeAgainstCsrf() { |
662 | return false; |
663 | } |
664 | |
665 | /** |
666 | * Returns true if this provider is exempt from autocreate user permissions check |
667 | * |
668 | * By default returns false, meaning this provider respects the normal rights |
669 | * of anonymous user creation. When true the permission checks will be bypassed |
670 | * and the user will always be created (subject to other limitations, like read |
671 | * only db and such). |
672 | * |
673 | * @stable to override |
674 | * @since 1.42 |
675 | */ |
676 | public function canAlwaysAutocreate(): bool { |
677 | return false; |
678 | } |
679 | |
680 | /** |
681 | * Hash data as a session ID |
682 | * |
683 | * Generally this will only be used when self::persistsSessionId() is false and |
684 | * the provider has to base the session ID on the verified user's identity |
685 | * or other static data. The SessionInfo should then typically have the |
686 | * 'forceUse' flag set to avoid persistent session failure if validation of |
687 | * the stored data fails. |
688 | * |
689 | * @param string $data |
690 | * @param string|null $key Defaults to $this->getConfig()->get( MainConfigNames::SecretKey ) |
691 | * @return string |
692 | */ |
693 | final protected function hashToSessionId( $data, $key = null ) { |
694 | if ( !is_string( $data ) ) { |
695 | throw new \InvalidArgumentException( |
696 | '$data must be a string, ' . gettype( $data ) . ' was passed' |
697 | ); |
698 | } |
699 | if ( $key !== null && !is_string( $key ) ) { |
700 | throw new \InvalidArgumentException( |
701 | '$key must be a string or null, ' . gettype( $key ) . ' was passed' |
702 | ); |
703 | } |
704 | |
705 | $hash = \MWCryptHash::hmac( "$this\n$data", |
706 | $key ?: $this->getConfig()->get( MainConfigNames::SecretKey ), false ); |
707 | if ( strlen( $hash ) < 32 ) { |
708 | // Should never happen, even md5 is 128 bits |
709 | // @codeCoverageIgnoreStart |
710 | throw new \UnexpectedValueException( 'Hash function returned less than 128 bits' ); |
711 | // @codeCoverageIgnoreEnd |
712 | } |
713 | if ( strlen( $hash ) >= 40 ) { |
714 | $hash = \Wikimedia\base_convert( $hash, 16, 32, 32 ); |
715 | } |
716 | return substr( $hash, -32 ); |
717 | } |
718 | |
719 | /** |
720 | * Throw an exception, later. Needed because during session initialization the framework |
721 | * isn't quite ready to handle an exception. |
722 | * |
723 | * This should be called from provideSessionInfo() to fail in |
724 | * a user-friendly way when a session mechanism is used in a way it's not supposed to be used |
725 | * (e.g. invalid credentials or a non-API request when the session provider only supports |
726 | * API requests), and the returned SessionInfo should be returned by provideSessionInfo(). |
727 | * |
728 | * @param string $key Key for the error message |
729 | * @param mixed ...$params Parameters as strings. |
730 | * @return SessionInfo An anonymous session info with maximum priority, to force an |
731 | * anonymous session in case throwing the exception doesn't happen. |
732 | */ |
733 | protected function makeException( $key, ...$params ): SessionInfo { |
734 | $msg = wfMessage( $key, $params ); |
735 | |
736 | if ( defined( 'MW_API' ) ) { |
737 | $this->hookContainer->register( |
738 | 'ApiBeforeMain', |
739 | // @phan-suppress-next-line PhanPluginNeverReturnFunction Closures should not get doc |
740 | static function () use ( $msg ) { |
741 | throw ApiUsageException::newWithMessage( null, $msg ); |
742 | } |
743 | ); |
744 | } elseif ( defined( 'MW_REST_API' ) ) { |
745 | // There are no suitable hooks in the REST API (T252591) |
746 | } else { |
747 | $this->hookContainer->register( |
748 | 'BeforeInitialize', |
749 | // @phan-suppress-next-line PhanPluginNeverReturnFunction Closures should not get doc |
750 | static function () use ( $msg ) { |
751 | RequestContext::getMain()->getOutput()->setStatusCode( 400 ); |
752 | throw new ErrorPageError( 'errorpagetitle', $msg ); |
753 | } |
754 | ); |
755 | // Disable file cache, which would be looked up before the BeforeInitialize hook call. |
756 | $this->hookContainer->register( |
757 | 'HTMLFileCache__useFileCache', |
758 | static function () { |
759 | return false; |
760 | } |
761 | ); |
762 | } |
763 | |
764 | $id = $this->hashToSessionId( 'bogus' ); |
765 | return new SessionInfo( SessionInfo::MAX_PRIORITY, [ |
766 | 'provider' => $this, |
767 | 'id' => $id, |
768 | 'userInfo' => UserInfo::newAnonymous(), |
769 | 'persisted' => false, |
770 | ] ); |
771 | } |
772 | |
773 | } |