Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalRenameRequest
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 17
1560
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
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
 userCanExecute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 displayRestrictionError
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 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 isGlobalUser
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 alterForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preHtml
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getFormFields
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
20
 suggestedUsername
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 validateNewname
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 validateEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onSubmit
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 onSuccess
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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 * @section LICENSE
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 */
21
22namespace MediaWiki\Extension\CentralAuth\Special;
23
24use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameDenylist;
25use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest;
26use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore;
27use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
28use MediaWiki\HTMLForm\HTMLForm;
29use MediaWiki\Message\Message;
30use MediaWiki\Parser\Sanitizer;
31use MediaWiki\SpecialPage\FormSpecialPage;
32use MediaWiki\Status\Status;
33use MediaWiki\User\User;
34use MediaWiki\User\UserNameUtils;
35use MediaWiki\WikiMap\WikiMap;
36use PermissionsError;
37use Wikimedia\Rdbms\IDBAccessObject;
38
39/**
40 * Request an account rename.
41 *
42 * @author Bryan Davis <bd808@wikimedia.org>
43 * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
44 */
45class SpecialGlobalRenameRequest extends FormSpecialPage {
46
47    private GlobalRenameDenylist $globalRenameDenylist;
48    private UserNameUtils $userNameUtils;
49    private GlobalRenameRequestStore $globalRenameRequestStore;
50
51    public function __construct(
52        GlobalRenameDenylist $globalRenameDenylist,
53        UserNameUtils $userNameUtils,
54        GlobalRenameRequestStore $globalRenameRequestStore
55    ) {
56        parent::__construct( 'GlobalRenameRequest' );
57        $this->globalRenameDenylist = $globalRenameDenylist;
58        $this->userNameUtils = $userNameUtils;
59        $this->globalRenameRequestStore = $globalRenameRequestStore;
60    }
61
62    public function doesWrites() {
63        return true;
64    }
65
66    /** @inheritDoc */
67    public function userCanExecute( User $user ) {
68        return $this->globalRenameDenylist->checkUser( $user->getName() );
69    }
70
71    public function displayRestrictionError() {
72        throw new PermissionsError( null, [ 'centralauth-badaccess-blacklisted' ] );
73    }
74
75    /**
76     * @param string|null $par Subpage string if one was specified
77     */
78    public function execute( $par ) {
79        $this->requireNamedUser();
80        $this->addHelpLink( 'Help:Extension:CentralAuth/Global_rename' );
81
82        switch ( $par ) {
83            case 'status':
84                // Render status page
85                $user = $this->getUser();
86                $username = $user->getName();
87                $wiki = $this->isGlobalUser() ? null : WikiMap::getCurrentWikiId();
88                $pending = $this->globalRenameRequestStore->newForUser(
89                    $username, $wiki
90                );
91                if ( !$pending->exists() ) {
92                    $this->getOutput()->redirect( $this->getPageTitle()->getFullURL(), 303 );
93                    return;
94                }
95                $out = $this->getOutput();
96                $out->setPageTitleMsg(
97                    $this->msg( 'globalrenamerequest-status-title' )
98                );
99                $out->addWikiMsg( 'globalrenamerequest-status-text',
100                    $username, $pending->getNewName()
101                );
102                break;
103
104            case 'available':
105                // TODO: ajax name availability check (T72623)
106                break;
107
108            default:
109                // Request form
110                $out = $this->getOutput();
111                $user = $this->getUser();
112                $wiki = $this->isGlobalUser() ? null : WikiMap::getCurrentWikiId();
113
114                $pending = $this->globalRenameRequestStore->newForUser(
115                    $user->getName(), $wiki
116                );
117                if ( $pending->exists() ) {
118                    $out->redirect(
119                        $this->getPageTitle( 'status' )->getFullURL(), '303'
120                    );
121                    return;
122                }
123                parent::execute( $par );
124                break;
125        }
126    }
127
128    /**
129     * Is the current user a global user?
130     * @return bool
131     */
132    protected function isGlobalUser() {
133        $user = $this->getUser();
134        $causer = CentralAuthUser::getInstance( $user );
135        return $causer->exists() && $causer->isAttached();
136    }
137
138    /**
139     * @param HTMLForm $form
140     */
141    protected function alterForm( HTMLForm $form ) {
142        $form->setSubmitTextMsg( 'globalrenamerequest-submit-text' );
143    }
144
145    /**
146     * @return string
147     */
148    protected function getDisplayFormat() {
149        return 'ooui';
150    }
151
152    /**
153     * @return string
154     */
155    protected function preHtml() {
156        $msg = $this->msg( 'globalrenamerequest-pretext' )->parse();
157        if ( !$this->isGlobalUser() ) {
158            $msg = $this->msg( 'globalrenamerequest-forced' )->parse() . $msg;
159        }
160        return $msg;
161    }
162
163    /**
164     * @return array[]
165     */
166    public function getFormFields() {
167        $suggestedUsername = $this->suggestedUsername();
168        if ( $suggestedUsername !== false ) {
169            $suggestedHelp = [
170                'globalrenamerequest-newname-help',
171                $suggestedUsername,
172            ];
173        } else {
174            // Help message if we couldn't generate a suggested username
175            $suggestedHelp = 'globalrenamerequest-newname-help-basic';
176        }
177        $fields = [
178            'username' => [
179                'cssclass'      => 'mw-globalrenamerequest-field',
180                'csshelpclass'  => 'mw-globalrenamerequest-help',
181                'default'       => $this->getUser()->getName(),
182                'label-message' => 'globalrenamerequest-username-label',
183                'type'          => 'info',
184            ],
185            'newname' => [
186                'cssclass'     => 'mw-globalrenamerequest-field',
187                'csshelpclass' => 'mw-globalrenamerequest-help',
188                'default'      => $this->getRequest()->getVal( 'newname', $this->par ),
189                'id'            => 'mw-renamerequest-newname',
190                'label-message' => 'globalrenamerequest-newname-label',
191                'name'          => 'newname',
192                'required'      => true,
193                'type'          => 'text',
194                'help-message'  => $suggestedHelp,
195                'validation-callback' => [ $this, 'validateNewname' ],
196            ],
197        ];
198
199        $currentEmail = $this->getUser()->getEmail();
200        if ( $currentEmail === '' ) {
201            $fields['email'] = [
202                'cssclass'      => 'mw-globalrenamerequest-field',
203                'csshelpclass'  => 'mw-globalrenamerequest-help',
204                'default'       => $this->getRequest()->getVal( 'email', $this->par ),
205                'id'            => 'mw-renamerequest-email',
206                'label-message' => 'globalrenamerequest-email-label',
207                'name'          => 'email',
208                'placeholder'   => 'username@example.com',
209                'required'      => true,
210                'type'          => 'email',
211                'help-message'  => 'globalrenamerequest-email-why-explain',
212            ];
213            $fields['email2'] = [
214                'cssclass'      => 'mw-globalrenamerequest-field',
215                'csshelpclass'  => 'mw-globalrenamerequest-help',
216                'default'       => $this->getRequest()->getVal( 'email2', $this->par ),
217                'id'            => 'mw-renamerequest-email2',
218                'label-message' => 'globalrenamerequest-email2-label',
219                'name'          => 'email2',
220                'placeholder'   => 'username@example.com',
221                'required'      => true,
222                'type'          => 'email',
223                'help-message'  => 'globalrenamerequest-email2-help',
224                'validation-callback' => [ $this, 'validateEmail' ],
225            ];
226        }
227
228        if ( $this->isGlobalUser() ) {
229            $fields['reason'] = [
230                'cssclass'      => 'mw-globalrenamerequest-field',
231                'default'       => $this->getRequest()->getVal( 'reason', $this->par ),
232                'id'            => 'mw-renamerequest-reason',
233                'label-message' => 'globalrenamerequest-reason-label',
234                'name'          => 'reason',
235                'required'      => true,
236                'rows'          => 5,
237                'type'          => 'textarea',
238            ];
239        }
240        return $fields;
241    }
242
243    /**
244     * Generate a username that appears to be globally available that an
245     * unimaginative user could use if they like.
246     *
247     * @return string|bool false if can't generate a username
248     */
249    protected function suggestedUsername() {
250        // Only allow 5 tries (T260865)
251        $counter = 0;
252        // Whether we found a username that is available to use
253        $found = false;
254        $rand = '';
255        while ( !$found && $counter < 5 ) {
256            $rand = $this->getUser()->getName() . rand( 123, 999 );
257            $found = GlobalRenameRequest::isNameAvailable( $rand, IDBAccessObject::READ_NORMAL )->isOK();
258            $counter++;
259        }
260        if ( $found ) {
261            return $rand;
262        } else {
263            return false;
264        }
265    }
266
267    /**
268     * @param string|array $value The value the field was submitted with
269     * @param array $alldata The data collected from the form
270     * @param HTMLForm $form
271     * @return bool|Message True on success, or String error to display, or
272     *   false to fail validation without displaying an error.
273     */
274    public function validateNewname( $value, $alldata, HTMLForm $form ) {
275        if ( $value === null ) {
276            // Not submitted yet
277            return true;
278        }
279        $check = GlobalRenameRequest::isNameAvailable( $value );
280        return $check->isGood() ? true : $check->getMessage();
281    }
282
283    /**
284     * @param string|array $value The value the field was submitted with
285     * @param array $alldata The data collected from the form
286     * @param HTMLForm $form
287     * @return bool|Message True on success, or String error to display, or
288     *   false to fail validation without displaying an error.
289     */
290    public function validateEmail( $value, $alldata, HTMLForm $form ) {
291        if ( $alldata['email'] !== $alldata['email2'] ) {
292            return $this->msg( 'globalrenamerequest-email-mismatch' );
293        } elseif ( !Sanitizer::validateEmail( $alldata['email'] ) ) {
294            return $this->msg( 'globalrenamerequest-email-invalid' );
295        }
296        return true;
297    }
298
299    /**
300     * @param array $data
301     * @return Status
302     */
303    public function onSubmit( array $data ) {
304        $wiki = $this->isGlobalUser() ? null : WikiMap::getCurrentWikiId();
305        $reason = $data['reason'] ?? null;
306        $safeName = $this->userNameUtils->getCanonical( $data['newname'], UserNameUtils::RIGOR_CREATABLE );
307
308        $request = $this->globalRenameRequestStore->newBlankRequest();
309        $request->setName( $this->getUser()->getName() );
310        $request->setWiki( $wiki );
311        $request->setNewName( $safeName );
312        $request->setReason( $reason );
313
314        if ( $this->globalRenameRequestStore->save( $request ) ) {
315            $status = Status::newGood();
316
317            if ( isset( $data['email'] ) ) {
318                $user = $this->getUser();
319                $user->setEmail( $data['email'] );
320                $user->saveSettings();
321                $status = $user->sendConfirmationMail( 'set' );
322            }
323        } else {
324            $status = Status::newFatal(
325                $this->msg( 'globalrenamerequest-save-error' )
326            );
327        }
328        return $status;
329    }
330
331    public function onSuccess() {
332        $this->getOutput()->redirect(
333            $this->getPageTitle( 'status' )->getFullURL(), '303'
334        );
335    }
336
337    /**
338     * Even blocked users should be able to ask for a rename.
339     * @return bool
340     */
341    public function requiresUnblock() {
342        return false;
343    }
344
345    /** @inheritDoc */
346    protected function getGroupName() {
347        return 'login';
348    }
349}