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