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