Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialChangeEmail
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 14
930
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
 doesWrites
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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getLoginSecurityLevel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkExecutePermissions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getFormFields
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 alterForm
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onSuccess
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 attemptChange
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 requiresUnblock
0.00% covered (danger)
0.00%
0 / 1
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
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use LogicException;
10use MediaWiki\Auth\AuthManager;
11use MediaWiki\Exception\ErrorPageError;
12use MediaWiki\Exception\PermissionsError;
13use MediaWiki\Html\Html;
14use MediaWiki\HTMLForm\HTMLForm;
15use MediaWiki\Logger\LoggerFactory;
16use MediaWiki\Parser\Sanitizer;
17use MediaWiki\SpecialPage\FormSpecialPage;
18use MediaWiki\Status\Status;
19use MediaWiki\Title\Title;
20use MediaWiki\User\User;
21
22/**
23 * Let users change their email address.
24 *
25 * @ingroup SpecialPage
26 */
27class SpecialChangeEmail extends FormSpecialPage {
28    /**
29     * @var Status
30     */
31    private $status;
32
33    public function __construct( AuthManager $authManager ) {
34        parent::__construct( 'ChangeEmail', 'editmyprivateinfo' );
35
36        $this->setAuthManager( $authManager );
37    }
38
39    /** @inheritDoc */
40    public function doesWrites() {
41        return true;
42    }
43
44    /**
45     * @return bool
46     */
47    public function isListed() {
48        return $this->getAuthManager()->allowsPropertyChange( 'emailaddress' );
49    }
50
51    /**
52     * Main execution point
53     * @param string|null $par
54     */
55    public function execute( $par ) {
56        $out = $this->getOutput();
57        $out->disallowUserJs();
58        $out->addModules( 'mediawiki.special.changeemail' );
59        parent::execute( $par );
60    }
61
62    /** @inheritDoc */
63    protected function getLoginSecurityLevel() {
64        return $this->getName();
65    }
66
67    protected function checkExecutePermissions( User $user ) {
68        if ( !$this->getAuthManager()->allowsPropertyChange( 'emailaddress' ) ) {
69            throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
70        }
71
72        $this->requireNamedUser( 'changeemail-no-info', 'exception-nologin', true );
73
74        // This could also let someone check the current email address, so
75        // require both permissions.
76        if ( !$this->getAuthority()->isAllowed( 'viewmyprivateinfo' ) ) {
77            throw new PermissionsError( 'viewmyprivateinfo' );
78        }
79
80        parent::checkExecutePermissions( $user );
81    }
82
83    /** @inheritDoc */
84    protected function getFormFields() {
85        $user = $this->getUser();
86
87        return [
88            'Name' => [
89                'type' => 'info',
90                'label-message' => 'username',
91                'default' => $user->getName(),
92            ],
93            'OldEmail' => [
94                'type' => 'info',
95                'label-message' => 'changeemail-oldemail',
96                'default' => $user->getEmail() ?: $this->msg( 'changeemail-none' )->text(),
97            ],
98            'NewEmail' => [
99                'type' => 'email',
100                'label-message' => 'changeemail-newemail',
101                'autofocus' => true,
102                'maxlength' => 255,
103                'help-message' => 'changeemail-newemail-help',
104            ],
105        ];
106    }
107
108    /** @inheritDoc */
109    protected function getDisplayFormat() {
110        return 'ooui';
111    }
112
113    protected function alterForm( HTMLForm $form ) {
114        $form->setId( 'mw-changeemail-form' );
115        $form->setTableId( 'mw-changeemail-table' );
116        $form->setSubmitTextMsg( 'changeemail-submit' );
117        $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
118
119        $form->addHeaderHtml( $this->msg( 'changeemail-header' )->parseAsBlock() );
120        $form->setSubmitID( 'change_email_submit' );
121    }
122
123    /** @inheritDoc */
124    public function onSubmit( array $data ) {
125        $this->status = $this->attemptChange( $this->getUser(), $data['NewEmail'] );
126
127        return $this->status;
128    }
129
130    public function onSuccess() {
131        $request = $this->getRequest();
132
133        $returnTo = $request->getVal( 'returnto' );
134        $titleObj = $returnTo !== null ? Title::newFromText( $returnTo ) : null;
135        if ( !$titleObj instanceof Title ) {
136            $titleObj = Title::newMainPage();
137        }
138        $query = $request->getVal( 'returntoquery', '' );
139
140        if ( $this->status->value === true ) {
141            $this->getOutput()->redirect( $titleObj->getFullUrlForRedirect( $query ) );
142        } elseif ( $this->status->value === 'eauth' ) {
143            # Notify user that a confirmation email has been sent...
144            $out = $this->getOutput();
145            $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
146            $out->addHTML(
147                Html::warningBox(
148                    $out->msg( 'eauthentsent', $this->getUser()->getName() )->parse()
149                )
150            );
151            // just show the link to go back
152            $this->getOutput()->addReturnTo( $titleObj, wfCgiToArray( $query ) );
153        }
154    }
155
156    /**
157     * @param User $user
158     * @param string $newAddr
159     *
160     * @return Status
161     */
162    private function attemptChange( User $user, $newAddr ) {
163        if ( $newAddr !== '' && !Sanitizer::validateEmail( $newAddr ) ) {
164            return Status::newFatal( 'invalidemailaddress' );
165        }
166
167        $oldAddr = $user->getEmail();
168        if ( $newAddr === $oldAddr ) {
169            return Status::newFatal( 'changeemail-nochange' );
170        }
171
172        if ( strlen( $newAddr ) > 255 ) {
173            return Status::newFatal( 'changeemail-maxlength' );
174        }
175
176        // To prevent spam, rate limit adding a new address, but do
177        // not rate limit removing an address.
178        if ( $newAddr !== '' ) {
179            // Enforce permissions, user blocks, and rate limits
180            $status = $this->authorizeAction( 'changeemail' );
181            if ( !$status->isGood() ) {
182                return Status::wrap( $status );
183            }
184        }
185
186        $userLatest = $user->getInstanceFromPrimary() ?? throw new LogicException( 'No user' );
187        $changeStatus = Status::newGood();
188        if (
189            !$this->getHookRunner()->onUserCanChangeEmail( $userLatest, $oldAddr, $newAddr, $changeStatus )
190            && !$changeStatus->isGood()
191        ) {
192            return $changeStatus;
193        }
194
195        $status = $userLatest->setEmailWithConfirmation( $newAddr );
196        if ( !$status->isGood() ) {
197            return $status;
198        }
199
200        LoggerFactory::getInstance( 'authentication' )->info(
201            'Changing email address for {user} from {oldemail} to {newemail}', [
202                'user' => $userLatest->getName(),
203                'oldemail' => $oldAddr,
204                'newemail' => $newAddr,
205            ]
206        );
207
208        $this->getHookRunner()->onPrefsEmailAudit( $userLatest, $oldAddr, $newAddr );
209
210        $userLatest->saveSettings();
211
212        return $status;
213    }
214
215    /** @inheritDoc */
216    public function requiresUnblock() {
217        return false;
218    }
219
220    /** @inheritDoc */
221    protected function getGroupName() {
222        return 'login';
223    }
224}
225
226/** @deprecated class alias since 1.41 */
227class_alias( SpecialChangeEmail::class, 'SpecialChangeEmail' );