r24313 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r24312‎ | r24313 | r24314 >
Date:14:45, 22 July 2007
Author:tstarling
Status:old
Tags:
Comment:
* Introduced FileRepoStatus -- result class for file repo operations.
* Ported file delete/restore to the filerepo framework. Some user-visible changes in error reporting.
* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally. Added a "deleted" directory for the default location, protected by a .htaccess file and the practical obscurity of content hashes.
* Fixed bug 2735: "Preview" shown in title bar for action=submit on special pages
* Removed "restore" links from the deletion log embedded in Special:Undelete
* Added img_sha1/oi_sha1 fields, preserved through upload, delete and restore
* Referenced the new oi_metadata etc. fields to preserve metadata across upload and delete/restore.
Modified paths:
  • /trunk/phase3/RELEASE-NOTES (modified) (history)
  • /trunk/phase3/StartProfiler.php (modified) (history)
  • /trunk/phase3/images/deleted (added) (history)
  • /trunk/phase3/images/deleted/.htaccess (added) (history)
  • /trunk/phase3/includes/Article.php (modified) (history)
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/EditPage.php (modified) (history)
  • /trunk/phase3/includes/FileStore.php (modified) (history)
  • /trunk/phase3/includes/GlobalFunctions.php (modified) (history)
  • /trunk/phase3/includes/ImagePage.php (modified) (history)
  • /trunk/phase3/includes/OutputPage.php (modified) (history)
  • /trunk/phase3/includes/PageHistory.php (modified) (history)
  • /trunk/phase3/includes/Setup.php (modified) (history)
  • /trunk/phase3/includes/SpecialLog.php (modified) (history)
  • /trunk/phase3/includes/SpecialUndelete.php (modified) (history)
  • /trunk/phase3/includes/SpecialUpload.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FSRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/File.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FileRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FileRepoStatus.php (added) (history)
  • /trunk/phase3/includes/filerepo/ForeignDBRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/LocalFile.php (modified) (history)
  • /trunk/phase3/includes/filerepo/LocalRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/OldLocalFile.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/maintenance/archives/patch-img_sha1.sql (added) (history)
  • /trunk/phase3/maintenance/rebuildImages.php (modified) (history)
  • /trunk/phase3/maintenance/tables.sql (modified) (history)
  • /trunk/phase3/maintenance/updaters.inc (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/archives/patch-img_sha1.sql
@@ -0,0 +1,8 @@
 2+-- Add img_sha1, oi_sha1 and related indexes
 3+ALTER TABLE image
 4+ ADD COLUMN img_sha1 varbinary(31) NOT NULL default '',
 5+ ADD INDEX img_sha1 (img_sha1);
 6+
 7+ALTER TABLE oldimage
 8+ ADD COLUMN oi_sha1 varbinary(31) NOT NULL default '',
 9+ ADD INDEX oi_sha1 (oi_sha1);
Property changes on: trunk/phase3/maintenance/archives/patch-img_sha1.sql
___________________________________________________________________
Added: svn:eol-style
110 + native
Index: trunk/phase3/maintenance/updaters.inc
@@ -81,6 +81,7 @@
8282 array( 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ),
8383 array( 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql'),
8484 array( 'archive', 'ar_page', 'patch-archive-ar_page.sql'),
 85+ array( 'image', 'img_sha1', 'patch-img_sha1.sql' ),
8586 );
8687
8788 # For extensions only, should be populated via hooks
Index: trunk/phase3/maintenance/rebuildImages.php
@@ -173,8 +173,6 @@
174174 function addMissingImage( $filename, $fullpath ) {
175175 $fname = 'ImageBuilder::addMissingImage';
176176
177 - $size = filesize( $fullpath );
178 - $info = $this->imageInfo( $fullpath );
179177 $timestamp = $this->dbw->timestamp( filemtime( $fullpath ) );
180178
181179 global $wgContLang;
Index: trunk/phase3/maintenance/tables.sql
@@ -686,14 +686,21 @@
687687 -- Time of the upload.
688688 img_timestamp varbinary(14) NOT NULL default '',
689689
 690+ -- SHA-1 content hash in base-36
 691+ img_sha1 varbinary(32) NOT NULL default '',
 692+
690693 PRIMARY KEY img_name (img_name),
691694
692695 INDEX img_usertext_timestamp (img_user_text,img_timestamp),
693696 -- Used by Special:Imagelist for sort-by-size
694697 INDEX img_size (img_size),
695698 -- Used by Special:Newimages and Special:Imagelist
696 - INDEX img_timestamp (img_timestamp)
 699+ INDEX img_timestamp (img_timestamp),
697700
 701+ -- For future use
 702+ INDEX img_sha1 (img_sha1),
 703+
 704+
698705 ) /*$wgDBTableOptions*/;
699706
700707 --
@@ -724,11 +731,13 @@
725732 oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
726733 oi_minor_mime varbinary(32) NOT NULL default "unknown",
727734 oi_deleted tinyint unsigned NOT NULL default '0',
 735+ oi_sha1 varbinary(32) NOT NULL default '',
728736
729737 INDEX oi_usertext_timestamp (oi_user_text,oi_timestamp),
730738 INDEX oi_name_timestamp (oi_name,oi_timestamp),
731739 -- oi_archive_name truncated to 14 to avoid key length overflow
732 - INDEX oi_name_archive_name (oi_name,oi_archive_name(14))
 740+ INDEX oi_name_archive_name (oi_name,oi_archive_name(14)),
 741+ INDEX oi_sha1 (oi_sha1)
733742
734743 ) /*$wgDBTableOptions*/;
735744
Index: trunk/phase3/images/deleted/.htaccess
@@ -0,0 +1 @@
 2+Order Allow,Deny
Index: trunk/phase3/includes/Article.php
@@ -2806,6 +2806,7 @@
28072807 $page = $this->mTitle->getSubjectPage();
28082808
28092809 $wgOut->setPagetitle( $page->getPrefixedText() );
 2810+ $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
28102811 $wgOut->setSubtitle( wfMsg( 'infosubtitle' ));
28112812
28122813 # first, see if the page exists at all.
@@ -3063,4 +3064,4 @@
30643065 $wgOut->addParserOutput( $parserOutput );
30653066 }
30663067
3067 -}
\ No newline at end of file
 3068+}
Index: trunk/phase3/includes/GlobalFunctions.php
@@ -237,6 +237,13 @@
238238 * Log to a file without getting "file size exceeded" signals
239239 */
240240 function wfErrorLog( $text, $file ) {
 241+ # Temp: add unique request prefix
 242+ static $prefix;
 243+ if ( !isset( $prefix ) ) {
 244+ $prefix = chr( mt_rand( 33, 126 ) ) . chr( mt_rand( 33, 126 ) ) . chr( mt_rand( 33, 126 ) ) . '| ';
 245+ }
 246+ $text = $prefix . $text;
 247+
241248 wfSuppressWarnings();
242249 $exists = file_exists( $file );
243250 $size = $exists ? filesize( $file ) : false;
@@ -2139,7 +2146,9 @@
21402147 }
21412148 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure);
21422149 session_cache_limiter( 'private, must-revalidate' );
 2150+ wfDebug( "Starting session..." );
21432151 @session_start();
 2152+ wfDebug( "ok\n" );
21442153 }
21452154
21462155 /**
@@ -2296,4 +2305,4 @@
22972306 */
22982307 function wfBoolToStr( $value ) {
22992308 return $value ? 'true' : 'false';
2300 -}
\ No newline at end of file
 2309+}
Index: trunk/phase3/includes/ImagePage.php
@@ -579,56 +579,56 @@
580580 $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
581581 return;
582582 }
583 - if ( !$this->doDeleteOldImage( $oldimage ) ) {
584 - return;
585 - }
 583+ $status = $this->doDeleteOldImage( $oldimage );
586584 $deleted = $oldimage;
587585 } else {
588 - $ok = $this->img->delete( $reason );
589 - if( !$ok ) {
590 - # If the deletion operation actually failed, bug out:
591 - $wgOut->showFileDeleteError( $this->img->getName() );
592 - return;
 586+ $status = $this->img->delete( $reason );
 587+ if ( !$status->isGood() ) {
 588+ // Warning or error
 589+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
593590 }
594 -
595 - # Image itself is now gone, and database is cleaned.
596 - # Now we remove the image description page.
597 -
598 - $article = new Article( $this->mTitle );
599 - $article->doDeleteArticle( $reason ); # ignore errors
600 -
601 - $deleted = $this->img->getName();
 591+ if ( $status->ok ) {
 592+ # Image itself is now gone, and database is cleaned.
 593+ # Now we remove the image description page.
 594+ $article = new Article( $this->mTitle );
 595+ $article->doDeleteArticle( $reason ); # ignore errors
 596+ $deleted = $this->img->getName();
 597+ }
602598 }
603599
604 - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
605600 $wgOut->setRobotpolicy( 'noindex,nofollow' );
606601
607 - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
608 - $text = wfMsg( 'deletedtext', $deleted, $loglink );
609 -
610 - $wgOut->addWikiText( $text );
611 -
612 - $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 602+ if ( !$status->ok ) {
 603+ // Fatal error flagged
 604+ $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
 605+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 606+ } else {
 607+ // Operation completed
 608+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
 609+ $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
 610+ $text = wfMsg( 'deletedtext', $deleted, $loglink );
 611+ $wgOut->addWikiText( $text );
 612+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 613+ }
613614 }
614615
615616 /**
616 - * @return success
 617+ * Delete an old revision of an image,
 618+ * @return FileRepoStatus
617619 */
618 - function doDeleteOldImage( $oldimage )
619 - {
 620+ function doDeleteOldImage( $oldimage ) {
620621 global $wgOut;
621622
622 - $ok = $this->img->deleteOld( $oldimage, '' );
623 - if( !$ok ) {
624 - # If we actually have a file and can't delete it, throw an error.
625 - # Something went awry...
626 - $wgOut->showFileDeleteError( "$oldimage" );
627 - } else {
 623+ $status = $this->img->deleteOld( $oldimage, '' );
 624+ if( !$status->isGood() ) {
 625+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
 626+ }
 627+ if ( $status->ok ) {
628628 # Log the deletion
629629 $log = new LogPage( 'delete' );
630630 $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) );
631631 }
632 - return $ok;
 632+ return $status;
633633 }
634634
635635 function revert() {
@@ -667,10 +667,11 @@
668668
669669 $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage );
670670 $comment = wfMsg( "reverted" );
671 - $result = $this->img->upload( $sourcePath, $comment, $comment );
 671+ // TODO: preserve file properties from DB instead of reloading from file
 672+ $status = $this->img->upload( $sourcePath, $comment, $comment );
672673
673 - if ( WikiError::isError( $result ) ) {
674 - $this->showError( $result );
 674+ if ( !$status->isGood() ) {
 675+ $this->showError( $status->getWikiText() );
675676 return;
676677 }
677678
@@ -699,15 +700,15 @@
700701 }
701702
702703 /**
703 - * Display an error from a wikitext-formatted WikiError object
 704+ * Display an error with a wikitext description
704705 */
705 - function showError( WikiError $error ) {
 706+ function showError( $description ) {
706707 global $wgOut;
707708 $wgOut->setPageTitle( wfMsg( "internalerror" ) );
708709 $wgOut->setRobotpolicy( "noindex,nofollow" );
709710 $wgOut->setArticleRelated( false );
710711 $wgOut->enableClientCache( false );
711 - $wgOut->addWikiText( $error->getMessage() );
 712+ $wgOut->addWikiText( $description );
712713 }
713714
714715 }
Index: trunk/phase3/includes/Setup.php
@@ -54,6 +54,11 @@
5555 if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
5656 if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
5757
 58+if ( empty( $wgFileStore['deleted']['directory'] ) ) {
 59+ $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
 60+}
 61+
 62+
5863 /**
5964 * Initialise $wgLocalFileRepo from backwards-compatible settings
6065 */
@@ -67,6 +72,8 @@
6873 'thumbScriptUrl' => $wgThumbnailScriptPath,
6974 'transformVia404' => !$wgGenerateThumbnailOnParse,
7075 'initialCapital' => $wgCapitalLinks,
 76+ 'deletedDir' => $wgFileStore['deleted']['directory'],
 77+ 'deletedHashLevels' => $wgFileStore['deleted']['hash']
7178 );
7279 }
7380 /**
@@ -87,7 +94,7 @@
8895 'dbUser' => $wgDBuser,
8996 'dbPassword' => $wgDBpassword,
9097 'dbName' => $wgSharedUploadDBname,
91 - 'dbFlags' => DBO_DEFAULT,
 98+ 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT,
9299 'tablePrefix' => $wgSharedUploadDBprefix,
93100 'hasSharedCache' => $wgCacheSharedUploads,
94101 'descBaseUrl' => $wgRepositoryBaseUrl,
Index: trunk/phase3/includes/filerepo/FileRepoStatus.php
@@ -0,0 +1,170 @@
 2+<?php
 3+
 4+/**
 5+ * Generic operation result class
 6+ * Has warning/error list, boolean status and arbitrary value
 7+ */
 8+class FileRepoStatus {
 9+ var $ok = true;
 10+ var $value;
 11+
 12+ /** Counters for batch operations */
 13+ var $successCount = 0, $failCount = 0;
 14+
 15+ /*semi-private*/ var $errors = array();
 16+ /*semi-private*/ var $cleanCallback = false;
 17+
 18+ /**
 19+ * Factory function for fatal errors
 20+ */
 21+ static function newFatal( $repo, $message /*, parameters...*/ ) {
 22+ $params = array_slice( func_get_args(), 1 );
 23+ $result = new self( $repo );
 24+ call_user_func_array( array( &$result, 'error' ), $params );
 25+ $result->ok = false;
 26+ }
 27+
 28+ static function newGood( $repo = false, $value = null ) {
 29+ $result = new self( $repo );
 30+ $result->value = $value;
 31+ return $result;
 32+ }
 33+
 34+ function __construct( $repo = false ) {
 35+ if ( $repo ) {
 36+ $this->cleanCallback = $repo->getErrorCleanupFunction();
 37+ }
 38+ }
 39+
 40+ function setResult( $ok, $value = null ) {
 41+ $this->ok = $ok;
 42+ $this->value = $value;
 43+ }
 44+
 45+ function isGood() {
 46+ return $this->ok && !$this->errors;
 47+ }
 48+
 49+ function isOK() {
 50+ return $this->ok;
 51+ }
 52+
 53+ function warning( $message /*, parameters... */ ) {
 54+ $params = array_slice( func_get_args(), 1 );
 55+ $this->errors[] = array(
 56+ 'type' => 'warning',
 57+ 'message' => $message,
 58+ 'params' => $params );
 59+ }
 60+
 61+ /**
 62+ * Add an error, do not set fatal flag
 63+ * This can be used for non-fatal errors
 64+ */
 65+ function error( $message /*, parameters... */ ) {
 66+ $params = array_slice( func_get_args(), 1 );
 67+ $this->errors[] = array(
 68+ 'type' => 'error',
 69+ 'message' => $message,
 70+ 'params' => $params );
 71+ }
 72+
 73+ /**
 74+ * Add an error and set OK to false, indicating that the operation as a whole was fatal
 75+ */
 76+ function fatal( $message /*, parameters... */ ) {
 77+ $params = array_slice( func_get_args(), 1 );
 78+ $this->errors[] = array(
 79+ 'type' => 'error',
 80+ 'message' => $message,
 81+ 'params' => $params );
 82+ $this->ok = false;
 83+ }
 84+
 85+ protected function cleanParams( $params ) {
 86+ if ( !$this->cleanCallback ) {
 87+ return $params;
 88+ }
 89+ $cleanParams = array();
 90+ foreach ( $params as $i => $param ) {
 91+ $cleanParams[$i] = call_user_func( $this->cleanCallback, $param );
 92+ }
 93+ return $cleanParams;
 94+ }
 95+
 96+ protected function getItemXML( $item ) {
 97+ $params = $this->cleanParams( $item['params'] );
 98+ $xml = "<{$item['type']}>\n" .
 99+ Xml::element( 'message', null, $item['message'] ) . "\n" .
 100+ Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n";
 101+ foreach ( $params as $param ) {
 102+ $xml .= Xml::element( 'param', null, $param );
 103+ }
 104+ $xml .= "</{$this->type}>\n";
 105+ return $xml;
 106+ }
 107+
 108+ /**
 109+ * Get the error list as XML
 110+ */
 111+ function getXML() {
 112+ $xml = "<errors>\n";
 113+ foreach ( $this->errors as $error ) {
 114+ $xml .= $this->getItemXML( $error );
 115+ }
 116+ $xml .= "</errors>\n";
 117+ return $xml;
 118+ }
 119+
 120+ /**
 121+ * Get the error list as a wikitext formatted list
 122+ * @param string $shortContext A short enclosing context message name, to be used
 123+ * when there is a single error
 124+ * @param string $longContext A long enclosing context message name, for a list
 125+ */
 126+ function getWikiText( $shortContext = false, $longContext = false ) {
 127+ if ( count( $this->errors ) == 0 ) {
 128+ if ( $this->ok ) {
 129+ $this->fatal( 'internalerror_info',
 130+ __METHOD__." called for a good result, this is incorrect\n" );
 131+ } else {
 132+ $this->fatal( 'internalerror_info',
 133+ __METHOD__.": Invalid result object: no error text but not OK\n" );
 134+ }
 135+ }
 136+ if ( count( $this->errors ) == 1 ) {
 137+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) );
 138+ $s = wfMsgReal( $this->errors[0]['message'], $params );
 139+ if ( $shortContext ) {
 140+ $s = wfMsg( $shortContext, $s );
 141+ } elseif ( $longContext ) {
 142+ $s = wfMsg( $longContext, "* $s\n" );
 143+ }
 144+ } else {
 145+ $s = '';
 146+ foreach ( $this->errors as $error ) {
 147+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) );
 148+ $s .= '* ' . wfMsgReal( $error['message'], $params ) . "\n";
 149+ }
 150+ if ( $longContext ) {
 151+ $s = wfMsg( $longContext, $s );
 152+ } elseif ( $shortContext ) {
 153+ $s = wfMsg( $shortContext, "\n* $s\n" );
 154+ }
 155+ }
 156+ return $s;
 157+ }
 158+
 159+ /**
 160+ * Merge another status object into this one
 161+ */
 162+ function merge( $other, $overwriteValue = false ) {
 163+ $this->errors = array_merge( $this->errors, $other->errors );
 164+ $this->ok = $this->ok && $other->ok;
 165+ if ( $overwriteValue ) {
 166+ $this->value = $other->value;
 167+ }
 168+ $this->successCount += $other->successCount;
 169+ $this->failCount += $other->failCount;
 170+ }
 171+}
Property changes on: trunk/phase3/includes/filerepo/FileRepoStatus.php
___________________________________________________________________
Added: svn:eol-style
1172 + native
Index: trunk/phase3/includes/filerepo/OldLocalFile.php
@@ -70,7 +70,7 @@
7171 }
7272 $oldImages = $wgMemc->get( $key );
7373
74 - if ( isset( $oldImages['version'] ) && $oldImages['version'] == MW_OLDFILE_VERSION ) {
 74+ if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) {
7575 unset( $oldImages['version'] );
7676 $more = isset( $oldImages['more'] );
7777 unset( $oldImages['more'] );
@@ -94,7 +94,8 @@
9595 if ( $found ) {
9696 wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" );
9797 $this->dataLoaded = true;
98 - foreach ( $cachedValues as $name => $value ) {
 98+ $this->fileExists = true;
 99+ foreach ( $info as $name => $value ) {
99100 $this->$name = $value;
100101 }
101102 } elseif ( $more ) {
@@ -130,7 +131,7 @@
131132 wfProfileIn( __METHOD__ );
132133
133134 $dbr = $this->repo->getSlaveDB();
134 - $res = $dbr->select( 'oldimage', $this->getCacheFields(),
 135+ $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ),
135136 array( 'oi_name' => $this->getName() ), __METHOD__,
136137 array(
137138 'LIMIT' => self::MAX_CACHE_ROWS + 1,
@@ -144,8 +145,8 @@
145146 }
146147 for ( $i = 0; $i < $numRows; $i++ ) {
147148 $row = $dbr->fetchObject( $res );
148 - $this->decodeRow( $row, 'oi_' );
149 - $cache[$row->oi_timestamp] = $row;
 149+ $decoded = $this->decodeRow( $row, 'oi_' );
 150+ $cache[$row->oi_timestamp] = $decoded;
150151 }
151152 $dbr->freeResult( $res );
152153 $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ );
@@ -169,6 +170,7 @@
170171 $this->fileExists = false;
171172 }
172173 $this->dataLoaded = true;
 174+ wfProfileOut( __METHOD__ );
173175 }
174176
175177 function getCacheFields( $prefix = 'img_' ) {
@@ -207,15 +209,14 @@
208210 #'oi_major_mime' => $major,
209211 #'oi_minor_mime' => $minor,
210212 #'oi_metadata' => $this->metadata,
211 - ), array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->requestedTime ),
 213+ 'oi_sha1' => $this->sha1,
 214+ ), array(
 215+ 'oi_name' => $this->getName(),
 216+ 'oi_archive_name' => $this->archive_name ),
212217 __METHOD__
213218 );
214219 wfProfileOut( __METHOD__ );
215220 }
216 -
217 - // XXX: Temporary hack before schema update
218 - function maybeUpgradeRow() {}
219 -
220221 }
221222
222223
Index: trunk/phase3/includes/filerepo/LocalFile.php
@@ -41,10 +41,12 @@
4242 $major_mime, # Major mime type
4343 $minor_mine, # Minor mime type
4444 $size, # Size in bytes (loadFromXxx)
45 - $metadata, # Metadata
 45+ $metadata, # Handler-specific metadata
4646 $timestamp, # Upload timestamp
 47+ $sha1, # SHA-1 base 36 content hash
4748 $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
48 - $upgraded; # Whether the row was upgraded on load
 49+ $upgraded, # Whether the row was upgraded on load
 50+ $locked; # True if the image row is locked
4951
5052 /**#@-*/
5153
@@ -156,7 +158,7 @@
157159
158160 function getCacheFields( $prefix = 'img_' ) {
159161 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
160 - 'major_mime', 'minor_mime', 'metadata', 'timestamp' );
 162+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' );
161163 static $results = array();
162164 if ( $prefix == '' ) {
163165 return $fields;
@@ -175,7 +177,9 @@
176178 * Load file metadata from the DB
177179 */
178180 function loadFromDB() {
179 - wfProfileIn( __METHOD__ );
 181+ # Polymorphic function name to distinguish foreign and local fetches
 182+ $fname = get_class( $this ) . '::' . __FUNCTION__;
 183+ wfProfileIn( $fname );
180184
181185 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
182186 $this->dataLoaded = true;
@@ -183,14 +187,14 @@
184188 $dbr = $this->repo->getSlaveDB();
185189
186190 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
187 - array( 'img_name' => $this->getName() ), __METHOD__ );
 191+ array( 'img_name' => $this->getName() ), $fname );
188192 if ( $row ) {
189193 $this->loadFromRow( $row );
190194 } else {
191195 $this->fileExists = false;
192196 }
193197
194 - wfProfileOut( __METHOD__ );
 198+ wfProfileOut( $fname );
195199 }
196200
197201 /**
@@ -218,6 +222,8 @@
219223 }
220224 $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
221225 }
 226+ # Trim zero padding from char/binary field
 227+ $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
222228 return $decoded;
223229 }
224230
@@ -254,7 +260,10 @@
255261 if ( wfReadOnly() ) {
256262 return;
257263 }
258 - if ( is_null($this->media_type) || $this->mime == 'image/svg' ) {
 264+ if ( is_null($this->media_type) ||
 265+ $this->mime == 'image/svg' ||
 266+ $this->sha1 == ''
 267+ ) {
259268 $this->upgradeRow();
260269 $this->upgraded = true;
261270 } else {
@@ -292,6 +301,7 @@
293302 'img_major_mime' => $major,
294303 'img_minor_mime' => $minor,
295304 'img_metadata' => $this->metadata,
 305+ 'img_sha1' => $this->sha1,
296306 ), array( 'img_name' => $this->getName() ),
297307 __METHOD__
298308 );
@@ -480,12 +490,23 @@
481491 function purgeMetadataCache() {
482492 $this->loadFromDB();
483493 $this->saveToCache();
 494+ $this->purgeHistory();
484495 }
485496
486497 /**
 498+ * Purge the shared history (OldLocalFile) cache
 499+ */
 500+ function purgeHistory() {
 501+ global $wgMemc;
 502+ $hashedName = md5($this->getName());
 503+ $oldKey = wfMemcKey( 'oldfile', $hashedName );
 504+ $wgMemc->delete( $oldKey );
 505+ }
 506+
 507+ /**
487508 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
488509 */
489 - function purgeCache( $archiveFiles = array() ) {
 510+ function purgeCache() {
490511 // Refresh metadata cache
491512 $this->purgeMetadataCache();
492513
@@ -596,6 +617,8 @@
597618 /** getHashPath inherited */
598619 /** getRel inherited */
599620 /** getUrlRel inherited */
 621+ /** getArchiveRel inherited */
 622+ /** getThumbRel inherited */
600623 /** getArchivePath inherited */
601624 /** getThumbPath inherited */
602625 /** getArchiveUrl inherited */
@@ -615,18 +638,19 @@
616639 * is already known
617640 * @param string $timestamp Timestamp for img_timestamp, or false to use the current time
618641 *
619 - * @return Returns the archive name on success or an empty string if it was a new upload.
620 - * Returns a wikitext-formatted WikiError on failure.
 642+ * @return FileRepoStatus object. On success, the value member contains the
 643+ * archive name, or an empty string if it was a new file.
621644 */
622645 function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) {
623 - $archive = $this->publish( $srcPath, $flags );
624 - if ( WikiError::isError( $archive ) ){
625 - return $archive;
 646+ $this->lock();
 647+ $status = $this->publish( $srcPath, $flags );
 648+ if ( $status->ok ) {
 649+ if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) {
 650+ $status->fatal( 'filenotfound', $srcPath );
 651+ }
626652 }
627 - if ( !$this->recordUpload2( $archive, $comment, $pageText, $props, $timestamp ) ) {
628 - return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) );
629 - }
630 - return $archive;
 653+ $this->unlock();
 654+ return $status;
631655 }
632656
633657 /**
@@ -695,6 +719,7 @@
696720 'img_user' => $wgUser->getID(),
697721 'img_user_text' => $wgUser->getName(),
698722 'img_metadata' => $this->metadata,
 723+ 'img_sha1' => $this->sha1
699724 ),
700725 __METHOD__,
701726 'IGNORE'
@@ -715,6 +740,11 @@
716741 'oi_description' => 'img_description',
717742 'oi_user' => 'img_user',
718743 'oi_user_text' => 'img_user_text',
 744+ 'oi_metadata' => 'img_metadata',
 745+ 'oi_media_type' => 'img_media_type',
 746+ 'oi_major_mime' => 'img_major_mime',
 747+ 'oi_minor_mime' => 'img_minor_mime',
 748+ 'oi_sha1' => 'img_sha1',
719749 ), array( 'img_name' => $this->getName() ), __METHOD__
720750 );
721751
@@ -733,6 +763,7 @@
734764 'img_user' => $wgUser->getID(),
735765 'img_user_text' => $wgUser->getName(),
736766 'img_metadata' => $this->metadata,
 767+ 'img_sha1' => $this->sha1
737768 ), array( /* WHERE */
738769 'img_name' => $this->getName()
739770 ), __METHOD__
@@ -792,22 +823,23 @@
793824 * @param integer $flags A bitwise combination of:
794825 * File::DELETE_SOURCE Delete the source file, i.e. move
795826 * rather than copy
796 - * @return The archive name on success or an empty string if it was a new
797 - * file, and a wikitext-formatted WikiError object on failure.
 827+ * @return FileRepoStatus object. On success, the value member contains the
 828+ * archive name, or an empty string if it was a new file.
798829 */
799830 function publish( $srcPath, $flags = 0 ) {
 831+ $this->lock();
800832 $dstRel = $this->getRel();
801833 $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
802834 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
803835 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
804836 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
805 - if ( WikiError::isError( $status ) ) {
806 - return $status;
807 - } elseif ( $status == 'new' ) {
808 - return '';
 837+ if ( $status->value == 'new' ) {
 838+ $status->value = '';
809839 } else {
810 - return $archiveName;
 840+ $status->value = $archiveName;
811841 }
 842+ $this->unlock();
 843+ return $status;
812844 }
813845
814846 /** getLinksTo inherited */
@@ -824,62 +856,34 @@
825857 * Cache purging is done; logging is caller's responsibility.
826858 *
827859 * @param $reason
828 - * @return true on success, false on some kind of failure
 860+ * @return FileRepoStatus object.
829861 */
830 - function delete( $reason, $suppress=false ) {
831 - $transaction = new FSTransaction();
832 - $urlArr = array( $this->getURL() );
 862+ function delete( $reason ) {
 863+ $this->lock();
 864+ $batch = new LocalFileDeleteBatch( $this, $reason );
 865+ $batch->addCurrent();
833866
834 - if( !FileStore::lock() ) {
835 - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
836 - return false;
 867+ # Get old version relative paths
 868+ $dbw = $this->repo->getMasterDB();
 869+ $result = $dbw->select( 'oldimage',
 870+ array( 'oi_archive_name' ),
 871+ array( 'oi_name' => $this->getName() ) );
 872+ while ( $row = $dbw->fetchObject( $result ) ) {
 873+ $batch->addOld( $row->oi_archive_name );
837874 }
 875+ $status = $batch->execute();
838876
839 - try {
840 - $dbw = $this->repo->getMasterDB();
841 - $dbw->begin();
842 -
843 - // Delete old versions
844 - $result = $dbw->select( 'oldimage',
845 - array( 'oi_archive_name' ),
846 - array( 'oi_name' => $this->getName() ) );
847 -
848 - while( $row = $dbw->fetchObject( $result ) ) {
849 - $oldName = $row->oi_archive_name;
850 -
851 - $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) );
852 -
853 - // We'll need to purge this URL from caches...
854 - $urlArr[] = $this->getArchiveUrl( $oldName );
855 - }
856 - $dbw->freeResult( $result );
857 -
858 - // And the current version...
859 - $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) );
860 -
861 - $dbw->immediateCommit();
862 - } catch( MWException $e ) {
863 - wfDebug( __METHOD__.": db error, rolling back file transactions\n" );
864 - $transaction->rollback();
865 - FileStore::unlock();
866 - throw $e;
 877+ if ( $status->ok ) {
 878+ // Update site_stats
 879+ $site_stats = $dbw->tableName( 'site_stats' );
 880+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
 881+ $this->purgeEverything();
867882 }
868883
869 - wfDebug( __METHOD__.": deleted db items, applying file transactions\n" );
870 - $transaction->commit();
871 - FileStore::unlock();
872 -
873 -
874 - // Update site_stats
875 - $site_stats = $dbw->tableName( 'site_stats' );
876 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
877 -
878 - $this->purgeEverything( $urlArr );
879 -
880 - return true;
 884+ $this->unlock();
 885+ return $status;
881886 }
882887
883 -
884888 /**
885889 * Delete an old version of the file.
886890 *
@@ -890,190 +894,22 @@
891895 *
892896 * @param $reason
893897 * @throws MWException or FSException on database or filestore failure
894 - * @return true on success, false on some kind of failure
 898+ * @return FileRepoStatus object.
895899 */
896 - function deleteOld( $archiveName, $reason, $suppress=false ) {
897 - $transaction = new FSTransaction();
898 - $urlArr = array();
899 -
900 - if( !FileStore::lock() ) {
901 - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
902 - return false;
 900+ function deleteOld( $archiveName, $reason ) {
 901+ $this->lock();
 902+ $batch = new LocalFileDeleteBatch( $this, $reason );
 903+ $batch->addOld( $archiveName );
 904+ $status = $batch->execute();
 905+ $this->unlock();
 906+ if ( $status->ok ) {
 907+ $this->purgeDescription();
 908+ $this->purgeHistory();
903909 }
904 -
905 - $transaction = new FSTransaction();
906 - try {
907 - $dbw = $this->repo->getMasterDB();
908 - $dbw->begin();
909 - $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) );
910 - $dbw->immediateCommit();
911 - } catch( MWException $e ) {
912 - wfDebug( __METHOD__.": db error, rolling back file transaction\n" );
913 - $transaction->rollback();
914 - FileStore::unlock();
915 - throw $e;
916 - }
917 -
918 - wfDebug( __METHOD__.": deleted db items, applying file transaction\n" );
919 - $transaction->commit();
920 - FileStore::unlock();
921 -
922 - $this->purgeDescription();
923 -
924 - // Squid purging
925 - global $wgUseSquid;
926 - if ( $wgUseSquid ) {
927 - $urlArr = array(
928 - $this->getArchiveUrl( $archiveName ),
929 - );
930 - wfPurgeSquidServers( $urlArr );
931 - }
932 - return true;
 910+ return $status;
933911 }
934912
935913 /**
936 - * Delete the current version of a file.
937 - * May throw a database error.
938 - * @return true on success, false on failure
939 - */
940 - private function prepareDeleteCurrent( $reason, $suppress=false ) {
941 - return $this->prepareDeleteVersion(
942 - $this->getFullPath(),
943 - $reason,
944 - 'image',
945 - array(
946 - 'fa_name' => 'img_name',
947 - 'fa_archive_name' => 'NULL',
948 - 'fa_size' => 'img_size',
949 - 'fa_width' => 'img_width',
950 - 'fa_height' => 'img_height',
951 - 'fa_metadata' => 'img_metadata',
952 - 'fa_bits' => 'img_bits',
953 - 'fa_media_type' => 'img_media_type',
954 - 'fa_major_mime' => 'img_major_mime',
955 - 'fa_minor_mime' => 'img_minor_mime',
956 - 'fa_description' => 'img_description',
957 - 'fa_user' => 'img_user',
958 - 'fa_user_text' => 'img_user_text',
959 - 'fa_timestamp' => 'img_timestamp' ),
960 - array( 'img_name' => $this->getName() ),
961 - $suppress,
962 - __METHOD__ );
963 - }
964 -
965 - /**
966 - * Delete a given older version of a file.
967 - * May throw a database error.
968 - * @return true on success, false on failure
969 - */
970 - private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) {
971 - $oldpath = $this->getArchivePath() .
972 - DIRECTORY_SEPARATOR . $archiveName;
973 - return $this->prepareDeleteVersion(
974 - $oldpath,
975 - $reason,
976 - 'oldimage',
977 - array(
978 - 'fa_name' => 'oi_name',
979 - 'fa_archive_name' => 'oi_archive_name',
980 - 'fa_size' => 'oi_size',
981 - 'fa_width' => 'oi_width',
982 - 'fa_height' => 'oi_height',
983 - 'fa_metadata' => 'NULL',
984 - 'fa_bits' => 'oi_bits',
985 - 'fa_media_type' => 'NULL',
986 - 'fa_major_mime' => 'NULL',
987 - 'fa_minor_mime' => 'NULL',
988 - 'fa_description' => 'oi_description',
989 - 'fa_user' => 'oi_user',
990 - 'fa_user_text' => 'oi_user_text',
991 - 'fa_timestamp' => 'oi_timestamp' ),
992 - array(
993 - 'oi_name' => $this->getName(),
994 - 'oi_archive_name' => $archiveName ),
995 - $suppress,
996 - __METHOD__ );
997 - }
998 -
999 - /**
1000 - * Do the dirty work of backing up an image row and its file
1001 - * (if $wgSaveDeletedFiles is on) and removing the originals.
1002 - *
1003 - * Must be run while the file store is locked and a database
1004 - * transaction is open to avoid race conditions.
1005 - *
1006 - * @return FSTransaction
1007 - */
1008 - private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) {
1009 - global $wgUser, $wgSaveDeletedFiles;
1010 -
1011 - // Dupe the file into the file store
1012 - if( file_exists( $path ) ) {
1013 - if( $wgSaveDeletedFiles ) {
1014 - $group = 'deleted';
1015 -
1016 - $store = FileStore::get( $group );
1017 - $key = FileStore::calculateKey( $path, $this->getExtension() );
1018 - $transaction = $store->insert( $key, $path,
1019 - FileStore::DELETE_ORIGINAL );
1020 - } else {
1021 - $group = null;
1022 - $key = null;
1023 - $transaction = FileStore::deleteFile( $path );
1024 - }
1025 - } else {
1026 - wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" );
1027 - $group = null;
1028 - $key = null;
1029 - $transaction = new FSTransaction(); // empty
1030 - }
1031 -
1032 - if( $transaction === false ) {
1033 - // Fail to restore?
1034 - wfDebug( __METHOD__.": import to file store failed, aborting\n" );
1035 - throw new MWException( "Could not archive and delete file $path" );
1036 - return false;
1037 - }
1038 -
1039 - // Bitfields to further supress the file content
1040 - // Note that currently, live files are stored elsewhere
1041 - // and cannot be partially deleted
1042 - $bitfield = 0;
1043 - if ( $suppress ) {
1044 - $bitfield |= self::DELETED_FILE;
1045 - $bitfield |= self::DELETED_COMMENT;
1046 - $bitfield |= self::DELETED_USER;
1047 - $bitfield |= self::DELETED_RESTRICTED;
1048 - }
1049 -
1050 - $dbw = $this->repo->getMasterDB();
1051 - $storageMap = array(
1052 - 'fa_storage_group' => $dbw->addQuotes( $group ),
1053 - 'fa_storage_key' => $dbw->addQuotes( $key ),
1054 -
1055 - 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ),
1056 - 'fa_deleted_timestamp' => $dbw->timestamp(),
1057 - 'fa_deleted_reason' => $dbw->addQuotes( $reason ),
1058 - 'fa_deleted' => $bitfield);
1059 - $allFields = array_merge( $storageMap, $fieldMap );
1060 -
1061 - try {
1062 - if( $wgSaveDeletedFiles ) {
1063 - $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
1064 - }
1065 - $dbw->delete( $table, $where, $fname );
1066 - } catch( DBQueryError $e ) {
1067 - // Something went horribly wrong!
1068 - // Leave the file as it was...
1069 - wfDebug( __METHOD__.": database error, rolling back file transaction\n" );
1070 - $transaction->rollback();
1071 - throw $e;
1072 - }
1073 -
1074 - return $transaction;
1075 - }
1076 -
1077 - /**
1078914 * Restore all or specified deleted revisions to the given file.
1079915 * Permissions and logging are left to the caller.
1080916 *
@@ -1081,202 +917,25 @@
1082918 *
1083919 * @param $versions set of record ids of deleted items to restore,
1084920 * or empty to restore all revisions.
1085 - * @return the number of file revisions restored if successful,
1086 - * or false on failure
 921+ * @return FileRepoStatus
1087922 */
1088 - function restore( $versions=array(), $Unsuppress=false ) {
1089 - global $wgUser;
1090 -
1091 - if( !FileStore::lock() ) {
1092 - wfDebug( __METHOD__." could not acquire filestore lock\n" );
1093 - return false;
 923+ function restore( $versions = array(), $unsuppress = false ) {
 924+ $batch = new LocalFileRestoreBatch( $this );
 925+ if ( !$versions ) {
 926+ $batch->addAll();
 927+ } else {
 928+ $batch->addIds( $versions );
1094929 }
1095 -
1096 - $transaction = new FSTransaction();
1097 - try {
1098 - $dbw = $this->repo->getMasterDB();
1099 - $dbw->begin();
1100 -
1101 - // Re-confirm whether this file presently exists;
1102 - // if no we'll need to create an file record for the
1103 - // first item we restore.
1104 - $exists = $dbw->selectField( 'image', '1',
1105 - array( 'img_name' => $this->getName() ),
1106 - __METHOD__ );
1107 -
1108 - // Fetch all or selected archived revisions for the file,
1109 - // sorted from the most recent to the oldest.
1110 - $conditions = array( 'fa_name' => $this->getName() );
1111 - if( $versions ) {
1112 - $conditions['fa_id'] = $versions;
1113 - }
1114 -
1115 - $result = $dbw->select( 'filearchive', '*',
1116 - $conditions,
1117 - __METHOD__,
1118 - array( 'ORDER BY' => 'fa_timestamp DESC' ) );
1119 -
1120 - if( $dbw->numRows( $result ) < count( $versions ) ) {
1121 - // There's some kind of conflict or confusion;
1122 - // we can't restore everything we were asked to.
1123 - wfDebug( __METHOD__.": couldn't find requested items\n" );
1124 - $dbw->rollback();
1125 - FileStore::unlock();
1126 - return false;
1127 - }
1128 -
1129 - if( $dbw->numRows( $result ) == 0 ) {
1130 - // Nothing to do.
1131 - wfDebug( __METHOD__.": nothing to do\n" );
1132 - $dbw->rollback();
1133 - FileStore::unlock();
1134 - return true;
1135 - }
1136 -
1137 - $revisions = 0;
1138 - while( $row = $dbw->fetchObject( $result ) ) {
1139 - if ( $Unsuppress ) {
1140 - // Currently, fa_deleted flags fall off upon restore, lets be careful about this
1141 - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
1142 - // Skip restoring file revisions that the user cannot restore
1143 - continue;
1144 - }
1145 - $revisions++;
1146 - $store = FileStore::get( $row->fa_storage_group );
1147 - if( !$store ) {
1148 - wfDebug( __METHOD__.": skipping row with no file.\n" );
1149 - continue;
1150 - }
1151 -
1152 - $restoredImage = new self( Title::makeTitle( NS_IMAGE, $row->fa_name ), $this->repo );
1153 -
1154 - if( $revisions == 1 && !$exists ) {
1155 - $destPath = $restoredImage->getFullPath();
1156 - $destDir = dirname( $destPath );
1157 - if ( !is_dir( $destDir ) ) {
1158 - wfMkdirParents( $destDir );
1159 - }
1160 -
1161 - // We may have to fill in data if this was originally
1162 - // an archived file revision.
1163 - if( is_null( $row->fa_metadata ) ) {
1164 - $tempFile = $store->filePath( $row->fa_storage_key );
1165 -
1166 - $magic = MimeMagic::singleton();
1167 - $mime = $magic->guessMimeType( $tempFile, true );
1168 - $media_type = $magic->getMediaType( $tempFile, $mime );
1169 - list( $major_mime, $minor_mime ) = self::splitMime( $mime );
1170 - $handler = MediaHandler::getHandler( $mime );
1171 - if ( $handler ) {
1172 - $metadata = $handler->getMetadata( false, $tempFile );
1173 - } else {
1174 - $metadata = '';
1175 - }
1176 - } else {
1177 - $metadata = $row->fa_metadata;
1178 - $major_mime = $row->fa_major_mime;
1179 - $minor_mime = $row->fa_minor_mime;
1180 - $media_type = $row->fa_media_type;
1181 - }
1182 -
1183 - $table = 'image';
1184 - $fields = array(
1185 - 'img_name' => $row->fa_name,
1186 - 'img_size' => $row->fa_size,
1187 - 'img_width' => $row->fa_width,
1188 - 'img_height' => $row->fa_height,
1189 - 'img_metadata' => $metadata,
1190 - 'img_bits' => $row->fa_bits,
1191 - 'img_media_type' => $media_type,
1192 - 'img_major_mime' => $major_mime,
1193 - 'img_minor_mime' => $minor_mime,
1194 - 'img_description' => $row->fa_description,
1195 - 'img_user' => $row->fa_user,
1196 - 'img_user_text' => $row->fa_user_text,
1197 - 'img_timestamp' => $row->fa_timestamp );
1198 - } else {
1199 - $archiveName = $row->fa_archive_name;
1200 - if( $archiveName == '' ) {
1201 - // This was originally a current version; we
1202 - // have to devise a new archive name for it.
1203 - // Format is <timestamp of archiving>!<name>
1204 - $archiveName =
1205 - wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) .
1206 - '!' . $row->fa_name;
1207 - }
1208 - $destDir = $restoredImage->getArchivePath();
1209 - if ( !is_dir( $destDir ) ) {
1210 - wfMkdirParents( $destDir );
1211 - }
1212 - $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName;
1213 -
1214 - $table = 'oldimage';
1215 - $fields = array(
1216 - 'oi_name' => $row->fa_name,
1217 - 'oi_archive_name' => $archiveName,
1218 - 'oi_size' => $row->fa_size,
1219 - 'oi_width' => $row->fa_width,
1220 - 'oi_height' => $row->fa_height,
1221 - 'oi_bits' => $row->fa_bits,
1222 - 'oi_description' => $row->fa_description,
1223 - 'oi_user' => $row->fa_user,
1224 - 'oi_user_text' => $row->fa_user_text,
1225 - 'oi_timestamp' => $row->fa_timestamp );
1226 - }
1227 -
1228 - $dbw->insert( $table, $fields, __METHOD__ );
1229 - // @todo this delete is not totally safe, potentially
1230 - $dbw->delete( 'filearchive',
1231 - array( 'fa_id' => $row->fa_id ),
1232 - __METHOD__ );
1233 -
1234 - // Check if any other stored revisions use this file;
1235 - // if so, we shouldn't remove the file from the deletion
1236 - // archives so they will still work.
1237 - $useCount = $dbw->selectField( 'filearchive',
1238 - 'COUNT(*)',
1239 - array(
1240 - 'fa_storage_group' => $row->fa_storage_group,
1241 - 'fa_storage_key' => $row->fa_storage_key ),
1242 - __METHOD__ );
1243 - if( $useCount == 0 ) {
1244 - wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
1245 - $flags = FileStore::DELETE_ORIGINAL;
1246 - } else {
1247 - $flags = 0;
1248 - }
1249 -
1250 - $transaction->add( $store->export( $row->fa_storage_key,
1251 - $destPath, $flags ) );
1252 - }
1253 -
1254 - $dbw->immediateCommit();
1255 - } catch( MWException $e ) {
1256 - wfDebug( __METHOD__." caught error, aborting\n" );
1257 - $transaction->rollback();
1258 - $dbw->rollback();
1259 - throw $e;
 930+ $status = $batch->execute();
 931+ if ( !$status->ok ) {
 932+ return $status;
1260933 }
1261934
1262 - $transaction->commit();
1263 - FileStore::unlock();
1264 -
1265 - if( $revisions > 0 ) {
1266 - if( !$exists ) {
1267 - wfDebug( __METHOD__." restored $revisions items, creating a new current\n" );
1268 -
1269 - // Update site_stats
1270 - $site_stats = $dbw->tableName( 'site_stats' );
1271 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1272 -
1273 - $this->purgeEverything();
1274 - } else {
1275 - wfDebug( __METHOD__." restored $revisions as archived versions\n" );
1276 - $this->purgeDescription();
1277 - }
1278 - }
1279 -
1280 - return $revisions;
 935+ $cleanupStatus = $batch->cleanup();
 936+ $cleanupStatus->successCount = 0;
 937+ $cleanupStatus->failCount = 0;
 938+ $status->merge( $cleanupStatus );
 939+ return $status;
1281940 }
1282941
1283942 /** isMultipage inherited */
@@ -1310,8 +969,52 @@
1311970 $this->load();
1312971 return $this->timestamp;
1313972 }
 973+
 974+ function getSha1() {
 975+ $this->load();
 976+ return $this->sha1;
 977+ }
 978+
 979+ /**
 980+ * Start a transaction and lock the image for update
 981+ * Increments a reference counter if the lock is already held
 982+ * @return boolean True if the image exists, false otherwise
 983+ */
 984+ function lock() {
 985+ $dbw = $this->repo->getMasterDB();
 986+ if ( !$this->locked ) {
 987+ $dbw->begin();
 988+ $this->locked++;
 989+ }
 990+ return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
 991+ }
 992+
 993+ /**
 994+ * Decrement the lock reference count. If the reference count is reduced to zero, commits
 995+ * the transaction and thereby releases the image lock.
 996+ */
 997+ function unlock() {
 998+ if ( $this->locked ) {
 999+ --$this->locked;
 1000+ if ( !$this->locked ) {
 1001+ $dbw = $this->repo->getMasterDB();
 1002+ $dbw->commit();
 1003+ }
 1004+ }
 1005+ }
 1006+
 1007+ /**
 1008+ * Roll back the DB transaction and mark the image unlocked
 1009+ */
 1010+ function unlockAndRollback() {
 1011+ $this->locked = false;
 1012+ $dbw = $this->repo->getMasterDB();
 1013+ $dbw->rollback();
 1014+ }
13141015 } // LocalFile class
13151016
 1017+#------------------------------------------------------------------------------
 1018+
13161019 /**
13171020 * Backwards compatibility class
13181021 */
@@ -1379,12 +1082,467 @@
13801083 }
13811084 }
13821085
 1086+#------------------------------------------------------------------------------
 1087+
13831088 /**
1384 - * Aliases for backwards compatibility with 1.6
 1089+ * Helper class for file deletion
13851090 */
1386 -define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
1387 -define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
1388 -define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
1389 -define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
 1091+class LocalFileDeleteBatch {
 1092+ var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
 1093+ var $status;
13901094
 1095+ function __construct( File $file, $reason = '' ) {
 1096+ $this->file = $file;
 1097+ $this->reason = $reason;
 1098+ $this->status = $file->repo->newGood();
 1099+ }
13911100
 1101+ function addCurrent() {
 1102+ $this->srcRels['.'] = $this->file->getRel();
 1103+ }
 1104+
 1105+ function addOld( $oldName ) {
 1106+ $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
 1107+ $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
 1108+ }
 1109+
 1110+ function getOldRels() {
 1111+ if ( !isset( $this->srcRels['.'] ) ) {
 1112+ $oldRels =& $this->srcRels;
 1113+ $deleteCurrent = false;
 1114+ } else {
 1115+ $oldRels = $this->srcRels;
 1116+ unset( $oldRels['.'] );
 1117+ $deleteCurrent = true;
 1118+ }
 1119+ return array( $oldRels, $deleteCurrent );
 1120+ }
 1121+
 1122+ /*protected*/ function getHashes() {
 1123+ $hashes = array();
 1124+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1125+ if ( $deleteCurrent ) {
 1126+ $hashes['.'] = $this->file->getSha1();
 1127+ }
 1128+ if ( count( $oldRels ) ) {
 1129+ $dbw = $this->file->repo->getMasterDB();
 1130+ $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ),
 1131+ 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
 1132+ __METHOD__ );
 1133+ while ( $row = $dbw->fetchObject( $res ) ) {
 1134+ if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
 1135+ // Get the hash from the file
 1136+ $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
 1137+ $props = $this->file->repo->getFileProps( $oldUrl );
 1138+ if ( $props['fileExists'] ) {
 1139+ // Upgrade the oldimage row
 1140+ $dbw->update( 'oldimage',
 1141+ array( 'oi_sha1' => $props['sha1'] ),
 1142+ array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
 1143+ __METHOD__ );
 1144+ $hashes[$row->oi_archive_name] = $props['sha1'];
 1145+ } else {
 1146+ $hashes[$row->oi_archive_name] = false;
 1147+ }
 1148+ } else {
 1149+ $hashes[$row->oi_archive_name] = $row->oi_sha1;
 1150+ }
 1151+ }
 1152+ }
 1153+ $missing = array_diff_key( $this->srcRels, $hashes );
 1154+ foreach ( $missing as $name => $rel ) {
 1155+ $this->status->error( 'filedelete-old-unregistered', $name );
 1156+ }
 1157+ foreach ( $hashes as $name => $hash ) {
 1158+ if ( !$hash ) {
 1159+ $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
 1160+ unset( $hashes[$name] );
 1161+ }
 1162+ }
 1163+
 1164+ return $hashes;
 1165+ }
 1166+
 1167+ function doDBInserts() {
 1168+ global $wgUser;
 1169+ $dbw = $this->file->repo->getMasterDB();
 1170+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
 1171+ $encUserId = $dbw->addQuotes( $wgUser->getId() );
 1172+ $encReason = $dbw->addQuotes( $this->reason );
 1173+ $encGroup = $dbw->addQuotes( 'deleted' );
 1174+ $ext = $this->file->getExtension();
 1175+ $dotExt = $ext === '' ? '' : ".$ext";
 1176+ $encExt = $dbw->addQuotes( $dotExt );
 1177+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1178+
 1179+ if ( $deleteCurrent ) {
 1180+ $where = array( 'img_name' => $this->file->getName() );
 1181+ $dbw->insertSelect( 'filearchive', 'image',
 1182+ array(
 1183+ 'fa_storage_group' => $encGroup,
 1184+ 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))",
 1185+
 1186+ 'fa_deleted_user' => $encUserId,
 1187+ 'fa_deleted_timestamp' => $encTimestamp,
 1188+ 'fa_deleted_reason' => $encReason,
 1189+ 'fa_deleted' => 0,
 1190+
 1191+ 'fa_name' => 'img_name',
 1192+ 'fa_archive_name' => 'NULL',
 1193+ 'fa_size' => 'img_size',
 1194+ 'fa_width' => 'img_width',
 1195+ 'fa_height' => 'img_height',
 1196+ 'fa_metadata' => 'img_metadata',
 1197+ 'fa_bits' => 'img_bits',
 1198+ 'fa_media_type' => 'img_media_type',
 1199+ 'fa_major_mime' => 'img_major_mime',
 1200+ 'fa_minor_mime' => 'img_minor_mime',
 1201+ 'fa_description' => 'img_description',
 1202+ 'fa_user' => 'img_user',
 1203+ 'fa_user_text' => 'img_user_text',
 1204+ 'fa_timestamp' => 'img_timestamp'
 1205+ ), $where, __METHOD__ );
 1206+ }
 1207+
 1208+ if ( count( $oldRels ) ) {
 1209+ $where = array(
 1210+ 'oi_name' => $this->file->getName(),
 1211+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
 1212+
 1213+ $dbw->insertSelect( 'filearchive', 'oldimage',
 1214+ array(
 1215+ 'fa_storage_group' => $encGroup,
 1216+ 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))",
 1217+
 1218+ 'fa_deleted_user' => $encUserId,
 1219+ 'fa_deleted_timestamp' => $encTimestamp,
 1220+ 'fa_deleted_reason' => $encReason,
 1221+ 'fa_deleted' => 0,
 1222+
 1223+ 'fa_name' => 'oi_name',
 1224+ 'fa_archive_name' => 'oi_archive_name',
 1225+ 'fa_size' => 'oi_size',
 1226+ 'fa_width' => 'oi_width',
 1227+ 'fa_height' => 'oi_height',
 1228+ 'fa_metadata' => 'oi_metadata',
 1229+ 'fa_bits' => 'oi_bits',
 1230+ 'fa_media_type' => 'oi_media_type',
 1231+ 'fa_major_mime' => 'oi_major_mime',
 1232+ 'fa_minor_mime' => 'oi_minor_mime',
 1233+ 'fa_description' => 'oi_description',
 1234+ 'fa_user' => 'oi_user',
 1235+ 'fa_user_text' => 'oi_user_text',
 1236+ 'fa_timestamp' => 'oi_timestamp'
 1237+ ), $where, __METHOD__ );
 1238+ }
 1239+ }
 1240+
 1241+ function doDBDeletes() {
 1242+ $dbw = $this->file->repo->getMasterDB();
 1243+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1244+ if ( $deleteCurrent ) {
 1245+ $where = array( 'img_name' => $this->file->getName() );
 1246+ $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
 1247+ }
 1248+ if ( count( $oldRels ) ) {
 1249+ $dbw->delete( 'oldimage',
 1250+ array(
 1251+ 'oi_name' => $this->file->getName(),
 1252+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
 1253+ ), __METHOD__ );
 1254+ }
 1255+ }
 1256+
 1257+ /**
 1258+ * Run the transaction
 1259+ */
 1260+ function execute() {
 1261+ global $wgUser, $wgUseSquid;
 1262+ wfProfileIn( __METHOD__ );
 1263+
 1264+ $this->file->lock();
 1265+
 1266+ // Prepare deletion batch
 1267+ $hashes = $this->getHashes();
 1268+ $this->deletionBatch = array();
 1269+ $ext = $this->file->getExtension();
 1270+ $dotExt = $ext === '' ? '' : ".$ext";
 1271+ foreach ( $this->srcRels as $name => $srcRel ) {
 1272+ // Skip files that have no hash (missing source)
 1273+ if ( isset( $hashes[$name] ) ) {
 1274+ $hash = $hashes[$name];
 1275+ $key = $hash . $dotExt;
 1276+ $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
 1277+ $this->deletionBatch[$name] = array( $srcRel, $dstRel );
 1278+ }
 1279+ }
 1280+
 1281+ // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
 1282+ // We acquire this lock by running the inserts now, before the file operations.
 1283+ //
 1284+ // This potentially has poor lock contention characteristics -- an alternative
 1285+ // scheme would be to insert stub filearchive entries with no fa_name and commit
 1286+ // them in a separate transaction, then run the file ops, then update the fa_name fields.
 1287+ $this->doDBInserts();
 1288+
 1289+ // Execute the file deletion batch
 1290+ $status = $this->file->repo->deleteBatch( $this->deletionBatch );
 1291+ if ( !$status->isGood() ) {
 1292+ $this->status->merge( $status );
 1293+ }
 1294+
 1295+ if ( !$this->status->ok ) {
 1296+ // Critical file deletion error
 1297+ // Roll back inserts, release lock and abort
 1298+ // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
 1299+ $this->file->unlockAndRollback();
 1300+ return $this->status;
 1301+ }
 1302+
 1303+ // Purge squid
 1304+ if ( $wgUseSquid ) {
 1305+ $urls = array();
 1306+ foreach ( $this->srcRels as $srcRel ) {
 1307+ $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
 1308+ $urls[] = $this->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
 1309+ }
 1310+ SquidUpdate::purge( $urls );
 1311+ }
 1312+
 1313+ // Delete image/oldimage rows
 1314+ $this->doDBDeletes();
 1315+
 1316+ // Commit and return
 1317+ $this->file->unlock();
 1318+ wfProfileOut( __METHOD__ );
 1319+ return $this->status;
 1320+ }
 1321+}
 1322+
 1323+#------------------------------------------------------------------------------
 1324+
 1325+/**
 1326+ * Helper class for file undeletion
 1327+ */
 1328+class LocalFileRestoreBatch {
 1329+ var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
 1330+
 1331+ function __construct( File $file ) {
 1332+ $this->file = $file;
 1333+ $this->cleanupBatch = $this->ids = array();
 1334+ $this->ids = array();
 1335+ }
 1336+
 1337+ /**
 1338+ * Add a file by ID
 1339+ */
 1340+ function addId( $fa_id ) {
 1341+ $this->ids[] = $fa_id;
 1342+ }
 1343+
 1344+ /**
 1345+ * Add a whole lot of files by ID
 1346+ */
 1347+ function addIds( $ids ) {
 1348+ $this->ids = array_merge( $this->ids, $ids );
 1349+ }
 1350+
 1351+ /**
 1352+ * Add all revisions of the file
 1353+ */
 1354+ function addAll() {
 1355+ $this->all = true;
 1356+ }
 1357+
 1358+ /**
 1359+ * Run the transaction, except the cleanup batch.
 1360+ * The cleanup batch should be run in a separate transaction, because it locks different
 1361+ * rows and there's no need to keep the image row locked while it's acquiring those locks
 1362+ * The caller may have its own transaction open.
 1363+ * So we save the batch and let the caller call cleanup()
 1364+ */
 1365+ function execute() {
 1366+ global $wgUser, $wgLang;
 1367+ if ( !$this->all && !$this->ids ) {
 1368+ // Do nothing
 1369+ return $this->file->repo->newGood();
 1370+ }
 1371+
 1372+ $exists = $this->file->lock();
 1373+ $dbw = $this->file->repo->getMasterDB();
 1374+ $status = $this->file->repo->newGood();
 1375+
 1376+ // Fetch all or selected archived revisions for the file,
 1377+ // sorted from the most recent to the oldest.
 1378+ $conditions = array( 'fa_name' => $this->file->getName() );
 1379+ if( !$this->all ) {
 1380+ $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
 1381+ }
 1382+
 1383+ $result = $dbw->select( 'filearchive', '*',
 1384+ $conditions,
 1385+ __METHOD__,
 1386+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
 1387+
 1388+ $idsPresent = array();
 1389+ $storeBatch = array();
 1390+ $insertBatch = array();
 1391+ $insertCurrent = false;
 1392+ $deleteIds = array();
 1393+ $first = true;
 1394+ $archiveNames = array();
 1395+ while( $row = $dbw->fetchObject( $result ) ) {
 1396+ $idsPresent[] = $row->fa_id;
 1397+ if ( $this->unsuppress ) {
 1398+ // Currently, fa_deleted flags fall off upon restore, lets be careful about this
 1399+ } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
 1400+ // Skip restoring file revisions that the user cannot restore
 1401+ continue;
 1402+ }
 1403+ if ( $row->fa_name != $this->file->getName() ) {
 1404+ $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
 1405+ $status->failCount++;
 1406+ continue;
 1407+ }
 1408+ if ( $row->fa_storage_key == '' ) {
 1409+ // Revision was missing pre-deletion
 1410+ $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
 1411+ $status->failCount++;
 1412+ continue;
 1413+ }
 1414+
 1415+ $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
 1416+ $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
 1417+
 1418+ $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
 1419+ # Fix leading zero
 1420+ if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
 1421+ $sha1 = substr( $sha1, 1 );
 1422+ }
 1423+
 1424+ if ( $first && !$exists ) {
 1425+ // This revision will be published as the new current version
 1426+ $destRel = $this->file->getRel();
 1427+ $info = $this->file->repo->getFileProps( $deletedUrl );
 1428+ $insertCurrent = array(
 1429+ 'img_name' => $row->fa_name,
 1430+ 'img_size' => $row->fa_size,
 1431+ 'img_width' => $row->fa_width,
 1432+ 'img_height' => $row->fa_height,
 1433+ 'img_metadata' => $row->fa_metadata,
 1434+ 'img_bits' => $row->fa_bits,
 1435+ 'img_media_type' => $row->fa_media_type,
 1436+ 'img_major_mime' => $row->fa_major_mime,
 1437+ 'img_minor_mime' => $row->fa_minor_mime,
 1438+ 'img_description' => $row->fa_description,
 1439+ 'img_user' => $row->fa_user,
 1440+ 'img_user_text' => $row->fa_user_text,
 1441+ 'img_timestamp' => $row->fa_timestamp,
 1442+ 'img_sha1' => $sha1);
 1443+ } else {
 1444+ $archiveName = $row->fa_archive_name;
 1445+ if( $archiveName == '' ) {
 1446+ // This was originally a current version; we
 1447+ // have to devise a new archive name for it.
 1448+ // Format is <timestamp of archiving>!<name>
 1449+ $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
 1450+ do {
 1451+ $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
 1452+ $timestamp++;
 1453+ } while ( isset( $archiveNames[$archiveName] ) );
 1454+ }
 1455+ $archiveNames[$archiveName] = true;
 1456+ $destRel = $this->file->getArchiveRel( $archiveName );
 1457+ $insertBatch[] = array(
 1458+ 'oi_name' => $row->fa_name,
 1459+ 'oi_archive_name' => $archiveName,
 1460+ 'oi_size' => $row->fa_size,
 1461+ 'oi_width' => $row->fa_width,
 1462+ 'oi_height' => $row->fa_height,
 1463+ 'oi_bits' => $row->fa_bits,
 1464+ 'oi_description' => $row->fa_description,
 1465+ 'oi_user' => $row->fa_user,
 1466+ 'oi_user_text' => $row->fa_user_text,
 1467+ 'oi_timestamp' => $row->fa_timestamp,
 1468+ 'oi_metadata' => $row->fa_metadata,
 1469+ 'oi_media_type' => $row->fa_media_type,
 1470+ 'oi_major_mime' => $row->fa_major_mime,
 1471+ 'oi_minor_mime' => $row->fa_minor_mime,
 1472+ 'oi_deleted' => $row->fa_deleted,
 1473+ 'oi_sha1' => $sha1 );
 1474+ }
 1475+
 1476+ $deleteIds[] = $row->fa_id;
 1477+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
 1478+ $this->cleanupBatch[] = $row->fa_storage_key;
 1479+ $first = false;
 1480+ }
 1481+ unset( $result );
 1482+
 1483+ // Add a warning to the status object for missing IDs
 1484+ $missingIds = array_diff( $this->ids, $idsPresent );
 1485+ foreach ( $missingIds as $id ) {
 1486+ $status->error( 'undelete-missing-filearchive', $id );
 1487+ }
 1488+
 1489+ // Run the store batch
 1490+ // Use the OVERWRITE_SAME flag to smooth over a common error
 1491+ $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
 1492+ $status->merge( $storeStatus );
 1493+
 1494+ if ( !$status->ok ) {
 1495+ // Store batch returned a critical error -- this usually means nothing was stored
 1496+ // Stop now and return an error
 1497+ $this->file->unlock();
 1498+ return $status;
 1499+ }
 1500+
 1501+ // Run the DB updates
 1502+ // Because we have locked the image row, key conflicts should be rare.
 1503+ // If they do occur, we can roll back the transaction at this time with
 1504+ // no data loss, but leaving unregistered files scattered throughout the
 1505+ // public zone.
 1506+ // This is not ideal, which is why it's important to lock the image row.
 1507+ if ( $insertCurrent ) {
 1508+ $dbw->insert( 'image', $insertCurrent, __METHOD__ );
 1509+ }
 1510+ if ( $insertBatch ) {
 1511+ $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
 1512+ }
 1513+ if ( $deleteIds ) {
 1514+ $dbw->delete( 'filearchive',
 1515+ array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
 1516+ __METHOD__ );
 1517+ }
 1518+
 1519+ if( $status->successCount > 0 ) {
 1520+ if( !$exists ) {
 1521+ wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" );
 1522+
 1523+ // Update site_stats
 1524+ $site_stats = $dbw->tableName( 'site_stats' );
 1525+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
 1526+
 1527+ $this->file->purgeEverything();
 1528+ } else {
 1529+ wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" );
 1530+ $this->file->purgeDescription();
 1531+ $this->file->purgeHistory();
 1532+ }
 1533+ }
 1534+ $this->file->unlock();
 1535+ return $status;
 1536+ }
 1537+
 1538+ /**
 1539+ * Delete unused files in the deleted zone.
 1540+ * This should be called from outside the transaction in which execute() was called.
 1541+ */
 1542+ function cleanup() {
 1543+ if ( !$this->cleanupBatch ) {
 1544+ return $this->file->repo->newGood();
 1545+ }
 1546+ $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
 1547+ return $status;
 1548+ }
 1549+}
Index: trunk/phase3/includes/filerepo/FSRepo.php
@@ -6,9 +6,10 @@
77 */
88
99 class FSRepo extends FileRepo {
10 - var $directory, $url, $hashLevels;
 10+ var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
1111 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
1212 var $oldFileFactory = false;
 13+ var $pathDisclosureProtection = 'simple';
1314
1415 function __construct( $info ) {
1516 parent::__construct( $info );
@@ -16,7 +17,12 @@
1718 // Required settings
1819 $this->directory = $info['directory'];
1920 $this->url = $info['url'];
20 - $this->hashLevels = $info['hashLevels'];
 21+
 22+ // Optional settings
 23+ $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
 24+ $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
 25+ $info['deletedHashLevels'] : $this->hashLevels;
 26+ $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
2127 }
2228
2329 /**
@@ -50,7 +56,7 @@
5157 case 'temp':
5258 return "{$this->directory}/temp";
5359 case 'deleted':
54 - return $GLOBALS['wgFileStore']['deleted']['directory'];
 60+ return $this->deletedDir;
5561 default:
5662 return false;
5763 }
@@ -66,7 +72,7 @@
6773 case 'temp':
6874 return "{$this->url}/temp";
6975 case 'deleted':
70 - return $GLOBALS['wgFileStore']['deleted']['url'];
 76+ return false; // no public URL
7177 default:
7278 return false;
7379 }
@@ -109,47 +115,101 @@
110116 }
111117
112118 /**
113 - * Store a file to a given destination.
 119+ * Store a batch of files
 120+ *
 121+ * @param array $triplets (src,zone,dest) triplets as per store()
 122+ * @param integer $flags Bitwise combination of the following flags:
 123+ * self::DELETE_SOURCE Delete the source file after upload
 124+ * self::OVERWRITE Overwrite an existing destination file instead of failing
 125+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
 126+ * same contents as the source
114127 */
115 - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
 128+ function storeBatch( $triplets, $flags = 0 ) {
116129 if ( !is_writable( $this->directory ) ) {
117 - return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
 130+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
118131 }
119 - $root = $this->getZonePath( $dstZone );
120 - if ( !$root ) {
121 - throw new MWException( "Invalid zone: $dstZone" );
 132+ $status = $this->newGood();
 133+ foreach ( $triplets as $i => $triplet ) {
 134+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 135+
 136+ $root = $this->getZonePath( $dstZone );
 137+ if ( !$root ) {
 138+ throw new MWException( "Invalid zone: $dstZone" );
 139+ }
 140+ if ( !$this->validateFilename( $dstRel ) ) {
 141+ throw new MWException( 'Validation error in $dstRel' );
 142+ }
 143+ $dstPath = "$root/$dstRel";
 144+ $dstDir = dirname( $dstPath );
 145+
 146+ if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
 147+ return $this->newFatal( 'directorycreateerror', $dstDir );
 148+ }
 149+
 150+ if ( self::isVirtualUrl( $srcPath ) ) {
 151+ $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
 152+ }
 153+ if ( !is_file( $srcPath ) ) {
 154+ // Make a list of files that don't exist for return to the caller
 155+ $status->fatal( 'filenotfound', $srcPath );
 156+ continue;
 157+ }
 158+ if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) {
 159+ if ( $flags & self::OVERWRITE_SAME ) {
 160+ $hashSource = sha1_file( $srcPath );
 161+ $hashDest = sha1_file( $dstPath );
 162+ if ( $hashSource != $hashDest ) {
 163+ $status->fatal( 'fileexists', $dstPath );
 164+ }
 165+ } else {
 166+ $status->fatal( 'fileexists', $dstPath );
 167+ }
 168+ }
122169 }
123 - $dstPath = "$root/$dstRel";
124170
125 - if ( !is_dir( dirname( $dstPath ) ) ) {
126 - wfMkdirParents( dirname( $dstPath ) );
 171+ $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
 172+
 173+ // Abort now on failure
 174+ if ( !$status->ok ) {
 175+ return $status;
127176 }
128 -
129 - if ( self::isVirtualUrl( $srcPath ) ) {
130 - $srcPath = $this->resolveVirtualUrl( $srcPath );
131 - }
132177
133 - if ( $flags & self::DELETE_SOURCE ) {
134 - if ( !rename( $srcPath, $dstPath ) ) {
135 - return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
136 - wfEscapeWikiText( $dstPath ) );
 178+ foreach ( $triplets as $triplet ) {
 179+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 180+ $root = $this->getZonePath( $dstZone );
 181+ $dstPath = "$root/$dstRel";
 182+ $good = true;
 183+
 184+ if ( $flags & self::DELETE_SOURCE ) {
 185+ if ( $deleteDest ) {
 186+ unlink( $dstPath );
 187+ }
 188+ if ( !rename( $srcPath, $dstPath ) ) {
 189+ $status->error( 'filerenameerror', $srcPath, $dstPath );
 190+ $good = false;
 191+ }
 192+ } else {
 193+ if ( !copy( $srcPath, $dstPath ) ) {
 194+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 195+ $good = false;
 196+ }
137197 }
138 - } else {
139 - if ( !copy( $srcPath, $dstPath ) ) {
140 - return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
141 - wfEscapeWikiText( $dstPath ) );
 198+ if ( $good ) {
 199+ chmod( $dstPath, 0644 );
 200+ $status->successCount++;
 201+ } else {
 202+ $status->failCount++;
142203 }
143204 }
144 - chmod( $dstPath, 0644 );
145 - return true;
 205+ return $status;
146206 }
147207
148208 /**
149209 * Pick a random name in the temp zone and store a file to it.
150 - * Returns the URL, or a WikiError on failure.
151210 * @param string $originalName The base name of the file as specified
152211 * by the user. The file extension will be maintained.
153212 * @param string $srcPath The current location of the file.
 213+ * @return FileRepoStatus object with the URL in the value.
154214 */
155215 function storeTemp( $originalName, $srcPath ) {
156216 $date = gmdate( "YmdHis" );
@@ -158,11 +218,8 @@
159219 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
160220
161221 $result = $this->store( $srcPath, 'temp', $dstRel );
162 - if ( WikiError::isError( $result ) ) {
163 - return $result;
164 - } else {
165 - return $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
166 - }
 222+ $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
 223+ return $result;
167224 }
168225
169226 /**
@@ -183,82 +240,186 @@
184241 return $success;
185242 }
186243
187 -
188244 /**
189 - * Copy or move a file either from the local filesystem or from an mwrepo://
190 - * virtual URL, into this repository at the specified destination location.
191 - *
192 - * @param string $srcPath The source path or URL
193 - * @param string $dstRel The destination relative path
194 - * @param string $archiveRel The relative path where the existing file is to
195 - * be archived, if there is one. Relative to the public zone root.
 245+ * Publish a batch of files
 246+ * @param array $triplets (source,dest,archive) triplets as per publish()
196247 * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
197 - * that the source file should be deleted if possible
 248+ * that the source files should be deleted if possible
198249 */
199 - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 250+ function publishBatch( $triplets, $flags = 0 ) {
 251+ // Perform initial checks
200252 if ( !is_writable( $this->directory ) ) {
201 - return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
 253+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
202254 }
203 - if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
204 - $srcPath = $this->resolveVirtualUrl( $srcPath );
 255+ $status = $this->newGood( array() );
 256+ foreach ( $triplets as $i => $triplet ) {
 257+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
 258+
 259+ if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
 260+ $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath );
 261+ }
 262+ if ( !$this->validateFilename( $dstRel ) ) {
 263+ throw new MWException( 'Validation error in $dstRel' );
 264+ }
 265+ if ( !$this->validateFilename( $archiveRel ) ) {
 266+ throw new MWException( 'Validation error in $archiveRel' );
 267+ }
 268+ $dstPath = "{$this->directory}/$dstRel";
 269+ $archivePath = "{$this->directory}/$archiveRel";
 270+
 271+ $dstDir = dirname( $dstPath );
 272+ $archiveDir = dirname( $archivePath );
 273+ // Abort immediately on directory creation errors since they're likely to be repetitive
 274+ if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
 275+ return $this->newFatal( 'directorycreateerror', $dstDir );
 276+ }
 277+ if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) {
 278+ return $this->newFatal( 'directorycreateerror', $archiveDir );
 279+ }
 280+ if ( !is_file( $srcPath ) ) {
 281+ // Make a list of files that don't exist for return to the caller
 282+ $status->fatal( 'filenotfound', $srcPath );
 283+ }
205284 }
206 - if ( !$this->validateFilename( $dstRel ) ) {
207 - throw new MWException( 'Validation error in $dstRel' );
 285+
 286+ if ( !$status->ok ) {
 287+ return $status;
208288 }
209 - if ( !$this->validateFilename( $archiveRel ) ) {
210 - throw new MWException( 'Validation error in $archiveRel' );
211 - }
212 - $dstPath = "{$this->directory}/$dstRel";
213 - $archivePath = "{$this->directory}/$archiveRel";
214289
215 - $dstDir = dirname( $dstPath );
216 - if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
 290+ foreach ( $triplets as $i => $triplet ) {
 291+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
 292+ $dstPath = "{$this->directory}/$dstRel";
 293+ $archivePath = "{$this->directory}/$archiveRel";
217294
218 - // Check if the source is missing before we attempt to move the dest to archive
219 - if ( !is_file( $srcPath ) ) {
220 - return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) );
221 - }
 295+ // Archive destination file if it exists
 296+ if( is_file( $dstPath ) ) {
 297+ // Check if the archive file exists
 298+ // This is a sanity check to avoid data loss. In UNIX, the rename primitive
 299+ // unlinks the destination file if it exists. DB-based synchronisation in
 300+ // publishBatch's caller should prevent races. In Windows there's no
 301+ // problem because the rename primitive fails if the destination exists.
 302+ if ( is_file( $archivePath ) ) {
 303+ $success = false;
 304+ } else {
 305+ wfSuppressWarnings();
 306+ $success = rename( $dstPath, $archivePath );
 307+ wfRestoreWarnings();
 308+ }
222309
223 - if( is_file( $dstPath ) ) {
224 - $archiveDir = dirname( $archivePath );
225 - if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
 310+ if( !$success ) {
 311+ $status->error( 'filerenameerror',$dstPath, $archivePath );
 312+ $status->failCount++;
 313+ continue;
 314+ } else {
 315+ wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
 316+ }
 317+ $status->value[$i] = 'archived';
 318+ } else {
 319+ $status->value[$i] = 'new';
 320+ }
 321+
 322+ $good = true;
226323 wfSuppressWarnings();
227 - $success = rename( $dstPath, $archivePath );
 324+ if ( $flags & self::DELETE_SOURCE ) {
 325+ if ( !rename( $srcPath, $dstPath ) ) {
 326+ $status->error( 'filerenameerror', $srcPath, $dstPath );
 327+ $good = false;
 328+ }
 329+ } else {
 330+ if ( !copy( $srcPath, $dstPath ) ) {
 331+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 332+ $good = false;
 333+ }
 334+ }
228335 wfRestoreWarnings();
229336
230 - if( ! $success ) {
231 - return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
232 - wfEscapeWikiText( $archivePath ) );
 337+ if ( $good ) {
 338+ $status->successCount++;
 339+ wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
 340+ // Thread-safe override for umask
 341+ chmod( $dstPath, 0644 );
 342+ } else {
 343+ $status->failCount++;
233344 }
234 - else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
235 - $status = 'archived';
236345 }
237 - else {
238 - $status = 'new';
 346+ return $status;
 347+ }
 348+
 349+ /**
 350+ * Move a group of files to the deletion archive.
 351+ * If no valid deletion archive is configured, this may either delete the
 352+ * file or throw an exception, depending on the preference of the repository.
 353+ *
 354+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
 355+ * is a two-element array containing the source file path relative to the
 356+ * public root in the first element, and the archive file path relative
 357+ * to the deleted zone root in the second element.
 358+ * @return FileRepoStatus
 359+ */
 360+ function deleteBatch( $sourceDestPairs ) {
 361+ $status = $this->newGood();
 362+ if ( !$this->deletedDir ) {
 363+ throw new MWException( __METHOD__.': no valid deletion archive directory' );
239364 }
240365
241 - $error = false;
242 - wfSuppressWarnings();
243 - if ( $flags & self::DELETE_SOURCE ) {
244 - if ( !rename( $srcPath, $dstPath ) ) {
245 - $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
246 - wfEscapeWikiText( $dstPath ) );
 366+ /**
 367+ * Validate filenames and create archive directories
 368+ */
 369+ foreach ( $sourceDestPairs as $pair ) {
 370+ list( $srcRel, $archiveRel ) = $pair;
 371+ if ( !$this->validateFilename( $srcRel ) ) {
 372+ throw new MWException( __METHOD__.':Validation error in $srcRel' );
247373 }
248 - } else {
249 - if ( !copy( $srcPath, $dstPath ) ) {
250 - $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
251 - wfEscapeWikiText( $dstPath ) );
 374+ if ( !$this->validateFilename( $archiveRel ) ) {
 375+ throw new MWException( __METHOD__.':Validation error in $archiveRel' );
252376 }
 377+ $archivePath = "{$this->deletedDir}/$archiveRel";
 378+ $archiveDir = dirname( $archivePath );
 379+ if ( !wfMkdirParents( $archiveDir ) ) {
 380+ $status->fatal( 'directorycreateerror', $archiveDir );
 381+ continue;
 382+ }
 383+ // Check if the archive directory is writable
 384+ // This doesn't appear to work on NTFS
 385+ if ( !is_writable( $archiveDir ) ) {
 386+ $status->fatal( 'filedelete-archive-read-only', $archiveDir );
 387+ }
253388 }
254 - wfRestoreWarnings();
 389+ if ( !$status->ok ) {
 390+ // Abort early
 391+ return $status;
 392+ }
255393
256 - if( $error ) {
257 - return $error;
258 - } else {
259 - wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
 394+ /**
 395+ * Move the files
 396+ * We're now committed to returning an OK result, which will lead to
 397+ * the files being moved in the DB also.
 398+ */
 399+ foreach ( $sourceDestPairs as $pair ) {
 400+ list( $srcRel, $archiveRel ) = $pair;
 401+ $srcPath = "{$this->directory}/$srcRel";
 402+ $archivePath = "{$this->deletedDir}/$archiveRel";
 403+ $good = true;
 404+ if ( file_exists( $archivePath ) ) {
 405+ # A file with this content hash is already archived
 406+ if ( !@unlink( $srcPath ) ) {
 407+ $status->error( 'filedeleteerror', $srcPath );
 408+ $good = false;
 409+ }
 410+ } else{
 411+ if ( !@rename( $srcPath, $archivePath ) ) {
 412+ $status->error( 'filerenameerror', $srcPath, $archivePath );
 413+ $good = false;
 414+ } else {
 415+ chmod( $archivePath, 0644 );
 416+ }
 417+ }
 418+ if ( $good ) {
 419+ $status->successCount++;
 420+ } else {
 421+ $status->failCount++;
 422+ }
260423 }
261 -
262 - chmod( $dstPath, 0644 );
263424 return $status;
264425 }
265426
@@ -271,6 +432,18 @@
272433 }
273434
274435 /**
 436+ * Get a relative path for a deletion archive key,
 437+ * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
 438+ */
 439+ function getDeletedHashPath( $key ) {
 440+ $path = '';
 441+ for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
 442+ $path .= $key[$i] . '/';
 443+ }
 444+ return $path;
 445+ }
 446+
 447+ /**
275448 * Call a callback function for every file in the repository.
276449 * Uses the filesystem even in child classes.
277450 */
@@ -308,6 +481,39 @@
309482 $path = $this->resolveVirtualUrl( $virtualUrl );
310483 return File::getPropsFromPath( $path );
311484 }
 485+
 486+ /**
 487+ * Path disclosure protection functions
 488+ *
 489+ * Get a callback function to use for cleaning error message parameters
 490+ */
 491+ function getErrorCleanupFunction() {
 492+ switch ( $this->pathDisclosureProtection ) {
 493+ case 'simple':
 494+ $callback = array( $this, 'simpleClean' );
 495+ break;
 496+ default:
 497+ $callback = parent::getErrorCleanupFunction();
 498+ }
 499+ return $callback;
 500+ }
 501+
 502+ function simpleClean( $param ) {
 503+ if ( !isset( $this->simpleCleanPairs ) ) {
 504+ global $IP;
 505+ $this->simpleCleanPairs = array(
 506+ $this->directory => 'public',
 507+ "{$this->directory}/temp" => 'temp',
 508+ $IP => '$IP',
 509+ dirname( __FILE__ ) => '$IP/extensions/WebStore',
 510+ );
 511+ if ( $this->deletedDir ) {
 512+ $this->simpleCleanPairs[$this->deletedDir] = 'deleted';
 513+ }
 514+ }
 515+ return strtr( $param, $this->simpleCleanPairs );
 516+ }
 517+
312518 }
313519
314520
Index: trunk/phase3/includes/filerepo/File.php
@@ -593,11 +593,9 @@
594594
595595 /**
596596 * Purge metadata and all affected pages when the file is created,
597 - * deleted, or majorly updated. A set of additional URLs may be
598 - * passed to purge, such as specific file files which have changed.
599 - * @param $urlArray array
 597+ * deleted, or majorly updated.
600598 */
601 - function purgeEverything( $urlArr=array() ) {
 599+ function purgeEverything() {
602600 // Delete thumbnails and refresh file metadata cache
603601 $this->purgeCache();
604602 $this->purgeDescription();
@@ -656,9 +654,9 @@
657655 return $this->getHashPath() . rawurlencode( $this->getName() );
658656 }
659657
660 - /** Get the path of the archive directory, or a particular file if $suffix is specified */
661 - function getArchivePath( $suffix = false ) {
662 - $path = $this->repo->getZonePath('public') . '/archive/' . $this->getHashPath();
 658+ /** Get the relative path for an archive file */
 659+ function getArchiveRel( $suffix = false ) {
 660+ $path = 'archive/' . $this->getHashPath();
663661 if ( $suffix === false ) {
664662 $path = substr( $path, 0, -1 );
665663 } else {
@@ -667,15 +665,25 @@
668666 return $path;
669667 }
670668
671 - /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
672 - function getThumbPath( $suffix = false ) {
673 - $path = $this->repo->getZonePath('public') . '/thumb/' . $this->getRel();
 669+ /** Get relative path for a thumbnail file */
 670+ function getThumbRel( $suffix = false ) {
 671+ $path = 'thumb/' . $this->getRel();
674672 if ( $suffix !== false ) {
675673 $path .= '/' . $suffix;
676674 }
677675 return $path;
678676 }
679677
 678+ /** Get the path of the archive directory, or a particular file if $suffix is specified */
 679+ function getArchivePath( $suffix = false ) {
 680+ return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel();
 681+ }
 682+
 683+ /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
 684+ function getThumbPath( $suffix = false ) {
 685+ return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix );
 686+ }
 687+
680688 /** Get the URL of the archive directory, or a particular file if $suffix is specified */
681689 function getArchiveUrl( $suffix = false ) {
682690 $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath();
@@ -980,13 +988,20 @@
981989 /**
982990 * Get the 14-character timestamp of the file upload, or false if
983991 */
984 - function getTimestmap() {
 992+ function getTimestamp() {
985993 $path = $this->getPath();
986994 if ( !file_exists( $path ) ) {
987995 return false;
988996 }
989997 return wfTimestamp( filemtime( $path ) );
990998 }
 999+
 1000+ /**
 1001+ * Get the SHA-1 base 36 hash of the file
 1002+ */
 1003+ function getSha1() {
 1004+ return self::sha1Base36( $this->getPath() );
 1005+ }
9911006
9921007 /**
9931008 * Determine if the current user is allowed to view a particular
@@ -1031,12 +1046,14 @@
10321047 $gis = false;
10331048 $info['metadata'] = '';
10341049 }
 1050+ $info['sha1'] = self::sha1Base36( $path );
10351051
10361052 wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
10371053 } else {
10381054 $info['mime'] = NULL;
10391055 $info['media_type'] = MEDIATYPE_UNKNOWN;
10401056 $info['metadata'] = '';
 1057+ $info['sha1'] = '';
10411058 wfDebug(__METHOD__.": $path NOT FOUND!\n");
10421059 }
10431060 if( $gis ) {
@@ -1056,6 +1073,30 @@
10571074 wfProfileOut( __METHOD__ );
10581075 return $info;
10591076 }
 1077+
 1078+ /**
 1079+ * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
 1080+ * encoding, zero padded to 31 digits.
 1081+ *
 1082+ * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
 1083+ * fairly neatly.
 1084+ *
 1085+ * Returns false on failure
 1086+ */
 1087+ static function sha1Base36( $path ) {
 1088+ $hash = sha1_file( $path );
 1089+ if ( $hash === false ) {
 1090+ return false;
 1091+ } else {
 1092+ return wfBaseConvert( $hash, 16, 36, 31 );
 1093+ }
 1094+ }
10601095 }
 1096+/**
 1097+ * Aliases for backwards compatibility with 1.6
 1098+ */
 1099+define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
 1100+define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
 1101+define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
 1102+define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
10611103
1062 -
Index: trunk/phase3/includes/filerepo/LocalRepo.php
@@ -28,4 +28,33 @@
2929 function newFromArchiveName( $title, $archiveName ) {
3030 return OldLocalFile::newFromArchiveName( $title, $this, $archiveName );
3131 }
 32+
 33+ /**
 34+ * Delete files in the deleted directory if they are not referenced in the
 35+ * filearchive table. This needs to be done in the repo because it needs to
 36+ * interleave database locks with file operations, which is potentially a
 37+ * remote operation.
 38+ * @return FileRepoStatus
 39+ */
 40+ function cleanupDeletedBatch( $storageKeys ) {
 41+ $root = $this->getZonePath( 'deleted' );
 42+ $dbw = $this->getMasterDB();
 43+ $status = $this->newGood();
 44+ foreach ( $storageKeys as $key ) {
 45+ $hashPath = $this->getDeletedHashPath( $key );
 46+ $path = "$root/$hashPath$key";
 47+ $dbw->begin();
 48+ $inuse = $dbw->select( 'filearchive', '1',
 49+ array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
 50+ __METHOD__, array( 'FOR UPDATE' ) );
 51+ if ( !$inuse && !unlink( $path ) ) {
 52+ $status->error( 'undelete-cleanup-error', $path );
 53+ $status->failCount++;
 54+ } else {
 55+ $status->successCount++;
 56+ }
 57+ $dbw->commit();
 58+ }
 59+ return $status;
 60+ }
3261 }
Index: trunk/phase3/includes/filerepo/FileRepo.php
@@ -6,9 +6,12 @@
77 */
88 abstract class FileRepo {
99 const DELETE_SOURCE = 1;
 10+ const OVERWRITE = 2;
 11+ const OVERWRITE_SAME = 4;
1012
1113 var $thumbScriptUrl, $transformVia404;
1214 var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
 15+ var $pathDisclosureProtection = 'paranoid';
1316
1417 /**
1518 * Factory functions for creating new files
@@ -23,7 +26,7 @@
2427 // Optional settings
2528 $this->initialCapital = true; // by default
2629 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
27 - 'thumbScriptUrl', 'initialCapital' ) as $var )
 30+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var )
2831 {
2932 if ( isset( $info[$var] ) ) {
3033 $this->$var = $info[$var];
@@ -200,12 +203,37 @@
201204
202205 /**
203206 * Store a file to a given destination.
 207+ *
 208+ * @param string $srcPath Source path or virtual URL
 209+ * @param string $dstZone Destination zone
 210+ * @param string $dstRel Destination relative path
 211+ * @param integer $flags Bitwise combination of the following flags:
 212+ * self::DELETE_SOURCE Delete the source file after upload
 213+ * self::OVERWRITE Overwrite an existing destination file instead of failing
 214+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
 215+ * same contents as the source
 216+ * @return FileRepoStatus
204217 */
205 - abstract function store( $srcPath, $dstZone, $dstRel, $flags = 0 );
 218+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
 219+ $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
 220+ if ( $status->successCount == 0 ) {
 221+ $status->ok = false;
 222+ }
 223+ return $status;
 224+ }
206225
207226 /**
 227+ * Store a batch of files
 228+ *
 229+ * @param array $triplets (src,zone,dest) triplets as per store()
 230+ * @param integer $flags Flags as per store
 231+ */
 232+ abstract function storeBatch( $triplets, $flags = 0 );
 233+
 234+ /**
208235 * Pick a random name in the temp zone and store a file to it.
209 - * Returns the URL, or a WikiError on failure.
 236+ * Returns a FileRepoStatus object with the URL in the value.
 237+ *
210238 * @param string $originalName The base name of the file as specified
211239 * by the user. The file extension will be maintained.
212240 * @param string $srcPath The current location of the file.
@@ -226,6 +254,9 @@
227255 * Copy or move a file either from the local filesystem or from an mwrepo://
228256 * virtual URL, into this repository at the specified destination location.
229257 *
 258+ * Returns a FileRepoStatus object. On success, the value contains "new" or
 259+ * "archived", to indicate whether the file was new with that name.
 260+ *
230261 * @param string $srcPath The source path or URL
231262 * @param string $dstRel The destination relative path
232263 * @param string $archiveRel The relative path where the existing file is to
@@ -233,9 +264,59 @@
234265 * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
235266 * that the source file should be deleted if possible
236267 */
237 - abstract function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 );
 268+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 269+ $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
 270+ if ( $status->successCount == 0 ) {
 271+ $status->ok = false;
 272+ }
 273+ if ( isset( $status->value[0] ) ) {
 274+ $status->value = $status->value[0];
 275+ } else {
 276+ $status->value = false;
 277+ }
 278+ return $status;
 279+ }
238280
239281 /**
 282+ * Publish a batch of files
 283+ * @param array $triplets (source,dest,archive) triplets as per publish()
 284+ * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
 285+ * that the source files should be deleted if possible
 286+ */
 287+ abstract function publishBatch( $triplets, $flags = 0 );
 288+
 289+ /**
 290+ * Move a group of files to the deletion archive.
 291+ *
 292+ * If no valid deletion archive is configured, this may either delete the
 293+ * file or throw an exception, depending on the preference of the repository.
 294+ *
 295+ * The overwrite policy is determined by the repository -- currently FSRepo
 296+ * assumes a naming scheme in the deleted zone based on content hash, as
 297+ * opposed to the public zone which is assumed to be unique.
 298+ *
 299+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
 300+ * is a two-element array containing the source file path relative to the
 301+ * public root in the first element, and the archive file path relative
 302+ * to the deleted zone root in the second element.
 303+ * @return FileRepoStatus
 304+ */
 305+ abstract function deleteBatch( $sourceDestPairs );
 306+
 307+ /**
 308+ * Move a file to the deletion archive.
 309+ * If no valid deletion archive exists, this may either delete the file
 310+ * or throw an exception, depending on the preference of the repository
 311+ * @param mixed $srcRel Relative path for the file to be deleted
 312+ * @param mixed $archiveRel Relative path for the archive location.
 313+ * Relative to a private archive directory.
 314+ * @return WikiError object (wikitext-formatted), or true for success
 315+ */
 316+ function delete( $srcRel, $archiveRel ) {
 317+ return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
 318+ }
 319+
 320+ /**
240321 * Get properties of a file with a given virtual URL
241322 * The virtual URL must refer to this repo
242323 * Properties should ultimately be obtained via File::getPropsFromPath()
@@ -276,5 +357,48 @@
277358 return true;
278359 }
279360 }
 361+
 362+ /**#@+
 363+ * Path disclosure protection functions
 364+ */
 365+ function paranoidClean( $param ) { return '[hidden]'; }
 366+ function passThrough( $param ) { return $param; }
 367+
 368+ /**
 369+ * Get a callback function to use for cleaning error message parameters
 370+ */
 371+ function getErrorCleanupFunction() {
 372+ switch ( $this->pathDisclosureProtection ) {
 373+ case 'none':
 374+ $callback = array( $this, 'passThrough' );
 375+ break;
 376+ default: // 'paranoid'
 377+ $callback = array( $this, 'paranoidClean' );
 378+ }
 379+ return $callback;
 380+ }
 381+ /**#@-*/
 382+
 383+ /**
 384+ * Create a new fatal error
 385+ */
 386+ function newFatal( $message /*, parameters...*/ ) {
 387+ $params = func_get_args();
 388+ array_unshift( $params, $this );
 389+ return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
 390+ }
 391+
 392+ /**
 393+ * Create a new good result
 394+ */
 395+ function newGood( $value = null ) {
 396+ return FileRepoStatus::newGood( $this, $value );
 397+ }
 398+
 399+ /**
 400+ * Delete files in the deleted directory if they are not referenced in the filearchive table
 401+ * STUB
 402+ */
 403+ function cleanupDeletedBatch( $storageKeys ) {}
280404 }
281405
Index: trunk/phase3/includes/filerepo/ForeignDBRepo.php
@@ -46,6 +46,12 @@
4747 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
4848 throw new MWException( get_class($this) . ': write operations are not supported' );
4949 }
 50+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 51+ throw new MWException( get_class($this) . ': write operations are not supported' );
 52+ }
 53+ function deleteBatch( $fileMap ) {
 54+ throw new MWException( get_class($this) . ': write operations are not supported' );
 55+ }
5056 }
5157
5258
Index: trunk/phase3/includes/EditPage.php
@@ -939,6 +939,10 @@
940940 # Enabled article-related sidebar, toplinks, etc.
941941 $wgOut->setArticleRelated( true );
942942
 943+ if ( $this->formtype == 'preview' ) {
 944+ $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
 945+ }
 946+
943947 if ( $this->isConflict ) {
944948 $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() );
945949 $wgOut->setPageTitle( $s );
Index: trunk/phase3/includes/SpecialUpload.php
@@ -433,8 +433,8 @@
434434
435435 $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
436436 File::DELETE_SOURCE, $this->mFileProps );
437 - if ( WikiError::isError( $status ) ) {
438 - $this->showError( $status );
 437+ if ( !$status->isGood() ) {
 438+ $this->showError( $status->getWikiText() );
439439 } else {
440440 if ( $this->mWatchthis ) {
441441 global $wgUser;
@@ -592,12 +592,12 @@
593593 function saveTempUploadedFile( $saveName, $tempName ) {
594594 global $wgOut;
595595 $repo = RepoGroup::singleton()->getLocalRepo();
596 - $result = $repo->storeTemp( $saveName, $tempName );
597 - if ( WikiError::isError( $result ) ) {
598 - $this->showError( $result );
 596+ $status = $repo->storeTemp( $saveName, $tempName );
 597+ if ( !$status->isGood() ) {
 598+ $this->showError( $status->getWikiText() );
599599 return false;
600600 } else {
601 - return $result;
 601+ return $status->value;
602602 }
603603 }
604604
@@ -1354,15 +1354,15 @@
13551355 }
13561356
13571357 /**
1358 - * Display an error from a wikitext-formatted WikiError object
 1358+ * Display an error with a wikitext description
13591359 */
1360 - function showError( WikiError $error ) {
 1360+ function showError( $description ) {
13611361 global $wgOut;
13621362 $wgOut->setPageTitle( wfMsg( "internalerror" ) );
13631363 $wgOut->setRobotpolicy( "noindex,nofollow" );
13641364 $wgOut->setArticleRelated( false );
13651365 $wgOut->enableClientCache( false );
1366 - $wgOut->addWikiText( $error->getMessage() );
 1366+ $wgOut->addWikiText( $description );
13671367 }
13681368
13691369 /**
Index: trunk/phase3/includes/OutputPage.php
@@ -28,6 +28,7 @@
2929
3030 var $mNewSectionLink = false;
3131 var $mNoGallery = false;
 32+ var $mPageTitleActionText = '';
3233
3334 /**
3435 * Constructor
@@ -189,26 +190,13 @@
190191 }
191192 }
192193
 194+ function setPageTitleActionText( $text ) {
 195+ $this->mPageTitleActionText = $text;
 196+ }
 197+
193198 function getPageTitleActionText () {
194 - global $action;
195 - switch($action) {
196 - case 'edit':
197 - case 'delete':
198 - case 'protect':
199 - case 'unprotect':
200 - case 'watch':
201 - case 'unwatch':
202 - // Display title is already customized
203 - return '';
204 - case 'history':
205 - return wfMsg('history_short');
206 - case 'submit':
207 - // FIXME: bug 2735; not correct for special pages etc
208 - return wfMsg('preview');
209 - case 'info':
210 - return wfMsg('info_short');
211 - default:
212 - return '';
 199+ if ( isset( $this->mPageTitleActionText ) ) {
 200+ return $this->mPageTitleActionText;
213201 }
214202 }
215203
Index: trunk/phase3/includes/AutoLoader.php
@@ -258,11 +258,14 @@
259259 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
260260 'File' => 'includes/filerepo/File.php',
261261 'FileRepo' => 'includes/filerepo/FileRepo.php',
 262+ 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
262263 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
263264 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
264265 'FSRepo' => 'includes/filerepo/FSRepo.php',
265266 'Image' => 'includes/filerepo/LocalFile.php',
266267 'LocalFile' => 'includes/filerepo/LocalFile.php',
 268+ 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
 269+ 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
267270 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
268271 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
269272 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
Index: trunk/phase3/includes/SpecialUndelete.php
@@ -23,6 +23,7 @@
2424 */
2525 class PageArchive {
2626 protected $title;
 27+ var $fileStatus;
2728
2829 function __construct( $title ) {
2930 if( is_null( $title ) ) {
@@ -270,7 +271,8 @@
271272
272273 if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
273274 $img = wfLocalFile( $this->title );
274 - $filesRestored = $img->restore( $fileVersions );
 275+ $this->fileStatus = $img->restore( $fileVersions );
 276+ $filesRestored = $this->fileStatus->successCount;
275277 } else {
276278 $filesRestored = 0;
277279 }
@@ -280,7 +282,7 @@
281283 } else {
282284 $textRestored = 0;
283285 }
284 -
 286+
285287 // Touch the log!
286288 global $wgContLang;
287289 $log = new LogPage( 'delete' );
@@ -303,8 +305,12 @@
304306 if( trim( $comment ) != '' )
305307 $reason .= ": {$comment}";
306308 $log->addEntry( 'restore', $this->title, $reason );
307 -
308 - return true;
 309+
 310+ if ( $this->fileStatus && !$this->fileStatus->ok ) {
 311+ return false;
 312+ } else {
 313+ return true;
 314+ }
309315 }
310316
311317 /**
@@ -448,6 +454,7 @@
449455 return $restored;
450456 }
451457
 458+ function getFileStatus() { return $this->fileStatus; }
452459 }
453460
454461 /**
@@ -731,8 +738,13 @@
732739 $logViewer = new LogViewer(
733740 new LogReader(
734741 new FauxRequest(
735 - array( 'page' => $this->mTargetObj->getPrefixedText(),
736 - 'type' => 'delete' ) ) ) );
 742+ array(
 743+ 'page' => $this->mTargetObj->getPrefixedText(),
 744+ 'type' => 'delete'
 745+ )
 746+ )
 747+ ), LogViewer::NO_ACTION_LINK
 748+ );
737749 $logViewer->showList( $wgOut );
738750
739751 if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
@@ -836,15 +848,23 @@
837849 $this->mTargetTimestamp,
838850 $this->mComment,
839851 $this->mFileVersions );
840 -
 852+
841853 if( $ok ) {
842854 $skin = $wgUser->getSkin();
843855 $link = $skin->makeKnownLinkObj( $this->mTargetObj );
844856 $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
845 - return true;
 857+ } else {
 858+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
846859 }
 860+
 861+ // Show file deletion warnings and errors
 862+ $status = $archive->getFileStatus();
 863+ if ( $status && !$status->isGood() ) {
 864+ $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
 865+ }
 866+ } else {
 867+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
847868 }
848 - $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
849869 return false;
850870 }
851871 }
Index: trunk/phase3/includes/SpecialLog.php
@@ -241,19 +241,25 @@
242242 * @addtogroup SpecialPage
243243 */
244244 class LogViewer {
 245+ const NO_ACTION_LINK = 1;
 246+
245247 /**
246248 * @var LogReader $reader
247249 */
248250 var $reader;
249251 var $numResults = 0;
 252+ var $flags = 0;
250253
251254 /**
252255 * @param LogReader &$reader where to get our data from
 256+ * @param integer $flags Bitwise combination of flags:
 257+ * self::NO_ACTION_LINK Don't show restore/unblock/block links
253258 */
254 - function LogViewer( &$reader ) {
 259+ function LogViewer( &$reader, $flags = 0 ) {
255260 global $wgUser;
256261 $this->skin = $wgUser->getSkin();
257262 $this->reader =& $reader;
 263+ $this->flags = $flags;
258264 }
259265
260266 /**
@@ -363,41 +369,43 @@
364370 $paramArray = LogPage::extractParams( $s->log_params );
365371 $revert = '';
366372 // show revertmove link
367 - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
368 - $destTitle = Title::newFromText( $paramArray[0] );
369 - if ( $destTitle ) {
370 - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
371 - wfMsg( 'revertmove' ),
372 - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
373 - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
374 - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
375 - '&wpMovetalk=0' ) . ')';
 373+ if ( !( $this->flags & self::NO_ACTION_LINK ) ) {
 374+ if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
 375+ $destTitle = Title::newFromText( $paramArray[0] );
 376+ if ( $destTitle ) {
 377+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
 378+ wfMsg( 'revertmove' ),
 379+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
 380+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
 381+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
 382+ '&wpMovetalk=0' ) . ')';
 383+ }
 384+ // show undelete link
 385+ } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
 386+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
 387+ wfMsg( 'undeletebtn' ) ,
 388+ 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
 389+
 390+ // show unblock link
 391+ } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
 392+ $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
 393+ wfMsg( 'unblocklink' ),
 394+ 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
 395+ // show change protection link
 396+ } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
 397+ $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
 398+ // show user tool links for self created users
 399+ // TODO: The extension should be handling this, get it out of core!
 400+ } elseif ( $s->log_action == 'create2' ) {
 401+ if( isset( $paramArray[0] ) ) {
 402+ $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
 403+ } else {
 404+ # Fall back to a blue contributions link
 405+ $revert = $this->skin->userToolLinks( 1, $s->log_title );
 406+ }
 407+ # Suppress $comment from old entries, not needed and can contain incorrect links
 408+ $comment = '';
376409 }
377 - // show undelete link
378 - } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
379 - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
380 - wfMsg( 'undeletebtn' ) ,
381 - 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
382 -
383 - // show unblock link
384 - } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
385 - $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
386 - wfMsg( 'unblocklink' ),
387 - 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
388 - // show change protection link
389 - } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
390 - $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
391 - // show user tool links for self created users
392 - // TODO: The extension should be handling this, get it out of core!
393 - } elseif ( $s->log_action == 'create2' ) {
394 - if( isset( $paramArray[0] ) ) {
395 - $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
396 - } else {
397 - # Fall back to a blue contributions link
398 - $revert = $this->skin->userToolLinks( 1, $s->log_title );
399 - }
400 - # Suppress $comment from old entries, not needed and can contain incorrect links
401 - $comment = '';
402410 }
403411
404412 $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true );
Index: trunk/phase3/includes/DefaultSettings.php
@@ -164,16 +164,6 @@
165165 /**#@-*/
166166
167167 /**
168 - * By default deleted files are simply discarded; to save them and
169 - * make it possible to undelete images, create a directory which
170 - * is writable to the web server but is not exposed to the internet.
171 - *
172 - * Set $wgSaveDeletedFiles to true and set up the save path in
173 - * $wgFileStore['deleted']['directory'].
174 - */
175 -$wgSaveDeletedFiles = false;
176 -
177 -/**
178168 * New file storage paths; currently used only for deleted files.
179169 * Set it like this:
180170 *
@@ -181,7 +171,7 @@
182172 *
183173 */
184174 $wgFileStore = array();
185 -$wgFileStore['deleted']['directory'] = null; // Don't forget to set this.
 175+$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
186176 $wgFileStore['deleted']['url'] = null; // Private
187177 $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
188178
@@ -208,6 +198,10 @@
209199 * start with a capital letter. The current implementation may give incorrect
210200 * description page links when the local $wgCapitalLinks and initialCapital
211201 * are mismatched.
 202+ * pathDisclosureProtection
 203+ * May be 'paranoid' to remove all parameters from error messages, 'none' to
 204+ * leave the paths in unchanged, or 'simple' to replace paths with
 205+ * placeholders. Default for LocalRepo is 'simple'.
212206 *
213207 * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
214208 * for local repositories:
Index: trunk/phase3/includes/FileStore.php
@@ -219,7 +219,7 @@
220220 * Confirm that the given file key is valid.
221221 * Note that a valid key may refer to a file that does not exist.
222222 *
223 - * Key should consist of a 32-digit base-36 SHA-1 hash and
 223+ * Key should consist of a 31-digit base-36 SHA-1 hash and
224224 * an optional alphanumeric extension, all lowercase.
225225 * The whole must not exceed 64 characters.
226226 *
@@ -227,7 +227,7 @@
228228 * @return boolean
229229 */
230230 static function validKey( $key ) {
231 - return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key );
 231+ return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
232232 }
233233
234234
@@ -249,7 +249,7 @@
250250 return false;
251251 }
252252
253 - $base36 = wfBaseConvert( $hash, 16, 36, 32 );
 253+ $base36 = wfBaseConvert( $hash, 16, 36, 31 );
254254 if( $extension == '' ) {
255255 $key = $base36;
256256 } else {
Index: trunk/phase3/includes/PageHistory.php
@@ -62,6 +62,7 @@
6363 * Setup page variables.
6464 */
6565 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
 66+ $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
6667 $wgOut->setArticleFlag( false );
6768 $wgOut->setArticleRelated( true );
6869 $wgOut->setRobotpolicy( 'noindex,nofollow' );
Index: trunk/phase3/StartProfiler.php
@@ -1,22 +1,24 @@
22 <?php
33
4 -require_once( dirname(__FILE__).'/includes/ProfilerStub.php' );
 4+#require_once( './includes/ProfilerStub.php' );
55
66 /**
77 * To use a profiler, delete the line above and add something like this:
88 *
9 - * require_once( dirname(__FILE__).'/includes/Profiler.php' );
 9+ * require_once( './includes/Profiler.php' );
1010 * $wgProfiler = new Profiler;
1111 *
1212 * Or for a sampling profiler:
1313 * if ( !mt_rand( 0, 100 ) ) {
14 - * require_once( dirname(__FILE__).'/includes/Profiler.php' );
 14+ * require_once( './includes/Profiler.php' );
1515 * $wgProfiler = new Profiler;
1616 * } else {
17 - * require_once( dirname(__FILE__).'/includes/ProfilerStub.php' );
 17+ * require_once( './includes/ProfilerStub.php' );
1818 * }
1919 *
2020 * Configuration of the profiler output can be done in LocalSettings.php
2121 */
 22+require_once( dirname(__FILE__).'/includes/Profiler.php' );
 23+$wgProfiler = new Profiler;
2224
2325
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -761,10 +761,13 @@
762762 Please report this to an administrator, making note of the URL.',
763763 'readonly_lag' => 'The database has been automatically locked while the slave database servers catch up to the master',
764764 'internalerror' => 'Internal error',
 765+'internalerror_info' => 'Internal error: $1',
765766 'filecopyerror' => 'Could not copy file "$1" to "$2".',
766767 'filerenameerror' => 'Could not rename file "$1" to "$2".',
767768 'filedeleteerror' => 'Could not delete file "$1".',
 769+'directorycreateerror' => 'Could not create directory "$1".',
768770 'filenotfound' => 'Could not find file "$1".',
 771+'fileexists' => 'Unable to write to file "$1": file exists',
769772 'unexpected' => 'Unexpected value: "$1"="$2".',
770773 'formerror' => 'Error: could not submit form',
771774 'badarticleerror' => 'This action cannot be performed on this page.',
@@ -1885,6 +1888,13 @@
18861889 'undelete-search-prefix' => 'Show pages starting with:',
18871890 'undelete-search-submit' => 'Search',
18881891 'undelete-no-results' => 'No matching pages found in the deletion archive.',
 1892+'undelete-filename-mismatch' => 'Cannot undelete file revision with timestamp $1: filename mismatch',
 1893+'undelete-bad-store-key' => 'Cannot undelete file revision with timestamp $1: file was missing before deletion.',
 1894+'undelete-cleanup-error' => 'Error deleting unused archive file "$1".',
 1895+'undelete-missing-filearchive' => 'Unable to restore file archive ID $1 because it isn\'t in the database. ' .
 1896+ 'It may have already been undeleted.',
 1897+'undelete-error-short' => 'Error undeleting file: $1',
 1898+'undelete-error-long' => "Errors were encountered while undeleting the file:\n\n$1\n",
18891899
18901900 # Namespace form on various pages
18911901 'namespace' => 'Namespace:',
@@ -2362,6 +2372,12 @@
23632373
23642374 # Image deletion
23652375 'deletedrevision' => 'Deleted old revision $1.',
 2376+'filedeleteerror-short' => "Error deleting file: $1",
 2377+'filedeleteerror-long' => "Errors were encountered while deleting the file:\n\n$1\n",
 2378+'filedelete-missing' => 'The file "$1" cannot be deleted, because it doesn\'t exist.',
 2379+'filedelete-old-unregistered' => 'The specified file revision "$1" is not in the database.',
 2380+'filedelete-current-unregistered' => 'The specified file "$1" is not in the database.',
 2381+'filedelete-archive-read-only' => 'The archive directory "$1" is not writable by the webserver.',
23662382
23672383 # Browsing diffs
23682384 'previousdiff' => '← Previous diff',
Index: trunk/phase3/RELEASE-NOTES
@@ -26,6 +26,7 @@
2727 usergroups
2828 * $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites
2929 * $wgShowHostnames - Expose server host names through the API and HTML comments
 30+* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally
3031
3132 == New features since 1.10 ==
3233
@@ -318,8 +319,12 @@
319320 * (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0
320321 * Work around Safari bug with pages ending in ".gz" or ".tgz"
321322 * Removed obsolete maintenance/changeuser.sql script; use RenameUser extension
 323+* (bug 2735) "Preview" shown in title bar for action=submit on special pages
 324+* Removed "restore" links from the deletion log embedded in Special:Undelete
 325+* Improved error reporting and robustness for file delete/undelete.
 326+* Improved speed of file delete by storing the SHA-1 hash in image/oldimage
 327+* Fixed leading zero in base 36 SHA-1 hash
322328
323 -
324329 == API changes since 1.10 ==
325330
326331 Full API documentation is available at http://www.mediawiki.org/wiki/API

Follow-up revisions

RevisionCommit summaryAuthorDate
r24346Merged revisions 24302-24345 via svnmerge from...david23:41, 23 July 2007
r24424A message (fileexists) with the same name of another one was introduced in r2...rotem02:18, 29 July 2007
r24502Merged revisions 24415-24479 via svnmerge from...david22:31, 31 July 2007

Status & tagging log