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