MediaWiki  master
LocalFileDeleteBatch.php
Go to the documentation of this file.
1 <?php
26 
33  private $file;
34 
36  private $reason;
37 
39  private $srcRels = [];
40 
42  private $archiveUrls = [];
43 
45  private $deletionBatch;
46 
48  private $suppress;
49 
51  private $status;
52 
54  private $user;
55 
67  public function __construct(
68  File $file,
69  $param2 = '',
70  $param3 = '',
71  $param4 = false
72  ) {
73  $this->file = $file;
74 
75  if ( $param2 instanceof User ) {
76  // New signature
77  $user = $param2;
78  $reason = $param3;
79  $suppress = $param4;
80  } else {
81  // Old signature
83  'Construction of ' . __CLASS__ . ' without passing a user as ' .
84  'the second parameter was deprecated in MediaWiki 1.35. ' .
85  'See T245710 for more',
86  '1.35'
87  );
88 
89  $reason = $param2;
90 
91  // Suppress defaults to false if not provided
92  $suppress = ( $param3 === '' ? false : $param3 );
93 
94  if ( $param4 === false ) {
95  global $wgUser;
96  $user = $wgUser;
97  } else {
98  $user = $param4;
99  }
100  }
101 
102  $this->reason = $reason;
103  $this->suppress = $suppress;
104  $this->user = $user;
105  $this->status = $file->repo->newGood();
106  }
107 
108  public function addCurrent() {
109  $this->srcRels['.'] = $this->file->getRel();
110  }
111 
115  public function addOld( $oldName ) {
116  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
117  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
118  }
119 
124  public function addOlds() {
125  $archiveNames = [];
126 
127  $dbw = $this->file->repo->getMasterDB();
128  $result = $dbw->select( 'oldimage',
129  [ 'oi_archive_name' ],
130  [ 'oi_name' => $this->file->getName() ],
131  __METHOD__
132  );
133 
134  foreach ( $result as $row ) {
135  $this->addOld( $row->oi_archive_name );
136  $archiveNames[] = $row->oi_archive_name;
137  }
138 
139  return $archiveNames;
140  }
141 
145  protected function getOldRels() {
146  if ( !isset( $this->srcRels['.'] ) ) {
147  $oldRels =& $this->srcRels;
148  $deleteCurrent = false;
149  } else {
150  $oldRels = $this->srcRels;
151  unset( $oldRels['.'] );
152  $deleteCurrent = true;
153  }
154 
155  return [ $oldRels, $deleteCurrent ];
156  }
157 
161  protected function getHashes() {
162  $hashes = [];
163  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
164 
165  if ( $deleteCurrent ) {
166  $hashes['.'] = $this->file->getSha1();
167  }
168 
169  if ( count( $oldRels ) ) {
170  $dbw = $this->file->repo->getMasterDB();
171  $res = $dbw->select(
172  'oldimage',
173  [ 'oi_archive_name', 'oi_sha1' ],
174  [ 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) ),
175  'oi_name' => $this->file->getName() ], // performance
176  __METHOD__
177  );
178 
179  foreach ( $res as $row ) {
180  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
181  // Get the hash from the file
182  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
183  $props = $this->file->repo->getFileProps( $oldUrl );
184 
185  if ( $props['fileExists'] ) {
186  // Upgrade the oldimage row
187  $dbw->update( 'oldimage',
188  [ 'oi_sha1' => $props['sha1'] ],
189  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
190  __METHOD__ );
191  $hashes[$row->oi_archive_name] = $props['sha1'];
192  } else {
193  $hashes[$row->oi_archive_name] = false;
194  }
195  } else {
196  $hashes[$row->oi_archive_name] = $row->oi_sha1;
197  }
198  }
199  }
200 
201  $missing = array_diff_key( $this->srcRels, $hashes );
202 
203  foreach ( $missing as $name => $rel ) {
204  $this->status->error( 'filedelete-old-unregistered', $name );
205  }
206 
207  foreach ( $hashes as $name => $hash ) {
208  if ( !$hash ) {
209  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
210  unset( $hashes[$name] );
211  }
212  }
213 
214  return $hashes;
215  }
216 
217  protected function doDBInserts() {
218  $now = time();
219  $dbw = $this->file->repo->getMasterDB();
220 
221  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
222  $actorMigration = ActorMigration::newMigration();
223 
224  $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
225  $encUserId = $dbw->addQuotes( $this->user->getId() );
226  $encGroup = $dbw->addQuotes( 'deleted' );
227  $ext = $this->file->getExtension();
228  $dotExt = $ext === '' ? '' : ".$ext";
229  $encExt = $dbw->addQuotes( $dotExt );
230  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
231 
232  // Bitfields to further suppress the content
233  if ( $this->suppress ) {
234  $bitfield = RevisionRecord::SUPPRESSED_ALL;
235  } else {
236  $bitfield = 'oi_deleted';
237  }
238 
239  if ( $deleteCurrent ) {
240  $tables = [ 'image' ];
241  $fields = [
242  'fa_storage_group' => $encGroup,
243  'fa_storage_key' => $dbw->conditional(
244  [ 'img_sha1' => '' ],
245  $dbw->addQuotes( '' ),
246  $dbw->buildConcat( [ "img_sha1", $encExt ] )
247  ),
248  'fa_deleted_user' => $encUserId,
249  'fa_deleted_timestamp' => $encTimestamp,
250  'fa_deleted' => $this->suppress ? $bitfield : 0,
251  'fa_name' => 'img_name',
252  'fa_archive_name' => 'NULL',
253  'fa_size' => 'img_size',
254  'fa_width' => 'img_width',
255  'fa_height' => 'img_height',
256  'fa_metadata' => 'img_metadata',
257  'fa_bits' => 'img_bits',
258  'fa_media_type' => 'img_media_type',
259  'fa_major_mime' => 'img_major_mime',
260  'fa_minor_mime' => 'img_minor_mime',
261  'fa_description_id' => 'img_description_id',
262  'fa_timestamp' => 'img_timestamp',
263  'fa_sha1' => 'img_sha1',
264  'fa_actor' => 'img_actor',
265  ];
266  $joins = [];
267 
268  $fields += array_map(
269  [ $dbw, 'addQuotes' ],
270  $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
271  );
272 
273  $dbw->insertSelect( 'filearchive', $tables, $fields,
274  [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
275  }
276 
277  if ( count( $oldRels ) ) {
278  $fileQuery = OldLocalFile::getQueryInfo();
279  $res = $dbw->select(
280  $fileQuery['tables'],
281  $fileQuery['fields'],
282  [
283  'oi_name' => $this->file->getName(),
284  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
285  ],
286  __METHOD__,
287  [ 'FOR UPDATE' ],
288  $fileQuery['joins']
289  );
290  $rowsInsert = [];
291  if ( $res->numRows() ) {
292  $reason = $commentStore->createComment( $dbw, $this->reason );
293  foreach ( $res as $row ) {
294  $comment = $commentStore->getComment( 'oi_description', $row );
295  $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
296  $rowsInsert[] = [
297  // Deletion-specific fields
298  'fa_storage_group' => 'deleted',
299  'fa_storage_key' => ( $row->oi_sha1 === '' )
300  ? ''
301  : "{$row->oi_sha1}{$dotExt}",
302  'fa_deleted_user' => $this->user->getId(),
303  'fa_deleted_timestamp' => $dbw->timestamp( $now ),
304  // Counterpart fields
305  'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
306  'fa_name' => $row->oi_name,
307  'fa_archive_name' => $row->oi_archive_name,
308  'fa_size' => $row->oi_size,
309  'fa_width' => $row->oi_width,
310  'fa_height' => $row->oi_height,
311  'fa_metadata' => $row->oi_metadata,
312  'fa_bits' => $row->oi_bits,
313  'fa_media_type' => $row->oi_media_type,
314  'fa_major_mime' => $row->oi_major_mime,
315  'fa_minor_mime' => $row->oi_minor_mime,
316  'fa_timestamp' => $row->oi_timestamp,
317  'fa_sha1' => $row->oi_sha1
318  ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
319  + $commentStore->insert( $dbw, 'fa_description', $comment )
320  + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
321  }
322  }
323 
324  $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
325  }
326  }
327 
328  private function doDBDeletes() {
329  $dbw = $this->file->repo->getMasterDB();
330  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
331 
332  if ( count( $oldRels ) ) {
333  $dbw->delete( 'oldimage',
334  [
335  'oi_name' => $this->file->getName(),
336  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
337  ], __METHOD__ );
338  }
339 
340  if ( $deleteCurrent ) {
341  $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
342  }
343  }
344 
349  public function execute() {
350  $repo = $this->file->getRepo();
351  $this->file->lock();
352 
353  // Prepare deletion batch
354  $hashes = $this->getHashes();
355  $this->deletionBatch = [];
356  $ext = $this->file->getExtension();
357  $dotExt = $ext === '' ? '' : ".$ext";
358 
359  foreach ( $this->srcRels as $name => $srcRel ) {
360  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
361  if ( isset( $hashes[$name] ) ) {
362  $hash = $hashes[$name];
363  $key = $hash . $dotExt;
364  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
365  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
366  }
367  }
368 
369  if ( !$repo->hasSha1Storage() ) {
370  // Removes non-existent file from the batch, so we don't get errors.
371  // This also handles files in the 'deleted' zone deleted via revision deletion.
372  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
373  if ( !$checkStatus->isGood() ) {
374  $this->status->merge( $checkStatus );
375  return $this->status;
376  }
377  $this->deletionBatch = $checkStatus->value;
378 
379  // Execute the file deletion batch
380  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
381  if ( !$status->isGood() ) {
382  $this->status->merge( $status );
383  }
384  }
385 
386  if ( !$this->status->isOK() ) {
387  // Critical file deletion error; abort
388  $this->file->unlock();
389 
390  return $this->status;
391  }
392 
393  // Copy the image/oldimage rows to filearchive
394  $this->doDBInserts();
395  // Delete image/oldimage rows
396  $this->doDBDeletes();
397 
398  // Commit and return
399  $this->file->unlock();
400 
401  return $this->status;
402  }
403 
409  protected function removeNonexistentFiles( $batch ) {
410  $files = $newBatch = [];
411 
412  foreach ( $batch as $batchItem ) {
413  list( $src, ) = $batchItem;
414  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
415  }
416 
417  $result = $this->file->repo->fileExistsBatch( $files );
418  if ( in_array( null, $result, true ) ) {
419  return Status::newFatal( 'backend-fail-internal',
420  $this->file->repo->getBackend()->getName() );
421  }
422 
423  foreach ( $batch as $batchItem ) {
424  if ( $result[$batchItem[0]] ) {
425  $newBatch[] = $batchItem;
426  }
427  }
428 
429  return Status::newGood( $newBatch );
430  }
431 }
LocalFileDeleteBatch\$reason
string $reason
Definition: LocalFileDeleteBatch.php:36
LocalFileDeleteBatch\$deletionBatch
array $deletionBatch
Items to be processed in the deletion batch.
Definition: LocalFileDeleteBatch.php:45
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
LocalFileDeleteBatch\addOld
addOld( $oldName)
Definition: LocalFileDeleteBatch.php:115
LocalFileDeleteBatch\doDBDeletes
doDBDeletes()
Definition: LocalFileDeleteBatch.php:328
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
OldLocalFile\getQueryInfo
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new oldlocalfile object.
Definition: OldLocalFile.php:130
LocalFileDeleteBatch
Helper class for file deletion.
Definition: LocalFileDeleteBatch.php:31
LocalFileDeleteBatch\execute
execute()
Run the transaction.
Definition: LocalFileDeleteBatch.php:349
$res
$res
Definition: testCompression.php:57
LocalFileDeleteBatch\$status
Status $status
Definition: LocalFileDeleteBatch.php:51
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:139
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
StatusValue\isGood
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: StatusValue.php:122
LocalFileDeleteBatch\addCurrent
addCurrent()
Definition: LocalFileDeleteBatch.php:108
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:63
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1058
LocalFileDeleteBatch\__construct
__construct(File $file, $param2='', $param3='', $param4=false)
Definition: LocalFileDeleteBatch.php:67
LocalFileDeleteBatch\$file
LocalFile $file
Definition: LocalFileDeleteBatch.php:33
StatusValue\merge
merge( $other, $overwriteValue=false)
Merge another status object into this one.
Definition: StatusValue.php:224
LocalFileDeleteBatch\$suppress
bool $suppress
Whether to suppress all suppressable fields when deleting.
Definition: LocalFileDeleteBatch.php:48
LocalFileDeleteBatch\$user
User $user
Definition: LocalFileDeleteBatch.php:54
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:615
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:59
LocalFileDeleteBatch\getHashes
getHashes()
Definition: LocalFileDeleteBatch.php:161
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
LocalFileDeleteBatch\$archiveUrls
array $archiveUrls
Definition: LocalFileDeleteBatch.php:42
LocalFileDeleteBatch\addOlds
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFileDeleteBatch.php:124
LocalFileDeleteBatch\$srcRels
array $srcRels
Definition: LocalFileDeleteBatch.php:39
LocalFileDeleteBatch\getOldRels
getOldRels()
Definition: LocalFileDeleteBatch.php:145
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
$hashes
$hashes
Definition: testCompression.php:71
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:59
LocalFileDeleteBatch\removeNonexistentFiles
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
Definition: LocalFileDeleteBatch.php:409
LocalFileDeleteBatch\doDBInserts
doDBInserts()
Definition: LocalFileDeleteBatch.php:217