3 * Local file in the wiki's own database.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup FileAbstraction
24 use MediaWiki\MediaWikiServices
;
27 * Helper class for file deletion
28 * @ingroup FileAbstraction
30 class LocalFileDeleteBatch
{
38 private $srcRels = [];
41 private $archiveUrls = [];
43 /** @var array Items to be processed in the deletion batch */
44 private $deletionBatch;
46 /** @var bool Whether to suppress all suppressable fields when deleting */
57 * @param string $reason
58 * @param bool $suppress
59 * @param User|null $user
61 function __construct( File
$file, $reason = '', $suppress = false, $user = null ) {
63 $this->reason
= $reason;
64 $this->suppress
= $suppress;
66 $this->user
= $user ?
: $wgUser;
67 $this->status
= $file->repo
->newGood();
70 public function addCurrent() {
71 $this->srcRels
['.'] = $this->file
->getRel();
75 * @param string $oldName
77 public function addOld( $oldName ) {
78 $this->srcRels
[$oldName] = $this->file
->getArchiveRel( $oldName );
79 $this->archiveUrls
[] = $this->file
->getArchiveUrl( $oldName );
83 * Add the old versions of the image to the batch
84 * @return string[] List of archive names from old versions
86 public function addOlds() {
89 $dbw = $this->file
->repo
->getMasterDB();
90 $result = $dbw->select( 'oldimage',
91 [ 'oi_archive_name' ],
92 [ 'oi_name' => $this->file
->getName() ],
96 foreach ( $result as $row ) {
97 $this->addOld( $row->oi_archive_name
);
98 $archiveNames[] = $row->oi_archive_name
;
101 return $archiveNames;
107 protected function getOldRels() {
108 if ( !isset( $this->srcRels
['.'] ) ) {
109 $oldRels =& $this->srcRels
;
110 $deleteCurrent = false;
112 $oldRels = $this->srcRels
;
113 unset( $oldRels['.'] );
114 $deleteCurrent = true;
117 return [ $oldRels, $deleteCurrent ];
123 protected function getHashes() {
125 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
127 if ( $deleteCurrent ) {
128 $hashes['.'] = $this->file
->getSha1();
131 if ( count( $oldRels ) ) {
132 $dbw = $this->file
->repo
->getMasterDB();
135 [ 'oi_archive_name', 'oi_sha1' ],
136 [ 'oi_archive_name' => array_keys( $oldRels ),
137 'oi_name' => $this->file
->getName() ], // performance
141 foreach ( $res as $row ) {
142 if ( rtrim( $row->oi_sha1
, "\0" ) === '' ) {
143 // Get the hash from the file
144 $oldUrl = $this->file
->getArchiveVirtualUrl( $row->oi_archive_name
);
145 $props = $this->file
->repo
->getFileProps( $oldUrl );
147 if ( $props['fileExists'] ) {
148 // Upgrade the oldimage row
149 $dbw->update( 'oldimage',
150 [ 'oi_sha1' => $props['sha1'] ],
151 [ 'oi_name' => $this->file
->getName(), 'oi_archive_name' => $row->oi_archive_name
],
153 $hashes[$row->oi_archive_name
] = $props['sha1'];
155 $hashes[$row->oi_archive_name
] = false;
158 $hashes[$row->oi_archive_name
] = $row->oi_sha1
;
163 $missing = array_diff_key( $this->srcRels
, $hashes );
165 foreach ( $missing as $name => $rel ) {
166 $this->status
->error( 'filedelete-old-unregistered', $name );
169 foreach ( $hashes as $name => $hash ) {
171 $this->status
->error( 'filedelete-missing', $this->srcRels
[$name] );
172 unset( $hashes[$name] );
179 protected function doDBInserts() {
180 global $wgActorTableSchemaMigrationStage;
183 $dbw = $this->file
->repo
->getMasterDB();
185 $commentStore = MediaWikiServices
::getInstance()->getCommentStore();
186 $actorMigration = ActorMigration
::newMigration();
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();
196 // Bitfields to further suppress the content
197 if ( $this->suppress
) {
198 $bitfield = Revision
::SUPPRESSED_ALL
;
200 $bitfield = 'oi_deleted';
203 if ( $deleteCurrent ) {
204 $tables = [ 'image' ];
206 'fa_storage_group' => $encGroup,
207 'fa_storage_key' => $dbw->conditional(
208 [ 'img_sha1' => '' ],
209 $dbw->addQuotes( '' ),
210 $dbw->buildConcat( [ "img_sha1", $encExt ] )
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'
231 $fields +
= array_map(
232 [ $dbw, 'addQuotes' ],
233 $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason
)
236 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD
) {
237 $fields['fa_user'] = 'img_user';
238 $fields['fa_user_text'] = 'img_user_text';
240 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW
) {
241 $fields['fa_actor'] = 'img_actor';
245 ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_BOTH
) === SCHEMA_COMPAT_WRITE_BOTH
247 // Upgrade any rows that are still old-style. Otherwise an upgrade
248 // might be missed if a deletion happens while the migration script
252 [ 'img_name', 'img_user', 'img_user_text' ],
253 [ 'img_name' => $this->file
->getName(), 'img_actor' => 0 ],
256 foreach ( $res as $row ) {
257 $actorId = User
::newFromAnyId( $row->img_user
, $row->img_user_text
, null )->getActorId( $dbw );
260 [ 'img_actor' => $actorId ],
261 [ 'img_name' => $row->img_name
, 'img_actor' => 0 ],
267 $dbw->insertSelect( 'filearchive', $tables, $fields,
268 [ 'img_name' => $this->file
->getName() ], __METHOD__
, [], [], $joins );
271 if ( count( $oldRels ) ) {
272 $fileQuery = OldLocalFile
::getQueryInfo();
274 $fileQuery['tables'],
275 $fileQuery['fields'],
277 'oi_name' => $this->file
->getName(),
278 'oi_archive_name' => array_keys( $oldRels )
285 if ( $res->numRows() ) {
286 $reason = $commentStore->createComment( $dbw, $this->reason
);
287 foreach ( $res as $row ) {
288 $comment = $commentStore->getComment( 'oi_description', $row );
289 $user = User
::newFromAnyId( $row->oi_user
, $row->oi_user_text
, $row->oi_actor
);
291 // Deletion-specific fields
292 'fa_storage_group' => 'deleted',
293 'fa_storage_key' => ( $row->oi_sha1
=== '' )
295 : "{$row->oi_sha1}{$dotExt}",
296 'fa_deleted_user' => $this->user
->getId(),
297 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
298 // Counterpart fields
299 'fa_deleted' => $this->suppress ?
$bitfield : $row->oi_deleted
,
300 'fa_name' => $row->oi_name
,
301 'fa_archive_name' => $row->oi_archive_name
,
302 'fa_size' => $row->oi_size
,
303 'fa_width' => $row->oi_width
,
304 'fa_height' => $row->oi_height
,
305 'fa_metadata' => $row->oi_metadata
,
306 'fa_bits' => $row->oi_bits
,
307 'fa_media_type' => $row->oi_media_type
,
308 'fa_major_mime' => $row->oi_major_mime
,
309 'fa_minor_mime' => $row->oi_minor_mime
,
310 'fa_timestamp' => $row->oi_timestamp
,
311 'fa_sha1' => $row->oi_sha1
312 ] +
$commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
313 +
$commentStore->insert( $dbw, 'fa_description', $comment )
314 +
$actorMigration->getInsertValues( $dbw, 'fa_user', $user );
318 $dbw->insert( 'filearchive', $rowsInsert, __METHOD__
);
322 function doDBDeletes() {
323 $dbw = $this->file
->repo
->getMasterDB();
324 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
326 if ( count( $oldRels ) ) {
327 $dbw->delete( 'oldimage',
329 'oi_name' => $this->file
->getName(),
330 'oi_archive_name' => array_keys( $oldRels )
334 if ( $deleteCurrent ) {
335 $dbw->delete( 'image', [ 'img_name' => $this->file
->getName() ], __METHOD__
);
340 * Run the transaction
343 public function execute() {
344 $repo = $this->file
->getRepo();
347 // Prepare deletion batch
348 $hashes = $this->getHashes();
349 $this->deletionBatch
= [];
350 $ext = $this->file
->getExtension();
351 $dotExt = $ext === '' ?
'' : ".$ext";
353 foreach ( $this->srcRels
as $name => $srcRel ) {
354 // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
355 if ( isset( $hashes[$name] ) ) {
356 $hash = $hashes[$name];
357 $key = $hash . $dotExt;
358 $dstRel = $repo->getDeletedHashPath( $key ) . $key;
359 $this->deletionBatch
[$name] = [ $srcRel, $dstRel ];
363 if ( !$repo->hasSha1Storage() ) {
364 // Removes non-existent file from the batch, so we don't get errors.
365 // This also handles files in the 'deleted' zone deleted via revision deletion.
366 $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch
);
367 if ( !$checkStatus->isGood() ) {
368 $this->status
->merge( $checkStatus );
369 return $this->status
;
371 $this->deletionBatch
= $checkStatus->value
;
373 // Execute the file deletion batch
374 $status = $this->file
->repo
->deleteBatch( $this->deletionBatch
);
375 if ( !$status->isGood() ) {
376 $this->status
->merge( $status );
380 if ( !$this->status
->isOK() ) {
381 // Critical file deletion error; abort
382 $this->file
->unlock();
384 return $this->status
;
387 // Copy the image/oldimage rows to filearchive
388 $this->doDBInserts();
389 // Delete image/oldimage rows
390 $this->doDBDeletes();
393 $this->file
->unlock();
395 return $this->status
;
399 * Removes non-existent files from a deletion batch.
400 * @param array $batch
403 protected function removeNonexistentFiles( $batch ) {
404 $files = $newBatch = [];
406 foreach ( $batch as $batchItem ) {
407 list( $src, ) = $batchItem;
408 $files[$src] = $this->file
->repo
->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
411 $result = $this->file
->repo
->fileExistsBatch( $files );
412 if ( in_array( null, $result, true ) ) {
413 return Status
::newFatal( 'backend-fail-internal',
414 $this->file
->repo
->getBackend()->getName() );
417 foreach ( $batch as $batchItem ) {
418 if ( $result[$batchItem[0]] ) {
419 $newBatch[] = $batchItem;
423 return Status
::newGood( $newBatch );