MediaWiki REL1_39
LocalFileDeleteBatch.php
Go to the documentation of this file.
1<?php
24use 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 list( $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 list( $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 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_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.
Local file in the wiki's own database.
Definition LocalFile.php:60
Service locator for MediaWiki core services.
Page revision base class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
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.
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