MediaWiki REL1_37
LocalFileRestoreBatch.php
Go to the documentation of this file.
1<?php
25
32 private $file;
33
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
331 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
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}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$wgLang
Definition Setup.php:831
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archivedfile object.
const OVERWRITE_SAME
Definition FileRepo.php:48
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:42
Helper class for file undeletion.
execute()
Run the transaction, except the cleanup batch.
addIds( $ids)
Add a whole lot of files by ID.
addAll()
Add all revisions of the file.
string[] $ids
List of file IDs to restore.
bool $unsuppress
Whether to remove all settings for suppressed fields.
removeNonexistentFromCleanup( $batch)
Removes non-existent files from a cleanup batch.
addId( $fa_id)
Add a file by ID.
cleanup()
Delete unused files in the deleted zone.
removeNonexistentFiles( $triplets)
Removes non-existent files from a store batch.
string[] $cleanupBatch
List of file IDs to restore.
cleanupFailedBatch( $storeStatus, $storeBatch)
Cleanup a failed batch.
bool $all
Add all revisions of the file.
__construct(LocalFile $file, $unsuppress=false)
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:63
static getHashFromKey( $key)
Gets the SHA1 hash from a storage key.
MediaWikiServices is the service locator for the application scope of MediaWiki.
$mime
Definition router.php:60