Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 205 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| SpecialMWOAuthManageMyGrants | |
0.00% |
0 / 205 |
|
0.00% |
0 / 8 |
992 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
| addSubtitleLinks | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
| handleConsumerForm | |
0.00% |
0 / 109 |
|
0.00% |
0 / 1 |
156 | |||
| showConsumerList | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| formatRow | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
12 | |||
| getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Extension\OAuth\Frontend\SpecialPages; |
| 4 | |
| 5 | use MediaWiki\Context\IContextSource; |
| 6 | use MediaWiki\Exception\ErrorPageError; |
| 7 | use MediaWiki\Exception\PermissionsError; |
| 8 | use MediaWiki\Extension\OAuth\Backend\Consumer; |
| 9 | use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance; |
| 10 | use MediaWiki\Extension\OAuth\Backend\Utils; |
| 11 | use MediaWiki\Extension\OAuth\Control\ConsumerAcceptanceAccessControl; |
| 12 | use MediaWiki\Extension\OAuth\Control\ConsumerAcceptanceSubmitControl; |
| 13 | use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl; |
| 14 | use MediaWiki\Extension\OAuth\Control\SubmitControl; |
| 15 | use MediaWiki\Extension\OAuth\Frontend\Pagers\ManageMyGrantsPager; |
| 16 | use MediaWiki\Extension\OAuth\Frontend\UIUtils; |
| 17 | use MediaWiki\HTMLForm\HTMLForm; |
| 18 | use MediaWiki\Json\FormatJson; |
| 19 | use MediaWiki\Permissions\GrantsInfo; |
| 20 | use MediaWiki\Permissions\GrantsLocalization; |
| 21 | use MediaWiki\Permissions\PermissionManager; |
| 22 | use MediaWiki\SpecialPage\SpecialPage; |
| 23 | use MediaWiki\Status\Status; |
| 24 | use stdClass; |
| 25 | use Wikimedia\Rdbms\IDatabase; |
| 26 | |
| 27 | /** |
| 28 | * (c) Aaron Schulz 2013, GPL |
| 29 | * |
| 30 | * @license GPL-2.0-or-later |
| 31 | */ |
| 32 | |
| 33 | /** |
| 34 | * Special page for listing consumers this user granted access to and |
| 35 | * for manage the specific grants given or revoking access for the consumer |
| 36 | */ |
| 37 | class SpecialMWOAuthManageMyGrants extends SpecialPage { |
| 38 | public function __construct( |
| 39 | private readonly GrantsInfo $grantsInfo, |
| 40 | private readonly GrantsLocalization $grantsLocalization, |
| 41 | private readonly PermissionManager $permissionManager, |
| 42 | ) { |
| 43 | parent::__construct( 'OAuthManageMyGrants', 'mwoauthmanagemygrants' ); |
| 44 | } |
| 45 | |
| 46 | /** @inheritDoc */ |
| 47 | public function doesWrites() { |
| 48 | return true; |
| 49 | } |
| 50 | |
| 51 | /** @inheritDoc */ |
| 52 | public function execute( $par ) { |
| 53 | $this->setHeaders(); |
| 54 | $this->getOutput()->disallowUserJs(); |
| 55 | $this->addHelpLink( 'Help:OAuth' ); |
| 56 | $this->requireNamedUser( 'mwoauth-available-only-to-registered' ); |
| 57 | |
| 58 | $user = $this->getUser(); |
| 59 | if ( !$this->permissionManager->userHasRight( $user, 'mwoauthmanagemygrants' ) ) { |
| 60 | throw new PermissionsError( 'mwoauthmanagemygrants' ); |
| 61 | } |
| 62 | |
| 63 | // Format is Special:OAuthManageMyGrants[/list|/manage/<accesstoken>] |
| 64 | $navigation = $par !== null ? explode( '/', $par ) : []; |
| 65 | $typeKey = $navigation[0] ?? null; |
| 66 | $acceptanceId = $navigation[1] ?? null; |
| 67 | |
| 68 | if ( $this->getConfig()->get( 'MWOAuthReadOnly' ) |
| 69 | && in_array( $typeKey, [ 'update', 'revoke' ] ) |
| 70 | ) { |
| 71 | throw new ErrorPageError( 'mwoauth-error', 'mwoauth-db-readonly' ); |
| 72 | } |
| 73 | |
| 74 | switch ( $typeKey ) { |
| 75 | case 'update': |
| 76 | case 'revoke': |
| 77 | $this->handleConsumerForm( $acceptanceId ?? 0, $typeKey ); |
| 78 | break; |
| 79 | default: |
| 80 | $this->showConsumerList(); |
| 81 | break; |
| 82 | } |
| 83 | |
| 84 | $this->addSubtitleLinks( $acceptanceId ); |
| 85 | |
| 86 | $this->getOutput()->addModuleStyles( 'ext.MWOAuth.styles' ); |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Show navigation links |
| 91 | * |
| 92 | * @param string|null $acceptanceId |
| 93 | * @return void |
| 94 | */ |
| 95 | protected function addSubtitleLinks( $acceptanceId ) { |
| 96 | $listLinks = []; |
| 97 | |
| 98 | if ( $acceptanceId ) { |
| 99 | $dbr = Utils::getOAuthDB( DB_REPLICA ); |
| 100 | $cmraAc = ConsumerAcceptance::newFromId( $dbr, (int)$acceptanceId ); |
| 101 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
| 102 | $this->getPageTitle(), |
| 103 | $this->msg( 'mwoauthmanagemygrants-showlist' )->text() |
| 104 | ); |
| 105 | |
| 106 | if ( $cmraAc ) { |
| 107 | $cmrAc = Consumer::newFromId( $dbr, $cmraAc->getConsumerId() ); |
| 108 | $consumerKey = $cmrAc->getConsumerKey(); |
| 109 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
| 110 | SpecialPage::getTitleFor( 'OAuthListConsumers', "view/$consumerKey" ), |
| 111 | $this->msg( 'mwoauthconsumer-application-view' )->text() |
| 112 | ); |
| 113 | } |
| 114 | } else { |
| 115 | $listLinks[] = $this->msg( 'mwoauthmanagemygrants-showlist' )->escaped(); |
| 116 | } |
| 117 | |
| 118 | $linkHtml = $this->getLanguage()->pipeList( $listLinks ); |
| 119 | |
| 120 | $this->getOutput()->setSubtitle( |
| 121 | "<strong>" . $this->msg( 'mwoauthmanagemygrants-navigation' )->escaped() . |
| 122 | "</strong> [{$linkHtml}]" ); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Show the form to approve/reject/disable/re-enable consumers |
| 127 | * |
| 128 | * @param int $acceptanceId |
| 129 | * @param string $type One of (update,revoke) |
| 130 | * @throws PermissionsError |
| 131 | */ |
| 132 | protected function handleConsumerForm( $acceptanceId, $type ) { |
| 133 | $user = $this->getUser(); |
| 134 | $dbr = Utils::getOAuthDB( DB_REPLICA ); |
| 135 | |
| 136 | $centralUserId = Utils::getCentralIdFromLocalUser( $user ); |
| 137 | if ( !$centralUserId ) { |
| 138 | $this->getOutput()->addWikiMsg( 'badaccess-group0' ); |
| 139 | return; |
| 140 | } |
| 141 | |
| 142 | $cmraAc = ConsumerAcceptanceAccessControl::wrap( |
| 143 | ConsumerAcceptance::newFromId( $dbr, $acceptanceId ), $this->getContext() ); |
| 144 | if ( !$cmraAc || $cmraAc->getUserId() !== $centralUserId ) { |
| 145 | $this->getOutput()->addHTML( $this->msg( 'mwoauth-invalid-access-token' )->escaped() ); |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | $cmrAc = ConsumerAccessControl::wrap( |
| 150 | Consumer::newFromId( $dbr, $cmraAc->getConsumerId() ), $this->getContext() ); |
| 151 | if ( $cmrAc->getDeleted() |
| 152 | && !$this->permissionManager->userHasRight( $user, 'mwoauthviewsuppressed' ) ) { |
| 153 | throw new PermissionsError( 'mwoauthviewsuppressed' ); |
| 154 | } |
| 155 | |
| 156 | $action = ''; |
| 157 | if ( $this->getRequest()->getCheck( 'renounce' ) ) { |
| 158 | $action = 'renounce'; |
| 159 | } elseif ( $this->getRequest()->getCheck( 'update' ) ) { |
| 160 | $action = 'update'; |
| 161 | } |
| 162 | |
| 163 | $data = [ 'action' => $action ]; |
| 164 | $control = new ConsumerAcceptanceSubmitControl( |
| 165 | $this->getContext(), $data, $dbr, $cmraAc->getDAO()->getOAuthVersion() |
| 166 | ); |
| 167 | $form = HTMLForm::factory( 'ooui', |
| 168 | $control->registerValidators( [ |
| 169 | 'info' => [ |
| 170 | 'type' => 'info', |
| 171 | 'raw' => true, |
| 172 | 'default' => UIUtils::generateInfoTable( [ |
| 173 | 'mwoauth-consumer-name' => $cmrAc->getNameAndVersion(), |
| 174 | 'mwoauth-consumer-user' => $cmrAc->getUserName(), |
| 175 | 'mwoauth-consumer-description' => $cmrAc->getDescription(), |
| 176 | 'mwoauthmanagemygrants-wikiallowed' => $cmraAc->getWikiName(), |
| 177 | ], $this->getContext() ), |
| 178 | ], |
| 179 | 'grants' => [ |
| 180 | 'type' => 'checkmatrix', |
| 181 | 'label-message' => 'mwoauthmanagemygrants-applicablegrantsallowed', |
| 182 | 'columns' => [ |
| 183 | $this->msg( 'mwoauthmanagemygrants-grantaccept' )->escaped() => 'grant' |
| 184 | ], |
| 185 | 'rows' => array_combine( |
| 186 | array_map( [ $this->grantsLocalization, 'getGrantsLink' ], $cmrAc->getGrants() ), |
| 187 | $cmrAc->getGrants() |
| 188 | ), |
| 189 | 'default' => array_map( |
| 190 | static function ( $g ) { |
| 191 | return "grant-$g"; |
| 192 | }, |
| 193 | $cmraAc->getGrants() |
| 194 | ), |
| 195 | 'tooltips-html' => [ |
| 196 | $this->grantsLocalization->getGrantsLink( 'basic' ) => |
| 197 | $this->msg( 'mwoauthmanagemygrants-basic-tooltip' )->parse(), |
| 198 | $this->grantsLocalization->getGrantsLink( 'mwoauth-authonly' ) => |
| 199 | $this->msg( 'mwoauthmanagemygrants-authonly-tooltip' )->parse(), |
| 200 | $this->grantsLocalization->getGrantsLink( 'mwoauth-authonlyprivate' ) => |
| 201 | $this->msg( 'mwoauthmanagemygrants-authonly-tooltip' )->parse(), |
| 202 | ], |
| 203 | 'force-options-on' => array_map( |
| 204 | static function ( $g ) { |
| 205 | return "grant-$g"; |
| 206 | }, |
| 207 | ( $type === 'revoke' ) |
| 208 | ? array_merge( $this->grantsInfo->getValidGrants(), SubmitControl::getIrrevocableGrants() ) |
| 209 | : SubmitControl::getIrrevocableGrants() |
| 210 | ), |
| 211 | 'validation-callback' => null |
| 212 | ], |
| 213 | 'acceptanceId' => [ |
| 214 | 'type' => 'hidden', |
| 215 | 'default' => $cmraAc->getId(), |
| 216 | ] |
| 217 | ] ), |
| 218 | $this->getContext() |
| 219 | ); |
| 220 | $form->setSubmitCallback( |
| 221 | static function ( array $data, IContextSource $context ) use ( $action, $cmraAc ) { |
| 222 | $data['action'] = $action; |
| 223 | // adapt form to controller |
| 224 | $data['grants'] = FormatJson::encode( |
| 225 | preg_replace( '/^grant-/', '', $data['grants'] ) |
| 226 | ); |
| 227 | |
| 228 | $dbw = Utils::getOAuthDB( DB_PRIMARY ); |
| 229 | $control = new ConsumerAcceptanceSubmitControl( |
| 230 | $context, $data, $dbw, $cmraAc->getDAO()->getOAuthVersion() |
| 231 | ); |
| 232 | return $control->submit(); |
| 233 | } |
| 234 | ); |
| 235 | |
| 236 | $form->setWrapperLegendMsg( 'mwoauthmanagemygrants-confirm-legend' ); |
| 237 | $form->suppressDefaultSubmit(); |
| 238 | if ( $type === 'revoke' ) { |
| 239 | $form->addButton( [ |
| 240 | 'name' => 'renounce', |
| 241 | 'value' => $this->msg( 'mwoauthmanagemygrants-renounce' )->text(), |
| 242 | 'flags' => [ 'primary', 'destructive' ], |
| 243 | ] ); |
| 244 | } else { |
| 245 | $form->addButton( [ |
| 246 | 'name' => 'update', |
| 247 | 'value' => $this->msg( 'mwoauthmanagemygrants-update' )->text(), |
| 248 | 'flags' => [ 'primary', 'progressive' ], |
| 249 | ] ); |
| 250 | } |
| 251 | $form->addPreHtml( |
| 252 | $this->msg( "mwoauthmanagemygrants-$type-text" )->parseAsBlock() ); |
| 253 | |
| 254 | $status = $form->show(); |
| 255 | if ( $status instanceof Status && $status->isOK() ) { |
| 256 | // Messages: mwoauthmanagemygrants-success-update, mwoauthmanagemygrants-success-renounce |
| 257 | $this->getOutput()->addWikiMsg( "mwoauthmanagemygrants-success-$action" ); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * Show a paged list of consumers with links to details |
| 263 | * |
| 264 | * @return void |
| 265 | */ |
| 266 | protected function showConsumerList() { |
| 267 | $this->getOutput()->addWikiMsg( 'mwoauthmanagemygrants-text' ); |
| 268 | |
| 269 | $centralUserId = Utils::getCentralIdFromLocalUser( $this->getUser() ); |
| 270 | $pager = new ManageMyGrantsPager( $this, [], $centralUserId ); |
| 271 | if ( $pager->getNumRows() ) { |
| 272 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
| 273 | $this->getOutput()->addHTML( $pager->getBody() ); |
| 274 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
| 275 | } else { |
| 276 | $this->getOutput()->addWikiMsg( "mwoauthmanagemygrants-none" ); |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * @param IDatabase $db |
| 282 | * @param stdClass $row |
| 283 | * @return string |
| 284 | */ |
| 285 | public function formatRow( IDatabase $db, $row ) { |
| 286 | $cmrAc = ConsumerAccessControl::wrap( |
| 287 | Consumer::newFromRow( $db, $row ), $this->getContext() ); |
| 288 | $cmraAc = ConsumerAcceptanceAccessControl::wrap( |
| 289 | ConsumerAcceptance::newFromRow( $db, $row ), $this->getContext() ); |
| 290 | |
| 291 | $linkRenderer = $this->getLinkRenderer(); |
| 292 | |
| 293 | $links = []; |
| 294 | if ( array_diff( $cmrAc->getGrants(), SubmitControl::getIrrevocableGrants() ) ) { |
| 295 | $links[] = $linkRenderer->makeKnownLink( |
| 296 | $this->getPageTitle( 'update/' . $cmraAc->getId() ), |
| 297 | $this->msg( 'mwoauthmanagemygrants-review' )->text() |
| 298 | ); |
| 299 | } |
| 300 | $links[] = $linkRenderer->makeKnownLink( |
| 301 | $this->getPageTitle( 'revoke/' . $cmraAc->getId() ), |
| 302 | $this->msg( 'mwoauthmanagemygrants-revoke' )->text() |
| 303 | ); |
| 304 | $reviewLinks = $this->getLanguage()->pipeList( $links ); |
| 305 | |
| 306 | $encName = $cmrAc->escapeForHtml( $cmrAc->getNameAndVersion() ); |
| 307 | |
| 308 | $r = '<li class="mw-mwoauthmanagemygrants-list-item">'; |
| 309 | $r .= "<strong dir='ltr'>{$encName}</strong> (<strong>$reviewLinks</strong>)"; |
| 310 | $data = [ |
| 311 | 'mwoauthmanagemygrants-user' => $cmrAc->getUserName(), |
| 312 | 'mwoauthmanagemygrants-wikiallowed' => $cmraAc->getWikiName(), |
| 313 | ]; |
| 314 | |
| 315 | foreach ( $data as $msg => $val ) { |
| 316 | $r .= '<p>' . $this->msg( $msg )->escaped() . ' ' . $cmrAc->escapeForHtml( $val ) . '</p>'; |
| 317 | } |
| 318 | |
| 319 | $editsLink = $linkRenderer->makeKnownLink( |
| 320 | SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ), |
| 321 | $this->msg( 'mwoauthmanagemygrants-editslink', $this->getUser()->getName() )->text(), |
| 322 | [], |
| 323 | [ 'tagfilter' => Utils::getTagName( $cmrAc->getId() ) ] |
| 324 | ); |
| 325 | $r .= '<p>' . $editsLink . '</p>'; |
| 326 | $actionsLink = $linkRenderer->makeKnownLink( |
| 327 | SpecialPage::getTitleFor( 'Log' ), |
| 328 | $this->msg( 'mwoauthmanagemygrants-actionslink', $this->getUser()->getName() )->text(), |
| 329 | [], |
| 330 | [ |
| 331 | 'user' => $this->getUser()->getName(), |
| 332 | 'tagfilter' => Utils::getTagName( $cmrAc->getId() ), |
| 333 | ] |
| 334 | ); |
| 335 | $r .= '<p>' . $actionsLink . '</p>'; |
| 336 | |
| 337 | $r .= '</li>'; |
| 338 | |
| 339 | return $r; |
| 340 | } |
| 341 | |
| 342 | /** @inheritDoc */ |
| 343 | protected function getGroupName() { |
| 344 | return 'login'; |
| 345 | } |
| 346 | } |