Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1432311
LocalFileDeleteBatch.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
LocalFileDeleteBatch.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use
MediaWiki\FileRepo\File\FileSelectQueryBuilder
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Status\Status
;
use
MediaWiki\User\UserIdentity
;
use
Wikimedia\ScopedCallback
;
/**
* Helper class for file deletion
*
* @internal
* @ingroup FileAbstraction
*/
class
LocalFileDeleteBatch
{
/** @var LocalFile */
private
$file
;
/** @var string */
private
$reason
;
/** @var array */
private
$srcRels
=
[];
/** @var array */
private
$archiveUrls
=
[];
/** @var array[] Items to be processed in the deletion batch */
private
$deletionBatch
;
/** @var bool Whether to suppress all suppressable fields when deleting */
private
$suppress
;
/** @var UserIdentity */
private
$user
;
/**
* @param File $file
* @param UserIdentity $user
* @param string $reason
* @param bool $suppress
*/
public
function
__construct
(
File
$file
,
UserIdentity
$user
,
$reason
=
''
,
$suppress
=
false
)
{
$this
->
file
=
$file
;
$this
->
user
=
$user
;
$this
->
reason
=
$reason
;
$this
->
suppress
=
$suppress
;
}
public
function
addCurrent
()
{
$this
->
srcRels
[
'.'
]
=
$this
->
file
->
getRel
();
}
/**
* @param string $oldName
*/
public
function
addOld
(
$oldName
)
{
$this
->
srcRels
[
$oldName
]
=
$this
->
file
->
getArchiveRel
(
$oldName
);
$this
->
archiveUrls
[]
=
$this
->
file
->
getArchiveUrl
(
$oldName
);
}
/**
* Add the old versions of the image to the batch
* @return string[] List of archive names from old versions
*/
public
function
addOlds
()
{
$archiveNames
=
[];
$dbw
=
$this
->
file
->
repo
->
getPrimaryDB
();
$result
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
[
'oi_archive_name'
]
)
->
from
(
'oldimage'
)
->
where
(
[
'oi_name'
=>
$this
->
file
->
getName
()
]
)
->
caller
(
__METHOD__
)->
fetchResultSet
();
foreach
(
$result
as
$row
)
{
$this
->
addOld
(
$row
->
oi_archive_name
);
$archiveNames
[]
=
$row
->
oi_archive_name
;
}
return
$archiveNames
;
}
/**
* @return array
*/
protected
function
getOldRels
()
{
if
(
!
isset
(
$this
->
srcRels
[
'.'
]
)
)
{
$oldRels
=&
$this
->
srcRels
;
$deleteCurrent
=
false
;
}
else
{
$oldRels
=
$this
->
srcRels
;
unset
(
$oldRels
[
'.'
]
);
$deleteCurrent
=
true
;
}
return
[
$oldRels
,
$deleteCurrent
];
}
/**
* @param StatusValue $status To add error messages to
* @return array
*/
protected
function
getHashes
(
StatusValue
$status
):
array
{
$hashes
=
[];
[
$oldRels
,
$deleteCurrent
]
=
$this
->
getOldRels
();
if
(
$deleteCurrent
)
{
$hashes
[
'.'
]
=
$this
->
file
->
getSha1
();
}
if
(
count
(
$oldRels
)
)
{
$dbw
=
$this
->
file
->
repo
->
getPrimaryDB
();
$res
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
[
'oi_archive_name'
,
'oi_sha1'
]
)
->
from
(
'oldimage'
)
->
where
(
[
'oi_archive_name'
=>
array_map
(
'strval'
,
array_keys
(
$oldRels
)
),
'oi_name'
=>
$this
->
file
->
getName
()
// performance
]
)
->
caller
(
__METHOD__
)->
fetchResultSet
();
foreach
(
$res
as
$row
)
{
if
(
$row
->
oi_archive_name
===
''
)
{
// File lost, the check simulates OldLocalFile::exists
$hashes
[
$row
->
oi_archive_name
]
=
false
;
continue
;
}
if
(
rtrim
(
$row
->
oi_sha1
,
"
\0
"
)
===
''
)
{
// Get the hash from the file
$oldUrl
=
$this
->
file
->
getArchiveVirtualUrl
(
$row
->
oi_archive_name
);
$props
=
$this
->
file
->
repo
->
getFileProps
(
$oldUrl
);
if
(
$props
[
'fileExists'
]
)
{
// Upgrade the oldimage row
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'oldimage'
)
->
set
(
[
'oi_sha1'
=>
$props
[
'sha1'
]
]
)
->
where
(
[
'oi_name'
=>
$this
->
file
->
getName
(),
'oi_archive_name'
=>
$row
->
oi_archive_name
,
]
)
->
caller
(
__METHOD__
)->
execute
();
$hashes
[
$row
->
oi_archive_name
]
=
$props
[
'sha1'
];
}
else
{
$hashes
[
$row
->
oi_archive_name
]
=
false
;
}
}
else
{
$hashes
[
$row
->
oi_archive_name
]
=
$row
->
oi_sha1
;
}
}
}
$missing
=
array_diff_key
(
$this
->
srcRels
,
$hashes
);
foreach
(
$missing
as
$name
=>
$rel
)
{
$status
->
error
(
'filedelete-old-unregistered'
,
$name
);
}
foreach
(
$hashes
as
$name
=>
$hash
)
{
if
(
!
$hash
)
{
$status
->
error
(
'filedelete-missing'
,
$this
->
srcRels
[
$name
]
);
unset
(
$hashes
[
$name
]
);
}
}
return
$hashes
;
}
protected
function
doDBInserts
()
{
$now
=
time
();
$dbw
=
$this
->
file
->
repo
->
getPrimaryDB
();
$commentStore
=
MediaWikiServices
::
getInstance
()->
getCommentStore
();
$encTimestamp
=
$dbw
->
addQuotes
(
$dbw
->
timestamp
(
$now
)
);
$encUserId
=
$dbw
->
addQuotes
(
$this
->
user
->
getId
()
);
$encGroup
=
$dbw
->
addQuotes
(
'deleted'
);
$ext
=
$this
->
file
->
getExtension
();
$dotExt
=
$ext
===
''
?
''
:
".$ext"
;
$encExt
=
$dbw
->
addQuotes
(
$dotExt
);
[
$oldRels
,
$deleteCurrent
]
=
$this
->
getOldRels
();
// Bitfields to further suppress the content
if
(
$this
->
suppress
)
{
$bitfield
=
RevisionRecord
::
SUPPRESSED_ALL
;
}
else
{
$bitfield
=
'oi_deleted'
;
}
if
(
$deleteCurrent
)
{
$tables
=
[
'image'
];
$fields
=
[
'fa_storage_group'
=>
$encGroup
,
'fa_storage_key'
=>
$dbw
->
conditional
(
[
'img_sha1'
=>
''
],
$dbw
->
addQuotes
(
''
),
$dbw
->
buildConcat
(
[
"img_sha1"
,
$encExt
]
)
),
'fa_deleted_user'
=>
$encUserId
,
'fa_deleted_timestamp'
=>
$encTimestamp
,
'fa_deleted'
=>
$this
->
suppress
?
$bitfield
:
0
,
'fa_name'
=>
'img_name'
,
'fa_archive_name'
=>
'NULL'
,
'fa_size'
=>
'img_size'
,
'fa_width'
=>
'img_width'
,
'fa_height'
=>
'img_height'
,
'fa_metadata'
=>
'img_metadata'
,
'fa_bits'
=>
'img_bits'
,
'fa_media_type'
=>
'img_media_type'
,
'fa_major_mime'
=>
'img_major_mime'
,
'fa_minor_mime'
=>
'img_minor_mime'
,
'fa_description_id'
=>
'img_description_id'
,
'fa_timestamp'
=>
'img_timestamp'
,
'fa_sha1'
=>
'img_sha1'
,
'fa_actor'
=>
'img_actor'
,
];
$joins
=
[];
$fields
+=
array_map
(
[
$dbw
,
'addQuotes'
],
$commentStore
->
insert
(
$dbw
,
'fa_deleted_reason'
,
$this
->
reason
)
);
$dbw
->
insertSelect
(
'filearchive'
,
$tables
,
$fields
,
[
'img_name'
=>
$this
->
file
->
getName
()
],
__METHOD__
,
[
'IGNORE'
],
[],
$joins
);
}
if
(
count
(
$oldRels
)
)
{
$queryBuilder
=
FileSelectQueryBuilder
::
newForOldFile
(
$dbw
);
$queryBuilder
->
forUpdate
()
->
where
(
[
'oi_name'
=>
$this
->
file
->
getName
()
]
)
->
andWhere
(
[
'oi_archive_name'
=>
array_map
(
'strval'
,
array_keys
(
$oldRels
)
)
]
);
$res
=
$queryBuilder
->
caller
(
__METHOD__
)->
fetchResultSet
();
$rowsInsert
=
[];
if
(
$res
->
numRows
()
)
{
$reason
=
$commentStore
->
createComment
(
$dbw
,
$this
->
reason
);
foreach
(
$res
as
$row
)
{
$comment
=
$commentStore
->
getComment
(
'oi_description'
,
$row
);
$rowsInsert
[]
=
[
// Deletion-specific fields
'fa_storage_group'
=>
'deleted'
,
'fa_storage_key'
=>
(
$row
->
oi_sha1
===
''
)
?
''
:
"{$row->oi_sha1}{$dotExt}"
,
'fa_deleted_user'
=>
$this
->
user
->
getId
(),
'fa_deleted_timestamp'
=>
$dbw
->
timestamp
(
$now
),
// Counterpart fields
'fa_deleted'
=>
$this
->
suppress
?
$bitfield
:
$row
->
oi_deleted
,
'fa_name'
=>
$row
->
oi_name
,
'fa_archive_name'
=>
$row
->
oi_archive_name
,
'fa_size'
=>
$row
->
oi_size
,
'fa_width'
=>
$row
->
oi_width
,
'fa_height'
=>
$row
->
oi_height
,
'fa_metadata'
=>
$row
->
oi_metadata
,
'fa_bits'
=>
$row
->
oi_bits
,
'fa_media_type'
=>
$row
->
oi_media_type
,
'fa_major_mime'
=>
$row
->
oi_major_mime
,
'fa_minor_mime'
=>
$row
->
oi_minor_mime
,
'fa_actor'
=>
$row
->
oi_actor
,
'fa_timestamp'
=>
$row
->
oi_timestamp
,
'fa_sha1'
=>
$row
->
oi_sha1
]
+
$commentStore
->
insert
(
$dbw
,
'fa_deleted_reason'
,
$reason
)
+
$commentStore
->
insert
(
$dbw
,
'fa_description'
,
$comment
);
}
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'filearchive'
)
->
ignore
()
->
rows
(
$rowsInsert
)
->
caller
(
__METHOD__
)->
execute
();
}
}
private
function
doDBDeletes
()
{
$dbw
=
$this
->
file
->
repo
->
getPrimaryDB
();
[
$oldRels
,
$deleteCurrent
]
=
$this
->
getOldRels
();
if
(
count
(
$oldRels
)
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'oldimage'
)
->
where
(
[
'oi_name'
=>
$this
->
file
->
getName
(),
'oi_archive_name'
=>
array_map
(
'strval'
,
array_keys
(
$oldRels
)
)
]
)
->
caller
(
__METHOD__
)->
execute
();
}
if
(
$deleteCurrent
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'image'
)
->
where
(
[
'img_name'
=>
$this
->
file
->
getName
()
]
)
->
caller
(
__METHOD__
)->
execute
();
}
}
/**
* Run the transaction
* @return Status
*/
public
function
execute
()
{
$repo
=
$this
->
file
->
getRepo
();
$lockStatus
=
$this
->
file
->
acquireFileLock
();
if
(
!
$lockStatus
->
isOK
()
)
{
return
$lockStatus
;
}
$unlockScope
=
new
ScopedCallback
(
function
()
{
$this
->
file
->
releaseFileLock
();
}
);
$status
=
$this
->
file
->
repo
->
newGood
();
// Prepare deletion batch
$hashes
=
$this
->
getHashes
(
$status
);
$this
->
deletionBatch
=
[];
$ext
=
$this
->
file
->
getExtension
();
$dotExt
=
$ext
===
''
?
''
:
".$ext"
;
foreach
(
$this
->
srcRels
as
$name
=>
$srcRel
)
{
// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
if
(
isset
(
$hashes
[
$name
]
)
)
{
$hash
=
$hashes
[
$name
];
$key
=
$hash
.
$dotExt
;
$dstRel
=
$repo
->
getDeletedHashPath
(
$key
)
.
$key
;
$this
->
deletionBatch
[
$name
]
=
[
$srcRel
,
$dstRel
];
}
}
if
(
!
$repo
->
hasSha1Storage
()
)
{
// Removes non-existent file from the batch, so we don't get errors.
// This also handles files in the 'deleted' zone deleted via revision deletion.
$checkStatus
=
$this
->
removeNonexistentFiles
(
$this
->
deletionBatch
);
if
(
!
$checkStatus
->
isGood
()
)
{
$status
->
merge
(
$checkStatus
);
return
$status
;
}
$this
->
deletionBatch
=
$checkStatus
->
value
;
// Execute the file deletion batch
$status
=
$this
->
file
->
repo
->
deleteBatch
(
$this
->
deletionBatch
);
if
(
!
$status
->
isGood
()
)
{
$status
->
merge
(
$status
);
}
}
if
(
!
$status
->
isOK
()
)
{
// Critical file deletion error; abort
return
$status
;
}
$dbw
=
$this
->
file
->
repo
->
getPrimaryDB
();
$dbw
->
startAtomic
(
__METHOD__
);
// Copy the image/oldimage rows to filearchive
$this
->
doDBInserts
();
// Delete image/oldimage rows
$this
->
doDBDeletes
();
// This is typically a no-op since we are wrapped by another atomic
// section in FileDeleteForm and also the implicit transaction.
$dbw
->
endAtomic
(
__METHOD__
);
// Commit and return
ScopedCallback
::
consume
(
$unlockScope
);
return
$status
;
}
/**
* Removes non-existent files from a deletion batch.
* @param array[] $batch
* @return Status A good status with existing files in $batch as value, or a fatal status in case of I/O errors.
*/
protected
function
removeNonexistentFiles
(
$batch
)
{
$files
=
[];
foreach
(
$batch
as
[
$src
,
/* dest */
]
)
{
$files
[
$src
]
=
$this
->
file
->
repo
->
getVirtualUrl
(
'public'
)
.
'/'
.
rawurlencode
(
$src
);
}
$result
=
$this
->
file
->
repo
->
fileExistsBatch
(
$files
);
if
(
in_array
(
null
,
$result
,
true
)
)
{
return
Status
::
newFatal
(
'backend-fail-internal'
,
$this
->
file
->
repo
->
getBackend
()->
getName
()
);
}
$newBatch
=
[];
foreach
(
$batch
as
$batchItem
)
{
if
(
$result
[
$batchItem
[
0
]]
)
{
$newBatch
[]
=
$batchItem
;
}
}
return
Status
::
newGood
(
$newBatch
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 21:37 (1 d, 6 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
eb/19/b105b2775c26679b898b79c07a8a
Default Alt Text
LocalFileDeleteBatch.php (11 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment