* (bug 709) Cannot rename/move images and other media files.
Currently in experimental mode, use $wgAllowImageMoving to enable it. Known issues: * Doesn't work with rev_deleted * May also have some security and caching issues.
This commit is contained in:
parent
da51ea6b44
commit
8d85629c58
7 changed files with 288 additions and 3 deletions
|
|
@ -96,6 +96,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
|
||||||
and local one
|
and local one
|
||||||
* Update documentation links in auto-generated LocalSettings.php
|
* Update documentation links in auto-generated LocalSettings.php
|
||||||
* (bug 13584) The new hook SkinTemplateToolboxEnd was added.
|
* (bug 13584) The new hook SkinTemplateToolboxEnd was added.
|
||||||
|
* (bug 709) Cannot rename/move images and other media files [EXPERIMENTAL]
|
||||||
|
|
||||||
=== Bug fixes in 1.13 ===
|
=== Bug fixes in 1.13 ===
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1655,6 +1655,18 @@ class Database {
|
||||||
return " IF($cond, $trueVal, $falseVal) ";
|
return " IF($cond, $trueVal, $falseVal) ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a comand for str_replace function in SQL query.
|
||||||
|
* Uses REPLACE() in MySQL
|
||||||
|
*
|
||||||
|
* @param string $orig String or column to modify
|
||||||
|
* @param string $old String or column to seek
|
||||||
|
* @param string $new String or column to replace with
|
||||||
|
*/
|
||||||
|
function strreplace( $orig, $old, $new ) {
|
||||||
|
return "REPLACE({$orig}, {$old}, {$new})";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the last failure was due to a deadlock
|
* Determines if the last failure was due to a deadlock
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1526,6 +1526,9 @@ $wgAllowExternalImages = false;
|
||||||
*/
|
*/
|
||||||
$wgAllowExternalImagesFrom = '';
|
$wgAllowExternalImagesFrom = '';
|
||||||
|
|
||||||
|
/** Allows to move images and other media files. Experemintal, not sure if it always works */
|
||||||
|
$wgAllowImageMoving = false;
|
||||||
|
|
||||||
/** Disable database-intensive features */
|
/** Disable database-intensive features */
|
||||||
$wgMiserMode = false;
|
$wgMiserMode = false;
|
||||||
/** Disable all query pages if miser mode is on, not just some */
|
/** Disable all query pages if miser mode is on, not just some */
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ class MWNamespace {
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function isMovable( $index ) {
|
public static function isMovable( $index ) {
|
||||||
return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY );
|
global $wgAllowImageMoving;
|
||||||
|
return !( $index < NS_MAIN || ($index == NS_IMAGE && !$wgAllowImageMoving) || $index == NS_CATEGORY );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2388,6 +2388,19 @@ class Title {
|
||||||
return 'badarticleerror';
|
return 'badarticleerror';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image-specific checks
|
||||||
|
if( $this->getNamespace() == NS_IMAGE ) {
|
||||||
|
$file = wfLocalFile( $this );
|
||||||
|
if( $file->exists() ) {
|
||||||
|
if( $nt->getNamespace() != NS_IMAGE ) {
|
||||||
|
return 'imagenocrossnamespace';
|
||||||
|
}
|
||||||
|
if( !File::checkExtesnionCompatibility( $file, $nt->getDbKey() ) ) {
|
||||||
|
return 'imagetypemismatch';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( $auth ) {
|
if ( $auth ) {
|
||||||
global $wgUser;
|
global $wgUser;
|
||||||
$errors = array_merge($this->getUserPermissionsErrors('move', $wgUser),
|
$errors = array_merge($this->getUserPermissionsErrors('move', $wgUser),
|
||||||
|
|
@ -2439,12 +2452,15 @@ class Title {
|
||||||
|
|
||||||
$pageid = $this->getArticleID();
|
$pageid = $this->getArticleID();
|
||||||
if( $nt->exists() ) {
|
if( $nt->exists() ) {
|
||||||
$this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
|
$err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
|
||||||
$pageCountChange = ($createRedirect ? 0 : -1);
|
$pageCountChange = ($createRedirect ? 0 : -1);
|
||||||
} else { # Target didn't exist, do normal move.
|
} else { # Target didn't exist, do normal move.
|
||||||
$this->moveToNewTitle( $nt, $reason, $createRedirect );
|
$err = $this->moveToNewTitle( $nt, $reason, $createRedirect );
|
||||||
$pageCountChange = ($createRedirect ? 1 : 0);
|
$pageCountChange = ($createRedirect ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
if( is_string( $err ) ) {
|
||||||
|
return $err;
|
||||||
|
}
|
||||||
$redirid = $this->getArticleID();
|
$redirid = $this->getArticleID();
|
||||||
|
|
||||||
// Category memberships include a sort key which may be customized.
|
// Category memberships include a sort key which may be customized.
|
||||||
|
|
@ -2541,6 +2557,17 @@ class Title {
|
||||||
$oldid = $this->getArticleID();
|
$oldid = $this->getArticleID();
|
||||||
$dbw = wfGetDB( DB_MASTER );
|
$dbw = wfGetDB( DB_MASTER );
|
||||||
|
|
||||||
|
# Move an image if it is
|
||||||
|
if( $this->getNamespace() == NS_IMAGE ) {
|
||||||
|
$file = wfLocalFile( $this );
|
||||||
|
if( $file->exists() ) {
|
||||||
|
$status = $file->move( $nt );
|
||||||
|
if( !$status->isOk() ) {
|
||||||
|
return $status->getWikiText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Delete the old redirect. We don't save it to history since
|
# Delete the old redirect. We don't save it to history since
|
||||||
# by definition if we've got here it's rather uninteresting.
|
# by definition if we've got here it's rather uninteresting.
|
||||||
# We have to remove it so that the next step doesn't trigger
|
# We have to remove it so that the next step doesn't trigger
|
||||||
|
|
@ -2636,6 +2663,17 @@ class Title {
|
||||||
$dbw = wfGetDB( DB_MASTER );
|
$dbw = wfGetDB( DB_MASTER );
|
||||||
$now = $dbw->timestamp();
|
$now = $dbw->timestamp();
|
||||||
|
|
||||||
|
# Move an image if it is
|
||||||
|
if( $this->getNamespace() == NS_IMAGE ) {
|
||||||
|
$file = wfLocalFile( $this );
|
||||||
|
if( $file->exists() ) {
|
||||||
|
$status = $file->move( $nt );
|
||||||
|
if( !$status->isOk() ) {
|
||||||
|
return $status->getWikiText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Save a null revision in the page's history notifying of the move
|
# Save a null revision in the page's history notifying of the move
|
||||||
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
|
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
|
||||||
$nullRevId = $nullRevision->insertOn( $dbw );
|
$nullRevId = $nullRevision->insertOn( $dbw );
|
||||||
|
|
@ -2701,6 +2739,15 @@ class Title {
|
||||||
$fname = 'Title::isValidMoveTarget';
|
$fname = 'Title::isValidMoveTarget';
|
||||||
$dbw = wfGetDB( DB_MASTER );
|
$dbw = wfGetDB( DB_MASTER );
|
||||||
|
|
||||||
|
# Is it an existsing file?
|
||||||
|
if( $nt->getNamespace() == NS_IMAGE ) {
|
||||||
|
$file = wfLocalFile( $nt );
|
||||||
|
if( $file->exists() ) {
|
||||||
|
wfDebug( __METHOD__ . ": file exists\n" );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Is it a redirect?
|
# Is it a redirect?
|
||||||
$id = $nt->getArticleID();
|
$id = $nt->getArticleID();
|
||||||
$obj = $dbw->selectRow( array( 'page', 'revision', 'text'),
|
$obj = $dbw->selectRow( array( 'page', 'revision', 'text'),
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,21 @@ abstract class File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if file extensions are compatible
|
||||||
|
*
|
||||||
|
* @param $old File Old file
|
||||||
|
* @param $new string New name
|
||||||
|
*/
|
||||||
|
static function checkExtesnionCompatibility( File $old, $new ) {
|
||||||
|
$oldMime = $old->getMimeType();
|
||||||
|
$n = strrpos( $new, '.' );
|
||||||
|
$newExt = self::normalizeExtension(
|
||||||
|
$n ? substr( $new, $n + 1 ) : '' );
|
||||||
|
$mimeMagic = MimeMagic::singleton();
|
||||||
|
return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upgrade the database row if there is one
|
* Upgrade the database row if there is one
|
||||||
* Called by ImagePage
|
* Called by ImagePage
|
||||||
|
|
@ -916,6 +931,22 @@ abstract class File {
|
||||||
return $title && $title->isDeleted() > 0;
|
return $title && $title->isDeleted() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move file to the new title
|
||||||
|
*
|
||||||
|
* Move current, old version and all thumbnails
|
||||||
|
* to the new filename. Old file is deleted.
|
||||||
|
*
|
||||||
|
* Cache purging is done; checks for validity
|
||||||
|
* and logging are caller's responsibility
|
||||||
|
*
|
||||||
|
* @param $target Title New file name
|
||||||
|
* @return FileRepoStatus object.
|
||||||
|
*/
|
||||||
|
function move( $target ) {
|
||||||
|
$this->readOnlyError();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all versions of the file.
|
* Delete all versions of the file.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -909,6 +909,39 @@ class LocalFile extends File
|
||||||
/** isLocal inherited */
|
/** isLocal inherited */
|
||||||
/** wasDeleted inherited */
|
/** wasDeleted inherited */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move file to the new title
|
||||||
|
*
|
||||||
|
* Move current, old version and all thumbnails
|
||||||
|
* to the new filename. Old file is deleted.
|
||||||
|
*
|
||||||
|
* Cache purging is done; checks for validity
|
||||||
|
* and logging are caller's responsibility
|
||||||
|
*
|
||||||
|
* @param $target Title New file name
|
||||||
|
* @return FileRepoStatus object.
|
||||||
|
*/
|
||||||
|
function move( $target ) {
|
||||||
|
$this->lock();
|
||||||
|
$dbw = $this->repo->getMasterDB();
|
||||||
|
$batch = new LocalFileMoveBatch( $this, $target, $dbw );
|
||||||
|
$batch->addCurrent();
|
||||||
|
$batch->addOlds();
|
||||||
|
if( !$this->repo->canTransformVia404() ) {
|
||||||
|
$batch->addThumbs();
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $batch->execute();
|
||||||
|
$this->purgeEverything();
|
||||||
|
$this->unlock();
|
||||||
|
|
||||||
|
// Now switch the object and repurge
|
||||||
|
$this->title = $target;
|
||||||
|
unset( $this->name );
|
||||||
|
$this->purgeEverything();
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all versions of the file.
|
* Delete all versions of the file.
|
||||||
*
|
*
|
||||||
|
|
@ -1606,3 +1639,160 @@ class LocalFileRestoreBatch {
|
||||||
return $status;
|
return $status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for file movement
|
||||||
|
*/
|
||||||
|
class LocalFileMoveBatch {
|
||||||
|
var $file, $cur, $olds, $archive, $thumbs, $target, $db;
|
||||||
|
|
||||||
|
function __construct( File $file, Title $target, Database $db ) {
|
||||||
|
$this->file = $file;
|
||||||
|
$this->target = $target;
|
||||||
|
$this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
|
||||||
|
$this->newHash = $this->file->repo->getHashPath( $this->target->getDbKey() );
|
||||||
|
$this->oldName = $this->file->getName();
|
||||||
|
$this->newName = $this->file->repo->getNameFromTitle( $this->target );
|
||||||
|
$this->oldRel = $this->oldHash . $this->oldName;
|
||||||
|
$this->newRel = $this->newHash . $this->newName;
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCurrent() {
|
||||||
|
$this->cur = array( $this->oldRel, $this->newRel );
|
||||||
|
}
|
||||||
|
|
||||||
|
function addThumbs() {
|
||||||
|
$this->thumbs = array();
|
||||||
|
$repo = $this->file->repo;
|
||||||
|
$thumbDirRel = 'thumb/' . $this->oldRel;
|
||||||
|
$thumbDir = $repo->getZonePath( 'public' ) . '/' . $thumbDirRel;
|
||||||
|
$newThumbDirRel = 'thumb/' . $this->newRel;
|
||||||
|
if( !is_dir( $thumbDir ) || !is_readable( $thumbDir ) ) {
|
||||||
|
$this->thumbs = array();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$files = scandir( $thumbDir );
|
||||||
|
foreach( $files as $file ) {
|
||||||
|
if( $file == '.' || $file == '..' ) continue;
|
||||||
|
if( preg_match( '/^(\d+)px-/', $file, $matches ) ) {
|
||||||
|
list( $unused, $width ) = $matches;
|
||||||
|
$this->thumbs[] = array(
|
||||||
|
$thumbDirRel . '/' . $file,
|
||||||
|
$newThumbDirRel . '/' . $width . 'px-' . $this->newName
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
wfDebug( 'Strange file in thumbnail directory: ' . $thumbDirRel . '/' . $file );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOlds() {
|
||||||
|
$archiveBase = 'archive';
|
||||||
|
$this->olds = array();
|
||||||
|
|
||||||
|
$result = $this->db->select( 'oldimage',
|
||||||
|
array( 'oi_archive_name' ),
|
||||||
|
array( 'oi_name' => $this->oldName ),
|
||||||
|
__METHOD__
|
||||||
|
);
|
||||||
|
while( $row = $this->db->fetchObject( $result ) ) {
|
||||||
|
$oldname = $row->oi_archive_name;
|
||||||
|
$bits = explode( '!', $oldname, 2 );
|
||||||
|
if( count( $bits ) != 2 ) {
|
||||||
|
wfDebug( 'Invalid old file name: ' . $oldname );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list( $timestamp, $filename ) = $bits;
|
||||||
|
if( $this->oldName != $filename ) {
|
||||||
|
wfDebug( 'Invalid old file name:' . $oldName );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->olds[] = array(
|
||||||
|
"{$archiveBase}/{$this->oldHash}{$oldname}",
|
||||||
|
"{$archiveBase}/{$this->oldHash}{$timestamp}!{$this->newName}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->db->freeResult( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
function execute() {
|
||||||
|
$repo = $this->file->repo;
|
||||||
|
$status = $repo->newGood();
|
||||||
|
$triplets = $this->getMoveTriplets();
|
||||||
|
|
||||||
|
$statusDb = $this->doDBUpdates();
|
||||||
|
$statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
|
||||||
|
if( !$statusMove->isOk() ) {
|
||||||
|
$this->db->rollback();
|
||||||
|
}
|
||||||
|
$status->merge( $statusDb );
|
||||||
|
$status->merge( $statusMove );
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doDBUpdates() {
|
||||||
|
$repo = $this->file->repo;
|
||||||
|
$status = $repo->newGood();
|
||||||
|
$dbw = $this->db;
|
||||||
|
|
||||||
|
// Update current image
|
||||||
|
$dbw->update(
|
||||||
|
'image',
|
||||||
|
array( 'img_name' => $this->newName ),
|
||||||
|
array( 'img_name' => $this->oldName ),
|
||||||
|
__METHOD__
|
||||||
|
);
|
||||||
|
if( $dbw->affectedRows() ) {
|
||||||
|
$status->successCount++;
|
||||||
|
} else {
|
||||||
|
$status->failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update old images
|
||||||
|
$dbw->update(
|
||||||
|
'oldimage',
|
||||||
|
array(
|
||||||
|
'oi_name' => $this->newName,
|
||||||
|
'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ),
|
||||||
|
),
|
||||||
|
array( 'oi_name' => $this->oldName ),
|
||||||
|
__METHOD__
|
||||||
|
);
|
||||||
|
$affected = $dbw->affectedRows();
|
||||||
|
$total = count( $this->olds );
|
||||||
|
$status->successCount += $affected;
|
||||||
|
$status->failCount += $total - $affected;
|
||||||
|
|
||||||
|
// Update deleted images
|
||||||
|
$dbw->update(
|
||||||
|
'filearchive',
|
||||||
|
array(
|
||||||
|
'fa_name' => $this->newName,
|
||||||
|
'fa_archive_name = ' . $dbw->strreplace( 'fa_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ),
|
||||||
|
),
|
||||||
|
array( 'fa_name' => $this->oldName ),
|
||||||
|
__METHOD__
|
||||||
|
);
|
||||||
|
$affected = $dbw->affectedRows();
|
||||||
|
$total = count( $this->olds );
|
||||||
|
$status->successCount += $affected;
|
||||||
|
$status->failCount += $total - $affected;
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates triplets for FSRepo::storeBatch()
|
||||||
|
function getMoveTriplets() {
|
||||||
|
$moves = array_merge( array( $this->cur ), $this->olds, $this->thumbs );
|
||||||
|
$triplets = array(); // The format is: (srcUrl,destZone,desrUrl)
|
||||||
|
foreach( $moves as $move ) {
|
||||||
|
$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
|
||||||
|
$triplets[] = array( $srcUrl, 'public', $move[1] );
|
||||||
|
}
|
||||||
|
return $triplets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue