MediaWiki REL1_37
LocalFileDeleteBatch.php
Go to the documentation of this file.
1<?php
27
34 private $file;
35
37 private $reason;
38
40 private $srcRels = [];
41
43 private $archiveUrls = [];
44
47
49 private $suppress;
50
52 private $status;
53
55 private $user;
56
63 public function __construct(
64 File $file,
66 $reason = '',
67 $suppress = false
68 ) {
69 $this->file = $file;
70 $this->user = $user;
71 $this->reason = $reason;
72 $this->suppress = $suppress;
73 $this->status = $file->repo->newGood();
74 }
75
76 public function addCurrent() {
77 $this->srcRels['.'] = $this->file->getRel();
78 }
79
83 public function addOld( $oldName ) {
84 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
85 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
86 }
87
92 public function addOlds() {
93 $archiveNames = [];
94
95 $dbw = $this->file->repo->getPrimaryDB();
96 $result = $dbw->select( 'oldimage',
97 [ 'oi_archive_name' ],
98 [ 'oi_name' => $this->file->getName() ],
99 __METHOD__
100 );
101
102 foreach ( $result as $row ) {
103 $this->addOld( $row->oi_archive_name );
104 $archiveNames[] = $row->oi_archive_name;
105 }
106
107 return $archiveNames;
108 }
109
113 protected function getOldRels() {
114 if ( !isset( $this->srcRels['.'] ) ) {
115 $oldRels =& $this->srcRels;
116 $deleteCurrent = false;
117 } else {
118 $oldRels = $this->srcRels;
119 unset( $oldRels['.'] );
120 $deleteCurrent = true;
121 }
122
123 return [ $oldRels, $deleteCurrent ];
124 }
125
129 protected function getHashes() {
130 $hashes = [];
131 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
132
133 if ( $deleteCurrent ) {
134 $hashes['.'] = $this->file->getSha1();
135 }
136
137 if ( count( $oldRels ) ) {
138 $dbw = $this->file->repo->getPrimaryDB();
139 $res = $dbw->select(
140 'oldimage',
141 [ 'oi_archive_name', 'oi_sha1' ],
142 [ 'oi_archive_name' => array_map( 'strval', array_keys( $oldRels ) ),
143 'oi_name' => $this->file->getName() ], // performance
144 __METHOD__
145 );
146
147 foreach ( $res as $row ) {
148 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
149 // Get the hash from the file
150 $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
151 $props = $this->file->repo->getFileProps( $oldUrl );
152
153 if ( $props['fileExists'] ) {
154 // Upgrade the oldimage row
155 $dbw->update( 'oldimage',
156 [ 'oi_sha1' => $props['sha1'] ],
157 [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
158 __METHOD__ );
159 $hashes[$row->oi_archive_name] = $props['sha1'];
160 } else {
161 $hashes[$row->oi_archive_name] = false;
162 }
163 } else {
164 $hashes[$row->oi_archive_name] = $row->oi_sha1;
165 }
166 }
167 }
168
169 $missing = array_diff_key( $this->srcRels, $hashes );
170
171 foreach ( $missing as $name => $rel ) {
172 $this->status->error( 'filedelete-old-unregistered', $name );
173 }
174
175 foreach ( $hashes as $name => $hash ) {
176 if ( !$hash ) {
177 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
178 unset( $hashes[$name] );
179 }
180 }
181
182 return $hashes;
183 }
184
185 protected function doDBInserts() {
186 $now = time();
187 $dbw = $this->file->repo->getPrimaryDB();
188
189 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
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_actor' => $row->oi_actor,
284 'fa_timestamp' => $row->oi_timestamp,
285 'fa_sha1' => $row->oi_sha1
286 ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
287 + $commentStore->insert( $dbw, 'fa_description', $comment );
288 }
289 }
290
291 $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
292 }
293 }
294
295 private function doDBDeletes() {
296 $dbw = $this->file->repo->getPrimaryDB();
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 [ $src, /* dest */ ] ) {
380 $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
381 }
382
383 $result = $this->file->repo->fileExistsBatch( $files );
384 if ( in_array( null, $result, true ) ) {
385 return Status::newFatal( 'backend-fail-internal',
386 $this->file->repo->getBackend()->getName() );
387 }
388
389 foreach ( $batch as $batchItem ) {
390 if ( $result[$batchItem[0]] ) {
391 $newBatch[] = $batchItem;
392 }
393 }
394
395 return Status::newGood( $newBatch );
396 }
397}
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:66
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.
bool $suppress
Whether to suppress all suppressable fields when deleting.
__construct(File $file, UserIdentity $user, $reason='', $suppress=false)
array[] $deletionBatch
Items to be processed in the deletion batch.
execute()
Run the transaction.
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:63
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
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:713
Interface for objects representing user identity.
if(!is_readable( $file)) $ext
Definition router.php:48