MediaWiki REL1_35
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
46
48 private $suppress;
49
51 private $status;
52
54 private $user;
55
67 public function __construct(
68 File $file,
69 $param2 = '',
70 $param3 = '',
71 $param4 = false
72 ) {
73 $this->file = $file;
74
75 if ( $param2 instanceof User ) {
76 // New signature
77 $user = $param2;
78 $reason = $param3;
79 $suppress = $param4;
80 } else {
81 // Old signature
83 'Construction of ' . __CLASS__ . ' without passing a user as ' .
84 'the second parameter was deprecated in MediaWiki 1.35. ' .
85 'See T245710 for more',
86 '1.35'
87 );
88
89 $reason = $param2;
90
91 // Suppress defaults to false if not provided
92 $suppress = ( $param3 === '' ? false : $param3 );
93
94 if ( $param4 === false ) {
95 global $wgUser;
96 $user = $wgUser;
97 } else {
98 $user = $param4;
99 }
100 }
101
102 $this->reason = $reason;
103 $this->suppress = $suppress;
104 $this->user = $user;
105 $this->status = $file->repo->newGood();
106 }
107
108 public function addCurrent() {
109 $this->srcRels['.'] = $this->file->getRel();
110 }
111
115 public function addOld( $oldName ) {
116 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
117 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
118 }
119
124 public function addOlds() {
125 $archiveNames = [];
126
127 $dbw = $this->file->repo->getMasterDB();
128 $result = $dbw->select( 'oldimage',
129 [ 'oi_archive_name' ],
130 [ 'oi_name' => $this->file->getName() ],
131 __METHOD__
132 );
133
134 foreach ( $result as $row ) {
135 $this->addOld( $row->oi_archive_name );
136 $archiveNames[] = $row->oi_archive_name;
137 }
138
139 return $archiveNames;
140 }
141
145 protected function getOldRels() {
146 if ( !isset( $this->srcRels['.'] ) ) {
147 $oldRels =& $this->srcRels;
148 $deleteCurrent = false;
149 } else {
150 $oldRels = $this->srcRels;
151 unset( $oldRels['.'] );
152 $deleteCurrent = true;
153 }
154
155 return [ $oldRels, $deleteCurrent ];
156 }
157
161 protected function getHashes() {
162 $hashes = [];
163 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
164
165 if ( $deleteCurrent ) {
166 $hashes['.'] = $this->file->getSha1();
167 }
168
169 if ( count( $oldRels ) ) {
170 $dbw = $this->file->repo->getMasterDB();
171 $res = $dbw->select(
172 'oldimage',
173 [ 'oi_archive_name', 'oi_sha1' ],
174 [ 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) ),
175 'oi_name' => $this->file->getName() ], // performance
176 __METHOD__
177 );
178
179 foreach ( $res as $row ) {
180 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
181 // Get the hash from the file
182 $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
183 $props = $this->file->repo->getFileProps( $oldUrl );
184
185 if ( $props['fileExists'] ) {
186 // Upgrade the oldimage row
187 $dbw->update( 'oldimage',
188 [ 'oi_sha1' => $props['sha1'] ],
189 [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
190 __METHOD__ );
191 $hashes[$row->oi_archive_name] = $props['sha1'];
192 } else {
193 $hashes[$row->oi_archive_name] = false;
194 }
195 } else {
196 $hashes[$row->oi_archive_name] = $row->oi_sha1;
197 }
198 }
199 }
200
201 $missing = array_diff_key( $this->srcRels, $hashes );
202
203 foreach ( $missing as $name => $rel ) {
204 $this->status->error( 'filedelete-old-unregistered', $name );
205 }
206
207 foreach ( $hashes as $name => $hash ) {
208 if ( !$hash ) {
209 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
210 unset( $hashes[$name] );
211 }
212 }
213
214 return $hashes;
215 }
216
217 protected function doDBInserts() {
218 $now = time();
219 $dbw = $this->file->repo->getMasterDB();
220
221 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
222 $actorMigration = ActorMigration::newMigration();
223
224 $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
225 $encUserId = $dbw->addQuotes( $this->user->getId() );
226 $encGroup = $dbw->addQuotes( 'deleted' );
227 $ext = $this->file->getExtension();
228 $dotExt = $ext === '' ? '' : ".$ext";
229 $encExt = $dbw->addQuotes( $dotExt );
230 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
231
232 // Bitfields to further suppress the content
233 if ( $this->suppress ) {
234 $bitfield = RevisionRecord::SUPPRESSED_ALL;
235 } else {
236 $bitfield = 'oi_deleted';
237 }
238
239 if ( $deleteCurrent ) {
240 $tables = [ 'image' ];
241 $fields = [
242 'fa_storage_group' => $encGroup,
243 'fa_storage_key' => $dbw->conditional(
244 [ 'img_sha1' => '' ],
245 $dbw->addQuotes( '' ),
246 $dbw->buildConcat( [ "img_sha1", $encExt ] )
247 ),
248 'fa_deleted_user' => $encUserId,
249 'fa_deleted_timestamp' => $encTimestamp,
250 'fa_deleted' => $this->suppress ? $bitfield : 0,
251 'fa_name' => 'img_name',
252 'fa_archive_name' => 'NULL',
253 'fa_size' => 'img_size',
254 'fa_width' => 'img_width',
255 'fa_height' => 'img_height',
256 'fa_metadata' => 'img_metadata',
257 'fa_bits' => 'img_bits',
258 'fa_media_type' => 'img_media_type',
259 'fa_major_mime' => 'img_major_mime',
260 'fa_minor_mime' => 'img_minor_mime',
261 'fa_description_id' => 'img_description_id',
262 'fa_timestamp' => 'img_timestamp',
263 'fa_sha1' => 'img_sha1',
264 'fa_actor' => 'img_actor',
265 ];
266 $joins = [];
267
268 $fields += array_map(
269 [ $dbw, 'addQuotes' ],
270 $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
271 );
272
273 $dbw->insertSelect( 'filearchive', $tables, $fields,
274 [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
275 }
276
277 if ( count( $oldRels ) ) {
278 $fileQuery = OldLocalFile::getQueryInfo();
279 $res = $dbw->select(
280 $fileQuery['tables'],
281 $fileQuery['fields'],
282 [
283 'oi_name' => $this->file->getName(),
284 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
285 ],
286 __METHOD__,
287 [ 'FOR UPDATE' ],
288 $fileQuery['joins']
289 );
290 $rowsInsert = [];
291 if ( $res->numRows() ) {
292 $reason = $commentStore->createComment( $dbw, $this->reason );
293 foreach ( $res as $row ) {
294 $comment = $commentStore->getComment( 'oi_description', $row );
295 $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
296 $rowsInsert[] = [
297 // Deletion-specific fields
298 'fa_storage_group' => 'deleted',
299 'fa_storage_key' => ( $row->oi_sha1 === '' )
300 ? ''
301 : "{$row->oi_sha1}{$dotExt}",
302 'fa_deleted_user' => $this->user->getId(),
303 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
304 // Counterpart fields
305 'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
306 'fa_name' => $row->oi_name,
307 'fa_archive_name' => $row->oi_archive_name,
308 'fa_size' => $row->oi_size,
309 'fa_width' => $row->oi_width,
310 'fa_height' => $row->oi_height,
311 'fa_metadata' => $row->oi_metadata,
312 'fa_bits' => $row->oi_bits,
313 'fa_media_type' => $row->oi_media_type,
314 'fa_major_mime' => $row->oi_major_mime,
315 'fa_minor_mime' => $row->oi_minor_mime,
316 'fa_timestamp' => $row->oi_timestamp,
317 'fa_sha1' => $row->oi_sha1
318 ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
319 + $commentStore->insert( $dbw, 'fa_description', $comment )
320 + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
321 }
322 }
323
324 $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
325 }
326 }
327
328 private function doDBDeletes() {
329 $dbw = $this->file->repo->getMasterDB();
330 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
331
332 if ( count( $oldRels ) ) {
333 $dbw->delete( 'oldimage',
334 [
335 'oi_name' => $this->file->getName(),
336 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) )
337 ], __METHOD__ );
338 }
339
340 if ( $deleteCurrent ) {
341 $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
342 }
343 }
344
349 public function execute() {
350 $repo = $this->file->getRepo();
351 $this->file->lock();
352
353 // Prepare deletion batch
354 $hashes = $this->getHashes();
355 $this->deletionBatch = [];
356 $ext = $this->file->getExtension();
357 $dotExt = $ext === '' ? '' : ".$ext";
358
359 foreach ( $this->srcRels as $name => $srcRel ) {
360 // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
361 if ( isset( $hashes[$name] ) ) {
362 $hash = $hashes[$name];
363 $key = $hash . $dotExt;
364 $dstRel = $repo->getDeletedHashPath( $key ) . $key;
365 $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
366 }
367 }
368
369 if ( !$repo->hasSha1Storage() ) {
370 // Removes non-existent file from the batch, so we don't get errors.
371 // This also handles files in the 'deleted' zone deleted via revision deletion.
372 $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
373 if ( !$checkStatus->isGood() ) {
374 $this->status->merge( $checkStatus );
375 return $this->status;
376 }
377 $this->deletionBatch = $checkStatus->value;
378
379 // Execute the file deletion batch
380 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
381 if ( !$status->isGood() ) {
382 $this->status->merge( $status );
383 }
384 }
385
386 if ( !$this->status->isOK() ) {
387 // Critical file deletion error; abort
388 $this->file->unlock();
389
390 return $this->status;
391 }
392
393 // Copy the image/oldimage rows to filearchive
394 $this->doDBInserts();
395 // Delete image/oldimage rows
396 $this->doDBDeletes();
397
398 // Commit and return
399 $this->file->unlock();
400
401 return $this->status;
402 }
403
409 protected function removeNonexistentFiles( $batch ) {
410 $files = $newBatch = [];
411
412 foreach ( $batch as $batchItem ) {
413 list( $src, ) = $batchItem;
414 $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
415 }
416
417 $result = $this->file->repo->fileExistsBatch( $files );
418 if ( in_array( null, $result, true ) ) {
419 return Status::newFatal( 'backend-fail-internal',
420 $this->file->repo->getBackend()->getName() );
421 }
422
423 foreach ( $batch as $batchItem ) {
424 if ( $result[$batchItem[0]] ) {
425 $newBatch[] = $batchItem;
426 }
427 }
428
429 return Status::newGood( $newBatch );
430 }
431}
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:63
Helper class for file deletion.
addOlds()
Add the old versions of the image to the batch.
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
__construct(File $file, $param2='', $param3='', $param4=false)
array $deletionBatch
Items to be processed in the deletion batch.
bool $suppress
Whether to suppress all suppressable fields when deleting.
execute()
Run the transaction.
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:59
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new oldlocalfile object.
merge( $other, $overwriteValue=false)
Merge another status object into this one.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:616
if(!is_readable( $file)) $ext
Definition router.php:48