19require_once __DIR__ .
'/TableCleanup.php';
29 private string $prefix;
32 parent::__construct();
33 $this->
addDescription(
'Script to clean up broken, unparseable titles' );
34 $this->
addOption(
'prefix',
"Broken pages will be renamed to titles with " .
35 "<prefix> prepended before the article name. Defaults to 'Broken'",
false,
true );
43 $this->prefix = $this->
getOption(
'prefix',
'Broken' ) .
"/";
49 $title = Title::newFromText( $this->prefix );
50 if ( !$title || !$title->canExist() || $title->getInterwiki() || $title->getNamespace() !== 0 ) {
51 $this->
fatalError(
"Invalid prefix {$this->prefix}. Must be a valid mainspace title." );
60 $display = Title::makeName( $row->page_namespace, $row->page_title );
62 $title = Title::newFromText( $verified );
66 && $title->getNamespace() == $row->page_namespace
67 && $title->getDBkey() === $row->page_title
75 if ( $row->page_namespace ==
NS_FILE && $this->fileExists( $row->page_title ) ) {
76 $this->
output(
"file $row->page_title needs cleanup, please run cleanupImages.php.\n" );
78 } elseif ( $title ===
null ) {
79 $this->
output(
"page $row->page_id ($display) is illegal.\n" );
83 $this->
output(
"page $row->page_id ($display) doesn't match self.\n" );
98 MainConfigNames::FileSchemaMigrationStage
101 $row = $dbr->newSelectQueryBuilder()
104 ->where( [
'img_name' => $name ] )
105 ->caller( __METHOD__ )
108 $row = $dbr->newSelectQueryBuilder()
112 'file_name' => $name,
115 ->caller( __METHOD__ )
119 return $row !==
false;
126 $legalChars = Title::legalChars();
127 $legalizedUnprefixed = preg_replace_callback(
"/([^$legalChars])/",
130 if ( $legalizedUnprefixed ==
'.' ) {
131 $legalizedUnprefixed =
'(dot)';
133 if ( $legalizedUnprefixed ==
'_' ) {
134 $legalizedUnprefixed =
'(space)';
136 $ns = (int)$row->page_namespace;
141 $subjectTitle = Title::newFromText( $legalizedUnprefixed );
142 if ( $subjectTitle && !$subjectTitle->isTalkPage() ) {
143 $talkTitle = $subjectTitle->getTalkPageIfDefined();
144 if ( $talkTitle !==
null && !$talkTitle->exists() ) {
145 $ns = $talkTitle->getNamespace();
151 if ( $title ===
null ) {
156 $namespaceName = $namespaceInfo->getCanonicalName( $ns );
157 if ( $namespaceName ===
false ) {
158 $namespaceName =
"NS$ns";
161 $legalizedUnprefixed =
"$namespaceName:$legalizedUnprefixed";
163 $title = Title::newFromText( $this->prefix . $legalizedUnprefixed );
166 if ( $title ===
null ) {
171 $legalizedUnprefixed = preg_replace_callback(
'!([^A-Za-z0-9_:\\-])!',
175 $title = Title::newFromText( $this->prefix . $legalizedUnprefixed );
178 if ( $title ===
null ) {
180 $clean = $this->prefix .
'id:' . $row->page_id;
181 $legalized = $this->prefix . $legalizedUnprefixed;
182 $this->
output(
"Couldn't legalize; form '$legalized' still invalid; using '$clean'\n" );
183 $title = Title::newFromText( $clean );
184 } elseif ( $title->exists( IDBAccessObject::READ_LATEST ) ) {
185 $clean = $this->prefix .
'id:' . $row->page_id;
186 $conflict = $title->getDBKey();
187 $this->
output(
"Legalized for '$conflict' exists; using '$clean'\n" );
188 $title = Title::newFromText( $clean );
191 if ( !$title || $title->exists( IDBAccessObject::READ_LATEST ) ) {
195 $this->
error(
"Destination page {$title->getText()} is invalid or already exists, skipping." );
199 $dest = $title->getDBkey();
200 if ( $this->dryrun ) {
201 $this->
output(
"DRY RUN: would rename $row->page_id ($row->page_namespace," .
202 "'$row->page_title') to ($ns,'$dest')\n" );
204 $this->
output(
"renaming $row->page_id ($row->page_namespace," .
205 "'$row->page_title') to ($ns,'$dest')\n" );
207 ->newUpdateQueryBuilder()
209 ->set( [
'page_title' => $dest,
'page_namespace' => $ns ] )
210 ->where( [
'page_id' => $row->page_id ] )
211 ->caller( __METHOD__ )->execute();
221 if ( $title->
exists( IDBAccessObject::READ_LATEST ) || $titleImpossible ) {
222 if ( $titleImpossible ) {
228 $ns = (int)$row->page_namespace;
229 # If a page is saved in the main namespace with a namespace prefix then try to move it into
230 # that namespace. If there's no conflict then it will succeed. Otherwise it will hit the condition
231 # } else if ($ns !== 0) { and be moved to Broken/Namespace:Title
232 # whereas without this check it would just go to Broken/Title
237 # Old cleanupTitles could move articles there. See T25147.
238 # or a page could be stored as (0, "Special:Foo") in which case the $titleImpossible
239 # condition would be true and we've already added a prefix so pretend we're in mainspace
240 # and don't add another
245 # Namespace which no longer exists. Put the page in the main namespace
246 # since we don't have any idea of the old namespace name. See T70501.
247 # We build the new title ourself rather than relying on getDBKey() because
248 # that will return Special:BadTitle
250 if ( !$namespaceInfo->exists( $ns ) ) {
251 $clean =
"{$this->prefix}NS$ns:$row->page_title";
253 } elseif ( !$titleImpossible && !$title->
exists( IDBAccessObject::READ_LATEST ) ) {
256 } elseif ( $ns !== 0 ) {
258 $nsName = $namespaceInfo->getCanonicalName( $ns );
259 $clean =
"{$this->prefix}$nsName:{$prior}";
262 $clean = $this->prefix . $prior;
264 $verified = Title::makeTitleSafe( $ns, $clean );
265 if ( !$verified || $verified->exists( IDBAccessObject::READ_LATEST ) ) {
266 $lastResort =
"{$this->prefix}id: {$row->page_id}";
267 $this->
output(
"Couldn't legalize; form '$clean' exists; using '$lastResort'\n" );
268 $verified = Title::makeTitleSafe( $ns, $lastResort );
269 if ( !$verified || $verified->exists( IDBAccessObject::READ_LATEST ) ) {
273 $this->
error(
"Destination page $lastResort invalid or already exists." );
283 if ( $this->dryrun ) {
284 $this->
output(
"DRY RUN: would rename $row->page_id ($row->page_namespace," .
285 "'$row->page_title') to ($ns,'$dest')\n" );
287 $this->
output(
"renaming $row->page_id ($row->page_namespace," .
288 "'$row->page_title') to ($ns,'$dest')\n" );
290 ->newUpdateQueryBuilder()
293 'page_namespace' => $ns,
294 'page_title' => $dest
296 ->where( [
'page_id' => $row->page_id ] )
297 ->caller( __METHOD__ )->execute();
305require_once RUN_MAINTENANCE_IF_MAIN;
const SCHEMA_COMPAT_READ_OLD
A class containing constants representing the names of configuration variables.
output( $out, $channel=null)
Throw some output to the user.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
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.
getReplicaDB(string|false $virtualDomain=false)
error( $err, $die=0)
Throw an error to the user.
getServiceContainer()
Returns the main service container.
getPrimaryDB(string|false $virtualDomain=false)
addDescription( $text)
Set the description text.
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,...