| Index: trunk/phase3/includes/filerepo/LocalFile.php |
| — | — | @@ -910,6 +910,39 @@ |
| 911 | 911 | /** wasDeleted inherited */ |
| 912 | 912 | |
| 913 | 913 | /** |
| | 914 | + * Move file to the new title |
| | 915 | + * |
| | 916 | + * Move current, old version and all thumbnails |
| | 917 | + * to the new filename. Old file is deleted. |
| | 918 | + * |
| | 919 | + * Cache purging is done; checks for validity |
| | 920 | + * and logging are caller's responsibility |
| | 921 | + * |
| | 922 | + * @param $target Title New file name |
| | 923 | + * @return FileRepoStatus object. |
| | 924 | + */ |
| | 925 | + function move( $target ) { |
| | 926 | + $this->lock(); |
| | 927 | + $dbw = $this->repo->getMasterDB(); |
| | 928 | + $batch = new LocalFileMoveBatch( $this, $target, $dbw ); |
| | 929 | + $batch->addCurrent(); |
| | 930 | + $batch->addOlds(); |
| | 931 | + if( !$this->repo->canTransformVia404() ) { |
| | 932 | + $batch->addThumbs(); |
| | 933 | + } |
| | 934 | + |
| | 935 | + $status = $batch->execute(); |
| | 936 | + $this->purgeEverything(); |
| | 937 | + $this->unlock(); |
| | 938 | + |
| | 939 | + // Now switch the object and repurge |
| | 940 | + $this->title = $target; |
| | 941 | + unset( $this->name ); |
| | 942 | + $this->purgeEverything(); |
| | 943 | + return $status; |
| | 944 | + } |
| | 945 | + |
| | 946 | + /** |
| 914 | 947 | * Delete all versions of the file. |
| 915 | 948 | * |
| 916 | 949 | * Moves the files into an archive directory (or deletes them) |
| — | — | @@ -1606,3 +1639,160 @@ |
| 1607 | 1640 | return $status; |
| 1608 | 1641 | } |
| 1609 | 1642 | } |
| | 1643 | + |
| | 1644 | +#------------------------------------------------------------------------------ |
| | 1645 | + |
| | 1646 | +/** |
| | 1647 | + * Helper class for file movement |
| | 1648 | + */ |
| | 1649 | +class LocalFileMoveBatch { |
| | 1650 | + var $file, $cur, $olds, $archive, $thumbs, $target, $db; |
| | 1651 | + |
| | 1652 | + function __construct( File $file, Title $target, Database $db ) { |
| | 1653 | + $this->file = $file; |
| | 1654 | + $this->target = $target; |
| | 1655 | + $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() ); |
| | 1656 | + $this->newHash = $this->file->repo->getHashPath( $this->target->getDbKey() ); |
| | 1657 | + $this->oldName = $this->file->getName(); |
| | 1658 | + $this->newName = $this->file->repo->getNameFromTitle( $this->target ); |
| | 1659 | + $this->oldRel = $this->oldHash . $this->oldName; |
| | 1660 | + $this->newRel = $this->newHash . $this->newName; |
| | 1661 | + $this->db = $db; |
| | 1662 | + } |
| | 1663 | + |
| | 1664 | + function addCurrent() { |
| | 1665 | + $this->cur = array( $this->oldRel, $this->newRel ); |
| | 1666 | + } |
| | 1667 | + |
| | 1668 | + function addThumbs() { |
| | 1669 | + $this->thumbs = array(); |
| | 1670 | + $repo = $this->file->repo; |
| | 1671 | + $thumbDirRel = 'thumb/' . $this->oldRel; |
| | 1672 | + $thumbDir = $repo->getZonePath( 'public' ) . '/' . $thumbDirRel; |
| | 1673 | + $newThumbDirRel = 'thumb/' . $this->newRel; |
| | 1674 | + if( !is_dir( $thumbDir ) || !is_readable( $thumbDir ) ) { |
| | 1675 | + $this->thumbs = array(); |
| | 1676 | + return; |
| | 1677 | + } else { |
| | 1678 | + $files = scandir( $thumbDir ); |
| | 1679 | + foreach( $files as $file ) { |
| | 1680 | + if( $file == '.' || $file == '..' ) continue; |
| | 1681 | + if( preg_match( '/^(\d+)px-/', $file, $matches ) ) { |
| | 1682 | + list( $unused, $width ) = $matches; |
| | 1683 | + $this->thumbs[] = array( |
| | 1684 | + $thumbDirRel . '/' . $file, |
| | 1685 | + $newThumbDirRel . '/' . $width . 'px-' . $this->newName |
| | 1686 | + ); |
| | 1687 | + } else { |
| | 1688 | + wfDebug( 'Strange file in thumbnail directory: ' . $thumbDirRel . '/' . $file ); |
| | 1689 | + } |
| | 1690 | + } |
| | 1691 | + } |
| | 1692 | + } |
| | 1693 | + |
| | 1694 | + function addOlds() { |
| | 1695 | + $archiveBase = 'archive'; |
| | 1696 | + $this->olds = array(); |
| | 1697 | + |
| | 1698 | + $result = $this->db->select( 'oldimage', |
| | 1699 | + array( 'oi_archive_name' ), |
| | 1700 | + array( 'oi_name' => $this->oldName ), |
| | 1701 | + __METHOD__ |
| | 1702 | + ); |
| | 1703 | + while( $row = $this->db->fetchObject( $result ) ) { |
| | 1704 | + $oldname = $row->oi_archive_name; |
| | 1705 | + $bits = explode( '!', $oldname, 2 ); |
| | 1706 | + if( count( $bits ) != 2 ) { |
| | 1707 | + wfDebug( 'Invalid old file name: ' . $oldname ); |
| | 1708 | + continue; |
| | 1709 | + } |
| | 1710 | + list( $timestamp, $filename ) = $bits; |
| | 1711 | + if( $this->oldName != $filename ) { |
| | 1712 | + wfDebug( 'Invalid old file name:' . $oldName ); |
| | 1713 | + continue; |
| | 1714 | + } |
| | 1715 | + $this->olds[] = array( |
| | 1716 | + "{$archiveBase}/{$this->oldHash}{$oldname}", |
| | 1717 | + "{$archiveBase}/{$this->oldHash}{$timestamp}!{$this->newName}" |
| | 1718 | + ); |
| | 1719 | + } |
| | 1720 | + $this->db->freeResult( $result ); |
| | 1721 | + } |
| | 1722 | + |
| | 1723 | + function execute() { |
| | 1724 | + $repo = $this->file->repo; |
| | 1725 | + $status = $repo->newGood(); |
| | 1726 | + $triplets = $this->getMoveTriplets(); |
| | 1727 | + |
| | 1728 | + $statusDb = $this->doDBUpdates(); |
| | 1729 | + $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE ); |
| | 1730 | + if( !$statusMove->isOk() ) { |
| | 1731 | + $this->db->rollback(); |
| | 1732 | + } |
| | 1733 | + $status->merge( $statusDb ); |
| | 1734 | + $status->merge( $statusMove ); |
| | 1735 | + return $status; |
| | 1736 | + } |
| | 1737 | + |
| | 1738 | + function doDBUpdates() { |
| | 1739 | + $repo = $this->file->repo; |
| | 1740 | + $status = $repo->newGood(); |
| | 1741 | + $dbw = $this->db; |
| | 1742 | + |
| | 1743 | + // Update current image |
| | 1744 | + $dbw->update( |
| | 1745 | + 'image', |
| | 1746 | + array( 'img_name' => $this->newName ), |
| | 1747 | + array( 'img_name' => $this->oldName ), |
| | 1748 | + __METHOD__ |
| | 1749 | + ); |
| | 1750 | + if( $dbw->affectedRows() ) { |
| | 1751 | + $status->successCount++; |
| | 1752 | + } else { |
| | 1753 | + $status->failCount++; |
| | 1754 | + } |
| | 1755 | + |
| | 1756 | + // Update old images |
| | 1757 | + $dbw->update( |
| | 1758 | + 'oldimage', |
| | 1759 | + array( |
| | 1760 | + 'oi_name' => $this->newName, |
| | 1761 | + 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ), |
| | 1762 | + ), |
| | 1763 | + array( 'oi_name' => $this->oldName ), |
| | 1764 | + __METHOD__ |
| | 1765 | + ); |
| | 1766 | + $affected = $dbw->affectedRows(); |
| | 1767 | + $total = count( $this->olds ); |
| | 1768 | + $status->successCount += $affected; |
| | 1769 | + $status->failCount += $total - $affected; |
| | 1770 | + |
| | 1771 | + // Update deleted images |
| | 1772 | + $dbw->update( |
| | 1773 | + 'filearchive', |
| | 1774 | + array( |
| | 1775 | + 'fa_name' => $this->newName, |
| | 1776 | + 'fa_archive_name = ' . $dbw->strreplace( 'fa_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ), |
| | 1777 | + ), |
| | 1778 | + array( 'fa_name' => $this->oldName ), |
| | 1779 | + __METHOD__ |
| | 1780 | + ); |
| | 1781 | + $affected = $dbw->affectedRows(); |
| | 1782 | + $total = count( $this->olds ); |
| | 1783 | + $status->successCount += $affected; |
| | 1784 | + $status->failCount += $total - $affected; |
| | 1785 | + |
| | 1786 | + return $status; |
| | 1787 | + } |
| | 1788 | + |
| | 1789 | + // Generates triplets for FSRepo::storeBatch() |
| | 1790 | + function getMoveTriplets() { |
| | 1791 | + $moves = array_merge( array( $this->cur ), $this->olds, $this->thumbs ); |
| | 1792 | + $triplets = array(); // The format is: (srcUrl,destZone,desrUrl) |
| | 1793 | + foreach( $moves as $move ) { |
| | 1794 | + $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] ); |
| | 1795 | + $triplets[] = array( $srcUrl, 'public', $move[1] ); |
| | 1796 | + } |
| | 1797 | + return $triplets; |
| | 1798 | + } |
| | 1799 | +} |
| Index: trunk/phase3/includes/filerepo/File.php |
| — | — | @@ -90,6 +90,21 @@ |
| 91 | 91 | } |
| 92 | 92 | |
| 93 | 93 | /** |
| | 94 | + * Checks if file extensions are compatible |
| | 95 | + * |
| | 96 | + * @param $old File Old file |
| | 97 | + * @param $new string New name |
| | 98 | + */ |
| | 99 | + static function checkExtesnionCompatibility( File $old, $new ) { |
| | 100 | + $oldMime = $old->getMimeType(); |
| | 101 | + $n = strrpos( $new, '.' ); |
| | 102 | + $newExt = self::normalizeExtension( |
| | 103 | + $n ? substr( $new, $n + 1 ) : '' ); |
| | 104 | + $mimeMagic = MimeMagic::singleton(); |
| | 105 | + return $mimeMagic->isMatchingExtension( $newExt, $oldMime ); |
| | 106 | + } |
| | 107 | + |
| | 108 | + /** |
| 94 | 109 | * Upgrade the database row if there is one |
| 95 | 110 | * Called by ImagePage |
| 96 | 111 | * STUB |
| — | — | @@ -917,6 +932,22 @@ |
| 918 | 933 | } |
| 919 | 934 | |
| 920 | 935 | /** |
| | 936 | + * Move file to the new title |
| | 937 | + * |
| | 938 | + * Move current, old version and all thumbnails |
| | 939 | + * to the new filename. Old file is deleted. |
| | 940 | + * |
| | 941 | + * Cache purging is done; checks for validity |
| | 942 | + * and logging are caller's responsibility |
| | 943 | + * |
| | 944 | + * @param $target Title New file name |
| | 945 | + * @return FileRepoStatus object. |
| | 946 | + */ |
| | 947 | + function move( $target ) { |
| | 948 | + $this->readOnlyError(); |
| | 949 | + } |
| | 950 | + |
| | 951 | + /** |
| 921 | 952 | * Delete all versions of the file. |
| 922 | 953 | * |
| 923 | 954 | * Moves the files into an archive directory (or deletes them) |
| Index: trunk/phase3/includes/Title.php |
| — | — | @@ -2388,6 +2388,19 @@ |
| 2389 | 2389 | return 'badarticleerror'; |
| 2390 | 2390 | } |
| 2391 | 2391 | |
| | 2392 | + // Image-specific checks |
| | 2393 | + if( $this->getNamespace() == NS_IMAGE ) { |
| | 2394 | + $file = wfLocalFile( $this ); |
| | 2395 | + if( $file->exists() ) { |
| | 2396 | + if( $nt->getNamespace() != NS_IMAGE ) { |
| | 2397 | + return 'imagenocrossnamespace'; |
| | 2398 | + } |
| | 2399 | + if( !File::checkExtesnionCompatibility( $file, $nt->getDbKey() ) ) { |
| | 2400 | + return 'imagetypemismatch'; |
| | 2401 | + } |
| | 2402 | + } |
| | 2403 | + } |
| | 2404 | + |
| 2392 | 2405 | if ( $auth ) { |
| 2393 | 2406 | global $wgUser; |
| 2394 | 2407 | $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser), |
| — | — | @@ -2439,12 +2452,15 @@ |
| 2440 | 2453 | |
| 2441 | 2454 | $pageid = $this->getArticleID(); |
| 2442 | 2455 | if( $nt->exists() ) { |
| 2443 | | - $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); |
| | 2456 | + $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); |
| 2444 | 2457 | $pageCountChange = ($createRedirect ? 0 : -1); |
| 2445 | 2458 | } else { # Target didn't exist, do normal move. |
| 2446 | | - $this->moveToNewTitle( $nt, $reason, $createRedirect ); |
| | 2459 | + $err = $this->moveToNewTitle( $nt, $reason, $createRedirect ); |
| 2447 | 2460 | $pageCountChange = ($createRedirect ? 1 : 0); |
| 2448 | 2461 | } |
| | 2462 | + if( is_string( $err ) ) { |
| | 2463 | + return $err; |
| | 2464 | + } |
| 2449 | 2465 | $redirid = $this->getArticleID(); |
| 2450 | 2466 | |
| 2451 | 2467 | // Category memberships include a sort key which may be customized. |
| — | — | @@ -2541,6 +2557,17 @@ |
| 2542 | 2558 | $oldid = $this->getArticleID(); |
| 2543 | 2559 | $dbw = wfGetDB( DB_MASTER ); |
| 2544 | 2560 | |
| | 2561 | + # Move an image if it is |
| | 2562 | + if( $this->getNamespace() == NS_IMAGE ) { |
| | 2563 | + $file = wfLocalFile( $this ); |
| | 2564 | + if( $file->exists() ) { |
| | 2565 | + $status = $file->move( $nt ); |
| | 2566 | + if( !$status->isOk() ) { |
| | 2567 | + return $status->getWikiText(); |
| | 2568 | + } |
| | 2569 | + } |
| | 2570 | + } |
| | 2571 | + |
| 2545 | 2572 | # Delete the old redirect. We don't save it to history since |
| 2546 | 2573 | # by definition if we've got here it's rather uninteresting. |
| 2547 | 2574 | # We have to remove it so that the next step doesn't trigger |
| — | — | @@ -2636,6 +2663,17 @@ |
| 2637 | 2664 | $dbw = wfGetDB( DB_MASTER ); |
| 2638 | 2665 | $now = $dbw->timestamp(); |
| 2639 | 2666 | |
| | 2667 | + # Move an image if it is |
| | 2668 | + if( $this->getNamespace() == NS_IMAGE ) { |
| | 2669 | + $file = wfLocalFile( $this ); |
| | 2670 | + if( $file->exists() ) { |
| | 2671 | + $status = $file->move( $nt ); |
| | 2672 | + if( !$status->isOk() ) { |
| | 2673 | + return $status->getWikiText(); |
| | 2674 | + } |
| | 2675 | + } |
| | 2676 | + } |
| | 2677 | + |
| 2640 | 2678 | # Save a null revision in the page's history notifying of the move |
| 2641 | 2679 | $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); |
| 2642 | 2680 | $nullRevId = $nullRevision->insertOn( $dbw ); |
| — | — | @@ -2701,6 +2739,15 @@ |
| 2702 | 2740 | $fname = 'Title::isValidMoveTarget'; |
| 2703 | 2741 | $dbw = wfGetDB( DB_MASTER ); |
| 2704 | 2742 | |
| | 2743 | + # Is it an existsing file? |
| | 2744 | + if( $nt->getNamespace() == NS_IMAGE ) { |
| | 2745 | + $file = wfLocalFile( $nt ); |
| | 2746 | + if( $file->exists() ) { |
| | 2747 | + wfDebug( __METHOD__ . ": file exists\n" ); |
| | 2748 | + return false; |
| | 2749 | + } |
| | 2750 | + } |
| | 2751 | + |
| 2705 | 2752 | # Is it a redirect? |
| 2706 | 2753 | $id = $nt->getArticleID(); |
| 2707 | 2754 | $obj = $dbw->selectRow( array( 'page', 'revision', 'text'), |
| Index: trunk/phase3/includes/Namespace.php |
| — | — | @@ -51,7 +51,8 @@ |
| 52 | 52 | * @return bool |
| 53 | 53 | */ |
| 54 | 54 | public static function isMovable( $index ) { |
| 55 | | - return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ); |
| | 55 | + global $wgAllowImageMoving; |
| | 56 | + return !( $index < NS_MAIN || ($index == NS_IMAGE && !$wgAllowImageMoving) || $index == NS_CATEGORY ); |
| 56 | 57 | } |
| 57 | 58 | |
| 58 | 59 | /** |
| Index: trunk/phase3/includes/DefaultSettings.php |
| — | — | @@ -1526,6 +1526,9 @@ |
| 1527 | 1527 | */ |
| 1528 | 1528 | $wgAllowExternalImagesFrom = ''; |
| 1529 | 1529 | |
| | 1530 | +/** Allows to move images and other media files. Experemintal, not sure if it always works */ |
| | 1531 | +$wgAllowImageMoving = false; |
| | 1532 | + |
| 1530 | 1533 | /** Disable database-intensive features */ |
| 1531 | 1534 | $wgMiserMode = false; |
| 1532 | 1535 | /** Disable all query pages if miser mode is on, not just some */ |
| Index: trunk/phase3/includes/Database.php |
| — | — | @@ -1656,6 +1656,18 @@ |
| 1657 | 1657 | } |
| 1658 | 1658 | |
| 1659 | 1659 | /** |
| | 1660 | + * Returns a comand for str_replace function in SQL query. |
| | 1661 | + * Uses REPLACE() in MySQL |
| | 1662 | + * |
| | 1663 | + * @param string $orig String or column to modify |
| | 1664 | + * @param string $old String or column to seek |
| | 1665 | + * @param string $new String or column to replace with |
| | 1666 | + */ |
| | 1667 | + function strreplace( $orig, $old, $new ) { |
| | 1668 | + return "REPLACE({$orig}, {$old}, {$new})"; |
| | 1669 | + } |
| | 1670 | + |
| | 1671 | + /** |
| 1660 | 1672 | * Determines if the last failure was due to a deadlock |
| 1661 | 1673 | */ |
| 1662 | 1674 | function wasDeadlock() { |
| Index: trunk/phase3/RELEASE-NOTES |
| — | — | @@ -96,6 +96,7 @@ |
| 97 | 97 | and local one |
| 98 | 98 | * Update documentation links in auto-generated LocalSettings.php |
| 99 | 99 | * (bug 13584) The new hook SkinTemplateToolboxEnd was added. |
| | 100 | +* (bug 709) Cannot rename/move images and other media files [EXPERIMENTAL] |
| 100 | 101 | |
| 101 | 102 | === Bug fixes in 1.13 === |
| 102 | 103 | |