32require_once __DIR__ .
'/TableCleanup.php';
42 private string $prefix;
45 parent::__construct();
46 $this->
addDescription(
'Script to clean up broken, unparseable titles' );
47 $this->
addOption(
'prefix',
"Broken pages will be renamed to titles with " .
48 "<prefix> prepended before the article name. Defaults to 'Broken'",
false,
true );
56 $this->prefix = $this->
getOption(
'prefix',
'Broken' ) .
"/";
62 $title = Title::newFromText( $this->prefix );
63 if ( !$title || !$title->canExist() || $title->getInterwiki() || $title->getNamespace() !== 0 ) {
64 $this->
fatalError(
"Invalid prefix {$this->prefix}. Must be a valid mainspace title." );
73 $display = Title::makeName( $row->page_namespace, $row->page_title );
75 $title = Title::newFromText( $verified );
79 && $title->getNamespace() == $row->page_namespace
80 && $title->getDBkey() === $row->page_title
88 if ( $row->page_namespace ==
NS_FILE && $this->fileExists( $row->page_title ) ) {
89 $this->
output(
"file $row->page_title needs cleanup, please run cleanupImages.php.\n" );
91 } elseif ( $title ===
null ) {
92 $this->
output(
"page $row->page_id ($display) is illegal.\n" );
96 $this->
output(
"page $row->page_id ($display) doesn't match self.\n" );
110 $row = $dbr->newSelectQueryBuilder()
113 ->where( [
'img_name' => $name ] )
114 ->caller( __METHOD__ )
117 return $row !==
false;
124 $legalChars = Title::legalChars();
125 $legalizedUnprefixed = preg_replace_callback(
"/([^$legalChars])/",
126 [ $this,
'hexChar' ],
128 if ( $legalizedUnprefixed ==
'.' ) {
129 $legalizedUnprefixed =
'(dot)';
131 if ( $legalizedUnprefixed ==
'_' ) {
132 $legalizedUnprefixed =
'(space)';
134 $ns = (int)$row->page_namespace;
139 $subjectTitle = Title::newFromText( $legalizedUnprefixed );
140 if ( $subjectTitle && !$subjectTitle->isTalkPage() ) {
141 $talkTitle = $subjectTitle->getTalkPageIfDefined();
142 if ( $talkTitle !==
null && !$talkTitle->exists() ) {
143 $ns = $talkTitle->getNamespace();
149 if ( $title ===
null ) {
154 $namespaceName = $namespaceInfo->getCanonicalName( $ns );
155 if ( $namespaceName ===
false ) {
156 $namespaceName =
"NS$ns";
159 $legalizedUnprefixed =
"$namespaceName:$legalizedUnprefixed";
161 $title = Title::newFromText( $this->prefix . $legalizedUnprefixed );
164 if ( $title ===
null ) {
169 $legalizedUnprefixed = preg_replace_callback(
'!([^A-Za-z0-9_:\\-])!',
170 [ $this,
'hexChar' ],
173 $title = Title::newFromText( $this->prefix . $legalizedUnprefixed );
176 if ( $title ===
null ) {
178 $clean = $this->prefix .
'id:' . $row->page_id;
179 $legalized = $this->prefix . $legalizedUnprefixed;
180 $this->
output(
"Couldn't legalize; form '$legalized' still invalid; using '$clean'\n" );
181 $title = Title::newFromText( $clean );
182 } elseif ( $title->exists( IDBAccessObject::READ_LATEST ) ) {
183 $clean = $this->prefix .
'id:' . $row->page_id;
184 $conflict = $title->getDBKey();
185 $this->
output(
"Legalized for '$conflict' exists; using '$clean'\n" );
186 $title = Title::newFromText( $clean );
189 if ( !$title || $title->exists( IDBAccessObject::READ_LATEST ) ) {
193 $this->
error(
"Destination page {$title->getText()} is invalid or already exists, skipping." );
197 $dest = $title->getDBkey();
198 if ( $this->dryrun ) {
199 $this->
output(
"DRY RUN: would rename $row->page_id ($row->page_namespace," .
200 "'$row->page_title') to ($ns,'$dest')\n" );
202 $this->
output(
"renaming $row->page_id ($row->page_namespace," .
203 "'$row->page_title') to ($ns,'$dest')\n" );
205 ->newUpdateQueryBuilder()
207 ->set( [
'page_title' => $dest,
'page_namespace' => $ns ] )
208 ->where( [
'page_id' => $row->page_id ] )
209 ->caller( __METHOD__ )->execute();
219 if ( $title->
exists( IDBAccessObject::READ_LATEST ) || $titleImpossible ) {
220 if ( $titleImpossible ) {
226 $ns = (int)$row->page_namespace;
227 # If a page is saved in the main namespace with a namespace prefix then try to move it into
228 # that namespace. If there's no conflict then it will succeed. Otherwise it will hit the condition
229 # } else if ($ns !== 0) { and be moved to Broken/Namespace:Title
230 # whereas without this check it would just go to Broken/Title
235 # Old cleanupTitles could move articles there. See T25147.
236 # or a page could be stored as (0, "Special:Foo") in which case the $titleImpossible
237 # condition would be true and we've already added a prefix so pretend we're in mainspace
238 # and don't add another
243 # Namespace which no longer exists. Put the page in the main namespace
244 # since we don't have any idea of the old namespace name. See T70501.
245 # We build the new title ourself rather than relying on getDBKey() because
246 # that will return Special:BadTitle
248 if ( !$namespaceInfo->exists( $ns ) ) {
249 $clean =
"{$this->prefix}NS$ns:$row->page_title";
251 } elseif ( !$titleImpossible && !$title->
exists( IDBAccessObject::READ_LATEST ) ) {
254 } elseif ( $ns !== 0 ) {
256 $nsName = $namespaceInfo->getCanonicalName( $ns );
257 $clean =
"{$this->prefix}$nsName:{$prior}";
260 $clean = $this->prefix . $prior;
262 $verified = Title::makeTitleSafe( $ns, $clean );
263 if ( !$verified || $verified->exists( IDBAccessObject::READ_LATEST ) ) {
264 $lastResort =
"{$this->prefix}id: {$row->page_id}";
265 $this->
output(
"Couldn't legalize; form '$clean' exists; using '$lastResort'\n" );
266 $verified = Title::makeTitleSafe( $ns, $lastResort );
267 if ( !$verified || $verified->exists( IDBAccessObject::READ_LATEST ) ) {
271 $this->
error(
"Destination page $lastResort invalid or already exists." );
281 if ( $this->dryrun ) {
282 $this->
output(
"DRY RUN: would rename $row->page_id ($row->page_namespace," .
283 "'$row->page_title') to ($ns,'$dest')\n" );
285 $this->
output(
"renaming $row->page_id ($row->page_namespace," .
286 "'$row->page_title') to ($ns,'$dest')\n" );
288 ->newUpdateQueryBuilder()
291 'page_namespace' => $ns,
292 'page_title' => $dest
294 ->where( [
'page_id' => $row->page_id ] )
295 ->caller( __METHOD__ )->execute();
303require_once RUN_MAINTENANCE_IF_MAIN;
error( $err, $die=0)
Throw an error to the user.
output( $out, $channel=null)
Throw some output to the user.
getServiceContainer()
Returns the main service container.
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Generic class to cleanup a database table.
Maintenance script to clean up broken, unparseable titles.
moveInconsistentPage( $row, Title $title)
__construct()
Default constructor.
execute()
Do the actual work.All child classes will need to implement thisbool|null|void True for success,...