Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 239 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
SpecialMWOAuthListConsumers | |
0.00% |
0 / 239 |
|
0.00% |
0 / 12 |
1892 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
showConsumerInfo | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
210 | |||
showConsumerListForm | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
2 | |||
showConsumerList | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
formatRow | |
0.00% |
0 / 55 |
|
0.00% |
0 / 1 |
30 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addNavigationSubtitle | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
updateLink | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
manageConsumerLink | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
manageMyGrantsLink | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
userGrantedAcceptance | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Frontend\SpecialPages; |
4 | |
5 | /** |
6 | * (c) Aaron Schulz 2013, GPL |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License as published by |
10 | * the Free Software Foundation; either version 2 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU General Public License along |
19 | * with this program; if not, write to the Free Software Foundation, Inc., |
20 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
21 | * http://www.gnu.org/copyleft/gpl.html |
22 | */ |
23 | |
24 | use HTMLForm; |
25 | use LogEventsList; |
26 | use LogPage; |
27 | use MediaWiki\Extension\OAuth\Backend\Consumer; |
28 | use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance; |
29 | use MediaWiki\Extension\OAuth\Backend\Utils; |
30 | use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl; |
31 | use MediaWiki\Extension\OAuth\Frontend\Pagers\ListConsumersPager; |
32 | use MediaWiki\Extension\OAuth\Frontend\UIUtils; |
33 | use MediaWiki\Linker\LinkRenderer; |
34 | use MediaWiki\MediaWikiServices; |
35 | use MediaWiki\Permissions\GrantsLocalization; |
36 | use MediaWiki\SpecialPage\SpecialPage; |
37 | use MediaWiki\User\User; |
38 | use MediaWiki\WikiMap\WikiMap; |
39 | use MWException; |
40 | use OOUI\HtmlSnippet; |
41 | use PermissionsError; |
42 | use stdClass; |
43 | use Wikimedia\Rdbms\IDatabase; |
44 | use Xml; |
45 | |
46 | /** |
47 | * Special page for listing the queue of consumer requests and managing |
48 | * their approval/rejection and also for listing approved/disabled consumers |
49 | */ |
50 | class SpecialMWOAuthListConsumers extends SpecialPage { |
51 | /** @var GrantsLocalization */ |
52 | private $grantsLocalization; |
53 | |
54 | /** |
55 | * @param GrantsLocalization $grantsLocalization |
56 | */ |
57 | public function __construct( GrantsLocalization $grantsLocalization ) { |
58 | parent::__construct( 'OAuthListConsumers' ); |
59 | $this->grantsLocalization = $grantsLocalization; |
60 | } |
61 | |
62 | public function execute( $par ) { |
63 | $this->setHeaders(); |
64 | $this->addHelpLink( 'Help:OAuth' ); |
65 | |
66 | // Format is Special:OAuthListConsumers[/list|/view/[<consumer key>]] |
67 | $navigation = $par !== null ? explode( '/', $par ) : []; |
68 | $type = $navigation[0] ?? null; |
69 | $consumerKey = $navigation[1] ?? ''; |
70 | |
71 | $this->showConsumerListForm(); |
72 | |
73 | switch ( $type ) { |
74 | case 'view': |
75 | $this->showConsumerInfo( $consumerKey ); |
76 | break; |
77 | default: |
78 | $this->showConsumerList(); |
79 | break; |
80 | } |
81 | |
82 | $this->getOutput()->addModuleStyles( 'ext.MWOAuth.styles' ); |
83 | } |
84 | |
85 | /** |
86 | * Show the form to approve/reject/disable/re-enable consumers |
87 | * |
88 | * @param string $consumerKey |
89 | * @throws PermissionsError |
90 | */ |
91 | protected function showConsumerInfo( $consumerKey ) { |
92 | $user = $this->getUser(); |
93 | $out = $this->getOutput(); |
94 | |
95 | if ( !$consumerKey ) { |
96 | $out->addWikiMsg( 'mwoauth-missing-consumer-key' ); |
97 | } |
98 | |
99 | $dbr = Utils::getCentralDB( DB_REPLICA ); |
100 | $cmrAc = ConsumerAccessControl::wrap( |
101 | Consumer::newFromKey( $dbr, $consumerKey ), $this->getContext() ); |
102 | $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); |
103 | |
104 | if ( !$cmrAc ) { |
105 | $out->addWikiMsg( 'mwoauth-invalid-consumer-key' ); |
106 | return; |
107 | } elseif ( $cmrAc->getDeleted() |
108 | && !$permissionManager->userHasRight( $user, 'mwoauthviewsuppressed' ) ) { |
109 | throw new PermissionsError( 'mwoauthviewsuppressed' ); |
110 | } |
111 | |
112 | $grants = $cmrAc->getGrants(); |
113 | if ( $grants === [ 'mwoauth-authonly' ] || $grants === [ 'mwoauth-authonlyprivate' ] ) { |
114 | $s = $this->msg( 'grant-' . $grants[0] )->plain(); |
115 | } elseif ( $grants === [ 'basic' ] ) { |
116 | $s = $this->msg( 'mwoauthlistconsumers-basicgrantsonly' )->plain(); |
117 | } else { |
118 | $s = $this->grantsLocalization->getGrantsWikiText( $grants, $this->getLanguage() ); |
119 | } |
120 | |
121 | $stageKey = Consumer::$stageNames[$cmrAc->getDAO()->getStage()]; |
122 | $data = [ |
123 | 'mwoauthlistconsumers-name' => $cmrAc->getName(), |
124 | 'mwoauthlistconsumers-version' => $cmrAc->getVersion(), |
125 | 'mwoauth-oauth-version' => $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 |
126 | ? $this->msg( 'mwoauth-oauth-version-2' ) |
127 | : $this->msg( 'mwoauth-oauth-version-1' ), |
128 | 'mwoauthlistconsumers-user' => $cmrAc->getUserName(), |
129 | 'mwoauthlistconsumers-status' => $this->msg( "mwoauthlistconsumers-status-$stageKey" ), |
130 | 'mwoauthlistconsumers-description' => $cmrAc->getDescription(), |
131 | 'mwoauthlistconsumers-owner-only' => $cmrAc->getDAO()->getOwnerOnly() |
132 | ? $this->msg( 'htmlform-yes' ) : $this->msg( 'htmlform-no' ), |
133 | 'mwoauthlistconsumers-wiki' => $cmrAc->getWikiName(), |
134 | 'mwoauthlistconsumers-callbackurl' => $cmrAc->getCallbackUrl(), |
135 | 'mwoauthlistconsumers-callbackisprefix' => $cmrAc->getCallbackIsPrefix() ? |
136 | $this->msg( 'htmlform-yes' ) : $this->msg( 'htmlform-no' ), |
137 | 'mwoauthlistconsumers-grants' => new HtmlSnippet( $out->parseInlineAsInterface( $s ) ), |
138 | ]; |
139 | if ( $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ) { |
140 | $data += [ |
141 | 'mwoauthlistconsumers-oauth2-is-confidential' => $cmrAc->isConfidential() ? |
142 | $this->msg( 'htmlform-yes' ) : $this->msg( 'htmlform-no' ), |
143 | ]; |
144 | } |
145 | |
146 | $out->addHTML( UIUtils::generateInfoTable( $data, $this->getContext() ) ); |
147 | |
148 | $rcLink = $this->getLinkRenderer()->makeKnownLink( |
149 | SpecialPage::getTitleFor( 'Recentchanges' ), |
150 | $this->msg( 'mwoauthlistconsumers-rclink' )->plain(), |
151 | [], |
152 | [ 'tagfilter' => Utils::getTagName( $cmrAc->getId() ) ] |
153 | ); |
154 | $out->addHTML( "<p>$rcLink</p>" ); |
155 | |
156 | $this->addNavigationSubtitle( $cmrAc ); |
157 | |
158 | if ( Utils::isCentralWiki() ) { |
159 | // Show all of the status updates |
160 | $logPage = new LogPage( 'mwoauthconsumer' ); |
161 | $out->addHTML( Xml::element( 'h2', null, $logPage->getName()->text() ) ); |
162 | LogEventsList::showLogExtract( $out, 'mwoauthconsumer', '', '', [ |
163 | 'conds' => [ |
164 | 'ls_field' => 'OAuthConsumer', |
165 | 'ls_value' => $cmrAc->getConsumerKey(), |
166 | ], |
167 | 'flags' => LogEventsList::NO_EXTRA_USER_LINKS, |
168 | ] ); |
169 | } |
170 | } |
171 | |
172 | /** |
173 | * Show a form for the paged list of consumers |
174 | */ |
175 | protected function showConsumerListForm() { |
176 | $form = HTMLForm::factory( 'ooui', |
177 | [ |
178 | 'name' => [ |
179 | 'name' => 'name', |
180 | 'type' => 'text', |
181 | 'label-message' => 'mwoauth-consumer-name', |
182 | 'required' => false, |
183 | ], |
184 | 'publisher' => [ |
185 | 'name' => 'publisher', |
186 | 'type' => 'text', |
187 | 'label-message' => 'mwoauth-consumer-user', |
188 | 'required' => false |
189 | ], |
190 | 'stage' => [ |
191 | 'name' => 'stage', |
192 | 'type' => 'select', |
193 | 'label-message' => 'mwoauth-consumer-stage', |
194 | 'options' => [ |
195 | $this->msg( 'mwoauth-consumer-stage-any' )->escaped() => -1, |
196 | $this->msg( 'mwoauth-consumer-stage-proposed' )->escaped() |
197 | => Consumer::STAGE_PROPOSED, |
198 | $this->msg( 'mwoauth-consumer-stage-approved' )->escaped() |
199 | => Consumer::STAGE_APPROVED, |
200 | $this->msg( 'mwoauth-consumer-stage-rejected' )->escaped() |
201 | => Consumer::STAGE_REJECTED, |
202 | $this->msg( 'mwoauth-consumer-stage-disabled' )->escaped() |
203 | => Consumer::STAGE_DISABLED, |
204 | $this->msg( 'mwoauth-consumer-stage-expired' )->escaped() |
205 | => Consumer::STAGE_EXPIRED |
206 | ], |
207 | 'default' => Consumer::STAGE_APPROVED, |
208 | 'required' => false |
209 | ] |
210 | ], |
211 | $this->getContext() |
212 | ); |
213 | // always go back to listings |
214 | $form->setAction( $this->getPageTitle()->getFullURL() ); |
215 | $form->setSubmitCallback( static function () { |
216 | return false; |
217 | } ); |
218 | $form->setMethod( 'get' ); |
219 | $form->setSubmitTextMsg( 'go' ); |
220 | $form->setWrapperLegendMsg( 'mwoauthlistconsumers-legend' ); |
221 | $form->show(); |
222 | } |
223 | |
224 | /** |
225 | * Show a paged list of consumers with links to details |
226 | */ |
227 | protected function showConsumerList() { |
228 | $request = $this->getRequest(); |
229 | |
230 | $name = $request->getVal( 'name', '' ); |
231 | $stage = $request->getInt( 'stage', Consumer::STAGE_APPROVED ); |
232 | if ( $request->getVal( 'publisher', '' ) !== '' ) { |
233 | $centralId = Utils::getCentralIdFromUserName( $request->getVal( 'publisher' ) ); |
234 | } else { |
235 | $centralId = null; |
236 | } |
237 | |
238 | $pager = new ListConsumersPager( $this, [], $name, $centralId, $stage ); |
239 | if ( $pager->getNumRows() ) { |
240 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
241 | $this->getOutput()->addHTML( $pager->getBody() ); |
242 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
243 | } else { |
244 | // Messages: mwoauthlistconsumers-none-proposed, mwoauthlistconsumers-none-rejected, |
245 | // mwoauthlistconsumers-none-expired, mwoauthlistconsumers-none-approved, |
246 | // mwoauthlistconsumers-none-disabled |
247 | $this->getOutput()->addWikiMsg( "mwoauthlistconsumers-none" ); |
248 | } |
249 | # Every 30th view, prune old deleted items |
250 | if ( mt_rand( 0, 29 ) == 0 ) { |
251 | Utils::runAutoMaintenance( Utils::getCentralDB( DB_PRIMARY ) ); |
252 | } |
253 | } |
254 | |
255 | /** |
256 | * @param IDatabase $db |
257 | * @param stdClass $row |
258 | * @return string |
259 | */ |
260 | public function formatRow( IDatabase $db, $row ) { |
261 | $cmrAc = ConsumerAccessControl::wrap( |
262 | Consumer::newFromRow( $db, $row ), $this->getContext() ); |
263 | $cmrKey = $cmrAc->getConsumerKey(); |
264 | $stageKey = Consumer::$stageNames[$cmrAc->getStage()]; |
265 | $permMgr = MediaWikiServices::getInstance()->getPermissionManager(); |
266 | |
267 | $links = []; |
268 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
269 | $this->getPageTitle( "view/{$cmrKey}" ), |
270 | $this->msg( 'mwoauthlistconsumers-view' )->text(), |
271 | [], |
272 | $this->getRequest()->getValues( 'name', 'publisher', 'stage' ) |
273 | ); |
274 | if ( $permMgr->userHasRight( $this->getUser(), 'mwoauthmanageconsumer' ) ) { |
275 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
276 | SpecialPage::getTitleFor( 'OAuthManageConsumers', $cmrKey ), |
277 | $this->msg( 'mwoauthmanageconsumers-review' )->text() |
278 | ); |
279 | } |
280 | $links = $this->getLanguage()->pipeList( $links ); |
281 | |
282 | $encStageKey = htmlspecialchars( $stageKey ); |
283 | $r = "<li class=\"mw-mwoauthlistconsumers-{$encStageKey}\">"; |
284 | |
285 | $name = $cmrAc->getNameAndVersion(); |
286 | $r .= '<strong>' . $cmrAc->escapeForHtml( $name ) . '</strong> ' . $this->msg( 'parentheses' ) |
287 | ->rawParams( "<strong>{$links}</strong>" )->escaped(); |
288 | |
289 | $lang = $this->getLanguage(); |
290 | $data = [ |
291 | 'mwoauth-oauth-version' => $cmrAc->escapeForHtml( |
292 | $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 |
293 | ? $this->msg( 'mwoauth-oauth-version-2' ) |
294 | : $this->msg( 'mwoauth-oauth-version-1' ) |
295 | ), |
296 | 'mwoauthlistconsumers-user' => $cmrAc->escapeForHtml( $cmrAc->getUserName() ), |
297 | 'mwoauthlistconsumers-description' => $cmrAc->escapeForHtml( |
298 | $cmrAc->get( 'description', static function ( $s ) use ( $lang ) { |
299 | return $lang->truncateForVisual( $s, 10024 ); |
300 | } ) |
301 | ), |
302 | 'mwoauthlistconsumers-wiki' => $cmrAc->escapeForHtml( $cmrAc->getWikiName() ), |
303 | 'mwoauthlistconsumers-status' => |
304 | $this->msg( "mwoauthlistconsumers-status-$stageKey" )->escaped(), |
305 | ]; |
306 | if ( $cmrAc->getDAO()->getOwnerOnly() ) { |
307 | $data = wfArrayInsertAfter( $data, [ |
308 | 'mwoauthlistconsumers-owner-only' => $this->msg( 'htmlform-yes' ), |
309 | ], 'mwoauthlistconsumers-description' ); |
310 | } |
311 | |
312 | foreach ( $data as $msg => $encValue ) { |
313 | $r .= '<p>' . $this->msg( $msg )->escaped() . ': ' . $encValue . '</p>'; |
314 | } |
315 | |
316 | $rcLink = $this->getLinkRenderer()->makeKnownLink( |
317 | SpecialPage::getTitleFor( 'Recentchanges' ), |
318 | $this->msg( 'mwoauthlistconsumers-rclink' )->plain(), |
319 | [], |
320 | [ 'tagfilter' => Utils::getTagName( $cmrAc->getId() ) ] |
321 | ); |
322 | $r .= '<p>' . $rcLink . '</p>'; |
323 | |
324 | $r .= '</li>'; |
325 | |
326 | return $r; |
327 | } |
328 | |
329 | protected function getGroupName() { |
330 | return 'users'; |
331 | } |
332 | |
333 | /** |
334 | * @param ConsumerAccessControl $cmrAc |
335 | * @throws MWException |
336 | */ |
337 | private function addNavigationSubtitle( ConsumerAccessControl $cmrAc ): void { |
338 | $user = $this->getUser(); |
339 | $centralUserId = Utils::getCentralIdFromLocalUser( $user ); |
340 | $linkRenderer = $this->getLinkRenderer(); |
341 | $consumer = $cmrAc->getDAO(); |
342 | |
343 | $siteLinks = array_merge( |
344 | $this->updateLink( $cmrAc, $centralUserId, $linkRenderer ), |
345 | $this->manageConsumerLink( $consumer, $user, $linkRenderer ), |
346 | $this->manageMyGrantsLink( $consumer, $centralUserId, $linkRenderer ) |
347 | ); |
348 | |
349 | if ( $siteLinks ) { |
350 | $links = $this->getLanguage()->pipeList( $siteLinks ); |
351 | $this->getOutput()->setSubtitle( |
352 | "<strong>" . $this->msg( 'mwoauthlistconsumers-navigation' )->escaped() . |
353 | "</strong> [{$links}]" ); |
354 | } |
355 | } |
356 | |
357 | /** |
358 | * @param ConsumerAccessControl $cmrAc |
359 | * @param int $centralUserId Add update link for this user id, if they can update the consumer |
360 | * @param LinkRenderer $linkRenderer |
361 | * @return string[] |
362 | * @throws MWException |
363 | */ |
364 | private function updateLink( |
365 | ConsumerAccessControl $cmrAc, $centralUserId, |
366 | LinkRenderer $linkRenderer |
367 | ): array { |
368 | if ( Utils::isCentralWiki() && $cmrAc->getDAO()->getUserId() === $centralUserId ) { |
369 | return [ |
370 | $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'OAuthConsumerRegistration', |
371 | 'update/' . $cmrAc->getDAO()->getConsumerKey() ), |
372 | $this->msg( 'mwoauthlistconsumers-update-link' )->text() ) |
373 | ]; |
374 | } |
375 | |
376 | return []; |
377 | } |
378 | |
379 | /** |
380 | * @param Consumer $consumer |
381 | * @param User $user |
382 | * @param LinkRenderer $linkRenderer |
383 | * @return string[] |
384 | * @throws MWException |
385 | */ |
386 | private function manageConsumerLink( |
387 | Consumer $consumer, User $user, LinkRenderer $linkRenderer |
388 | ): array { |
389 | $permMgr = MediaWikiServices::getInstance()->getPermissionManager(); |
390 | |
391 | if ( Utils::isCentralWiki() && $permMgr->userHasRight( $user, 'mwoauthmanageconsumer' ) ) { |
392 | return [ |
393 | $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'OAuthManageConsumers', |
394 | $consumer->getConsumerKey() ), |
395 | $this->msg( 'mwoauthlistconsumers-manage-link' )->text() ) |
396 | ]; |
397 | } |
398 | |
399 | return []; |
400 | } |
401 | |
402 | /** |
403 | * @param Consumer $consumer |
404 | * @param int $centralUserId Add link to manage grants for this user, if they've granted this |
405 | * consumer |
406 | * @param LinkRenderer $linkRenderer |
407 | * @return string[] |
408 | * @throws MWException |
409 | */ |
410 | private function manageMyGrantsLink( |
411 | Consumer $consumer, $centralUserId, LinkRenderer $linkRenderer |
412 | ): array { |
413 | $acceptance = $this->userGrantedAcceptance( $consumer, $centralUserId ); |
414 | if ( $acceptance !== false ) { |
415 | return [ |
416 | $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'OAuthManageMyGrants', |
417 | 'update/' . $acceptance->getId() ), |
418 | $this->msg( 'mwoauthlistconsumers-grants-link' )->text() ) |
419 | ]; |
420 | } |
421 | |
422 | return []; |
423 | } |
424 | |
425 | /** |
426 | * @param Consumer $consumer |
427 | * @param int $centralUserId UserId to retrieve the grants for |
428 | * @return bool|ConsumerAcceptance |
429 | */ |
430 | private function userGrantedAcceptance( Consumer $consumer, $centralUserId ) { |
431 | $dbr = Utils::getCentralDB( DB_REPLICA ); |
432 | $wikiSpecificGrant = |
433 | ConsumerAcceptance::newFromUserConsumerWiki( |
434 | $dbr, $centralUserId, $consumer, WikiMap::getCurrentWikiId() ); |
435 | |
436 | $allWikiGrant = ConsumerAcceptance::newFromUserConsumerWiki( |
437 | $dbr, $centralUserId, $consumer, '*' ); |
438 | |
439 | if ( $wikiSpecificGrant !== false ) { |
440 | return $wikiSpecificGrant; |
441 | } |
442 | if ( $allWikiGrant !== false ) { |
443 | return $allWikiGrant; |
444 | } |
445 | return false; |
446 | } |
447 | } |