MediaWiki  master
LocalFileDeleteBatch.php
Go to the documentation of this file.
1 <?php
27 use Wikimedia\ScopedCallback;
28 
35  private $file;
36 
38  private $reason;
39 
41  private $srcRels = [];
42 
44  private $archiveUrls = [];
45 
47  private $deletionBatch;
48 
50  private $suppress;
51 
53  private $user;
54 
61  public function __construct(
62  File $file,
64  $reason = '',
65  $suppress = false
66  ) {
67  $this->file = $file;
68  $this->user = $user;
69  $this->reason = $reason;
70  $this->suppress = $suppress;
71  }
72 
73  public function addCurrent() {
74  $this->srcRels['.'] = $this->file->getRel();
75  }
76 
80  public function addOld( $oldName ) {
81  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
82  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
83  }
84 
89  public function addOlds() {
90  $archiveNames = [];
91 
92  $dbw = $this->file->repo->getPrimaryDB();
93  $result = $dbw->select( 'oldimage',
94  [ 'oi_archive_name' ],
95  [ 'oi_name' => $this->file->getName() ],
96  __METHOD__
97  );
98 
99  foreach ( $result as $row ) {
100  $this->addOld( $row->oi_archive_name );
101  $archiveNames[] = $row->oi_archive_name;
102  }
103 
104  return $archiveNames;
105  }
106 
110  protected function getOldRels() {
111  if ( !isset( $this->srcRels['.'] ) ) {
112  $oldRels =& $this->srcRels;
113  $deleteCurrent = false;
114  } else {
115  $oldRels = $this->srcRels;
116  unset( $oldRels['.'] );
117  $deleteCurrent = true;
118  }
119 
120  return [ $oldRels, $deleteCurrent ];
121  }
122 
127  protected function getHashes( StatusValue $status ): array {
128  $hashes = [];
129  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
130 
131  if ( $deleteCurrent ) {
132  $hashes['.'] = $this->file->getSha1();
133  }
134 
135  if ( count( $oldRels ) ) {
136  $dbw = $this->file->repo->getPrimaryDB();
137  $res = $dbw->select(
138  'oldimage',
139  [ 'oi_archive_name', 'oi_sha1' ],
140  [ 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) ),
141  'oi_name' => $this->file->getName() ], // performance
142  __METHOD__
143  );
144 
145  foreach ( $res as $row ) {
146  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
147  // Get the hash from the file
148  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
149  $props = $this->file->repo->getFileProps( $oldUrl );
150 
151  if ( $props['fileExists'] ) {
152  // Upgrade the oldimage row
153  $dbw->update( 'oldimage',
154  [ 'oi_sha1' => $props['sha1'] ],
155  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
156  __METHOD__ );
157  $hashes[$row->oi_archive_name] = $props['sha1'];
158  } else {
159  $hashes[$row->oi_archive_name] = false;
160  }
161  } else {
162  $hashes[$row->oi_archive_name] = $row->oi_sha1;
163  }
164  }
165  }
166 
167  $missing = array_diff_key( $this->srcRels, $hashes );
168 
169  foreach ( $missing as $name => $rel ) {
170  $status->error( 'filedelete-old-unregistered', $name );
171  }
172 
173  foreach ( $hashes as $name => $hash ) {
174  if ( !$hash ) {
175  $status->error( 'filedelete-missing', $this->srcRels[$name] );
176  unset( $hashes[$name] );
177  }
178  }
179 
180  return $hashes;
181  }
182 
183  protected function doDBInserts() {
184  $now = time();
185  $dbw = $this->file->repo->getPrimaryDB();
186 
187  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
188 
189  $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
190  $encUserId = $dbw->addQuotes( $this->user->getId() );
191  $encGroup = $dbw->addQuotes( 'deleted' );
192  $ext = $this->file->getExtension();
193  $dotExt = $ext === '' ? '' : ".$ext";
194  $encExt = $dbw->addQuotes( $dotExt );
195  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
196 
197  // Bitfields to further suppress the content
198  if ( $this->suppress ) {
199  $bitfield = RevisionRecord::SUPPRESSED_ALL;
200  } else {
201  $bitfield = 'oi_deleted';
202  }
203 
204  if ( $deleteCurrent ) {
205  $tables = [ 'image' ];
206  $fields = [
207  'fa_storage_group' => $encGroup,
208  'fa_storage_key' => $dbw->conditional(
209  [ 'img_sha1' => '' ],
210  $dbw->addQuotes( '' ),
211  $dbw->buildConcat( [ "img_sha1", $encExt ] )
212  ),
213  'fa_deleted_user' => $encUserId,
214  'fa_deleted_timestamp' => $encTimestamp,
215  'fa_deleted' => $this->suppress ? $bitfield : 0,
216  'fa_name' => 'img_name',
217  'fa_archive_name' => 'NULL',
218  'fa_size' => 'img_size',
219  'fa_width' => 'img_width',
220  'fa_height' => 'img_height',
221  'fa_metadata' => 'img_metadata',
222  'fa_bits' => 'img_bits',
223  'fa_media_type' => 'img_media_type',
224  'fa_major_mime' => 'img_major_mime',
225  'fa_minor_mime' => 'img_minor_mime',
226  'fa_description_id' => 'img_description_id',
227  'fa_timestamp' => 'img_timestamp',
228  'fa_sha1' => 'img_sha1',
229  'fa_actor' => 'img_actor',
230  ];
231  $joins = [];
232 
233  $fields += array_map(
234  [ $dbw, 'addQuotes' ],
235  $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
236  );
237 
238  $dbw->insertSelect( 'filearchive', $tables, $fields,
239  [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
240  }
241 
242  if ( count( $oldRels ) ) {
243  $fileQuery = OldLocalFile::getQueryInfo();
244  $res = $dbw->select(
245  $fileQuery['tables'],
246  $fileQuery['fields'],
247  [
248  'oi_name' => $this->file->getName(),
249  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
250  ],
251  __METHOD__,
252  [ 'FOR UPDATE' ],
253  $fileQuery['joins']
254  );
255  $rowsInsert = [];
256  if ( $res->numRows() ) {
257  $reason = $commentStore->createComment( $dbw, $this->reason );
258  foreach ( $res as $row ) {
259  $comment = $commentStore->getComment( 'oi_description', $row );
260  $rowsInsert[] = [
261  // Deletion-specific fields
262  'fa_storage_group' => 'deleted',
263  'fa_storage_key' => ( $row->oi_sha1 === '' )
264  ? ''
265  : "{$row->oi_sha1}{$dotExt}",
266  'fa_deleted_user' => $this->user->getId(),
267  'fa_deleted_timestamp' => $dbw->timestamp( $now ),
268  // Counterpart fields
269  'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
270  'fa_name' => $row->oi_name,
271  'fa_archive_name' => $row->oi_archive_name,
272  'fa_size' => $row->oi_size,
273  'fa_width' => $row->oi_width,
274  'fa_height' => $row->oi_height,
275  'fa_metadata' => $row->oi_metadata,
276  'fa_bits' => $row->oi_bits,
277  'fa_media_type' => $row->oi_media_type,
278  'fa_major_mime' => $row->oi_major_mime,
279  'fa_minor_mime' => $row->oi_minor_mime,
280  'fa_actor' => $row->oi_actor,
281  'fa_timestamp' => $row->oi_timestamp,
282  'fa_sha1' => $row->oi_sha1
283  ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
284  + $commentStore->insert( $dbw, 'fa_description', $comment );
285  }
286  }
287 
288  $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
289  }
290  }
291 
292  private function doDBDeletes() {
293  $dbw = $this->file->repo->getPrimaryDB();
294  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
295 
296  if ( count( $oldRels ) ) {
297  $dbw->delete( 'oldimage',
298  [
299  'oi_name' => $this->file->getName(),
300  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
301  ], __METHOD__ );
302  }
303 
304  if ( $deleteCurrent ) {
305  $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
306  }
307  }
308 
313  public function execute() {
314  $repo = $this->file->getRepo();
315  $lockStatus = $this->file->acquireFileLock();
316  if ( !$lockStatus->isOK() ) {
317  return $lockStatus;
318  }
319  $unlockScope = new ScopedCallback( function () {
320  $this->file->releaseFileLock();
321  } );
322 
323  $status = $this->file->repo->newGood();
324  // Prepare deletion batch
325  $hashes = $this->getHashes( $status );
326  $this->deletionBatch = [];
327  $ext = $this->file->getExtension();
328  $dotExt = $ext === '' ? '' : ".$ext";
329 
330  foreach ( $this->srcRels as $name => $srcRel ) {
331  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
332  if ( isset( $hashes[$name] ) ) {
333  $hash = $hashes[$name];
334  $key = $hash . $dotExt;
335  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
336  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
337  }
338  }
339 
340  if ( !$repo->hasSha1Storage() ) {
341  // Removes non-existent file from the batch, so we don't get errors.
342  // This also handles files in the 'deleted' zone deleted via revision deletion.
343  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
344  if ( !$checkStatus->isGood() ) {
345  $status->merge( $checkStatus );
346  return $status;
347  }
348  $this->deletionBatch = $checkStatus->value;
349 
350  // Execute the file deletion batch
351  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
352  if ( !$status->isGood() ) {
353  $status->merge( $status );
354  }
355  }
356 
357  if ( !$status->isOK() ) {
358  // Critical file deletion error; abort
359  return $status;
360  }
361 
362  $dbw = $this->file->repo->getPrimaryDB();
363 
364  $dbw->startAtomic( __METHOD__ );
365 
366  // Copy the image/oldimage rows to filearchive
367  $this->doDBInserts();
368  // Delete image/oldimage rows
369  $this->doDBDeletes();
370 
371  // This is typically a no-op since we are wrapped by another atomic
372  // section in FileDeleteForm and also the implicit transaction.
373  $dbw->endAtomic( __METHOD__ );
374 
375  // Commit and return
376  ScopedCallback::consume( $unlockScope );
377 
378  return $status;
379  }
380 
386  protected function removeNonexistentFiles( $batch ) {
387  $files = [];
388 
389  foreach ( $batch as [ $src, /* dest */ ] ) {
390  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
391  }
392 
393  $result = $this->file->repo->fileExistsBatch( $files );
394  if ( in_array( null, $result, true ) ) {
395  return Status::newFatal( 'backend-fail-internal',
396  $this->file->repo->getBackend()->getName() );
397  }
398 
399  $newBatch = [];
400  foreach ( $batch as $batchItem ) {
401  if ( $result[$batchItem[0]] ) {
402  $newBatch[] = $batchItem;
403  }
404  }
405 
406  return Status::newGood( $newBatch );
407  }
408 }
LocalFileDeleteBatch\$reason
string $reason
Definition: LocalFileDeleteBatch.php:38
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
LocalFileDeleteBatch\addOld
addOld( $oldName)
Definition: LocalFileDeleteBatch.php:80
LocalFileDeleteBatch\doDBDeletes
doDBDeletes()
Definition: LocalFileDeleteBatch.php:292
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
StatusValue\error
error( $message,... $parameters)
Add an error, do not set fatal flag This can be used for non-fatal errors.
Definition: StatusValue.php:250
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:137
LocalFileDeleteBatch
Helper class for file deletion.
Definition: LocalFileDeleteBatch.php:33
LocalFileDeleteBatch\execute
execute()
Run the transaction.
Definition: LocalFileDeleteBatch.php:313
LocalFileDeleteBatch\$user
UserIdentity $user
Definition: LocalFileDeleteBatch.php:53
LocalFileDeleteBatch\__construct
__construct(File $file, UserIdentity $user, $reason='', $suppress=false)
Definition: LocalFileDeleteBatch.php:61
$res
$res
Definition: testCompression.php:57
LocalFileDeleteBatch\$deletionBatch
array[] $deletionBatch
Items to be processed in the deletion batch.
Definition: LocalFileDeleteBatch.php:47
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
LocalFileDeleteBatch\addCurrent
addCurrent()
Definition: LocalFileDeleteBatch.php:73
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:67
LocalFileDeleteBatch\$file
LocalFile $file
Definition: LocalFileDeleteBatch.php:35
LocalFileDeleteBatch\$suppress
bool $suppress
Whether to suppress all suppressable fields when deleting.
Definition: LocalFileDeleteBatch.php:50
LocalFileDeleteBatch\getHashes
getHashes(StatusValue $status)
Definition: LocalFileDeleteBatch.php:127
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
LocalFileDeleteBatch\$archiveUrls
array $archiveUrls
Definition: LocalFileDeleteBatch.php:44
LocalFileDeleteBatch\addOlds
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFileDeleteBatch.php:89
LocalFileDeleteBatch\$srcRels
array $srcRels
Definition: LocalFileDeleteBatch.php:41
LocalFileDeleteBatch\getOldRels
getOldRels()
Definition: LocalFileDeleteBatch.php:110
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
$hashes
$hashes
Definition: testCompression.php:71
LocalFileDeleteBatch\removeNonexistentFiles
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
Definition: LocalFileDeleteBatch.php:386
LocalFileDeleteBatch\doDBInserts
doDBInserts()
Definition: LocalFileDeleteBatch.php:183