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 |
1 | 10 | + native |
Index: trunk/phase3/maintenance/updaters.inc |
— | — | @@ -81,6 +81,7 @@ |
82 | 82 | array( 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ), |
83 | 83 | array( 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql'), |
84 | 84 | array( 'archive', 'ar_page', 'patch-archive-ar_page.sql'), |
| 85 | + array( 'image', 'img_sha1', 'patch-img_sha1.sql' ), |
85 | 86 | ); |
86 | 87 | |
87 | 88 | # For extensions only, should be populated via hooks |
Index: trunk/phase3/maintenance/rebuildImages.php |
— | — | @@ -173,8 +173,6 @@ |
174 | 174 | function addMissingImage( $filename, $fullpath ) { |
175 | 175 | $fname = 'ImageBuilder::addMissingImage'; |
176 | 176 | |
177 | | - $size = filesize( $fullpath ); |
178 | | - $info = $this->imageInfo( $fullpath ); |
179 | 177 | $timestamp = $this->dbw->timestamp( filemtime( $fullpath ) ); |
180 | 178 | |
181 | 179 | global $wgContLang; |
Index: trunk/phase3/maintenance/tables.sql |
— | — | @@ -686,14 +686,21 @@ |
687 | 687 | -- Time of the upload. |
688 | 688 | img_timestamp varbinary(14) NOT NULL default '', |
689 | 689 | |
| 690 | + -- SHA-1 content hash in base-36 |
| 691 | + img_sha1 varbinary(32) NOT NULL default '', |
| 692 | + |
690 | 693 | PRIMARY KEY img_name (img_name), |
691 | 694 | |
692 | 695 | INDEX img_usertext_timestamp (img_user_text,img_timestamp), |
693 | 696 | -- Used by Special:Imagelist for sort-by-size |
694 | 697 | INDEX img_size (img_size), |
695 | 698 | -- Used by Special:Newimages and Special:Imagelist |
696 | | - INDEX img_timestamp (img_timestamp) |
| 699 | + INDEX img_timestamp (img_timestamp), |
697 | 700 | |
| 701 | + -- For future use |
| 702 | + INDEX img_sha1 (img_sha1), |
| 703 | + |
| 704 | + |
698 | 705 | ) /*$wgDBTableOptions*/; |
699 | 706 | |
700 | 707 | -- |
— | — | @@ -724,11 +731,13 @@ |
725 | 732 | oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", |
726 | 733 | oi_minor_mime varbinary(32) NOT NULL default "unknown", |
727 | 734 | oi_deleted tinyint unsigned NOT NULL default '0', |
| 735 | + oi_sha1 varbinary(32) NOT NULL default '', |
728 | 736 | |
729 | 737 | INDEX oi_usertext_timestamp (oi_user_text,oi_timestamp), |
730 | 738 | INDEX oi_name_timestamp (oi_name,oi_timestamp), |
731 | 739 | -- 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) |
733 | 742 | |
734 | 743 | ) /*$wgDBTableOptions*/; |
735 | 744 | |
Index: trunk/phase3/images/deleted/.htaccess |
— | — | @@ -0,0 +1 @@ |
| 2 | +Order Allow,Deny
|
Index: trunk/phase3/includes/Article.php |
— | — | @@ -2806,6 +2806,7 @@ |
2807 | 2807 | $page = $this->mTitle->getSubjectPage(); |
2808 | 2808 | |
2809 | 2809 | $wgOut->setPagetitle( $page->getPrefixedText() ); |
| 2810 | + $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); |
2810 | 2811 | $wgOut->setSubtitle( wfMsg( 'infosubtitle' )); |
2811 | 2812 | |
2812 | 2813 | # first, see if the page exists at all. |
— | — | @@ -3063,4 +3064,4 @@ |
3064 | 3065 | $wgOut->addParserOutput( $parserOutput ); |
3065 | 3066 | } |
3066 | 3067 | |
3067 | | -} |
\ No newline at end of file |
| 3068 | +} |
Index: trunk/phase3/includes/GlobalFunctions.php |
— | — | @@ -237,6 +237,13 @@ |
238 | 238 | * Log to a file without getting "file size exceeded" signals |
239 | 239 | */ |
240 | 240 | 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 | + |
241 | 248 | wfSuppressWarnings(); |
242 | 249 | $exists = file_exists( $file ); |
243 | 250 | $size = $exists ? filesize( $file ) : false; |
— | — | @@ -2139,7 +2146,9 @@ |
2140 | 2147 | } |
2141 | 2148 | session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); |
2142 | 2149 | session_cache_limiter( 'private, must-revalidate' ); |
| 2150 | + wfDebug( "Starting session..." ); |
2143 | 2151 | @session_start(); |
| 2152 | + wfDebug( "ok\n" ); |
2144 | 2153 | } |
2145 | 2154 | |
2146 | 2155 | /** |
— | — | @@ -2296,4 +2305,4 @@ |
2297 | 2306 | */ |
2298 | 2307 | function wfBoolToStr( $value ) { |
2299 | 2308 | return $value ? 'true' : 'false'; |
2300 | | -} |
\ No newline at end of file |
| 2309 | +} |
Index: trunk/phase3/includes/ImagePage.php |
— | — | @@ -579,56 +579,56 @@ |
580 | 580 | $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); |
581 | 581 | return; |
582 | 582 | } |
583 | | - if ( !$this->doDeleteOldImage( $oldimage ) ) { |
584 | | - return; |
585 | | - } |
| 583 | + $status = $this->doDeleteOldImage( $oldimage ); |
586 | 584 | $deleted = $oldimage; |
587 | 585 | } 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' ) ); |
593 | 590 | } |
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 | + } |
602 | 598 | } |
603 | 599 | |
604 | | - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); |
605 | 600 | $wgOut->setRobotpolicy( 'noindex,nofollow' ); |
606 | 601 | |
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 | + } |
613 | 614 | } |
614 | 615 | |
615 | 616 | /** |
616 | | - * @return success |
| 617 | + * Delete an old revision of an image, |
| 618 | + * @return FileRepoStatus |
617 | 619 | */ |
618 | | - function doDeleteOldImage( $oldimage ) |
619 | | - { |
| 620 | + function doDeleteOldImage( $oldimage ) { |
620 | 621 | global $wgOut; |
621 | 622 | |
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 ) { |
628 | 628 | # Log the deletion |
629 | 629 | $log = new LogPage( 'delete' ); |
630 | 630 | $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) ); |
631 | 631 | } |
632 | | - return $ok; |
| 632 | + return $status; |
633 | 633 | } |
634 | 634 | |
635 | 635 | function revert() { |
— | — | @@ -667,10 +667,11 @@ |
668 | 668 | |
669 | 669 | $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage ); |
670 | 670 | $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 ); |
672 | 673 | |
673 | | - if ( WikiError::isError( $result ) ) { |
674 | | - $this->showError( $result ); |
| 674 | + if ( !$status->isGood() ) { |
| 675 | + $this->showError( $status->getWikiText() ); |
675 | 676 | return; |
676 | 677 | } |
677 | 678 | |
— | — | @@ -699,15 +700,15 @@ |
700 | 701 | } |
701 | 702 | |
702 | 703 | /** |
703 | | - * Display an error from a wikitext-formatted WikiError object |
| 704 | + * Display an error with a wikitext description |
704 | 705 | */ |
705 | | - function showError( WikiError $error ) { |
| 706 | + function showError( $description ) { |
706 | 707 | global $wgOut; |
707 | 708 | $wgOut->setPageTitle( wfMsg( "internalerror" ) ); |
708 | 709 | $wgOut->setRobotpolicy( "noindex,nofollow" ); |
709 | 710 | $wgOut->setArticleRelated( false ); |
710 | 711 | $wgOut->enableClientCache( false ); |
711 | | - $wgOut->addWikiText( $error->getMessage() ); |
| 712 | + $wgOut->addWikiText( $description ); |
712 | 713 | } |
713 | 714 | |
714 | 715 | } |
Index: trunk/phase3/includes/Setup.php |
— | — | @@ -54,6 +54,11 @@ |
55 | 55 | if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; |
56 | 56 | if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; |
57 | 57 | |
| 58 | +if ( empty( $wgFileStore['deleted']['directory'] ) ) { |
| 59 | + $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted"; |
| 60 | +} |
| 61 | + |
| 62 | + |
58 | 63 | /** |
59 | 64 | * Initialise $wgLocalFileRepo from backwards-compatible settings |
60 | 65 | */ |
— | — | @@ -67,6 +72,8 @@ |
68 | 73 | 'thumbScriptUrl' => $wgThumbnailScriptPath, |
69 | 74 | 'transformVia404' => !$wgGenerateThumbnailOnParse, |
70 | 75 | 'initialCapital' => $wgCapitalLinks, |
| 76 | + 'deletedDir' => $wgFileStore['deleted']['directory'], |
| 77 | + 'deletedHashLevels' => $wgFileStore['deleted']['hash'] |
71 | 78 | ); |
72 | 79 | } |
73 | 80 | /** |
— | — | @@ -87,7 +94,7 @@ |
88 | 95 | 'dbUser' => $wgDBuser, |
89 | 96 | 'dbPassword' => $wgDBpassword, |
90 | 97 | 'dbName' => $wgSharedUploadDBname, |
91 | | - 'dbFlags' => DBO_DEFAULT, |
| 98 | + 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT, |
92 | 99 | 'tablePrefix' => $wgSharedUploadDBprefix, |
93 | 100 | 'hasSharedCache' => $wgCacheSharedUploads, |
94 | 101 | '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 |
1 | 172 | + native |
Index: trunk/phase3/includes/filerepo/OldLocalFile.php |
— | — | @@ -70,7 +70,7 @@ |
71 | 71 | } |
72 | 72 | $oldImages = $wgMemc->get( $key ); |
73 | 73 | |
74 | | - if ( isset( $oldImages['version'] ) && $oldImages['version'] == MW_OLDFILE_VERSION ) { |
| 74 | + if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) { |
75 | 75 | unset( $oldImages['version'] ); |
76 | 76 | $more = isset( $oldImages['more'] ); |
77 | 77 | unset( $oldImages['more'] ); |
— | — | @@ -94,7 +94,8 @@ |
95 | 95 | if ( $found ) { |
96 | 96 | wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" ); |
97 | 97 | $this->dataLoaded = true; |
98 | | - foreach ( $cachedValues as $name => $value ) { |
| 98 | + $this->fileExists = true; |
| 99 | + foreach ( $info as $name => $value ) { |
99 | 100 | $this->$name = $value; |
100 | 101 | } |
101 | 102 | } elseif ( $more ) { |
— | — | @@ -130,7 +131,7 @@ |
131 | 132 | wfProfileIn( __METHOD__ ); |
132 | 133 | |
133 | 134 | $dbr = $this->repo->getSlaveDB(); |
134 | | - $res = $dbr->select( 'oldimage', $this->getCacheFields(), |
| 135 | + $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ), |
135 | 136 | array( 'oi_name' => $this->getName() ), __METHOD__, |
136 | 137 | array( |
137 | 138 | 'LIMIT' => self::MAX_CACHE_ROWS + 1, |
— | — | @@ -144,8 +145,8 @@ |
145 | 146 | } |
146 | 147 | for ( $i = 0; $i < $numRows; $i++ ) { |
147 | 148 | $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; |
150 | 151 | } |
151 | 152 | $dbr->freeResult( $res ); |
152 | 153 | $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ ); |
— | — | @@ -169,6 +170,7 @@ |
170 | 171 | $this->fileExists = false; |
171 | 172 | } |
172 | 173 | $this->dataLoaded = true; |
| 174 | + wfProfileOut( __METHOD__ ); |
173 | 175 | } |
174 | 176 | |
175 | 177 | function getCacheFields( $prefix = 'img_' ) { |
— | — | @@ -207,15 +209,14 @@ |
208 | 210 | #'oi_major_mime' => $major, |
209 | 211 | #'oi_minor_mime' => $minor, |
210 | 212 | #'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 ), |
212 | 217 | __METHOD__ |
213 | 218 | ); |
214 | 219 | wfProfileOut( __METHOD__ ); |
215 | 220 | } |
216 | | - |
217 | | - // XXX: Temporary hack before schema update |
218 | | - function maybeUpgradeRow() {} |
219 | | - |
220 | 221 | } |
221 | 222 | |
222 | 223 | |
Index: trunk/phase3/includes/filerepo/LocalFile.php |
— | — | @@ -41,10 +41,12 @@ |
42 | 42 | $major_mime, # Major mime type |
43 | 43 | $minor_mine, # Minor mime type |
44 | 44 | $size, # Size in bytes (loadFromXxx) |
45 | | - $metadata, # Metadata |
| 45 | + $metadata, # Handler-specific metadata |
46 | 46 | $timestamp, # Upload timestamp |
| 47 | + $sha1, # SHA-1 base 36 content hash |
47 | 48 | $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 |
49 | 51 | |
50 | 52 | /**#@-*/ |
51 | 53 | |
— | — | @@ -156,7 +158,7 @@ |
157 | 159 | |
158 | 160 | function getCacheFields( $prefix = 'img_' ) { |
159 | 161 | static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', |
160 | | - 'major_mime', 'minor_mime', 'metadata', 'timestamp' ); |
| 162 | + 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' ); |
161 | 163 | static $results = array(); |
162 | 164 | if ( $prefix == '' ) { |
163 | 165 | return $fields; |
— | — | @@ -175,7 +177,9 @@ |
176 | 178 | * Load file metadata from the DB |
177 | 179 | */ |
178 | 180 | function loadFromDB() { |
179 | | - wfProfileIn( __METHOD__ ); |
| 181 | + # Polymorphic function name to distinguish foreign and local fetches |
| 182 | + $fname = get_class( $this ) . '::' . __FUNCTION__; |
| 183 | + wfProfileIn( $fname ); |
180 | 184 | |
181 | 185 | # Unconditionally set loaded=true, we don't want the accessors constantly rechecking |
182 | 186 | $this->dataLoaded = true; |
— | — | @@ -183,14 +187,14 @@ |
184 | 188 | $dbr = $this->repo->getSlaveDB(); |
185 | 189 | |
186 | 190 | $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), |
187 | | - array( 'img_name' => $this->getName() ), __METHOD__ ); |
| 191 | + array( 'img_name' => $this->getName() ), $fname ); |
188 | 192 | if ( $row ) { |
189 | 193 | $this->loadFromRow( $row ); |
190 | 194 | } else { |
191 | 195 | $this->fileExists = false; |
192 | 196 | } |
193 | 197 | |
194 | | - wfProfileOut( __METHOD__ ); |
| 198 | + wfProfileOut( $fname ); |
195 | 199 | } |
196 | 200 | |
197 | 201 | /** |
— | — | @@ -218,6 +222,8 @@ |
219 | 223 | } |
220 | 224 | $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime']; |
221 | 225 | } |
| 226 | + # Trim zero padding from char/binary field |
| 227 | + $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); |
222 | 228 | return $decoded; |
223 | 229 | } |
224 | 230 | |
— | — | @@ -254,7 +260,10 @@ |
255 | 261 | if ( wfReadOnly() ) { |
256 | 262 | return; |
257 | 263 | } |
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 | + ) { |
259 | 268 | $this->upgradeRow(); |
260 | 269 | $this->upgraded = true; |
261 | 270 | } else { |
— | — | @@ -292,6 +301,7 @@ |
293 | 302 | 'img_major_mime' => $major, |
294 | 303 | 'img_minor_mime' => $minor, |
295 | 304 | 'img_metadata' => $this->metadata, |
| 305 | + 'img_sha1' => $this->sha1, |
296 | 306 | ), array( 'img_name' => $this->getName() ), |
297 | 307 | __METHOD__ |
298 | 308 | ); |
— | — | @@ -480,12 +490,23 @@ |
481 | 491 | function purgeMetadataCache() { |
482 | 492 | $this->loadFromDB(); |
483 | 493 | $this->saveToCache(); |
| 494 | + $this->purgeHistory(); |
484 | 495 | } |
485 | 496 | |
486 | 497 | /** |
| 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 | + /** |
487 | 508 | * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid |
488 | 509 | */ |
489 | | - function purgeCache( $archiveFiles = array() ) { |
| 510 | + function purgeCache() { |
490 | 511 | // Refresh metadata cache |
491 | 512 | $this->purgeMetadataCache(); |
492 | 513 | |
— | — | @@ -596,6 +617,8 @@ |
597 | 618 | /** getHashPath inherited */ |
598 | 619 | /** getRel inherited */ |
599 | 620 | /** getUrlRel inherited */ |
| 621 | + /** getArchiveRel inherited */ |
| 622 | + /** getThumbRel inherited */ |
600 | 623 | /** getArchivePath inherited */ |
601 | 624 | /** getThumbPath inherited */ |
602 | 625 | /** getArchiveUrl inherited */ |
— | — | @@ -615,18 +638,19 @@ |
616 | 639 | * is already known |
617 | 640 | * @param string $timestamp Timestamp for img_timestamp, or false to use the current time |
618 | 641 | * |
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. |
621 | 644 | */ |
622 | 645 | 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 | + } |
626 | 652 | } |
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; |
631 | 655 | } |
632 | 656 | |
633 | 657 | /** |
— | — | @@ -695,6 +719,7 @@ |
696 | 720 | 'img_user' => $wgUser->getID(), |
697 | 721 | 'img_user_text' => $wgUser->getName(), |
698 | 722 | 'img_metadata' => $this->metadata, |
| 723 | + 'img_sha1' => $this->sha1 |
699 | 724 | ), |
700 | 725 | __METHOD__, |
701 | 726 | 'IGNORE' |
— | — | @@ -715,6 +740,11 @@ |
716 | 741 | 'oi_description' => 'img_description', |
717 | 742 | 'oi_user' => 'img_user', |
718 | 743 | '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', |
719 | 749 | ), array( 'img_name' => $this->getName() ), __METHOD__ |
720 | 750 | ); |
721 | 751 | |
— | — | @@ -733,6 +763,7 @@ |
734 | 764 | 'img_user' => $wgUser->getID(), |
735 | 765 | 'img_user_text' => $wgUser->getName(), |
736 | 766 | 'img_metadata' => $this->metadata, |
| 767 | + 'img_sha1' => $this->sha1 |
737 | 768 | ), array( /* WHERE */ |
738 | 769 | 'img_name' => $this->getName() |
739 | 770 | ), __METHOD__ |
— | — | @@ -792,22 +823,23 @@ |
793 | 824 | * @param integer $flags A bitwise combination of: |
794 | 825 | * File::DELETE_SOURCE Delete the source file, i.e. move |
795 | 826 | * 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. |
798 | 829 | */ |
799 | 830 | function publish( $srcPath, $flags = 0 ) { |
| 831 | + $this->lock(); |
800 | 832 | $dstRel = $this->getRel(); |
801 | 833 | $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); |
802 | 834 | $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; |
803 | 835 | $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; |
804 | 836 | $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 = ''; |
809 | 839 | } else { |
810 | | - return $archiveName; |
| 840 | + $status->value = $archiveName; |
811 | 841 | } |
| 842 | + $this->unlock(); |
| 843 | + return $status; |
812 | 844 | } |
813 | 845 | |
814 | 846 | /** getLinksTo inherited */ |
— | — | @@ -824,62 +856,34 @@ |
825 | 857 | * Cache purging is done; logging is caller's responsibility. |
826 | 858 | * |
827 | 859 | * @param $reason |
828 | | - * @return true on success, false on some kind of failure |
| 860 | + * @return FileRepoStatus object. |
829 | 861 | */ |
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(); |
833 | 866 | |
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 ); |
837 | 874 | } |
| 875 | + $status = $batch->execute(); |
838 | 876 | |
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(); |
867 | 882 | } |
868 | 883 | |
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; |
881 | 886 | } |
882 | 887 | |
883 | | - |
884 | 888 | /** |
885 | 889 | * Delete an old version of the file. |
886 | 890 | * |
— | — | @@ -890,190 +894,22 @@ |
891 | 895 | * |
892 | 896 | * @param $reason |
893 | 897 | * @throws MWException or FSException on database or filestore failure |
894 | | - * @return true on success, false on some kind of failure |
| 898 | + * @return FileRepoStatus object. |
895 | 899 | */ |
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(); |
903 | 909 | } |
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; |
933 | 911 | } |
934 | 912 | |
935 | 913 | /** |
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 | | - /** |
1078 | 914 | * Restore all or specified deleted revisions to the given file. |
1079 | 915 | * Permissions and logging are left to the caller. |
1080 | 916 | * |
— | — | @@ -1081,202 +917,25 @@ |
1082 | 918 | * |
1083 | 919 | * @param $versions set of record ids of deleted items to restore, |
1084 | 920 | * or empty to restore all revisions. |
1085 | | - * @return the number of file revisions restored if successful, |
1086 | | - * or false on failure |
| 921 | + * @return FileRepoStatus |
1087 | 922 | */ |
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 ); |
1094 | 929 | } |
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; |
1260 | 933 | } |
1261 | 934 | |
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; |
1281 | 940 | } |
1282 | 941 | |
1283 | 942 | /** isMultipage inherited */ |
— | — | @@ -1310,8 +969,52 @@ |
1311 | 970 | $this->load(); |
1312 | 971 | return $this->timestamp; |
1313 | 972 | } |
| 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 | + } |
1314 | 1015 | } // LocalFile class |
1315 | 1016 | |
| 1017 | +#------------------------------------------------------------------------------ |
| 1018 | + |
1316 | 1019 | /** |
1317 | 1020 | * Backwards compatibility class |
1318 | 1021 | */ |
— | — | @@ -1379,12 +1082,467 @@ |
1380 | 1083 | } |
1381 | 1084 | } |
1382 | 1085 | |
| 1086 | +#------------------------------------------------------------------------------ |
| 1087 | + |
1383 | 1088 | /** |
1384 | | - * Aliases for backwards compatibility with 1.6 |
| 1089 | + * Helper class for file deletion |
1385 | 1090 | */ |
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; |
1390 | 1094 | |
| 1095 | + function __construct( File $file, $reason = '' ) { |
| 1096 | + $this->file = $file; |
| 1097 | + $this->reason = $reason; |
| 1098 | + $this->status = $file->repo->newGood(); |
| 1099 | + } |
1391 | 1100 | |
| 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 @@ |
7 | 7 | */ |
8 | 8 | |
9 | 9 | class FSRepo extends FileRepo { |
10 | | - var $directory, $url, $hashLevels; |
| 10 | + var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels; |
11 | 11 | var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); |
12 | 12 | var $oldFileFactory = false; |
| 13 | + var $pathDisclosureProtection = 'simple'; |
13 | 14 | |
14 | 15 | function __construct( $info ) { |
15 | 16 | parent::__construct( $info ); |
— | — | @@ -16,7 +17,12 @@ |
17 | 18 | // Required settings |
18 | 19 | $this->directory = $info['directory']; |
19 | 20 | $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; |
21 | 27 | } |
22 | 28 | |
23 | 29 | /** |
— | — | @@ -50,7 +56,7 @@ |
51 | 57 | case 'temp': |
52 | 58 | return "{$this->directory}/temp"; |
53 | 59 | case 'deleted': |
54 | | - return $GLOBALS['wgFileStore']['deleted']['directory']; |
| 60 | + return $this->deletedDir; |
55 | 61 | default: |
56 | 62 | return false; |
57 | 63 | } |
— | — | @@ -66,7 +72,7 @@ |
67 | 73 | case 'temp': |
68 | 74 | return "{$this->url}/temp"; |
69 | 75 | case 'deleted': |
70 | | - return $GLOBALS['wgFileStore']['deleted']['url']; |
| 76 | + return false; // no public URL |
71 | 77 | default: |
72 | 78 | return false; |
73 | 79 | } |
— | — | @@ -109,47 +115,101 @@ |
110 | 116 | } |
111 | 117 | |
112 | 118 | /** |
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 |
114 | 127 | */ |
115 | | - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
| 128 | + function storeBatch( $triplets, $flags = 0 ) { |
116 | 129 | 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 ); |
118 | 131 | } |
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 | + } |
122 | 169 | } |
123 | | - $dstPath = "$root/$dstRel"; |
124 | 170 | |
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; |
127 | 176 | } |
128 | | - |
129 | | - if ( self::isVirtualUrl( $srcPath ) ) { |
130 | | - $srcPath = $this->resolveVirtualUrl( $srcPath ); |
131 | | - } |
132 | 177 | |
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 | + } |
137 | 197 | } |
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++; |
142 | 203 | } |
143 | 204 | } |
144 | | - chmod( $dstPath, 0644 ); |
145 | | - return true; |
| 205 | + return $status; |
146 | 206 | } |
147 | 207 | |
148 | 208 | /** |
149 | 209 | * Pick a random name in the temp zone and store a file to it. |
150 | | - * Returns the URL, or a WikiError on failure. |
151 | 210 | * @param string $originalName The base name of the file as specified |
152 | 211 | * by the user. The file extension will be maintained. |
153 | 212 | * @param string $srcPath The current location of the file. |
| 213 | + * @return FileRepoStatus object with the URL in the value. |
154 | 214 | */ |
155 | 215 | function storeTemp( $originalName, $srcPath ) { |
156 | 216 | $date = gmdate( "YmdHis" ); |
— | — | @@ -158,11 +218,8 @@ |
159 | 219 | $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); |
160 | 220 | |
161 | 221 | $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; |
167 | 224 | } |
168 | 225 | |
169 | 226 | /** |
— | — | @@ -183,82 +240,186 @@ |
184 | 241 | return $success; |
185 | 242 | } |
186 | 243 | |
187 | | - |
188 | 244 | /** |
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() |
196 | 247 | * @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 |
198 | 249 | */ |
199 | | - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
| 250 | + function publishBatch( $triplets, $flags = 0 ) { |
| 251 | + // Perform initial checks |
200 | 252 | 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 ); |
202 | 254 | } |
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 | + } |
205 | 284 | } |
206 | | - if ( !$this->validateFilename( $dstRel ) ) { |
207 | | - throw new MWException( 'Validation error in $dstRel' ); |
| 285 | + |
| 286 | + if ( !$status->ok ) { |
| 287 | + return $status; |
208 | 288 | } |
209 | | - if ( !$this->validateFilename( $archiveRel ) ) { |
210 | | - throw new MWException( 'Validation error in $archiveRel' ); |
211 | | - } |
212 | | - $dstPath = "{$this->directory}/$dstRel"; |
213 | | - $archivePath = "{$this->directory}/$archiveRel"; |
214 | 289 | |
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"; |
217 | 294 | |
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 | + } |
222 | 309 | |
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; |
226 | 323 | 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 | + } |
228 | 335 | wfRestoreWarnings(); |
229 | 336 | |
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++; |
233 | 344 | } |
234 | | - else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n"); |
235 | | - $status = 'archived'; |
236 | 345 | } |
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' ); |
239 | 364 | } |
240 | 365 | |
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' ); |
247 | 373 | } |
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' ); |
252 | 376 | } |
| 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 | + } |
253 | 388 | } |
254 | | - wfRestoreWarnings(); |
| 389 | + if ( !$status->ok ) { |
| 390 | + // Abort early |
| 391 | + return $status; |
| 392 | + } |
255 | 393 | |
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 | + } |
260 | 423 | } |
261 | | - |
262 | | - chmod( $dstPath, 0644 ); |
263 | 424 | return $status; |
264 | 425 | } |
265 | 426 | |
— | — | @@ -271,6 +432,18 @@ |
272 | 433 | } |
273 | 434 | |
274 | 435 | /** |
| 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 | + /** |
275 | 448 | * Call a callback function for every file in the repository. |
276 | 449 | * Uses the filesystem even in child classes. |
277 | 450 | */ |
— | — | @@ -308,6 +481,39 @@ |
309 | 482 | $path = $this->resolveVirtualUrl( $virtualUrl ); |
310 | 483 | return File::getPropsFromPath( $path ); |
311 | 484 | } |
| 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 | + |
312 | 518 | } |
313 | 519 | |
314 | 520 | |
Index: trunk/phase3/includes/filerepo/File.php |
— | — | @@ -593,11 +593,9 @@ |
594 | 594 | |
595 | 595 | /** |
596 | 596 | * 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. |
600 | 598 | */ |
601 | | - function purgeEverything( $urlArr=array() ) { |
| 599 | + function purgeEverything() { |
602 | 600 | // Delete thumbnails and refresh file metadata cache |
603 | 601 | $this->purgeCache(); |
604 | 602 | $this->purgeDescription(); |
— | — | @@ -656,9 +654,9 @@ |
657 | 655 | return $this->getHashPath() . rawurlencode( $this->getName() ); |
658 | 656 | } |
659 | 657 | |
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(); |
663 | 661 | if ( $suffix === false ) { |
664 | 662 | $path = substr( $path, 0, -1 ); |
665 | 663 | } else { |
— | — | @@ -667,15 +665,25 @@ |
668 | 666 | return $path; |
669 | 667 | } |
670 | 668 | |
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(); |
674 | 672 | if ( $suffix !== false ) { |
675 | 673 | $path .= '/' . $suffix; |
676 | 674 | } |
677 | 675 | return $path; |
678 | 676 | } |
679 | 677 | |
| 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 | + |
680 | 688 | /** Get the URL of the archive directory, or a particular file if $suffix is specified */ |
681 | 689 | function getArchiveUrl( $suffix = false ) { |
682 | 690 | $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath(); |
— | — | @@ -980,13 +988,20 @@ |
981 | 989 | /** |
982 | 990 | * Get the 14-character timestamp of the file upload, or false if |
983 | 991 | */ |
984 | | - function getTimestmap() { |
| 992 | + function getTimestamp() { |
985 | 993 | $path = $this->getPath(); |
986 | 994 | if ( !file_exists( $path ) ) { |
987 | 995 | return false; |
988 | 996 | } |
989 | 997 | return wfTimestamp( filemtime( $path ) ); |
990 | 998 | } |
| 999 | + |
| 1000 | + /** |
| 1001 | + * Get the SHA-1 base 36 hash of the file |
| 1002 | + */ |
| 1003 | + function getSha1() { |
| 1004 | + return self::sha1Base36( $this->getPath() ); |
| 1005 | + } |
991 | 1006 | |
992 | 1007 | /** |
993 | 1008 | * Determine if the current user is allowed to view a particular |
— | — | @@ -1031,12 +1046,14 @@ |
1032 | 1047 | $gis = false; |
1033 | 1048 | $info['metadata'] = ''; |
1034 | 1049 | } |
| 1050 | + $info['sha1'] = self::sha1Base36( $path ); |
1035 | 1051 | |
1036 | 1052 | wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n"); |
1037 | 1053 | } else { |
1038 | 1054 | $info['mime'] = NULL; |
1039 | 1055 | $info['media_type'] = MEDIATYPE_UNKNOWN; |
1040 | 1056 | $info['metadata'] = ''; |
| 1057 | + $info['sha1'] = ''; |
1041 | 1058 | wfDebug(__METHOD__.": $path NOT FOUND!\n"); |
1042 | 1059 | } |
1043 | 1060 | if( $gis ) { |
— | — | @@ -1056,6 +1073,30 @@ |
1057 | 1074 | wfProfileOut( __METHOD__ ); |
1058 | 1075 | return $info; |
1059 | 1076 | } |
| 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 | + } |
1060 | 1095 | } |
| 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 ); |
1061 | 1103 | |
1062 | | - |
Index: trunk/phase3/includes/filerepo/LocalRepo.php |
— | — | @@ -28,4 +28,33 @@ |
29 | 29 | function newFromArchiveName( $title, $archiveName ) { |
30 | 30 | return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); |
31 | 31 | } |
| 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 | + } |
32 | 61 | } |
Index: trunk/phase3/includes/filerepo/FileRepo.php |
— | — | @@ -6,9 +6,12 @@ |
7 | 7 | */ |
8 | 8 | abstract class FileRepo { |
9 | 9 | const DELETE_SOURCE = 1; |
| 10 | + const OVERWRITE = 2; |
| 11 | + const OVERWRITE_SAME = 4; |
10 | 12 | |
11 | 13 | var $thumbScriptUrl, $transformVia404; |
12 | 14 | var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; |
| 15 | + var $pathDisclosureProtection = 'paranoid'; |
13 | 16 | |
14 | 17 | /** |
15 | 18 | * Factory functions for creating new files |
— | — | @@ -23,7 +26,7 @@ |
24 | 27 | // Optional settings |
25 | 28 | $this->initialCapital = true; // by default |
26 | 29 | foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', |
27 | | - 'thumbScriptUrl', 'initialCapital' ) as $var ) |
| 30 | + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var ) |
28 | 31 | { |
29 | 32 | if ( isset( $info[$var] ) ) { |
30 | 33 | $this->$var = $info[$var]; |
— | — | @@ -200,12 +203,37 @@ |
201 | 204 | |
202 | 205 | /** |
203 | 206 | * 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 |
204 | 217 | */ |
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 | + } |
206 | 225 | |
207 | 226 | /** |
| 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 | + /** |
208 | 235 | * 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 | + * |
210 | 238 | * @param string $originalName The base name of the file as specified |
211 | 239 | * by the user. The file extension will be maintained. |
212 | 240 | * @param string $srcPath The current location of the file. |
— | — | @@ -226,6 +254,9 @@ |
227 | 255 | * Copy or move a file either from the local filesystem or from an mwrepo:// |
228 | 256 | * virtual URL, into this repository at the specified destination location. |
229 | 257 | * |
| 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 | + * |
230 | 261 | * @param string $srcPath The source path or URL |
231 | 262 | * @param string $dstRel The destination relative path |
232 | 263 | * @param string $archiveRel The relative path where the existing file is to |
— | — | @@ -233,9 +264,59 @@ |
234 | 265 | * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate |
235 | 266 | * that the source file should be deleted if possible |
236 | 267 | */ |
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 | + } |
238 | 280 | |
239 | 281 | /** |
| 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 | + /** |
240 | 321 | * Get properties of a file with a given virtual URL |
241 | 322 | * The virtual URL must refer to this repo |
242 | 323 | * Properties should ultimately be obtained via File::getPropsFromPath() |
— | — | @@ -276,5 +357,48 @@ |
277 | 358 | return true; |
278 | 359 | } |
279 | 360 | } |
| 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 ) {} |
280 | 404 | } |
281 | 405 | |
Index: trunk/phase3/includes/filerepo/ForeignDBRepo.php |
— | — | @@ -46,6 +46,12 @@ |
47 | 47 | function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
48 | 48 | throw new MWException( get_class($this) . ': write operations are not supported' ); |
49 | 49 | } |
| 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 | + } |
50 | 56 | } |
51 | 57 | |
52 | 58 | |
Index: trunk/phase3/includes/EditPage.php |
— | — | @@ -939,6 +939,10 @@ |
940 | 940 | # Enabled article-related sidebar, toplinks, etc. |
941 | 941 | $wgOut->setArticleRelated( true ); |
942 | 942 | |
| 943 | + if ( $this->formtype == 'preview' ) { |
| 944 | + $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); |
| 945 | + } |
| 946 | + |
943 | 947 | if ( $this->isConflict ) { |
944 | 948 | $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); |
945 | 949 | $wgOut->setPageTitle( $s ); |
Index: trunk/phase3/includes/SpecialUpload.php |
— | — | @@ -433,8 +433,8 @@ |
434 | 434 | |
435 | 435 | $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, |
436 | 436 | File::DELETE_SOURCE, $this->mFileProps ); |
437 | | - if ( WikiError::isError( $status ) ) { |
438 | | - $this->showError( $status ); |
| 437 | + if ( !$status->isGood() ) { |
| 438 | + $this->showError( $status->getWikiText() ); |
439 | 439 | } else { |
440 | 440 | if ( $this->mWatchthis ) { |
441 | 441 | global $wgUser; |
— | — | @@ -592,12 +592,12 @@ |
593 | 593 | function saveTempUploadedFile( $saveName, $tempName ) { |
594 | 594 | global $wgOut; |
595 | 595 | $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() ); |
599 | 599 | return false; |
600 | 600 | } else { |
601 | | - return $result; |
| 601 | + return $status->value; |
602 | 602 | } |
603 | 603 | } |
604 | 604 | |
— | — | @@ -1354,15 +1354,15 @@ |
1355 | 1355 | } |
1356 | 1356 | |
1357 | 1357 | /** |
1358 | | - * Display an error from a wikitext-formatted WikiError object |
| 1358 | + * Display an error with a wikitext description |
1359 | 1359 | */ |
1360 | | - function showError( WikiError $error ) { |
| 1360 | + function showError( $description ) { |
1361 | 1361 | global $wgOut; |
1362 | 1362 | $wgOut->setPageTitle( wfMsg( "internalerror" ) ); |
1363 | 1363 | $wgOut->setRobotpolicy( "noindex,nofollow" ); |
1364 | 1364 | $wgOut->setArticleRelated( false ); |
1365 | 1365 | $wgOut->enableClientCache( false ); |
1366 | | - $wgOut->addWikiText( $error->getMessage() ); |
| 1366 | + $wgOut->addWikiText( $description ); |
1367 | 1367 | } |
1368 | 1368 | |
1369 | 1369 | /** |
Index: trunk/phase3/includes/OutputPage.php |
— | — | @@ -28,6 +28,7 @@ |
29 | 29 | |
30 | 30 | var $mNewSectionLink = false; |
31 | 31 | var $mNoGallery = false; |
| 32 | + var $mPageTitleActionText = ''; |
32 | 33 | |
33 | 34 | /** |
34 | 35 | * Constructor |
— | — | @@ -189,26 +190,13 @@ |
190 | 191 | } |
191 | 192 | } |
192 | 193 | |
| 194 | + function setPageTitleActionText( $text ) { |
| 195 | + $this->mPageTitleActionText = $text; |
| 196 | + } |
| 197 | + |
193 | 198 | 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; |
213 | 201 | } |
214 | 202 | } |
215 | 203 | |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -258,11 +258,14 @@ |
259 | 259 | 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', |
260 | 260 | 'File' => 'includes/filerepo/File.php', |
261 | 261 | 'FileRepo' => 'includes/filerepo/FileRepo.php', |
| 262 | + 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', |
262 | 263 | 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', |
263 | 264 | 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', |
264 | 265 | 'FSRepo' => 'includes/filerepo/FSRepo.php', |
265 | 266 | 'Image' => 'includes/filerepo/LocalFile.php', |
266 | 267 | 'LocalFile' => 'includes/filerepo/LocalFile.php', |
| 268 | + 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', |
| 269 | + 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', |
267 | 270 | 'LocalRepo' => 'includes/filerepo/LocalRepo.php', |
268 | 271 | 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', |
269 | 272 | 'RepoGroup' => 'includes/filerepo/RepoGroup.php', |
Index: trunk/phase3/includes/SpecialUndelete.php |
— | — | @@ -23,6 +23,7 @@ |
24 | 24 | */ |
25 | 25 | class PageArchive { |
26 | 26 | protected $title; |
| 27 | + var $fileStatus; |
27 | 28 | |
28 | 29 | function __construct( $title ) { |
29 | 30 | if( is_null( $title ) ) { |
— | — | @@ -270,7 +271,8 @@ |
271 | 272 | |
272 | 273 | if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) { |
273 | 274 | $img = wfLocalFile( $this->title ); |
274 | | - $filesRestored = $img->restore( $fileVersions ); |
| 275 | + $this->fileStatus = $img->restore( $fileVersions ); |
| 276 | + $filesRestored = $this->fileStatus->successCount; |
275 | 277 | } else { |
276 | 278 | $filesRestored = 0; |
277 | 279 | } |
— | — | @@ -280,7 +282,7 @@ |
281 | 283 | } else { |
282 | 284 | $textRestored = 0; |
283 | 285 | } |
284 | | - |
| 286 | + |
285 | 287 | // Touch the log! |
286 | 288 | global $wgContLang; |
287 | 289 | $log = new LogPage( 'delete' ); |
— | — | @@ -303,8 +305,12 @@ |
304 | 306 | if( trim( $comment ) != '' ) |
305 | 307 | $reason .= ": {$comment}"; |
306 | 308 | $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 | + } |
309 | 315 | } |
310 | 316 | |
311 | 317 | /** |
— | — | @@ -448,6 +454,7 @@ |
449 | 455 | return $restored; |
450 | 456 | } |
451 | 457 | |
| 458 | + function getFileStatus() { return $this->fileStatus; } |
452 | 459 | } |
453 | 460 | |
454 | 461 | /** |
— | — | @@ -731,8 +738,13 @@ |
732 | 739 | $logViewer = new LogViewer( |
733 | 740 | new LogReader( |
734 | 741 | 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 | + ); |
737 | 749 | $logViewer->showList( $wgOut ); |
738 | 750 | |
739 | 751 | if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { |
— | — | @@ -836,15 +848,23 @@ |
837 | 849 | $this->mTargetTimestamp, |
838 | 850 | $this->mComment, |
839 | 851 | $this->mFileVersions ); |
840 | | - |
| 852 | + |
841 | 853 | if( $ok ) { |
842 | 854 | $skin = $wgUser->getSkin(); |
843 | 855 | $link = $skin->makeKnownLinkObj( $this->mTargetObj ); |
844 | 856 | $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); |
845 | | - return true; |
| 857 | + } else { |
| 858 | + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); |
846 | 859 | } |
| 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" ) ); |
847 | 868 | } |
848 | | - $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); |
849 | 869 | return false; |
850 | 870 | } |
851 | 871 | } |
Index: trunk/phase3/includes/SpecialLog.php |
— | — | @@ -241,19 +241,25 @@ |
242 | 242 | * @addtogroup SpecialPage |
243 | 243 | */ |
244 | 244 | class LogViewer { |
| 245 | + const NO_ACTION_LINK = 1; |
| 246 | + |
245 | 247 | /** |
246 | 248 | * @var LogReader $reader |
247 | 249 | */ |
248 | 250 | var $reader; |
249 | 251 | var $numResults = 0; |
| 252 | + var $flags = 0; |
250 | 253 | |
251 | 254 | /** |
252 | 255 | * @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 |
253 | 258 | */ |
254 | | - function LogViewer( &$reader ) { |
| 259 | + function LogViewer( &$reader, $flags = 0 ) { |
255 | 260 | global $wgUser; |
256 | 261 | $this->skin = $wgUser->getSkin(); |
257 | 262 | $this->reader =& $reader; |
| 263 | + $this->flags = $flags; |
258 | 264 | } |
259 | 265 | |
260 | 266 | /** |
— | — | @@ -363,41 +369,43 @@ |
364 | 370 | $paramArray = LogPage::extractParams( $s->log_params ); |
365 | 371 | $revert = ''; |
366 | 372 | // 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 = ''; |
376 | 409 | } |
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 = ''; |
402 | 410 | } |
403 | 411 | |
404 | 412 | $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 @@ |
165 | 165 | /**#@-*/ |
166 | 166 | |
167 | 167 | /** |
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 | | -/** |
178 | 168 | * New file storage paths; currently used only for deleted files. |
179 | 169 | * Set it like this: |
180 | 170 | * |
— | — | @@ -181,7 +171,7 @@ |
182 | 172 | * |
183 | 173 | */ |
184 | 174 | $wgFileStore = array(); |
185 | | -$wgFileStore['deleted']['directory'] = null; // Don't forget to set this. |
| 175 | +$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted |
186 | 176 | $wgFileStore['deleted']['url'] = null; // Private |
187 | 177 | $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split |
188 | 178 | |
— | — | @@ -208,6 +198,10 @@ |
209 | 199 | * start with a capital letter. The current implementation may give incorrect |
210 | 200 | * description page links when the local $wgCapitalLinks and initialCapital |
211 | 201 | * 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'. |
212 | 206 | * |
213 | 207 | * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored |
214 | 208 | * for local repositories: |
Index: trunk/phase3/includes/FileStore.php |
— | — | @@ -219,7 +219,7 @@ |
220 | 220 | * Confirm that the given file key is valid. |
221 | 221 | * Note that a valid key may refer to a file that does not exist. |
222 | 222 | * |
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 |
224 | 224 | * an optional alphanumeric extension, all lowercase. |
225 | 225 | * The whole must not exceed 64 characters. |
226 | 226 | * |
— | — | @@ -227,7 +227,7 @@ |
228 | 228 | * @return boolean |
229 | 229 | */ |
230 | 230 | 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 ); |
232 | 232 | } |
233 | 233 | |
234 | 234 | |
— | — | @@ -249,7 +249,7 @@ |
250 | 250 | return false; |
251 | 251 | } |
252 | 252 | |
253 | | - $base36 = wfBaseConvert( $hash, 16, 36, 32 ); |
| 253 | + $base36 = wfBaseConvert( $hash, 16, 36, 31 ); |
254 | 254 | if( $extension == '' ) { |
255 | 255 | $key = $base36; |
256 | 256 | } else { |
Index: trunk/phase3/includes/PageHistory.php |
— | — | @@ -62,6 +62,7 @@ |
63 | 63 | * Setup page variables. |
64 | 64 | */ |
65 | 65 | $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); |
| 66 | + $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); |
66 | 67 | $wgOut->setArticleFlag( false ); |
67 | 68 | $wgOut->setArticleRelated( true ); |
68 | 69 | $wgOut->setRobotpolicy( 'noindex,nofollow' ); |
Index: trunk/phase3/StartProfiler.php |
— | — | @@ -1,22 +1,24 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | | -require_once( dirname(__FILE__).'/includes/ProfilerStub.php' ); |
| 4 | +#require_once( './includes/ProfilerStub.php' ); |
5 | 5 | |
6 | 6 | /** |
7 | 7 | * To use a profiler, delete the line above and add something like this: |
8 | 8 | * |
9 | | - * require_once( dirname(__FILE__).'/includes/Profiler.php' ); |
| 9 | + * require_once( './includes/Profiler.php' ); |
10 | 10 | * $wgProfiler = new Profiler; |
11 | 11 | * |
12 | 12 | * Or for a sampling profiler: |
13 | 13 | * if ( !mt_rand( 0, 100 ) ) { |
14 | | - * require_once( dirname(__FILE__).'/includes/Profiler.php' ); |
| 14 | + * require_once( './includes/Profiler.php' ); |
15 | 15 | * $wgProfiler = new Profiler; |
16 | 16 | * } else { |
17 | | - * require_once( dirname(__FILE__).'/includes/ProfilerStub.php' ); |
| 17 | + * require_once( './includes/ProfilerStub.php' ); |
18 | 18 | * } |
19 | 19 | * |
20 | 20 | * Configuration of the profiler output can be done in LocalSettings.php |
21 | 21 | */ |
| 22 | +require_once( dirname(__FILE__).'/includes/Profiler.php' ); |
| 23 | +$wgProfiler = new Profiler; |
22 | 24 | |
23 | 25 | |
Index: trunk/phase3/languages/messages/MessagesEn.php |
— | — | @@ -761,10 +761,13 @@ |
762 | 762 | Please report this to an administrator, making note of the URL.', |
763 | 763 | 'readonly_lag' => 'The database has been automatically locked while the slave database servers catch up to the master', |
764 | 764 | 'internalerror' => 'Internal error', |
| 765 | +'internalerror_info' => 'Internal error: $1', |
765 | 766 | 'filecopyerror' => 'Could not copy file "$1" to "$2".', |
766 | 767 | 'filerenameerror' => 'Could not rename file "$1" to "$2".', |
767 | 768 | 'filedeleteerror' => 'Could not delete file "$1".', |
| 769 | +'directorycreateerror' => 'Could not create directory "$1".', |
768 | 770 | 'filenotfound' => 'Could not find file "$1".', |
| 771 | +'fileexists' => 'Unable to write to file "$1": file exists', |
769 | 772 | 'unexpected' => 'Unexpected value: "$1"="$2".', |
770 | 773 | 'formerror' => 'Error: could not submit form', |
771 | 774 | 'badarticleerror' => 'This action cannot be performed on this page.', |
— | — | @@ -1885,6 +1888,13 @@ |
1886 | 1889 | 'undelete-search-prefix' => 'Show pages starting with:', |
1887 | 1890 | 'undelete-search-submit' => 'Search', |
1888 | 1891 | '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", |
1889 | 1899 | |
1890 | 1900 | # Namespace form on various pages |
1891 | 1901 | 'namespace' => 'Namespace:', |
— | — | @@ -2362,6 +2372,12 @@ |
2363 | 2373 | |
2364 | 2374 | # Image deletion |
2365 | 2375 | '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.', |
2366 | 2382 | |
2367 | 2383 | # Browsing diffs |
2368 | 2384 | 'previousdiff' => '← Previous diff', |
Index: trunk/phase3/RELEASE-NOTES |
— | — | @@ -26,6 +26,7 @@ |
27 | 27 | usergroups |
28 | 28 | * $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites |
29 | 29 | * $wgShowHostnames - Expose server host names through the API and HTML comments |
| 30 | +* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally |
30 | 31 | |
31 | 32 | == New features since 1.10 == |
32 | 33 | |
— | — | @@ -318,8 +319,12 @@ |
319 | 320 | * (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0 |
320 | 321 | * Work around Safari bug with pages ending in ".gz" or ".tgz" |
321 | 322 | * 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 |
322 | 328 | |
323 | | - |
324 | 329 | == API changes since 1.10 == |
325 | 330 | |
326 | 331 | Full API documentation is available at http://www.mediawiki.org/wiki/API |