Index: branches/liquidthreads/config/index.php |
— | — | @@ -485,8 +485,8 @@ |
486 | 486 | if( !( $conf->turck || $conf->eaccel || $conf->apc || $conf->xcache ) ) { |
487 | 487 | echo( '<li>Couldn\'t find <a href="http://turck-mmcache.sourceforge.net">Turck MMCache</a>, |
488 | 488 | <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>' ); |
491 | 491 | } |
492 | 492 | |
493 | 493 | $conf->diff3 = false; |
Index: branches/liquidthreads/languages/LanguageConverter.php |
— | — | @@ -777,7 +777,7 @@ |
778 | 778 | * MediaWiki:conversiontable* is updated |
779 | 779 | * @private |
780 | 780 | */ |
781 | | - function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section) { |
| 781 | + function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section, $flags, $revision) { |
782 | 782 | $titleobj = $article->getTitle(); |
783 | 783 | if($titleobj->getNamespace() == NS_MEDIAWIKI) { |
784 | 784 | /* |
Index: branches/liquidthreads/languages/messages/MessagesEn.php |
— | — | @@ -646,7 +646,7 @@ |
647 | 647 | 'redirectedfrom' => '(Redirected from $1)', |
648 | 648 | 'redirectpagesub' => 'Redirect page', |
649 | 649 | '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}}.', |
651 | 651 | 'protectedpage' => 'Protected page', |
652 | 652 | 'jumpto' => 'Jump to:', |
653 | 653 | 'jumptonavigation' => 'navigation', |
— | — | @@ -765,10 +765,13 @@ |
766 | 766 | Please report this to an administrator, making note of the URL.', |
767 | 767 | 'readonly_lag' => 'The database has been automatically locked while the slave database servers catch up to the master', |
768 | 768 | 'internalerror' => 'Internal error', |
| 769 | +'internalerror_info' => 'Internal error: $1', |
769 | 770 | 'filecopyerror' => 'Could not copy file "$1" to "$2".', |
770 | 771 | 'filerenameerror' => 'Could not rename file "$1" to "$2".', |
771 | 772 | 'filedeleteerror' => 'Could not delete file "$1".', |
| 773 | +'directorycreateerror' => 'Could not create directory "$1".', |
772 | 774 | 'filenotfound' => 'Could not find file "$1".', |
| 775 | +'fileexists' => 'Unable to write to file "$1": file exists', |
773 | 776 | 'unexpected' => 'Unexpected value: "$1"="$2".', |
774 | 777 | 'formerror' => 'Error: could not submit form', |
775 | 778 | 'badarticleerror' => 'This action cannot be performed on this page.', |
— | — | @@ -1889,6 +1892,13 @@ |
1890 | 1893 | 'undelete-search-prefix' => 'Show pages starting with:', |
1891 | 1894 | 'undelete-search-submit' => 'Search', |
1892 | 1895 | '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", |
1893 | 1903 | |
1894 | 1904 | # Namespace form on various pages |
1895 | 1905 | 'namespace' => 'Namespace:', |
— | — | @@ -2366,6 +2376,12 @@ |
2367 | 2377 | |
2368 | 2378 | # Image deletion |
2369 | 2379 | '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.', |
2370 | 2386 | |
2371 | 2387 | # Browsing diffs |
2372 | 2388 | 'previousdiff' => '← Previous diff', |
Index: branches/liquidthreads/languages/messages/MessagesHe.php |
— | — | @@ -499,10 +499,13 @@ |
500 | 500 | אנא דווח על כך למפתח תוך שמירת פרטי כתובת ה־URL.', |
501 | 501 | 'readonly_lag' => 'בסיס הנתונים ננעל אוטומטית כדי לאפשר לבסיסי הנתונים המשניים להתעדכן מהבסיס הראשי.', |
502 | 502 | '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" נכשלה.', |
506 | 508 | 'filenotfound' => 'הקובץ "$1" לא נמצא.', |
| 509 | +'fileexists' => 'הכתיבה לקובץ "$1" נכשלה: הקובץ קיים', |
507 | 510 | 'unexpected' => 'ערך לא צפוי: "$1"="$2"', |
508 | 511 | 'formerror' => 'שגיאה: לא יכול לשלוח טופס.', |
509 | 512 | 'badarticleerror' => 'לא ניתן לבצע פעולה זו בדף זה.', |
— | — | @@ -1446,34 +1449,44 @@ |
1447 | 1450 | 'restriction-level-all' => 'כל רמה', |
1448 | 1451 | |
1449 | 1452 | # 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 שוחזר בהצלחה.''' |
1470 | 1473 | |
1471 | 1474 | ראו את [[{{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' => 'שגיאות שאירעו בעת שחזור הקובץ: |
1477 | 1486 | |
| 1487 | +$1 |
| 1488 | +', |
| 1489 | + |
| 1490 | + |
1478 | 1491 | # Namespace form on various pages |
1479 | 1492 | 'namespace' => 'מרחב שם:', |
1480 | 1493 | 'invert' => 'ללא מרחב זה', |
— | — | @@ -1730,11 +1743,6 @@ |
1731 | 1744 | 'importnosources' => 'אין מקורות לייבוא בין־אתרי, וייבוא ישיר של דף עם היסטוריה אינו מאופשר כעת.', |
1732 | 1745 | 'importnofile' => 'לא הועלה קובץ ייבוא.', |
1733 | 1746 | 'importuploaderror' => 'העלאת קובץ ייבוא נכשלה; ייתכן שהקובץ גדול מגודל ההעלאה המותר.', |
1734 | | -'import-parse-failure' => 'שגיאת עיבוד בייבוא קובץ XML', |
1735 | | -'import-articlename' => 'שם דף חדש:', |
1736 | | -'import-noarticle' => 'אין דף לייבוא!', |
1737 | | -'import-nonewrevisions' => 'כל הגרסאות יובאו כבר בעבר.', |
1738 | | -'xml-error-string' => '$1 בשורה $2, עמודה $3 (בית $4): $5', |
1739 | 1747 | |
1740 | 1748 | # Import log |
1741 | 1749 | 'importlogpage' => 'יומן ייבוא', |
— | — | @@ -1873,8 +1881,17 @@ |
1874 | 1882 | 'patrol-log-diff' => 'גרסה $1', |
1875 | 1883 | |
1876 | 1884 | # Image deletion |
1877 | | -'deletedrevision' => 'מחק גרסה ישנה $1.', |
| 1885 | +'deletedrevision' => 'מחק גרסה ישנה $1.', |
| 1886 | +'filedeleteerror-short' => 'שגיאה במחיקת הקובץ: $1', |
| 1887 | +'filedeleteerror-long' => 'שגיאות שאירעו בעת מחיקת הקובץ: |
1878 | 1888 | |
| 1889 | +$1 |
| 1890 | +', |
| 1891 | +'filedelete-missing' => 'מחיקת הקובץ "$1" נכשלה, כיוון שהוא אינו קיים.', |
| 1892 | +'filedelete-old-unregistered' => 'גרסת הקובץ "$1" אינה רשומה במסד הנתונים.', |
| 1893 | +'filedelete-current-unregistered' => 'הקובץ "$1" אינו רשום במסד הנתונים.', |
| 1894 | +'filedelete-archive-read-only' => 'השרת אינו יכול לכתוב לתיקיית הארכיון "$1".', |
| 1895 | + |
1879 | 1896 | # Browsing diffs |
1880 | 1897 | 'previousdiff' => '→ עבור להשוואת הגרסאות הקודמת', |
1881 | 1898 | 'nextdiff' => 'עבור להשוואת הגרסאות הבאה ←', |
Index: branches/liquidthreads/languages/messages/MessagesDa.php |
— | — | @@ -424,9 +424,11 @@ |
425 | 425 | 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.', |
426 | 426 | 'readonly_lag' => 'Databasen er automatisk blevet låst mens slave database serverne synkronisere med master databasen', |
427 | 427 | 'internalerror' => 'Intern fejl', |
| 428 | +'internalerror_info' => 'Internal fejl: $1', |
428 | 429 | 'filecopyerror' => 'Kunne ikke kopiere filen "$1" til "$2".', |
429 | 430 | 'filerenameerror' => 'Kunne ikke omdøbe filen "$1" til "$2".', |
430 | 431 | 'filedeleteerror' => 'Kunne ikke slette filen "$1".', |
| 432 | +'directorycreateerror' => 'Kunne ikke oprette kataloget "$1".', |
431 | 433 | 'filenotfound' => 'Kunne ikke finde filen "$1".', |
432 | 434 | 'unexpected' => 'Uventet værdi: "$1"="$2".', |
433 | 435 | 'formerror' => 'Fejl: Kunne ikke afsende formular', |
— | — | @@ -1450,6 +1452,12 @@ |
1451 | 1453 | 'undelete-search-prefix' => 'Søgebegreb (odets start uden wildcards):', |
1452 | 1454 | 'undelete-search-submit' => 'Søg', |
1453 | 1455 | '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", |
1454 | 1462 | |
1455 | 1463 | # Namespace form on various pages |
1456 | 1464 | 'namespace' => 'Navnerum:', |
— | — | @@ -1823,6 +1831,12 @@ |
1824 | 1832 | |
1825 | 1833 | # Image deletion |
1826 | 1834 | '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".', |
1827 | 1841 | |
1828 | 1842 | # Browsing diffs |
1829 | 1843 | 'previousdiff' => '← Gå til forrige forskel', |
Index: branches/liquidthreads/RELEASE-NOTES |
— | — | @@ -26,6 +26,7 @@ |
27 | 27 | usergroups |
28 | 28 | * $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites |
29 | 29 | * $wgShowHostnames - Expose server host names through the API and HTML comments |
| 30 | +* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally |
30 | 31 | |
31 | 32 | == New features since 1.10 == |
32 | 33 | |
— | — | @@ -152,6 +153,11 @@ |
153 | 154 | * Pass the user as an argument to 'isValidPassword' hook callbacks; see |
154 | 155 | docs/hooks.txt for more information |
155 | 156 | * 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 |
156 | 162 | |
157 | 163 | == Bugfixes since 1.10 == |
158 | 164 | |
— | — | @@ -318,8 +324,12 @@ |
319 | 325 | * (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0 |
320 | 326 | * Work around Safari bug with pages ending in ".gz" or ".tgz" |
321 | 327 | * 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 |
322 | 333 | |
323 | | - |
324 | 334 | == API changes since 1.10 == |
325 | 335 | |
326 | 336 | Full API documentation is available at http://www.mediawiki.org/wiki/API |
Index: branches/liquidthreads/maintenance/tables.sql |
— | — | @@ -376,6 +376,10 @@ |
377 | 377 | |
378 | 378 | -- Length of this revision in bytes |
379 | 379 | 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, |
380 | 384 | |
381 | 385 | KEY name_title_timestamp (ar_namespace,ar_title,ar_timestamp), |
382 | 386 | KEY usertext_timestamp (ar_user_text,ar_timestamp) |
— | — | @@ -686,14 +690,21 @@ |
687 | 691 | -- Time of the upload. |
688 | 692 | img_timestamp varbinary(14) NOT NULL default '', |
689 | 693 | |
| 694 | + -- SHA-1 content hash in base-36 |
| 695 | + img_sha1 varbinary(32) NOT NULL default '', |
| 696 | + |
690 | 697 | PRIMARY KEY img_name (img_name), |
691 | 698 | |
692 | 699 | INDEX img_usertext_timestamp (img_user_text,img_timestamp), |
693 | 700 | -- Used by Special:Imagelist for sort-by-size |
694 | 701 | INDEX img_size (img_size), |
695 | 702 | -- Used by Special:Newimages and Special:Imagelist |
696 | | - INDEX img_timestamp (img_timestamp) |
| 703 | + INDEX img_timestamp (img_timestamp), |
697 | 704 | |
| 705 | + -- For future use |
| 706 | + INDEX img_sha1 (img_sha1) |
| 707 | + |
| 708 | + |
698 | 709 | ) /*$wgDBTableOptions*/; |
699 | 710 | |
700 | 711 | -- |
— | — | @@ -724,11 +735,13 @@ |
725 | 736 | oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", |
726 | 737 | oi_minor_mime varbinary(32) NOT NULL default "unknown", |
727 | 738 | oi_deleted tinyint unsigned NOT NULL default '0', |
| 739 | + oi_sha1 varbinary(32) NOT NULL default '', |
728 | 740 | |
729 | 741 | INDEX oi_usertext_timestamp (oi_user_text,oi_timestamp), |
730 | 742 | INDEX oi_name_timestamp (oi_name,oi_timestamp), |
731 | 743 | -- 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) |
733 | 746 | |
734 | 747 | ) /*$wgDBTableOptions*/; |
735 | 748 | |
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 |
1 | 10 | + 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 @@ |
174 | 174 | function addMissingImage( $filename, $fullpath ) { |
175 | 175 | $fname = 'ImageBuilder::addMissingImage'; |
176 | 176 | |
177 | | - $size = filesize( $fullpath ); |
178 | | - $info = $this->imageInfo( $fullpath ); |
179 | 177 | $timestamp = $this->dbw->timestamp( filemtime( $fullpath ) ); |
180 | 178 | |
181 | 179 | global $wgContLang; |
Index: branches/liquidthreads/maintenance/updaters.inc |
— | — | @@ -80,6 +80,8 @@ |
81 | 81 | array( 'page_restrictions', 'pr_id', 'patch-page_restrictions_sortkey.sql' ), |
82 | 82 | array( 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ), |
83 | 83 | 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' ), |
84 | 86 | ); |
85 | 87 | |
86 | 88 | # For extensions only, should be populated via hooks |
— | — | @@ -1347,6 +1349,7 @@ |
1348 | 1350 | array("recentchanges", "rc_old_len", "INTEGER"), |
1349 | 1351 | array("recentchanges", "rc_params", "TEXT"), |
1350 | 1352 | array("revision", "rev_len", "INTEGER"), |
| 1353 | + array("archive", "ar_page", "INTEGER NOT NULL DEFAULT 0"), |
1351 | 1354 | ); |
1352 | 1355 | |
1353 | 1356 | |
Index: branches/liquidthreads/maintenance/language/messages.inc |
— | — | @@ -298,10 +298,13 @@ |
299 | 299 | 'missingarticle', |
300 | 300 | 'readonly_lag', |
301 | 301 | 'internalerror', |
| 302 | + 'internalerror_info', |
302 | 303 | 'filecopyerror', |
303 | 304 | 'filerenameerror', |
304 | 305 | 'filedeleteerror', |
| 306 | + 'directorycreateerror', |
305 | 307 | 'filenotfound', |
| 308 | + 'fileexists', |
306 | 309 | 'unexpected', |
307 | 310 | 'formerror', |
308 | 311 | 'badarticleerror', |
— | — | @@ -1223,6 +1226,12 @@ |
1224 | 1227 | 'undelete-search-prefix', |
1225 | 1228 | 'undelete-search-submit', |
1226 | 1229 | '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', |
1227 | 1236 | ), |
1228 | 1237 | 'nsform' => array( |
1229 | 1238 | 'namespace', |
— | — | @@ -1640,6 +1649,12 @@ |
1641 | 1650 | ), |
1642 | 1651 | 'imagedeletion' => array( |
1643 | 1652 | 'deletedrevision', |
| 1653 | + 'filedeleteerror-short', |
| 1654 | + 'filedeleteerror-long', |
| 1655 | + 'filedelete-missing', |
| 1656 | + 'filedelete-old-unregistered', |
| 1657 | + 'filedelete-current-unregistered', |
| 1658 | + 'filedelete-archive-read-only', |
1644 | 1659 | ), |
1645 | 1660 | 'browsediffs' => array( |
1646 | 1661 | 'previousdiff', |
Index: branches/liquidthreads/docs/hooks.txt |
— | — | @@ -267,6 +267,17 @@ |
268 | 268 | $user: the user that deleted the article |
269 | 269 | $reason: the reason the article was deleted |
270 | 270 | |
| 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 | + |
271 | 282 | 'ArticleProtect': before an article is protected |
272 | 283 | $article: the article being protected |
273 | 284 | $user: the user doing the protection |
— | — | @@ -290,6 +301,17 @@ |
291 | 302 | $iswatch: watch flag |
292 | 303 | $section: section # |
293 | 304 | |
| 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 | + |
294 | 316 | 'ArticleSaveComplete': after an article is saved |
295 | 317 | $article: the article (object) saved |
296 | 318 | $user: the user (object) who saved the article |
— | — | @@ -299,6 +321,8 @@ |
300 | 322 | $iswatch: watch flag |
301 | 323 | $section: section # |
302 | 324 | |
| 325 | +wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); |
| 326 | + |
303 | 327 | 'ArticleUndeleted': When one or more revisions of an article are restored |
304 | 328 | $title: Title corresponding to the article restored |
305 | 329 | $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 @@ |
1120 | 1120 | /** |
1121 | 1121 | * Get the user ID. Returns 0 if the user is anonymous or nonexistent. |
1122 | 1122 | */ |
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; |
1126 | 1133 | } |
1127 | 1134 | |
1128 | 1135 | /** |
— | — | @@ -1715,7 +1722,11 @@ |
1716 | 1723 | * @return bool |
1717 | 1724 | */ |
1718 | 1725 | 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; |
1720 | 1731 | } |
1721 | 1732 | |
1722 | 1733 | /** |
Index: branches/liquidthreads/includes/BagOStuff.php |
— | — | @@ -709,6 +709,7 @@ |
710 | 710 | |
711 | 711 | function delete( $key, $time = 0 ) { |
712 | 712 | wfProfileIn( __METHOD__ ); |
| 713 | + wfDebug( __METHOD__."($key)\n" ); |
713 | 714 | $handle = $this->getWriter(); |
714 | 715 | if ( !$handle ) { |
715 | 716 | return false; |
Index: branches/liquidthreads/includes/Article.php |
— | — | @@ -1457,19 +1457,16 @@ |
1458 | 1458 | # Clear caches |
1459 | 1459 | Article::onArticleCreate( $this->mTitle ); |
1460 | 1460 | |
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 ) ); |
1464 | 1463 | } |
1465 | 1464 | |
1466 | 1465 | if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) { |
1467 | 1466 | wfDoUpdates(); |
1468 | 1467 | } |
1469 | 1468 | |
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 ) ); |
1474 | 1471 | |
1475 | 1472 | wfProfileOut( __METHOD__ ); |
1476 | 1473 | return $good; |
— | — | @@ -2116,7 +2113,8 @@ |
2117 | 2114 | 'ar_text_id' => 'rev_text_id', |
2118 | 2115 | 'ar_text' => '\'\'', // Be explicit to appease |
2119 | 2116 | 'ar_flags' => '\'\'', // MySQL's "strict mode"... |
2120 | | - 'ar_len' => 'rev_len' |
| 2117 | + 'ar_len' => 'rev_len', |
| 2118 | + 'ar_page' => $id |
2121 | 2119 | ), array( |
2122 | 2120 | 'page_id' => $id, |
2123 | 2121 | 'page_id = rev_page' |
— | — | @@ -2805,6 +2803,7 @@ |
2806 | 2804 | $page = $this->mTitle->getSubjectPage(); |
2807 | 2805 | |
2808 | 2806 | $wgOut->setPagetitle( $page->getPrefixedText() ); |
| 2807 | + $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); |
2809 | 2808 | $wgOut->setSubtitle( wfMsg( 'infosubtitle' )); |
2810 | 2809 | |
2811 | 2810 | # first, see if the page exists at all. |
— | — | @@ -3062,4 +3061,4 @@ |
3063 | 3062 | $wgOut->addParserOutput( $parserOutput ); |
3064 | 3063 | } |
3065 | 3064 | |
3066 | | -} |
\ No newline at end of file |
| 3065 | +} |
Index: branches/liquidthreads/includes/GlobalFunctions.php |
— | — | @@ -2296,4 +2296,4 @@ |
2297 | 2297 | */ |
2298 | 2298 | function wfBoolToStr( $value ) { |
2299 | 2299 | return $value ? 'true' : 'false'; |
2300 | | -} |
\ No newline at end of file |
| 2300 | +} |
Index: branches/liquidthreads/includes/ImagePage.php |
— | — | @@ -579,56 +579,56 @@ |
580 | 580 | $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); |
581 | 581 | return; |
582 | 582 | } |
583 | | - if ( !$this->doDeleteOldImage( $oldimage ) ) { |
584 | | - return; |
585 | | - } |
| 583 | + $status = $this->doDeleteOldImage( $oldimage ); |
586 | 584 | $deleted = $oldimage; |
587 | 585 | } else { |
588 | | - $ok = $this->img->delete( $reason ); |
589 | | - if( !$ok ) { |
590 | | - # If the deletion operation actually failed, bug out: |
591 | | - $wgOut->showFileDeleteError( $this->img->getName() ); |
592 | | - return; |
| 586 | + $status = $this->img->delete( $reason ); |
| 587 | + if ( !$status->isGood() ) { |
| 588 | + // Warning or error |
| 589 | + $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); |
593 | 590 | } |
594 | | - |
595 | | - # Image itself is now gone, and database is cleaned. |
596 | | - # Now we remove the image description page. |
597 | | - |
598 | | - $article = new Article( $this->mTitle ); |
599 | | - $article->doDeleteArticle( $reason ); # ignore errors |
600 | | - |
601 | | - $deleted = $this->img->getName(); |
| 591 | + if ( $status->ok ) { |
| 592 | + # Image itself is now gone, and database is cleaned. |
| 593 | + # Now we remove the image description page. |
| 594 | + $article = new Article( $this->mTitle ); |
| 595 | + $article->doDeleteArticle( $reason ); # ignore errors |
| 596 | + $deleted = $this->img->getName(); |
| 597 | + } |
602 | 598 | } |
603 | 599 | |
604 | | - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); |
605 | 600 | $wgOut->setRobotpolicy( 'noindex,nofollow' ); |
606 | 601 | |
607 | | - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; |
608 | | - $text = wfMsg( 'deletedtext', $deleted, $loglink ); |
609 | | - |
610 | | - $wgOut->addWikiText( $text ); |
611 | | - |
612 | | - $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); |
| 602 | + if ( !$status->ok ) { |
| 603 | + // Fatal error flagged |
| 604 | + $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); |
| 605 | + $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); |
| 606 | + } else { |
| 607 | + // Operation completed |
| 608 | + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); |
| 609 | + $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; |
| 610 | + $text = wfMsg( 'deletedtext', $deleted, $loglink ); |
| 611 | + $wgOut->addWikiText( $text ); |
| 612 | + $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); |
| 613 | + } |
613 | 614 | } |
614 | 615 | |
615 | 616 | /** |
616 | | - * @return success |
| 617 | + * Delete an old revision of an image, |
| 618 | + * @return FileRepoStatus |
617 | 619 | */ |
618 | | - function doDeleteOldImage( $oldimage ) |
619 | | - { |
| 620 | + function doDeleteOldImage( $oldimage ) { |
620 | 621 | global $wgOut; |
621 | 622 | |
622 | | - $ok = $this->img->deleteOld( $oldimage, '' ); |
623 | | - if( !$ok ) { |
624 | | - # If we actually have a file and can't delete it, throw an error. |
625 | | - # Something went awry... |
626 | | - $wgOut->showFileDeleteError( "$oldimage" ); |
627 | | - } else { |
| 623 | + $status = $this->img->deleteOld( $oldimage, '' ); |
| 624 | + if( !$status->isGood() ) { |
| 625 | + $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); |
| 626 | + } |
| 627 | + if ( $status->ok ) { |
628 | 628 | # Log the deletion |
629 | 629 | $log = new LogPage( 'delete' ); |
630 | 630 | $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) ); |
631 | 631 | } |
632 | | - return $ok; |
| 632 | + return $status; |
633 | 633 | } |
634 | 634 | |
635 | 635 | function revert() { |
— | — | @@ -667,10 +667,11 @@ |
668 | 668 | |
669 | 669 | $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage ); |
670 | 670 | $comment = wfMsg( "reverted" ); |
671 | | - $result = $this->img->upload( $sourcePath, $comment, $comment ); |
| 671 | + // TODO: preserve file properties from DB instead of reloading from file |
| 672 | + $status = $this->img->upload( $sourcePath, $comment, $comment ); |
672 | 673 | |
673 | | - if ( WikiError::isError( $result ) ) { |
674 | | - $this->showError( $result ); |
| 674 | + if ( !$status->isGood() ) { |
| 675 | + $this->showError( $status->getWikiText() ); |
675 | 676 | return; |
676 | 677 | } |
677 | 678 | |
— | — | @@ -699,15 +700,15 @@ |
700 | 701 | } |
701 | 702 | |
702 | 703 | /** |
703 | | - * Display an error from a wikitext-formatted WikiError object |
| 704 | + * Display an error with a wikitext description |
704 | 705 | */ |
705 | | - function showError( WikiError $error ) { |
| 706 | + function showError( $description ) { |
706 | 707 | global $wgOut; |
707 | 708 | $wgOut->setPageTitle( wfMsg( "internalerror" ) ); |
708 | 709 | $wgOut->setRobotpolicy( "noindex,nofollow" ); |
709 | 710 | $wgOut->setArticleRelated( false ); |
710 | 711 | $wgOut->enableClientCache( false ); |
711 | | - $wgOut->addWikiText( $error->getMessage() ); |
| 712 | + $wgOut->addWikiText( $description ); |
712 | 713 | } |
713 | 714 | |
714 | 715 | } |
Index: branches/liquidthreads/includes/Linker.php |
— | — | @@ -1094,7 +1094,7 @@ |
1095 | 1095 | * @param $hook String, name of hook to run |
1096 | 1096 | * @return String, HTML to use for edit link |
1097 | 1097 | */ |
1098 | | - private function doEditSectionLink( Title $nt, $section, $hint, $hook ) { |
| 1098 | + protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) { |
1099 | 1099 | global $wgContLang; |
1100 | 1100 | $editurl = '§ion='.$section; |
1101 | 1101 | $url = $this->makeKnownLinkObj( |
Index: branches/liquidthreads/includes/Setup.php |
— | — | @@ -54,6 +54,11 @@ |
55 | 55 | if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; |
56 | 56 | if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; |
57 | 57 | |
| 58 | +if ( empty( $wgFileStore['deleted']['directory'] ) ) { |
| 59 | + $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted"; |
| 60 | +} |
| 61 | + |
| 62 | + |
58 | 63 | /** |
59 | 64 | * Initialise $wgLocalFileRepo from backwards-compatible settings |
60 | 65 | */ |
— | — | @@ -67,6 +72,8 @@ |
68 | 73 | 'thumbScriptUrl' => $wgThumbnailScriptPath, |
69 | 74 | 'transformVia404' => !$wgGenerateThumbnailOnParse, |
70 | 75 | 'initialCapital' => $wgCapitalLinks, |
| 76 | + 'deletedDir' => $wgFileStore['deleted']['directory'], |
| 77 | + 'deletedHashLevels' => $wgFileStore['deleted']['hash'] |
71 | 78 | ); |
72 | 79 | } |
73 | 80 | /** |
— | — | @@ -87,7 +94,7 @@ |
88 | 95 | 'dbUser' => $wgDBuser, |
89 | 96 | 'dbPassword' => $wgDBpassword, |
90 | 97 | 'dbName' => $wgSharedUploadDBname, |
91 | | - 'dbFlags' => DBO_DEFAULT, |
| 98 | + 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT, |
92 | 99 | 'tablePrefix' => $wgSharedUploadDBprefix, |
93 | 100 | 'hasSharedCache' => $wgCacheSharedUploads, |
94 | 101 | 'descBaseUrl' => $wgRepositoryBaseUrl, |
Index: branches/liquidthreads/includes/filerepo/FileRepo.php |
— | — | @@ -6,9 +6,12 @@ |
7 | 7 | */ |
8 | 8 | abstract class FileRepo { |
9 | 9 | const DELETE_SOURCE = 1; |
| 10 | + const OVERWRITE = 2; |
| 11 | + const OVERWRITE_SAME = 4; |
10 | 12 | |
11 | 13 | var $thumbScriptUrl, $transformVia404; |
12 | 14 | var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; |
| 15 | + var $pathDisclosureProtection = 'paranoid'; |
13 | 16 | |
14 | 17 | /** |
15 | 18 | * Factory functions for creating new files |
— | — | @@ -23,7 +26,7 @@ |
24 | 27 | // Optional settings |
25 | 28 | $this->initialCapital = true; // by default |
26 | 29 | foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', |
27 | | - 'thumbScriptUrl', 'initialCapital' ) as $var ) |
| 30 | + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var ) |
28 | 31 | { |
29 | 32 | if ( isset( $info[$var] ) ) { |
30 | 33 | $this->$var = $info[$var]; |
— | — | @@ -200,12 +203,37 @@ |
201 | 204 | |
202 | 205 | /** |
203 | 206 | * Store a file to a given destination. |
| 207 | + * |
| 208 | + * @param string $srcPath Source path or virtual URL |
| 209 | + * @param string $dstZone Destination zone |
| 210 | + * @param string $dstRel Destination relative path |
| 211 | + * @param integer $flags Bitwise combination of the following flags: |
| 212 | + * self::DELETE_SOURCE Delete the source file after upload |
| 213 | + * self::OVERWRITE Overwrite an existing destination file instead of failing |
| 214 | + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the |
| 215 | + * same contents as the source |
| 216 | + * @return FileRepoStatus |
204 | 217 | */ |
205 | | - abstract function store( $srcPath, $dstZone, $dstRel, $flags = 0 ); |
| 218 | + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
| 219 | + $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); |
| 220 | + if ( $status->successCount == 0 ) { |
| 221 | + $status->ok = false; |
| 222 | + } |
| 223 | + return $status; |
| 224 | + } |
206 | 225 | |
207 | 226 | /** |
| 227 | + * Store a batch of files |
| 228 | + * |
| 229 | + * @param array $triplets (src,zone,dest) triplets as per store() |
| 230 | + * @param integer $flags Flags as per store |
| 231 | + */ |
| 232 | + abstract function storeBatch( $triplets, $flags = 0 ); |
| 233 | + |
| 234 | + /** |
208 | 235 | * Pick a random name in the temp zone and store a file to it. |
209 | | - * Returns the URL, or a WikiError on failure. |
| 236 | + * Returns a FileRepoStatus object with the URL in the value. |
| 237 | + * |
210 | 238 | * @param string $originalName The base name of the file as specified |
211 | 239 | * by the user. The file extension will be maintained. |
212 | 240 | * @param string $srcPath The current location of the file. |
— | — | @@ -226,6 +254,9 @@ |
227 | 255 | * Copy or move a file either from the local filesystem or from an mwrepo:// |
228 | 256 | * virtual URL, into this repository at the specified destination location. |
229 | 257 | * |
| 258 | + * Returns a FileRepoStatus object. On success, the value contains "new" or |
| 259 | + * "archived", to indicate whether the file was new with that name. |
| 260 | + * |
230 | 261 | * @param string $srcPath The source path or URL |
231 | 262 | * @param string $dstRel The destination relative path |
232 | 263 | * @param string $archiveRel The relative path where the existing file is to |
— | — | @@ -233,9 +264,59 @@ |
234 | 265 | * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate |
235 | 266 | * that the source file should be deleted if possible |
236 | 267 | */ |
237 | | - abstract function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ); |
| 268 | + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
| 269 | + $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags ); |
| 270 | + if ( $status->successCount == 0 ) { |
| 271 | + $status->ok = false; |
| 272 | + } |
| 273 | + if ( isset( $status->value[0] ) ) { |
| 274 | + $status->value = $status->value[0]; |
| 275 | + } else { |
| 276 | + $status->value = false; |
| 277 | + } |
| 278 | + return $status; |
| 279 | + } |
238 | 280 | |
239 | 281 | /** |
| 282 | + * Publish a batch of files |
| 283 | + * @param array $triplets (source,dest,archive) triplets as per publish() |
| 284 | + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate |
| 285 | + * that the source files should be deleted if possible |
| 286 | + */ |
| 287 | + abstract function publishBatch( $triplets, $flags = 0 ); |
| 288 | + |
| 289 | + /** |
| 290 | + * Move a group of files to the deletion archive. |
| 291 | + * |
| 292 | + * If no valid deletion archive is configured, this may either delete the |
| 293 | + * file or throw an exception, depending on the preference of the repository. |
| 294 | + * |
| 295 | + * The overwrite policy is determined by the repository -- currently FSRepo |
| 296 | + * assumes a naming scheme in the deleted zone based on content hash, as |
| 297 | + * opposed to the public zone which is assumed to be unique. |
| 298 | + * |
| 299 | + * @param array $sourceDestPairs Array of source/destination pairs. Each element |
| 300 | + * is a two-element array containing the source file path relative to the |
| 301 | + * public root in the first element, and the archive file path relative |
| 302 | + * to the deleted zone root in the second element. |
| 303 | + * @return FileRepoStatus |
| 304 | + */ |
| 305 | + abstract function deleteBatch( $sourceDestPairs ); |
| 306 | + |
| 307 | + /** |
| 308 | + * Move a file to the deletion archive. |
| 309 | + * If no valid deletion archive exists, this may either delete the file |
| 310 | + * or throw an exception, depending on the preference of the repository |
| 311 | + * @param mixed $srcRel Relative path for the file to be deleted |
| 312 | + * @param mixed $archiveRel Relative path for the archive location. |
| 313 | + * Relative to a private archive directory. |
| 314 | + * @return WikiError object (wikitext-formatted), or true for success |
| 315 | + */ |
| 316 | + function delete( $srcRel, $archiveRel ) { |
| 317 | + return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); |
| 318 | + } |
| 319 | + |
| 320 | + /** |
240 | 321 | * Get properties of a file with a given virtual URL |
241 | 322 | * The virtual URL must refer to this repo |
242 | 323 | * Properties should ultimately be obtained via File::getPropsFromPath() |
— | — | @@ -276,5 +357,48 @@ |
277 | 358 | return true; |
278 | 359 | } |
279 | 360 | } |
| 361 | + |
| 362 | + /**#@+ |
| 363 | + * Path disclosure protection functions |
| 364 | + */ |
| 365 | + function paranoidClean( $param ) { return '[hidden]'; } |
| 366 | + function passThrough( $param ) { return $param; } |
| 367 | + |
| 368 | + /** |
| 369 | + * Get a callback function to use for cleaning error message parameters |
| 370 | + */ |
| 371 | + function getErrorCleanupFunction() { |
| 372 | + switch ( $this->pathDisclosureProtection ) { |
| 373 | + case 'none': |
| 374 | + $callback = array( $this, 'passThrough' ); |
| 375 | + break; |
| 376 | + default: // 'paranoid' |
| 377 | + $callback = array( $this, 'paranoidClean' ); |
| 378 | + } |
| 379 | + return $callback; |
| 380 | + } |
| 381 | + /**#@-*/ |
| 382 | + |
| 383 | + /** |
| 384 | + * Create a new fatal error |
| 385 | + */ |
| 386 | + function newFatal( $message /*, parameters...*/ ) { |
| 387 | + $params = func_get_args(); |
| 388 | + array_unshift( $params, $this ); |
| 389 | + return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params ); |
| 390 | + } |
| 391 | + |
| 392 | + /** |
| 393 | + * Create a new good result |
| 394 | + */ |
| 395 | + function newGood( $value = null ) { |
| 396 | + return FileRepoStatus::newGood( $this, $value ); |
| 397 | + } |
| 398 | + |
| 399 | + /** |
| 400 | + * Delete files in the deleted directory if they are not referenced in the filearchive table |
| 401 | + * STUB |
| 402 | + */ |
| 403 | + function cleanupDeletedBatch( $storageKeys ) {} |
280 | 404 | } |
281 | 405 | |
Index: branches/liquidthreads/includes/filerepo/ForeignDBRepo.php |
— | — | @@ -46,6 +46,12 @@ |
47 | 47 | function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
48 | 48 | throw new MWException( get_class($this) . ': write operations are not supported' ); |
49 | 49 | } |
| 50 | + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
| 51 | + throw new MWException( get_class($this) . ': write operations are not supported' ); |
| 52 | + } |
| 53 | + function deleteBatch( $fileMap ) { |
| 54 | + throw new MWException( get_class($this) . ': write operations are not supported' ); |
| 55 | + } |
50 | 56 | } |
51 | 57 | |
52 | 58 | |
Index: 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 |
1 | 172 | + native |
Index: branches/liquidthreads/includes/filerepo/OldLocalFile.php |
— | — | @@ -70,7 +70,7 @@ |
71 | 71 | } |
72 | 72 | $oldImages = $wgMemc->get( $key ); |
73 | 73 | |
74 | | - if ( isset( $oldImages['version'] ) && $oldImages['version'] == MW_OLDFILE_VERSION ) { |
| 74 | + if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) { |
75 | 75 | unset( $oldImages['version'] ); |
76 | 76 | $more = isset( $oldImages['more'] ); |
77 | 77 | unset( $oldImages['more'] ); |
— | — | @@ -94,7 +94,8 @@ |
95 | 95 | if ( $found ) { |
96 | 96 | wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" ); |
97 | 97 | $this->dataLoaded = true; |
98 | | - foreach ( $cachedValues as $name => $value ) { |
| 98 | + $this->fileExists = true; |
| 99 | + foreach ( $info as $name => $value ) { |
99 | 100 | $this->$name = $value; |
100 | 101 | } |
101 | 102 | } elseif ( $more ) { |
— | — | @@ -130,7 +131,7 @@ |
131 | 132 | wfProfileIn( __METHOD__ ); |
132 | 133 | |
133 | 134 | $dbr = $this->repo->getSlaveDB(); |
134 | | - $res = $dbr->select( 'oldimage', $this->getCacheFields(), |
| 135 | + $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ), |
135 | 136 | array( 'oi_name' => $this->getName() ), __METHOD__, |
136 | 137 | array( |
137 | 138 | 'LIMIT' => self::MAX_CACHE_ROWS + 1, |
— | — | @@ -144,8 +145,8 @@ |
145 | 146 | } |
146 | 147 | for ( $i = 0; $i < $numRows; $i++ ) { |
147 | 148 | $row = $dbr->fetchObject( $res ); |
148 | | - $this->decodeRow( $row, 'oi_' ); |
149 | | - $cache[$row->oi_timestamp] = $row; |
| 149 | + $decoded = $this->decodeRow( $row, 'oi_' ); |
| 150 | + $cache[$row->oi_timestamp] = $decoded; |
150 | 151 | } |
151 | 152 | $dbr->freeResult( $res ); |
152 | 153 | $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ ); |
— | — | @@ -169,6 +170,7 @@ |
170 | 171 | $this->fileExists = false; |
171 | 172 | } |
172 | 173 | $this->dataLoaded = true; |
| 174 | + wfProfileOut( __METHOD__ ); |
173 | 175 | } |
174 | 176 | |
175 | 177 | function getCacheFields( $prefix = 'img_' ) { |
— | — | @@ -207,15 +209,14 @@ |
208 | 210 | #'oi_major_mime' => $major, |
209 | 211 | #'oi_minor_mime' => $minor, |
210 | 212 | #'oi_metadata' => $this->metadata, |
211 | | - ), array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->requestedTime ), |
| 213 | + 'oi_sha1' => $this->sha1, |
| 214 | + ), array( |
| 215 | + 'oi_name' => $this->getName(), |
| 216 | + 'oi_archive_name' => $this->archive_name ), |
212 | 217 | __METHOD__ |
213 | 218 | ); |
214 | 219 | wfProfileOut( __METHOD__ ); |
215 | 220 | } |
216 | | - |
217 | | - // XXX: Temporary hack before schema update |
218 | | - function maybeUpgradeRow() {} |
219 | | - |
220 | 221 | } |
221 | 222 | |
222 | 223 | |
Index: branches/liquidthreads/includes/filerepo/LocalFile.php |
— | — | @@ -41,10 +41,12 @@ |
42 | 42 | $major_mime, # Major mime type |
43 | 43 | $minor_mine, # Minor mime type |
44 | 44 | $size, # Size in bytes (loadFromXxx) |
45 | | - $metadata, # Metadata |
| 45 | + $metadata, # Handler-specific metadata |
46 | 46 | $timestamp, # Upload timestamp |
| 47 | + $sha1, # SHA-1 base 36 content hash |
47 | 48 | $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) |
48 | | - $upgraded; # Whether the row was upgraded on load |
| 49 | + $upgraded, # Whether the row was upgraded on load |
| 50 | + $locked; # True if the image row is locked |
49 | 51 | |
50 | 52 | /**#@-*/ |
51 | 53 | |
— | — | @@ -156,7 +158,7 @@ |
157 | 159 | |
158 | 160 | function getCacheFields( $prefix = 'img_' ) { |
159 | 161 | static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', |
160 | | - 'major_mime', 'minor_mime', 'metadata', 'timestamp' ); |
| 162 | + 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' ); |
161 | 163 | static $results = array(); |
162 | 164 | if ( $prefix == '' ) { |
163 | 165 | return $fields; |
— | — | @@ -175,7 +177,9 @@ |
176 | 178 | * Load file metadata from the DB |
177 | 179 | */ |
178 | 180 | function loadFromDB() { |
179 | | - wfProfileIn( __METHOD__ ); |
| 181 | + # Polymorphic function name to distinguish foreign and local fetches |
| 182 | + $fname = get_class( $this ) . '::' . __FUNCTION__; |
| 183 | + wfProfileIn( $fname ); |
180 | 184 | |
181 | 185 | # Unconditionally set loaded=true, we don't want the accessors constantly rechecking |
182 | 186 | $this->dataLoaded = true; |
— | — | @@ -183,14 +187,14 @@ |
184 | 188 | $dbr = $this->repo->getSlaveDB(); |
185 | 189 | |
186 | 190 | $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), |
187 | | - array( 'img_name' => $this->getName() ), __METHOD__ ); |
| 191 | + array( 'img_name' => $this->getName() ), $fname ); |
188 | 192 | if ( $row ) { |
189 | 193 | $this->loadFromRow( $row ); |
190 | 194 | } else { |
191 | 195 | $this->fileExists = false; |
192 | 196 | } |
193 | 197 | |
194 | | - wfProfileOut( __METHOD__ ); |
| 198 | + wfProfileOut( $fname ); |
195 | 199 | } |
196 | 200 | |
197 | 201 | /** |
— | — | @@ -218,6 +222,8 @@ |
219 | 223 | } |
220 | 224 | $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime']; |
221 | 225 | } |
| 226 | + # Trim zero padding from char/binary field |
| 227 | + $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); |
222 | 228 | return $decoded; |
223 | 229 | } |
224 | 230 | |
— | — | @@ -254,7 +260,10 @@ |
255 | 261 | if ( wfReadOnly() ) { |
256 | 262 | return; |
257 | 263 | } |
258 | | - if ( is_null($this->media_type) || $this->mime == 'image/svg' ) { |
| 264 | + if ( is_null($this->media_type) || |
| 265 | + $this->mime == 'image/svg' || |
| 266 | + $this->sha1 == '' |
| 267 | + ) { |
259 | 268 | $this->upgradeRow(); |
260 | 269 | $this->upgraded = true; |
261 | 270 | } else { |
— | — | @@ -292,6 +301,7 @@ |
293 | 302 | 'img_major_mime' => $major, |
294 | 303 | 'img_minor_mime' => $minor, |
295 | 304 | 'img_metadata' => $this->metadata, |
| 305 | + 'img_sha1' => $this->sha1, |
296 | 306 | ), array( 'img_name' => $this->getName() ), |
297 | 307 | __METHOD__ |
298 | 308 | ); |
— | — | @@ -480,12 +490,23 @@ |
481 | 491 | function purgeMetadataCache() { |
482 | 492 | $this->loadFromDB(); |
483 | 493 | $this->saveToCache(); |
| 494 | + $this->purgeHistory(); |
484 | 495 | } |
485 | 496 | |
486 | 497 | /** |
| 498 | + * Purge the shared history (OldLocalFile) cache |
| 499 | + */ |
| 500 | + function purgeHistory() { |
| 501 | + global $wgMemc; |
| 502 | + $hashedName = md5($this->getName()); |
| 503 | + $oldKey = wfMemcKey( 'oldfile', $hashedName ); |
| 504 | + $wgMemc->delete( $oldKey ); |
| 505 | + } |
| 506 | + |
| 507 | + /** |
487 | 508 | * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid |
488 | 509 | */ |
489 | | - function purgeCache( $archiveFiles = array() ) { |
| 510 | + function purgeCache() { |
490 | 511 | // Refresh metadata cache |
491 | 512 | $this->purgeMetadataCache(); |
492 | 513 | |
— | — | @@ -596,6 +617,8 @@ |
597 | 618 | /** getHashPath inherited */ |
598 | 619 | /** getRel inherited */ |
599 | 620 | /** getUrlRel inherited */ |
| 621 | + /** getArchiveRel inherited */ |
| 622 | + /** getThumbRel inherited */ |
600 | 623 | /** getArchivePath inherited */ |
601 | 624 | /** getThumbPath inherited */ |
602 | 625 | /** getArchiveUrl inherited */ |
— | — | @@ -615,18 +638,19 @@ |
616 | 639 | * is already known |
617 | 640 | * @param string $timestamp Timestamp for img_timestamp, or false to use the current time |
618 | 641 | * |
619 | | - * @return Returns the archive name on success or an empty string if it was a new upload. |
620 | | - * Returns a wikitext-formatted WikiError on failure. |
| 642 | + * @return FileRepoStatus object. On success, the value member contains the |
| 643 | + * archive name, or an empty string if it was a new file. |
621 | 644 | */ |
622 | 645 | function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) { |
623 | | - $archive = $this->publish( $srcPath, $flags ); |
624 | | - if ( WikiError::isError( $archive ) ){ |
625 | | - return $archive; |
| 646 | + $this->lock(); |
| 647 | + $status = $this->publish( $srcPath, $flags ); |
| 648 | + if ( $status->ok ) { |
| 649 | + if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) { |
| 650 | + $status->fatal( 'filenotfound', $srcPath ); |
| 651 | + } |
626 | 652 | } |
627 | | - if ( !$this->recordUpload2( $archive, $comment, $pageText, $props, $timestamp ) ) { |
628 | | - return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) ); |
629 | | - } |
630 | | - return $archive; |
| 653 | + $this->unlock(); |
| 654 | + return $status; |
631 | 655 | } |
632 | 656 | |
633 | 657 | /** |
— | — | @@ -695,6 +719,7 @@ |
696 | 720 | 'img_user' => $wgUser->getID(), |
697 | 721 | 'img_user_text' => $wgUser->getName(), |
698 | 722 | 'img_metadata' => $this->metadata, |
| 723 | + 'img_sha1' => $this->sha1 |
699 | 724 | ), |
700 | 725 | __METHOD__, |
701 | 726 | 'IGNORE' |
— | — | @@ -715,6 +740,11 @@ |
716 | 741 | 'oi_description' => 'img_description', |
717 | 742 | 'oi_user' => 'img_user', |
718 | 743 | 'oi_user_text' => 'img_user_text', |
| 744 | + 'oi_metadata' => 'img_metadata', |
| 745 | + 'oi_media_type' => 'img_media_type', |
| 746 | + 'oi_major_mime' => 'img_major_mime', |
| 747 | + 'oi_minor_mime' => 'img_minor_mime', |
| 748 | + 'oi_sha1' => 'img_sha1', |
719 | 749 | ), array( 'img_name' => $this->getName() ), __METHOD__ |
720 | 750 | ); |
721 | 751 | |
— | — | @@ -733,6 +763,7 @@ |
734 | 764 | 'img_user' => $wgUser->getID(), |
735 | 765 | 'img_user_text' => $wgUser->getName(), |
736 | 766 | 'img_metadata' => $this->metadata, |
| 767 | + 'img_sha1' => $this->sha1 |
737 | 768 | ), array( /* WHERE */ |
738 | 769 | 'img_name' => $this->getName() |
739 | 770 | ), __METHOD__ |
— | — | @@ -792,22 +823,23 @@ |
793 | 824 | * @param integer $flags A bitwise combination of: |
794 | 825 | * File::DELETE_SOURCE Delete the source file, i.e. move |
795 | 826 | * rather than copy |
796 | | - * @return The archive name on success or an empty string if it was a new |
797 | | - * file, and a wikitext-formatted WikiError object on failure. |
| 827 | + * @return FileRepoStatus object. On success, the value member contains the |
| 828 | + * archive name, or an empty string if it was a new file. |
798 | 829 | */ |
799 | 830 | function publish( $srcPath, $flags = 0 ) { |
| 831 | + $this->lock(); |
800 | 832 | $dstRel = $this->getRel(); |
801 | 833 | $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); |
802 | 834 | $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; |
803 | 835 | $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; |
804 | 836 | $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags ); |
805 | | - if ( WikiError::isError( $status ) ) { |
806 | | - return $status; |
807 | | - } elseif ( $status == 'new' ) { |
808 | | - return ''; |
| 837 | + if ( $status->value == 'new' ) { |
| 838 | + $status->value = ''; |
809 | 839 | } else { |
810 | | - return $archiveName; |
| 840 | + $status->value = $archiveName; |
811 | 841 | } |
| 842 | + $this->unlock(); |
| 843 | + return $status; |
812 | 844 | } |
813 | 845 | |
814 | 846 | /** getLinksTo inherited */ |
— | — | @@ -824,62 +856,34 @@ |
825 | 857 | * Cache purging is done; logging is caller's responsibility. |
826 | 858 | * |
827 | 859 | * @param $reason |
828 | | - * @return true on success, false on some kind of failure |
| 860 | + * @return FileRepoStatus object. |
829 | 861 | */ |
830 | | - function delete( $reason, $suppress=false ) { |
831 | | - $transaction = new FSTransaction(); |
832 | | - $urlArr = array( $this->getURL() ); |
| 862 | + function delete( $reason ) { |
| 863 | + $this->lock(); |
| 864 | + $batch = new LocalFileDeleteBatch( $this, $reason ); |
| 865 | + $batch->addCurrent(); |
833 | 866 | |
834 | | - if( !FileStore::lock() ) { |
835 | | - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); |
836 | | - return false; |
| 867 | + # Get old version relative paths |
| 868 | + $dbw = $this->repo->getMasterDB(); |
| 869 | + $result = $dbw->select( 'oldimage', |
| 870 | + array( 'oi_archive_name' ), |
| 871 | + array( 'oi_name' => $this->getName() ) ); |
| 872 | + while ( $row = $dbw->fetchObject( $result ) ) { |
| 873 | + $batch->addOld( $row->oi_archive_name ); |
837 | 874 | } |
| 875 | + $status = $batch->execute(); |
838 | 876 | |
839 | | - try { |
840 | | - $dbw = $this->repo->getMasterDB(); |
841 | | - $dbw->begin(); |
842 | | - |
843 | | - // Delete old versions |
844 | | - $result = $dbw->select( 'oldimage', |
845 | | - array( 'oi_archive_name' ), |
846 | | - array( 'oi_name' => $this->getName() ) ); |
847 | | - |
848 | | - while( $row = $dbw->fetchObject( $result ) ) { |
849 | | - $oldName = $row->oi_archive_name; |
850 | | - |
851 | | - $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) ); |
852 | | - |
853 | | - // We'll need to purge this URL from caches... |
854 | | - $urlArr[] = $this->getArchiveUrl( $oldName ); |
855 | | - } |
856 | | - $dbw->freeResult( $result ); |
857 | | - |
858 | | - // And the current version... |
859 | | - $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) ); |
860 | | - |
861 | | - $dbw->immediateCommit(); |
862 | | - } catch( MWException $e ) { |
863 | | - wfDebug( __METHOD__.": db error, rolling back file transactions\n" ); |
864 | | - $transaction->rollback(); |
865 | | - FileStore::unlock(); |
866 | | - throw $e; |
| 877 | + if ( $status->ok ) { |
| 878 | + // Update site_stats |
| 879 | + $site_stats = $dbw->tableName( 'site_stats' ); |
| 880 | + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); |
| 881 | + $this->purgeEverything(); |
867 | 882 | } |
868 | 883 | |
869 | | - wfDebug( __METHOD__.": deleted db items, applying file transactions\n" ); |
870 | | - $transaction->commit(); |
871 | | - FileStore::unlock(); |
872 | | - |
873 | | - |
874 | | - // Update site_stats |
875 | | - $site_stats = $dbw->tableName( 'site_stats' ); |
876 | | - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); |
877 | | - |
878 | | - $this->purgeEverything( $urlArr ); |
879 | | - |
880 | | - return true; |
| 884 | + $this->unlock(); |
| 885 | + return $status; |
881 | 886 | } |
882 | 887 | |
883 | | - |
884 | 888 | /** |
885 | 889 | * Delete an old version of the file. |
886 | 890 | * |
— | — | @@ -890,190 +894,22 @@ |
891 | 895 | * |
892 | 896 | * @param $reason |
893 | 897 | * @throws MWException or FSException on database or filestore failure |
894 | | - * @return true on success, false on some kind of failure |
| 898 | + * @return FileRepoStatus object. |
895 | 899 | */ |
896 | | - function deleteOld( $archiveName, $reason, $suppress=false ) { |
897 | | - $transaction = new FSTransaction(); |
898 | | - $urlArr = array(); |
899 | | - |
900 | | - if( !FileStore::lock() ) { |
901 | | - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); |
902 | | - return false; |
| 900 | + function deleteOld( $archiveName, $reason ) { |
| 901 | + $this->lock(); |
| 902 | + $batch = new LocalFileDeleteBatch( $this, $reason ); |
| 903 | + $batch->addOld( $archiveName ); |
| 904 | + $status = $batch->execute(); |
| 905 | + $this->unlock(); |
| 906 | + if ( $status->ok ) { |
| 907 | + $this->purgeDescription(); |
| 908 | + $this->purgeHistory(); |
903 | 909 | } |
904 | | - |
905 | | - $transaction = new FSTransaction(); |
906 | | - try { |
907 | | - $dbw = $this->repo->getMasterDB(); |
908 | | - $dbw->begin(); |
909 | | - $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) ); |
910 | | - $dbw->immediateCommit(); |
911 | | - } catch( MWException $e ) { |
912 | | - wfDebug( __METHOD__.": db error, rolling back file transaction\n" ); |
913 | | - $transaction->rollback(); |
914 | | - FileStore::unlock(); |
915 | | - throw $e; |
916 | | - } |
917 | | - |
918 | | - wfDebug( __METHOD__.": deleted db items, applying file transaction\n" ); |
919 | | - $transaction->commit(); |
920 | | - FileStore::unlock(); |
921 | | - |
922 | | - $this->purgeDescription(); |
923 | | - |
924 | | - // Squid purging |
925 | | - global $wgUseSquid; |
926 | | - if ( $wgUseSquid ) { |
927 | | - $urlArr = array( |
928 | | - $this->getArchiveUrl( $archiveName ), |
929 | | - ); |
930 | | - wfPurgeSquidServers( $urlArr ); |
931 | | - } |
932 | | - return true; |
| 910 | + return $status; |
933 | 911 | } |
934 | 912 | |
935 | 913 | /** |
936 | | - * Delete the current version of a file. |
937 | | - * May throw a database error. |
938 | | - * @return true on success, false on failure |
939 | | - */ |
940 | | - private function prepareDeleteCurrent( $reason, $suppress=false ) { |
941 | | - return $this->prepareDeleteVersion( |
942 | | - $this->getFullPath(), |
943 | | - $reason, |
944 | | - 'image', |
945 | | - array( |
946 | | - 'fa_name' => 'img_name', |
947 | | - 'fa_archive_name' => 'NULL', |
948 | | - 'fa_size' => 'img_size', |
949 | | - 'fa_width' => 'img_width', |
950 | | - 'fa_height' => 'img_height', |
951 | | - 'fa_metadata' => 'img_metadata', |
952 | | - 'fa_bits' => 'img_bits', |
953 | | - 'fa_media_type' => 'img_media_type', |
954 | | - 'fa_major_mime' => 'img_major_mime', |
955 | | - 'fa_minor_mime' => 'img_minor_mime', |
956 | | - 'fa_description' => 'img_description', |
957 | | - 'fa_user' => 'img_user', |
958 | | - 'fa_user_text' => 'img_user_text', |
959 | | - 'fa_timestamp' => 'img_timestamp' ), |
960 | | - array( 'img_name' => $this->getName() ), |
961 | | - $suppress, |
962 | | - __METHOD__ ); |
963 | | - } |
964 | | - |
965 | | - /** |
966 | | - * Delete a given older version of a file. |
967 | | - * May throw a database error. |
968 | | - * @return true on success, false on failure |
969 | | - */ |
970 | | - private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) { |
971 | | - $oldpath = $this->getArchivePath() . |
972 | | - DIRECTORY_SEPARATOR . $archiveName; |
973 | | - return $this->prepareDeleteVersion( |
974 | | - $oldpath, |
975 | | - $reason, |
976 | | - 'oldimage', |
977 | | - array( |
978 | | - 'fa_name' => 'oi_name', |
979 | | - 'fa_archive_name' => 'oi_archive_name', |
980 | | - 'fa_size' => 'oi_size', |
981 | | - 'fa_width' => 'oi_width', |
982 | | - 'fa_height' => 'oi_height', |
983 | | - 'fa_metadata' => 'NULL', |
984 | | - 'fa_bits' => 'oi_bits', |
985 | | - 'fa_media_type' => 'NULL', |
986 | | - 'fa_major_mime' => 'NULL', |
987 | | - 'fa_minor_mime' => 'NULL', |
988 | | - 'fa_description' => 'oi_description', |
989 | | - 'fa_user' => 'oi_user', |
990 | | - 'fa_user_text' => 'oi_user_text', |
991 | | - 'fa_timestamp' => 'oi_timestamp' ), |
992 | | - array( |
993 | | - 'oi_name' => $this->getName(), |
994 | | - 'oi_archive_name' => $archiveName ), |
995 | | - $suppress, |
996 | | - __METHOD__ ); |
997 | | - } |
998 | | - |
999 | | - /** |
1000 | | - * Do the dirty work of backing up an image row and its file |
1001 | | - * (if $wgSaveDeletedFiles is on) and removing the originals. |
1002 | | - * |
1003 | | - * Must be run while the file store is locked and a database |
1004 | | - * transaction is open to avoid race conditions. |
1005 | | - * |
1006 | | - * @return FSTransaction |
1007 | | - */ |
1008 | | - private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) { |
1009 | | - global $wgUser, $wgSaveDeletedFiles; |
1010 | | - |
1011 | | - // Dupe the file into the file store |
1012 | | - if( file_exists( $path ) ) { |
1013 | | - if( $wgSaveDeletedFiles ) { |
1014 | | - $group = 'deleted'; |
1015 | | - |
1016 | | - $store = FileStore::get( $group ); |
1017 | | - $key = FileStore::calculateKey( $path, $this->getExtension() ); |
1018 | | - $transaction = $store->insert( $key, $path, |
1019 | | - FileStore::DELETE_ORIGINAL ); |
1020 | | - } else { |
1021 | | - $group = null; |
1022 | | - $key = null; |
1023 | | - $transaction = FileStore::deleteFile( $path ); |
1024 | | - } |
1025 | | - } else { |
1026 | | - wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" ); |
1027 | | - $group = null; |
1028 | | - $key = null; |
1029 | | - $transaction = new FSTransaction(); // empty |
1030 | | - } |
1031 | | - |
1032 | | - if( $transaction === false ) { |
1033 | | - // Fail to restore? |
1034 | | - wfDebug( __METHOD__.": import to file store failed, aborting\n" ); |
1035 | | - throw new MWException( "Could not archive and delete file $path" ); |
1036 | | - return false; |
1037 | | - } |
1038 | | - |
1039 | | - // Bitfields to further supress the file content |
1040 | | - // Note that currently, live files are stored elsewhere |
1041 | | - // and cannot be partially deleted |
1042 | | - $bitfield = 0; |
1043 | | - if ( $suppress ) { |
1044 | | - $bitfield |= self::DELETED_FILE; |
1045 | | - $bitfield |= self::DELETED_COMMENT; |
1046 | | - $bitfield |= self::DELETED_USER; |
1047 | | - $bitfield |= self::DELETED_RESTRICTED; |
1048 | | - } |
1049 | | - |
1050 | | - $dbw = $this->repo->getMasterDB(); |
1051 | | - $storageMap = array( |
1052 | | - 'fa_storage_group' => $dbw->addQuotes( $group ), |
1053 | | - 'fa_storage_key' => $dbw->addQuotes( $key ), |
1054 | | - |
1055 | | - 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ), |
1056 | | - 'fa_deleted_timestamp' => $dbw->timestamp(), |
1057 | | - 'fa_deleted_reason' => $dbw->addQuotes( $reason ), |
1058 | | - 'fa_deleted' => $bitfield); |
1059 | | - $allFields = array_merge( $storageMap, $fieldMap ); |
1060 | | - |
1061 | | - try { |
1062 | | - if( $wgSaveDeletedFiles ) { |
1063 | | - $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname ); |
1064 | | - } |
1065 | | - $dbw->delete( $table, $where, $fname ); |
1066 | | - } catch( DBQueryError $e ) { |
1067 | | - // Something went horribly wrong! |
1068 | | - // Leave the file as it was... |
1069 | | - wfDebug( __METHOD__.": database error, rolling back file transaction\n" ); |
1070 | | - $transaction->rollback(); |
1071 | | - throw $e; |
1072 | | - } |
1073 | | - |
1074 | | - return $transaction; |
1075 | | - } |
1076 | | - |
1077 | | - /** |
1078 | 914 | * Restore all or specified deleted revisions to the given file. |
1079 | 915 | * Permissions and logging are left to the caller. |
1080 | 916 | * |
— | — | @@ -1081,202 +917,25 @@ |
1082 | 918 | * |
1083 | 919 | * @param $versions set of record ids of deleted items to restore, |
1084 | 920 | * or empty to restore all revisions. |
1085 | | - * @return the number of file revisions restored if successful, |
1086 | | - * or false on failure |
| 921 | + * @return FileRepoStatus |
1087 | 922 | */ |
1088 | | - function restore( $versions=array(), $Unsuppress=false ) { |
1089 | | - global $wgUser; |
1090 | | - |
1091 | | - if( !FileStore::lock() ) { |
1092 | | - wfDebug( __METHOD__." could not acquire filestore lock\n" ); |
1093 | | - return false; |
| 923 | + function restore( $versions = array(), $unsuppress = false ) { |
| 924 | + $batch = new LocalFileRestoreBatch( $this ); |
| 925 | + if ( !$versions ) { |
| 926 | + $batch->addAll(); |
| 927 | + } else { |
| 928 | + $batch->addIds( $versions ); |
1094 | 929 | } |
1095 | | - |
1096 | | - $transaction = new FSTransaction(); |
1097 | | - try { |
1098 | | - $dbw = $this->repo->getMasterDB(); |
1099 | | - $dbw->begin(); |
1100 | | - |
1101 | | - // Re-confirm whether this file presently exists; |
1102 | | - // if no we'll need to create an file record for the |
1103 | | - // first item we restore. |
1104 | | - $exists = $dbw->selectField( 'image', '1', |
1105 | | - array( 'img_name' => $this->getName() ), |
1106 | | - __METHOD__ ); |
1107 | | - |
1108 | | - // Fetch all or selected archived revisions for the file, |
1109 | | - // sorted from the most recent to the oldest. |
1110 | | - $conditions = array( 'fa_name' => $this->getName() ); |
1111 | | - if( $versions ) { |
1112 | | - $conditions['fa_id'] = $versions; |
1113 | | - } |
1114 | | - |
1115 | | - $result = $dbw->select( 'filearchive', '*', |
1116 | | - $conditions, |
1117 | | - __METHOD__, |
1118 | | - array( 'ORDER BY' => 'fa_timestamp DESC' ) ); |
1119 | | - |
1120 | | - if( $dbw->numRows( $result ) < count( $versions ) ) { |
1121 | | - // There's some kind of conflict or confusion; |
1122 | | - // we can't restore everything we were asked to. |
1123 | | - wfDebug( __METHOD__.": couldn't find requested items\n" ); |
1124 | | - $dbw->rollback(); |
1125 | | - FileStore::unlock(); |
1126 | | - return false; |
1127 | | - } |
1128 | | - |
1129 | | - if( $dbw->numRows( $result ) == 0 ) { |
1130 | | - // Nothing to do. |
1131 | | - wfDebug( __METHOD__.": nothing to do\n" ); |
1132 | | - $dbw->rollback(); |
1133 | | - FileStore::unlock(); |
1134 | | - return true; |
1135 | | - } |
1136 | | - |
1137 | | - $revisions = 0; |
1138 | | - while( $row = $dbw->fetchObject( $result ) ) { |
1139 | | - if ( $Unsuppress ) { |
1140 | | - // Currently, fa_deleted flags fall off upon restore, lets be careful about this |
1141 | | - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { |
1142 | | - // Skip restoring file revisions that the user cannot restore |
1143 | | - continue; |
1144 | | - } |
1145 | | - $revisions++; |
1146 | | - $store = FileStore::get( $row->fa_storage_group ); |
1147 | | - if( !$store ) { |
1148 | | - wfDebug( __METHOD__.": skipping row with no file.\n" ); |
1149 | | - continue; |
1150 | | - } |
1151 | | - |
1152 | | - $restoredImage = new self( Title::makeTitle( NS_IMAGE, $row->fa_name ), $this->repo ); |
1153 | | - |
1154 | | - if( $revisions == 1 && !$exists ) { |
1155 | | - $destPath = $restoredImage->getFullPath(); |
1156 | | - $destDir = dirname( $destPath ); |
1157 | | - if ( !is_dir( $destDir ) ) { |
1158 | | - wfMkdirParents( $destDir ); |
1159 | | - } |
1160 | | - |
1161 | | - // We may have to fill in data if this was originally |
1162 | | - // an archived file revision. |
1163 | | - if( is_null( $row->fa_metadata ) ) { |
1164 | | - $tempFile = $store->filePath( $row->fa_storage_key ); |
1165 | | - |
1166 | | - $magic = MimeMagic::singleton(); |
1167 | | - $mime = $magic->guessMimeType( $tempFile, true ); |
1168 | | - $media_type = $magic->getMediaType( $tempFile, $mime ); |
1169 | | - list( $major_mime, $minor_mime ) = self::splitMime( $mime ); |
1170 | | - $handler = MediaHandler::getHandler( $mime ); |
1171 | | - if ( $handler ) { |
1172 | | - $metadata = $handler->getMetadata( false, $tempFile ); |
1173 | | - } else { |
1174 | | - $metadata = ''; |
1175 | | - } |
1176 | | - } else { |
1177 | | - $metadata = $row->fa_metadata; |
1178 | | - $major_mime = $row->fa_major_mime; |
1179 | | - $minor_mime = $row->fa_minor_mime; |
1180 | | - $media_type = $row->fa_media_type; |
1181 | | - } |
1182 | | - |
1183 | | - $table = 'image'; |
1184 | | - $fields = array( |
1185 | | - 'img_name' => $row->fa_name, |
1186 | | - 'img_size' => $row->fa_size, |
1187 | | - 'img_width' => $row->fa_width, |
1188 | | - 'img_height' => $row->fa_height, |
1189 | | - 'img_metadata' => $metadata, |
1190 | | - 'img_bits' => $row->fa_bits, |
1191 | | - 'img_media_type' => $media_type, |
1192 | | - 'img_major_mime' => $major_mime, |
1193 | | - 'img_minor_mime' => $minor_mime, |
1194 | | - 'img_description' => $row->fa_description, |
1195 | | - 'img_user' => $row->fa_user, |
1196 | | - 'img_user_text' => $row->fa_user_text, |
1197 | | - 'img_timestamp' => $row->fa_timestamp ); |
1198 | | - } else { |
1199 | | - $archiveName = $row->fa_archive_name; |
1200 | | - if( $archiveName == '' ) { |
1201 | | - // This was originally a current version; we |
1202 | | - // have to devise a new archive name for it. |
1203 | | - // Format is <timestamp of archiving>!<name> |
1204 | | - $archiveName = |
1205 | | - wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) . |
1206 | | - '!' . $row->fa_name; |
1207 | | - } |
1208 | | - $destDir = $restoredImage->getArchivePath(); |
1209 | | - if ( !is_dir( $destDir ) ) { |
1210 | | - wfMkdirParents( $destDir ); |
1211 | | - } |
1212 | | - $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName; |
1213 | | - |
1214 | | - $table = 'oldimage'; |
1215 | | - $fields = array( |
1216 | | - 'oi_name' => $row->fa_name, |
1217 | | - 'oi_archive_name' => $archiveName, |
1218 | | - 'oi_size' => $row->fa_size, |
1219 | | - 'oi_width' => $row->fa_width, |
1220 | | - 'oi_height' => $row->fa_height, |
1221 | | - 'oi_bits' => $row->fa_bits, |
1222 | | - 'oi_description' => $row->fa_description, |
1223 | | - 'oi_user' => $row->fa_user, |
1224 | | - 'oi_user_text' => $row->fa_user_text, |
1225 | | - 'oi_timestamp' => $row->fa_timestamp ); |
1226 | | - } |
1227 | | - |
1228 | | - $dbw->insert( $table, $fields, __METHOD__ ); |
1229 | | - // @todo this delete is not totally safe, potentially |
1230 | | - $dbw->delete( 'filearchive', |
1231 | | - array( 'fa_id' => $row->fa_id ), |
1232 | | - __METHOD__ ); |
1233 | | - |
1234 | | - // Check if any other stored revisions use this file; |
1235 | | - // if so, we shouldn't remove the file from the deletion |
1236 | | - // archives so they will still work. |
1237 | | - $useCount = $dbw->selectField( 'filearchive', |
1238 | | - 'COUNT(*)', |
1239 | | - array( |
1240 | | - 'fa_storage_group' => $row->fa_storage_group, |
1241 | | - 'fa_storage_key' => $row->fa_storage_key ), |
1242 | | - __METHOD__ ); |
1243 | | - if( $useCount == 0 ) { |
1244 | | - wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" ); |
1245 | | - $flags = FileStore::DELETE_ORIGINAL; |
1246 | | - } else { |
1247 | | - $flags = 0; |
1248 | | - } |
1249 | | - |
1250 | | - $transaction->add( $store->export( $row->fa_storage_key, |
1251 | | - $destPath, $flags ) ); |
1252 | | - } |
1253 | | - |
1254 | | - $dbw->immediateCommit(); |
1255 | | - } catch( MWException $e ) { |
1256 | | - wfDebug( __METHOD__." caught error, aborting\n" ); |
1257 | | - $transaction->rollback(); |
1258 | | - $dbw->rollback(); |
1259 | | - throw $e; |
| 930 | + $status = $batch->execute(); |
| 931 | + if ( !$status->ok ) { |
| 932 | + return $status; |
1260 | 933 | } |
1261 | 934 | |
1262 | | - $transaction->commit(); |
1263 | | - FileStore::unlock(); |
1264 | | - |
1265 | | - if( $revisions > 0 ) { |
1266 | | - if( !$exists ) { |
1267 | | - wfDebug( __METHOD__." restored $revisions items, creating a new current\n" ); |
1268 | | - |
1269 | | - // Update site_stats |
1270 | | - $site_stats = $dbw->tableName( 'site_stats' ); |
1271 | | - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); |
1272 | | - |
1273 | | - $this->purgeEverything(); |
1274 | | - } else { |
1275 | | - wfDebug( __METHOD__." restored $revisions as archived versions\n" ); |
1276 | | - $this->purgeDescription(); |
1277 | | - } |
1278 | | - } |
1279 | | - |
1280 | | - return $revisions; |
| 935 | + $cleanupStatus = $batch->cleanup(); |
| 936 | + $cleanupStatus->successCount = 0; |
| 937 | + $cleanupStatus->failCount = 0; |
| 938 | + $status->merge( $cleanupStatus ); |
| 939 | + return $status; |
1281 | 940 | } |
1282 | 941 | |
1283 | 942 | /** isMultipage inherited */ |
— | — | @@ -1310,8 +969,52 @@ |
1311 | 970 | $this->load(); |
1312 | 971 | return $this->timestamp; |
1313 | 972 | } |
| 973 | + |
| 974 | + function getSha1() { |
| 975 | + $this->load(); |
| 976 | + return $this->sha1; |
| 977 | + } |
| 978 | + |
| 979 | + /** |
| 980 | + * Start a transaction and lock the image for update |
| 981 | + * Increments a reference counter if the lock is already held |
| 982 | + * @return boolean True if the image exists, false otherwise |
| 983 | + */ |
| 984 | + function lock() { |
| 985 | + $dbw = $this->repo->getMasterDB(); |
| 986 | + if ( !$this->locked ) { |
| 987 | + $dbw->begin(); |
| 988 | + $this->locked++; |
| 989 | + } |
| 990 | + return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ ); |
| 991 | + } |
| 992 | + |
| 993 | + /** |
| 994 | + * Decrement the lock reference count. If the reference count is reduced to zero, commits |
| 995 | + * the transaction and thereby releases the image lock. |
| 996 | + */ |
| 997 | + function unlock() { |
| 998 | + if ( $this->locked ) { |
| 999 | + --$this->locked; |
| 1000 | + if ( !$this->locked ) { |
| 1001 | + $dbw = $this->repo->getMasterDB(); |
| 1002 | + $dbw->commit(); |
| 1003 | + } |
| 1004 | + } |
| 1005 | + } |
| 1006 | + |
| 1007 | + /** |
| 1008 | + * Roll back the DB transaction and mark the image unlocked |
| 1009 | + */ |
| 1010 | + function unlockAndRollback() { |
| 1011 | + $this->locked = false; |
| 1012 | + $dbw = $this->repo->getMasterDB(); |
| 1013 | + $dbw->rollback(); |
| 1014 | + } |
1314 | 1015 | } // LocalFile class |
1315 | 1016 | |
| 1017 | +#------------------------------------------------------------------------------ |
| 1018 | + |
1316 | 1019 | /** |
1317 | 1020 | * Backwards compatibility class |
1318 | 1021 | */ |
— | — | @@ -1379,12 +1082,467 @@ |
1380 | 1083 | } |
1381 | 1084 | } |
1382 | 1085 | |
| 1086 | +#------------------------------------------------------------------------------ |
| 1087 | + |
1383 | 1088 | /** |
1384 | | - * Aliases for backwards compatibility with 1.6 |
| 1089 | + * Helper class for file deletion |
1385 | 1090 | */ |
1386 | | -define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE ); |
1387 | | -define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT ); |
1388 | | -define( 'MW_IMG_DELETED_USER', File::DELETED_USER ); |
1389 | | -define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED ); |
| 1091 | +class LocalFileDeleteBatch { |
| 1092 | + var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch; |
| 1093 | + var $status; |
1390 | 1094 | |
| 1095 | + function __construct( File $file, $reason = '' ) { |
| 1096 | + $this->file = $file; |
| 1097 | + $this->reason = $reason; |
| 1098 | + $this->status = $file->repo->newGood(); |
| 1099 | + } |
1391 | 1100 | |
| 1101 | + function addCurrent() { |
| 1102 | + $this->srcRels['.'] = $this->file->getRel(); |
| 1103 | + } |
| 1104 | + |
| 1105 | + function addOld( $oldName ) { |
| 1106 | + $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName ); |
| 1107 | + $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName ); |
| 1108 | + } |
| 1109 | + |
| 1110 | + function getOldRels() { |
| 1111 | + if ( !isset( $this->srcRels['.'] ) ) { |
| 1112 | + $oldRels =& $this->srcRels; |
| 1113 | + $deleteCurrent = false; |
| 1114 | + } else { |
| 1115 | + $oldRels = $this->srcRels; |
| 1116 | + unset( $oldRels['.'] ); |
| 1117 | + $deleteCurrent = true; |
| 1118 | + } |
| 1119 | + return array( $oldRels, $deleteCurrent ); |
| 1120 | + } |
| 1121 | + |
| 1122 | + /*protected*/ function getHashes() { |
| 1123 | + $hashes = array(); |
| 1124 | + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); |
| 1125 | + if ( $deleteCurrent ) { |
| 1126 | + $hashes['.'] = $this->file->getSha1(); |
| 1127 | + } |
| 1128 | + if ( count( $oldRels ) ) { |
| 1129 | + $dbw = $this->file->repo->getMasterDB(); |
| 1130 | + $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ), |
| 1131 | + 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')', |
| 1132 | + __METHOD__ ); |
| 1133 | + while ( $row = $dbw->fetchObject( $res ) ) { |
| 1134 | + if ( rtrim( $row->oi_sha1, "\0" ) === '' ) { |
| 1135 | + // Get the hash from the file |
| 1136 | + $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name ); |
| 1137 | + $props = $this->file->repo->getFileProps( $oldUrl ); |
| 1138 | + if ( $props['fileExists'] ) { |
| 1139 | + // Upgrade the oldimage row |
| 1140 | + $dbw->update( 'oldimage', |
| 1141 | + array( 'oi_sha1' => $props['sha1'] ), |
| 1142 | + array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ), |
| 1143 | + __METHOD__ ); |
| 1144 | + $hashes[$row->oi_archive_name] = $props['sha1']; |
| 1145 | + } else { |
| 1146 | + $hashes[$row->oi_archive_name] = false; |
| 1147 | + } |
| 1148 | + } else { |
| 1149 | + $hashes[$row->oi_archive_name] = $row->oi_sha1; |
| 1150 | + } |
| 1151 | + } |
| 1152 | + } |
| 1153 | + $missing = array_diff_key( $this->srcRels, $hashes ); |
| 1154 | + foreach ( $missing as $name => $rel ) { |
| 1155 | + $this->status->error( 'filedelete-old-unregistered', $name ); |
| 1156 | + } |
| 1157 | + foreach ( $hashes as $name => $hash ) { |
| 1158 | + if ( !$hash ) { |
| 1159 | + $this->status->error( 'filedelete-missing', $this->srcRels[$name] ); |
| 1160 | + unset( $hashes[$name] ); |
| 1161 | + } |
| 1162 | + } |
| 1163 | + |
| 1164 | + return $hashes; |
| 1165 | + } |
| 1166 | + |
| 1167 | + function doDBInserts() { |
| 1168 | + global $wgUser; |
| 1169 | + $dbw = $this->file->repo->getMasterDB(); |
| 1170 | + $encTimestamp = $dbw->addQuotes( $dbw->timestamp() ); |
| 1171 | + $encUserId = $dbw->addQuotes( $wgUser->getId() ); |
| 1172 | + $encReason = $dbw->addQuotes( $this->reason ); |
| 1173 | + $encGroup = $dbw->addQuotes( 'deleted' ); |
| 1174 | + $ext = $this->file->getExtension(); |
| 1175 | + $dotExt = $ext === '' ? '' : ".$ext"; |
| 1176 | + $encExt = $dbw->addQuotes( $dotExt ); |
| 1177 | + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); |
| 1178 | + |
| 1179 | + if ( $deleteCurrent ) { |
| 1180 | + $where = array( 'img_name' => $this->file->getName() ); |
| 1181 | + $dbw->insertSelect( 'filearchive', 'image', |
| 1182 | + array( |
| 1183 | + 'fa_storage_group' => $encGroup, |
| 1184 | + 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))", |
| 1185 | + |
| 1186 | + 'fa_deleted_user' => $encUserId, |
| 1187 | + 'fa_deleted_timestamp' => $encTimestamp, |
| 1188 | + 'fa_deleted_reason' => $encReason, |
| 1189 | + 'fa_deleted' => 0, |
| 1190 | + |
| 1191 | + 'fa_name' => 'img_name', |
| 1192 | + 'fa_archive_name' => 'NULL', |
| 1193 | + 'fa_size' => 'img_size', |
| 1194 | + 'fa_width' => 'img_width', |
| 1195 | + 'fa_height' => 'img_height', |
| 1196 | + 'fa_metadata' => 'img_metadata', |
| 1197 | + 'fa_bits' => 'img_bits', |
| 1198 | + 'fa_media_type' => 'img_media_type', |
| 1199 | + 'fa_major_mime' => 'img_major_mime', |
| 1200 | + 'fa_minor_mime' => 'img_minor_mime', |
| 1201 | + 'fa_description' => 'img_description', |
| 1202 | + 'fa_user' => 'img_user', |
| 1203 | + 'fa_user_text' => 'img_user_text', |
| 1204 | + 'fa_timestamp' => 'img_timestamp' |
| 1205 | + ), $where, __METHOD__ ); |
| 1206 | + } |
| 1207 | + |
| 1208 | + if ( count( $oldRels ) ) { |
| 1209 | + $where = array( |
| 1210 | + 'oi_name' => $this->file->getName(), |
| 1211 | + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ); |
| 1212 | + |
| 1213 | + $dbw->insertSelect( 'filearchive', 'oldimage', |
| 1214 | + array( |
| 1215 | + 'fa_storage_group' => $encGroup, |
| 1216 | + 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))", |
| 1217 | + |
| 1218 | + 'fa_deleted_user' => $encUserId, |
| 1219 | + 'fa_deleted_timestamp' => $encTimestamp, |
| 1220 | + 'fa_deleted_reason' => $encReason, |
| 1221 | + 'fa_deleted' => 0, |
| 1222 | + |
| 1223 | + 'fa_name' => 'oi_name', |
| 1224 | + 'fa_archive_name' => 'oi_archive_name', |
| 1225 | + 'fa_size' => 'oi_size', |
| 1226 | + 'fa_width' => 'oi_width', |
| 1227 | + 'fa_height' => 'oi_height', |
| 1228 | + 'fa_metadata' => 'oi_metadata', |
| 1229 | + 'fa_bits' => 'oi_bits', |
| 1230 | + 'fa_media_type' => 'oi_media_type', |
| 1231 | + 'fa_major_mime' => 'oi_major_mime', |
| 1232 | + 'fa_minor_mime' => 'oi_minor_mime', |
| 1233 | + 'fa_description' => 'oi_description', |
| 1234 | + 'fa_user' => 'oi_user', |
| 1235 | + 'fa_user_text' => 'oi_user_text', |
| 1236 | + 'fa_timestamp' => 'oi_timestamp' |
| 1237 | + ), $where, __METHOD__ ); |
| 1238 | + } |
| 1239 | + } |
| 1240 | + |
| 1241 | + function doDBDeletes() { |
| 1242 | + $dbw = $this->file->repo->getMasterDB(); |
| 1243 | + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); |
| 1244 | + if ( $deleteCurrent ) { |
| 1245 | + $where = array( 'img_name' => $this->file->getName() ); |
| 1246 | + $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ ); |
| 1247 | + } |
| 1248 | + if ( count( $oldRels ) ) { |
| 1249 | + $dbw->delete( 'oldimage', |
| 1250 | + array( |
| 1251 | + 'oi_name' => $this->file->getName(), |
| 1252 | + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' |
| 1253 | + ), __METHOD__ ); |
| 1254 | + } |
| 1255 | + } |
| 1256 | + |
| 1257 | + /** |
| 1258 | + * Run the transaction |
| 1259 | + */ |
| 1260 | + function execute() { |
| 1261 | + global $wgUser, $wgUseSquid; |
| 1262 | + wfProfileIn( __METHOD__ ); |
| 1263 | + |
| 1264 | + $this->file->lock(); |
| 1265 | + |
| 1266 | + // Prepare deletion batch |
| 1267 | + $hashes = $this->getHashes(); |
| 1268 | + $this->deletionBatch = array(); |
| 1269 | + $ext = $this->file->getExtension(); |
| 1270 | + $dotExt = $ext === '' ? '' : ".$ext"; |
| 1271 | + foreach ( $this->srcRels as $name => $srcRel ) { |
| 1272 | + // Skip files that have no hash (missing source) |
| 1273 | + if ( isset( $hashes[$name] ) ) { |
| 1274 | + $hash = $hashes[$name]; |
| 1275 | + $key = $hash . $dotExt; |
| 1276 | + $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; |
| 1277 | + $this->deletionBatch[$name] = array( $srcRel, $dstRel ); |
| 1278 | + } |
| 1279 | + } |
| 1280 | + |
| 1281 | + // Lock the filearchive rows so that the files don't get deleted by a cleanup operation |
| 1282 | + // We acquire this lock by running the inserts now, before the file operations. |
| 1283 | + // |
| 1284 | + // This potentially has poor lock contention characteristics -- an alternative |
| 1285 | + // scheme would be to insert stub filearchive entries with no fa_name and commit |
| 1286 | + // them in a separate transaction, then run the file ops, then update the fa_name fields. |
| 1287 | + $this->doDBInserts(); |
| 1288 | + |
| 1289 | + // Execute the file deletion batch |
| 1290 | + $status = $this->file->repo->deleteBatch( $this->deletionBatch ); |
| 1291 | + if ( !$status->isGood() ) { |
| 1292 | + $this->status->merge( $status ); |
| 1293 | + } |
| 1294 | + |
| 1295 | + if ( !$this->status->ok ) { |
| 1296 | + // Critical file deletion error |
| 1297 | + // Roll back inserts, release lock and abort |
| 1298 | + // TODO: delete the defunct filearchive rows if we are using a non-transactional DB |
| 1299 | + $this->file->unlockAndRollback(); |
| 1300 | + return $this->status; |
| 1301 | + } |
| 1302 | + |
| 1303 | + // Purge squid |
| 1304 | + if ( $wgUseSquid ) { |
| 1305 | + $urls = array(); |
| 1306 | + foreach ( $this->srcRels as $srcRel ) { |
| 1307 | + $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) ); |
| 1308 | + $urls[] = $this->repo->getZoneUrl( 'public' ) . '/' . $urlRel; |
| 1309 | + } |
| 1310 | + SquidUpdate::purge( $urls ); |
| 1311 | + } |
| 1312 | + |
| 1313 | + // Delete image/oldimage rows |
| 1314 | + $this->doDBDeletes(); |
| 1315 | + |
| 1316 | + // Commit and return |
| 1317 | + $this->file->unlock(); |
| 1318 | + wfProfileOut( __METHOD__ ); |
| 1319 | + return $this->status; |
| 1320 | + } |
| 1321 | +} |
| 1322 | + |
| 1323 | +#------------------------------------------------------------------------------ |
| 1324 | + |
| 1325 | +/** |
| 1326 | + * Helper class for file undeletion |
| 1327 | + */ |
| 1328 | +class LocalFileRestoreBatch { |
| 1329 | + var $file, $cleanupBatch, $ids, $all, $unsuppress = false; |
| 1330 | + |
| 1331 | + function __construct( File $file ) { |
| 1332 | + $this->file = $file; |
| 1333 | + $this->cleanupBatch = $this->ids = array(); |
| 1334 | + $this->ids = array(); |
| 1335 | + } |
| 1336 | + |
| 1337 | + /** |
| 1338 | + * Add a file by ID |
| 1339 | + */ |
| 1340 | + function addId( $fa_id ) { |
| 1341 | + $this->ids[] = $fa_id; |
| 1342 | + } |
| 1343 | + |
| 1344 | + /** |
| 1345 | + * Add a whole lot of files by ID |
| 1346 | + */ |
| 1347 | + function addIds( $ids ) { |
| 1348 | + $this->ids = array_merge( $this->ids, $ids ); |
| 1349 | + } |
| 1350 | + |
| 1351 | + /** |
| 1352 | + * Add all revisions of the file |
| 1353 | + */ |
| 1354 | + function addAll() { |
| 1355 | + $this->all = true; |
| 1356 | + } |
| 1357 | + |
| 1358 | + /** |
| 1359 | + * Run the transaction, except the cleanup batch. |
| 1360 | + * The cleanup batch should be run in a separate transaction, because it locks different |
| 1361 | + * rows and there's no need to keep the image row locked while it's acquiring those locks |
| 1362 | + * The caller may have its own transaction open. |
| 1363 | + * So we save the batch and let the caller call cleanup() |
| 1364 | + */ |
| 1365 | + function execute() { |
| 1366 | + global $wgUser, $wgLang; |
| 1367 | + if ( !$this->all && !$this->ids ) { |
| 1368 | + // Do nothing |
| 1369 | + return $this->file->repo->newGood(); |
| 1370 | + } |
| 1371 | + |
| 1372 | + $exists = $this->file->lock(); |
| 1373 | + $dbw = $this->file->repo->getMasterDB(); |
| 1374 | + $status = $this->file->repo->newGood(); |
| 1375 | + |
| 1376 | + // Fetch all or selected archived revisions for the file, |
| 1377 | + // sorted from the most recent to the oldest. |
| 1378 | + $conditions = array( 'fa_name' => $this->file->getName() ); |
| 1379 | + if( !$this->all ) { |
| 1380 | + $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')'; |
| 1381 | + } |
| 1382 | + |
| 1383 | + $result = $dbw->select( 'filearchive', '*', |
| 1384 | + $conditions, |
| 1385 | + __METHOD__, |
| 1386 | + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); |
| 1387 | + |
| 1388 | + $idsPresent = array(); |
| 1389 | + $storeBatch = array(); |
| 1390 | + $insertBatch = array(); |
| 1391 | + $insertCurrent = false; |
| 1392 | + $deleteIds = array(); |
| 1393 | + $first = true; |
| 1394 | + $archiveNames = array(); |
| 1395 | + while( $row = $dbw->fetchObject( $result ) ) { |
| 1396 | + $idsPresent[] = $row->fa_id; |
| 1397 | + if ( $this->unsuppress ) { |
| 1398 | + // Currently, fa_deleted flags fall off upon restore, lets be careful about this |
| 1399 | + } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { |
| 1400 | + // Skip restoring file revisions that the user cannot restore |
| 1401 | + continue; |
| 1402 | + } |
| 1403 | + if ( $row->fa_name != $this->file->getName() ) { |
| 1404 | + $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) ); |
| 1405 | + $status->failCount++; |
| 1406 | + continue; |
| 1407 | + } |
| 1408 | + if ( $row->fa_storage_key == '' ) { |
| 1409 | + // Revision was missing pre-deletion |
| 1410 | + $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) ); |
| 1411 | + $status->failCount++; |
| 1412 | + continue; |
| 1413 | + } |
| 1414 | + |
| 1415 | + $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key; |
| 1416 | + $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; |
| 1417 | + |
| 1418 | + $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) ); |
| 1419 | + # Fix leading zero |
| 1420 | + if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) { |
| 1421 | + $sha1 = substr( $sha1, 1 ); |
| 1422 | + } |
| 1423 | + |
| 1424 | + if ( $first && !$exists ) { |
| 1425 | + // This revision will be published as the new current version |
| 1426 | + $destRel = $this->file->getRel(); |
| 1427 | + $info = $this->file->repo->getFileProps( $deletedUrl ); |
| 1428 | + $insertCurrent = array( |
| 1429 | + 'img_name' => $row->fa_name, |
| 1430 | + 'img_size' => $row->fa_size, |
| 1431 | + 'img_width' => $row->fa_width, |
| 1432 | + 'img_height' => $row->fa_height, |
| 1433 | + 'img_metadata' => $row->fa_metadata, |
| 1434 | + 'img_bits' => $row->fa_bits, |
| 1435 | + 'img_media_type' => $row->fa_media_type, |
| 1436 | + 'img_major_mime' => $row->fa_major_mime, |
| 1437 | + 'img_minor_mime' => $row->fa_minor_mime, |
| 1438 | + 'img_description' => $row->fa_description, |
| 1439 | + 'img_user' => $row->fa_user, |
| 1440 | + 'img_user_text' => $row->fa_user_text, |
| 1441 | + 'img_timestamp' => $row->fa_timestamp, |
| 1442 | + 'img_sha1' => $sha1); |
| 1443 | + } else { |
| 1444 | + $archiveName = $row->fa_archive_name; |
| 1445 | + if( $archiveName == '' ) { |
| 1446 | + // This was originally a current version; we |
| 1447 | + // have to devise a new archive name for it. |
| 1448 | + // Format is <timestamp of archiving>!<name> |
| 1449 | + $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp ); |
| 1450 | + do { |
| 1451 | + $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name; |
| 1452 | + $timestamp++; |
| 1453 | + } while ( isset( $archiveNames[$archiveName] ) ); |
| 1454 | + } |
| 1455 | + $archiveNames[$archiveName] = true; |
| 1456 | + $destRel = $this->file->getArchiveRel( $archiveName ); |
| 1457 | + $insertBatch[] = array( |
| 1458 | + 'oi_name' => $row->fa_name, |
| 1459 | + 'oi_archive_name' => $archiveName, |
| 1460 | + 'oi_size' => $row->fa_size, |
| 1461 | + 'oi_width' => $row->fa_width, |
| 1462 | + 'oi_height' => $row->fa_height, |
| 1463 | + 'oi_bits' => $row->fa_bits, |
| 1464 | + 'oi_description' => $row->fa_description, |
| 1465 | + 'oi_user' => $row->fa_user, |
| 1466 | + 'oi_user_text' => $row->fa_user_text, |
| 1467 | + 'oi_timestamp' => $row->fa_timestamp, |
| 1468 | + 'oi_metadata' => $row->fa_metadata, |
| 1469 | + 'oi_media_type' => $row->fa_media_type, |
| 1470 | + 'oi_major_mime' => $row->fa_major_mime, |
| 1471 | + 'oi_minor_mime' => $row->fa_minor_mime, |
| 1472 | + 'oi_deleted' => $row->fa_deleted, |
| 1473 | + 'oi_sha1' => $sha1 ); |
| 1474 | + } |
| 1475 | + |
| 1476 | + $deleteIds[] = $row->fa_id; |
| 1477 | + $storeBatch[] = array( $deletedUrl, 'public', $destRel ); |
| 1478 | + $this->cleanupBatch[] = $row->fa_storage_key; |
| 1479 | + $first = false; |
| 1480 | + } |
| 1481 | + unset( $result ); |
| 1482 | + |
| 1483 | + // Add a warning to the status object for missing IDs |
| 1484 | + $missingIds = array_diff( $this->ids, $idsPresent ); |
| 1485 | + foreach ( $missingIds as $id ) { |
| 1486 | + $status->error( 'undelete-missing-filearchive', $id ); |
| 1487 | + } |
| 1488 | + |
| 1489 | + // Run the store batch |
| 1490 | + // Use the OVERWRITE_SAME flag to smooth over a common error |
| 1491 | + $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); |
| 1492 | + $status->merge( $storeStatus ); |
| 1493 | + |
| 1494 | + if ( !$status->ok ) { |
| 1495 | + // Store batch returned a critical error -- this usually means nothing was stored |
| 1496 | + // Stop now and return an error |
| 1497 | + $this->file->unlock(); |
| 1498 | + return $status; |
| 1499 | + } |
| 1500 | + |
| 1501 | + // Run the DB updates |
| 1502 | + // Because we have locked the image row, key conflicts should be rare. |
| 1503 | + // If they do occur, we can roll back the transaction at this time with |
| 1504 | + // no data loss, but leaving unregistered files scattered throughout the |
| 1505 | + // public zone. |
| 1506 | + // This is not ideal, which is why it's important to lock the image row. |
| 1507 | + if ( $insertCurrent ) { |
| 1508 | + $dbw->insert( 'image', $insertCurrent, __METHOD__ ); |
| 1509 | + } |
| 1510 | + if ( $insertBatch ) { |
| 1511 | + $dbw->insert( 'oldimage', $insertBatch, __METHOD__ ); |
| 1512 | + } |
| 1513 | + if ( $deleteIds ) { |
| 1514 | + $dbw->delete( 'filearchive', |
| 1515 | + array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), |
| 1516 | + __METHOD__ ); |
| 1517 | + } |
| 1518 | + |
| 1519 | + if( $status->successCount > 0 ) { |
| 1520 | + if( !$exists ) { |
| 1521 | + wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" ); |
| 1522 | + |
| 1523 | + // Update site_stats |
| 1524 | + $site_stats = $dbw->tableName( 'site_stats' ); |
| 1525 | + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); |
| 1526 | + |
| 1527 | + $this->file->purgeEverything(); |
| 1528 | + } else { |
| 1529 | + wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" ); |
| 1530 | + $this->file->purgeDescription(); |
| 1531 | + $this->file->purgeHistory(); |
| 1532 | + } |
| 1533 | + } |
| 1534 | + $this->file->unlock(); |
| 1535 | + return $status; |
| 1536 | + } |
| 1537 | + |
| 1538 | + /** |
| 1539 | + * Delete unused files in the deleted zone. |
| 1540 | + * This should be called from outside the transaction in which execute() was called. |
| 1541 | + */ |
| 1542 | + function cleanup() { |
| 1543 | + if ( !$this->cleanupBatch ) { |
| 1544 | + return $this->file->repo->newGood(); |
| 1545 | + } |
| 1546 | + $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch ); |
| 1547 | + return $status; |
| 1548 | + } |
| 1549 | +} |
Index: branches/liquidthreads/includes/filerepo/FSRepo.php |
— | — | @@ -6,9 +6,10 @@ |
7 | 7 | */ |
8 | 8 | |
9 | 9 | class FSRepo extends FileRepo { |
10 | | - var $directory, $url, $hashLevels; |
| 10 | + var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels; |
11 | 11 | var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); |
12 | 12 | var $oldFileFactory = false; |
| 13 | + var $pathDisclosureProtection = 'simple'; |
13 | 14 | |
14 | 15 | function __construct( $info ) { |
15 | 16 | parent::__construct( $info ); |
— | — | @@ -16,7 +17,12 @@ |
17 | 18 | // Required settings |
18 | 19 | $this->directory = $info['directory']; |
19 | 20 | $this->url = $info['url']; |
20 | | - $this->hashLevels = $info['hashLevels']; |
| 21 | + |
| 22 | + // Optional settings |
| 23 | + $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2; |
| 24 | + $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? |
| 25 | + $info['deletedHashLevels'] : $this->hashLevels; |
| 26 | + $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false; |
21 | 27 | } |
22 | 28 | |
23 | 29 | /** |
— | — | @@ -50,7 +56,7 @@ |
51 | 57 | case 'temp': |
52 | 58 | return "{$this->directory}/temp"; |
53 | 59 | case 'deleted': |
54 | | - return $GLOBALS['wgFileStore']['deleted']['directory']; |
| 60 | + return $this->deletedDir; |
55 | 61 | default: |
56 | 62 | return false; |
57 | 63 | } |
— | — | @@ -66,7 +72,7 @@ |
67 | 73 | case 'temp': |
68 | 74 | return "{$this->url}/temp"; |
69 | 75 | case 'deleted': |
70 | | - return $GLOBALS['wgFileStore']['deleted']['url']; |
| 76 | + return false; // no public URL |
71 | 77 | default: |
72 | 78 | return false; |
73 | 79 | } |
— | — | @@ -109,47 +115,108 @@ |
110 | 116 | } |
111 | 117 | |
112 | 118 | /** |
113 | | - * Store a file to a given destination. |
| 119 | + * Store a batch of files |
| 120 | + * |
| 121 | + * @param array $triplets (src,zone,dest) triplets as per store() |
| 122 | + * @param integer $flags Bitwise combination of the following flags: |
| 123 | + * self::DELETE_SOURCE Delete the source file after upload |
| 124 | + * self::OVERWRITE Overwrite an existing destination file instead of failing |
| 125 | + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the |
| 126 | + * same contents as the source |
114 | 127 | */ |
115 | | - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
| 128 | + function storeBatch( $triplets, $flags = 0 ) { |
116 | 129 | if ( !is_writable( $this->directory ) ) { |
117 | | - return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) ); |
| 130 | + return $this->newFatal( 'upload_directory_read_only', $this->directory ); |
118 | 131 | } |
119 | | - $root = $this->getZonePath( $dstZone ); |
120 | | - if ( !$root ) { |
121 | | - throw new MWException( "Invalid zone: $dstZone" ); |
| 132 | + $status = $this->newGood(); |
| 133 | + foreach ( $triplets as $i => $triplet ) { |
| 134 | + list( $srcPath, $dstZone, $dstRel ) = $triplet; |
| 135 | + |
| 136 | + $root = $this->getZonePath( $dstZone ); |
| 137 | + if ( !$root ) { |
| 138 | + throw new MWException( "Invalid zone: $dstZone" ); |
| 139 | + } |
| 140 | + if ( !$this->validateFilename( $dstRel ) ) { |
| 141 | + throw new MWException( 'Validation error in $dstRel' ); |
| 142 | + } |
| 143 | + $dstPath = "$root/$dstRel"; |
| 144 | + $dstDir = dirname( $dstPath ); |
| 145 | + |
| 146 | + if ( !is_dir( $dstDir ) ) { |
| 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 | + } |
122 | 176 | } |
123 | | - $dstPath = "$root/$dstRel"; |
124 | 177 | |
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; |
127 | 183 | } |
128 | | - |
129 | | - if ( self::isVirtualUrl( $srcPath ) ) { |
130 | | - $srcPath = $this->resolveVirtualUrl( $srcPath ); |
131 | | - } |
132 | 184 | |
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 | + } |
137 | 204 | } |
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++; |
142 | 210 | } |
143 | 211 | } |
144 | | - chmod( $dstPath, 0644 ); |
145 | | - return true; |
| 212 | + return $status; |
146 | 213 | } |
147 | 214 | |
148 | 215 | /** |
149 | 216 | * Pick a random name in the temp zone and store a file to it. |
150 | | - * Returns the URL, or a WikiError on failure. |
151 | 217 | * @param string $originalName The base name of the file as specified |
152 | 218 | * by the user. The file extension will be maintained. |
153 | 219 | * @param string $srcPath The current location of the file. |
| 220 | + * @return FileRepoStatus object with the URL in the value. |
154 | 221 | */ |
155 | 222 | function storeTemp( $originalName, $srcPath ) { |
156 | 223 | $date = gmdate( "YmdHis" ); |
— | — | @@ -158,11 +225,8 @@ |
159 | 226 | $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); |
160 | 227 | |
161 | 228 | $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; |
167 | 231 | } |
168 | 232 | |
169 | 233 | /** |
— | — | @@ -183,82 +247,190 @@ |
184 | 248 | return $success; |
185 | 249 | } |
186 | 250 | |
187 | | - |
188 | 251 | /** |
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() |
196 | 254 | * @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 |
198 | 256 | */ |
199 | | - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
| 257 | + function publishBatch( $triplets, $flags = 0 ) { |
| 258 | + // Perform initial checks |
200 | 259 | 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 ); |
202 | 261 | } |
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 | + } |
205 | 291 | } |
206 | | - if ( !$this->validateFilename( $dstRel ) ) { |
207 | | - throw new MWException( 'Validation error in $dstRel' ); |
| 292 | + |
| 293 | + if ( !$status->ok ) { |
| 294 | + return $status; |
208 | 295 | } |
209 | | - if ( !$this->validateFilename( $archiveRel ) ) { |
210 | | - throw new MWException( 'Validation error in $archiveRel' ); |
211 | | - } |
212 | | - $dstPath = "{$this->directory}/$dstRel"; |
213 | | - $archivePath = "{$this->directory}/$archiveRel"; |
214 | 296 | |
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"; |
217 | 301 | |
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 | + } |
222 | 316 | |
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; |
226 | 330 | 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 | + } |
228 | 342 | wfRestoreWarnings(); |
229 | 343 | |
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++; |
233 | 351 | } |
234 | | - else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n"); |
235 | | - $status = 'archived'; |
236 | 352 | } |
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' ); |
239 | 371 | } |
240 | 372 | |
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' ); |
247 | 380 | } |
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' ); |
252 | 383 | } |
| 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 | + } |
253 | 399 | } |
254 | | - wfRestoreWarnings(); |
| 400 | + if ( !$status->ok ) { |
| 401 | + // Abort early |
| 402 | + return $status; |
| 403 | + } |
255 | 404 | |
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 | + } |
260 | 434 | } |
261 | | - |
262 | | - chmod( $dstPath, 0644 ); |
263 | 435 | return $status; |
264 | 436 | } |
265 | 437 | |
— | — | @@ -271,6 +443,18 @@ |
272 | 444 | } |
273 | 445 | |
274 | 446 | /** |
| 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 | + /** |
275 | 459 | * Call a callback function for every file in the repository. |
276 | 460 | * Uses the filesystem even in child classes. |
277 | 461 | */ |
— | — | @@ -308,6 +492,39 @@ |
309 | 493 | $path = $this->resolveVirtualUrl( $virtualUrl ); |
310 | 494 | return File::getPropsFromPath( $path ); |
311 | 495 | } |
| 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 | + |
312 | 529 | } |
313 | 530 | |
314 | 531 | |
Index: branches/liquidthreads/includes/filerepo/File.php |
— | — | @@ -593,11 +593,9 @@ |
594 | 594 | |
595 | 595 | /** |
596 | 596 | * Purge metadata and all affected pages when the file is created, |
597 | | - * deleted, or majorly updated. A set of additional URLs may be |
598 | | - * passed to purge, such as specific file files which have changed. |
599 | | - * @param $urlArray array |
| 597 | + * deleted, or majorly updated. |
600 | 598 | */ |
601 | | - function purgeEverything( $urlArr=array() ) { |
| 599 | + function purgeEverything() { |
602 | 600 | // Delete thumbnails and refresh file metadata cache |
603 | 601 | $this->purgeCache(); |
604 | 602 | $this->purgeDescription(); |
— | — | @@ -656,9 +654,9 @@ |
657 | 655 | return $this->getHashPath() . rawurlencode( $this->getName() ); |
658 | 656 | } |
659 | 657 | |
660 | | - /** Get the path of the archive directory, or a particular file if $suffix is specified */ |
661 | | - function getArchivePath( $suffix = false ) { |
662 | | - $path = $this->repo->getZonePath('public') . '/archive/' . $this->getHashPath(); |
| 658 | + /** Get the relative path for an archive file */ |
| 659 | + function getArchiveRel( $suffix = false ) { |
| 660 | + $path = 'archive/' . $this->getHashPath(); |
663 | 661 | if ( $suffix === false ) { |
664 | 662 | $path = substr( $path, 0, -1 ); |
665 | 663 | } else { |
— | — | @@ -667,15 +665,25 @@ |
668 | 666 | return $path; |
669 | 667 | } |
670 | 668 | |
671 | | - /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ |
672 | | - function getThumbPath( $suffix = false ) { |
673 | | - $path = $this->repo->getZonePath('public') . '/thumb/' . $this->getRel(); |
| 669 | + /** Get relative path for a thumbnail file */ |
| 670 | + function getThumbRel( $suffix = false ) { |
| 671 | + $path = 'thumb/' . $this->getRel(); |
674 | 672 | if ( $suffix !== false ) { |
675 | 673 | $path .= '/' . $suffix; |
676 | 674 | } |
677 | 675 | return $path; |
678 | 676 | } |
679 | 677 | |
| 678 | + /** Get the path of the archive directory, or a particular file if $suffix is specified */ |
| 679 | + function getArchivePath( $suffix = false ) { |
| 680 | + return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel(); |
| 681 | + } |
| 682 | + |
| 683 | + /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ |
| 684 | + function getThumbPath( $suffix = false ) { |
| 685 | + return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix ); |
| 686 | + } |
| 687 | + |
680 | 688 | /** Get the URL of the archive directory, or a particular file if $suffix is specified */ |
681 | 689 | function getArchiveUrl( $suffix = false ) { |
682 | 690 | $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath(); |
— | — | @@ -980,13 +988,20 @@ |
981 | 989 | /** |
982 | 990 | * Get the 14-character timestamp of the file upload, or false if |
983 | 991 | */ |
984 | | - function getTimestmap() { |
| 992 | + function getTimestamp() { |
985 | 993 | $path = $this->getPath(); |
986 | 994 | if ( !file_exists( $path ) ) { |
987 | 995 | return false; |
988 | 996 | } |
989 | 997 | return wfTimestamp( filemtime( $path ) ); |
990 | 998 | } |
| 999 | + |
| 1000 | + /** |
| 1001 | + * Get the SHA-1 base 36 hash of the file |
| 1002 | + */ |
| 1003 | + function getSha1() { |
| 1004 | + return self::sha1Base36( $this->getPath() ); |
| 1005 | + } |
991 | 1006 | |
992 | 1007 | /** |
993 | 1008 | * Determine if the current user is allowed to view a particular |
— | — | @@ -1031,12 +1046,14 @@ |
1032 | 1047 | $gis = false; |
1033 | 1048 | $info['metadata'] = ''; |
1034 | 1049 | } |
| 1050 | + $info['sha1'] = self::sha1Base36( $path ); |
1035 | 1051 | |
1036 | 1052 | wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n"); |
1037 | 1053 | } else { |
1038 | 1054 | $info['mime'] = NULL; |
1039 | 1055 | $info['media_type'] = MEDIATYPE_UNKNOWN; |
1040 | 1056 | $info['metadata'] = ''; |
| 1057 | + $info['sha1'] = ''; |
1041 | 1058 | wfDebug(__METHOD__.": $path NOT FOUND!\n"); |
1042 | 1059 | } |
1043 | 1060 | if( $gis ) { |
— | — | @@ -1056,6 +1073,30 @@ |
1057 | 1074 | wfProfileOut( __METHOD__ ); |
1058 | 1075 | return $info; |
1059 | 1076 | } |
| 1077 | + |
| 1078 | + /** |
| 1079 | + * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case |
| 1080 | + * encoding, zero padded to 31 digits. |
| 1081 | + * |
| 1082 | + * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36 |
| 1083 | + * fairly neatly. |
| 1084 | + * |
| 1085 | + * Returns false on failure |
| 1086 | + */ |
| 1087 | + static function sha1Base36( $path ) { |
| 1088 | + $hash = sha1_file( $path ); |
| 1089 | + if ( $hash === false ) { |
| 1090 | + return false; |
| 1091 | + } else { |
| 1092 | + return wfBaseConvert( $hash, 16, 36, 31 ); |
| 1093 | + } |
| 1094 | + } |
1060 | 1095 | } |
| 1096 | +/** |
| 1097 | + * Aliases for backwards compatibility with 1.6 |
| 1098 | + */ |
| 1099 | +define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE ); |
| 1100 | +define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT ); |
| 1101 | +define( 'MW_IMG_DELETED_USER', File::DELETED_USER ); |
| 1102 | +define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED ); |
1061 | 1103 | |
1062 | | - |
Index: branches/liquidthreads/includes/filerepo/LocalRepo.php |
— | — | @@ -28,4 +28,37 @@ |
29 | 29 | function newFromArchiveName( $title, $archiveName ) { |
30 | 30 | return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); |
31 | 31 | } |
| 32 | + |
| 33 | + /** |
| 34 | + * Delete files in the deleted directory if they are not referenced in the |
| 35 | + * filearchive table. This needs to be done in the repo because it needs to |
| 36 | + * interleave database locks with file operations, which is potentially a |
| 37 | + * remote operation. |
| 38 | + * @return FileRepoStatus |
| 39 | + */ |
| 40 | + function cleanupDeletedBatch( $storageKeys ) { |
| 41 | + $root = $this->getZonePath( 'deleted' ); |
| 42 | + $dbw = $this->getMasterDB(); |
| 43 | + $status = $this->newGood(); |
| 44 | + foreach ( $storageKeys as $key ) { |
| 45 | + $hashPath = $this->getDeletedHashPath( $key ); |
| 46 | + $path = "$root/$hashPath$key"; |
| 47 | + $dbw->begin(); |
| 48 | + $inuse = $dbw->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 | + } |
32 | 65 | } |
Index: branches/liquidthreads/includes/EditPage.php |
— | — | @@ -949,6 +949,10 @@ |
950 | 950 | # Enabled article-related sidebar, toplinks, etc. |
951 | 951 | $wgOut->setArticleRelated( true ); |
952 | 952 | |
| 953 | + if ( $this->formtype == 'preview' ) { |
| 954 | + $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); |
| 955 | + } |
| 956 | + |
953 | 957 | if ( $this->isConflict ) { |
954 | 958 | $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); |
955 | 959 | $wgOut->setPageTitle( $s ); |
Index: branches/liquidthreads/includes/SpecialUpload.php |
— | — | @@ -46,9 +46,11 @@ |
47 | 47 | global $wgAllowCopyUploads; |
48 | 48 | $this->mDesiredDestName = $request->getText( 'wpDestFile' ); |
49 | 49 | $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); |
| 50 | + $this->mComment = $request->getText( 'wpUploadDescription' ); |
50 | 51 | |
51 | 52 | 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 |
53 | 55 | return; |
54 | 56 | } |
55 | 57 | |
— | — | @@ -59,7 +61,6 @@ |
60 | 62 | $this->mReUpload = $request->getCheck( 'wpReUpload' ); |
61 | 63 | $this->mUploadClicked = $request->getCheck( 'wpUpload' ); |
62 | 64 | |
63 | | - $this->mComment = $request->getText( 'wpUploadDescription' ); |
64 | 65 | $this->mLicense = $request->getText( 'wpLicense' ); |
65 | 66 | $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); |
66 | 67 | $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); |
— | — | @@ -433,8 +434,8 @@ |
434 | 435 | |
435 | 436 | $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, |
436 | 437 | File::DELETE_SOURCE, $this->mFileProps ); |
437 | | - if ( WikiError::isError( $status ) ) { |
438 | | - $this->showError( $status ); |
| 438 | + if ( !$status->isGood() ) { |
| 439 | + $this->showError( $status->getWikiText() ); |
439 | 440 | } else { |
440 | 441 | if ( $this->mWatchthis ) { |
441 | 442 | global $wgUser; |
— | — | @@ -592,12 +593,12 @@ |
593 | 594 | function saveTempUploadedFile( $saveName, $tempName ) { |
594 | 595 | global $wgOut; |
595 | 596 | $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() ); |
599 | 600 | return false; |
600 | 601 | } else { |
601 | | - return $result; |
| 602 | + return $status->value; |
602 | 603 | } |
603 | 604 | } |
604 | 605 | |
— | — | @@ -1354,15 +1355,15 @@ |
1355 | 1356 | } |
1356 | 1357 | |
1357 | 1358 | /** |
1358 | | - * Display an error from a wikitext-formatted WikiError object |
| 1359 | + * Display an error with a wikitext description |
1359 | 1360 | */ |
1360 | | - function showError( WikiError $error ) { |
| 1361 | + function showError( $description ) { |
1361 | 1362 | global $wgOut; |
1362 | 1363 | $wgOut->setPageTitle( wfMsg( "internalerror" ) ); |
1363 | 1364 | $wgOut->setRobotpolicy( "noindex,nofollow" ); |
1364 | 1365 | $wgOut->setArticleRelated( false ); |
1365 | 1366 | $wgOut->enableClientCache( false ); |
1366 | | - $wgOut->addWikiText( $error->getMessage() ); |
| 1367 | + $wgOut->addWikiText( $description ); |
1367 | 1368 | } |
1368 | 1369 | |
1369 | 1370 | /** |
Index: branches/liquidthreads/includes/OutputPage.php |
— | — | @@ -28,6 +28,7 @@ |
29 | 29 | |
30 | 30 | var $mNewSectionLink = false; |
31 | 31 | var $mNoGallery = false; |
| 32 | + var $mPageTitleActionText = ''; |
32 | 33 | |
33 | 34 | /** |
34 | 35 | * Constructor |
— | — | @@ -192,26 +193,13 @@ |
193 | 194 | } |
194 | 195 | } |
195 | 196 | |
| 197 | + function setPageTitleActionText( $text ) { |
| 198 | + $this->mPageTitleActionText = $text; |
| 199 | + } |
| 200 | + |
196 | 201 | 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; |
216 | 204 | } |
217 | 205 | } |
218 | 206 | |
Index: branches/liquidthreads/includes/AutoLoader.php |
— | — | @@ -258,11 +258,14 @@ |
259 | 259 | 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', |
260 | 260 | 'File' => 'includes/filerepo/File.php', |
261 | 261 | 'FileRepo' => 'includes/filerepo/FileRepo.php', |
| 262 | + 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', |
262 | 263 | 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', |
263 | 264 | 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', |
264 | 265 | 'FSRepo' => 'includes/filerepo/FSRepo.php', |
265 | 266 | 'Image' => 'includes/filerepo/LocalFile.php', |
266 | 267 | 'LocalFile' => 'includes/filerepo/LocalFile.php', |
| 268 | + 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', |
| 269 | + 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', |
267 | 270 | 'LocalRepo' => 'includes/filerepo/LocalRepo.php', |
268 | 271 | 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', |
269 | 272 | 'RepoGroup' => 'includes/filerepo/RepoGroup.php', |
Index: branches/liquidthreads/includes/SpecialUndelete.php |
— | — | @@ -23,6 +23,7 @@ |
24 | 24 | */ |
25 | 25 | class PageArchive { |
26 | 26 | protected $title; |
| 27 | + var $fileStatus; |
27 | 28 | |
28 | 29 | function __construct( $title ) { |
29 | 30 | if( is_null( $title ) ) { |
— | — | @@ -270,7 +271,8 @@ |
271 | 272 | |
272 | 273 | if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) { |
273 | 274 | $img = wfLocalFile( $this->title ); |
274 | | - $filesRestored = $img->restore( $fileVersions ); |
| 275 | + $this->fileStatus = $img->restore( $fileVersions ); |
| 276 | + $filesRestored = $this->fileStatus->successCount; |
275 | 277 | } else { |
276 | 278 | $filesRestored = 0; |
277 | 279 | } |
— | — | @@ -280,7 +282,7 @@ |
281 | 283 | } else { |
282 | 284 | $textRestored = 0; |
283 | 285 | } |
284 | | - |
| 286 | + |
285 | 287 | // Touch the log! |
286 | 288 | global $wgContLang; |
287 | 289 | $log = new LogPage( 'delete' ); |
— | — | @@ -303,8 +305,12 @@ |
304 | 306 | if( trim( $comment ) != '' ) |
305 | 307 | $reason .= ": {$comment}"; |
306 | 308 | $log->addEntry( 'restore', $this->title, $reason ); |
307 | | - |
308 | | - return true; |
| 309 | + |
| 310 | + if ( $this->fileStatus && !$this->fileStatus->ok ) { |
| 311 | + return false; |
| 312 | + } else { |
| 313 | + return true; |
| 314 | + } |
309 | 315 | } |
310 | 316 | |
311 | 317 | /** |
— | — | @@ -448,6 +454,7 @@ |
449 | 455 | return $restored; |
450 | 456 | } |
451 | 457 | |
| 458 | + function getFileStatus() { return $this->fileStatus; } |
452 | 459 | } |
453 | 460 | |
454 | 461 | /** |
— | — | @@ -731,8 +738,13 @@ |
732 | 739 | $logViewer = new LogViewer( |
733 | 740 | new LogReader( |
734 | 741 | new FauxRequest( |
735 | | - array( 'page' => $this->mTargetObj->getPrefixedText(), |
736 | | - 'type' => 'delete' ) ) ) ); |
| 742 | + array( |
| 743 | + 'page' => $this->mTargetObj->getPrefixedText(), |
| 744 | + 'type' => 'delete' |
| 745 | + ) |
| 746 | + ) |
| 747 | + ), LogViewer::NO_ACTION_LINK |
| 748 | + ); |
737 | 749 | $logViewer->showList( $wgOut ); |
738 | 750 | |
739 | 751 | if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { |
— | — | @@ -836,15 +848,23 @@ |
837 | 849 | $this->mTargetTimestamp, |
838 | 850 | $this->mComment, |
839 | 851 | $this->mFileVersions ); |
840 | | - |
| 852 | + |
841 | 853 | if( $ok ) { |
842 | 854 | $skin = $wgUser->getSkin(); |
843 | 855 | $link = $skin->makeKnownLinkObj( $this->mTargetObj ); |
844 | 856 | $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); |
845 | | - return true; |
| 857 | + } else { |
| 858 | + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); |
846 | 859 | } |
| 860 | + |
| 861 | + // Show file deletion warnings and errors |
| 862 | + $status = $archive->getFileStatus(); |
| 863 | + if ( $status && !$status->isGood() ) { |
| 864 | + $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) ); |
| 865 | + } |
| 866 | + } else { |
| 867 | + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); |
847 | 868 | } |
848 | | - $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); |
849 | 869 | return false; |
850 | 870 | } |
851 | 871 | } |
Index: branches/liquidthreads/includes/DifferenceEngine.php |
— | — | @@ -290,8 +290,8 @@ |
291 | 291 | * Returns false if the diff could not be generated, otherwise returns true |
292 | 292 | */ |
293 | 293 | 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' ); |
296 | 296 | if ( $diff === false ) { |
297 | 297 | $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) ); |
298 | 298 | return false; |
— | — | @@ -314,12 +314,15 @@ |
315 | 315 | } |
316 | 316 | |
317 | 317 | /** |
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 |
321 | 324 | */ |
322 | | - function getDiff( $otitle, $ntitle ) { |
323 | | - $body = $this->getDiffBody(); |
| 325 | + function getDiff( $otitle, $ntitle, $skipCache = false ) { |
| 326 | + $body = $this->getDiffBody( $skipCache ); |
324 | 327 | if ( $body === false ) { |
325 | 328 | return false; |
326 | 329 | } else { |
— | — | @@ -330,17 +333,18 @@ |
331 | 334 | |
332 | 335 | /** |
333 | 336 | * 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 |
336 | 340 | */ |
337 | | - function getDiffBody() { |
| 341 | + function getDiffBody( $skipCache = false ) { |
338 | 342 | global $wgMemc; |
339 | 343 | $fname = 'DifferenceEngine::getDiffBody'; |
340 | 344 | wfProfileIn( $fname ); |
341 | 345 | |
342 | 346 | // Cacheable? |
343 | 347 | $key = false; |
344 | | - if ( $this->mOldid && $this->mNewid ) { |
| 348 | + if ( $this->mOldid && $this->mNewid && !$skipCache ) { |
345 | 349 | // Try cache |
346 | 350 | $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid ); |
347 | 351 | $difftext = $wgMemc->get( $key ); |
Index: branches/liquidthreads/includes/SpecialLog.php |
— | — | @@ -241,19 +241,25 @@ |
242 | 242 | * @addtogroup SpecialPage |
243 | 243 | */ |
244 | 244 | class LogViewer { |
| 245 | + const NO_ACTION_LINK = 1; |
| 246 | + |
245 | 247 | /** |
246 | 248 | * @var LogReader $reader |
247 | 249 | */ |
248 | 250 | var $reader; |
249 | 251 | var $numResults = 0; |
| 252 | + var $flags = 0; |
250 | 253 | |
251 | 254 | /** |
252 | 255 | * @param LogReader &$reader where to get our data from |
| 256 | + * @param integer $flags Bitwise combination of flags: |
| 257 | + * self::NO_ACTION_LINK Don't show restore/unblock/block links |
253 | 258 | */ |
254 | | - function LogViewer( &$reader ) { |
| 259 | + function LogViewer( &$reader, $flags = 0 ) { |
255 | 260 | global $wgUser; |
256 | 261 | $this->skin = $wgUser->getSkin(); |
257 | 262 | $this->reader =& $reader; |
| 263 | + $this->flags = $flags; |
258 | 264 | } |
259 | 265 | |
260 | 266 | /** |
— | — | @@ -363,41 +369,43 @@ |
364 | 370 | $paramArray = LogPage::extractParams( $s->log_params ); |
365 | 371 | $revert = ''; |
366 | 372 | // show revertmove link |
367 | | - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { |
368 | | - $destTitle = Title::newFromText( $paramArray[0] ); |
369 | | - if ( $destTitle ) { |
370 | | - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), |
371 | | - wfMsg( 'revertmove' ), |
372 | | - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . |
373 | | - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . |
374 | | - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . |
375 | | - '&wpMovetalk=0' ) . ')'; |
| 373 | + if ( !( $this->flags & self::NO_ACTION_LINK ) ) { |
| 374 | + if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { |
| 375 | + $destTitle = Title::newFromText( $paramArray[0] ); |
| 376 | + if ( $destTitle ) { |
| 377 | + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), |
| 378 | + wfMsg( 'revertmove' ), |
| 379 | + 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . |
| 380 | + '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . |
| 381 | + '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . |
| 382 | + '&wpMovetalk=0' ) . ')'; |
| 383 | + } |
| 384 | + // show undelete link |
| 385 | + } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { |
| 386 | + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), |
| 387 | + wfMsg( 'undeletebtn' ) , |
| 388 | + 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; |
| 389 | + |
| 390 | + // show unblock link |
| 391 | + } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { |
| 392 | + $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), |
| 393 | + wfMsg( 'unblocklink' ), |
| 394 | + 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')'; |
| 395 | + // show change protection link |
| 396 | + } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) { |
| 397 | + $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')'; |
| 398 | + // show user tool links for self created users |
| 399 | + // TODO: The extension should be handling this, get it out of core! |
| 400 | + } elseif ( $s->log_action == 'create2' ) { |
| 401 | + if( isset( $paramArray[0] ) ) { |
| 402 | + $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true ); |
| 403 | + } else { |
| 404 | + # Fall back to a blue contributions link |
| 405 | + $revert = $this->skin->userToolLinks( 1, $s->log_title ); |
| 406 | + } |
| 407 | + # Suppress $comment from old entries, not needed and can contain incorrect links |
| 408 | + $comment = ''; |
376 | 409 | } |
377 | | - // show undelete link |
378 | | - } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { |
379 | | - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), |
380 | | - wfMsg( 'undeletebtn' ) , |
381 | | - 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; |
382 | | - |
383 | | - // show unblock link |
384 | | - } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { |
385 | | - $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), |
386 | | - wfMsg( 'unblocklink' ), |
387 | | - 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')'; |
388 | | - // show change protection link |
389 | | - } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) { |
390 | | - $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')'; |
391 | | - // show user tool links for self created users |
392 | | - // TODO: The extension should be handling this, get it out of core! |
393 | | - } elseif ( $s->log_action == 'create2' ) { |
394 | | - if( isset( $paramArray[0] ) ) { |
395 | | - $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true ); |
396 | | - } else { |
397 | | - # Fall back to a blue contributions link |
398 | | - $revert = $this->skin->userToolLinks( 1, $s->log_title ); |
399 | | - } |
400 | | - # Suppress $comment from old entries, not needed and can contain incorrect links |
401 | | - $comment = ''; |
402 | 410 | } |
403 | 411 | |
404 | 412 | $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true ); |
Index: branches/liquidthreads/includes/DefaultSettings.php |
— | — | @@ -164,16 +164,6 @@ |
165 | 165 | /**#@-*/ |
166 | 166 | |
167 | 167 | /** |
168 | | - * By default deleted files are simply discarded; to save them and |
169 | | - * make it possible to undelete images, create a directory which |
170 | | - * is writable to the web server but is not exposed to the internet. |
171 | | - * |
172 | | - * Set $wgSaveDeletedFiles to true and set up the save path in |
173 | | - * $wgFileStore['deleted']['directory']. |
174 | | - */ |
175 | | -$wgSaveDeletedFiles = false; |
176 | | - |
177 | | -/** |
178 | 168 | * New file storage paths; currently used only for deleted files. |
179 | 169 | * Set it like this: |
180 | 170 | * |
— | — | @@ -181,7 +171,7 @@ |
182 | 172 | * |
183 | 173 | */ |
184 | 174 | $wgFileStore = array(); |
185 | | -$wgFileStore['deleted']['directory'] = null; // Don't forget to set this. |
| 175 | +$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted |
186 | 176 | $wgFileStore['deleted']['url'] = null; // Private |
187 | 177 | $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split |
188 | 178 | |
— | — | @@ -208,6 +198,10 @@ |
209 | 199 | * start with a capital letter. The current implementation may give incorrect |
210 | 200 | * description page links when the local $wgCapitalLinks and initialCapital |
211 | 201 | * are mismatched. |
| 202 | + * pathDisclosureProtection |
| 203 | + * May be 'paranoid' to remove all parameters from error messages, 'none' to |
| 204 | + * leave the paths in unchanged, or 'simple' to replace paths with |
| 205 | + * placeholders. Default for LocalRepo is 'simple'. |
212 | 206 | * |
213 | 207 | * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored |
214 | 208 | * for local repositories: |
Index: branches/liquidthreads/includes/FileStore.php |
— | — | @@ -219,7 +219,7 @@ |
220 | 220 | * Confirm that the given file key is valid. |
221 | 221 | * Note that a valid key may refer to a file that does not exist. |
222 | 222 | * |
223 | | - * Key should consist of a 32-digit base-36 SHA-1 hash and |
| 223 | + * Key should consist of a 31-digit base-36 SHA-1 hash and |
224 | 224 | * an optional alphanumeric extension, all lowercase. |
225 | 225 | * The whole must not exceed 64 characters. |
226 | 226 | * |
— | — | @@ -227,7 +227,7 @@ |
228 | 228 | * @return boolean |
229 | 229 | */ |
230 | 230 | static function validKey( $key ) { |
231 | | - return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key ); |
| 231 | + return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key ); |
232 | 232 | } |
233 | 233 | |
234 | 234 | |
— | — | @@ -249,7 +249,7 @@ |
250 | 250 | return false; |
251 | 251 | } |
252 | 252 | |
253 | | - $base36 = wfBaseConvert( $hash, 16, 36, 32 ); |
| 253 | + $base36 = wfBaseConvert( $hash, 16, 36, 31 ); |
254 | 254 | if( $extension == '' ) { |
255 | 255 | $key = $base36; |
256 | 256 | } else { |
Index: branches/liquidthreads/includes/PageHistory.php |
— | — | @@ -62,6 +62,7 @@ |
63 | 63 | * Setup page variables. |
64 | 64 | */ |
65 | 65 | $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); |
| 66 | + $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); |
66 | 67 | $wgOut->setArticleFlag( false ); |
67 | 68 | $wgOut->setArticleRelated( true ); |
68 | 69 | $wgOut->setRobotpolicy( 'noindex,nofollow' ); |
Property changes on: branches/liquidthreads |
___________________________________________________________________ |
Modified: svnmerge-integrated |
69 | 70 | - /trunk/phase3:1-24301 |
70 | 71 | + /trunk/phase3:1-24345 |