From: Victor Vasiliev Date: Sat, 3 May 2008 13:09:34 +0000 (+0000) Subject: * (bug 709) Cannot rename/move images and other media files. X-Git-Tag: 1.31.0-rc.0~47934 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=8d85629c58a9ea5e68ec2ef72d8538ea9df24571;p=lhc%2Fweb%2Fwiklou.git * (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. --- diff --git a/RELEASE-NOTES b/RELEASE-NOTES index ff399a454f..19fe2ee0f9 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -96,6 +96,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN and local one * Update documentation links in auto-generated LocalSettings.php * (bug 13584) The new hook SkinTemplateToolboxEnd was added. +* (bug 709) Cannot rename/move images and other media files [EXPERIMENTAL] === Bug fixes in 1.13 === diff --git a/includes/Database.php b/includes/Database.php index df68f7ab5c..1f19226e32 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -1655,6 +1655,18 @@ class Database { 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 */ diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 43beaf328f..92dac4af0e 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1526,6 +1526,9 @@ $wgAllowExternalImages = false; */ $wgAllowExternalImagesFrom = ''; +/** Allows to move images and other media files. Experemintal, not sure if it always works */ +$wgAllowImageMoving = false; + /** Disable database-intensive features */ $wgMiserMode = false; /** Disable all query pages if miser mode is on, not just some */ diff --git a/includes/Namespace.php b/includes/Namespace.php index af1641aaa2..6797425f30 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -51,7 +51,8 @@ class MWNamespace { * @return bool */ 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 ); } /** diff --git a/includes/Title.php b/includes/Title.php index d1a035d568..83805e2895 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -2388,6 +2388,19 @@ class Title { 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 ) { global $wgUser; $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser), @@ -2439,12 +2452,15 @@ class Title { $pageid = $this->getArticleID(); if( $nt->exists() ) { - $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); + $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); $pageCountChange = ($createRedirect ? 0 : -1); } else { # Target didn't exist, do normal move. - $this->moveToNewTitle( $nt, $reason, $createRedirect ); + $err = $this->moveToNewTitle( $nt, $reason, $createRedirect ); $pageCountChange = ($createRedirect ? 1 : 0); } + if( is_string( $err ) ) { + return $err; + } $redirid = $this->getArticleID(); // Category memberships include a sort key which may be customized. @@ -2541,6 +2557,17 @@ class Title { $oldid = $this->getArticleID(); $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 # by definition if we've got here it's rather uninteresting. # We have to remove it so that the next step doesn't trigger @@ -2636,6 +2663,17 @@ class Title { $dbw = wfGetDB( DB_MASTER ); $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 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); @@ -2701,6 +2739,15 @@ class Title { $fname = 'Title::isValidMoveTarget'; $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? $id = $nt->getArticleID(); $obj = $dbw->selectRow( array( 'page', 'revision', 'text'), diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 11a0159c52..987564cdfd 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -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 * Called by ImagePage @@ -916,6 +931,22 @@ abstract class File { 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. * diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 85347df118..3409c34042 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -909,6 +909,39 @@ class LocalFile extends File /** isLocal 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. * @@ -1606,3 +1639,160 @@ class LocalFileRestoreBatch { 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; + } +}