Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
88 / 88
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
CleanupCaps
100.00% covered (success)
100.00%
88 / 88
100.00% covered (success)
100.00%
5 / 5
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 processRowToUppercase
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
7
 processRowToLowercase
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 movePage
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Clean up broken page links when somebody turns on $wgCapitalLinks.
4 *
5 * Usage: php cleanupCaps.php [--dry-run]
6 * Options:
7 *   --dry-run  don't actually try moving them
8 *
9 * Copyright © 2005 Brooke Vibber <bvibber@wikimedia.org>
10 * https://www.mediawiki.org/
11 *
12 * @license GPL-2.0-or-later
13 * @file
14 * @author Brooke Vibber <bvibber@wikimedia.org>
15 * @ingroup Maintenance
16 */
17
18use MediaWiki\Title\Title;
19use MediaWiki\User\User;
20
21// @codeCoverageIgnoreStart
22require_once __DIR__ . '/TableCleanup.php';
23// @codeCoverageIgnoreEnd
24
25/**
26 * Maintenance script to clean up broken page links when somebody turns
27 * on or off $wgCapitalLinks.
28 *
29 * @ingroup Maintenance
30 */
31class CleanupCaps extends TableCleanup {
32
33    /** @var User */
34    private $user;
35    /** @var int */
36    private $namespace;
37
38    public function __construct() {
39        parent::__construct();
40        $this->addDescription( 'Script to cleanup capitalization' );
41        $this->addOption( 'namespace', 'Namespace number to run caps cleanup on', false, true );
42    }
43
44    public function execute() {
45        $this->user = User::newSystemUser( 'Conversion script', [ 'steal' => true ] );
46
47        $this->namespace = intval( $this->getOption( 'namespace', 0 ) );
48
49        if (
50            $this->getServiceContainer()->getNamespaceInfo()->
51                isCapitalized( $this->namespace )
52        ) {
53            $this->output( "Will be moving pages to first letter capitalized titles\n" );
54            $callback = 'processRowToUppercase';
55        } else {
56            $this->output( "Will be moving pages to first letter lowercase titles\n" );
57            $callback = 'processRowToLowercase';
58        }
59
60        $this->dryrun = $this->hasOption( 'dry-run' );
61
62        $this->runTable( [
63            'table' => 'page',
64            'conds' => [ 'page_namespace' => $this->namespace ],
65            'index' => 'page_id',
66            'callback' => $callback ] );
67    }
68
69    protected function processRowToUppercase( \stdClass $row ) {
70        $current = Title::makeTitle( $row->page_namespace, $row->page_title );
71        // Set the ID of the page because Title::exists will return false
72        // unless the Article ID is already known, because Title::canExist will be false
73        // when $wgCapitalLinks is true and the old title is in lower case.
74        $current->mArticleID = $row->page_id;
75        $display = $current->getPrefixedText();
76        $lower = $row->page_title;
77        $upper = $this->getServiceContainer()->getContentLanguage()->ucfirst( $row->page_title );
78        if ( $upper == $lower ) {
79            $this->output( "\"$display\" already uppercase.\n" );
80
81            $this->progress( 0 );
82            return;
83        }
84
85        $target = Title::makeTitle( $row->page_namespace, $upper );
86        if ( $target->exists() ) {
87            // Prefix "CapsCleanup" to bypass the conflict
88            $target = Title::newFromText( 'CapsCleanup/' . $display );
89        }
90        $ok = $this->movePage(
91            $current,
92            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable target is always valid
93            $target,
94            'Converting page title to first-letter uppercase',
95            false
96        );
97        if ( !$ok ) {
98            $this->progress( 0 );
99            return;
100        }
101
102        $this->progress( 1 );
103        if ( $row->page_namespace == $this->namespace ) {
104            // We need to fetch the existence of the talk page from the DB directly because
105            // Title::exists will return false if Title::canExist returns false. Title::canExist will
106            // return false if $wgCapitalLinks is true and the old title is in lowercase form.
107            $namespaceInfo = $this->getServiceContainer()->getNamespaceInfo();
108            if ( $namespaceInfo->canHaveTalkPage( $current ) ) {
109                $talkNamespace = $namespaceInfo->getTalk( $row->page_namespace );
110                $talkPageExists = $this->getReplicaDB()->newSelectQueryBuilder()
111                    ->select( '1' )
112                    ->from( 'page' )
113                    ->where( [ 'page_title' => $row->page_title, 'page_namespace' => $talkNamespace ] )
114                    ->caller( __METHOD__ )
115                    ->fetchField();
116                if ( $talkPageExists ) {
117                    $row->page_namespace = $talkNamespace;
118                    $this->processRowToUppercase( $row );
119                }
120            }
121        }
122    }
123
124    protected function processRowToLowercase( \stdClass $row ) {
125        $current = Title::makeTitle( $row->page_namespace, $row->page_title );
126        $display = $current->getPrefixedText();
127        $upper = $row->page_title;
128        $lower = $this->getServiceContainer()->getContentLanguage()->lcfirst( $row->page_title );
129        if ( $upper == $lower ) {
130            $this->output( "\"$display\" already lowercase.\n" );
131
132            $this->progress( 0 );
133            return;
134        }
135
136        $target = Title::makeTitle( $row->page_namespace, $lower );
137        if ( $target->exists() ) {
138            $targetDisplay = $target->getPrefixedText();
139            $this->output( "\"$display\" skipped; \"$targetDisplay\" already exists\n" );
140
141            $this->progress( 0 );
142            return;
143        }
144
145        $ok = $this->movePage( $current, $target, 'Converting page titles to lowercase', true );
146        if ( $ok !== true ) {
147            $this->progress( 0 );
148        }
149
150        $this->progress( 1 );
151        if ( $row->page_namespace == $this->namespace ) {
152            $talk = $current->getTalkPage();
153            if ( $talk->exists() ) {
154                $row->page_namespace = $talk->getNamespace();
155                $this->processRowToLowercase( $row );
156            }
157        }
158    }
159
160    /**
161     * @param Title $current
162     * @param Title $target
163     * @param string $reason
164     * @param bool $createRedirect
165     * @return bool Success
166     */
167    private function movePage( Title $current, Title $target, $reason, $createRedirect ) {
168        $display = $current->getPrefixedText();
169        $targetDisplay = $target->getPrefixedText();
170
171        if ( $this->dryrun ) {
172            $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
173            $ok = 'OK';
174        } else {
175            $mp = $this->getServiceContainer()->getMovePageFactory()
176                ->newMovePage( $current, $target );
177            $status = $mp->move( $this->user, $reason, $createRedirect );
178            $ok = $status->isOK() ? 'OK' : 'FAILED';
179            $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
180            if ( !$status->isOK() ) {
181                $this->error( $status );
182            }
183        }
184
185        return $ok === 'OK';
186    }
187}
188
189// @codeCoverageIgnoreStart
190$maintClass = CleanupCaps::class;
191require_once RUN_MAINTENANCE_IF_MAIN;
192// @codeCoverageIgnoreEnd