Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
ImmutableSessionProviderWithCookie
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
8 / 8
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getSessionIdFromCookie
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 persistsSessionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canChangeUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 persistSession
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 unpersistSession
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getVaryCookies
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
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 MediaWiki\MainConfigNames;
27use MediaWiki\Request\WebRequest;
28
29/**
30 * An ImmutableSessionProviderWithCookie doesn't persist the user, but
31 * optionally can use a cookie to support multiple IDs per session.
32 *
33 * As mentioned in the documentation for SessionProvider, many methods that are
34 * technically "cannot persist ID" could be turned into "can persist ID but
35 * not changing User" using a session cookie. This class implements such an
36 * optional session cookie.
37 *
38 * @stable to extend
39 * @ingroup Session
40 * @since 1.27
41 */
42abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
43
44    /** @var string|null */
45    protected $sessionCookieName = null;
46    /** @var mixed[] */
47    protected $sessionCookieOptions = [];
48
49    /**
50     * @stable to call
51     * @param array $params Keys include:
52     *  - sessionCookieName: Session cookie name, if multiple sessions per
53     *    client are to be supported.
54     *  - sessionCookieOptions: Options to pass to WebResponse::setCookie().
55     */
56    public function __construct( $params = [] ) {
57        parent::__construct();
58
59        if ( isset( $params['sessionCookieName'] ) ) {
60            if ( !is_string( $params['sessionCookieName'] ) ) {
61                throw new \InvalidArgumentException( 'sessionCookieName must be a string' );
62            }
63            $this->sessionCookieName = $params['sessionCookieName'];
64        }
65        if ( isset( $params['sessionCookieOptions'] ) ) {
66            if ( !is_array( $params['sessionCookieOptions'] ) ) {
67                throw new \InvalidArgumentException( 'sessionCookieOptions must be an array' );
68            }
69            $this->sessionCookieOptions = $params['sessionCookieOptions'];
70        }
71    }
72
73    /**
74     * Get the session ID from the cookie, if any.
75     *
76     * Only call this if $this->sessionCookieName !== null. If
77     * sessionCookieName is null, do some logic (probably involving a call to
78     * $this->hashToSessionId()) to create the single session ID corresponding
79     * to this WebRequest instead of calling this method.
80     *
81     * @param WebRequest $request
82     * @return string|null
83     */
84    protected function getSessionIdFromCookie( WebRequest $request ) {
85        if ( $this->sessionCookieName === null ) {
86            throw new \BadMethodCallException(
87                __METHOD__ . ' may not be called when $this->sessionCookieName === null'
88            );
89        }
90
91        $prefix = $this->sessionCookieOptions['prefix']
92            ?? $this->getConfig()->get( MainConfigNames::CookiePrefix );
93        $id = $request->getCookie( $this->sessionCookieName, $prefix );
94        return SessionManager::validateSessionId( $id ) ? $id : null;
95    }
96
97    /**
98     * @inheritDoc
99     * @stable to override
100     */
101    public function persistsSessionId() {
102        return $this->sessionCookieName !== null;
103    }
104
105    /**
106     * @inheritDoc
107     * @stable to override
108     */
109    public function canChangeUser() {
110        return false;
111    }
112
113    /**
114     * @inheritDoc
115     * @stable to override
116     */
117    public function persistSession( SessionBackend $session, WebRequest $request ) {
118        if ( $this->sessionCookieName === null ) {
119            return;
120        }
121
122        $response = $request->response();
123        if ( $response->headersSent() ) {
124            // Can't do anything now
125            $this->logger->debug( __METHOD__ . ': Headers already sent' );
126            return;
127        }
128
129        $options = $this->sessionCookieOptions;
130        if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
131            // Send a cookie unless $wgForceHTTPS is set (T256095)
132            if ( !$this->getConfig()->get( MainConfigNames::ForceHTTPS ) ) {
133                $response->setCookie( 'forceHTTPS', 'true', null,
134                    [ 'prefix' => '', 'secure' => false ] + $options );
135            }
136            $options['secure'] = true;
137        }
138
139        $response->setCookie( $this->sessionCookieName, $session->getId(), null, $options );
140    }
141
142    /**
143     * @inheritDoc
144     * @stable to override
145     */
146    public function unpersistSession( WebRequest $request ) {
147        if ( $this->sessionCookieName === null ) {
148            return;
149        }
150
151        $response = $request->response();
152        if ( $response->headersSent() ) {
153            // Can't do anything now
154            $this->logger->debug( __METHOD__ . ': Headers already sent' );
155            return;
156        }
157
158        $response->clearCookie( $this->sessionCookieName, $this->sessionCookieOptions );
159    }
160
161    /**
162     * @inheritDoc
163     * @stable to override
164     */
165    public function getVaryCookies() {
166        if ( $this->sessionCookieName === null ) {
167            return [];
168        }
169
170        $prefix = $this->sessionCookieOptions['prefix'] ??
171            $this->getConfig()->get( MainConfigNames::CookiePrefix );
172        return [ $prefix . $this->sessionCookieName ];
173    }
174
175    public function whyNoSession() {
176        return wfMessage( 'sessionprovider-nocookies' );
177    }
178}