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