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 Language;
29use MediaWiki\Config\Config;
30use MediaWiki\Context\RequestContext;
31use MediaWiki\HookContainer\HookContainer;
32use MediaWiki\HookContainer\HookRunner;
33use MediaWiki\MainConfigNames;
34use MediaWiki\Request\WebRequest;
35use MediaWiki\User\User;
36use MediaWiki\User\UserNameUtils;
37use MWRestrictions;
38use 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 */
86abstract 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}