61 public function check(
bool $fix =
false,
string|
false $xml =
'' ) {
64 print
"Checking, will fix errors if possible...\n";
66 print
"Checking...\n";
68 $maxRevId = $dbr->newSelectQueryBuilder()
69 ->select(
'MAX(rev_id)' )
71 ->caller( __METHOD__ )->fetchField();
75 $knownFlags = [
'external',
'gzip',
'object',
'utf-8' ];
78 'restore revision' => [],
84 for ( $chunkStart = 1; $chunkStart < $maxRevId; $chunkStart += $chunkSize ) {
85 $chunkEnd = $chunkStart + $chunkSize - 1;
92 $res = $dbr->newSelectQueryBuilder()
93 ->select( [
'slot_revision_id',
'content_address' ] )
95 ->join(
'content',
null,
'content_id = slot_content_id' )
97 $dbr->expr(
'slot_revision_id',
'>=', $chunkStart ),
98 $dbr->expr(
'slot_revision_id',
'<=', $chunkEnd ),
100 ->caller( __METHOD__ )->fetchResultSet();
103 '@phan-var \MediaWiki\Storage\SqlBlobStore $blobStore';
104 foreach ( $res as $row ) {
105 $textId = $blobStore->getTextIdFromAddress( $row->content_address );
107 if ( !isset( $this->oldIdMap[$textId] ) ) {
108 $this->oldIdMap[ $textId ] = [ $row->slot_revision_id ];
109 } elseif ( !in_array( $row->slot_revision_id, $this->oldIdMap[$textId] ) ) {
110 $this->oldIdMap[ $textId ][] = $row->slot_revision_id;
115 if ( !count( $this->oldIdMap ) ) {
123 $res = $dbr->newSelectQueryBuilder()
124 ->select( [
'old_id',
'old_flags' ] )
126 ->where( [
'old_id' => array_keys( $this->oldIdMap ) ] )
127 ->caller( __METHOD__ )->fetchResultSet();
128 foreach ( $res as $row ) {
132 $flags = $row->old_flags;
136 $flagStats += [ $flags => 0 ];
138 $flagStats[$flags]++;
141 unset( $missingTextRows[$row->old_id] );
144 if ( $flags ==
'' ) {
147 $flagArray = explode(
',', $flags );
149 if ( in_array(
'external', $flagArray ) ) {
150 $externalRevs[] = $id;
151 } elseif ( in_array(
'object', $flagArray ) ) {
156 if ( $flags ==
'0' ) {
160 $this->addError(
'fixed',
"Warning: old_flags set to 0", $id );
163 $dbw->newUpdateQueryBuilder()
165 ->set( [
'old_flags' =>
'' ] )
166 ->where( [
'old_id' => $id ] )
167 ->caller( __METHOD__ )
171 $this->addError(
'fixable',
"Warning: old_flags set to 0", $id );
173 } elseif ( count( array_diff( $flagArray, $knownFlags ) ) ) {
174 $this->addError(
'unfixable',
"Error: invalid flags field \"$flags\"", $id );
179 foreach ( $missingTextRows as $oldId => $revIds ) {
180 $this->addError(
'restore revision',
"Error: missing text row", $oldId );
184 $externalConcatBlobs = [];
185 $externalNormalBlobs = [];
186 if ( count( $externalRevs ) ) {
187 $res = $dbr->newSelectQueryBuilder()
188 ->select( [
'old_id',
'old_flags',
'old_text' ] )
190 ->where( [
'old_id' => $externalRevs ] )
191 ->caller( __METHOD__ )->fetchResultSet();
192 foreach ( $res as $row ) {
193 $urlParts = explode(
'://', $row->old_text, 2 );
194 if ( count( $urlParts ) !== 2 || $urlParts[1] ==
'' ) {
195 $this->addError(
'restore text',
"Error: invalid URL \"{$row->old_text}\"", $row->old_id );
198 [ $proto, ] = $urlParts;
199 if ( $proto !=
'DB' ) {
202 "Error: invalid external protocol \"$proto\"",
206 $path = explode(
'/', $row->old_text );
209 if ( isset(
$path[4] ) ) {
210 $externalConcatBlobs[$cluster][$id][] = $row->old_id;
212 $externalNormalBlobs[$cluster][$id][] = $row->old_id;
218 $this->checkExternalConcatBlobs( $externalConcatBlobs );
221 if ( count( $externalNormalBlobs ) ) {
222 if ( $this->dbStore ===
null ) {
224 $this->dbStore = $esFactory->getDatabaseStore();
226 foreach ( $externalConcatBlobs as $cluster => $xBlobIds ) {
227 $blobIds = array_keys( $xBlobIds );
228 $extDb = $this->dbStore->getReplica( $cluster );
229 $blobsTable = $this->dbStore->getTable( $cluster );
230 $res = $extDb->newSelectQueryBuilder()
231 ->select( [
'blob_id' ] )
232 ->from( $blobsTable )
233 ->where( [
'blob_id' => $blobIds ] )
234 ->caller( __METHOD__ )->fetchResultSet();
235 foreach ( $res as $row ) {
236 unset( $xBlobIds[$row->blob_id] );
239 foreach ( $xBlobIds as $blobId => $oldId ) {
242 "Error: missing target $blobId for one-part ES URL",
252 if ( count( $objectRevs ) ) {
254 $res = $dbr->newSelectQueryBuilder()
255 ->select( [
'old_id',
'old_flags',
"LEFT(old_text, $headerLength) AS header" ] )
257 ->where( [
'old_id' => $objectRevs ] )
258 ->caller( __METHOD__ )->fetchResultSet();
259 foreach ( $res as $row ) {
260 $oldId = $row->old_id;
262 if ( !preg_match(
'/^O:(\d+):"(\w+)"/', $row->header,
$matches ) ) {
263 $this->addError(
'restore text',
"Error: invalid object header", $oldId );
267 $className = strtolower(
$matches[2] );
268 if ( strlen( $className ) !=
$matches[1] ) {
271 "Error: invalid object header, wrong class name length",
277 $objectStats += [ $className => 0 ];
278 $objectStats[$className]++;
280 switch ( $className ) {
281 case 'concatenatedgziphistoryblob':
284 case 'historyblobstub':
285 case 'historyblobcurstub':
286 if ( strlen( $row->header ) == $headerLength ) {
287 $this->addError(
'unfixable',
"Error: overlong stub header", $oldId );
290 $stubObj = unserialize( $row->header );
291 if ( !is_object( $stubObj ) ) {
292 $this->addError(
'restore text',
"Error: unable to unserialize stub object", $oldId );
295 if ( $className ==
'historyblobstub' ) {
296 $concatBlobs[$stubObj->getLocation()][] = $oldId;
298 $curIds[$stubObj->mCurId][] = $oldId;
302 $this->addError(
'unfixable',
"Error: unrecognised object class \"$className\"", $oldId );
308 $externalConcatBlobs = [];
309 if ( count( $concatBlobs ) ) {
311 $res = $dbr->newSelectQueryBuilder()
312 ->select( [
'old_id',
'old_flags',
"LEFT(old_text, $headerLength) AS header" ] )
314 ->where( [
'old_id' => array_keys( $concatBlobs ) ] )
315 ->caller( __METHOD__ )->fetchResultSet();
316 foreach ( $res as $row ) {
317 $flags = explode(
',', $row->old_flags );
318 if ( in_array(
'external', $flags ) ) {
320 if ( in_array(
'object', $flags ) ) {
321 $urlParts = explode(
'/', $row->header );
322 if ( $urlParts[0] !=
'DB:' ) {
325 "Error: unrecognised external storage type \"{$urlParts[0]}",
329 $cluster = $urlParts[2];
331 if ( !isset( $externalConcatBlobs[$cluster][$id] ) ) {
332 $externalConcatBlobs[$cluster][$id] = [];
334 $externalConcatBlobs[$cluster][$id] = array_merge(
335 $externalConcatBlobs[$cluster][$id], $concatBlobs[$row->old_id]
341 "Error: invalid flags \"{$row->old_flags}\" on concat bulk row {$row->old_id}",
342 $concatBlobs[$row->old_id] );
344 } elseif ( strcasecmp(
345 substr( $row->header, 0, strlen( self::CONCAT_HEADER ) ),
350 "Error: Incorrect object header for concat bulk row {$row->old_id}",
351 $concatBlobs[$row->old_id]
355 unset( $concatBlobs[$row->old_id] );
360 $this->checkExternalConcatBlobs( $externalConcatBlobs );
364 print
"\n\nErrors:\n";
365 foreach ( $this->errors as $name =>
$errors ) {
367 $description = $this->errorDescriptions[$name];
368 echo
"$description: " . implode(
',', array_keys(
$errors ) ) .
"\n";
372 if ( count( $this->errors[
'restore text'] ) && $fix ) {
373 if ( (
string)$xml !==
'' ) {
374 $this->restoreText( array_keys( $this->errors[
'restore text'] ), $xml );
376 echo
"Can't fix text, no XML backup specified\n";
380 print
"\nFlag statistics:\n";
381 $total = array_sum( $flagStats );
382 foreach ( $flagStats as $flag => $count ) {
383 printf(
"%-30s %10d %5.2f%%\n", $flag, $count, $count / $total * 100 );
385 print
"\nLocal object statistics:\n";
386 $total = array_sum( $objectStats );
387 foreach ( $objectStats as $className => $count ) {
388 printf(
"%-30s %10d %5.2f%%\n", $className, $count, $count / $total * 100 );