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 ErrorPageError;
27use InvalidArgumentException;
28use MediaWiki\Api\ApiUsageException;
29use MediaWiki\Config\Config;
30use MediaWiki\Context\RequestContext;
31use MediaWiki\HookContainer\HookContainer;
32use MediaWiki\HookContainer\HookRunner;
33use MediaWiki\Language\Language;
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;
42use Wikimedia\Message\MessageParam;
43use 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 */
91abstract 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}