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