MediaWiki  master
LocalFileRestoreBatch.php
Go to the documentation of this file.
1 <?php
25 
32  private $file;
33 
35  private $cleanupBatch;
36 
38  private $ids;
39 
41  private $all;
42 
44  private $unsuppress;
45 
50  public function __construct( LocalFile $file, $unsuppress = false ) {
51  $this->file = $file;
52  $this->cleanupBatch = [];
53  $this->ids = [];
54  $this->unsuppress = $unsuppress;
55  }
56 
61  public function addId( $fa_id ) {
62  $this->ids[] = $fa_id;
63  }
64 
69  public function addIds( $ids ) {
70  $this->ids = array_merge( $this->ids, $ids );
71  }
72 
76  public function addAll() {
77  $this->all = true;
78  }
79 
88  public function execute() {
90  global $wgLang;
91 
92  $repo = $this->file->getRepo();
93  if ( !$this->all && !$this->ids ) {
94  // Do nothing
95  return $repo->newGood();
96  }
97 
98  $lockOwnsTrx = $this->file->lock();
99 
100  $dbw = $this->file->repo->getPrimaryDB();
101 
102  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
103 
104  $status = $this->file->repo->newGood();
105 
106  $exists = (bool)$dbw->selectField( 'image', '1',
107  [ 'img_name' => $this->file->getName() ],
108  __METHOD__,
109  // The lock() should already prevents changes, but this still may need
110  // to bypass any transaction snapshot. However, if lock() started the
111  // trx (which it probably did) then snapshot is post-lock and up-to-date.
112  $lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
113  );
114 
115  // Fetch all or selected archived revisions for the file,
116  // sorted from the most recent to the oldest.
117  $conditions = [ 'fa_name' => $this->file->getName() ];
118 
119  if ( !$this->all ) {
120  $conditions['fa_id'] = $this->ids;
121  }
122 
123  $arFileQuery = ArchivedFile::getQueryInfo();
124  $result = $dbw->select(
125  $arFileQuery['tables'],
126  $arFileQuery['fields'],
127  $conditions,
128  __METHOD__,
129  [ 'ORDER BY' => 'fa_timestamp DESC' ],
130  $arFileQuery['joins']
131  );
132 
133  $idsPresent = [];
134  $storeBatch = [];
135  $insertBatch = [];
136  $insertCurrent = false;
137  $deleteIds = [];
138  $first = true;
139  $archiveNames = [];
140 
141  foreach ( $result as $row ) {
142  $idsPresent[] = $row->fa_id;
143 
144  if ( $row->fa_name != $this->file->getName() ) {
145  $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
146  $status->failCount++;
147  continue;
148  }
149 
150  if ( $row->fa_storage_key == '' ) {
151  // Revision was missing pre-deletion
152  $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
153  $status->failCount++;
154  continue;
155  }
156 
157  $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
158  $row->fa_storage_key;
159  $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
160 
161  if ( isset( $row->fa_sha1 ) ) {
162  $sha1 = $row->fa_sha1;
163  } else {
164  // old row, populate from key
165  $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
166  }
167 
168  # Fix leading zero
169  if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
170  $sha1 = substr( $sha1, 1 );
171  }
172 
173  if ( $row->fa_major_mime === null || $row->fa_major_mime == 'unknown'
174  || $row->fa_minor_mime === null || $row->fa_minor_mime == 'unknown'
175  || $row->fa_media_type === null || $row->fa_media_type == 'UNKNOWN'
176  || $row->fa_metadata === null
177  ) {
178  // Refresh our metadata
179  // Required for a new current revision; nice for older ones too. :)
180  $this->file->loadFromFile( $deletedUrl );
181  $mime = $this->file->getMimeType();
182  list( $majorMime, $minorMime ) = File::splitMime( $mime );
183  $mediaInfo = [
184  'minor_mime' => $minorMime,
185  'major_mime' => $majorMime,
186  'media_type' => $this->file->getMediaType(),
187  'metadata' => $this->file->getMetadataForDb( $dbw )
188  ];
189  } else {
190  $mediaInfo = [
191  'minor_mime' => $row->fa_minor_mime,
192  'major_mime' => $row->fa_major_mime,
193  'media_type' => $row->fa_media_type,
194  'metadata' => $row->fa_metadata
195  ];
196  }
197 
198  $comment = $commentStore->getComment( 'fa_description', $row );
199  if ( $first && !$exists ) {
200  // This revision will be published as the new current version
201  $destRel = $this->file->getRel();
202  $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
203  $insertCurrent = [
204  'img_name' => $row->fa_name,
205  'img_size' => $row->fa_size,
206  'img_width' => $row->fa_width,
207  'img_height' => $row->fa_height,
208  'img_metadata' => $mediaInfo['metadata'],
209  'img_bits' => $row->fa_bits,
210  'img_media_type' => $mediaInfo['media_type'],
211  'img_major_mime' => $mediaInfo['major_mime'],
212  'img_minor_mime' => $mediaInfo['minor_mime'],
213  'img_actor' => $row->fa_actor,
214  'img_timestamp' => $row->fa_timestamp,
215  'img_sha1' => $sha1
216  ] + $commentFields;
217 
218  // The live (current) version cannot be hidden!
219  if ( !$this->unsuppress && $row->fa_deleted ) {
220  $status->fatal( 'undeleterevdel' );
221  $this->file->unlock();
222  return $status;
223  }
224  } else {
225  $archiveName = $row->fa_archive_name;
226 
227  if ( $archiveName == '' ) {
228  // This was originally a current version; we
229  // have to devise a new archive name for it.
230  // Format is <timestamp of archiving>!<name>
231  $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
232 
233  do {
234  $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
235  $timestamp++;
236  } while ( isset( $archiveNames[$archiveName] ) );
237  }
238 
239  $archiveNames[$archiveName] = true;
240  $destRel = $this->file->getArchiveRel( $archiveName );
241  $insertBatch[] = [
242  'oi_name' => $row->fa_name,
243  'oi_archive_name' => $archiveName,
244  'oi_size' => $row->fa_size,
245  'oi_width' => $row->fa_width,
246  'oi_height' => $row->fa_height,
247  'oi_bits' => $row->fa_bits,
248  'oi_actor' => $row->fa_actor,
249  'oi_timestamp' => $row->fa_timestamp,
250  'oi_metadata' => $mediaInfo['metadata'],
251  'oi_media_type' => $mediaInfo['media_type'],
252  'oi_major_mime' => $mediaInfo['major_mime'],
253  'oi_minor_mime' => $mediaInfo['minor_mime'],
254  'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
255  'oi_sha1' => $sha1
256  ] + $commentStore->insert( $dbw, 'oi_description', $comment );
257  }
258 
259  $deleteIds[] = $row->fa_id;
260 
261  if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
262  // private files can stay where they are
263  $status->successCount++;
264  } else {
265  $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
266  $this->cleanupBatch[] = $row->fa_storage_key;
267  }
268 
269  $first = false;
270  }
271 
272  unset( $result );
273 
274  // Add a warning to the status object for missing IDs
275  $missingIds = array_diff( $this->ids, $idsPresent );
276 
277  foreach ( $missingIds as $id ) {
278  $status->error( 'undelete-missing-filearchive', $id );
279  }
280 
281  if ( !$repo->hasSha1Storage() ) {
282  // Remove missing files from batch, so we don't get errors when undeleting them
283  $checkStatus = $this->removeNonexistentFiles( $storeBatch );
284  if ( !$checkStatus->isGood() ) {
285  $status->merge( $checkStatus );
286  return $status;
287  }
288  $storeBatch = $checkStatus->value;
289 
290  // Run the store batch
291  // Use the OVERWRITE_SAME flag to smooth over a common error
292  $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
293  $status->merge( $storeStatus );
294 
295  if ( !$status->isGood() ) {
296  // Even if some files could be copied, fail entirely as that is the
297  // easiest thing to do without data loss
298  $this->cleanupFailedBatch( $storeStatus, $storeBatch );
299  $status->setOK( false );
300  $this->file->unlock();
301 
302  return $status;
303  }
304  }
305 
306  // Run the DB updates
307  // Because we have locked the image row, key conflicts should be rare.
308  // If they do occur, we can roll back the transaction at this time with
309  // no data loss, but leaving unregistered files scattered throughout the
310  // public zone.
311  // This is not ideal, which is why it's important to lock the image row.
312  if ( $insertCurrent ) {
313  $dbw->insert( 'image', $insertCurrent, __METHOD__ );
314  }
315 
316  if ( $insertBatch ) {
317  $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
318  }
319 
320  if ( $deleteIds ) {
321  $dbw->delete( 'filearchive',
322  [ 'fa_id' => $deleteIds ],
323  __METHOD__ );
324  }
325 
326  // If store batch is empty (all files are missing), deletion is to be considered successful
327  if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
328  if ( !$exists ) {
329  wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current" );
330 
332 
333  $this->file->purgeEverything();
334  } else {
335  wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions" );
336  $this->file->purgeDescription();
337  }
338  }
339 
340  $this->file->unlock();
341 
342  return $status;
343  }
344 
350  protected function removeNonexistentFiles( $triplets ) {
351  $files = $filteredTriplets = [];
352  foreach ( $triplets as $file ) {
353  $files[$file[0]] = $file[0];
354  }
355 
356  $result = $this->file->repo->fileExistsBatch( $files );
357  if ( in_array( null, $result, true ) ) {
358  return Status::newFatal( 'backend-fail-internal',
359  $this->file->repo->getBackend()->getName() );
360  }
361 
362  foreach ( $triplets as $file ) {
363  if ( $result[$file[0]] ) {
364  $filteredTriplets[] = $file;
365  }
366  }
367 
368  return Status::newGood( $filteredTriplets );
369  }
370 
376  protected function removeNonexistentFromCleanup( $batch ) {
377  $files = $newBatch = [];
378  $repo = $this->file->repo;
379 
380  foreach ( $batch as $file ) {
381  $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
382  rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
383  }
384 
385  $result = $repo->fileExistsBatch( $files );
386 
387  foreach ( $batch as $file ) {
388  if ( $result[$file] ) {
389  $newBatch[] = $file;
390  }
391  }
392 
393  return $newBatch;
394  }
395 
401  public function cleanup() {
402  if ( !$this->cleanupBatch ) {
403  return $this->file->repo->newGood();
404  }
405 
406  $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
407 
408  $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
409 
410  return $status;
411  }
412 
420  protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
421  $cleanupBatch = [];
422 
423  foreach ( $storeStatus->success as $i => $success ) {
424  // Check if this item of the batch was successfully copied
425  if ( $success ) {
426  // Item was successfully copied and needs to be removed again
427  // Extract ($dstZone, $dstRel) from the batch
428  $cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
429  }
430  }
431  $this->file->repo->cleanupBatch( $cleanupBatch );
432  }
433 }
LocalFileRestoreBatch
Helper class for file undeletion.
Definition: LocalFileRestoreBatch.php:30
LocalFileRestoreBatch\$cleanupBatch
string[] $cleanupBatch
List of file IDs to restore.
Definition: LocalFileRestoreBatch.php:35
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
FileRepo\OVERWRITE_SAME
const OVERWRITE_SAME
Definition: FileRepo.php:48
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
LocalFileRestoreBatch\execute
execute()
Run the transaction, except the cleanup batch.
Definition: LocalFileRestoreBatch.php:88
LocalFileRestoreBatch\addId
addId( $fa_id)
Add a file by ID.
Definition: LocalFileRestoreBatch.php:61
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1668
LocalFileRestoreBatch\$ids
string[] $ids
List of file IDs to restore.
Definition: LocalFileRestoreBatch.php:38
LocalFileRestoreBatch\$all
bool $all
Add all revisions of the file.
Definition: LocalFileRestoreBatch.php:41
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:119
ArchivedFile\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archivedfile object.
Definition: ArchivedFile.php:247
File\splitMime
static splitMime( $mime)
Split an internet media type into its two components; if not a two-part name, set the minor type to '...
Definition: File.php:305
LocalFileRestoreBatch\$file
LocalFile $file
Definition: LocalFileRestoreBatch.php:32
$success
$success
Definition: NoLocalSettings.php:42
$wgLang
$wgLang
Definition: Setup.php:834
LocalRepo\getHashFromKey
static getHashFromKey( $key)
Gets the SHA1 hash from a storage key.
Definition: LocalRepo.php:225
LocalFileRestoreBatch\cleanupFailedBatch
cleanupFailedBatch( $storeStatus, $storeBatch)
Cleanup a failed batch.
Definition: LocalFileRestoreBatch.php:420
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
LocalFileRestoreBatch\addAll
addAll()
Add all revisions of the file.
Definition: LocalFileRestoreBatch.php:76
LocalFileRestoreBatch\$unsuppress
bool $unsuppress
Whether to remove all settings for suppressed fields.
Definition: LocalFileRestoreBatch.php:44
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:63
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
LocalFileRestoreBatch\__construct
__construct(LocalFile $file, $unsuppress=false)
Definition: LocalFileRestoreBatch.php:50
LocalFileRestoreBatch\addIds
addIds( $ids)
Add a whole lot of files by ID.
Definition: LocalFileRestoreBatch.php:69
LocalFileRestoreBatch\cleanup
cleanup()
Delete unused files in the deleted zone.
Definition: LocalFileRestoreBatch.php:401
LocalFileRestoreBatch\removeNonexistentFiles
removeNonexistentFiles( $triplets)
Removes non-existent files from a store batch.
Definition: LocalFileRestoreBatch.php:350
File\DELETED_FILE
const DELETED_FILE
Definition: File.php:70
$mime
$mime
Definition: router.php:60
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
LocalFileRestoreBatch\removeNonexistentFromCleanup
removeNonexistentFromCleanup( $batch)
Removes non-existent files from a cleanup batch.
Definition: LocalFileRestoreBatch.php:376