Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.35% covered (danger)
4.35%
5 / 115
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalRenameUserJob
4.35% covered (danger)
4.35%
5 / 115
16.67% covered (danger)
16.67%
1 / 6
485.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 doRun
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
90
 promoteToGlobal
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 movePages
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
12
 done
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 escapeReplacement
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob;
4
5use LogicException;
6use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest;
7use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\RenameUser\RenameuserSQL;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12use MediaWiki\WikiMap\WikiMap;
13use Wikimedia\Rdbms\IExpression;
14use Wikimedia\Rdbms\LikeValue;
15
16/**
17 * Job class to rename a user locally
18 * This is intended to be run on each wiki individually
19 */
20class LocalRenameUserJob extends LocalRenameJob {
21
22    /**
23     * @param Title $title
24     * @param array $params An associative array of options:
25     *   from - old username
26     *   to - new username
27     *   force - try to do the rename even if the old username is invalid
28     *   renamer - whom the renaming should be attributed in logs
29     *   reason - reason to use in the rename log
30     *   movepages - move user / user talk pages and their subpages
31     *   suppressredirects - when moving pages, suppress redirects
32     *   reattach - after rename, attach the local account. When used, should be set to
33     *     [ wiki ID => [ 'attachedMethod' => method, 'attachedTimestamp' => timestamp ].
34     *     See CentralAuthUser::queryAttached. (default: false)
35     *   promotetoglobal - globalize the new user account (default: false)
36     *   session - session data from RequestContext::exportSession, for checkuser data
37     *   ignorestatus - ignore update status, run the job even if it seems like another job
38     *     is already working on it
39     */
40    public function __construct( $title, $params ) {
41        $this->command = 'LocalRenameUserJob';
42
43        // For back-compat
44        if ( !isset( $params['promotetoglobal'] ) ) {
45            $params['promotetoglobal'] = false;
46        }
47        if ( !isset( $params['reason'] ) ) {
48            $params['reason'] = '';
49        }
50        if ( !isset( $params['reattach'] ) ) {
51            $params['reattach'] = false;
52        }
53        if ( !isset( $params['type'] ) ) {
54            $params['type'] = GlobalRenameRequest::RENAME;
55        }
56
57        parent::__construct( $title, $params );
58    }
59
60    /** @inheritDoc */
61    public function doRun( $fnameTrxOwner ) {
62        $from = $this->params['from'];
63        $to = $this->params['to'];
64
65        $this->updateStatus( 'inprogress' );
66        $services = MediaWikiServices::getInstance();
67        // Make the status update visible to all other transactions immediately
68        $factory = $services->getDBLoadBalancerFactory();
69        $factory->commitPrimaryChanges( $fnameTrxOwner );
70
71        if ( isset( $this->params['force'] ) && $this->params['force'] ) {
72            // If we're dealing with an invalid username, load the data ourselves to avoid
73            // any normalization at all done by User or Title.
74            $userQuery = User::getQueryInfo();
75            $row = $services->getConnectionProvider()->getPrimaryDatabase()->newSelectQueryBuilder()
76                ->tables( $userQuery['tables'] )
77                ->select( $userQuery['fields'] )
78                ->where( [ 'user_name' => $from ] )
79                ->joinConds( $userQuery['joins'] )
80                ->caller( __METHOD__ )
81                ->fetchRow();
82            $oldUser = User::newFromRow( $row );
83        } else {
84            $oldUser = User::newFromName( $from );
85        }
86
87        $rename = new RenameuserSQL(
88            $from,
89            $to,
90            $oldUser->getId(),
91            $this->getRenameUser(),
92            [
93                'checkIfUserExists' => false,
94                'debugPrefix' => 'GlobalRename',
95                'reason' => $this->params['reason'],
96            ]
97        );
98        if ( !$rename->rename() ) {
99            // This should never happen!
100            // If it does happen, the user will be locked out of their account
101            // until a sysadmin intervenes...
102            throw new LogicException( 'RenameuserSQL::rename returned false.' );
103        }
104
105        if ( $this->params['type'] === GlobalRenameRequest::VANISH ) {
106            // $renamedUser should always be truthy, otherwise the exception
107            // above would be thrown
108            $renamedUser = $services->getUserFactory()->newFromName( $to );
109            if ( $renamedUser ) {
110                User::newSystemUser( $renamedUser->getName(), [ 'steal' => true ] );
111            }
112        }
113
114        if ( $this->params['reattach'] ) {
115            $caUser = CentralAuthUser::getInstanceByName( $this->params['to'] );
116            $wikiId = WikiMap::getCurrentWikiId();
117            $details = $this->params['reattach'][$wikiId];
118            $caUser->attach(
119                $wikiId,
120                $details['attachedMethod'],
121                false,
122                $details['attachedTimestamp']
123            );
124        }
125
126        if ( $this->params['movepages'] ) {
127            $this->movePages( $oldUser );
128        }
129
130        if ( $this->params['promotetoglobal'] ) {
131            $this->promoteToGlobal();
132        }
133
134        $this->done();
135    }
136
137    private function promoteToGlobal() {
138        $newName = $this->params['to'];
139        $caUser = CentralAuthUser::getPrimaryInstanceByName( $newName );
140        $status = $caUser->promoteToGlobal( WikiMap::getCurrentWikiId() );
141        if ( !$status->isOK() ) {
142            if ( $status->hasMessage( 'promote-not-on-wiki' ) ) {
143                // Eh, what?
144                throw new LogicException( "Tried to promote '$newName' to a global account except it " .
145                    "doesn't exist locally" );
146            } elseif ( $status->hasMessage( 'promote-already-exists' ) ) {
147                // Even more wtf.
148                throw new LogicException( "Tried to prommote '$newName' to a global account except it " .
149                    "already exists" );
150            }
151        }
152
153        $caUser->quickInvalidateCache();
154    }
155
156    /**
157     * Queue up jobs to move pages
158     * @param User $oldUser
159     */
160    public function movePages( User $oldUser ) {
161        $from = $this->params['from'];
162        $to = $this->params['to'];
163
164        $fromDBkey = $oldUser->getUserPage()->getDBkey();
165        $toDBkey = Title::makeTitleSafe( NS_USER, $to )->getDBkey();
166        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
167
168        $rows = $dbr->newSelectQueryBuilder()
169            ->select( [ 'page_namespace', 'page_title' ] )
170            ->from( 'page' )
171            ->where( [
172                'page_namespace' => [ NS_USER, NS_USER_TALK ],
173                $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $fromDBkey . '/', $dbr->anyString() ) )
174                    ->or( 'page_title', '=', $fromDBkey )
175            ] )
176            ->caller( __METHOD__ )
177            ->fetchResultSet();
178
179        $jobParams = [
180            'to' => $to,
181            'from' => $from,
182            'renamer' => $this->getRenameUser()->getName(),
183            'suppressredirects' => $this->params['suppressredirects'],
184        ];
185        if ( isset( $this->params['session'] ) ) {
186            $jobParams['session'] = $this->params['session'];
187        }
188        $jobs = [];
189
190        $toReplace = static::escapeReplacement( $toDBkey );
191        foreach ( $rows as $row ) {
192            $oldPage = Title::newFromRow( $row );
193            $newPage = Title::makeTitleSafe( $row->page_namespace,
194                preg_replace( '!^[^/]+!', $toReplace, $row->page_title ) );
195            $jobs[] = new LocalPageMoveJob(
196                Title::newFromText( 'LocalRenameUserJob' ),
197                $jobParams + [
198                    'old' => [ $oldPage->getNamespace(), $oldPage->getDBkey() ],
199                    'new' => [ $newPage->getNamespace(), $newPage->getDBkey() ],
200                ]
201            );
202        }
203
204        MediaWikiServices::getInstance()->getJobQueueGroup()->push( $jobs );
205    }
206
207    protected function done() {
208        parent::done();
209        $caOld = CentralAuthUser::getInstanceByName( $this->params['from'] );
210        $caOld->quickInvalidateCache();
211    }
212
213    /**
214     * Escape a string to be used as a replacement by preg_replace so that
215     * anything in it that looks like a backreference is treated as a literal
216     * substitution.
217     *
218     * @param string $str String to escape
219     * @return string
220     */
221    protected static function escapeReplacement( $str ) {
222        // T188171: escape any occurrence of '$n' or '\n' in the replacement
223        // string passed to preg_replace so that it will not be treated as
224        // a backreference.
225        return preg_replace(
226            // find $n, ${n}, and \n
227            '/[$\\\\]{?\d+}?/',
228            // prepend with a literal '\\'
229            '\\\\${0}',
230            $str
231        );
232    }
233}