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 
62  public function __construct(
63  File $file,
64  User $user,
65  $reason = '',
66  $suppress = false
67  ) {
68  $this->file = $file;
69  $this->user = $user;
70  $this->reason = $reason;
71  $this->suppress = $suppress;
72  $this->status = $file->repo->newGood();
73  }
74 
75  public function addCurrent() {
76  $this->srcRels['.'] = $this->file->getRel();
77  }
78 
82  public function addOld( $oldName ) {
83  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
84  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
85  }
86 
91  public function addOlds() {
92  $archiveNames = [];
93 
94  $dbw = $this->file->repo->getMasterDB();
95  $result = $dbw->select( 'oldimage',
96  [ 'oi_archive_name' ],
97  [ 'oi_name' => $this->file->getName() ],
98  __METHOD__
99  );
100 
101  foreach ( $result as $row ) {
102  $this->addOld( $row->oi_archive_name );
103  $archiveNames[] = $row->oi_archive_name;
104  }
105 
106  return $archiveNames;
107  }
108 
112  protected function getOldRels() {
113  if ( !isset( $this->srcRels['.'] ) ) {
114  $oldRels =& $this->srcRels;
115  $deleteCurrent = false;
116  } else {
117  $oldRels = $this->srcRels;
118  unset( $oldRels['.'] );
119  $deleteCurrent = true;
120  }
121 
122  return [ $oldRels, $deleteCurrent ];
123  }
124 
128  protected function getHashes() {
129  $hashes = [];
130  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
131 
132  if ( $deleteCurrent ) {
133  $hashes['.'] = $this->file->getSha1();
134  }
135 
136  if ( count( $oldRels ) ) {
137  $dbw = $this->file->repo->getMasterDB();
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 ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
148  // Get the hash from the file
149  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
150  $props = $this->file->repo->getFileProps( $oldUrl );
151 
152  if ( $props['fileExists'] ) {
153  // Upgrade the oldimage row
154  $dbw->update( 'oldimage',
155  [ 'oi_sha1' => $props['sha1'] ],
156  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
157  __METHOD__ );
158  $hashes[$row->oi_archive_name] = $props['sha1'];
159  } else {
160  $hashes[$row->oi_archive_name] = false;
161  }
162  } else {
163  $hashes[$row->oi_archive_name] = $row->oi_sha1;
164  }
165  }
166  }
167 
168  $missing = array_diff_key( $this->srcRels, $hashes );
169 
170  foreach ( $missing as $name => $rel ) {
171  $this->status->error( 'filedelete-old-unregistered', $name );
172  }
173 
174  foreach ( $hashes as $name => $hash ) {
175  if ( !$hash ) {
176  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
177  unset( $hashes[$name] );
178  }
179  }
180 
181  return $hashes;
182  }
183 
184  protected function doDBInserts() {
185  $now = time();
186  $dbw = $this->file->repo->getMasterDB();
187 
188  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
189  $actorMigration = ActorMigration::newMigration();
190 
191  $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
192  $encUserId = $dbw->addQuotes( $this->user->getId() );
193  $encGroup = $dbw->addQuotes( 'deleted' );
194  $ext = $this->file->getExtension();
195  $dotExt = $ext === '' ? '' : ".$ext";
196  $encExt = $dbw->addQuotes( $dotExt );
197  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
198 
199  // Bitfields to further suppress the content
200  if ( $this->suppress ) {
201  $bitfield = RevisionRecord::SUPPRESSED_ALL;
202  } else {
203  $bitfield = 'oi_deleted';
204  }
205 
206  if ( $deleteCurrent ) {
207  $tables = [ 'image' ];
208  $fields = [
209  'fa_storage_group' => $encGroup,
210  'fa_storage_key' => $dbw->conditional(
211  [ 'img_sha1' => '' ],
212  $dbw->addQuotes( '' ),
213  $dbw->buildConcat( [ "img_sha1", $encExt ] )
214  ),
215  'fa_deleted_user' => $encUserId,
216  'fa_deleted_timestamp' => $encTimestamp,
217  'fa_deleted' => $this->suppress ? $bitfield : 0,
218  'fa_name' => 'img_name',
219  'fa_archive_name' => 'NULL',
220  'fa_size' => 'img_size',
221  'fa_width' => 'img_width',
222  'fa_height' => 'img_height',
223  'fa_metadata' => 'img_metadata',
224  'fa_bits' => 'img_bits',
225  'fa_media_type' => 'img_media_type',
226  'fa_major_mime' => 'img_major_mime',
227  'fa_minor_mime' => 'img_minor_mime',
228  'fa_description_id' => 'img_description_id',
229  'fa_timestamp' => 'img_timestamp',
230  'fa_sha1' => 'img_sha1',
231  'fa_actor' => 'img_actor',
232  ];
233  $joins = [];
234 
235  $fields += array_map(
236  [ $dbw, 'addQuotes' ],
237  $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
238  );
239 
240  $dbw->insertSelect( 'filearchive', $tables, $fields,
241  [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
242  }
243 
244  if ( count( $oldRels ) ) {
245  $fileQuery = OldLocalFile::getQueryInfo();
246  $res = $dbw->select(
247  $fileQuery['tables'],
248  $fileQuery['fields'],
249  [
250  'oi_name' => $this->file->getName(),
251  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
252  ],
253  __METHOD__,
254  [ 'FOR UPDATE' ],
255  $fileQuery['joins']
256  );
257  $rowsInsert = [];
258  if ( $res->numRows() ) {
259  $reason = $commentStore->createComment( $dbw, $this->reason );
260  foreach ( $res as $row ) {
261  $comment = $commentStore->getComment( 'oi_description', $row );
262  $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
263  $rowsInsert[] = [
264  // Deletion-specific fields
265  'fa_storage_group' => 'deleted',
266  'fa_storage_key' => ( $row->oi_sha1 === '' )
267  ? ''
268  : "{$row->oi_sha1}{$dotExt}",
269  'fa_deleted_user' => $this->user->getId(),
270  'fa_deleted_timestamp' => $dbw->timestamp( $now ),
271  // Counterpart fields
272  'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
273  'fa_name' => $row->oi_name,
274  'fa_archive_name' => $row->oi_archive_name,
275  'fa_size' => $row->oi_size,
276  'fa_width' => $row->oi_width,
277  'fa_height' => $row->oi_height,
278  'fa_metadata' => $row->oi_metadata,
279  'fa_bits' => $row->oi_bits,
280  'fa_media_type' => $row->oi_media_type,
281  'fa_major_mime' => $row->oi_major_mime,
282  'fa_minor_mime' => $row->oi_minor_mime,
283  'fa_timestamp' => $row->oi_timestamp,
284  'fa_sha1' => $row->oi_sha1
285  ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
286  + $commentStore->insert( $dbw, 'fa_description', $comment )
287  + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
288  }
289  }
290 
291  $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
292  }
293  }
294 
295  private function doDBDeletes() {
296  $dbw = $this->file->repo->getMasterDB();
297  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
298 
299  if ( count( $oldRels ) ) {
300  $dbw->delete( 'oldimage',
301  [
302  'oi_name' => $this->file->getName(),
303  'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
304  ], __METHOD__ );
305  }
306 
307  if ( $deleteCurrent ) {
308  $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
309  }
310  }
311 
316  public function execute() {
317  $repo = $this->file->getRepo();
318  $this->file->lock();
319 
320  // Prepare deletion batch
321  $hashes = $this->getHashes();
322  $this->deletionBatch = [];
323  $ext = $this->file->getExtension();
324  $dotExt = $ext === '' ? '' : ".$ext";
325 
326  foreach ( $this->srcRels as $name => $srcRel ) {
327  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
328  if ( isset( $hashes[$name] ) ) {
329  $hash = $hashes[$name];
330  $key = $hash . $dotExt;
331  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
332  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
333  }
334  }
335 
336  if ( !$repo->hasSha1Storage() ) {
337  // Removes non-existent file from the batch, so we don't get errors.
338  // This also handles files in the 'deleted' zone deleted via revision deletion.
339  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
340  if ( !$checkStatus->isGood() ) {
341  $this->status->merge( $checkStatus );
342  return $this->status;
343  }
344  $this->deletionBatch = $checkStatus->value;
345 
346  // Execute the file deletion batch
347  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
348  if ( !$status->isGood() ) {
349  $this->status->merge( $status );
350  }
351  }
352 
353  if ( !$this->status->isOK() ) {
354  // Critical file deletion error; abort
355  $this->file->unlock();
356 
357  return $this->status;
358  }
359 
360  // Copy the image/oldimage rows to filearchive
361  $this->doDBInserts();
362  // Delete image/oldimage rows
363  $this->doDBDeletes();
364 
365  // Commit and return
366  $this->file->unlock();
367 
368  return $this->status;
369  }
370 
376  protected function removeNonexistentFiles( $batch ) {
377  $files = $newBatch = [];
378 
379  foreach ( $batch as $batchItem ) {
380  list( $src, ) = $batchItem;
381  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
382  }
383 
384  $result = $this->file->repo->fileExistsBatch( $files );
385  if ( in_array( null, $result, true ) ) {
386  return Status::newFatal( 'backend-fail-internal',
387  $this->file->repo->getBackend()->getName() );
388  }
389 
390  foreach ( $batch as $batchItem ) {
391  if ( $result[$batchItem[0]] ) {
392  $newBatch[] = $batchItem;
393  }
394  }
395 
396  return Status::newGood( $newBatch );
397  }
398 }
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:82
LocalFileDeleteBatch\doDBDeletes
doDBDeletes()
Definition: LocalFileDeleteBatch.php:295
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:157
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:316
$res
$res
Definition: testCompression.php:57
LocalFileDeleteBatch\$status
Status $status
Definition: LocalFileDeleteBatch.php:51
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:140
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:75
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:63
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
LocalFileDeleteBatch\__construct
__construct(File $file, User $user, $reason='', $suppress=false)
Definition: LocalFileDeleteBatch.php:62
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:613
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:59
LocalFileDeleteBatch\getHashes
getHashes()
Definition: LocalFileDeleteBatch.php:128
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:91
LocalFileDeleteBatch\$srcRels
array $srcRels
Definition: LocalFileDeleteBatch.php:39
LocalFileDeleteBatch\getOldRels
getOldRels()
Definition: LocalFileDeleteBatch.php:112
$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:55
LocalFileDeleteBatch\removeNonexistentFiles
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
Definition: LocalFileDeleteBatch.php:376
LocalFileDeleteBatch\doDBInserts
doDBInserts()
Definition: LocalFileDeleteBatch.php:184