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