MediaWiki  1.34.0
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 
62  function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
63  $this->file = $file;
64  $this->reason = $reason;
65  $this->suppress = $suppress;
66  global $wgUser;
67  $this->user = $user ?: $wgUser;
68  $this->status = $file->repo->newGood();
69  }
70 
71  public function addCurrent() {
72  $this->srcRels['.'] = $this->file->getRel();
73  }
74 
78  public function addOld( $oldName ) {
79  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
80  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
81  }
82 
87  public function addOlds() {
88  $archiveNames = [];
89 
90  $dbw = $this->file->repo->getMasterDB();
91  $result = $dbw->select( 'oldimage',
92  [ 'oi_archive_name' ],
93  [ 'oi_name' => $this->file->getName() ],
94  __METHOD__
95  );
96 
97  foreach ( $result as $row ) {
98  $this->addOld( $row->oi_archive_name );
99  $archiveNames[] = $row->oi_archive_name;
100  }
101 
102  return $archiveNames;
103  }
104 
108  protected function getOldRels() {
109  if ( !isset( $this->srcRels['.'] ) ) {
110  $oldRels =& $this->srcRels;
111  $deleteCurrent = false;
112  } else {
113  $oldRels = $this->srcRels;
114  unset( $oldRels['.'] );
115  $deleteCurrent = true;
116  }
117 
118  return [ $oldRels, $deleteCurrent ];
119  }
120 
124  protected function getHashes() {
125  $hashes = [];
126  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
127 
128  if ( $deleteCurrent ) {
129  $hashes['.'] = $this->file->getSha1();
130  }
131 
132  if ( count( $oldRels ) ) {
133  $dbw = $this->file->repo->getMasterDB();
134  $res = $dbw->select(
135  'oldimage',
136  [ 'oi_archive_name', 'oi_sha1' ],
137  [ 'oi_archive_name' => array_keys( $oldRels ),
138  'oi_name' => $this->file->getName() ], // performance
139  __METHOD__
140  );
141 
142  foreach ( $res as $row ) {
143  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
144  // Get the hash from the file
145  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
146  $props = $this->file->repo->getFileProps( $oldUrl );
147 
148  if ( $props['fileExists'] ) {
149  // Upgrade the oldimage row
150  $dbw->update( 'oldimage',
151  [ 'oi_sha1' => $props['sha1'] ],
152  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
153  __METHOD__ );
154  $hashes[$row->oi_archive_name] = $props['sha1'];
155  } else {
156  $hashes[$row->oi_archive_name] = false;
157  }
158  } else {
159  $hashes[$row->oi_archive_name] = $row->oi_sha1;
160  }
161  }
162  }
163 
164  $missing = array_diff_key( $this->srcRels, $hashes );
165 
166  foreach ( $missing as $name => $rel ) {
167  $this->status->error( 'filedelete-old-unregistered', $name );
168  }
169 
170  foreach ( $hashes as $name => $hash ) {
171  if ( !$hash ) {
172  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
173  unset( $hashes[$name] );
174  }
175  }
176 
177  return $hashes;
178  }
179 
180  protected function doDBInserts() {
181  $now = time();
182  $dbw = $this->file->repo->getMasterDB();
183 
184  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
185  $actorMigration = ActorMigration::newMigration();
186 
187  $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
188  $encUserId = $dbw->addQuotes( $this->user->getId() );
189  $encGroup = $dbw->addQuotes( 'deleted' );
190  $ext = $this->file->getExtension();
191  $dotExt = $ext === '' ? '' : ".$ext";
192  $encExt = $dbw->addQuotes( $dotExt );
193  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
194 
195  // Bitfields to further suppress the content
196  if ( $this->suppress ) {
197  $bitfield = RevisionRecord::SUPPRESSED_ALL;
198  } else {
199  $bitfield = 'oi_deleted';
200  }
201 
202  if ( $deleteCurrent ) {
203  $tables = [ 'image' ];
204  $fields = [
205  'fa_storage_group' => $encGroup,
206  'fa_storage_key' => $dbw->conditional(
207  [ 'img_sha1' => '' ],
208  $dbw->addQuotes( '' ),
209  $dbw->buildConcat( [ "img_sha1", $encExt ] )
210  ),
211  'fa_deleted_user' => $encUserId,
212  'fa_deleted_timestamp' => $encTimestamp,
213  'fa_deleted' => $this->suppress ? $bitfield : 0,
214  'fa_name' => 'img_name',
215  'fa_archive_name' => 'NULL',
216  'fa_size' => 'img_size',
217  'fa_width' => 'img_width',
218  'fa_height' => 'img_height',
219  'fa_metadata' => 'img_metadata',
220  'fa_bits' => 'img_bits',
221  'fa_media_type' => 'img_media_type',
222  'fa_major_mime' => 'img_major_mime',
223  'fa_minor_mime' => 'img_minor_mime',
224  'fa_description_id' => 'img_description_id',
225  'fa_timestamp' => 'img_timestamp',
226  'fa_sha1' => 'img_sha1',
227  'fa_actor' => 'img_actor',
228  ];
229  $joins = [];
230 
231  $fields += array_map(
232  [ $dbw, 'addQuotes' ],
233  $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
234  );
235 
236  $dbw->insertSelect( 'filearchive', $tables, $fields,
237  [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
238  }
239 
240  if ( count( $oldRels ) ) {
241  $fileQuery = OldLocalFile::getQueryInfo();
242  $res = $dbw->select(
243  $fileQuery['tables'],
244  $fileQuery['fields'],
245  [
246  'oi_name' => $this->file->getName(),
247  'oi_archive_name' => array_keys( $oldRels )
248  ],
249  __METHOD__,
250  [ 'FOR UPDATE' ],
251  $fileQuery['joins']
252  );
253  $rowsInsert = [];
254  if ( $res->numRows() ) {
255  $reason = $commentStore->createComment( $dbw, $this->reason );
256  foreach ( $res as $row ) {
257  $comment = $commentStore->getComment( 'oi_description', $row );
258  $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
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_timestamp' => $row->oi_timestamp,
280  'fa_sha1' => $row->oi_sha1
281  ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
282  + $commentStore->insert( $dbw, 'fa_description', $comment )
283  + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
284  }
285  }
286 
287  $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
288  }
289  }
290 
291  function doDBDeletes() {
292  $dbw = $this->file->repo->getMasterDB();
293  list( $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_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  $this->file->lock();
315 
316  // Prepare deletion batch
317  $hashes = $this->getHashes();
318  $this->deletionBatch = [];
319  $ext = $this->file->getExtension();
320  $dotExt = $ext === '' ? '' : ".$ext";
321 
322  foreach ( $this->srcRels as $name => $srcRel ) {
323  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
324  if ( isset( $hashes[$name] ) ) {
325  $hash = $hashes[$name];
326  $key = $hash . $dotExt;
327  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
328  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
329  }
330  }
331 
332  if ( !$repo->hasSha1Storage() ) {
333  // Removes non-existent file from the batch, so we don't get errors.
334  // This also handles files in the 'deleted' zone deleted via revision deletion.
335  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
336  if ( !$checkStatus->isGood() ) {
337  $this->status->merge( $checkStatus );
338  return $this->status;
339  }
340  $this->deletionBatch = $checkStatus->value;
341 
342  // Execute the file deletion batch
343  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
344  if ( !$status->isGood() ) {
345  $this->status->merge( $status );
346  }
347  }
348 
349  if ( !$this->status->isOK() ) {
350  // Critical file deletion error; abort
351  $this->file->unlock();
352 
353  return $this->status;
354  }
355 
356  // Copy the image/oldimage rows to filearchive
357  $this->doDBInserts();
358  // Delete image/oldimage rows
359  $this->doDBDeletes();
360 
361  // Commit and return
362  $this->file->unlock();
363 
364  return $this->status;
365  }
366 
372  protected function removeNonexistentFiles( $batch ) {
373  $files = $newBatch = [];
374 
375  foreach ( $batch as $batchItem ) {
376  list( $src, ) = $batchItem;
377  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
378  }
379 
380  $result = $this->file->repo->fileExistsBatch( $files );
381  if ( in_array( null, $result, true ) ) {
382  return Status::newFatal( 'backend-fail-internal',
383  $this->file->repo->getBackend()->getName() );
384  }
385 
386  foreach ( $batch as $batchItem ) {
387  if ( $result[$batchItem[0]] ) {
388  $newBatch[] = $batchItem;
389  }
390  }
391 
392  return Status::newGood( $newBatch );
393  }
394 }
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:69
LocalFileDeleteBatch\addOld
addOld( $oldName)
Definition: LocalFileDeleteBatch.php:78
LocalFileDeleteBatch\doDBDeletes
doDBDeletes()
Definition: LocalFileDeleteBatch.php:291
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
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:120
LocalFileDeleteBatch
Helper class for file deletion.
Definition: LocalFileDeleteBatch.php:31
LocalFileDeleteBatch\execute
execute()
Run the transaction.
Definition: LocalFileDeleteBatch.php:312
$res
$res
Definition: testCompression.php:52
LocalFileDeleteBatch\$status
Status $status
Definition: LocalFileDeleteBatch.php:51
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:136
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:40
StatusValue\isGood
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: StatusValue.php:121
LocalFileDeleteBatch\addCurrent
addCurrent()
Definition: LocalFileDeleteBatch.php:71
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:61
LocalFileDeleteBatch\$file
LocalFile $file
Definition: LocalFileDeleteBatch.php:33
StatusValue\merge
merge( $other, $overwriteValue=false)
Merge another status object into this one.
Definition: StatusValue.php:223
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:596
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:56
LocalFileDeleteBatch\getHashes
getHashes()
Definition: LocalFileDeleteBatch.php:124
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
LocalFileDeleteBatch\$archiveUrls
array $archiveUrls
Definition: LocalFileDeleteBatch.php:42
LocalFileDeleteBatch\addOlds
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFileDeleteBatch.php:87
LocalFileDeleteBatch\$srcRels
array $srcRels
Definition: LocalFileDeleteBatch.php:39
LocalFileDeleteBatch\getOldRels
getOldRels()
Definition: LocalFileDeleteBatch.php:108
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
$hashes
$hashes
Definition: testCompression.php:66
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
LocalFileDeleteBatch\__construct
__construct(File $file, $reason='', $suppress=false, $user=null)
Definition: LocalFileDeleteBatch.php:62
LocalFileDeleteBatch\removeNonexistentFiles
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
Definition: LocalFileDeleteBatch.php:372
LocalFileDeleteBatch\doDBInserts
doDBInserts()
Definition: LocalFileDeleteBatch.php:180