Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
40.32% |
25 / 62 |
|
20.00% |
2 / 10 |
CRAP | |
0.00% |
0 / 1 |
SharedDomainUtils | |
40.32% |
25 / 62 |
|
20.00% |
2 / 10 |
221.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
isSharedDomain | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
shouldRestrictCurrentDomain | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
isSul3Enabled | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
8 | |||
assertSul3Enabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
assertIsSharedDomain | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
assertIsNotSharedDomain | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getUrlForSharedDomainAction | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
shouldUseMobile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
makeUrlDeviceCompliant | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames; |
7 | use MediaWiki\Extension\CentralAuth\Hooks\Handlers\SharedDomainHookHandler; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\Request\WebRequest; |
10 | use MediaWiki\Title\TitleFactory; |
11 | use MobileContext; |
12 | use RuntimeException; |
13 | use Wikimedia\Assert\Assert; |
14 | |
15 | /** |
16 | * Utilities for handling the shared domain name used for SUL3 login. |
17 | * This class is kept lightweight, so it can be used in early hooks. |
18 | */ |
19 | class SharedDomainUtils { |
20 | |
21 | private const SUL3_COOKIE_FLAG = 'sul3OptIn'; |
22 | private Config $config; |
23 | private TitleFactory $titleFactory; |
24 | private ?bool $isSharedDomain = null; |
25 | private ?MobileContext $mobileContext; |
26 | private bool $isApiRequest; |
27 | |
28 | public function __construct( |
29 | Config $config, |
30 | TitleFactory $titleFactory, |
31 | ?MobileContext $mobileContext, |
32 | bool $isApiRequest |
33 | ) { |
34 | $this->config = $config; |
35 | $this->titleFactory = $titleFactory; |
36 | $this->mobileContext = $mobileContext; |
37 | $this->isApiRequest = $isApiRequest; |
38 | } |
39 | |
40 | /** |
41 | * Whether the current request is to the shared domain used for SUL3 login. |
42 | * |
43 | * This assumes: |
44 | * - $wgCentralAuthSharedDomainPrefix contains the shared domain. |
45 | * - $wgCanonicalServer is set in site configuration to the current domain |
46 | * (instead of the actual canonical domain) for requests to the shared domain. |
47 | * |
48 | * @return bool |
49 | */ |
50 | public function isSharedDomain(): bool { |
51 | if ( $this->isSharedDomain === null ) { |
52 | $sharedDomainPrefix = $this->config->get( CAMainConfigNames::CentralAuthSharedDomainPrefix ); |
53 | if ( !$sharedDomainPrefix ) { |
54 | $this->isSharedDomain = false; |
55 | } else { |
56 | $sharedDomain = parse_url( $sharedDomainPrefix, PHP_URL_HOST ); |
57 | $currentDomain = parse_url( |
58 | $this->config->get( MainConfigNames::CanonicalServer ), PHP_URL_HOST |
59 | ); |
60 | $this->isSharedDomain = $sharedDomain && $currentDomain === $sharedDomain; |
61 | } |
62 | } |
63 | return $this->isSharedDomain; |
64 | } |
65 | |
66 | /** |
67 | * Whether the current request must deny non-auth actions. |
68 | * |
69 | * If $wgCentralAuthRestrictSharedDomain is enabled, then requests to the "fake" |
70 | * shared domain within $wgCentralAuthSharedDomainPrefix must only be for authentication |
71 | * purposes. All non-authentication-related actions should be prevented. |
72 | * |
73 | * SUL3 login supports both using a dedicated login wiki for the domain where the central |
74 | * session cookies are stored, and a shared domain which serve any wiki (from a virtual |
75 | * sub directory). In the latter case, we want to prevent non-authentication actions |
76 | * to prevent complications like cache splits. This flag differentiates between the two |
77 | * setups. |
78 | * |
79 | * @return bool |
80 | * @see SharedDomainHookHandler |
81 | */ |
82 | public function shouldRestrictCurrentDomain(): bool { |
83 | return $this->isSharedDomain() && $this->config->get( CAMainConfigNames::CentralAuthRestrictSharedDomain ); |
84 | } |
85 | |
86 | /** |
87 | * Whether SUL3 mode is enabled on this wiki and/or this request. |
88 | * |
89 | * In order to facilitate testing of SUL3 migration, this method |
90 | * provides mechanisms for testing the SUL3 feature including a |
91 | * cookie-based feature flag. |
92 | * |
93 | * SUL3 mode is enabled if any of the following conditions is true: |
94 | * - $wgCentralAuthEnableSul3 contains 'always' |
95 | * - $wgCentralAuthEnableSul3 contains 'cookie' and there is a |
96 | * cookie named 'sul3OptIn' with the value '1' |
97 | * - $wgCentralAuthEnableSul3 contains 'query-flag' and the URL has |
98 | * a query parameter 'usesul3' with the value "1". The value "0" |
99 | * means switch off SUL3 mode. |
100 | * |
101 | * @param WebRequest $request |
102 | * @return bool |
103 | */ |
104 | public function isSul3Enabled( WebRequest $request ): bool { |
105 | // T379816: `clientlogin` API should still work in SUL3 mode as if we're |
106 | // in SUL2 mode regardless of whether SUL3 is enabled or not. This provider |
107 | // should operate the same in both modes when the request is an API request. |
108 | if ( $this->isApiRequest && !$this->isSharedDomain() ) { |
109 | return false; |
110 | } |
111 | |
112 | $sul3Config = $this->config->get( CAMainConfigNames::CentralAuthEnableSul3 ); |
113 | |
114 | if ( in_array( 'query-flag', $sul3Config, true ) |
115 | && $request->getCheck( 'usesul3' ) |
116 | ) { |
117 | return $request->getFuzzyBool( 'usesul3' ); |
118 | } elseif ( in_array( 'cookie', $sul3Config, true ) |
119 | && $request->getCookie( self::SUL3_COOKIE_FLAG, '' ) === '1' |
120 | ) { |
121 | return true; |
122 | } elseif ( in_array( 'always', $sul3Config, true ) ) { |
123 | return true; |
124 | } else { |
125 | return false; |
126 | } |
127 | } |
128 | |
129 | /** |
130 | * Assert that the SUL3 mode is allowed. |
131 | * |
132 | * @param WebRequest $request |
133 | * @return void |
134 | */ |
135 | public function assertSul3Enabled( WebRequest $request ) { |
136 | Assert::precondition( |
137 | $this->isSul3Enabled( $request ), |
138 | 'SUL3 is not enabled. Set $wgCentralAuthEnableSul3 to boolean true.' |
139 | ); |
140 | } |
141 | |
142 | /** |
143 | * Assert that we're on the shared login domain. |
144 | * |
145 | * @return void |
146 | */ |
147 | public function assertIsSharedDomain() { |
148 | Assert::precondition( |
149 | $this->isSharedDomain(), |
150 | 'This action is not allowed because the domain is not the shared login domain.' |
151 | ); |
152 | } |
153 | |
154 | /** |
155 | * Assert that we're not on the shared login domain. |
156 | * |
157 | * @return void |
158 | */ |
159 | public function assertIsNotSharedDomain() { |
160 | Assert::precondition( |
161 | !( $this->isSharedDomain() ), |
162 | 'This action is not allowed because the domain is not the shared login domain.' |
163 | ); |
164 | } |
165 | |
166 | /** |
167 | * Get the login/signup URL on the shared login domain wiki. |
168 | * |
169 | * @param string $action 'login' or 'signup' action |
170 | * @param WebRequest|null $request There could be more to look at |
171 | * in the request like if we're coming from a campaign link. |
172 | * |
173 | * @return string |
174 | */ |
175 | public function getUrlForSharedDomainAction( string $action, ?WebRequest $request = null ): string { |
176 | switch ( $action ) { |
177 | case 'login': |
178 | $localUrl = $this->titleFactory->newFromText( 'Special:UserLogin' )->getLocalURL(); |
179 | break; |
180 | case 'signup': |
181 | $localUrl = $this->titleFactory->newFromText( 'Special:CreateAccount' )->getLocalURL(); |
182 | break; |
183 | default: |
184 | throw new RuntimeException( 'Unknown action: ' . $action ); |
185 | } |
186 | |
187 | $url = $this->makeUrlDeviceCompliant( |
188 | $this->config->get( CAMainConfigNames::CentralAuthSharedDomainPrefix ) . $localUrl |
189 | ); |
190 | |
191 | return wfAppendQuery( $url, [ |
192 | // TODO: Fix T369467 |
193 | 'returnto' => 'Main_Page', |
194 | 'usesul3' => '1', |
195 | 'campaign' => $request ? $request->getRawVal( 'campaign' ) : null, |
196 | ] ); |
197 | } |
198 | |
199 | /** |
200 | * @return bool True if on mobile device |
201 | */ |
202 | public function shouldUseMobile(): bool { |
203 | return $this->mobileContext && $this->mobileContext->shouldDisplayMobileView(); |
204 | } |
205 | |
206 | /** |
207 | * Check the URL and apply transformation based on the device |
208 | * that is currently looking at it. If mobile, apply the mobile |
209 | * transformation to the URL so we view the correct rendering. |
210 | * |
211 | * Get the mobile domain (m.) version of the URL if available |
212 | * configured (in that WMF is currently configured to have separate |
213 | * domain for mobile and desktop versions of sites) and we want that |
214 | * instead of just appending a `useformat` query parameter, if the |
215 | * domain is a mobile domain, just return it but if it's not, we |
216 | * detect that and append a `useformat` query param.. |
217 | * |
218 | * @param string $url |
219 | * |
220 | * @return string |
221 | */ |
222 | public function makeUrlDeviceCompliant( string $url ): string { |
223 | // Assume either all or none of the wikis in the farm have MobileFrontend |
224 | if ( !$this->mobileContext ) { |
225 | return $url; |
226 | } |
227 | |
228 | $mobileUrl = $this->mobileContext->getMobileUrl( $url ); |
229 | // Some wikis don't have separate mobile and desktop versions at different URLs, |
230 | // in which case getMobileUrl() is a no-op. |
231 | $hasMobileUrl = ( $mobileUrl !== $url ); |
232 | |
233 | if ( $this->mobileContext->shouldDisplayMobileView() ) { |
234 | return $hasMobileUrl ? $mobileUrl : wfAppendQuery( $url, [ 'useformat' => 'mobile' ] ); |
235 | } else { |
236 | // useformat=desktop is the default, and so we don't really need to set it, |
237 | // but we want to consider the possibility that the user has previously used |
238 | // the central domain and set it to mobile mode via a cookie. In that case, |
239 | // we want to prioritize the consistency of the current mode over that setting. |
240 | return $hasMobileUrl ? $url : wfAppendQuery( $url, [ 'useformat' => 'desktop' ] ); |
241 | } |
242 | } |
243 | |
244 | } |