* (bug 709) Cannot rename/move images and other media files.
authorVictor Vasiliev <vasilievvv@users.mediawiki.org>
Sat, 3 May 2008 13:09:34 +0000 (13:09 +0000)
committerVictor Vasiliev <vasilievvv@users.mediawiki.org>
Sat, 3 May 2008 13:09:34 +0000 (13:09 +0000)
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.

RELEASE-NOTES
includes/Database.php
includes/DefaultSettings.php
includes/Namespace.php
includes/Title.php
includes/filerepo/File.php
includes/filerepo/LocalFile.php

index ff399a4..19fe2ee 100644 (file)
@@ -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 ===
 
index df68f7a..1f19226 100644 (file)
@@ -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
         */
index 43beaf3..92dac4a 100644 (file)
@@ -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 */
index af1641a..6797425 100644 (file)
@@ -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 );
        }
 
        /**
index d1a035d..83805e2 100644 (file)
@@ -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'),
index 11a0159..987564c 100644 (file)
@@ -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.
         *
index 85347df..3409c34 100644 (file)
@@ -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;
+       }
+}