MediaWiki  master
SpecialChangeCredentials.php
Go to the documentation of this file.
1 <?php
2 
9 
16  protected static $allowedActions = [ AuthManager::ACTION_CHANGE ];
17 
18  protected static $messagePrefix = 'changecredentials';
19 
21  protected static $loadUserData = true;
22 
26  public function __construct( AuthManager $authManager ) {
27  parent::__construct( 'ChangeCredentials', 'editmyprivateinfo' );
28  $this->setAuthManager( $authManager );
29  }
30 
31  protected function getGroupName() {
32  return 'users';
33  }
34 
35  public function isListed() {
36  $this->loadAuth( '' );
37  return (bool)$this->authRequests;
38  }
39 
40  public function doesWrites() {
41  return true;
42  }
43 
44  protected function getDefaultAction( $subPage ) {
45  return AuthManager::ACTION_CHANGE;
46  }
47 
48  protected function getPreservedParams( $withToken = false ) {
49  $request = $this->getRequest();
50  $params = parent::getPreservedParams( $withToken );
51  $params += [
52  'returnto' => $request->getVal( 'returnto' ),
53  'returntoquery' => $request->getVal( 'returntoquery' ),
54  ];
55  return $params;
56  }
57 
58  public function execute( $subPage ) {
59  $this->setHeaders();
60  $this->outputHeader();
61 
62  $this->loadAuth( $subPage );
63 
64  if ( !$subPage ) {
65  $this->showSubpageList();
66  return;
67  }
68 
69  if ( !$this->authRequests ) {
70  // messages used: changecredentials-invalidsubpage, removecredentials-invalidsubpage
71  $this->showSubpageList( $this->msg( static::$messagePrefix . '-invalidsubpage', $subPage ) );
72  return;
73  }
74 
75  $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() );
76 
77  $status = $this->trySubmit();
78 
79  if ( $status === false || !$status->isOK() ) {
80  $this->displayForm( $status );
81  return;
82  }
83 
84  $response = $status->getValue();
85 
86  switch ( $response->status ) {
87  case AuthenticationResponse::PASS:
88  $this->success();
89  break;
90  case AuthenticationResponse::FAIL:
91  $this->displayForm( Status::newFatal( $response->message ) );
92  break;
93  default:
94  throw new LogicException( 'invalid AuthenticationResponse' );
95  }
96  }
97 
98  protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
99  parent::loadAuth( $subPage, $authAction );
100  if ( $subPage ) {
101  $foundReqs = [];
102  foreach ( $this->authRequests as $req ) {
103  if ( $req->getUniqueId() === $subPage ) {
104  $foundReqs[] = $req;
105  }
106  }
107  if ( count( $foundReqs ) > 1 ) {
108  throw new LogicException( 'Multiple AuthenticationRequest objects with same ID!' );
109  }
110  $this->authRequests = $foundReqs;
111  }
112  }
113 
115  public function onAuthChangeFormFields(
116  array $requests, array $fieldInfo, array &$formDescriptor, $action
117  ) {
118  parent::onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
119 
120  // Add some UI flair for password changes, the most common use case for this page.
121  if ( AuthenticationRequest::getRequestByClass( $this->authRequests,
122  PasswordAuthenticationRequest::class )
123  ) {
124  $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor, [
125  'password' => [
126  'autocomplete' => 'new-password',
127  'placeholder-message' => 'createacct-yourpassword-ph',
128  'help-message' => 'createacct-useuniquepass',
129  ],
130  'retype' => [
131  'autocomplete' => 'new-password',
132  'placeholder-message' => 'createacct-yourpasswordagain-ph',
133  ],
134  // T263927 - the Chromium password form guide recommends always having a username field
135  'username' => [
136  'type' => 'text',
137  'baseField' => 'password',
138  'autocomplete' => 'username',
139  'nodata' => true,
140  'readonly' => true,
141  'cssclass' => 'mw-htmlform-hidden-field',
142  'label-message' => 'userlogin-yourname',
143  'placeholder-message' => 'userlogin-yourname-ph',
144  ],
145  ] );
146  }
147  }
148 
149  protected function getAuthFormDescriptor( $requests, $action ) {
150  if ( !static::$loadUserData ) {
151  return [];
152  } else {
153  $descriptor = parent::getAuthFormDescriptor( $requests, $action );
154 
155  $any = false;
156  foreach ( $descriptor as &$field ) {
157  if ( $field['type'] === 'password' && $field['name'] !== 'retype' ) {
158  $any = true;
159  if ( isset( $field['cssclass'] ) ) {
160  $field['cssclass'] .= ' mw-changecredentials-validate-password';
161  } else {
162  $field['cssclass'] = 'mw-changecredentials-validate-password';
163  }
164  }
165  }
166 
167  if ( $any ) {
168  $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' );
169  }
170 
171  return $descriptor;
172  }
173  }
174 
175  protected function getAuthForm( array $requests, $action ) {
176  $form = parent::getAuthForm( $requests, $action );
177  $req = reset( $requests );
178  $info = $req->describeCredentials();
179 
180  $form->addPreText(
181  Html::openElement( 'dl' )
182  . Html::element( 'dt', [], $this->msg( 'credentialsform-provider' )->text() )
183  . Html::element( 'dd', [], $info['provider']->text() )
184  . Html::element( 'dt', [], $this->msg( 'credentialsform-account' )->text() )
185  . Html::element( 'dd', [], $info['account']->text() )
186  . Html::closeElement( 'dl' )
187  );
188 
189  // messages used: changecredentials-submit removecredentials-submit
190  $form->setSubmitTextMsg( static::$messagePrefix . '-submit' );
191  $form->showCancel()->setCancelTarget( $this->getReturnUrl() ?: Title::newMainPage() );
192 
193  return $form;
194  }
195 
196  protected function needsSubmitButton( array $requests ) {
197  // Change/remove forms show are built from a single AuthenticationRequest and do not allow
198  // for redirect flow; they always need a submit button.
199  return true;
200  }
201 
202  public function handleFormSubmit( $data ) {
203  // remove requests do not accept user input
204  $requests = $this->authRequests;
205  if ( static::$loadUserData ) {
206  $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
207  }
208 
209  $response = $this->performAuthenticationStep( $this->authAction, $requests );
210 
211  // we can't handle FAIL or similar as failure here since it might require changing the form
212  return Status::newGood( $response );
213  }
214 
218  protected function showSubpageList( $error = null ) {
219  $out = $this->getOutput();
220 
221  if ( $error ) {
222  $out->addHTML( $error->parse() );
223  }
224 
225  $groupedRequests = [];
226  foreach ( $this->authRequests as $req ) {
227  $info = $req->describeCredentials();
228  $groupedRequests[$info['provider']->text()][] = $req;
229  }
230 
231  $linkRenderer = $this->getLinkRenderer();
232  $out->addHTML( Html::openElement( 'dl' ) );
233  foreach ( $groupedRequests as $group => $members ) {
234  $out->addHTML( Html::element( 'dt', [], $group ) );
235  foreach ( $members as $req ) {
237  $info = $req->describeCredentials();
238  $out->addHTML( Html::rawElement( 'dd', [],
239  $linkRenderer->makeLink(
240  $this->getPageTitle( $req->getUniqueId() ),
241  $info['account']->text()
242  )
243  ) );
244  }
245  }
246  $out->addHTML( Html::closeElement( 'dl' ) );
247  }
248 
249  protected function success() {
250  $session = $this->getRequest()->getSession();
251  $user = $this->getUser();
252  $out = $this->getOutput();
253  $returnUrl = $this->getReturnUrl();
254 
255  // change user token and update the session
256  SessionManager::singleton()->invalidateSessionsForUser( $user );
257  $session->setUser( $user );
258  $session->resetId();
259 
260  if ( $returnUrl ) {
261  $out->redirect( $returnUrl );
262  } else {
263  // messages used: changecredentials-success removecredentials-success
264  $out->addHtml(
266  $out->msg( static::$messagePrefix . '-success' )->parse()
267  )
268  );
269  $out->returnToMain();
270  }
271  }
272 
276  protected function getReturnUrl() {
277  $request = $this->getRequest();
278  $returnTo = $request->getText( 'returnto' );
279  $returnToQuery = $request->getText( 'returntoquery', '' );
280 
281  if ( !$returnTo ) {
282  return null;
283  }
284 
285  $title = Title::newFromText( $returnTo );
286  return $title->getFullUrlForRedirect( $returnToQuery );
287  }
288 
289  protected function getRequestBlacklist() {
290  return $this->getConfig()->get( MainConfigNames::ChangeCredentialsBlacklist );
291  }
292 }
A special page subclass for authentication-related special pages.
string $authAction
one of the AuthManager::ACTION_* constants.
performAuthenticationStep( $action, array $requests)
displayForm( $status)
Display the form.
AuthenticationRequest[] $authRequests
string $subPage
Subpage of the special page.
static mergeDefaultFormDescriptor(array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor)
Apply defaults to a form descriptor, without creating non-existent fields.
getRequest()
Get the WebRequest being used for this instance.
trySubmit()
Attempts to do an authentication step with the submitted data.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:256
static successBox( $html, $className='')
Return a success box.
Definition: Html.php:800
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:320
This serves as the entry point to the authentication system.
This is a value object for authentication requests.
This is a value object to hold authentication response data.
This is a value object for authentication requests with a username and password.
A class containing constants representing the names of configuration variables.
This serves as the entry point to the MediaWiki session handling system.
Special change to change credentials (such as the password).
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getAuthForm(array $requests, $action)
doesWrites()
Indicates whether this special page may perform database writes.
handleFormSubmit( $data)
Submit handler callback for HTMLForm.
getPreservedParams( $withToken=false)
Returns URL query parameters which can be used to reload the page (or leave and return) while preserv...
isListed()
Whether this special page is listed in Special:SpecialPages.
getRequestBlacklist()
Allows blacklisting certain request types.
getAuthFormDescriptor( $requests, $action)
Generates a HTMLForm descriptor array from a set of authentication requests.
loadAuth( $subPage, $authAction=null, $reset=false)
Load or initialize $authAction, $authRequests and $subPage.
getDefaultAction( $subPage)
Get the default action for this special page, if none is given via URL/POST data.
__construct(AuthManager $authManager)
needsSubmitButton(array $requests)
Returns true if the form built from the given AuthenticationRequests needs a submit button.
static $loadUserData
Change action needs user data; remove action does not.
execute( $subPage)
Default execute method Checks user permissions.
onAuthChangeFormFields(array $requests, array $fieldInfo, array &$formDescriptor, $action)
Change the form descriptor that determines how a field will look in the authentication form....
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
AuthManager null $authManager
Definition: SpecialPage.php:89
LinkRenderer null $linkRenderer
Definition: SpecialPage.php:81
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
setAuthManager(AuthManager $authManager)
Set the injected AuthManager from the special page constructor.
getPageTitle( $subpage=false)
Get a self-referential title object.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:700
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370