63 parent::__construct();
65This script cleans up the title fields in various tables to
remove entries that
66will be rejected by the constructor of TitleValue. This constructor
throws an
67exception when invalid data is encountered, which will not normally occur on
68regular page views, but can happen on query special pages.
70The script targets titles matching the regular expression /^_|[ \r\n\t]|_$/.
71Because any foreign key relationships involving these titles will already be
72broken, the titles are corrected to a valid version or the rows are deleted
73entirely, depending on the table.
75The script runs with the expectation that STDOUT is redirected to a file.
78 $this->
addOption(
'fix',
'Actually clean up invalid titles. If this parameter is ' .
79 'not specified, the script will report invalid titles but not clean them up.',
81 $this->
addOption(
'table',
'The table(s) to process. This option can be specified ' .
82 'more than once (e.g. -t category -t watchlist). If not specified, all available ' .
83 'tables will be processed. Available tables are: ' .
84 implode(
', ', array_column( static::$tables, 0 ) ),
false,
true,
't',
true );
134 [ $table, $prefix ] = $tableParams;
135 $idField = $tableParams[
'idField'] ??
"{$prefix}_id";
136 $nsField = $tableParams[
'nsField'] ??
"{$prefix}_namespace";
137 $titleField = $tableParams[
'titleField'] ??
"{$prefix}_title";
139 $this->
outputStatus(
"Looking for invalid $titleField entries in $table...\n" );
150 if ( isset( $linksMigration::$mapping[$table] ) ) {
151 [ $nsField, $titleField ] = $linksMigration->getTitleFields( $table );
152 $joinConds = $linksMigration->getQueryInfo( $table )[
'joins'];
153 $tables = $linksMigration->getQueryInfo( $table )[
'tables'];
157 $percent = $dbr->anyString();
160 ->expr( $titleField, IExpression::LIKE,
new LikeValue( $percent,
' ', $percent ) )
161 ->or( $titleField, IExpression::LIKE,
new LikeValue( $percent,
"\r", $percent ) )
162 ->or( $titleField, IExpression::LIKE,
new LikeValue( $percent,
"\n", $percent ) )
163 ->or( $titleField, IExpression::LIKE,
new LikeValue( $percent,
"\t", $percent ) )
164 ->or( $titleField, IExpression::LIKE,
new LikeValue(
'_', $percent ) )
165 ->or( $titleField, IExpression::LIKE,
new LikeValue( $percent,
'_' ) );
166 $res = $dbr->newSelectQueryBuilder()
170 'title' => $titleField,
174 ->joinConds( $joinConds )
176 ->caller( __METHOD__ )
179 $this->
outputStatus(
"Number of invalid rows: " . $res->numRows() .
"\n" );
180 if ( !$res->numRows() ) {
187 $this->
writeToReport( sprintf(
"%10s | ns | dbkey\n", $idField ) );
189 foreach ( $res as $row ) {
190 $this->
writeToReport( sprintf(
"%10d | %3d | %s\n", $row->id, $row->ns, $row->title ) );
197 if ( $table ===
'logging' || $table ===
'archive' ) {
198 $this->
writeToReport(
"The following updates would be run with the --fix flag:\n" );
199 foreach ( $res as $row ) {
202 "$idField={$row->id}: update '{$row->title}' to '$newTitle'\n" );
206 if ( $table !==
'page' && $table !==
'redirect' ) {
207 $this->
outputStatus(
"Run with --fix to clean up these rows\n" );
224IMPORTANT: This script does not fix invalid entries in the $table table.
225Consider repairing these rows, and rows in related tables, by hand.
226You may like to
run, or borrow logic from, the cleanupTitles.php script.
239 "Updating these rows, setting $titleField to the closest valid DB key...\n" );
240 $affectedRowCount = 0;
241 foreach ( $res as $row ) {
244 "$idField={$row->id}: updating '{$row->title}' to '$newTitle'\n" );
246 $dbw->newUpdateQueryBuilder()
248 ->set( [ $titleField => $newTitle ] )
249 ->where( [ $idField => $row->id ] )
250 ->caller( __METHOD__ )
252 $affectedRowCount += $dbw->affectedRows();
255 $this->
outputStatus(
"Updated $affectedRowCount rows on $table.\n" );
259 case 'recentchanges':
265 $this->
outputStatus(
"Deleting invalid $table rows...\n" );
266 $dbw->newDeleteQueryBuilder()
267 ->deleteFrom( $table )
268 ->where( [ $idField => $ids ] )
269 ->caller( __METHOD__ )->execute();
271 $this->
outputStatus(
'Deleted ' . $dbw->affectedRows() .
" rows from $table.\n" );
274 case 'protected_titles':
278 $this->
outputStatus(
"Deleting invalid $table rows...\n" );
279 $affectedRowCount = 0;
280 foreach ( $res as $row ) {
281 $dbw->newDeleteQueryBuilder()
282 ->deleteFrom( $table )
283 ->where( [ $nsField => $row->ns, $titleField => $row->title ] )
284 ->caller( __METHOD__ )->execute();
285 $affectedRowCount += $dbw->affectedRows();
288 $this->
outputStatus(
"Deleted $affectedRowCount rows from $table.\n" );
292 case 'templatelinks':
293 case 'categorylinks':
298 $this->
outputStatus(
"Queueing link update jobs for the pages in $idField...\n" );
300 $wikiPageFactory = $services->getWikiPageFactory();
301 foreach ( $res as $row ) {
302 $wp = $wikiPageFactory->newFromID( $row->id );
304 RefreshLinks::fixLinksFromArticle( $row->id );
306 if ( isset( $linksMigration::$mapping[$table] ) ) {
307 $conds = $linksMigration->getLinksConditions(
309 Title::makeTitle( $row->ns, $row->title )
312 $conds = [ $nsField => $row->ns, $titleField => $row->title ];
315 $dbw->newDeleteQueryBuilder()
316 ->deleteFrom( $table )
317 ->where( array_merge( [ $idField => $row->id ], $conds ) )
318 ->caller( __METHOD__ )->execute();
322 $this->
outputStatus(
"Link update jobs have been added to the job queue.\n" );