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