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