r24346 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r24345‎ | r24346 | r24347 >
Date:23:41, 23 July 2007
Author:david
Status:old
Tags:
Comment:
Merged revisions 24302-24345 via svnmerge from
svn+ssh://david@svn.wikimedia.org/svnroot/mediawiki/trunk/phase3

........
r24310 | aaron | 2007-07-21 18:40:58 -0700 (Sat, 21 Jul 2007) | 1 line

*Add ar_page; allows for easier restoration when several large pages are merged into one deleted history.
........
r24312 | aaron | 2007-07-21 20:26:50 -0700 (Sat, 21 Jul 2007) | 1 line

*Store page_id in ar_page
........
r24313 | tstarling | 2007-07-22 07:45:12 -0700 (Sun, 22 Jul 2007) | 7 lines

* Introduced FileRepoStatus -- result class for file repo operations.
* Ported file delete/restore to the filerepo framework. Some user-visible changes in error reporting.
* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally. Added a "deleted" directory for the default location, protected by a .htaccess file and the practical obscurity of content hashes.
* Fixed bug 2735: "Preview" shown in title bar for action=submit on special pages
* Removed "restore" links from the deletion log embedded in Special:Undelete
* Added img_sha1/oi_sha1 fields, preserved through upload, delete and restore
* Referenced the new oi_metadata etc. fields to preserve metadata across upload and delete/restore.
........
r24314 | tstarling | 2007-07-22 08:07:54 -0700 (Sun, 22 Jul 2007) | 1 line

rv again
........
r24315 | jeluf | 2007-07-22 08:15:58 -0700 (Sun, 22 Jul 2007) | 1 line

Commas at the end of enumerations only works in PHP...
........
r24316 | rotem | 2007-07-22 08:29:44 -0700 (Sun, 22 Jul 2007) | 1 line

Update.
........
r24317 | wegge | 2007-07-22 14:30:21 -0700 (Sun, 22 Jul 2007) | 1 line

Adding missing translations for da
........
r24318 | robchurch | 2007-07-22 14:55:52 -0700 (Sun, 22 Jul 2007) | 1 line

Correct language - not "no object caching", rather, "no object caching [using the items we haven't detected]"
........
r24323 | robchurch | 2007-07-22 16:16:48 -0700 (Sun, 22 Jul 2007) | 2 lines

* Pass new Revision to the 'ArticleInsertComplete' and 'ArticleSaveComplete' hooks; see docs/hooks.txt for more information
* Document 'ArticleInsertComplete' hook
........
r24324 | robchurch | 2007-07-22 16:25:26 -0700 (Sun, 22 Jul 2007) | 1 line

(bug 9575) Accept upload description from GET parameters
........
r24325 | robchurch | 2007-07-22 16:26:04 -0700 (Sun, 22 Jul 2007) | 1 line

Bug number for r24323
........
r24326 | robchurch | 2007-07-22 16:37:01 -0700 (Sun, 22 Jul 2007) | 1 line

Skip the difference engine cache when 'action=purge' is used while requesting a difference page, to allow refreshing the cache in case of errors
........
r24328 | aaron | 2007-07-22 20:03:04 -0700 (Sun, 22 Jul 2007) | 1 line

*grr
........
r24329 | robchurch | 2007-07-23 01:58:09 -0700 (Mon, 23 Jul 2007) | 1 line

s/one time/once/
........
r24335 | tstarling | 2007-07-23 10:18:32 -0700 (Mon, 23 Jul 2007) | 1 line

debugging code accidentally left in
........
r24336 | tstarling | 2007-07-23 10:19:56 -0700 (Mon, 23 Jul 2007) | 1 line

Debugging
........
r24337 | tstarling | 2007-07-23 10:21:20 -0700 (Mon, 23 Jul 2007) | 1 line

Seed subdirs in the deleted zone with a blank index.html file, to prevent crawling.
........
r24338 | tstarling | 2007-07-23 10:22:09 -0700 (Mon, 23 Jul 2007) | 1 line

Fixed LocalRepo::cleanupDeletedBatch(), wasn't working
........
r24339 | tstarling | 2007-07-23 11:28:52 -0700 (Mon, 23 Jul 2007) | 1 line

32 width
........
r24340 | simetrical | 2007-07-23 12:39:53 -0700 (Mon, 23 Jul 2007) | 1 line

Optimize User::getID() for special cases, and User::isLoggedIn() generally (the latter seems to have always required a database query in the past, when in fact it never should).
........
r24342 | simetrical | 2007-07-23 14:40:05 -0700 (Mon, 23 Jul 2007) | 1 line

(bug 10672) Make Linker::doEditSectionLink protected, not private
........
r24343 | tstarling | 2007-07-23 15:37:52 -0700 (Mon, 23 Jul 2007) | 1 line

Added ar_page
........
r24344 | tstarling | 2007-07-23 15:38:12 -0700 (Mon, 23 Jul 2007) | 1 line

fixed comment
........
Modified paths:
  • /branches/liquidthreads (modified) (history)
  • /branches/liquidthreads/RELEASE-NOTES (modified) (history)
  • /branches/liquidthreads/config/index.php (modified) (history)
  • /branches/liquidthreads/docs/hooks.txt (modified) (history)
  • /branches/liquidthreads/images/deleted (added) (history)
  • /branches/liquidthreads/images/deleted (added) (history)
  • /branches/liquidthreads/includes/Article.php (modified) (history)
  • /branches/liquidthreads/includes/AutoLoader.php (modified) (history)
  • /branches/liquidthreads/includes/BagOStuff.php (modified) (history)
  • /branches/liquidthreads/includes/DefaultSettings.php (modified) (history)
  • /branches/liquidthreads/includes/DifferenceEngine.php (modified) (history)
  • /branches/liquidthreads/includes/EditPage.php (modified) (history)
  • /branches/liquidthreads/includes/FileStore.php (modified) (history)
  • /branches/liquidthreads/includes/GlobalFunctions.php (modified) (history)
  • /branches/liquidthreads/includes/ImagePage.php (modified) (history)
  • /branches/liquidthreads/includes/Linker.php (modified) (history)
  • /branches/liquidthreads/includes/OutputPage.php (modified) (history)
  • /branches/liquidthreads/includes/PageHistory.php (modified) (history)
  • /branches/liquidthreads/includes/Setup.php (modified) (history)
  • /branches/liquidthreads/includes/SpecialLog.php (modified) (history)
  • /branches/liquidthreads/includes/SpecialUndelete.php (modified) (history)
  • /branches/liquidthreads/includes/SpecialUpload.php (modified) (history)
  • /branches/liquidthreads/includes/User.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/FSRepo.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/File.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/FileRepo.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/FileRepoStatus.php (added) (history)
  • /branches/liquidthreads/includes/filerepo/FileRepoStatus.php (added) (history)
  • /branches/liquidthreads/includes/filerepo/ForeignDBRepo.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/LocalFile.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/LocalRepo.php (modified) (history)
  • /branches/liquidthreads/includes/filerepo/OldLocalFile.php (modified) (history)
  • /branches/liquidthreads/languages/LanguageConverter.php (modified) (history)
  • /branches/liquidthreads/languages/messages/MessagesDa.php (modified) (history)
  • /branches/liquidthreads/languages/messages/MessagesEn.php (modified) (history)
  • /branches/liquidthreads/languages/messages/MessagesHe.php (modified) (history)
  • /branches/liquidthreads/maintenance/archives/patch-archive-ar_page.sql (added) (history)
  • /branches/liquidthreads/maintenance/archives/patch-archive-ar_page.sql (added) (history)
  • /branches/liquidthreads/maintenance/archives/patch-img_sha1.sql (added) (history)
  • /branches/liquidthreads/maintenance/archives/patch-img_sha1.sql (added) (history)
  • /branches/liquidthreads/maintenance/language/messages.inc (modified) (history)
  • /branches/liquidthreads/maintenance/rebuildImages.php (modified) (history)
  • /branches/liquidthreads/maintenance/tables.sql (modified) (history)
  • /branches/liquidthreads/maintenance/updaters.inc (modified) (history)

Diff [purge]

Index: branches/liquidthreads/config/index.php
@@ -485,8 +485,8 @@
486486 if( !( $conf->turck || $conf->eaccel || $conf->apc || $conf->xcache ) ) {
487487 echo( '<li>Couldn\'t find <a href="http://turck-mmcache.sourceforge.net">Turck MMCache</a>,
488488 <a href="http://eaccelerator.sourceforge.net">eAccelerator</a>,
489 - <a href="http://www.php.net/apc">APC</a> or <a href="http://trac.lighttpd.net/xcache/">XCache</a>.
490 - Object caching functions cannot be used.</li>' );
 489+ <a href="http://www.php.net/apc">APC</a> or <a href="http://trac.lighttpd.net/xcache/">XCache</a>;
 490+ cannot use these for object caching.</li>' );
491491 }
492492
493493 $conf->diff3 = false;
Index: branches/liquidthreads/languages/LanguageConverter.php
@@ -777,7 +777,7 @@
778778 * MediaWiki:conversiontable* is updated
779779 * @private
780780 */
781 - function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section) {
 781+ function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section, $flags, $revision) {
782782 $titleobj = $article->getTitle();
783783 if($titleobj->getNamespace() == NS_MEDIAWIKI) {
784784 /*
Index: branches/liquidthreads/languages/messages/MessagesEn.php
@@ -646,7 +646,7 @@
647647 'redirectedfrom' => '(Redirected from $1)',
648648 'redirectpagesub' => 'Redirect page',
649649 'lastmodifiedat' => 'This page was last modified $2, $1.', # $1 date, $2 time
650 -'viewcount' => 'This page has been accessed {{PLURAL:$1|one time|$1 times}}.',
 650+'viewcount' => 'This page has been accessed {{PLURAL:$1|once|$1 times}}.',
651651 'protectedpage' => 'Protected page',
652652 'jumpto' => 'Jump to:',
653653 'jumptonavigation' => 'navigation',
@@ -765,10 +765,13 @@
766766 Please report this to an administrator, making note of the URL.',
767767 'readonly_lag' => 'The database has been automatically locked while the slave database servers catch up to the master',
768768 'internalerror' => 'Internal error',
 769+'internalerror_info' => 'Internal error: $1',
769770 'filecopyerror' => 'Could not copy file "$1" to "$2".',
770771 'filerenameerror' => 'Could not rename file "$1" to "$2".',
771772 'filedeleteerror' => 'Could not delete file "$1".',
 773+'directorycreateerror' => 'Could not create directory "$1".',
772774 'filenotfound' => 'Could not find file "$1".',
 775+'fileexists' => 'Unable to write to file "$1": file exists',
773776 'unexpected' => 'Unexpected value: "$1"="$2".',
774777 'formerror' => 'Error: could not submit form',
775778 'badarticleerror' => 'This action cannot be performed on this page.',
@@ -1889,6 +1892,13 @@
18901893 'undelete-search-prefix' => 'Show pages starting with:',
18911894 'undelete-search-submit' => 'Search',
18921895 'undelete-no-results' => 'No matching pages found in the deletion archive.',
 1896+'undelete-filename-mismatch' => 'Cannot undelete file revision with timestamp $1: filename mismatch',
 1897+'undelete-bad-store-key' => 'Cannot undelete file revision with timestamp $1: file was missing before deletion.',
 1898+'undelete-cleanup-error' => 'Error deleting unused archive file "$1".',
 1899+'undelete-missing-filearchive' => 'Unable to restore file archive ID $1 because it isn\'t in the database. ' .
 1900+ 'It may have already been undeleted.',
 1901+'undelete-error-short' => 'Error undeleting file: $1',
 1902+'undelete-error-long' => "Errors were encountered while undeleting the file:\n\n$1\n",
18931903
18941904 # Namespace form on various pages
18951905 'namespace' => 'Namespace:',
@@ -2366,6 +2376,12 @@
23672377
23682378 # Image deletion
23692379 'deletedrevision' => 'Deleted old revision $1.',
 2380+'filedeleteerror-short' => "Error deleting file: $1",
 2381+'filedeleteerror-long' => "Errors were encountered while deleting the file:\n\n$1\n",
 2382+'filedelete-missing' => 'The file "$1" cannot be deleted, because it doesn\'t exist.',
 2383+'filedelete-old-unregistered' => 'The specified file revision "$1" is not in the database.',
 2384+'filedelete-current-unregistered' => 'The specified file "$1" is not in the database.',
 2385+'filedelete-archive-read-only' => 'The archive directory "$1" is not writable by the webserver.',
23702386
23712387 # Browsing diffs
23722388 'previousdiff' => '← Previous diff',
Index: branches/liquidthreads/languages/messages/MessagesHe.php
@@ -499,10 +499,13 @@
500500 אנא דווח על כך למפתח תוך שמירת פרטי כתובת ה־URL.',
501501 'readonly_lag' => 'בסיס הנתונים ננעל אוטומטית כדי לאפשר לבסיסי הנתונים המשניים להתעדכן מהבסיס הראשי.',
502502 'internalerror' => 'שגיאה פנימית',
503 -'filecopyerror' => 'העתקת "$1" ל־"$2" לא הצליחה.',
504 -'filerenameerror' => 'שינוי השם של "$1" ל-"$2" לא הצליח.',
505 -'filedeleteerror' => 'מחיקת "$1" לא הצליחה.',
 503+'internalerror_info' => 'שגיאה פנימית: $1',
 504+'filecopyerror' => 'העתקת "$1" ל־"$2" נכשלה.',
 505+'filerenameerror' => 'שינוי השם של "$1" ל־"$2" נכשל.',
 506+'filedeleteerror' => 'מחיקת "$1" נכשלה.',
 507+'directorycreateerror' => 'יצירת התיקייה "$1" נכשלה.',
506508 'filenotfound' => 'הקובץ "$1" לא נמצא.',
 509+'fileexists' => 'הכתיבה לקובץ "$1" נכשלה: הקובץ קיים',
507510 'unexpected' => 'ערך לא צפוי: "$1"="$2"',
508511 'formerror' => 'שגיאה: לא יכול לשלוח טופס.',
509512 'badarticleerror' => 'לא ניתן לבצע פעולה זו בדף זה.',
@@ -1446,34 +1449,44 @@
14471450 'restriction-level-all' => 'כל רמה',
14481451
14491452 # Undelete
1450 -'undelete' => 'צפיה בדפים מחוקים',
1451 -'undeletepage' => 'צפיה ושחזור דפים מחוקים',
1452 -'viewdeletedpage' => 'צפיה בדפים מחוקים',
1453 -'undeletepagetext' => 'הדפים שלהלן נמחקו, אך הם עדיין בארכיון וניתן לשחזר אותם. הארכיון מנוקה מעת לעת.',
1454 -'undeleteextrahelp' => 'לשחזור הדף כולו, אל תסמנו אף תיבת סימון ולחצו על "שחזור". לשחזור של גרסאות מסוימות בלבד, סמנו את תיבות הסימון של הגרסאות הללו, ולחצו על "שחזור". לחיצה על "איפוס" תנקה את התקציר, ואת כל תיבות הסימון.',
1455 -'undeleterevisions' => '{{plural:$1|גרסה אחת נשמרה|$1 גרסאות נשמרו}} בארכיון',
1456 -'undeletehistory' => 'אם תשחזרו את הדף, כל הגרסאות תשוחזרנה להיסטוריית השינויים שלו. אם כבר יש דף חדש באותו השם, הגרסאות והשינויים יופיעו רק בדף ההיסטוריה שלו, והגרסה הנוכחית של הדף לא תוחלף אוטומטית. יש לציין שהגבלות המוטלות על גרסאות קבצים נמחקות במהלך השחזור.',
1457 -'undeleterevdel' => 'השחזור לא יבוצע אם הגרסה הנוכחית של הדף מחוקה בחלקה. במקרה כזה, עליכם לבטל את ההסתרה של הגרסאות המחוקות החדשות ביותר. גרסאות של קבצים שאין לכם הרשאה לצפות בהם לא ישוחזרו.',
1458 -'undeletehistorynoadmin' => 'דף זה נמחק. הסיבה למחיקה מוצגת בתקציר מטה, ביחד עם פרטים על המשתמשים שערכו את הדף לפני מחיקתו. הטקסט של גרסאות אלו זמין רק למפעילי מערכת.',
1459 -'undelete-revision' => 'גרסה שנמחקה מהדף $1 מתאריך $2:',
1460 -'undeleterevision-missing' => 'הגרסה שגויה או חסרה. ייתכן שמדובר בקישור שבור, או שהגרסה שוחזרה או הוסרה מהארכיון.',
1461 -'undeletebtn' => 'שחזור',
1462 -'undeletereset' => 'איפוס',
1463 -'undeletecomment' => 'תקציר:',
1464 -'undeletedarticle' => 'שחזר את "[[$1]]"',
1465 -'undeletedrevisions' => 'שחזר $1 גרסאות',
1466 -'undeletedrevisions-files' => 'שחזר $1 גרסאות ו־$2 קבצים',
1467 -'undeletedfiles' => 'שחזר $1 קבצים',
1468 -'cannotundelete' => 'השחזור נכשל; ייתכן שמישהו אחר כבר שחזר את הדף.',
1469 -'undeletedpage' => "'''הדף $1 שוחזר בהצלחה.'''
 1453+'undelete' => 'צפיה בדפים מחוקים',
 1454+'undeletepage' => 'צפיה ושחזור דפים מחוקים',
 1455+'viewdeletedpage' => 'צפיה בדפים מחוקים',
 1456+'undeletepagetext' => 'הדפים שלהלן נמחקו, אך הם עדיין בארכיון וניתן לשחזר אותם. הארכיון מנוקה מעת לעת.',
 1457+'undeleteextrahelp' => 'לשחזור הדף כולו, אל תסמנו אף תיבת סימון ולחצו על "שחזור". לשחזור של גרסאות מסוימות בלבד, סמנו את תיבות הסימון של הגרסאות הללו, ולחצו על "שחזור". לחיצה על "איפוס" תנקה את התקציר, ואת כל תיבות הסימון.',
 1458+'undeleterevisions' => '{{plural:$1|גרסה אחת נשמרה|$1 גרסאות נשמרו}} בארכיון',
 1459+'undeletehistory' => 'אם תשחזרו את הדף, כל הגרסאות תשוחזרנה להיסטוריית השינויים שלו. אם כבר יש דף חדש באותו השם, הגרסאות והשינויים יופיעו רק בדף ההיסטוריה שלו, והגרסה הנוכחית של הדף לא תוחלף אוטומטית. יש לציין שהגבלות המוטלות על גרסאות קבצים נמחקות במהלך השחזור.',
 1460+'undeleterevdel' => 'השחזור לא יבוצע אם הגרסה הנוכחית של הדף מחוקה בחלקה. במקרה כזה, עליכם לבטל את ההסתרה של הגרסאות המחוקות החדשות ביותר. גרסאות של קבצים שאין לכם הרשאה לצפות בהם לא ישוחזרו.',
 1461+'undeletehistorynoadmin' => 'דף זה נמחק. הסיבה למחיקה מוצגת בתקציר מטה, ביחד עם פרטים על המשתמשים שערכו את הדף לפני מחיקתו. הטקסט של גרסאות אלו זמין למפעילי מערכת בלבד.',
 1462+'undelete-revision' => 'גרסה שנמחקה מהדף $1 מתאריך $2:',
 1463+'undeleterevision-missing' => 'הגרסה שגויה או חסרה. ייתכן שמדובר בקישור שבור, או שהגרסה שוחזרה או הוסרה מהארכיון.',
 1464+'undeletebtn' => 'שחזור',
 1465+'undeletereset' => 'איפוס',
 1466+'undeletecomment' => 'תקציר:',
 1467+'undeletedarticle' => 'שחזר את "[[$1]]"',
 1468+'undeletedrevisions' => 'שחזר $1 גרסאות',
 1469+'undeletedrevisions-files' => 'שחזר $1 גרסאות ו־$2 קבצים',
 1470+'undeletedfiles' => 'שחזר $1 קבצים',
 1471+'cannotundelete' => 'השחזור נכשל; ייתכן שמישהו אחר כבר שחזר את הדף.',
 1472+'undeletedpage' => "'''הדף $1 שוחזר בהצלחה.'''
14701473
14711474 ראו את [[{{ns:special}}:Log/delete|יומן המחיקות]] לרשימה של מחיקות ושחזורים אחרונים.",
1472 -'undelete-header' => 'ראו את [[{{ns:special}}:Log/delete|יומן המחיקות]] לדפים שנמחקו לאחרונה.',
1473 -'undelete-search-box' => 'חיפוש דפים שנמחקו',
1474 -'undelete-search-prefix' => 'הצגת דפים החל מ:',
1475 -'undelete-search-submit' => 'חיפוש',
1476 -'undelete-no-results' => 'לא נמצאו דפים תואמים בארכיון המחיקות.',
 1475+'undelete-header' => 'ראו את [[{{ns:special}}:Log/delete|יומן המחיקות]] לדפים שנמחקו לאחרונה.',
 1476+'undelete-search-box' => 'חיפוש דפים שנמחקו',
 1477+'undelete-search-prefix' => 'הצגת דפים החל מ:',
 1478+'undelete-search-submit' => 'חיפוש',
 1479+'undelete-no-results' => 'לא נמצאו דפים תואמים בארכיון המחיקות.',
 1480+'undelete-filename-mismatch' => 'שחזור גרסת הקובץ מהתאריך $1 נכשל: שם קובץ לא תואם',
 1481+'undelete-bad-store-key' => 'שחזור גרסת הקובץ מהתאריך $1 נכשל: הקובץ היה חסר לפני המחיקה.',
 1482+'undelete-cleanup-error' => 'שגיאת בעת מחיקת קובץ הארכיון "$1" שאינו בשימוש.',
 1483+'undelete-missing-filearchive' => 'שחזור קובץ הארכיון שמספרו $1 נכשל כיוון שהוא אינו במסד הנתונים. ייתכן שהוא כבר שוחזר.',
 1484+'undelete-error-short' => 'שגיאה בשחזור הקובץ: $1',
 1485+'undelete-error-long' => 'שגיאות שאירעו בעת שחזור הקובץ:
14771486
 1487+$1
 1488+',
 1489+
 1490+
14781491 # Namespace form on various pages
14791492 'namespace' => 'מרחב שם:',
14801493 'invert' => 'ללא מרחב זה',
@@ -1730,11 +1743,6 @@
17311744 'importnosources' => 'אין מקורות לייבוא בין־אתרי, וייבוא ישיר של דף עם היסטוריה אינו מאופשר כעת.',
17321745 'importnofile' => 'לא הועלה קובץ ייבוא.',
17331746 'importuploaderror' => 'העלאת קובץ ייבוא נכשלה; ייתכן שהקובץ גדול מגודל ההעלאה המותר.',
1734 -'import-parse-failure' => 'שגיאת עיבוד בייבוא קובץ XML',
1735 -'import-articlename' => 'שם דף חדש:',
1736 -'import-noarticle' => 'אין דף לייבוא!',
1737 -'import-nonewrevisions' => 'כל הגרסאות יובאו כבר בעבר.',
1738 -'xml-error-string' => '$1 בשורה $2, עמודה $3 (בית $4): $5',
17391747
17401748 # Import log
17411749 'importlogpage' => 'יומן ייבוא',
@@ -1873,8 +1881,17 @@
18741882 'patrol-log-diff' => 'גרסה $1',
18751883
18761884 # Image deletion
1877 -'deletedrevision' => 'מחק גרסה ישנה $1.',
 1885+'deletedrevision' => 'מחק גרסה ישנה $1.',
 1886+'filedeleteerror-short' => 'שגיאה במחיקת הקובץ: $1',
 1887+'filedeleteerror-long' => 'שגיאות שאירעו בעת מחיקת הקובץ:
18781888
 1889+$1
 1890+',
 1891+'filedelete-missing' => 'מחיקת הקובץ "$1" נכשלה, כיוון שהוא אינו קיים.',
 1892+'filedelete-old-unregistered' => 'גרסת הקובץ "$1" אינה רשומה במסד הנתונים.',
 1893+'filedelete-current-unregistered' => 'הקובץ "$1" אינו רשום במסד הנתונים.',
 1894+'filedelete-archive-read-only' => 'השרת אינו יכול לכתוב לתיקיית הארכיון "$1".',
 1895+
18791896 # Browsing diffs
18801897 'previousdiff' => '→ עבור להשוואת הגרסאות הקודמת',
18811898 'nextdiff' => 'עבור להשוואת הגרסאות הבאה ←',
Index: branches/liquidthreads/languages/messages/MessagesDa.php
@@ -424,9 +424,11 @@
425425 Hvis det ikke er tilfældet, har du måske fundet en fejl i programmet. Meld det til en [[{{MediaWiki:grouppage-sysop}}|Administrator]] med angivelse af adressen.',
426426 'readonly_lag' => 'Databasen er automatisk blevet låst mens slave database serverne synkronisere med master databasen',
427427 'internalerror' => 'Intern fejl',
 428+'internalerror_info' => 'Internal fejl: $1',
428429 'filecopyerror' => 'Kunne ikke kopiere filen "$1" til "$2".',
429430 'filerenameerror' => 'Kunne ikke omdøbe filen "$1" til "$2".',
430431 'filedeleteerror' => 'Kunne ikke slette filen "$1".',
 432+'directorycreateerror' => 'Kunne ikke oprette kataloget "$1".',
431433 'filenotfound' => 'Kunne ikke finde filen "$1".',
432434 'unexpected' => 'Uventet værdi: "$1"="$2".',
433435 'formerror' => 'Fejl: Kunne ikke afsende formular',
@@ -1450,6 +1452,12 @@
14511453 'undelete-search-prefix' => 'Søgebegreb (odets start uden wildcards):',
14521454 'undelete-search-submit' => 'Søg',
14531455 'undelete-no-results' => 'Der blev ikke fundet en passende side i arkivet.',
 1456+'undelete-filename-mismatch' => 'Kan ikke gendanne filen med tidsstempel $1: forkert filnavn',
 1457+'undelete-bad-store-key' => 'Kan ikke gendanne filen med tidsstempel $1: file fandtes ikke da den blev slettet',
 1458+'undelete-cleanup-error' => 'Fejl under sletning af ubrugt arkiveret version "$1".',
 1459+'undelete-missing-filearchive' => 'Kunne ikke genskabe arkiveret fil med ID $1 fordi den ikke findes i databasen. Måske er den allerede gendannet.',
 1460+'undelete-error-short' => 'Fejl under gendannelsen af fil: $1',
 1461+'undelete-error-long' => "Der opstod en fejl under gendannelsen af filen:\n\n$1\n",
14541462
14551463 # Namespace form on various pages
14561464 'namespace' => 'Navnerum:',
@@ -1823,6 +1831,12 @@
18241832
18251833 # Image deletion
18261834 'deletedrevision' => 'Slettede gammel version $1.',
 1835+'filedeleteerror-short' => "Fejl under sletning af fil: $1",
 1836+'filedeleteerror-long' => "Der opstod en fejl under sletningen af filen:\n\n$1\n",
 1837+'filedelete-missing' => 'Filen "$1" kan ikke slettes fordi den ikke findes.',
 1838+'filedelete-old-unregistered' => 'Den angivne version "$1" findes ikke i databasen.',
 1839+'filedelete-current-unregistered' => 'Den angiovne fil "$1" findes ikke i databasen.',
 1840+'filedelete-archive-read-only' => 'Webserveren har ikke skriveadgang til arkiv-kataloget "$1".',
18271841
18281842 # Browsing diffs
18291843 'previousdiff' => '← Gå til forrige forskel',
Index: branches/liquidthreads/RELEASE-NOTES
@@ -26,6 +26,7 @@
2727 usergroups
2828 * $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites
2929 * $wgShowHostnames - Expose server host names through the API and HTML comments
 30+* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally
3031
3132 == New features since 1.10 ==
3233
@@ -152,6 +153,11 @@
153154 * Pass the user as an argument to 'isValidPassword' hook callbacks; see
154155 docs/hooks.txt for more information
155156 * Introduce 'UserGetRights' hook; see docs/hooks.txt for more information
 157+* (bug 9595) Pass new Revision to the 'ArticleInsertComplete' and
 158+ 'ArticleSaveComplete' hooks; see docs/hooks.txt for more information
 159+* (bug 9575) Accept upload description from GET parameters
 160+* Skip the difference engine cache when 'action=purge' is used while requesting
 161+ a difference page, to allow refreshing the cache in case of errors
156162
157163 == Bugfixes since 1.10 ==
158164
@@ -318,8 +324,12 @@
319325 * (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0
320326 * Work around Safari bug with pages ending in ".gz" or ".tgz"
321327 * Removed obsolete maintenance/changeuser.sql script; use RenameUser extension
 328+* (bug 2735) "Preview" shown in title bar for action=submit on special pages
 329+* Removed "restore" links from the deletion log embedded in Special:Undelete
 330+* Improved error reporting and robustness for file delete/undelete.
 331+* Improved speed of file delete by storing the SHA-1 hash in image/oldimage
 332+* Fixed leading zero in base 36 SHA-1 hash
322333
323 -
324334 == API changes since 1.10 ==
325335
326336 Full API documentation is available at http://www.mediawiki.org/wiki/API
Index: branches/liquidthreads/maintenance/tables.sql
@@ -376,6 +376,10 @@
377377
378378 -- Length of this revision in bytes
379379 ar_len int unsigned,
 380+
 381+ -- Reference to page_id. Useful for sysadmin fixing of large pages
 382+ -- merged together in the archives
 383+ ar_page int unsigned NOT NULL,
380384
381385 KEY name_title_timestamp (ar_namespace,ar_title,ar_timestamp),
382386 KEY usertext_timestamp (ar_user_text,ar_timestamp)
@@ -686,14 +690,21 @@
687691 -- Time of the upload.
688692 img_timestamp varbinary(14) NOT NULL default '',
689693
 694+ -- SHA-1 content hash in base-36
 695+ img_sha1 varbinary(32) NOT NULL default '',
 696+
690697 PRIMARY KEY img_name (img_name),
691698
692699 INDEX img_usertext_timestamp (img_user_text,img_timestamp),
693700 -- Used by Special:Imagelist for sort-by-size
694701 INDEX img_size (img_size),
695702 -- Used by Special:Newimages and Special:Imagelist
696 - INDEX img_timestamp (img_timestamp)
 703+ INDEX img_timestamp (img_timestamp),
697704
 705+ -- For future use
 706+ INDEX img_sha1 (img_sha1)
 707+
 708+
698709 ) /*$wgDBTableOptions*/;
699710
700711 --
@@ -724,11 +735,13 @@
725736 oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
726737 oi_minor_mime varbinary(32) NOT NULL default "unknown",
727738 oi_deleted tinyint unsigned NOT NULL default '0',
 739+ oi_sha1 varbinary(32) NOT NULL default '',
728740
729741 INDEX oi_usertext_timestamp (oi_user_text,oi_timestamp),
730742 INDEX oi_name_timestamp (oi_name,oi_timestamp),
731743 -- oi_archive_name truncated to 14 to avoid key length overflow
732 - INDEX oi_name_archive_name (oi_name,oi_archive_name(14))
 744+ INDEX oi_name_archive_name (oi_name,oi_archive_name(14)),
 745+ INDEX oi_sha1 (oi_sha1)
733746
734747 ) /*$wgDBTableOptions*/;
735748
Index: branches/liquidthreads/maintenance/archives/patch-img_sha1.sql
@@ -0,0 +1,8 @@
 2+-- Add img_sha1, oi_sha1 and related indexes
 3+ALTER TABLE /*$wgDBprefix*/image
 4+ ADD COLUMN img_sha1 varbinary(32) NOT NULL default '',
 5+ ADD INDEX img_sha1 (img_sha1);
 6+
 7+ALTER TABLE /*$wgDBprefix*/oldimage
 8+ ADD COLUMN oi_sha1 varbinary(32) NOT NULL default '',
 9+ ADD INDEX oi_sha1 (oi_sha1);
Property changes on: branches/liquidthreads/maintenance/archives/patch-img_sha1.sql
___________________________________________________________________
Added: svn:eol-style
110 + native
Index: branches/liquidthreads/maintenance/archives/patch-archive-ar_page.sql
@@ -0,0 +1,6 @@
 2+-- Reference to page_id. Useful for sysadmin fixing of large
 3+-- pages merged together in the archives
 4+-- Added 2007-07-21
 5+
 6+ALTER TABLE /*$wgDBprefix*/archive
 7+ ADD ar_page int unsigned NOT NULL;
Index: branches/liquidthreads/maintenance/rebuildImages.php
@@ -173,8 +173,6 @@
174174 function addMissingImage( $filename, $fullpath ) {
175175 $fname = 'ImageBuilder::addMissingImage';
176176
177 - $size = filesize( $fullpath );
178 - $info = $this->imageInfo( $fullpath );
179177 $timestamp = $this->dbw->timestamp( filemtime( $fullpath ) );
180178
181179 global $wgContLang;
Index: branches/liquidthreads/maintenance/updaters.inc
@@ -80,6 +80,8 @@
8181 array( 'page_restrictions', 'pr_id', 'patch-page_restrictions_sortkey.sql' ),
8282 array( 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ),
8383 array( 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql'),
 84+ array( 'archive', 'ar_page', 'patch-archive-ar_page.sql'),
 85+ array( 'image', 'img_sha1', 'patch-img_sha1.sql' ),
8486 );
8587
8688 # For extensions only, should be populated via hooks
@@ -1347,6 +1349,7 @@
13481350 array("recentchanges", "rc_old_len", "INTEGER"),
13491351 array("recentchanges", "rc_params", "TEXT"),
13501352 array("revision", "rev_len", "INTEGER"),
 1353+ array("archive", "ar_page", "INTEGER NOT NULL DEFAULT 0"),
13511354 );
13521355
13531356
Index: branches/liquidthreads/maintenance/language/messages.inc
@@ -298,10 +298,13 @@
299299 'missingarticle',
300300 'readonly_lag',
301301 'internalerror',
 302+ 'internalerror_info',
302303 'filecopyerror',
303304 'filerenameerror',
304305 'filedeleteerror',
 306+ 'directorycreateerror',
305307 'filenotfound',
 308+ 'fileexists',
306309 'unexpected',
307310 'formerror',
308311 'badarticleerror',
@@ -1223,6 +1226,12 @@
12241227 'undelete-search-prefix',
12251228 'undelete-search-submit',
12261229 'undelete-no-results',
 1230+ 'undelete-filename-mismatch',
 1231+ 'undelete-bad-store-key',
 1232+ 'undelete-cleanup-error',
 1233+ 'undelete-missing-filearchive',
 1234+ 'undelete-error-short',
 1235+ 'undelete-error-long',
12271236 ),
12281237 'nsform' => array(
12291238 'namespace',
@@ -1640,6 +1649,12 @@
16411650 ),
16421651 'imagedeletion' => array(
16431652 'deletedrevision',
 1653+ 'filedeleteerror-short',
 1654+ 'filedeleteerror-long',
 1655+ 'filedelete-missing',
 1656+ 'filedelete-old-unregistered',
 1657+ 'filedelete-current-unregistered',
 1658+ 'filedelete-archive-read-only',
16441659 ),
16451660 'browsediffs' => array(
16461661 'previousdiff',
Index: branches/liquidthreads/docs/hooks.txt
@@ -267,6 +267,17 @@
268268 $user: the user that deleted the article
269269 $reason: the reason the article was deleted
270270
 271+'ArticleInsertComplete': After an article is created
 272+$article: Article created
 273+$user: User creating the article
 274+$text: New content
 275+$summary: Edit summary/comment
 276+$isMinor: Whether or not the edit was marked as minor
 277+$isWatch: (No longer used)
 278+$section: (No longer used)
 279+$flags: Flags passed to Article::doEdit()
 280+$revision: New Revision of the article
 281+
271282 'ArticleProtect': before an article is protected
272283 $article: the article being protected
273284 $user: the user doing the protection
@@ -290,6 +301,17 @@
291302 $iswatch: watch flag
292303 $section: section #
293304
 305+'ArticleSaveComplete': After an article has been updated
 306+$article: Article modified
 307+$user: User performing the modification
 308+$text: New content
 309+$summary: Edit summary/comment
 310+$isMinor: Whether or not the edit was marked as minor
 311+$isWatch: (No longer used)
 312+$section: (No longer used)
 313+$flags: Flags passed to Article::doEdit()
 314+$revision: New Revision of the article
 315+
294316 'ArticleSaveComplete': after an article is saved
295317 $article: the article (object) saved
296318 $user: the user (object) who saved the article
@@ -299,6 +321,8 @@
300322 $iswatch: watch flag
301323 $section: section #
302324
 325+wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 326+
303327 'ArticleUndeleted': When one or more revisions of an article are restored
304328 $title: Title corresponding to the article restored
305329 $create: Whether or not the restoration caused the page to be created
Index: branches/liquidthreads/images/deleted/.htaccess
@@ -0,0 +1 @@
 2+Order Allow,Deny
Index: branches/liquidthreads/includes/User.php
@@ -1119,9 +1119,16 @@
11201120 /**
11211121 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
11221122 */
1123 - function getID() {
1124 - $this->load();
1125 - return $this->mId;
 1123+ function getID() {
 1124+ if( $this->mId === null and $this->mName !== null
 1125+ and User::isIP( $this->mName ) ) {
 1126+ // Special case, we know the user is anonymous
 1127+ return 0;
 1128+ } elseif( $this->mId === null ) {
 1129+ // Don't load if this was initialized from an ID
 1130+ $this->load();
 1131+ }
 1132+ return $this->mId;
11261133 }
11271134
11281135 /**
@@ -1715,7 +1722,11 @@
17161723 * @return bool
17171724 */
17181725 function isLoggedIn() {
1719 - return( $this->getID() != 0 );
 1726+ if( $this->mId === null and $this->mName !== null ) {
 1727+ // Special-case optimization
 1728+ return !self::isIP( $this->mName );
 1729+ }
 1730+ return $this->getID() != 0;
17201731 }
17211732
17221733 /**
Index: branches/liquidthreads/includes/BagOStuff.php
@@ -709,6 +709,7 @@
710710
711711 function delete( $key, $time = 0 ) {
712712 wfProfileIn( __METHOD__ );
 713+ wfDebug( __METHOD__."($key)\n" );
713714 $handle = $this->getWriter();
714715 if ( !$handle ) {
715716 return false;
Index: branches/liquidthreads/includes/Article.php
@@ -1457,19 +1457,16 @@
14581458 # Clear caches
14591459 Article::onArticleCreate( $this->mTitle );
14601460
1461 - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text,
1462 - $summary, $flags & EDIT_MINOR,
1463 - null, null, &$flags ) );
 1461+ wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, $summary,
 1462+ $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
14641463 }
14651464
14661465 if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) {
14671466 wfDoUpdates();
14681467 }
14691468
1470 - wfRunHooks( 'ArticleSaveComplete',
1471 - array( &$this, &$wgUser, $text,
1472 - $summary, $flags & EDIT_MINOR,
1473 - null, null, &$flags ) );
 1469+ wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary,
 1470+ $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
14741471
14751472 wfProfileOut( __METHOD__ );
14761473 return $good;
@@ -2116,7 +2113,8 @@
21172114 'ar_text_id' => 'rev_text_id',
21182115 'ar_text' => '\'\'', // Be explicit to appease
21192116 'ar_flags' => '\'\'', // MySQL's "strict mode"...
2120 - 'ar_len' => 'rev_len'
 2117+ 'ar_len' => 'rev_len',
 2118+ 'ar_page' => $id
21212119 ), array(
21222120 'page_id' => $id,
21232121 'page_id = rev_page'
@@ -2805,6 +2803,7 @@
28062804 $page = $this->mTitle->getSubjectPage();
28072805
28082806 $wgOut->setPagetitle( $page->getPrefixedText() );
 2807+ $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
28092808 $wgOut->setSubtitle( wfMsg( 'infosubtitle' ));
28102809
28112810 # first, see if the page exists at all.
@@ -3062,4 +3061,4 @@
30633062 $wgOut->addParserOutput( $parserOutput );
30643063 }
30653064
3066 -}
\ No newline at end of file
 3065+}
Index: branches/liquidthreads/includes/GlobalFunctions.php
@@ -2296,4 +2296,4 @@
22972297 */
22982298 function wfBoolToStr( $value ) {
22992299 return $value ? 'true' : 'false';
2300 -}
\ No newline at end of file
 2300+}
Index: branches/liquidthreads/includes/ImagePage.php
@@ -579,56 +579,56 @@
580580 $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
581581 return;
582582 }
583 - if ( !$this->doDeleteOldImage( $oldimage ) ) {
584 - return;
585 - }
 583+ $status = $this->doDeleteOldImage( $oldimage );
586584 $deleted = $oldimage;
587585 } else {
588 - $ok = $this->img->delete( $reason );
589 - if( !$ok ) {
590 - # If the deletion operation actually failed, bug out:
591 - $wgOut->showFileDeleteError( $this->img->getName() );
592 - return;
 586+ $status = $this->img->delete( $reason );
 587+ if ( !$status->isGood() ) {
 588+ // Warning or error
 589+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
593590 }
594 -
595 - # Image itself is now gone, and database is cleaned.
596 - # Now we remove the image description page.
597 -
598 - $article = new Article( $this->mTitle );
599 - $article->doDeleteArticle( $reason ); # ignore errors
600 -
601 - $deleted = $this->img->getName();
 591+ if ( $status->ok ) {
 592+ # Image itself is now gone, and database is cleaned.
 593+ # Now we remove the image description page.
 594+ $article = new Article( $this->mTitle );
 595+ $article->doDeleteArticle( $reason ); # ignore errors
 596+ $deleted = $this->img->getName();
 597+ }
602598 }
603599
604 - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
605600 $wgOut->setRobotpolicy( 'noindex,nofollow' );
606601
607 - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
608 - $text = wfMsg( 'deletedtext', $deleted, $loglink );
609 -
610 - $wgOut->addWikiText( $text );
611 -
612 - $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 602+ if ( !$status->ok ) {
 603+ // Fatal error flagged
 604+ $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
 605+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 606+ } else {
 607+ // Operation completed
 608+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
 609+ $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
 610+ $text = wfMsg( 'deletedtext', $deleted, $loglink );
 611+ $wgOut->addWikiText( $text );
 612+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
 613+ }
613614 }
614615
615616 /**
616 - * @return success
 617+ * Delete an old revision of an image,
 618+ * @return FileRepoStatus
617619 */
618 - function doDeleteOldImage( $oldimage )
619 - {
 620+ function doDeleteOldImage( $oldimage ) {
620621 global $wgOut;
621622
622 - $ok = $this->img->deleteOld( $oldimage, '' );
623 - if( !$ok ) {
624 - # If we actually have a file and can't delete it, throw an error.
625 - # Something went awry...
626 - $wgOut->showFileDeleteError( "$oldimage" );
627 - } else {
 623+ $status = $this->img->deleteOld( $oldimage, '' );
 624+ if( !$status->isGood() ) {
 625+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
 626+ }
 627+ if ( $status->ok ) {
628628 # Log the deletion
629629 $log = new LogPage( 'delete' );
630630 $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) );
631631 }
632 - return $ok;
 632+ return $status;
633633 }
634634
635635 function revert() {
@@ -667,10 +667,11 @@
668668
669669 $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage );
670670 $comment = wfMsg( "reverted" );
671 - $result = $this->img->upload( $sourcePath, $comment, $comment );
 671+ // TODO: preserve file properties from DB instead of reloading from file
 672+ $status = $this->img->upload( $sourcePath, $comment, $comment );
672673
673 - if ( WikiError::isError( $result ) ) {
674 - $this->showError( $result );
 674+ if ( !$status->isGood() ) {
 675+ $this->showError( $status->getWikiText() );
675676 return;
676677 }
677678
@@ -699,15 +700,15 @@
700701 }
701702
702703 /**
703 - * Display an error from a wikitext-formatted WikiError object
 704+ * Display an error with a wikitext description
704705 */
705 - function showError( WikiError $error ) {
 706+ function showError( $description ) {
706707 global $wgOut;
707708 $wgOut->setPageTitle( wfMsg( "internalerror" ) );
708709 $wgOut->setRobotpolicy( "noindex,nofollow" );
709710 $wgOut->setArticleRelated( false );
710711 $wgOut->enableClientCache( false );
711 - $wgOut->addWikiText( $error->getMessage() );
 712+ $wgOut->addWikiText( $description );
712713 }
713714
714715 }
Index: branches/liquidthreads/includes/Linker.php
@@ -1094,7 +1094,7 @@
10951095 * @param $hook String, name of hook to run
10961096 * @return String, HTML to use for edit link
10971097 */
1098 - private function doEditSectionLink( Title $nt, $section, $hint, $hook ) {
 1098+ protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) {
10991099 global $wgContLang;
11001100 $editurl = '&section='.$section;
11011101 $url = $this->makeKnownLinkObj(
Index: branches/liquidthreads/includes/Setup.php
@@ -54,6 +54,11 @@
5555 if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
5656 if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
5757
 58+if ( empty( $wgFileStore['deleted']['directory'] ) ) {
 59+ $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
 60+}
 61+
 62+
5863 /**
5964 * Initialise $wgLocalFileRepo from backwards-compatible settings
6065 */
@@ -67,6 +72,8 @@
6873 'thumbScriptUrl' => $wgThumbnailScriptPath,
6974 'transformVia404' => !$wgGenerateThumbnailOnParse,
7075 'initialCapital' => $wgCapitalLinks,
 76+ 'deletedDir' => $wgFileStore['deleted']['directory'],
 77+ 'deletedHashLevels' => $wgFileStore['deleted']['hash']
7178 );
7279 }
7380 /**
@@ -87,7 +94,7 @@
8895 'dbUser' => $wgDBuser,
8996 'dbPassword' => $wgDBpassword,
9097 'dbName' => $wgSharedUploadDBname,
91 - 'dbFlags' => DBO_DEFAULT,
 98+ 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT,
9299 'tablePrefix' => $wgSharedUploadDBprefix,
93100 'hasSharedCache' => $wgCacheSharedUploads,
94101 'descBaseUrl' => $wgRepositoryBaseUrl,
Index: branches/liquidthreads/includes/filerepo/FileRepo.php
@@ -6,9 +6,12 @@
77 */
88 abstract class FileRepo {
99 const DELETE_SOURCE = 1;
 10+ const OVERWRITE = 2;
 11+ const OVERWRITE_SAME = 4;
1012
1113 var $thumbScriptUrl, $transformVia404;
1214 var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
 15+ var $pathDisclosureProtection = 'paranoid';
1316
1417 /**
1518 * Factory functions for creating new files
@@ -23,7 +26,7 @@
2427 // Optional settings
2528 $this->initialCapital = true; // by default
2629 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
27 - 'thumbScriptUrl', 'initialCapital' ) as $var )
 30+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var )
2831 {
2932 if ( isset( $info[$var] ) ) {
3033 $this->$var = $info[$var];
@@ -200,12 +203,37 @@
201204
202205 /**
203206 * Store a file to a given destination.
 207+ *
 208+ * @param string $srcPath Source path or virtual URL
 209+ * @param string $dstZone Destination zone
 210+ * @param string $dstRel Destination relative path
 211+ * @param integer $flags Bitwise combination of the following flags:
 212+ * self::DELETE_SOURCE Delete the source file after upload
 213+ * self::OVERWRITE Overwrite an existing destination file instead of failing
 214+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
 215+ * same contents as the source
 216+ * @return FileRepoStatus
204217 */
205 - abstract function store( $srcPath, $dstZone, $dstRel, $flags = 0 );
 218+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
 219+ $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
 220+ if ( $status->successCount == 0 ) {
 221+ $status->ok = false;
 222+ }
 223+ return $status;
 224+ }
206225
207226 /**
 227+ * Store a batch of files
 228+ *
 229+ * @param array $triplets (src,zone,dest) triplets as per store()
 230+ * @param integer $flags Flags as per store
 231+ */
 232+ abstract function storeBatch( $triplets, $flags = 0 );
 233+
 234+ /**
208235 * Pick a random name in the temp zone and store a file to it.
209 - * Returns the URL, or a WikiError on failure.
 236+ * Returns a FileRepoStatus object with the URL in the value.
 237+ *
210238 * @param string $originalName The base name of the file as specified
211239 * by the user. The file extension will be maintained.
212240 * @param string $srcPath The current location of the file.
@@ -226,6 +254,9 @@
227255 * Copy or move a file either from the local filesystem or from an mwrepo://
228256 * virtual URL, into this repository at the specified destination location.
229257 *
 258+ * Returns a FileRepoStatus object. On success, the value contains "new" or
 259+ * "archived", to indicate whether the file was new with that name.
 260+ *
230261 * @param string $srcPath The source path or URL
231262 * @param string $dstRel The destination relative path
232263 * @param string $archiveRel The relative path where the existing file is to
@@ -233,9 +264,59 @@
234265 * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
235266 * that the source file should be deleted if possible
236267 */
237 - abstract function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 );
 268+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 269+ $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
 270+ if ( $status->successCount == 0 ) {
 271+ $status->ok = false;
 272+ }
 273+ if ( isset( $status->value[0] ) ) {
 274+ $status->value = $status->value[0];
 275+ } else {
 276+ $status->value = false;
 277+ }
 278+ return $status;
 279+ }
238280
239281 /**
 282+ * Publish a batch of files
 283+ * @param array $triplets (source,dest,archive) triplets as per publish()
 284+ * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
 285+ * that the source files should be deleted if possible
 286+ */
 287+ abstract function publishBatch( $triplets, $flags = 0 );
 288+
 289+ /**
 290+ * Move a group of files to the deletion archive.
 291+ *
 292+ * If no valid deletion archive is configured, this may either delete the
 293+ * file or throw an exception, depending on the preference of the repository.
 294+ *
 295+ * The overwrite policy is determined by the repository -- currently FSRepo
 296+ * assumes a naming scheme in the deleted zone based on content hash, as
 297+ * opposed to the public zone which is assumed to be unique.
 298+ *
 299+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
 300+ * is a two-element array containing the source file path relative to the
 301+ * public root in the first element, and the archive file path relative
 302+ * to the deleted zone root in the second element.
 303+ * @return FileRepoStatus
 304+ */
 305+ abstract function deleteBatch( $sourceDestPairs );
 306+
 307+ /**
 308+ * Move a file to the deletion archive.
 309+ * If no valid deletion archive exists, this may either delete the file
 310+ * or throw an exception, depending on the preference of the repository
 311+ * @param mixed $srcRel Relative path for the file to be deleted
 312+ * @param mixed $archiveRel Relative path for the archive location.
 313+ * Relative to a private archive directory.
 314+ * @return WikiError object (wikitext-formatted), or true for success
 315+ */
 316+ function delete( $srcRel, $archiveRel ) {
 317+ return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
 318+ }
 319+
 320+ /**
240321 * Get properties of a file with a given virtual URL
241322 * The virtual URL must refer to this repo
242323 * Properties should ultimately be obtained via File::getPropsFromPath()
@@ -276,5 +357,48 @@
277358 return true;
278359 }
279360 }
 361+
 362+ /**#@+
 363+ * Path disclosure protection functions
 364+ */
 365+ function paranoidClean( $param ) { return '[hidden]'; }
 366+ function passThrough( $param ) { return $param; }
 367+
 368+ /**
 369+ * Get a callback function to use for cleaning error message parameters
 370+ */
 371+ function getErrorCleanupFunction() {
 372+ switch ( $this->pathDisclosureProtection ) {
 373+ case 'none':
 374+ $callback = array( $this, 'passThrough' );
 375+ break;
 376+ default: // 'paranoid'
 377+ $callback = array( $this, 'paranoidClean' );
 378+ }
 379+ return $callback;
 380+ }
 381+ /**#@-*/
 382+
 383+ /**
 384+ * Create a new fatal error
 385+ */
 386+ function newFatal( $message /*, parameters...*/ ) {
 387+ $params = func_get_args();
 388+ array_unshift( $params, $this );
 389+ return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
 390+ }
 391+
 392+ /**
 393+ * Create a new good result
 394+ */
 395+ function newGood( $value = null ) {
 396+ return FileRepoStatus::newGood( $this, $value );
 397+ }
 398+
 399+ /**
 400+ * Delete files in the deleted directory if they are not referenced in the filearchive table
 401+ * STUB
 402+ */
 403+ function cleanupDeletedBatch( $storageKeys ) {}
280404 }
281405
Index: branches/liquidthreads/includes/filerepo/ForeignDBRepo.php
@@ -46,6 +46,12 @@
4747 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
4848 throw new MWException( get_class($this) . ': write operations are not supported' );
4949 }
 50+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 51+ throw new MWException( get_class($this) . ': write operations are not supported' );
 52+ }
 53+ function deleteBatch( $fileMap ) {
 54+ throw new MWException( get_class($this) . ': write operations are not supported' );
 55+ }
5056 }
5157
5258
Index: branches/liquidthreads/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: branches/liquidthreads/includes/filerepo/FileRepoStatus.php
___________________________________________________________________
Added: svn:eol-style
1172 + native
Index: branches/liquidthreads/includes/filerepo/OldLocalFile.php
@@ -70,7 +70,7 @@
7171 }
7272 $oldImages = $wgMemc->get( $key );
7373
74 - if ( isset( $oldImages['version'] ) && $oldImages['version'] == MW_OLDFILE_VERSION ) {
 74+ if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) {
7575 unset( $oldImages['version'] );
7676 $more = isset( $oldImages['more'] );
7777 unset( $oldImages['more'] );
@@ -94,7 +94,8 @@
9595 if ( $found ) {
9696 wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" );
9797 $this->dataLoaded = true;
98 - foreach ( $cachedValues as $name => $value ) {
 98+ $this->fileExists = true;
 99+ foreach ( $info as $name => $value ) {
99100 $this->$name = $value;
100101 }
101102 } elseif ( $more ) {
@@ -130,7 +131,7 @@
131132 wfProfileIn( __METHOD__ );
132133
133134 $dbr = $this->repo->getSlaveDB();
134 - $res = $dbr->select( 'oldimage', $this->getCacheFields(),
 135+ $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ),
135136 array( 'oi_name' => $this->getName() ), __METHOD__,
136137 array(
137138 'LIMIT' => self::MAX_CACHE_ROWS + 1,
@@ -144,8 +145,8 @@
145146 }
146147 for ( $i = 0; $i < $numRows; $i++ ) {
147148 $row = $dbr->fetchObject( $res );
148 - $this->decodeRow( $row, 'oi_' );
149 - $cache[$row->oi_timestamp] = $row;
 149+ $decoded = $this->decodeRow( $row, 'oi_' );
 150+ $cache[$row->oi_timestamp] = $decoded;
150151 }
151152 $dbr->freeResult( $res );
152153 $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ );
@@ -169,6 +170,7 @@
170171 $this->fileExists = false;
171172 }
172173 $this->dataLoaded = true;
 174+ wfProfileOut( __METHOD__ );
173175 }
174176
175177 function getCacheFields( $prefix = 'img_' ) {
@@ -207,15 +209,14 @@
208210 #'oi_major_mime' => $major,
209211 #'oi_minor_mime' => $minor,
210212 #'oi_metadata' => $this->metadata,
211 - ), array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->requestedTime ),
 213+ 'oi_sha1' => $this->sha1,
 214+ ), array(
 215+ 'oi_name' => $this->getName(),
 216+ 'oi_archive_name' => $this->archive_name ),
212217 __METHOD__
213218 );
214219 wfProfileOut( __METHOD__ );
215220 }
216 -
217 - // XXX: Temporary hack before schema update
218 - function maybeUpgradeRow() {}
219 -
220221 }
221222
222223
Index: branches/liquidthreads/includes/filerepo/LocalFile.php
@@ -41,10 +41,12 @@
4242 $major_mime, # Major mime type
4343 $minor_mine, # Minor mime type
4444 $size, # Size in bytes (loadFromXxx)
45 - $metadata, # Metadata
 45+ $metadata, # Handler-specific metadata
4646 $timestamp, # Upload timestamp
 47+ $sha1, # SHA-1 base 36 content hash
4748 $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
48 - $upgraded; # Whether the row was upgraded on load
 49+ $upgraded, # Whether the row was upgraded on load
 50+ $locked; # True if the image row is locked
4951
5052 /**#@-*/
5153
@@ -156,7 +158,7 @@
157159
158160 function getCacheFields( $prefix = 'img_' ) {
159161 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
160 - 'major_mime', 'minor_mime', 'metadata', 'timestamp' );
 162+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' );
161163 static $results = array();
162164 if ( $prefix == '' ) {
163165 return $fields;
@@ -175,7 +177,9 @@
176178 * Load file metadata from the DB
177179 */
178180 function loadFromDB() {
179 - wfProfileIn( __METHOD__ );
 181+ # Polymorphic function name to distinguish foreign and local fetches
 182+ $fname = get_class( $this ) . '::' . __FUNCTION__;
 183+ wfProfileIn( $fname );
180184
181185 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
182186 $this->dataLoaded = true;
@@ -183,14 +187,14 @@
184188 $dbr = $this->repo->getSlaveDB();
185189
186190 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
187 - array( 'img_name' => $this->getName() ), __METHOD__ );
 191+ array( 'img_name' => $this->getName() ), $fname );
188192 if ( $row ) {
189193 $this->loadFromRow( $row );
190194 } else {
191195 $this->fileExists = false;
192196 }
193197
194 - wfProfileOut( __METHOD__ );
 198+ wfProfileOut( $fname );
195199 }
196200
197201 /**
@@ -218,6 +222,8 @@
219223 }
220224 $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
221225 }
 226+ # Trim zero padding from char/binary field
 227+ $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
222228 return $decoded;
223229 }
224230
@@ -254,7 +260,10 @@
255261 if ( wfReadOnly() ) {
256262 return;
257263 }
258 - if ( is_null($this->media_type) || $this->mime == 'image/svg' ) {
 264+ if ( is_null($this->media_type) ||
 265+ $this->mime == 'image/svg' ||
 266+ $this->sha1 == ''
 267+ ) {
259268 $this->upgradeRow();
260269 $this->upgraded = true;
261270 } else {
@@ -292,6 +301,7 @@
293302 'img_major_mime' => $major,
294303 'img_minor_mime' => $minor,
295304 'img_metadata' => $this->metadata,
 305+ 'img_sha1' => $this->sha1,
296306 ), array( 'img_name' => $this->getName() ),
297307 __METHOD__
298308 );
@@ -480,12 +490,23 @@
481491 function purgeMetadataCache() {
482492 $this->loadFromDB();
483493 $this->saveToCache();
 494+ $this->purgeHistory();
484495 }
485496
486497 /**
 498+ * Purge the shared history (OldLocalFile) cache
 499+ */
 500+ function purgeHistory() {
 501+ global $wgMemc;
 502+ $hashedName = md5($this->getName());
 503+ $oldKey = wfMemcKey( 'oldfile', $hashedName );
 504+ $wgMemc->delete( $oldKey );
 505+ }
 506+
 507+ /**
487508 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
488509 */
489 - function purgeCache( $archiveFiles = array() ) {
 510+ function purgeCache() {
490511 // Refresh metadata cache
491512 $this->purgeMetadataCache();
492513
@@ -596,6 +617,8 @@
597618 /** getHashPath inherited */
598619 /** getRel inherited */
599620 /** getUrlRel inherited */
 621+ /** getArchiveRel inherited */
 622+ /** getThumbRel inherited */
600623 /** getArchivePath inherited */
601624 /** getThumbPath inherited */
602625 /** getArchiveUrl inherited */
@@ -615,18 +638,19 @@
616639 * is already known
617640 * @param string $timestamp Timestamp for img_timestamp, or false to use the current time
618641 *
619 - * @return Returns the archive name on success or an empty string if it was a new upload.
620 - * Returns a wikitext-formatted WikiError on failure.
 642+ * @return FileRepoStatus object. On success, the value member contains the
 643+ * archive name, or an empty string if it was a new file.
621644 */
622645 function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) {
623 - $archive = $this->publish( $srcPath, $flags );
624 - if ( WikiError::isError( $archive ) ){
625 - return $archive;
 646+ $this->lock();
 647+ $status = $this->publish( $srcPath, $flags );
 648+ if ( $status->ok ) {
 649+ if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) {
 650+ $status->fatal( 'filenotfound', $srcPath );
 651+ }
626652 }
627 - if ( !$this->recordUpload2( $archive, $comment, $pageText, $props, $timestamp ) ) {
628 - return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) );
629 - }
630 - return $archive;
 653+ $this->unlock();
 654+ return $status;
631655 }
632656
633657 /**
@@ -695,6 +719,7 @@
696720 'img_user' => $wgUser->getID(),
697721 'img_user_text' => $wgUser->getName(),
698722 'img_metadata' => $this->metadata,
 723+ 'img_sha1' => $this->sha1
699724 ),
700725 __METHOD__,
701726 'IGNORE'
@@ -715,6 +740,11 @@
716741 'oi_description' => 'img_description',
717742 'oi_user' => 'img_user',
718743 'oi_user_text' => 'img_user_text',
 744+ 'oi_metadata' => 'img_metadata',
 745+ 'oi_media_type' => 'img_media_type',
 746+ 'oi_major_mime' => 'img_major_mime',
 747+ 'oi_minor_mime' => 'img_minor_mime',
 748+ 'oi_sha1' => 'img_sha1',
719749 ), array( 'img_name' => $this->getName() ), __METHOD__
720750 );
721751
@@ -733,6 +763,7 @@
734764 'img_user' => $wgUser->getID(),
735765 'img_user_text' => $wgUser->getName(),
736766 'img_metadata' => $this->metadata,
 767+ 'img_sha1' => $this->sha1
737768 ), array( /* WHERE */
738769 'img_name' => $this->getName()
739770 ), __METHOD__
@@ -792,22 +823,23 @@
793824 * @param integer $flags A bitwise combination of:
794825 * File::DELETE_SOURCE Delete the source file, i.e. move
795826 * rather than copy
796 - * @return The archive name on success or an empty string if it was a new
797 - * file, and a wikitext-formatted WikiError object on failure.
 827+ * @return FileRepoStatus object. On success, the value member contains the
 828+ * archive name, or an empty string if it was a new file.
798829 */
799830 function publish( $srcPath, $flags = 0 ) {
 831+ $this->lock();
800832 $dstRel = $this->getRel();
801833 $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
802834 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
803835 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
804836 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
805 - if ( WikiError::isError( $status ) ) {
806 - return $status;
807 - } elseif ( $status == 'new' ) {
808 - return '';
 837+ if ( $status->value == 'new' ) {
 838+ $status->value = '';
809839 } else {
810 - return $archiveName;
 840+ $status->value = $archiveName;
811841 }
 842+ $this->unlock();
 843+ return $status;
812844 }
813845
814846 /** getLinksTo inherited */
@@ -824,62 +856,34 @@
825857 * Cache purging is done; logging is caller's responsibility.
826858 *
827859 * @param $reason
828 - * @return true on success, false on some kind of failure
 860+ * @return FileRepoStatus object.
829861 */
830 - function delete( $reason, $suppress=false ) {
831 - $transaction = new FSTransaction();
832 - $urlArr = array( $this->getURL() );
 862+ function delete( $reason ) {
 863+ $this->lock();
 864+ $batch = new LocalFileDeleteBatch( $this, $reason );
 865+ $batch->addCurrent();
833866
834 - if( !FileStore::lock() ) {
835 - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
836 - return false;
 867+ # Get old version relative paths
 868+ $dbw = $this->repo->getMasterDB();
 869+ $result = $dbw->select( 'oldimage',
 870+ array( 'oi_archive_name' ),
 871+ array( 'oi_name' => $this->getName() ) );
 872+ while ( $row = $dbw->fetchObject( $result ) ) {
 873+ $batch->addOld( $row->oi_archive_name );
837874 }
 875+ $status = $batch->execute();
838876
839 - try {
840 - $dbw = $this->repo->getMasterDB();
841 - $dbw->begin();
842 -
843 - // Delete old versions
844 - $result = $dbw->select( 'oldimage',
845 - array( 'oi_archive_name' ),
846 - array( 'oi_name' => $this->getName() ) );
847 -
848 - while( $row = $dbw->fetchObject( $result ) ) {
849 - $oldName = $row->oi_archive_name;
850 -
851 - $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) );
852 -
853 - // We'll need to purge this URL from caches...
854 - $urlArr[] = $this->getArchiveUrl( $oldName );
855 - }
856 - $dbw->freeResult( $result );
857 -
858 - // And the current version...
859 - $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) );
860 -
861 - $dbw->immediateCommit();
862 - } catch( MWException $e ) {
863 - wfDebug( __METHOD__.": db error, rolling back file transactions\n" );
864 - $transaction->rollback();
865 - FileStore::unlock();
866 - throw $e;
 877+ if ( $status->ok ) {
 878+ // Update site_stats
 879+ $site_stats = $dbw->tableName( 'site_stats' );
 880+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
 881+ $this->purgeEverything();
867882 }
868883
869 - wfDebug( __METHOD__.": deleted db items, applying file transactions\n" );
870 - $transaction->commit();
871 - FileStore::unlock();
872 -
873 -
874 - // Update site_stats
875 - $site_stats = $dbw->tableName( 'site_stats' );
876 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
877 -
878 - $this->purgeEverything( $urlArr );
879 -
880 - return true;
 884+ $this->unlock();
 885+ return $status;
881886 }
882887
883 -
884888 /**
885889 * Delete an old version of the file.
886890 *
@@ -890,190 +894,22 @@
891895 *
892896 * @param $reason
893897 * @throws MWException or FSException on database or filestore failure
894 - * @return true on success, false on some kind of failure
 898+ * @return FileRepoStatus object.
895899 */
896 - function deleteOld( $archiveName, $reason, $suppress=false ) {
897 - $transaction = new FSTransaction();
898 - $urlArr = array();
899 -
900 - if( !FileStore::lock() ) {
901 - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
902 - return false;
 900+ function deleteOld( $archiveName, $reason ) {
 901+ $this->lock();
 902+ $batch = new LocalFileDeleteBatch( $this, $reason );
 903+ $batch->addOld( $archiveName );
 904+ $status = $batch->execute();
 905+ $this->unlock();
 906+ if ( $status->ok ) {
 907+ $this->purgeDescription();
 908+ $this->purgeHistory();
903909 }
904 -
905 - $transaction = new FSTransaction();
906 - try {
907 - $dbw = $this->repo->getMasterDB();
908 - $dbw->begin();
909 - $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) );
910 - $dbw->immediateCommit();
911 - } catch( MWException $e ) {
912 - wfDebug( __METHOD__.": db error, rolling back file transaction\n" );
913 - $transaction->rollback();
914 - FileStore::unlock();
915 - throw $e;
916 - }
917 -
918 - wfDebug( __METHOD__.": deleted db items, applying file transaction\n" );
919 - $transaction->commit();
920 - FileStore::unlock();
921 -
922 - $this->purgeDescription();
923 -
924 - // Squid purging
925 - global $wgUseSquid;
926 - if ( $wgUseSquid ) {
927 - $urlArr = array(
928 - $this->getArchiveUrl( $archiveName ),
929 - );
930 - wfPurgeSquidServers( $urlArr );
931 - }
932 - return true;
 910+ return $status;
933911 }
934912
935913 /**
936 - * Delete the current version of a file.
937 - * May throw a database error.
938 - * @return true on success, false on failure
939 - */
940 - private function prepareDeleteCurrent( $reason, $suppress=false ) {
941 - return $this->prepareDeleteVersion(
942 - $this->getFullPath(),
943 - $reason,
944 - 'image',
945 - array(
946 - 'fa_name' => 'img_name',
947 - 'fa_archive_name' => 'NULL',
948 - 'fa_size' => 'img_size',
949 - 'fa_width' => 'img_width',
950 - 'fa_height' => 'img_height',
951 - 'fa_metadata' => 'img_metadata',
952 - 'fa_bits' => 'img_bits',
953 - 'fa_media_type' => 'img_media_type',
954 - 'fa_major_mime' => 'img_major_mime',
955 - 'fa_minor_mime' => 'img_minor_mime',
956 - 'fa_description' => 'img_description',
957 - 'fa_user' => 'img_user',
958 - 'fa_user_text' => 'img_user_text',
959 - 'fa_timestamp' => 'img_timestamp' ),
960 - array( 'img_name' => $this->getName() ),
961 - $suppress,
962 - __METHOD__ );
963 - }
964 -
965 - /**
966 - * Delete a given older version of a file.
967 - * May throw a database error.
968 - * @return true on success, false on failure
969 - */
970 - private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) {
971 - $oldpath = $this->getArchivePath() .
972 - DIRECTORY_SEPARATOR . $archiveName;
973 - return $this->prepareDeleteVersion(
974 - $oldpath,
975 - $reason,
976 - 'oldimage',
977 - array(
978 - 'fa_name' => 'oi_name',
979 - 'fa_archive_name' => 'oi_archive_name',
980 - 'fa_size' => 'oi_size',
981 - 'fa_width' => 'oi_width',
982 - 'fa_height' => 'oi_height',
983 - 'fa_metadata' => 'NULL',
984 - 'fa_bits' => 'oi_bits',
985 - 'fa_media_type' => 'NULL',
986 - 'fa_major_mime' => 'NULL',
987 - 'fa_minor_mime' => 'NULL',
988 - 'fa_description' => 'oi_description',
989 - 'fa_user' => 'oi_user',
990 - 'fa_user_text' => 'oi_user_text',
991 - 'fa_timestamp' => 'oi_timestamp' ),
992 - array(
993 - 'oi_name' => $this->getName(),
994 - 'oi_archive_name' => $archiveName ),
995 - $suppress,
996 - __METHOD__ );
997 - }
998 -
999 - /**
1000 - * Do the dirty work of backing up an image row and its file
1001 - * (if $wgSaveDeletedFiles is on) and removing the originals.
1002 - *
1003 - * Must be run while the file store is locked and a database
1004 - * transaction is open to avoid race conditions.
1005 - *
1006 - * @return FSTransaction
1007 - */
1008 - private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) {
1009 - global $wgUser, $wgSaveDeletedFiles;
1010 -
1011 - // Dupe the file into the file store
1012 - if( file_exists( $path ) ) {
1013 - if( $wgSaveDeletedFiles ) {
1014 - $group = 'deleted';
1015 -
1016 - $store = FileStore::get( $group );
1017 - $key = FileStore::calculateKey( $path, $this->getExtension() );
1018 - $transaction = $store->insert( $key, $path,
1019 - FileStore::DELETE_ORIGINAL );
1020 - } else {
1021 - $group = null;
1022 - $key = null;
1023 - $transaction = FileStore::deleteFile( $path );
1024 - }
1025 - } else {
1026 - wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" );
1027 - $group = null;
1028 - $key = null;
1029 - $transaction = new FSTransaction(); // empty
1030 - }
1031 -
1032 - if( $transaction === false ) {
1033 - // Fail to restore?
1034 - wfDebug( __METHOD__.": import to file store failed, aborting\n" );
1035 - throw new MWException( "Could not archive and delete file $path" );
1036 - return false;
1037 - }
1038 -
1039 - // Bitfields to further supress the file content
1040 - // Note that currently, live files are stored elsewhere
1041 - // and cannot be partially deleted
1042 - $bitfield = 0;
1043 - if ( $suppress ) {
1044 - $bitfield |= self::DELETED_FILE;
1045 - $bitfield |= self::DELETED_COMMENT;
1046 - $bitfield |= self::DELETED_USER;
1047 - $bitfield |= self::DELETED_RESTRICTED;
1048 - }
1049 -
1050 - $dbw = $this->repo->getMasterDB();
1051 - $storageMap = array(
1052 - 'fa_storage_group' => $dbw->addQuotes( $group ),
1053 - 'fa_storage_key' => $dbw->addQuotes( $key ),
1054 -
1055 - 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ),
1056 - 'fa_deleted_timestamp' => $dbw->timestamp(),
1057 - 'fa_deleted_reason' => $dbw->addQuotes( $reason ),
1058 - 'fa_deleted' => $bitfield);
1059 - $allFields = array_merge( $storageMap, $fieldMap );
1060 -
1061 - try {
1062 - if( $wgSaveDeletedFiles ) {
1063 - $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
1064 - }
1065 - $dbw->delete( $table, $where, $fname );
1066 - } catch( DBQueryError $e ) {
1067 - // Something went horribly wrong!
1068 - // Leave the file as it was...
1069 - wfDebug( __METHOD__.": database error, rolling back file transaction\n" );
1070 - $transaction->rollback();
1071 - throw $e;
1072 - }
1073 -
1074 - return $transaction;
1075 - }
1076 -
1077 - /**
1078914 * Restore all or specified deleted revisions to the given file.
1079915 * Permissions and logging are left to the caller.
1080916 *
@@ -1081,202 +917,25 @@
1082918 *
1083919 * @param $versions set of record ids of deleted items to restore,
1084920 * or empty to restore all revisions.
1085 - * @return the number of file revisions restored if successful,
1086 - * or false on failure
 921+ * @return FileRepoStatus
1087922 */
1088 - function restore( $versions=array(), $Unsuppress=false ) {
1089 - global $wgUser;
1090 -
1091 - if( !FileStore::lock() ) {
1092 - wfDebug( __METHOD__." could not acquire filestore lock\n" );
1093 - return false;
 923+ function restore( $versions = array(), $unsuppress = false ) {
 924+ $batch = new LocalFileRestoreBatch( $this );
 925+ if ( !$versions ) {
 926+ $batch->addAll();
 927+ } else {
 928+ $batch->addIds( $versions );
1094929 }
1095 -
1096 - $transaction = new FSTransaction();
1097 - try {
1098 - $dbw = $this->repo->getMasterDB();
1099 - $dbw->begin();
1100 -
1101 - // Re-confirm whether this file presently exists;
1102 - // if no we'll need to create an file record for the
1103 - // first item we restore.
1104 - $exists = $dbw->selectField( 'image', '1',
1105 - array( 'img_name' => $this->getName() ),
1106 - __METHOD__ );
1107 -
1108 - // Fetch all or selected archived revisions for the file,
1109 - // sorted from the most recent to the oldest.
1110 - $conditions = array( 'fa_name' => $this->getName() );
1111 - if( $versions ) {
1112 - $conditions['fa_id'] = $versions;
1113 - }
1114 -
1115 - $result = $dbw->select( 'filearchive', '*',
1116 - $conditions,
1117 - __METHOD__,
1118 - array( 'ORDER BY' => 'fa_timestamp DESC' ) );
1119 -
1120 - if( $dbw->numRows( $result ) < count( $versions ) ) {
1121 - // There's some kind of conflict or confusion;
1122 - // we can't restore everything we were asked to.
1123 - wfDebug( __METHOD__.": couldn't find requested items\n" );
1124 - $dbw->rollback();
1125 - FileStore::unlock();
1126 - return false;
1127 - }
1128 -
1129 - if( $dbw->numRows( $result ) == 0 ) {
1130 - // Nothing to do.
1131 - wfDebug( __METHOD__.": nothing to do\n" );
1132 - $dbw->rollback();
1133 - FileStore::unlock();
1134 - return true;
1135 - }
1136 -
1137 - $revisions = 0;
1138 - while( $row = $dbw->fetchObject( $result ) ) {
1139 - if ( $Unsuppress ) {
1140 - // Currently, fa_deleted flags fall off upon restore, lets be careful about this
1141 - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
1142 - // Skip restoring file revisions that the user cannot restore
1143 - continue;
1144 - }
1145 - $revisions++;
1146 - $store = FileStore::get( $row->fa_storage_group );
1147 - if( !$store ) {
1148 - wfDebug( __METHOD__.": skipping row with no file.\n" );
1149 - continue;
1150 - }
1151 -
1152 - $restoredImage = new self( Title::makeTitle( NS_IMAGE, $row->fa_name ), $this->repo );
1153 -
1154 - if( $revisions == 1 && !$exists ) {
1155 - $destPath = $restoredImage->getFullPath();
1156 - $destDir = dirname( $destPath );
1157 - if ( !is_dir( $destDir ) ) {
1158 - wfMkdirParents( $destDir );
1159 - }
1160 -
1161 - // We may have to fill in data if this was originally
1162 - // an archived file revision.
1163 - if( is_null( $row->fa_metadata ) ) {
1164 - $tempFile = $store->filePath( $row->fa_storage_key );
1165 -
1166 - $magic = MimeMagic::singleton();
1167 - $mime = $magic->guessMimeType( $tempFile, true );
1168 - $media_type = $magic->getMediaType( $tempFile, $mime );
1169 - list( $major_mime, $minor_mime ) = self::splitMime( $mime );
1170 - $handler = MediaHandler::getHandler( $mime );
1171 - if ( $handler ) {
1172 - $metadata = $handler->getMetadata( false, $tempFile );
1173 - } else {
1174 - $metadata = '';
1175 - }
1176 - } else {
1177 - $metadata = $row->fa_metadata;
1178 - $major_mime = $row->fa_major_mime;
1179 - $minor_mime = $row->fa_minor_mime;
1180 - $media_type = $row->fa_media_type;
1181 - }
1182 -
1183 - $table = 'image';
1184 - $fields = array(
1185 - 'img_name' => $row->fa_name,
1186 - 'img_size' => $row->fa_size,
1187 - 'img_width' => $row->fa_width,
1188 - 'img_height' => $row->fa_height,
1189 - 'img_metadata' => $metadata,
1190 - 'img_bits' => $row->fa_bits,
1191 - 'img_media_type' => $media_type,
1192 - 'img_major_mime' => $major_mime,
1193 - 'img_minor_mime' => $minor_mime,
1194 - 'img_description' => $row->fa_description,
1195 - 'img_user' => $row->fa_user,
1196 - 'img_user_text' => $row->fa_user_text,
1197 - 'img_timestamp' => $row->fa_timestamp );
1198 - } else {
1199 - $archiveName = $row->fa_archive_name;
1200 - if( $archiveName == '' ) {
1201 - // This was originally a current version; we
1202 - // have to devise a new archive name for it.
1203 - // Format is <timestamp of archiving>!<name>
1204 - $archiveName =
1205 - wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) .
1206 - '!' . $row->fa_name;
1207 - }
1208 - $destDir = $restoredImage->getArchivePath();
1209 - if ( !is_dir( $destDir ) ) {
1210 - wfMkdirParents( $destDir );
1211 - }
1212 - $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName;
1213 -
1214 - $table = 'oldimage';
1215 - $fields = array(
1216 - 'oi_name' => $row->fa_name,
1217 - 'oi_archive_name' => $archiveName,
1218 - 'oi_size' => $row->fa_size,
1219 - 'oi_width' => $row->fa_width,
1220 - 'oi_height' => $row->fa_height,
1221 - 'oi_bits' => $row->fa_bits,
1222 - 'oi_description' => $row->fa_description,
1223 - 'oi_user' => $row->fa_user,
1224 - 'oi_user_text' => $row->fa_user_text,
1225 - 'oi_timestamp' => $row->fa_timestamp );
1226 - }
1227 -
1228 - $dbw->insert( $table, $fields, __METHOD__ );
1229 - // @todo this delete is not totally safe, potentially
1230 - $dbw->delete( 'filearchive',
1231 - array( 'fa_id' => $row->fa_id ),
1232 - __METHOD__ );
1233 -
1234 - // Check if any other stored revisions use this file;
1235 - // if so, we shouldn't remove the file from the deletion
1236 - // archives so they will still work.
1237 - $useCount = $dbw->selectField( 'filearchive',
1238 - 'COUNT(*)',
1239 - array(
1240 - 'fa_storage_group' => $row->fa_storage_group,
1241 - 'fa_storage_key' => $row->fa_storage_key ),
1242 - __METHOD__ );
1243 - if( $useCount == 0 ) {
1244 - wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
1245 - $flags = FileStore::DELETE_ORIGINAL;
1246 - } else {
1247 - $flags = 0;
1248 - }
1249 -
1250 - $transaction->add( $store->export( $row->fa_storage_key,
1251 - $destPath, $flags ) );
1252 - }
1253 -
1254 - $dbw->immediateCommit();
1255 - } catch( MWException $e ) {
1256 - wfDebug( __METHOD__." caught error, aborting\n" );
1257 - $transaction->rollback();
1258 - $dbw->rollback();
1259 - throw $e;
 930+ $status = $batch->execute();
 931+ if ( !$status->ok ) {
 932+ return $status;
1260933 }
1261934
1262 - $transaction->commit();
1263 - FileStore::unlock();
1264 -
1265 - if( $revisions > 0 ) {
1266 - if( !$exists ) {
1267 - wfDebug( __METHOD__." restored $revisions items, creating a new current\n" );
1268 -
1269 - // Update site_stats
1270 - $site_stats = $dbw->tableName( 'site_stats' );
1271 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1272 -
1273 - $this->purgeEverything();
1274 - } else {
1275 - wfDebug( __METHOD__." restored $revisions as archived versions\n" );
1276 - $this->purgeDescription();
1277 - }
1278 - }
1279 -
1280 - return $revisions;
 935+ $cleanupStatus = $batch->cleanup();
 936+ $cleanupStatus->successCount = 0;
 937+ $cleanupStatus->failCount = 0;
 938+ $status->merge( $cleanupStatus );
 939+ return $status;
1281940 }
1282941
1283942 /** isMultipage inherited */
@@ -1310,8 +969,52 @@
1311970 $this->load();
1312971 return $this->timestamp;
1313972 }
 973+
 974+ function getSha1() {
 975+ $this->load();
 976+ return $this->sha1;
 977+ }
 978+
 979+ /**
 980+ * Start a transaction and lock the image for update
 981+ * Increments a reference counter if the lock is already held
 982+ * @return boolean True if the image exists, false otherwise
 983+ */
 984+ function lock() {
 985+ $dbw = $this->repo->getMasterDB();
 986+ if ( !$this->locked ) {
 987+ $dbw->begin();
 988+ $this->locked++;
 989+ }
 990+ return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
 991+ }
 992+
 993+ /**
 994+ * Decrement the lock reference count. If the reference count is reduced to zero, commits
 995+ * the transaction and thereby releases the image lock.
 996+ */
 997+ function unlock() {
 998+ if ( $this->locked ) {
 999+ --$this->locked;
 1000+ if ( !$this->locked ) {
 1001+ $dbw = $this->repo->getMasterDB();
 1002+ $dbw->commit();
 1003+ }
 1004+ }
 1005+ }
 1006+
 1007+ /**
 1008+ * Roll back the DB transaction and mark the image unlocked
 1009+ */
 1010+ function unlockAndRollback() {
 1011+ $this->locked = false;
 1012+ $dbw = $this->repo->getMasterDB();
 1013+ $dbw->rollback();
 1014+ }
13141015 } // LocalFile class
13151016
 1017+#------------------------------------------------------------------------------
 1018+
13161019 /**
13171020 * Backwards compatibility class
13181021 */
@@ -1379,12 +1082,467 @@
13801083 }
13811084 }
13821085
 1086+#------------------------------------------------------------------------------
 1087+
13831088 /**
1384 - * Aliases for backwards compatibility with 1.6
 1089+ * Helper class for file deletion
13851090 */
1386 -define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
1387 -define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
1388 -define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
1389 -define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
 1091+class LocalFileDeleteBatch {
 1092+ var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
 1093+ var $status;
13901094
 1095+ function __construct( File $file, $reason = '' ) {
 1096+ $this->file = $file;
 1097+ $this->reason = $reason;
 1098+ $this->status = $file->repo->newGood();
 1099+ }
13911100
 1101+ function addCurrent() {
 1102+ $this->srcRels['.'] = $this->file->getRel();
 1103+ }
 1104+
 1105+ function addOld( $oldName ) {
 1106+ $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
 1107+ $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
 1108+ }
 1109+
 1110+ function getOldRels() {
 1111+ if ( !isset( $this->srcRels['.'] ) ) {
 1112+ $oldRels =& $this->srcRels;
 1113+ $deleteCurrent = false;
 1114+ } else {
 1115+ $oldRels = $this->srcRels;
 1116+ unset( $oldRels['.'] );
 1117+ $deleteCurrent = true;
 1118+ }
 1119+ return array( $oldRels, $deleteCurrent );
 1120+ }
 1121+
 1122+ /*protected*/ function getHashes() {
 1123+ $hashes = array();
 1124+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1125+ if ( $deleteCurrent ) {
 1126+ $hashes['.'] = $this->file->getSha1();
 1127+ }
 1128+ if ( count( $oldRels ) ) {
 1129+ $dbw = $this->file->repo->getMasterDB();
 1130+ $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ),
 1131+ 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
 1132+ __METHOD__ );
 1133+ while ( $row = $dbw->fetchObject( $res ) ) {
 1134+ if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
 1135+ // Get the hash from the file
 1136+ $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
 1137+ $props = $this->file->repo->getFileProps( $oldUrl );
 1138+ if ( $props['fileExists'] ) {
 1139+ // Upgrade the oldimage row
 1140+ $dbw->update( 'oldimage',
 1141+ array( 'oi_sha1' => $props['sha1'] ),
 1142+ array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
 1143+ __METHOD__ );
 1144+ $hashes[$row->oi_archive_name] = $props['sha1'];
 1145+ } else {
 1146+ $hashes[$row->oi_archive_name] = false;
 1147+ }
 1148+ } else {
 1149+ $hashes[$row->oi_archive_name] = $row->oi_sha1;
 1150+ }
 1151+ }
 1152+ }
 1153+ $missing = array_diff_key( $this->srcRels, $hashes );
 1154+ foreach ( $missing as $name => $rel ) {
 1155+ $this->status->error( 'filedelete-old-unregistered', $name );
 1156+ }
 1157+ foreach ( $hashes as $name => $hash ) {
 1158+ if ( !$hash ) {
 1159+ $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
 1160+ unset( $hashes[$name] );
 1161+ }
 1162+ }
 1163+
 1164+ return $hashes;
 1165+ }
 1166+
 1167+ function doDBInserts() {
 1168+ global $wgUser;
 1169+ $dbw = $this->file->repo->getMasterDB();
 1170+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
 1171+ $encUserId = $dbw->addQuotes( $wgUser->getId() );
 1172+ $encReason = $dbw->addQuotes( $this->reason );
 1173+ $encGroup = $dbw->addQuotes( 'deleted' );
 1174+ $ext = $this->file->getExtension();
 1175+ $dotExt = $ext === '' ? '' : ".$ext";
 1176+ $encExt = $dbw->addQuotes( $dotExt );
 1177+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1178+
 1179+ if ( $deleteCurrent ) {
 1180+ $where = array( 'img_name' => $this->file->getName() );
 1181+ $dbw->insertSelect( 'filearchive', 'image',
 1182+ array(
 1183+ 'fa_storage_group' => $encGroup,
 1184+ 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))",
 1185+
 1186+ 'fa_deleted_user' => $encUserId,
 1187+ 'fa_deleted_timestamp' => $encTimestamp,
 1188+ 'fa_deleted_reason' => $encReason,
 1189+ 'fa_deleted' => 0,
 1190+
 1191+ 'fa_name' => 'img_name',
 1192+ 'fa_archive_name' => 'NULL',
 1193+ 'fa_size' => 'img_size',
 1194+ 'fa_width' => 'img_width',
 1195+ 'fa_height' => 'img_height',
 1196+ 'fa_metadata' => 'img_metadata',
 1197+ 'fa_bits' => 'img_bits',
 1198+ 'fa_media_type' => 'img_media_type',
 1199+ 'fa_major_mime' => 'img_major_mime',
 1200+ 'fa_minor_mime' => 'img_minor_mime',
 1201+ 'fa_description' => 'img_description',
 1202+ 'fa_user' => 'img_user',
 1203+ 'fa_user_text' => 'img_user_text',
 1204+ 'fa_timestamp' => 'img_timestamp'
 1205+ ), $where, __METHOD__ );
 1206+ }
 1207+
 1208+ if ( count( $oldRels ) ) {
 1209+ $where = array(
 1210+ 'oi_name' => $this->file->getName(),
 1211+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
 1212+
 1213+ $dbw->insertSelect( 'filearchive', 'oldimage',
 1214+ array(
 1215+ 'fa_storage_group' => $encGroup,
 1216+ 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))",
 1217+
 1218+ 'fa_deleted_user' => $encUserId,
 1219+ 'fa_deleted_timestamp' => $encTimestamp,
 1220+ 'fa_deleted_reason' => $encReason,
 1221+ 'fa_deleted' => 0,
 1222+
 1223+ 'fa_name' => 'oi_name',
 1224+ 'fa_archive_name' => 'oi_archive_name',
 1225+ 'fa_size' => 'oi_size',
 1226+ 'fa_width' => 'oi_width',
 1227+ 'fa_height' => 'oi_height',
 1228+ 'fa_metadata' => 'oi_metadata',
 1229+ 'fa_bits' => 'oi_bits',
 1230+ 'fa_media_type' => 'oi_media_type',
 1231+ 'fa_major_mime' => 'oi_major_mime',
 1232+ 'fa_minor_mime' => 'oi_minor_mime',
 1233+ 'fa_description' => 'oi_description',
 1234+ 'fa_user' => 'oi_user',
 1235+ 'fa_user_text' => 'oi_user_text',
 1236+ 'fa_timestamp' => 'oi_timestamp'
 1237+ ), $where, __METHOD__ );
 1238+ }
 1239+ }
 1240+
 1241+ function doDBDeletes() {
 1242+ $dbw = $this->file->repo->getMasterDB();
 1243+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1244+ if ( $deleteCurrent ) {
 1245+ $where = array( 'img_name' => $this->file->getName() );
 1246+ $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
 1247+ }
 1248+ if ( count( $oldRels ) ) {
 1249+ $dbw->delete( 'oldimage',
 1250+ array(
 1251+ 'oi_name' => $this->file->getName(),
 1252+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
 1253+ ), __METHOD__ );
 1254+ }
 1255+ }
 1256+
 1257+ /**
 1258+ * Run the transaction
 1259+ */
 1260+ function execute() {
 1261+ global $wgUser, $wgUseSquid;
 1262+ wfProfileIn( __METHOD__ );
 1263+
 1264+ $this->file->lock();
 1265+
 1266+ // Prepare deletion batch
 1267+ $hashes = $this->getHashes();
 1268+ $this->deletionBatch = array();
 1269+ $ext = $this->file->getExtension();
 1270+ $dotExt = $ext === '' ? '' : ".$ext";
 1271+ foreach ( $this->srcRels as $name => $srcRel ) {
 1272+ // Skip files that have no hash (missing source)
 1273+ if ( isset( $hashes[$name] ) ) {
 1274+ $hash = $hashes[$name];
 1275+ $key = $hash . $dotExt;
 1276+ $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
 1277+ $this->deletionBatch[$name] = array( $srcRel, $dstRel );
 1278+ }
 1279+ }
 1280+
 1281+ // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
 1282+ // We acquire this lock by running the inserts now, before the file operations.
 1283+ //
 1284+ // This potentially has poor lock contention characteristics -- an alternative
 1285+ // scheme would be to insert stub filearchive entries with no fa_name and commit
 1286+ // them in a separate transaction, then run the file ops, then update the fa_name fields.
 1287+ $this->doDBInserts();
 1288+
 1289+ // Execute the file deletion batch
 1290+ $status = $this->file->repo->deleteBatch( $this->deletionBatch );
 1291+ if ( !$status->isGood() ) {
 1292+ $this->status->merge( $status );
 1293+ }
 1294+
 1295+ if ( !$this->status->ok ) {
 1296+ // Critical file deletion error
 1297+ // Roll back inserts, release lock and abort
 1298+ // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
 1299+ $this->file->unlockAndRollback();
 1300+ return $this->status;
 1301+ }
 1302+
 1303+ // Purge squid
 1304+ if ( $wgUseSquid ) {
 1305+ $urls = array();
 1306+ foreach ( $this->srcRels as $srcRel ) {
 1307+ $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
 1308+ $urls[] = $this->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
 1309+ }
 1310+ SquidUpdate::purge( $urls );
 1311+ }
 1312+
 1313+ // Delete image/oldimage rows
 1314+ $this->doDBDeletes();
 1315+
 1316+ // Commit and return
 1317+ $this->file->unlock();
 1318+ wfProfileOut( __METHOD__ );
 1319+ return $this->status;
 1320+ }
 1321+}
 1322+
 1323+#------------------------------------------------------------------------------
 1324+
 1325+/**
 1326+ * Helper class for file undeletion
 1327+ */
 1328+class LocalFileRestoreBatch {
 1329+ var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
 1330+
 1331+ function __construct( File $file ) {
 1332+ $this->file = $file;
 1333+ $this->cleanupBatch = $this->ids = array();
 1334+ $this->ids = array();
 1335+ }
 1336+
 1337+ /**
 1338+ * Add a file by ID
 1339+ */
 1340+ function addId( $fa_id ) {
 1341+ $this->ids[] = $fa_id;
 1342+ }
 1343+
 1344+ /**
 1345+ * Add a whole lot of files by ID
 1346+ */
 1347+ function addIds( $ids ) {
 1348+ $this->ids = array_merge( $this->ids, $ids );
 1349+ }
 1350+
 1351+ /**
 1352+ * Add all revisions of the file
 1353+ */
 1354+ function addAll() {
 1355+ $this->all = true;
 1356+ }
 1357+
 1358+ /**
 1359+ * Run the transaction, except the cleanup batch.
 1360+ * The cleanup batch should be run in a separate transaction, because it locks different
 1361+ * rows and there's no need to keep the image row locked while it's acquiring those locks
 1362+ * The caller may have its own transaction open.
 1363+ * So we save the batch and let the caller call cleanup()
 1364+ */
 1365+ function execute() {
 1366+ global $wgUser, $wgLang;
 1367+ if ( !$this->all && !$this->ids ) {
 1368+ // Do nothing
 1369+ return $this->file->repo->newGood();
 1370+ }
 1371+
 1372+ $exists = $this->file->lock();
 1373+ $dbw = $this->file->repo->getMasterDB();
 1374+ $status = $this->file->repo->newGood();
 1375+
 1376+ // Fetch all or selected archived revisions for the file,
 1377+ // sorted from the most recent to the oldest.
 1378+ $conditions = array( 'fa_name' => $this->file->getName() );
 1379+ if( !$this->all ) {
 1380+ $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
 1381+ }
 1382+
 1383+ $result = $dbw->select( 'filearchive', '*',
 1384+ $conditions,
 1385+ __METHOD__,
 1386+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
 1387+
 1388+ $idsPresent = array();
 1389+ $storeBatch = array();
 1390+ $insertBatch = array();
 1391+ $insertCurrent = false;
 1392+ $deleteIds = array();
 1393+ $first = true;
 1394+ $archiveNames = array();
 1395+ while( $row = $dbw->fetchObject( $result ) ) {
 1396+ $idsPresent[] = $row->fa_id;
 1397+ if ( $this->unsuppress ) {
 1398+ // Currently, fa_deleted flags fall off upon restore, lets be careful about this
 1399+ } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
 1400+ // Skip restoring file revisions that the user cannot restore
 1401+ continue;
 1402+ }
 1403+ if ( $row->fa_name != $this->file->getName() ) {
 1404+ $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
 1405+ $status->failCount++;
 1406+ continue;
 1407+ }
 1408+ if ( $row->fa_storage_key == '' ) {
 1409+ // Revision was missing pre-deletion
 1410+ $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
 1411+ $status->failCount++;
 1412+ continue;
 1413+ }
 1414+
 1415+ $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
 1416+ $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
 1417+
 1418+ $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
 1419+ # Fix leading zero
 1420+ if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
 1421+ $sha1 = substr( $sha1, 1 );
 1422+ }
 1423+
 1424+ if ( $first && !$exists ) {
 1425+ // This revision will be published as the new current version
 1426+ $destRel = $this->file->getRel();
 1427+ $info = $this->file->repo->getFileProps( $deletedUrl );
 1428+ $insertCurrent = array(
 1429+ 'img_name' => $row->fa_name,
 1430+ 'img_size' => $row->fa_size,
 1431+ 'img_width' => $row->fa_width,
 1432+ 'img_height' => $row->fa_height,
 1433+ 'img_metadata' => $row->fa_metadata,
 1434+ 'img_bits' => $row->fa_bits,
 1435+ 'img_media_type' => $row->fa_media_type,
 1436+ 'img_major_mime' => $row->fa_major_mime,
 1437+ 'img_minor_mime' => $row->fa_minor_mime,
 1438+ 'img_description' => $row->fa_description,
 1439+ 'img_user' => $row->fa_user,
 1440+ 'img_user_text' => $row->fa_user_text,
 1441+ 'img_timestamp' => $row->fa_timestamp,
 1442+ 'img_sha1' => $sha1);
 1443+ } else {
 1444+ $archiveName = $row->fa_archive_name;
 1445+ if( $archiveName == '' ) {
 1446+ // This was originally a current version; we
 1447+ // have to devise a new archive name for it.
 1448+ // Format is <timestamp of archiving>!<name>
 1449+ $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
 1450+ do {
 1451+ $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
 1452+ $timestamp++;
 1453+ } while ( isset( $archiveNames[$archiveName] ) );
 1454+ }
 1455+ $archiveNames[$archiveName] = true;
 1456+ $destRel = $this->file->getArchiveRel( $archiveName );
 1457+ $insertBatch[] = array(
 1458+ 'oi_name' => $row->fa_name,
 1459+ 'oi_archive_name' => $archiveName,
 1460+ 'oi_size' => $row->fa_size,
 1461+ 'oi_width' => $row->fa_width,
 1462+ 'oi_height' => $row->fa_height,
 1463+ 'oi_bits' => $row->fa_bits,
 1464+ 'oi_description' => $row->fa_description,
 1465+ 'oi_user' => $row->fa_user,
 1466+ 'oi_user_text' => $row->fa_user_text,
 1467+ 'oi_timestamp' => $row->fa_timestamp,
 1468+ 'oi_metadata' => $row->fa_metadata,
 1469+ 'oi_media_type' => $row->fa_media_type,
 1470+ 'oi_major_mime' => $row->fa_major_mime,
 1471+ 'oi_minor_mime' => $row->fa_minor_mime,
 1472+ 'oi_deleted' => $row->fa_deleted,
 1473+ 'oi_sha1' => $sha1 );
 1474+ }
 1475+
 1476+ $deleteIds[] = $row->fa_id;
 1477+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
 1478+ $this->cleanupBatch[] = $row->fa_storage_key;
 1479+ $first = false;
 1480+ }
 1481+ unset( $result );
 1482+
 1483+ // Add a warning to the status object for missing IDs
 1484+ $missingIds = array_diff( $this->ids, $idsPresent );
 1485+ foreach ( $missingIds as $id ) {
 1486+ $status->error( 'undelete-missing-filearchive', $id );
 1487+ }
 1488+
 1489+ // Run the store batch
 1490+ // Use the OVERWRITE_SAME flag to smooth over a common error
 1491+ $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
 1492+ $status->merge( $storeStatus );
 1493+
 1494+ if ( !$status->ok ) {
 1495+ // Store batch returned a critical error -- this usually means nothing was stored
 1496+ // Stop now and return an error
 1497+ $this->file->unlock();
 1498+ return $status;
 1499+ }
 1500+
 1501+ // Run the DB updates
 1502+ // Because we have locked the image row, key conflicts should be rare.
 1503+ // If they do occur, we can roll back the transaction at this time with
 1504+ // no data loss, but leaving unregistered files scattered throughout the
 1505+ // public zone.
 1506+ // This is not ideal, which is why it's important to lock the image row.
 1507+ if ( $insertCurrent ) {
 1508+ $dbw->insert( 'image', $insertCurrent, __METHOD__ );
 1509+ }
 1510+ if ( $insertBatch ) {
 1511+ $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
 1512+ }
 1513+ if ( $deleteIds ) {
 1514+ $dbw->delete( 'filearchive',
 1515+ array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
 1516+ __METHOD__ );
 1517+ }
 1518+
 1519+ if( $status->successCount > 0 ) {
 1520+ if( !$exists ) {
 1521+ wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" );
 1522+
 1523+ // Update site_stats
 1524+ $site_stats = $dbw->tableName( 'site_stats' );
 1525+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
 1526+
 1527+ $this->file->purgeEverything();
 1528+ } else {
 1529+ wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" );
 1530+ $this->file->purgeDescription();
 1531+ $this->file->purgeHistory();
 1532+ }
 1533+ }
 1534+ $this->file->unlock();
 1535+ return $status;
 1536+ }
 1537+
 1538+ /**
 1539+ * Delete unused files in the deleted zone.
 1540+ * This should be called from outside the transaction in which execute() was called.
 1541+ */
 1542+ function cleanup() {
 1543+ if ( !$this->cleanupBatch ) {
 1544+ return $this->file->repo->newGood();
 1545+ }
 1546+ $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
 1547+ return $status;
 1548+ }
 1549+}
Index: branches/liquidthreads/includes/filerepo/FSRepo.php
@@ -6,9 +6,10 @@
77 */
88
99 class FSRepo extends FileRepo {
10 - var $directory, $url, $hashLevels;
 10+ var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
1111 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
1212 var $oldFileFactory = false;
 13+ var $pathDisclosureProtection = 'simple';
1314
1415 function __construct( $info ) {
1516 parent::__construct( $info );
@@ -16,7 +17,12 @@
1718 // Required settings
1819 $this->directory = $info['directory'];
1920 $this->url = $info['url'];
20 - $this->hashLevels = $info['hashLevels'];
 21+
 22+ // Optional settings
 23+ $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
 24+ $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
 25+ $info['deletedHashLevels'] : $this->hashLevels;
 26+ $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
2127 }
2228
2329 /**
@@ -50,7 +56,7 @@
5157 case 'temp':
5258 return "{$this->directory}/temp";
5359 case 'deleted':
54 - return $GLOBALS['wgFileStore']['deleted']['directory'];
 60+ return $this->deletedDir;
5561 default:
5662 return false;
5763 }
@@ -66,7 +72,7 @@
6773 case 'temp':
6874 return "{$this->url}/temp";
6975 case 'deleted':
70 - return $GLOBALS['wgFileStore']['deleted']['url'];
 76+ return false; // no public URL
7177 default:
7278 return false;
7379 }
@@ -109,47 +115,108 @@
110116 }
111117
112118 /**
113 - * Store a file to a given destination.
 119+ * Store a batch of files
 120+ *
 121+ * @param array $triplets (src,zone,dest) triplets as per store()
 122+ * @param integer $flags Bitwise combination of the following flags:
 123+ * self::DELETE_SOURCE Delete the source file after upload
 124+ * self::OVERWRITE Overwrite an existing destination file instead of failing
 125+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
 126+ * same contents as the source
114127 */
115 - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
 128+ function storeBatch( $triplets, $flags = 0 ) {
116129 if ( !is_writable( $this->directory ) ) {
117 - return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
 130+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
118131 }
119 - $root = $this->getZonePath( $dstZone );
120 - if ( !$root ) {
121 - throw new MWException( "Invalid zone: $dstZone" );
 132+ $status = $this->newGood();
 133+ foreach ( $triplets as $i => $triplet ) {
 134+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 135+
 136+ $root = $this->getZonePath( $dstZone );
 137+ if ( !$root ) {
 138+ throw new MWException( "Invalid zone: $dstZone" );
 139+ }
 140+ if ( !$this->validateFilename( $dstRel ) ) {
 141+ throw new MWException( 'Validation error in $dstRel' );
 142+ }
 143+ $dstPath = "$root/$dstRel";
 144+ $dstDir = dirname( $dstPath );
 145+
 146+ if ( !is_dir( $dstDir ) ) {
 147+ if ( !wfMkdirParents( $dstDir ) ) {
 148+ return $this->newFatal( 'directorycreateerror', $dstDir );
 149+ }
 150+ // In the deleted zone, seed new directories with a blank
 151+ // index.html, to prevent crawling
 152+ if ( $dstZone == 'deleted' ) {
 153+ file_put_contents( "$dstDir/index.html", '' );
 154+ }
 155+ }
 156+
 157+ if ( self::isVirtualUrl( $srcPath ) ) {
 158+ $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
 159+ }
 160+ if ( !is_file( $srcPath ) ) {
 161+ // Make a list of files that don't exist for return to the caller
 162+ $status->fatal( 'filenotfound', $srcPath );
 163+ continue;
 164+ }
 165+ if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) {
 166+ if ( $flags & self::OVERWRITE_SAME ) {
 167+ $hashSource = sha1_file( $srcPath );
 168+ $hashDest = sha1_file( $dstPath );
 169+ if ( $hashSource != $hashDest ) {
 170+ $status->fatal( 'fileexists', $dstPath );
 171+ }
 172+ } else {
 173+ $status->fatal( 'fileexists', $dstPath );
 174+ }
 175+ }
122176 }
123 - $dstPath = "$root/$dstRel";
124177
125 - if ( !is_dir( dirname( $dstPath ) ) ) {
126 - wfMkdirParents( dirname( $dstPath ) );
 178+ $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
 179+
 180+ // Abort now on failure
 181+ if ( !$status->ok ) {
 182+ return $status;
127183 }
128 -
129 - if ( self::isVirtualUrl( $srcPath ) ) {
130 - $srcPath = $this->resolveVirtualUrl( $srcPath );
131 - }
132184
133 - if ( $flags & self::DELETE_SOURCE ) {
134 - if ( !rename( $srcPath, $dstPath ) ) {
135 - return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
136 - wfEscapeWikiText( $dstPath ) );
 185+ foreach ( $triplets as $triplet ) {
 186+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 187+ $root = $this->getZonePath( $dstZone );
 188+ $dstPath = "$root/$dstRel";
 189+ $good = true;
 190+
 191+ if ( $flags & self::DELETE_SOURCE ) {
 192+ if ( $deleteDest ) {
 193+ unlink( $dstPath );
 194+ }
 195+ if ( !rename( $srcPath, $dstPath ) ) {
 196+ $status->error( 'filerenameerror', $srcPath, $dstPath );
 197+ $good = false;
 198+ }
 199+ } else {
 200+ if ( !copy( $srcPath, $dstPath ) ) {
 201+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 202+ $good = false;
 203+ }
137204 }
138 - } else {
139 - if ( !copy( $srcPath, $dstPath ) ) {
140 - return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
141 - wfEscapeWikiText( $dstPath ) );
 205+ if ( $good ) {
 206+ chmod( $dstPath, 0644 );
 207+ $status->successCount++;
 208+ } else {
 209+ $status->failCount++;
142210 }
143211 }
144 - chmod( $dstPath, 0644 );
145 - return true;
 212+ return $status;
146213 }
147214
148215 /**
149216 * Pick a random name in the temp zone and store a file to it.
150 - * Returns the URL, or a WikiError on failure.
151217 * @param string $originalName The base name of the file as specified
152218 * by the user. The file extension will be maintained.
153219 * @param string $srcPath The current location of the file.
 220+ * @return FileRepoStatus object with the URL in the value.
154221 */
155222 function storeTemp( $originalName, $srcPath ) {
156223 $date = gmdate( "YmdHis" );
@@ -158,11 +225,8 @@
159226 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
160227
161228 $result = $this->store( $srcPath, 'temp', $dstRel );
162 - if ( WikiError::isError( $result ) ) {
163 - return $result;
164 - } else {
165 - return $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
166 - }
 229+ $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
 230+ return $result;
167231 }
168232
169233 /**
@@ -183,82 +247,190 @@
184248 return $success;
185249 }
186250
187 -
188251 /**
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.
 252+ * Publish a batch of files
 253+ * @param array $triplets (source,dest,archive) triplets as per publish()
196254 * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
197 - * that the source file should be deleted if possible
 255+ * that the source files should be deleted if possible
198256 */
199 - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
 257+ function publishBatch( $triplets, $flags = 0 ) {
 258+ // Perform initial checks
200259 if ( !is_writable( $this->directory ) ) {
201 - return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
 260+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
202261 }
203 - if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
204 - $srcPath = $this->resolveVirtualUrl( $srcPath );
 262+ $status = $this->newGood( array() );
 263+ foreach ( $triplets as $i => $triplet ) {
 264+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
 265+
 266+ if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
 267+ $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath );
 268+ }
 269+ if ( !$this->validateFilename( $dstRel ) ) {
 270+ throw new MWException( 'Validation error in $dstRel' );
 271+ }
 272+ if ( !$this->validateFilename( $archiveRel ) ) {
 273+ throw new MWException( 'Validation error in $archiveRel' );
 274+ }
 275+ $dstPath = "{$this->directory}/$dstRel";
 276+ $archivePath = "{$this->directory}/$archiveRel";
 277+
 278+ $dstDir = dirname( $dstPath );
 279+ $archiveDir = dirname( $archivePath );
 280+ // Abort immediately on directory creation errors since they're likely to be repetitive
 281+ if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
 282+ return $this->newFatal( 'directorycreateerror', $dstDir );
 283+ }
 284+ if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) {
 285+ return $this->newFatal( 'directorycreateerror', $archiveDir );
 286+ }
 287+ if ( !is_file( $srcPath ) ) {
 288+ // Make a list of files that don't exist for return to the caller
 289+ $status->fatal( 'filenotfound', $srcPath );
 290+ }
205291 }
206 - if ( !$this->validateFilename( $dstRel ) ) {
207 - throw new MWException( 'Validation error in $dstRel' );
 292+
 293+ if ( !$status->ok ) {
 294+ return $status;
208295 }
209 - if ( !$this->validateFilename( $archiveRel ) ) {
210 - throw new MWException( 'Validation error in $archiveRel' );
211 - }
212 - $dstPath = "{$this->directory}/$dstRel";
213 - $archivePath = "{$this->directory}/$archiveRel";
214296
215 - $dstDir = dirname( $dstPath );
216 - if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
 297+ foreach ( $triplets as $i => $triplet ) {
 298+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
 299+ $dstPath = "{$this->directory}/$dstRel";
 300+ $archivePath = "{$this->directory}/$archiveRel";
217301
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 - }
 302+ // Archive destination file if it exists
 303+ if( is_file( $dstPath ) ) {
 304+ // Check if the archive file exists
 305+ // This is a sanity check to avoid data loss. In UNIX, the rename primitive
 306+ // unlinks the destination file if it exists. DB-based synchronisation in
 307+ // publishBatch's caller should prevent races. In Windows there's no
 308+ // problem because the rename primitive fails if the destination exists.
 309+ if ( is_file( $archivePath ) ) {
 310+ $success = false;
 311+ } else {
 312+ wfSuppressWarnings();
 313+ $success = rename( $dstPath, $archivePath );
 314+ wfRestoreWarnings();
 315+ }
222316
223 - if( is_file( $dstPath ) ) {
224 - $archiveDir = dirname( $archivePath );
225 - if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
 317+ if( !$success ) {
 318+ $status->error( 'filerenameerror',$dstPath, $archivePath );
 319+ $status->failCount++;
 320+ continue;
 321+ } else {
 322+ wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
 323+ }
 324+ $status->value[$i] = 'archived';
 325+ } else {
 326+ $status->value[$i] = 'new';
 327+ }
 328+
 329+ $good = true;
226330 wfSuppressWarnings();
227 - $success = rename( $dstPath, $archivePath );
 331+ if ( $flags & self::DELETE_SOURCE ) {
 332+ if ( !rename( $srcPath, $dstPath ) ) {
 333+ $status->error( 'filerenameerror', $srcPath, $dstPath );
 334+ $good = false;
 335+ }
 336+ } else {
 337+ if ( !copy( $srcPath, $dstPath ) ) {
 338+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 339+ $good = false;
 340+ }
 341+ }
228342 wfRestoreWarnings();
229343
230 - if( ! $success ) {
231 - return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
232 - wfEscapeWikiText( $archivePath ) );
 344+ if ( $good ) {
 345+ $status->successCount++;
 346+ wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
 347+ // Thread-safe override for umask
 348+ chmod( $dstPath, 0644 );
 349+ } else {
 350+ $status->failCount++;
233351 }
234 - else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
235 - $status = 'archived';
236352 }
237 - else {
238 - $status = 'new';
 353+ return $status;
 354+ }
 355+
 356+ /**
 357+ * Move a group of files to the deletion archive.
 358+ * If no valid deletion archive is configured, this may either delete the
 359+ * file or throw an exception, depending on the preference of the repository.
 360+ *
 361+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
 362+ * is a two-element array containing the source file path relative to the
 363+ * public root in the first element, and the archive file path relative
 364+ * to the deleted zone root in the second element.
 365+ * @return FileRepoStatus
 366+ */
 367+ function deleteBatch( $sourceDestPairs ) {
 368+ $status = $this->newGood();
 369+ if ( !$this->deletedDir ) {
 370+ throw new MWException( __METHOD__.': no valid deletion archive directory' );
239371 }
240372
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 ) );
 373+ /**
 374+ * Validate filenames and create archive directories
 375+ */
 376+ foreach ( $sourceDestPairs as $pair ) {
 377+ list( $srcRel, $archiveRel ) = $pair;
 378+ if ( !$this->validateFilename( $srcRel ) ) {
 379+ throw new MWException( __METHOD__.':Validation error in $srcRel' );
247380 }
248 - } else {
249 - if ( !copy( $srcPath, $dstPath ) ) {
250 - $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
251 - wfEscapeWikiText( $dstPath ) );
 381+ if ( !$this->validateFilename( $archiveRel ) ) {
 382+ throw new MWException( __METHOD__.':Validation error in $archiveRel' );
252383 }
 384+ $archivePath = "{$this->deletedDir}/$archiveRel";
 385+ $archiveDir = dirname( $archivePath );
 386+ if ( !is_dir( $archiveDir ) ) {
 387+ if ( !wfMkdirParents( $archiveDir ) ) {
 388+ $status->fatal( 'directorycreateerror', $archiveDir );
 389+ continue;
 390+ }
 391+ // Seed new directories with a blank index.html, to prevent crawling
 392+ file_put_contents( "$archiveDir/index.html", '' );
 393+ }
 394+ // Check if the archive directory is writable
 395+ // This doesn't appear to work on NTFS
 396+ if ( !is_writable( $archiveDir ) ) {
 397+ $status->fatal( 'filedelete-archive-read-only', $archiveDir );
 398+ }
253399 }
254 - wfRestoreWarnings();
 400+ if ( !$status->ok ) {
 401+ // Abort early
 402+ return $status;
 403+ }
255404
256 - if( $error ) {
257 - return $error;
258 - } else {
259 - wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
 405+ /**
 406+ * Move the files
 407+ * We're now committed to returning an OK result, which will lead to
 408+ * the files being moved in the DB also.
 409+ */
 410+ foreach ( $sourceDestPairs as $pair ) {
 411+ list( $srcRel, $archiveRel ) = $pair;
 412+ $srcPath = "{$this->directory}/$srcRel";
 413+ $archivePath = "{$this->deletedDir}/$archiveRel";
 414+ $good = true;
 415+ if ( file_exists( $archivePath ) ) {
 416+ # A file with this content hash is already archived
 417+ if ( !@unlink( $srcPath ) ) {
 418+ $status->error( 'filedeleteerror', $srcPath );
 419+ $good = false;
 420+ }
 421+ } else{
 422+ if ( !@rename( $srcPath, $archivePath ) ) {
 423+ $status->error( 'filerenameerror', $srcPath, $archivePath );
 424+ $good = false;
 425+ } else {
 426+ chmod( $archivePath, 0644 );
 427+ }
 428+ }
 429+ if ( $good ) {
 430+ $status->successCount++;
 431+ } else {
 432+ $status->failCount++;
 433+ }
260434 }
261 -
262 - chmod( $dstPath, 0644 );
263435 return $status;
264436 }
265437
@@ -271,6 +443,18 @@
272444 }
273445
274446 /**
 447+ * Get a relative path for a deletion archive key,
 448+ * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
 449+ */
 450+ function getDeletedHashPath( $key ) {
 451+ $path = '';
 452+ for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
 453+ $path .= $key[$i] . '/';
 454+ }
 455+ return $path;
 456+ }
 457+
 458+ /**
275459 * Call a callback function for every file in the repository.
276460 * Uses the filesystem even in child classes.
277461 */
@@ -308,6 +492,39 @@
309493 $path = $this->resolveVirtualUrl( $virtualUrl );
310494 return File::getPropsFromPath( $path );
311495 }
 496+
 497+ /**
 498+ * Path disclosure protection functions
 499+ *
 500+ * Get a callback function to use for cleaning error message parameters
 501+ */
 502+ function getErrorCleanupFunction() {
 503+ switch ( $this->pathDisclosureProtection ) {
 504+ case 'simple':
 505+ $callback = array( $this, 'simpleClean' );
 506+ break;
 507+ default:
 508+ $callback = parent::getErrorCleanupFunction();
 509+ }
 510+ return $callback;
 511+ }
 512+
 513+ function simpleClean( $param ) {
 514+ if ( !isset( $this->simpleCleanPairs ) ) {
 515+ global $IP;
 516+ $this->simpleCleanPairs = array(
 517+ $this->directory => 'public',
 518+ "{$this->directory}/temp" => 'temp',
 519+ $IP => '$IP',
 520+ dirname( __FILE__ ) => '$IP/extensions/WebStore',
 521+ );
 522+ if ( $this->deletedDir ) {
 523+ $this->simpleCleanPairs[$this->deletedDir] = 'deleted';
 524+ }
 525+ }
 526+ return strtr( $param, $this->simpleCleanPairs );
 527+ }
 528+
312529 }
313530
314531
Index: branches/liquidthreads/includes/filerepo/File.php
@@ -593,11 +593,9 @@
594594
595595 /**
596596 * Purge metadata and all affected pages when the file is created,
597 - * deleted, or majorly updated. A set of additional URLs may be
598 - * passed to purge, such as specific file files which have changed.
599 - * @param $urlArray array
 597+ * deleted, or majorly updated.
600598 */
601 - function purgeEverything( $urlArr=array() ) {
 599+ function purgeEverything() {
602600 // Delete thumbnails and refresh file metadata cache
603601 $this->purgeCache();
604602 $this->purgeDescription();
@@ -656,9 +654,9 @@
657655 return $this->getHashPath() . rawurlencode( $this->getName() );
658656 }
659657
660 - /** Get the path of the archive directory, or a particular file if $suffix is specified */
661 - function getArchivePath( $suffix = false ) {
662 - $path = $this->repo->getZonePath('public') . '/archive/' . $this->getHashPath();
 658+ /** Get the relative path for an archive file */
 659+ function getArchiveRel( $suffix = false ) {
 660+ $path = 'archive/' . $this->getHashPath();
663661 if ( $suffix === false ) {
664662 $path = substr( $path, 0, -1 );
665663 } else {
@@ -667,15 +665,25 @@
668666 return $path;
669667 }
670668
671 - /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
672 - function getThumbPath( $suffix = false ) {
673 - $path = $this->repo->getZonePath('public') . '/thumb/' . $this->getRel();
 669+ /** Get relative path for a thumbnail file */
 670+ function getThumbRel( $suffix = false ) {
 671+ $path = 'thumb/' . $this->getRel();
674672 if ( $suffix !== false ) {
675673 $path .= '/' . $suffix;
676674 }
677675 return $path;
678676 }
679677
 678+ /** Get the path of the archive directory, or a particular file if $suffix is specified */
 679+ function getArchivePath( $suffix = false ) {
 680+ return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel();
 681+ }
 682+
 683+ /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
 684+ function getThumbPath( $suffix = false ) {
 685+ return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix );
 686+ }
 687+
680688 /** Get the URL of the archive directory, or a particular file if $suffix is specified */
681689 function getArchiveUrl( $suffix = false ) {
682690 $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath();
@@ -980,13 +988,20 @@
981989 /**
982990 * Get the 14-character timestamp of the file upload, or false if
983991 */
984 - function getTimestmap() {
 992+ function getTimestamp() {
985993 $path = $this->getPath();
986994 if ( !file_exists( $path ) ) {
987995 return false;
988996 }
989997 return wfTimestamp( filemtime( $path ) );
990998 }
 999+
 1000+ /**
 1001+ * Get the SHA-1 base 36 hash of the file
 1002+ */
 1003+ function getSha1() {
 1004+ return self::sha1Base36( $this->getPath() );
 1005+ }
9911006
9921007 /**
9931008 * Determine if the current user is allowed to view a particular
@@ -1031,12 +1046,14 @@
10321047 $gis = false;
10331048 $info['metadata'] = '';
10341049 }
 1050+ $info['sha1'] = self::sha1Base36( $path );
10351051
10361052 wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
10371053 } else {
10381054 $info['mime'] = NULL;
10391055 $info['media_type'] = MEDIATYPE_UNKNOWN;
10401056 $info['metadata'] = '';
 1057+ $info['sha1'] = '';
10411058 wfDebug(__METHOD__.": $path NOT FOUND!\n");
10421059 }
10431060 if( $gis ) {
@@ -1056,6 +1073,30 @@
10571074 wfProfileOut( __METHOD__ );
10581075 return $info;
10591076 }
 1077+
 1078+ /**
 1079+ * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
 1080+ * encoding, zero padded to 31 digits.
 1081+ *
 1082+ * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
 1083+ * fairly neatly.
 1084+ *
 1085+ * Returns false on failure
 1086+ */
 1087+ static function sha1Base36( $path ) {
 1088+ $hash = sha1_file( $path );
 1089+ if ( $hash === false ) {
 1090+ return false;
 1091+ } else {
 1092+ return wfBaseConvert( $hash, 16, 36, 31 );
 1093+ }
 1094+ }
10601095 }
 1096+/**
 1097+ * Aliases for backwards compatibility with 1.6
 1098+ */
 1099+define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
 1100+define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
 1101+define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
 1102+define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
10611103
1062 -
Index: branches/liquidthreads/includes/filerepo/LocalRepo.php
@@ -28,4 +28,37 @@
2929 function newFromArchiveName( $title, $archiveName ) {
3030 return OldLocalFile::newFromArchiveName( $title, $this, $archiveName );
3131 }
 32+
 33+ /**
 34+ * Delete files in the deleted directory if they are not referenced in the
 35+ * filearchive table. This needs to be done in the repo because it needs to
 36+ * interleave database locks with file operations, which is potentially a
 37+ * remote operation.
 38+ * @return FileRepoStatus
 39+ */
 40+ function cleanupDeletedBatch( $storageKeys ) {
 41+ $root = $this->getZonePath( 'deleted' );
 42+ $dbw = $this->getMasterDB();
 43+ $status = $this->newGood();
 44+ foreach ( $storageKeys as $key ) {
 45+ $hashPath = $this->getDeletedHashPath( $key );
 46+ $path = "$root/$hashPath$key";
 47+ $dbw->begin();
 48+ $inuse = $dbw->selectField( 'filearchive', '1',
 49+ array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
 50+ __METHOD__, array( 'FOR UPDATE' ) );
 51+ if ( !$inuse ) {
 52+ wfDebug( __METHOD__ . ": deleting $key\n" );
 53+ if ( !@unlink( $path ) ) {
 54+ $status->error( 'undelete-cleanup-error', $path );
 55+ $status->failCount++;
 56+ }
 57+ } else {
 58+ wfDebug( __METHOD__ . ": $key still in use\n" );
 59+ $status->successCount++;
 60+ }
 61+ $dbw->commit();
 62+ }
 63+ return $status;
 64+ }
3265 }
Index: branches/liquidthreads/includes/EditPage.php
@@ -949,6 +949,10 @@
950950 # Enabled article-related sidebar, toplinks, etc.
951951 $wgOut->setArticleRelated( true );
952952
 953+ if ( $this->formtype == 'preview' ) {
 954+ $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
 955+ }
 956+
953957 if ( $this->isConflict ) {
954958 $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() );
955959 $wgOut->setPageTitle( $s );
Index: branches/liquidthreads/includes/SpecialUpload.php
@@ -46,9 +46,11 @@
4747 global $wgAllowCopyUploads;
4848 $this->mDesiredDestName = $request->getText( 'wpDestFile' );
4949 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
 50+ $this->mComment = $request->getText( 'wpUploadDescription' );
5051
5152 if( !$request->wasPosted() ) {
52 - # GET requests just give the main form; no data except wpDestfile.
 53+ # GET requests just give the main form; no data except destination
 54+ # filename and description
5355 return;
5456 }
5557
@@ -59,7 +61,6 @@
6062 $this->mReUpload = $request->getCheck( 'wpReUpload' );
6163 $this->mUploadClicked = $request->getCheck( 'wpUpload' );
6264
63 - $this->mComment = $request->getText( 'wpUploadDescription' );
6465 $this->mLicense = $request->getText( 'wpLicense' );
6566 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
6667 $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
@@ -433,8 +434,8 @@
434435
435436 $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
436437 File::DELETE_SOURCE, $this->mFileProps );
437 - if ( WikiError::isError( $status ) ) {
438 - $this->showError( $status );
 438+ if ( !$status->isGood() ) {
 439+ $this->showError( $status->getWikiText() );
439440 } else {
440441 if ( $this->mWatchthis ) {
441442 global $wgUser;
@@ -592,12 +593,12 @@
593594 function saveTempUploadedFile( $saveName, $tempName ) {
594595 global $wgOut;
595596 $repo = RepoGroup::singleton()->getLocalRepo();
596 - $result = $repo->storeTemp( $saveName, $tempName );
597 - if ( WikiError::isError( $result ) ) {
598 - $this->showError( $result );
 597+ $status = $repo->storeTemp( $saveName, $tempName );
 598+ if ( !$status->isGood() ) {
 599+ $this->showError( $status->getWikiText() );
599600 return false;
600601 } else {
601 - return $result;
 602+ return $status->value;
602603 }
603604 }
604605
@@ -1354,15 +1355,15 @@
13551356 }
13561357
13571358 /**
1358 - * Display an error from a wikitext-formatted WikiError object
 1359+ * Display an error with a wikitext description
13591360 */
1360 - function showError( WikiError $error ) {
 1361+ function showError( $description ) {
13611362 global $wgOut;
13621363 $wgOut->setPageTitle( wfMsg( "internalerror" ) );
13631364 $wgOut->setRobotpolicy( "noindex,nofollow" );
13641365 $wgOut->setArticleRelated( false );
13651366 $wgOut->enableClientCache( false );
1366 - $wgOut->addWikiText( $error->getMessage() );
 1367+ $wgOut->addWikiText( $description );
13671368 }
13681369
13691370 /**
Index: branches/liquidthreads/includes/OutputPage.php
@@ -28,6 +28,7 @@
2929
3030 var $mNewSectionLink = false;
3131 var $mNoGallery = false;
 32+ var $mPageTitleActionText = '';
3233
3334 /**
3435 * Constructor
@@ -192,26 +193,13 @@
193194 }
194195 }
195196
 197+ function setPageTitleActionText( $text ) {
 198+ $this->mPageTitleActionText = $text;
 199+ }
 200+
196201 function getPageTitleActionText () {
197 - global $action;
198 - switch($action) {
199 - case 'edit':
200 - case 'delete':
201 - case 'protect':
202 - case 'unprotect':
203 - case 'watch':
204 - case 'unwatch':
205 - // Display title is already customized
206 - return '';
207 - case 'history':
208 - return wfMsg('history_short');
209 - case 'submit':
210 - // FIXME: bug 2735; not correct for special pages etc
211 - return wfMsg('preview');
212 - case 'info':
213 - return wfMsg('info_short');
214 - default:
215 - return '';
 202+ if ( isset( $this->mPageTitleActionText ) ) {
 203+ return $this->mPageTitleActionText;
216204 }
217205 }
218206
Index: branches/liquidthreads/includes/AutoLoader.php
@@ -258,11 +258,14 @@
259259 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
260260 'File' => 'includes/filerepo/File.php',
261261 'FileRepo' => 'includes/filerepo/FileRepo.php',
 262+ 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
262263 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
263264 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
264265 'FSRepo' => 'includes/filerepo/FSRepo.php',
265266 'Image' => 'includes/filerepo/LocalFile.php',
266267 'LocalFile' => 'includes/filerepo/LocalFile.php',
 268+ 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
 269+ 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
267270 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
268271 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
269272 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
Index: branches/liquidthreads/includes/SpecialUndelete.php
@@ -23,6 +23,7 @@
2424 */
2525 class PageArchive {
2626 protected $title;
 27+ var $fileStatus;
2728
2829 function __construct( $title ) {
2930 if( is_null( $title ) ) {
@@ -270,7 +271,8 @@
271272
272273 if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
273274 $img = wfLocalFile( $this->title );
274 - $filesRestored = $img->restore( $fileVersions );
 275+ $this->fileStatus = $img->restore( $fileVersions );
 276+ $filesRestored = $this->fileStatus->successCount;
275277 } else {
276278 $filesRestored = 0;
277279 }
@@ -280,7 +282,7 @@
281283 } else {
282284 $textRestored = 0;
283285 }
284 -
 286+
285287 // Touch the log!
286288 global $wgContLang;
287289 $log = new LogPage( 'delete' );
@@ -303,8 +305,12 @@
304306 if( trim( $comment ) != '' )
305307 $reason .= ": {$comment}";
306308 $log->addEntry( 'restore', $this->title, $reason );
307 -
308 - return true;
 309+
 310+ if ( $this->fileStatus && !$this->fileStatus->ok ) {
 311+ return false;
 312+ } else {
 313+ return true;
 314+ }
309315 }
310316
311317 /**
@@ -448,6 +454,7 @@
449455 return $restored;
450456 }
451457
 458+ function getFileStatus() { return $this->fileStatus; }
452459 }
453460
454461 /**
@@ -731,8 +738,13 @@
732739 $logViewer = new LogViewer(
733740 new LogReader(
734741 new FauxRequest(
735 - array( 'page' => $this->mTargetObj->getPrefixedText(),
736 - 'type' => 'delete' ) ) ) );
 742+ array(
 743+ 'page' => $this->mTargetObj->getPrefixedText(),
 744+ 'type' => 'delete'
 745+ )
 746+ )
 747+ ), LogViewer::NO_ACTION_LINK
 748+ );
737749 $logViewer->showList( $wgOut );
738750
739751 if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
@@ -836,15 +848,23 @@
837849 $this->mTargetTimestamp,
838850 $this->mComment,
839851 $this->mFileVersions );
840 -
 852+
841853 if( $ok ) {
842854 $skin = $wgUser->getSkin();
843855 $link = $skin->makeKnownLinkObj( $this->mTargetObj );
844856 $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
845 - return true;
 857+ } else {
 858+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
846859 }
 860+
 861+ // Show file deletion warnings and errors
 862+ $status = $archive->getFileStatus();
 863+ if ( $status && !$status->isGood() ) {
 864+ $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
 865+ }
 866+ } else {
 867+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
847868 }
848 - $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
849869 return false;
850870 }
851871 }
Index: branches/liquidthreads/includes/DifferenceEngine.php
@@ -290,8 +290,8 @@
291291 * Returns false if the diff could not be generated, otherwise returns true
292292 */
293293 function showDiff( $otitle, $ntitle ) {
294 - global $wgOut;
295 - $diff = $this->getDiff( $otitle, $ntitle );
 294+ global $wgOut, $wgRequest;
 295+ $diff = $this->getDiff( $otitle, $ntitle, $wgRequest->getVal( 'action' ) == 'purge' );
296296 if ( $diff === false ) {
297297 $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
298298 return false;
@@ -314,12 +314,15 @@
315315 }
316316
317317 /**
318 - * Get diff table, including header
319 - * Note that the interface has changed, it's no longer static.
320 - * Returns false on error
 318+ * Get complete diff table, including header
 319+ *
 320+ * @param Title $otitle Old title
 321+ * @param Title $ntitle New title
 322+ * @param bool $skipCache Skip the diff cache for this request?
 323+ * @return mixed
321324 */
322 - function getDiff( $otitle, $ntitle ) {
323 - $body = $this->getDiffBody();
 325+ function getDiff( $otitle, $ntitle, $skipCache = false ) {
 326+ $body = $this->getDiffBody( $skipCache );
324327 if ( $body === false ) {
325328 return false;
326329 } else {
@@ -330,17 +333,18 @@
331334
332335 /**
333336 * Get the diff table body, without header
334 - * Results are cached
335 - * Returns false on error
 337+ *
 338+ * @param bool $skipCache Skip cache for this request?
 339+ * @return mixed
336340 */
337 - function getDiffBody() {
 341+ function getDiffBody( $skipCache = false ) {
338342 global $wgMemc;
339343 $fname = 'DifferenceEngine::getDiffBody';
340344 wfProfileIn( $fname );
341345
342346 // Cacheable?
343347 $key = false;
344 - if ( $this->mOldid && $this->mNewid ) {
 348+ if ( $this->mOldid && $this->mNewid && !$skipCache ) {
345349 // Try cache
346350 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
347351 $difftext = $wgMemc->get( $key );
Index: branches/liquidthreads/includes/SpecialLog.php
@@ -241,19 +241,25 @@
242242 * @addtogroup SpecialPage
243243 */
244244 class LogViewer {
 245+ const NO_ACTION_LINK = 1;
 246+
245247 /**
246248 * @var LogReader $reader
247249 */
248250 var $reader;
249251 var $numResults = 0;
 252+ var $flags = 0;
250253
251254 /**
252255 * @param LogReader &$reader where to get our data from
 256+ * @param integer $flags Bitwise combination of flags:
 257+ * self::NO_ACTION_LINK Don't show restore/unblock/block links
253258 */
254 - function LogViewer( &$reader ) {
 259+ function LogViewer( &$reader, $flags = 0 ) {
255260 global $wgUser;
256261 $this->skin = $wgUser->getSkin();
257262 $this->reader =& $reader;
 263+ $this->flags = $flags;
258264 }
259265
260266 /**
@@ -363,41 +369,43 @@
364370 $paramArray = LogPage::extractParams( $s->log_params );
365371 $revert = '';
366372 // show revertmove link
367 - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
368 - $destTitle = Title::newFromText( $paramArray[0] );
369 - if ( $destTitle ) {
370 - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
371 - wfMsg( 'revertmove' ),
372 - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
373 - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
374 - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
375 - '&wpMovetalk=0' ) . ')';
 373+ if ( !( $this->flags & self::NO_ACTION_LINK ) ) {
 374+ if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
 375+ $destTitle = Title::newFromText( $paramArray[0] );
 376+ if ( $destTitle ) {
 377+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
 378+ wfMsg( 'revertmove' ),
 379+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
 380+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
 381+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
 382+ '&wpMovetalk=0' ) . ')';
 383+ }
 384+ // show undelete link
 385+ } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
 386+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
 387+ wfMsg( 'undeletebtn' ) ,
 388+ 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
 389+
 390+ // show unblock link
 391+ } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
 392+ $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
 393+ wfMsg( 'unblocklink' ),
 394+ 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
 395+ // show change protection link
 396+ } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
 397+ $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
 398+ // show user tool links for self created users
 399+ // TODO: The extension should be handling this, get it out of core!
 400+ } elseif ( $s->log_action == 'create2' ) {
 401+ if( isset( $paramArray[0] ) ) {
 402+ $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
 403+ } else {
 404+ # Fall back to a blue contributions link
 405+ $revert = $this->skin->userToolLinks( 1, $s->log_title );
 406+ }
 407+ # Suppress $comment from old entries, not needed and can contain incorrect links
 408+ $comment = '';
376409 }
377 - // show undelete link
378 - } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
379 - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
380 - wfMsg( 'undeletebtn' ) ,
381 - 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
382 -
383 - // show unblock link
384 - } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
385 - $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
386 - wfMsg( 'unblocklink' ),
387 - 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
388 - // show change protection link
389 - } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
390 - $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
391 - // show user tool links for self created users
392 - // TODO: The extension should be handling this, get it out of core!
393 - } elseif ( $s->log_action == 'create2' ) {
394 - if( isset( $paramArray[0] ) ) {
395 - $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
396 - } else {
397 - # Fall back to a blue contributions link
398 - $revert = $this->skin->userToolLinks( 1, $s->log_title );
399 - }
400 - # Suppress $comment from old entries, not needed and can contain incorrect links
401 - $comment = '';
402410 }
403411
404412 $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true );
Index: branches/liquidthreads/includes/DefaultSettings.php
@@ -164,16 +164,6 @@
165165 /**#@-*/
166166
167167 /**
168 - * By default deleted files are simply discarded; to save them and
169 - * make it possible to undelete images, create a directory which
170 - * is writable to the web server but is not exposed to the internet.
171 - *
172 - * Set $wgSaveDeletedFiles to true and set up the save path in
173 - * $wgFileStore['deleted']['directory'].
174 - */
175 -$wgSaveDeletedFiles = false;
176 -
177 -/**
178168 * New file storage paths; currently used only for deleted files.
179169 * Set it like this:
180170 *
@@ -181,7 +171,7 @@
182172 *
183173 */
184174 $wgFileStore = array();
185 -$wgFileStore['deleted']['directory'] = null; // Don't forget to set this.
 175+$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
186176 $wgFileStore['deleted']['url'] = null; // Private
187177 $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
188178
@@ -208,6 +198,10 @@
209199 * start with a capital letter. The current implementation may give incorrect
210200 * description page links when the local $wgCapitalLinks and initialCapital
211201 * are mismatched.
 202+ * pathDisclosureProtection
 203+ * May be 'paranoid' to remove all parameters from error messages, 'none' to
 204+ * leave the paths in unchanged, or 'simple' to replace paths with
 205+ * placeholders. Default for LocalRepo is 'simple'.
212206 *
213207 * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
214208 * for local repositories:
Index: branches/liquidthreads/includes/FileStore.php
@@ -219,7 +219,7 @@
220220 * Confirm that the given file key is valid.
221221 * Note that a valid key may refer to a file that does not exist.
222222 *
223 - * Key should consist of a 32-digit base-36 SHA-1 hash and
 223+ * Key should consist of a 31-digit base-36 SHA-1 hash and
224224 * an optional alphanumeric extension, all lowercase.
225225 * The whole must not exceed 64 characters.
226226 *
@@ -227,7 +227,7 @@
228228 * @return boolean
229229 */
230230 static function validKey( $key ) {
231 - return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key );
 231+ return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
232232 }
233233
234234
@@ -249,7 +249,7 @@
250250 return false;
251251 }
252252
253 - $base36 = wfBaseConvert( $hash, 16, 36, 32 );
 253+ $base36 = wfBaseConvert( $hash, 16, 36, 31 );
254254 if( $extension == '' ) {
255255 $key = $base36;
256256 } else {
Index: branches/liquidthreads/includes/PageHistory.php
@@ -62,6 +62,7 @@
6363 * Setup page variables.
6464 */
6565 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
 66+ $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
6667 $wgOut->setArticleFlag( false );
6768 $wgOut->setArticleRelated( true );
6869 $wgOut->setRobotpolicy( 'noindex,nofollow' );
Property changes on: branches/liquidthreads
___________________________________________________________________
Modified: svnmerge-integrated
6970 - /trunk/phase3:1-24301
7071 + /trunk/phase3:1-24345

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r24310*Add ar_page; allows for easier restoration when several large pages are merg...aaron01:40, 22 July 2007
r24312*Store page_id in ar_pageaaron03:26, 22 July 2007
r24313* Introduced FileRepoStatus -- result class for file repo operations....tstarling14:45, 22 July 2007
r24314rv againtstarling15:07, 22 July 2007
r24315Commas at the end of enumerations only works in PHP...jeluf15:15, 22 July 2007
r24316Update.rotem15:29, 22 July 2007
r24317Adding missing translations for dawegge21:30, 22 July 2007
r24318Correct language - not "no object caching", rather, "no object caching [using...robchurch21:55, 22 July 2007
r24323* Pass new Revision to the 'ArticleInsertComplete' and 'ArticleSaveComplete' ...robchurch23:16, 22 July 2007
r24324(bug 9575) Accept upload description from GET parametersrobchurch23:25, 22 July 2007
r24325Bug number for r24323robchurch23:26, 22 July 2007
r24326Skip the difference engine cache when 'action=purge' is used while requesting...robchurch23:37, 22 July 2007
r24328*grraaron03:03, 23 July 2007
r24329s/one time/once/robchurch08:58, 23 July 2007
r24335debugging code accidentally left intstarling17:18, 23 July 2007
r24336Debuggingtstarling17:19, 23 July 2007
r24337Seed subdirs in the deleted zone with a blank index.html file, to prevent cra...tstarling17:21, 23 July 2007
r24338Fixed LocalRepo::cleanupDeletedBatch(), wasn't workingtstarling17:22, 23 July 2007
r2433932 widthtstarling18:28, 23 July 2007
r24340Optimize User::getID() for special cases, and User::isLoggedIn() generally (t...simetrical19:39, 23 July 2007
r24342(bug 10672) Make Linker::doEditSectionLink protected, not privatesimetrical21:40, 23 July 2007
r24343Added ar_pagetstarling22:37, 23 July 2007
r24344fixed commenttstarling22:38, 23 July 2007

Status & tagging log