1<?php 2 3use MediaWiki\Auth\AuthenticationRequest; 4use MediaWiki\Auth\AuthenticationResponse; 5use MediaWiki\Auth\AuthManager; 6use MediaWiki\Session\SessionManager; 7 8/** 9 * Special change to change credentials (such as the password). 10 * 11 * Also does most of the work for SpecialRemoveCredentials. 12 */ 13class SpecialChangeCredentials extends AuthManagerSpecialPage { 14 protected static $allowedActions = [ AuthManager::ACTION_CHANGE ]; 15 16 protected static $messagePrefix = 'changecredentials'; 17 18 /** Change action needs user data; remove action does not */ 19 protected static $loadUserData = true; 20 21 public function __construct( $name = 'ChangeCredentials' ) { 22 parent::__construct( $name, 'editmyprivateinfo' ); 23 } 24 25 protected function getGroupName() { 26 return 'users'; 27 } 28 29 public function isListed() { 30 $this->loadAuth( '' ); 31 return (bool)$this->authRequests; 32 } 33 34 public function doesWrites() { 35 return true; 36 } 37 38 protected function getDefaultAction( $subPage ) { 39 return AuthManager::ACTION_CHANGE; 40 } 41 42 protected function getPreservedParams( $withToken = false ) { 43 $request = $this->getRequest(); 44 $params = parent::getPreservedParams( $withToken ); 45 $params += [ 46 'returnto' => $request->getVal( 'returnto' ), 47 'returntoquery' => $request->getVal( 'returntoquery' ), 48 ]; 49 return $params; 50 } 51 52 public function execute( $subPage ) { 53 $this->setHeaders(); 54 $this->outputHeader(); 55 56 $this->loadAuth( $subPage ); 57 58 if ( !$subPage ) { 59 $this->showSubpageList(); 60 return; 61 } 62 63 if ( !$this->authRequests ) { 64 // messages used: changecredentials-invalidsubpage, removecredentials-invalidsubpage 65 $this->showSubpageList( $this->msg( static::$messagePrefix . '-invalidsubpage', $subPage ) ); 66 return; 67 } 68 69 $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() ); 70 71 $status = $this->trySubmit(); 72 73 if ( $status === false || !$status->isOK() ) { 74 $this->displayForm( $status ); 75 return; 76 } 77 78 $response = $status->getValue(); 79 80 switch ( $response->status ) { 81 case AuthenticationResponse::PASS: 82 $this->success(); 83 break; 84 case AuthenticationResponse::FAIL: 85 $this->displayForm( Status::newFatal( $response->message ) ); 86 break; 87 default: 88 throw new LogicException( 'invalid AuthenticationResponse' ); 89 } 90 } 91 92 protected function loadAuth( $subPage, $authAction = null, $reset = false ) { 93 parent::loadAuth( $subPage, $authAction ); 94 if ( $subPage ) { 95 $foundReqs = []; 96 foreach ( $this->authRequests as $req ) { 97 if ( $req->getUniqueId() === $subPage ) { 98 $foundReqs[] = $req; 99 } 100 } 101 if ( count( $foundReqs ) > 1 ) { 102 throw new LogicException( 'Multiple AuthenticationRequest objects with same ID!' ); 103 } 104 $this->authRequests = $foundReqs; 105 } 106 } 107 108 protected function getAuthFormDescriptor( $requests, $action ) { 109 if ( !static::$loadUserData ) { 110 return []; 111 } else { 112 $descriptor = parent::getAuthFormDescriptor( $requests, $action ); 113 114 $any = false; 115 foreach ( $descriptor as &$field ) { 116 if ( $field['type'] === 'password' && $field['name'] !== 'retype' ) { 117 $any = true; 118 if ( isset( $field['cssclass'] ) ) { 119 $field['cssclass'] .= ' mw-changecredentials-validate-password'; 120 } else { 121 $field['cssclass'] = 'mw-changecredentials-validate-password'; 122 } 123 } 124 } 125 126 if ( $any ) { 127 $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' ); 128 } 129 130 return $descriptor; 131 } 132 } 133 134 protected function getAuthForm( array $requests, $action ) { 135 $form = parent::getAuthForm( $requests, $action ); 136 $req = reset( $requests ); 137 $info = $req->describeCredentials(); 138 139 $form->addPreText( 140 Html::openElement( 'dl' ) 141 . Html::element( 'dt', [], $this->msg( 'credentialsform-provider' )->text() ) 142 . Html::element( 'dd', [], $info['provider'] ) 143 . Html::element( 'dt', [], $this->msg( 'credentialsform-account' )->text() ) 144 . Html::element( 'dd', [], $info['account'] ) 145 . Html::closeElement( 'dl' ) 146 ); 147 148 // messages used: changecredentials-submit removecredentials-submit 149 $form->setSubmitTextMsg( static::$messagePrefix . '-submit' ); 150 $form->showCancel()->setCancelTarget( $this->getReturnUrl() ?: Title::newMainPage() ); 151 152 return $form; 153 } 154 155 protected function needsSubmitButton( array $requests ) { 156 // Change/remove forms show are built from a single AuthenticationRequest and do not allow 157 // for redirect flow; they always need a submit button. 158 return true; 159 } 160 161 public function handleFormSubmit( $data ) { 162 // remove requests do not accept user input 163 $requests = $this->authRequests; 164 if ( static::$loadUserData ) { 165 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data ); 166 } 167 168 $response = $this->performAuthenticationStep( $this->authAction, $requests ); 169 170 // we can't handle FAIL or similar as failure here since it might require changing the form 171 return Status::newGood( $response ); 172 } 173 174 /** 175 * @param Message|null $error 176 */ 177 protected function showSubpageList( $error = null ) { 178 $out = $this->getOutput(); 179 180 if ( $error ) { 181 $out->addHTML( $error->parse() ); 182 } 183 184 $groupedRequests = []; 185 foreach ( $this->authRequests as $req ) { 186 $info = $req->describeCredentials(); 187 $groupedRequests[(string)$info['provider']][] = $req; 188 } 189 190 $linkRenderer = $this->getLinkRenderer(); 191 $out->addHTML( Html::openElement( 'dl' ) ); 192 foreach ( $groupedRequests as $group => $members ) { 193 $out->addHTML( Html::element( 'dt', [], $group ) ); 194 foreach ( $members as $req ) { 195 /** @var AuthenticationRequest $req */ 196 $info = $req->describeCredentials(); 197 $out->addHTML( Html::rawElement( 'dd', [], 198 $linkRenderer->makeLink( 199 $this->getPageTitle( $req->getUniqueId() ), 200 $info['account'] 201 ) 202 ) ); 203 } 204 } 205 $out->addHTML( Html::closeElement( 'dl' ) ); 206 } 207 208 protected function success() { 209 $session = $this->getRequest()->getSession(); 210 $user = $this->getUser(); 211 $out = $this->getOutput(); 212 $returnUrl = $this->getReturnUrl(); 213 214 // change user token and update the session 215 SessionManager::singleton()->invalidateSessionsForUser( $user ); 216 $session->setUser( $user ); 217 $session->resetId(); 218 219 if ( $returnUrl ) { 220 $out->redirect( $returnUrl ); 221 } else { 222 // messages used: changecredentials-success removecredentials-success 223 $out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", static::$messagePrefix 224 . '-success' ); 225 $out->returnToMain(); 226 } 227 } 228 229 /** 230 * @return string|null 231 */ 232 protected function getReturnUrl() { 233 $request = $this->getRequest(); 234 $returnTo = $request->getText( 'returnto' ); 235 $returnToQuery = $request->getText( 'returntoquery', '' ); 236 237 if ( !$returnTo ) { 238 return null; 239 } 240 241 $title = Title::newFromText( $returnTo ); 242 return $title->getFullUrlForRedirect( $returnToQuery ); 243 } 244 245 protected function getRequestBlacklist() { 246 return $this->getConfig()->get( 'ChangeCredentialsBlacklist' ); 247 } 248} 249