Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 185 |
|
0.00% |
0 / 25 |
CRAP | |
0.00% |
0 / 1 |
Utils | |
0.00% |
0 / 185 |
|
0.00% |
0 / 25 |
4830 | |
0.00% |
0 / 1 |
isCentralWiki | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCentralWiki | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCentralDB | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getSessionCache | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getNonceCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getConsumerStateCounts | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getHeaders | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
hasOAuthHeaders | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
runAutoMaintenance | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
getWikiIdName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getAllWikiNames | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
newMWOAuthServer | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
newMWOAuthDataStore | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getCentralUserNameFromId | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
getLocalUserFromCentralId | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getCentralIdFromLocalUser | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
getCentralIdFromUserName | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
hmacDBSecret | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getCentralUserTalk | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
grantsAreValid | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
locateUsersToNotify | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
110 | |||
getTagName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isReservedTagName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOAuthAdmins | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Backend; |
4 | |
5 | use BagOStuff; |
6 | use MediaWiki\Deferred\AutoCommitUpdate; |
7 | use MediaWiki\Deferred\DeferredUpdates; |
8 | use MediaWiki\Extension\Notifications\Model\Event; |
9 | use MediaWiki\Extension\OAuth\Lib\OAuthSignatureMethod_HMAC_SHA1; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Request\WebRequest; |
12 | use MediaWiki\Title\Title; |
13 | use MediaWiki\User\CentralId\CentralIdLookup; |
14 | use MediaWiki\User\User; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use MWException; |
17 | use ObjectCache; |
18 | use RequestContext; |
19 | use Wikimedia\Rdbms\IDatabase; |
20 | |
21 | /** |
22 | * Static utility functions for OAuth |
23 | * |
24 | * @file |
25 | * @ingroup OAuth |
26 | */ |
27 | class Utils { |
28 | /** |
29 | * @return bool |
30 | */ |
31 | public static function isCentralWiki() { |
32 | global $wgMWOAuthCentralWiki; |
33 | |
34 | return ( WikiMap::getCurrentWikiId() === $wgMWOAuthCentralWiki ); |
35 | } |
36 | |
37 | /** |
38 | * @return string|bool |
39 | */ |
40 | public static function getCentralWiki() { |
41 | global $wgMWOAuthCentralWiki; |
42 | |
43 | return $wgMWOAuthCentralWiki; |
44 | } |
45 | |
46 | /** |
47 | * @param int $index DB_PRIMARY/DB_REPLICA |
48 | * @return IDatabase |
49 | */ |
50 | public static function getCentralDB( $index ) { |
51 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
52 | |
53 | // T244415: Use the primary database if there were changes |
54 | if ( $index === DB_REPLICA && $lbFactory->hasOrMadeRecentPrimaryChanges() ) { |
55 | $index = DB_PRIMARY; |
56 | } |
57 | $wikiId = self::getCentralWiki(); |
58 | if ( WikiMap::isCurrentWikiId( $wikiId ) ) { |
59 | $wikiId = false; |
60 | } |
61 | |
62 | return $lbFactory->getMainLB( $wikiId )->getConnection( |
63 | $index, [], $wikiId ); |
64 | } |
65 | |
66 | /** |
67 | * @return BagOStuff |
68 | */ |
69 | public static function getSessionCache() { |
70 | global $wgMWOAuthSessionCacheType; |
71 | global $wgSessionCacheType; |
72 | |
73 | $sessionCacheType = $wgMWOAuthSessionCacheType ?? $wgSessionCacheType; |
74 | return ObjectCache::getInstance( $sessionCacheType ); |
75 | } |
76 | |
77 | /** |
78 | * Get the cache type for OAuth 1.0 nonces |
79 | * @return BagOStuff |
80 | */ |
81 | public static function getNonceCache() { |
82 | global $wgMWOAuthNonceCacheType, $wgMWOAuthSessionCacheType, $wgSessionCacheType; |
83 | |
84 | $cacheType = $wgMWOAuthNonceCacheType |
85 | ?? $wgMWOAuthSessionCacheType ?? $wgSessionCacheType; |
86 | return ObjectCache::getInstance( $cacheType ); |
87 | } |
88 | |
89 | /** |
90 | * @param IDatabase $db |
91 | * @return int[] |
92 | */ |
93 | public static function getConsumerStateCounts( IDatabase $db ) { |
94 | $res = $db->newSelectQueryBuilder() |
95 | ->select( [ 'oarc_stage', 'count' => 'COUNT(*)' ] ) |
96 | ->from( 'oauth_registered_consumer' ) |
97 | ->groupBy( 'oarc_stage' ) |
98 | ->caller( __METHOD__ ) |
99 | ->fetchResultSet(); |
100 | $table = [ |
101 | Consumer::STAGE_APPROVED => 0, |
102 | Consumer::STAGE_DISABLED => 0, |
103 | Consumer::STAGE_EXPIRED => 0, |
104 | Consumer::STAGE_PROPOSED => 0, |
105 | Consumer::STAGE_REJECTED => 0, |
106 | ]; |
107 | foreach ( $res as $row ) { |
108 | $table[(int)$row->oarc_stage] = (int)$row->count; |
109 | } |
110 | return $table; |
111 | } |
112 | |
113 | /** |
114 | * Get request headers. |
115 | * Sanitizes the output of apache_request_headers because |
116 | * we always want the keys to be Cased-Like-This and arh() |
117 | * returns the headers in the same case as they are in the |
118 | * request |
119 | * @return array Header name => value |
120 | */ |
121 | public static function getHeaders() { |
122 | $request = RequestContext::getMain()->getRequest(); |
123 | $headers = $request->getAllHeaders(); |
124 | |
125 | $out = []; |
126 | foreach ( $headers as $key => $value ) { |
127 | $key = str_replace( |
128 | " ", |
129 | "-", |
130 | ucwords( strtolower( str_replace( "-", " ", $key ) ) ) |
131 | ); |
132 | $out[$key] = $value; |
133 | } |
134 | return $out; |
135 | } |
136 | |
137 | /** |
138 | * Test this request for an OAuth Authorization header |
139 | * @param WebRequest $request the MediaWiki request |
140 | * @return bool true if a header was found |
141 | */ |
142 | public static function hasOAuthHeaders( WebRequest $request ) { |
143 | $header = $request->getHeader( 'Authorization' ); |
144 | |
145 | return $header !== false && strpos( $header, 'OAuth ' ) === 0; |
146 | } |
147 | |
148 | /** |
149 | * Make a cache key for the given arguments, that (hopefully) won't clash with |
150 | * anything else in your cache |
151 | * @param string ...$args |
152 | * @return string |
153 | */ |
154 | public static function getCacheKey( ...$args ) { |
155 | global $wgMWOAuthCentralWiki; |
156 | |
157 | return "OAUTH:$wgMWOAuthCentralWiki:" . implode( ':', $args ); |
158 | } |
159 | |
160 | /** |
161 | * @param IDatabase $dbw |
162 | * @return void |
163 | */ |
164 | public static function runAutoMaintenance( IDatabase $dbw ) { |
165 | global $wgMWOAuthRequestExpirationAge; |
166 | |
167 | if ( $wgMWOAuthRequestExpirationAge <= 0 ) { |
168 | return; |
169 | } |
170 | |
171 | $cutoff = time() - $wgMWOAuthRequestExpirationAge; |
172 | $fname = __METHOD__; |
173 | DeferredUpdates::addUpdate( |
174 | new AutoCommitUpdate( |
175 | $dbw, |
176 | __METHOD__, |
177 | static function ( IDatabase $dbw ) use ( $cutoff, $fname ) { |
178 | $dbw->newUpdateQueryBuilder() |
179 | ->update( 'oauth_registered_consumer' ) |
180 | ->set( [ |
181 | 'oarc_stage' => Consumer::STAGE_EXPIRED, |
182 | 'oarc_stage_timestamp' => $dbw->timestamp() |
183 | ] ) |
184 | ->where( [ |
185 | 'oarc_stage' => Consumer::STAGE_PROPOSED, |
186 | $dbw->expr( 'oarc_stage_timestamp', '<', $dbw->timestamp( $cutoff ) ) |
187 | ] ) |
188 | ->caller( $fname ) |
189 | ->execute(); |
190 | } |
191 | ) |
192 | ); |
193 | } |
194 | |
195 | /** |
196 | * Get the pretty name of an OAuth wiki ID restriction value |
197 | * |
198 | * @param string $wikiId A wiki ID or '*' |
199 | * @return string |
200 | */ |
201 | public static function getWikiIdName( $wikiId ) { |
202 | if ( $wikiId === '*' ) { |
203 | return wfMessage( 'mwoauth-consumer-allwikis' )->text(); |
204 | } |
205 | |
206 | $host = WikiMap::getWikiName( $wikiId ); |
207 | if ( strpos( $host, '.' ) ) { |
208 | // e.g. "en.wikipedia.org" |
209 | return $host; |
210 | } |
211 | |
212 | return $wikiId; |
213 | } |
214 | |
215 | /** |
216 | * Get the pretty names of all local wikis |
217 | * |
218 | * @return string[] associative array of local wiki names indexed by wiki ID |
219 | */ |
220 | public static function getAllWikiNames() { |
221 | global $wgConf; |
222 | $wikiNames = []; |
223 | foreach ( $wgConf->getLocalDatabases() as $dbname ) { |
224 | $name = self::getWikiIdName( $dbname ); |
225 | if ( $name != $dbname ) { |
226 | $wikiNames[$dbname] = $name; |
227 | } |
228 | } |
229 | return $wikiNames; |
230 | } |
231 | |
232 | /** |
233 | * Quickly get a new server with all the default configurations |
234 | * |
235 | * @return MWOAuthServer with default configurations |
236 | */ |
237 | public static function newMWOAuthServer() { |
238 | $store = static::newMWOAuthDataStore(); |
239 | $server = new MWOAuthServer( $store ); |
240 | $server->add_signature_method( new OAuthSignatureMethod_HMAC_SHA1() ); |
241 | $server->add_signature_method( new MWOAuthSignatureMethod_RSA_SHA1( $store ) ); |
242 | |
243 | return $server; |
244 | } |
245 | |
246 | public static function newMWOAuthDataStore() { |
247 | $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); |
248 | $dbr = self::getCentralDB( DB_REPLICA ); |
249 | $dbw = $lb->getServerCount() > 1 ? self::getCentralDB( DB_PRIMARY ) : null; |
250 | return new MWOAuthDataStore( $dbr, $dbw, self::getSessionCache(), self::getNonceCache() ); |
251 | } |
252 | |
253 | /** |
254 | * Given a central wiki user ID, get a central username |
255 | * |
256 | * @param int $userId |
257 | * @param bool|User|string $audience show hidden names based on this user, or false for public |
258 | * @throws MWException |
259 | * @return string|bool Username, false if not found, empty string if name is hidden |
260 | */ |
261 | public static function getCentralUserNameFromId( $userId, $audience = false ) { |
262 | global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; |
263 | |
264 | // global ID required via hook |
265 | if ( $wgMWOAuthSharedUserIDs ) { |
266 | $lookup = MediaWikiServices::getInstance() |
267 | ->getCentralIdLookupFactory() |
268 | ->getLookup( $wgMWOAuthSharedUserSource ); |
269 | $name = $lookup->nameFromCentralId( |
270 | $userId, |
271 | $audience === 'raw' |
272 | ? CentralIdLookup::AUDIENCE_RAW |
273 | : ( $audience ?: CentralIdLookup::AUDIENCE_PUBLIC ) |
274 | ); |
275 | if ( $name === null ) { |
276 | $name = false; |
277 | } |
278 | } else { |
279 | $name = ''; |
280 | $user = User::newFromId( $userId ); |
281 | $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); |
282 | |
283 | if ( $audience === 'raw' |
284 | || !$user->isHidden() |
285 | || ( $audience instanceof User && $permissionManager->userHasRight( $audience, 'hideuser' ) ) |
286 | ) { |
287 | $name = $user->getName(); |
288 | } |
289 | } |
290 | |
291 | return $name; |
292 | } |
293 | |
294 | /** |
295 | * Given a central wiki user ID, get a local User object |
296 | * |
297 | * @param int $userId |
298 | * @return User|false False if not found |
299 | */ |
300 | public static function getLocalUserFromCentralId( $userId ) { |
301 | global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; |
302 | |
303 | // global ID required via hook |
304 | if ( $wgMWOAuthSharedUserIDs ) { |
305 | $lookup = MediaWikiServices::getInstance() |
306 | ->getCentralIdLookupFactory() |
307 | ->getLookup( $wgMWOAuthSharedUserSource ); |
308 | $user = $lookup->localUserFromCentralId( $userId ); |
309 | if ( $user === null || !$lookup->isAttached( $user ) ) { |
310 | return false; |
311 | } |
312 | return User::newFromIdentity( $user ); |
313 | } |
314 | |
315 | return User::newFromId( $userId ); |
316 | } |
317 | |
318 | /** |
319 | * Given a local User object, get the user ID for that user on the central wiki |
320 | * |
321 | * @param User $user |
322 | * @return int|bool ID or false if not found |
323 | */ |
324 | public static function getCentralIdFromLocalUser( User $user ) { |
325 | global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; |
326 | |
327 | // global ID required via hook |
328 | if ( $wgMWOAuthSharedUserIDs ) { |
329 | // T227688 do not rely on array auto-creation for non-stdClass |
330 | if ( !isset( $user->oAuthUserData ) ) { |
331 | $user->oAuthUserData = []; |
332 | } |
333 | |
334 | if ( isset( $user->oAuthUserData['centralId'] ) ) { |
335 | $id = $user->oAuthUserData['centralId']; |
336 | } else { |
337 | $lookup = MediaWikiServices::getInstance() |
338 | ->getCentralIdLookupFactory() |
339 | ->getLookup( $wgMWOAuthSharedUserSource ); |
340 | if ( !$lookup->isAttached( $user ) ) { |
341 | $id = false; |
342 | } else { |
343 | $id = $lookup->centralIdFromLocalUser( $user ); |
344 | if ( $id === 0 ) { |
345 | $id = false; |
346 | } |
347 | } |
348 | // Process cache the result to avoid queries |
349 | $user->oAuthUserData['centralId'] = $id; |
350 | } |
351 | } else { |
352 | $id = $user->getId(); |
353 | } |
354 | |
355 | return $id; |
356 | } |
357 | |
358 | /** |
359 | * Given a username, get the user ID for that user on the central wiki. |
360 | * @param string $username |
361 | * @return int|bool ID or false if not found |
362 | */ |
363 | public static function getCentralIdFromUserName( $username ) { |
364 | global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; |
365 | |
366 | // global ID required via hook |
367 | if ( $wgMWOAuthSharedUserIDs ) { |
368 | $lookup = MediaWikiServices::getInstance() |
369 | ->getCentralIdLookupFactory() |
370 | ->getLookup( $wgMWOAuthSharedUserSource ); |
371 | $id = $lookup->centralIdFromName( $username ); |
372 | if ( $id === 0 ) { |
373 | $id = false; |
374 | } |
375 | } else { |
376 | $id = false; |
377 | $user = User::newFromName( $username ); |
378 | if ( $user instanceof User && $user->getId() > 0 ) { |
379 | $id = $user->getId(); |
380 | } |
381 | } |
382 | |
383 | return $id; |
384 | } |
385 | |
386 | /** |
387 | * Get the effective secret key/token to use for OAuth purposes. |
388 | * |
389 | * For example, the "secret key" and "access secret" values that are |
390 | * used for authenticating request should be the result of applying this |
391 | * function to the respective values stored in the DB. This means that |
392 | * a leak of DB values is not enough to impersonate consumers. |
393 | * |
394 | * @param string $secret |
395 | * @return string |
396 | */ |
397 | public static function hmacDBSecret( $secret ) { |
398 | global $wgOAuthSecretKey, $wgSecretKey; |
399 | |
400 | $secretKey = $wgOAuthSecretKey ?? $wgSecretKey; |
401 | |
402 | return $secretKey ? hash_hmac( 'sha1', $secret, $secretKey ) : $secret; |
403 | } |
404 | |
405 | /** |
406 | * Get a link to the central wiki's user talk page of a user. |
407 | * |
408 | * @param string $username the username of the User Talk link |
409 | * @return string the (proto-relative, urlencoded) url of the central wiki's user talk page |
410 | */ |
411 | public static function getCentralUserTalk( $username ) { |
412 | global $wgMWOAuthCentralWiki, $wgMWOAuthSharedUserIDs; |
413 | |
414 | if ( $wgMWOAuthSharedUserIDs ) { |
415 | $url = WikiMap::getForeignURL( |
416 | $wgMWOAuthCentralWiki, |
417 | "User_talk:$username" |
418 | ); |
419 | } else { |
420 | $url = Title::makeTitleSafe( NS_USER_TALK, $username )->getFullURL(); |
421 | } |
422 | return $url; |
423 | } |
424 | |
425 | /** |
426 | * @param array $grants |
427 | * @return bool |
428 | */ |
429 | public static function grantsAreValid( array $grants ) { |
430 | // Remove our special grants before calling the core method |
431 | $grants = array_diff( $grants, [ 'mwoauth-authonly', 'mwoauth-authonlyprivate' ] ); |
432 | return MediaWikiServices::getInstance() |
433 | ->getGrantsInfo() |
434 | ->grantsAreValid( $grants ); |
435 | } |
436 | |
437 | /** |
438 | * Given an OAuth consumer stage change event, find out who needs to be notified. |
439 | * Will be used as an EchoAttributeManager::ATTR_LOCATORS callback. |
440 | * @param Event $event |
441 | * @return User[] |
442 | */ |
443 | public static function locateUsersToNotify( Event $event ) { |
444 | $agent = $event->getAgent(); |
445 | $owner = self::getLocalUserFromCentralId( $event->getExtraParam( 'owner-id' ) ); |
446 | |
447 | $users = []; |
448 | switch ( $event->getType() ) { |
449 | case 'oauth-app-propose': |
450 | // notify OAuth admins about new proposed apps |
451 | $oauthAdmins = self::getOAuthAdmins(); |
452 | foreach ( $oauthAdmins as $admin ) { |
453 | if ( $admin->equals( $owner ) ) { |
454 | continue; |
455 | } |
456 | $users[$admin->getId()] = $admin; |
457 | } |
458 | break; |
459 | case 'oauth-app-update': |
460 | case 'oauth-app-approve': |
461 | case 'oauth-app-reject': |
462 | case 'oauth-app-disable': |
463 | case 'oauth-app-reenable': |
464 | // notify owner if someone else changed the status of the app |
465 | if ( !$owner->equals( $agent ) ) { |
466 | $users[$owner->getId()] = $owner; |
467 | } |
468 | break; |
469 | } |
470 | return $users; |
471 | } |
472 | |
473 | /** |
474 | * Get the change tag name for a given consumer. |
475 | * @param int $consumerId |
476 | * @return string |
477 | */ |
478 | public static function getTagName( $consumerId ) { |
479 | return 'OAuth CID: ' . (int)$consumerId; |
480 | } |
481 | |
482 | /** |
483 | * Check if a given change tag name should be reserved for this extension. |
484 | * @param string $tagName |
485 | * @return bool |
486 | */ |
487 | public static function isReservedTagName( $tagName ) { |
488 | return stripos( $tagName, 'oauth cid:' ) === 0; |
489 | } |
490 | |
491 | /** |
492 | * Return a list of all OAuth admins (or the first 5000 in the unlikely case that there is more |
493 | * than that). |
494 | * Should be called on the central OAuth wiki. |
495 | * @return User[] |
496 | */ |
497 | protected static function getOAuthAdmins() { |
498 | global $wgOAuthGroupsToNotify; |
499 | |
500 | if ( !$wgOAuthGroupsToNotify ) { |
501 | return []; |
502 | } |
503 | |
504 | return iterator_to_array( User::findUsersByGroup( $wgOAuthGroupsToNotify ) ); |
505 | } |
506 | } |