r26281 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r26280‎ | r26281 | r26282 >
Date:19:38, 1 October 2007
Author:aaron
Status:old
Tags:
Comment:
*Clean up deletion of revisions and remove some gaps
*Allow blocking of users to hide names
*Implement revision deletion for images/deleted files/deleted revs
*Log deletion set off for now
*Add 'hidden' file dir
*Dissallow merging via undelete (which was inefficient and hard to reverse)
*Use restore points and diffs to special:undelete
*Add a special page to merge pages
*Get changeslist to use tables to avoid ugly formatting
*Add logs into RC for rebuildrecentchanges.php
*Add private logs
*List private logs at specialpages
*Tweak/add some deletion and merge messages
Modified paths:
  • /trunk/phase3/includes/Article.php (modified) (history)
  • /trunk/phase3/includes/ChangesList.php (modified) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/DifferenceEngine.php (modified) (history)
  • /trunk/phase3/includes/EditPage.php (modified) (history)
  • /trunk/phase3/includes/FileDeleteForm.php (modified) (history)
  • /trunk/phase3/includes/ImagePage.php (modified) (history)
  • /trunk/phase3/includes/Linker.php (modified) (history)
  • /trunk/phase3/includes/LogPage.php (modified) (history)
  • /trunk/phase3/includes/PageHistory.php (modified) (history)
  • /trunk/phase3/includes/RecentChange.php (modified) (history)
  • /trunk/phase3/includes/Revision.php (modified) (history)
  • /trunk/phase3/includes/Setup.php (modified) (history)
  • /trunk/phase3/includes/SpecialBlockip.php (modified) (history)
  • /trunk/phase3/includes/SpecialContributions.php (modified) (history)
  • /trunk/phase3/includes/SpecialIpblocklist.php (modified) (history)
  • /trunk/phase3/includes/SpecialLog.php (modified) (history)
  • /trunk/phase3/includes/SpecialMergeHistory.php (added) (history)
  • /trunk/phase3/includes/SpecialPage.php (modified) (history)
  • /trunk/phase3/includes/SpecialRecentchanges.php (modified) (history)
  • /trunk/phase3/includes/SpecialRevisiondelete.php (modified) (history)
  • /trunk/phase3/includes/SpecialSpecialpages.php (modified) (history)
  • /trunk/phase3/includes/SpecialUndelete.php (modified) (history)
  • /trunk/phase3/includes/SpecialWatchlist.php (modified) (history)
  • /trunk/phase3/includes/Title.php (modified) (history)
  • /trunk/phase3/includes/filerepo/ArchivedFile.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FSRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/File.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FileRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/LocalFile.php (modified) (history)
  • /trunk/phase3/includes/filerepo/OldLocalFile.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/maintenance/rebuildrecentchanges.inc (modified) (history)
  • /trunk/phase3/maintenance/rebuildrecentchanges.php (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/rebuildrecentchanges.inc
@@ -11,7 +11,6 @@
1212 {
1313 $fname = 'rebuildRecentChangesTablePass1';
1414 $dbw = wfGetDB( DB_MASTER );
15 - extract( $dbw->tableNames( 'recentchanges', 'cur', 'old' ) );
1615
1716 $dbw->delete( 'recentchanges', '*' );
1817
@@ -35,6 +34,7 @@
3635 'rc_this_oldid' => 'rev_id',
3736 'rc_last_oldid' => 0, // is this ok?
3837 'rc_type' => $dbw->conditional( 'page_is_new != 0', RC_NEW, RC_EDIT ),
 38+ 'rc_deleted' => 'rev_deleted'
3939 ), array(
4040 'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $cutoff ) ),
4141 'rev_page=page_id'
@@ -99,6 +99,70 @@
100100
101101 function rebuildRecentChangesTablePass3()
102102 {
 103+ $fname = 'rebuildRecentChangesTablePass3';
 104+ $dbw = wfGetDB( DB_MASTER );
 105+
 106+ print( "Loading from user, page, and logging tables...\n" );
 107+
 108+ global $wgRCMaxAge, $wgLogRestrictions;
 109+
 110+ // Exclude non-public logs
 111+ $avoidLogs = array();
 112+ if( isset($wgLogRestrictions) ) {
 113+ foreach ( $wgLogRestrictions as $logtype => $right ) {
 114+ // Do not show private logs when not specifically requested
 115+ if ( $right !='*' ) {
 116+ $safetype =$dbw->strencode( $logtype );
 117+ $avoidLogs[] = "'$safetype'";
 118+ }
 119+ }
 120+ }
 121+ // Some logs don't go in RC. This can't really detect all of those ... :(
 122+ $avoidLogs[] = "'patrol'"; // hack...we don't want this here
 123+
 124+ if( !empty($avoidLogs) ) {
 125+ $skipLogs = 'log_type NOT IN(' . implode(',',$avoidLogs) . ')';
 126+ } else {
 127+ $skipLogs = '1 = 1';
 128+ }
 129+
 130+ $cutoff = time() - $wgRCMaxAge;
 131+ $dbw->insertSelect( 'recentchanges', array( 'logging', 'page', 'user' ),
 132+ array(
 133+ 'rc_timestamp' => 'log_timestamp',
 134+ 'rc_cur_time' => 'log_timestamp',
 135+ 'rc_user' => 'log_user',
 136+ 'rc_user_text' => 'user_name',
 137+ 'rc_namespace' => 'log_namespace',
 138+ 'rc_title' => 'log_title',
 139+ 'rc_comment' => 'log_comment',
 140+ 'rc_minor' => 0,
 141+ 'rc_bot' => 0,
 142+ 'rc_new' => 0,
 143+ 'rc_cur_id' => 0,
 144+ 'rc_this_oldid' => 0,
 145+ 'rc_last_oldid' => 0,
 146+ 'rc_type' => RC_LOG,
 147+ 'rc_cur_id' => 'page_id',
 148+ 'rc_log_type' => 'log_type',
 149+ 'rc_log_action' => 'log_action',
 150+ 'rc_logid' => 'log_id',
 151+ 'rc_params' => 'log_params',
 152+ 'rc_deleted' => 'log_deleted'
 153+ ), array(
 154+ 'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $cutoff ) ),
 155+ 'log_user=user_id',
 156+ 'log_namespace=page_namespace',
 157+ 'log_title=page_title',
 158+ $skipLogs
 159+ ), $fname,
 160+ array(), // INSERT options
 161+ array( 'ORDER BY' => 'log_timestamp DESC', 'LIMIT' => 5000 ) // SELECT options
 162+ );
 163+}
 164+
 165+function rebuildRecentChangesTablePass4()
 166+{
103167 global $wgGroupPermissions, $wgUseRCPatrol;
104168
105169 $dbw = wfGetDB( DB_MASTER );
Index: trunk/phase3/maintenance/rebuildrecentchanges.php
@@ -17,7 +17,8 @@
1818
1919 rebuildRecentChangesTablePass1();
2020 rebuildRecentChangesTablePass2();
21 -rebuildRecentChangesTablePass3(); // flag bot edits
 21+rebuildRecentChangesTablePass3(); // logs entries
 22+rebuildRecentChangesTablePass4(); // flag bot edits
2223
2324 print "Done.\n";
2425 exit();
Index: trunk/phase3/includes/ChangesList.php
@@ -75,7 +75,7 @@
7676 : $nothing;
7777 $f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing;
7878 $f .= $patrolled ? '<span class="unpatrolled">!</span>' : $nothing;
79 - return $f;
 79+ return "<tt>$f</tt>";
8080 }
8181
8282 /**
@@ -101,6 +101,32 @@
102102 }
103103 }
104104
 105+ /**
 106+ * int $field one of DELETED_* bitfield constants
 107+ * @return bool
 108+ */
 109+ function isDeleted( $rc, $field ) {
 110+ return ($rc->mAttribs['rc_deleted'] & $field) == $field;
 111+ }
 112+
 113+ /**
 114+ * Determine if the current user is allowed to view a particular
 115+ * field of this revision, if it's marked as deleted.
 116+ * @param int $field
 117+ * @return bool
 118+ */
 119+ function userCan( $rc, $field ) {
 120+ if( ( $rc->mAttribs['rc_deleted'] & $field ) == $field ) {
 121+ global $wgUser;
 122+ $permission = ( $rc->mAttribs['rc_deleted'] & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED
 123+ ? 'hiderevision'
 124+ : 'deleterevision';
 125+ wfDebug( "Checking for $permission due to $field match on $rc->mAttribs['rc_deleted']\n" );
 126+ return $wgUser->isAllowed( $permission );
 127+ } else {
 128+ return true;
 129+ }
 130+ }
105131
106132 function insertMove( &$s, $rc ) {
107133 # Diff
@@ -136,11 +162,12 @@
137163 $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')';
138164 }
139165
140 -
141166 function insertDiffHist(&$s, &$rc, $unpatrolled) {
142167 # Diff link
143 - if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
 168+ if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
144169 $diffLink = $this->message['diff'];
 170+ } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
 171+ $diffLink = $this->message['diff'];
145172 } else {
146173 $rcidparam = $unpatrolled
147174 ? array( 'rcid' => $rc->mAttribs['rc_id'] )
@@ -170,7 +197,12 @@
171198 $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW )
172199 ? 'rcid='.$rc->mAttribs['rc_id']
173200 : '';
174 - $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
 201+ if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
 202+ $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
 203+ $articlelink = '<span class="history-deleted">'.$articlelink.'</span>';
 204+ } else {
 205+ $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
 206+ }
175207 if( $watched )
176208 $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
177209 global $wgContLang;
@@ -187,15 +219,38 @@
188220
189221 /** Insert links to user page, user talk page and eventually a blocking link */
190222 function insertUserRelatedLinks(&$s, &$rc) {
191 - $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
192 - $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
 223+ if ( $this->isDeleted($rc,Revision::DELETED_USER) ) {
 224+ $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';
 225+ } else {
 226+ $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
 227+ $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
 228+ }
193229 }
194230
 231+ /** insert a formatted action */
 232+ function insertAction(&$s, &$rc) {
 233+ # Add comment
 234+ if( $rc->mAttribs['rc_type'] == RC_LOG ) {
 235+ // log action
 236+ if ( $this->isDeleted($rc,LogViewer::DELETED_ACTION) ) {
 237+ $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 238+ } else {
 239+ $s .= ' ' . LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'],
 240+ $rc->getTitle(), $this->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true );
 241+ }
 242+ }
 243+ }
 244+
195245 /** insert a formatted comment */
196246 function insertComment(&$s, &$rc) {
197247 # Add comment
198248 if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
199 - $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
 249+ // log comment
 250+ if ( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) {
 251+ $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
 252+ } else {
 253+ $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
 254+ }
200255 }
201256 }
202257
@@ -251,18 +306,22 @@
252307
253308 $s .= '<li>';
254309
255 - // moved pages
 310+ // Moved pages
256311 if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
257312 $this->insertMove( $s, $rc );
258 - // log entries
259 - } elseif ( $rc_namespace == NS_SPECIAL ) {
 313+ // Log entries
 314+ } elseif( $rc_log_type !='' ) {
 315+ $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
 316+ $this->insertLog( $s, $logtitle, $rc_log_type );
 317+ // Log entries (old format) or log targets, and special pages
 318+ } elseif( $rc_namespace == NS_SPECIAL ) {
260319 list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
261320 if ( $specialName == 'Log' ) {
262321 $this->insertLog( $s, $rc->getTitle(), $specialSubpage );
263322 } else {
264323 wfDebug( "Unexpected special page in recentchanges\n" );
265324 }
266 - // all other stuff
 325+ // Log entries
267326 } else {
268327 wfProfileIn($fname.'-page');
269328
@@ -284,10 +343,16 @@
285344 }
286345
287346 $this->insertUserRelatedLinks($s,$rc);
 347+ $this->insertAction($s, $rc);
288348 $this->insertComment($s, $rc);
 349+
 350+ # Mark revision as deleted
 351+ if ( !$rc_log_type && $this->isDeleted($rc,Revision::DELETED_TEXT) )
 352+ $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
 353+ if($rc->numberofWatchingusers > 0) {
 354+ $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers));
 355+ }
289356
290 - $s .= rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers));
291 -
292357 $s .= "</li>\n";
293358
294359 wfProfileOut( $fname.'-rest' );
@@ -334,12 +399,14 @@
335400 $rc->unpatrolled = false;
336401 }
337402
 403+ $showrev=true;
338404 # Make article link
339405 if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
340406 $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir";
341407 $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ),
342408 $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
343 - } elseif( $rc_namespace == NS_SPECIAL ) {
 409+ } else if( $rc_namespace == NS_SPECIAL ) {
 410+ // Log entries (old format) and special pages
344411 list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
345412 if ( $specialName == 'Log' ) {
346413 # Log updates, etc
@@ -349,7 +416,16 @@
350417 wfDebug( "Unexpected special page in recentchanges\n" );
351418 $clink = '';
352419 }
353 - } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) {
 420+ } elseif ( $rc_log_type !='' ) {
 421+ // Log entries
 422+ $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
 423+ $logname = LogPage::logName( $rc_log_type );
 424+ $clink = '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
 425+ } if ( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
 426+ $clink = '<span class="history-deleted">' . $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ) . '</span>';
 427+ if ( !ChangesList::userCan($rc,Revision::DELETED_TEXT) )
 428+ $showrev=false;
 429+ } else if( $rc->unpatrolled && $rc_type == RC_NEW ) {
354430 # Unpatrolled new page, give rc_id in query
355431 $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" );
356432 } else {
@@ -372,7 +448,10 @@
373449 $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery";
374450 $aprops = ' tabindex="'.$baseRC->counter.'"';
375451 $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops );
376 - if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
 452+ if ( !$showrev ) {
 453+ $curLink = $this->message['cur'];
 454+ $diffLink = $this->message['diff'];
 455+ } else if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
377456 if( $rc_type != RC_NEW ) {
378457 $curLink = $this->message['cur'];
379458 }
@@ -382,21 +461,27 @@
383462 }
384463
385464 # Make "last" link
386 - if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
 465+ if ( !$showrev ) {
 466+ $lastLink = $this->message['last'];
 467+ } else if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
387468 $lastLink = $this->message['last'];
388469 } else {
389470 $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'],
390 - $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
 471+ $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
391472 }
 473+
 474+ # Make user links
 475+ if ( $this->isDeleted($rc,Revision::DELETED_USER) ) {
 476+ $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';
 477+ } else {
 478+ $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
 479+ $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
 480+ }
392481
393 - $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
394 -
395482 $rc->lastlink = $lastLink;
396483 $rc->curlink = $curLink;
397484 $rc->difflink = $diffLink;
398485
399 - $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
400 -
401486 # Put accumulated information into the cache, for later display
402487 # Page moves go on their own line
403488 $title = $rc->getTitle();
@@ -418,10 +503,11 @@
419504 */
420505 function recentChangesBlockGroup( $block ) {
421506 global $wgLang, $wgContLang, $wgRCShowChangedSize;
422 - $r = '';
 507+ $r = '<table cellpadding="0" cellspacing="0"><tr>';
423508
424509 # Collate list of users
425510 $isnew = false;
 511+ $namehidden = true;
426512 $unpatrolled = false;
427513 $userlinks = array();
428514 foreach( $block as $rcObj ) {
@@ -429,6 +515,11 @@
430516 if( $rcObj->mAttribs['rc_new'] ) {
431517 $isnew = true;
432518 }
 519+ // if all log actions to this page were hidden, then don't
 520+ // give the name of the affected page for this block
 521+ if( !($rcObj->mAttribs['rc_deleted'] & LogViewer::DELETED_ACTION) ) {
 522+ $namehidden = false;
 523+ }
433524 $u = $rcObj->userlink;
434525 if( !isset( $userlinks[$u] ) ) {
435526 $userlinks[$u] = 0;
@@ -462,24 +553,25 @@
463554 $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')";
464555 $tl = '<span id="'.$rcm.'"><a href="'.$toggleLink.'">' . $this->sideArrow() . '</a></span>';
465556 $tl .= '<span id="'.$rcl.'" style="display:none"><a href="'.$toggleLink.'">' . $this->downArrow() . '</a></span>';
466 - $r .= $tl;
 557+ $r .= '<td valign="top">'.$tl;
467558
468559 # Main line
469 - $r .= '<tt>';
470 - $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
 560+ $r .= ' '.$this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
471561
472562 # Timestamp
473 - $r .= ' '.$block[0]->timestamp.' </tt>';
 563+ $r .= '&nbsp;'.$block[0]->timestamp.'&nbsp;&nbsp;</td><td>';
474564
475565 # Article link
476 - $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
 566+ if ( $namehidden )
 567+ $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 568+ else
 569+ $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
477570 $r .= $wgContLang->getDirMark();
478571
479572 $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id'];
480573 $currentRevision = $block[0]->mAttribs['rc_this_oldid'];
481574 if( $block[0]->mAttribs['rc_type'] != RC_LOG ) {
482575 # Changes
483 -
484576 $n = count($block);
485577 static $nchanges = array();
486578 if ( !isset( $nchanges[$n] ) ) {
@@ -489,25 +581,25 @@
490582
491583 $r .= ' (';
492584
493 - if( $isnew ) {
 585+ if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
 586+ $r .= $nchanges[$n];
 587+ } else if( $isnew ) {
494588 $r .= $nchanges[$n];
495589 } else {
496590 $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
497591 $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" );
498592 }
499593
500 - $r .= ') . . ';
501 -
502594 if( $wgRCShowChangedSize ) {
503595 # Character difference
504596 $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'],
505597 $block[0]->mAttribs['rc_new_len'] );
506598 if( $chardiff == '' ) {
507 - $r .= ' (';
 599+ $r .= ') ';
508600 } else {
509601 $r .= ' ' . $chardiff. ' . . ';
510602 }
511 - }
 603+ }
512604
513605 # History
514606 $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
@@ -516,51 +608,70 @@
517609 }
518610
519611 $r .= $users;
 612+ $r .=$this->numberofWatchingusers($block[0]->numberofWatchingusers);
 613+
 614+ $r .= "</td></tr></table>\n";
520615
521 - $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
522 - $r .= "<br />\n";
523 -
524616 # Sub-entries
525 - $r .= '<div id="'.$rci.'" style="display:none">';
 617+ $r .= '<div id="'.$rci.'" style="display:none; font-size:95%;"><table cellpadding="0" cellspacing="0">';
526618 foreach( $block as $rcObj ) {
527619 # Get rc_xxxx variables
528620 // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
529621 extract( $rcObj->mAttribs );
530622
531 - $r .= $this->spacerArrow();
532 - $r .= '<tt>&nbsp; &nbsp; &nbsp; &nbsp;';
 623+ #$r .= '<tr><td valign="top">'.$this->spacerArrow();
 624+ $r .= '<tr><td valign="top">'.$this->spacerIndent();
 625+ $r .= '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
533626 $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
534 - $r .= '&nbsp;</tt>';
 627+ $r .= '&nbsp;&nbsp;</td><td valign="top">';
535628
536629 $o = '';
537630 if( $rc_this_oldid != 0 ) {
538631 $o = 'oldid='.$rc_this_oldid;
539632 }
 633+ # Revision link
540634 if( $rc_type == RC_LOG ) {
541 - $link = $rcObj->timestamp;
 635+ $link = $rcObj->timestamp.' ';
 636+ } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
 637+ $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> ';
542638 } else {
543639 $link = $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o );
 640+ if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
 641+ $link = '<span class="history-deleted">'.$link.'</span> ';
544642 }
545 - $link = '<tt>'.$link.'</tt>';
546 -
547643 $r .= $link;
548 - $r .= ' (';
549 - $r .= $rcObj->curlink;
550 - $r .= '; ';
551 - $r .= $rcObj->lastlink;
552 - $r .= ') . . ';
 644+
 645+ if ( !$rc_log_type ) {
 646+ $r .= ' (';
 647+ $r .= $rcObj->curlink;
 648+ $r .= '; ';
 649+ $r .= $rcObj->lastlink;
 650+ $r .= ')';
 651+ } else {
 652+ $logname = LogPage::logName( $rc_log_type );
 653+ $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
 654+ $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
 655+ }
 656+ $r .= ' . . ';
553657
554658 # Character diff
555659 if( $wgRCShowChangedSize ) {
556660 $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ;
557661 }
558 -
 662+ # User links
559663 $r .= $rcObj->userlink;
560664 $r .= $rcObj->usertalklink;
561 - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
562 - $r .= "<br />\n";
 665+ // log action
 666+ parent::insertAction($r, $rcObj);
 667+ // log comment
 668+ parent::insertComment($r, $rcObj);
 669+ # Mark revision as deleted
 670+ if ( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
 671+ $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
 672+
 673+ $r .= "</td></tr>\n";
563674 }
564 - $r .= "</div>\n";
 675+ $r .= "</table></div>\n";
565676
566677 $this->rcCacheIndex++;
567678 return $r;
@@ -617,8 +728,23 @@
618729 * @access private
619730 */
620731 function spacerArrow() {
 732+ //FIXME: problems with FF 1.5x
621733 return $this->arrow( '', ' ' );
622734 }
 735+
 736+ /**
 737+ * Generate HTML for the equivilant of a spacer image for tables
 738+ * @return string HTML <td> tag
 739+ * @access private
 740+ */
 741+ function spacerColumn() {
 742+ return '<td width="12"></td>';
 743+ }
 744+
 745+ // Adds a few spaces
 746+ function spacerIndent() {
 747+ return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
 748+ }
623749
624750 /**
625751 * Enhanced RC ungrouped line.
@@ -632,46 +758,64 @@
633759 extract( $rcObj->mAttribs );
634760 $curIdEq = 'curid='.$rc_cur_id;
635761
636 - $r = '';
 762+ $r = '<table cellspacing="0" cellpadding="0"><tr><td>';
637763
638 - # Spacer image
639 - $r .= $this->spacerArrow();
640 -
 764+ # spacerArrow() causes issues in FF
 765+ $r .= $this->spacerColumn();
 766+ $r .= '<td valign="top">';
 767+
641768 # Flag and Timestamp
642 - $r .= '<tt>';
643 -
644769 if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
645 - $r .= '&nbsp;&nbsp;&nbsp;';
 770+ $r .= '&nbsp;&nbsp;&nbsp;&nbsp;';
646771 } else {
647 - $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
 772+ $r .= '&nbsp;'.$this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
648773 }
649 - $r .= ' '.$rcObj->timestamp.' </tt>';
650 -
 774+ $r .= '&nbsp;'.$rcObj->timestamp.'&nbsp;&nbsp;</td><td>';
 775+
651776 # Article link
652 - $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
653 -
654 - # Diff
655 - $r .= ' ('. $rcObj->difflink .'; ';
656 -
657 - # Hist
658 - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . ';
659 -
 777+ if ( $rc_log_type !='' ) {
 778+ $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
 779+ $logname = LogPage::logName( $rc_log_type );
 780+ $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
 781+ // All other stuff
 782+ } else {
 783+ $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
 784+ }
 785+ if ( $rc_type != RC_LOG ) {
 786+ # Diff
 787+ $r .= ' ('. $rcObj->difflink .'; ';
 788+ # Hist
 789+ $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ')';
 790+ }
 791+ $r .= ' . . ';
 792+
660793 # Character diff
661794 if( $wgRCShowChangedSize ) {
662795 $r .= ( $rcObj->getCharacterDifference() == '' ? '' : '&nbsp;' . $rcObj->getCharacterDifference() . ' . . ' ) ;
663796 }
664797
665798 # User/talk
666 - $r .= $rcObj->userlink . $rcObj->usertalklink;
 799+ $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
667800
668801 # Comment
669802 if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) {
670 - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
 803+ // log action
 804+ if ( $this->isDeleted($rcObj,LogViewer::DELETED_ACTION) ) {
 805+ $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 806+ } else {
 807+ $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(), $this->skin, LogPage::extractParams($rc_params), true, true );
 808+ }
 809+ // log comment
 810+ if ( $this->isDeleted($rcObj,LogViewer::DELETED_COMMENT) ) {
 811+ $r .= ' <span class="history-deleted">' . wfMsg('rev-deleted-comment') . '</span>';
 812+ } else {
 813+ $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
 814+ }
671815 }
672816
673817 $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
674818
675 - $r .= "<br />\n";
 819+ $r .= "</td></tr></table>\n";
676820 return $r;
677821 }
678822
Index: trunk/phase3/includes/SpecialUndelete.php
@@ -78,7 +78,7 @@
7979 array(
8080 'ar_namespace',
8181 'ar_title',
82 - 'COUNT(*) AS count',
 82+ 'COUNT(*) AS count'
8383 ),
8484 $condition,
8585 __METHOD__,
@@ -92,24 +92,6 @@
9393 }
9494
9595 /**
96 - * List the revisions of the given page. Returns result wrapper with
97 - * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
98 - *
99 - * @return ResultWrapper
100 - */
101 - function listRevisions() {
102 - $dbr = wfGetDB( DB_SLAVE );
103 - $res = $dbr->select( 'archive',
104 - array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len' ),
105 - array( 'ar_namespace' => $this->title->getNamespace(),
106 - 'ar_title' => $this->title->getDBkey() ),
107 - 'PageArchive::listRevisions',
108 - array( 'ORDER BY' => 'ar_timestamp DESC' ) );
109 - $ret = $dbr->resultObject( $res );
110 - return $ret;
111 - }
112 -
113 - /**
11496 * List the deleted file revisions for this page, if it's a file page.
11597 * Returns a result wrapper with various filearchive fields, or null
11698 * if not a file page.
@@ -124,14 +106,22 @@
125107 array(
126108 'fa_id',
127109 'fa_name',
 110+ 'fa_archive_name',
128111 'fa_storage_key',
 112+ 'fa_storage_group',
129113 'fa_size',
130114 'fa_width',
131115 'fa_height',
 116+ 'fa_bits',
 117+ 'fa_metadata',
 118+ 'fa_media_type',
 119+ 'fa_major_mime',
 120+ 'fa_minor_mime',
132121 'fa_description',
133122 'fa_user',
134123 'fa_user_text',
135 - 'fa_timestamp' ),
 124+ 'fa_timestamp',
 125+ 'fa_deleted' ),
136126 array( 'fa_name' => $this->title->getDbKey() ),
137127 __METHOD__,
138128 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -152,14 +142,25 @@
153143 $rev = $this->getRevision( $timestamp );
154144 return $rev ? $rev->getText() : null;
155145 }
 146+
 147+ function getRevisionConds( $timestamp, $id ) {
 148+ if( $id ) {
 149+ $id = intval($id);
 150+ return "ar_rev_id=$id";
 151+ } else if( $timestamp ) {
 152+ return "ar_timestamp=$timestamp";
 153+ } else {
 154+ return 'ar_rev_id=0';
 155+ }
 156+ }
156157
157158 /**
158159 * Return a Revision object containing data for the deleted revision.
159 - * Note that the result *may* or *may not* have a null page ID.
160 - * @param string $timestamp
 160+ * Note that the result *may* have a null page ID.
 161+ * @param string $timestamp or $id
161162 * @return Revision
162163 */
163 - function getRevision( $timestamp ) {
 164+ function getRevision( $timestamp, $id=null ) {
164165 $dbr = wfGetDB( DB_SLAVE );
165166 $row = $dbr->selectRow( 'archive',
166167 array(
@@ -172,10 +173,11 @@
173174 'ar_minor_edit',
174175 'ar_flags',
175176 'ar_text_id',
 177+ 'ar_deleted',
176178 'ar_len' ),
177179 array( 'ar_namespace' => $this->title->getNamespace(),
178180 'ar_title' => $this->title->getDbkey(),
179 - 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
 181+ $this->getRevisionConds( $dbr->timestamp($timestamp), $id ) ),
180182 __METHOD__ );
181183 if( $row ) {
182184 return new Revision( array(
@@ -189,7 +191,9 @@
190192 'user_text' => $row->ar_user_text,
191193 'timestamp' => $row->ar_timestamp,
192194 'minor_edit' => $row->ar_minor_edit,
193 - 'text_id' => $row->ar_text_id ) );
 195+ 'text_id' => $row->ar_text_id,
 196+ 'deleted' => $row->ar_deleted,
 197+ 'len' => $row->ar_len) );
194198 } else {
195199 return null;
196200 }
@@ -254,48 +258,50 @@
255259 * Restore the given (or all) text and file revisions for the page.
256260 * Once restored, the items will be removed from the archive tables.
257261 * The deletion log will be updated with an undeletion notice.
 262+ * Use -1 for the one of the timestamps to only restore files or text
258263 *
259 - * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
 264+ * @param string $pagetimestamp, restore all revisions since this time
260265 * @param string $comment
261 - * @param array $fileVersions
 266+ * @param string $filetimestamp, restore all revision from this time on
 267+ * @param bool $Unsuppress
262268 *
263269 * @return true on success.
264270 */
265 - function undelete( $timestamps, $comment = '', $fileVersions = array() ) {
 271+ function undelete( $pagetimestamp = 0, $comment = '', $filetimestamp = 0, $Unsuppress = false) {
266272 // If both the set of text revisions and file revisions are empty,
267273 // restore everything. Otherwise, just restore the requested items.
268 - $restoreAll = empty( $timestamps ) && empty( $fileVersions );
 274+ $restoreAll = ($pagetimestamp==0 && $filetimestamp==0);
269275
270 - $restoreText = $restoreAll || !empty( $timestamps );
271 - $restoreFiles = $restoreAll || !empty( $fileVersions );
 276+ $restoreText = ($restoreAll || $pagetimestamp );
 277+ $restoreFiles = ($restoreAll || $filetimestamp );
272278
273 - if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
 279+ if( $restoreText && $pagetimestamp >= 0 ) {
 280+ $textRestored = $this->undeleteRevisions( $pagetimestamp, $Unsuppress );
 281+ } else {
 282+ $textRestored = 0;
 283+ }
 284+
 285+ if( $restoreFiles && $filetimestamp >= 0 && $this->title->getNamespace()==NS_IMAGE ) {
274286 $img = wfLocalFile( $this->title );
275 - $this->fileStatus = $img->restore( $fileVersions );
 287+ $this->fileStatus = $img->restore( $filetimestamp, $Unsuppress );
276288 $filesRestored = $this->fileStatus->successCount;
277289 } else {
278290 $filesRestored = 0;
279291 }
280 -
281 - if( $restoreText ) {
282 - $textRestored = $this->undeleteRevisions( $timestamps );
283 - } else {
284 - $textRestored = 0;
285 - }
286292
287293 // Touch the log!
288294 global $wgContLang;
289295 $log = new LogPage( 'delete' );
290296
291297 if( $textRestored && $filesRestored ) {
292 - $reason = wfMsgForContent( 'undeletedrevisions-files',
 298+ $reason = wfMsgExt( 'undeletedrevisions-files', array('parsemag'),
293299 $wgContLang->formatNum( $textRestored ),
294300 $wgContLang->formatNum( $filesRestored ) );
295301 } elseif( $textRestored ) {
296 - $reason = wfMsgForContent( 'undeletedrevisions',
 302+ $reason = wfMsgExt( 'undeletedrevisions', array('parsemag'),
297303 $wgContLang->formatNum( $textRestored ) );
298304 } elseif( $filesRestored ) {
299 - $reason = wfMsgForContent( 'undeletedfiles',
 305+ $reason = wfMsgExt( 'undeletedfiles', array('parsemag'),
300306 $wgContLang->formatNum( $filesRestored ) );
301307 } else {
302308 wfDebug( "Undelete: nothing undeleted...\n" );
@@ -304,7 +310,7 @@
305311
306312 if( trim( $comment ) != '' )
307313 $reason .= ": {$comment}";
308 - $log->addEntry( 'restore', $this->title, $reason );
 314+ $log->addEntry( 'restore', $this->title, $reason, array($pagetimestamp,$filetimestamp) );
309315
310316 if ( $this->fileStatus && !$this->fileStatus->ok ) {
311317 return false;
@@ -318,52 +324,73 @@
319325 * to the cur/old tables. If the page currently exists, all revisions will
320326 * be stuffed into old, otherwise the most recent will go into cur.
321327 *
322 - * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
 328+ * @param string $timestamps, restore all revisions since this time
323329 * @param string $comment
324330 * @param array $fileVersions
 331+ * @param bool $Unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
325332 *
326333 * @return int number of revisions restored
327334 */
328 - private function undeleteRevisions( $timestamps ) {
329 - $restoreAll = empty( $timestamps );
 335+ private function undeleteRevisions( $timestamp, $Unsuppress = false ) {
 336+ $restoreAll = ($timestamp==0);
330337
331338 $dbw = wfGetDB( DB_MASTER );
 339+ $makepage = false; // Do we need to make a new page?
332340
333341 # Does this page already exist? We'll have to update it...
334342 $article = new Article( $this->title );
335343 $options = 'FOR UPDATE';
336344 $page = $dbw->selectRow( 'page',
337345 array( 'page_id', 'page_latest' ),
338 - array( 'page_namespace' => $this->title->getNamespace(),
339 - 'page_title' => $this->title->getDBkey() ),
 346+ array( 'page_namespace' => $this->title->getNamespace(),
 347+ 'page_title' => $this->title->getDBkey() ),
340348 __METHOD__,
341349 $options );
 350+
342351 if( $page ) {
343352 # Page already exists. Import the history, and if necessary
344353 # we'll update the latest revision field in the record.
345354 $newid = 0;
346355 $pageId = $page->page_id;
347356 $previousRevId = $page->page_latest;
 357+ # Get the time span of this page
 358+ $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
 359+ array( 'rev_id' => $previousRevId ),
 360+ __METHOD__ );
 361+
 362+ if( $previousTimestamp === false ) {
 363+ wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
 364+ return false;
 365+ }
 366+ # Do not fuck up histories by merging them in annoying, unrevertable ways
 367+ # This page id should match any deleted ones (excepting NULL values)
 368+ # We can allow restoration into redirect pages with no edit history
 369+ $otherpages = $dbw->selectField( 'archive', 'COUNT(*)',
 370+ array( 'ar_namespace' => $this->title->getNamespace(),
 371+ 'ar_title' => $this->title->getDBkey(),
 372+ 'ar_page_id IS NOT NULL', "ar_page_id != $pageId" ),
 373+ __METHOD__,
 374+ array('LIMIT' => 1) );
 375+ if( $otherpages && !$this->title->isValidRestoreOverTarget() ) {
 376+ return false;
 377+ }
 378+
348379 } else {
349380 # Have to create a new article...
350 - $newid = $article->insertOn( $dbw );
351 - $pageId = $newid;
 381+ $makepage = true;
352382 $previousRevId = 0;
 383+ $previousTimestamp = 0;
353384 }
354385
355 - if( $restoreAll ) {
356 - $oldones = '1 = 1'; # All revisions...
357 - } else {
358 - $oldts = implode( ',',
359 - array_map( array( &$dbw, 'addQuotes' ),
360 - array_map( array( &$dbw, 'timestamp' ),
361 - $timestamps ) ) );
362 -
363 - $oldones = "ar_timestamp IN ( {$oldts} )";
 386+ $conditions = array(
 387+ 'ar_namespace' => $this->title->getNamespace(),
 388+ 'ar_title' => $this->title->getDBkey() );
 389+ if( $timestamp ) {
 390+ $conditions[] = "ar_timestamp >= {$timestamp}";
364391 }
365392
366393 /**
367 - * Restore each revision...
 394+ * Select each archived revision...
368395 */
369396 $result = $dbw->select( 'archive',
370397 /* fields */ array(
@@ -376,24 +403,40 @@
377404 'ar_minor_edit',
378405 'ar_flags',
379406 'ar_text_id',
 407+ 'ar_deleted',
380408 'ar_len' ),
381 - /* WHERE */ array(
382 - 'ar_namespace' => $this->title->getNamespace(),
383 - 'ar_title' => $this->title->getDBkey(),
384 - $oldones ),
 409+ /* WHERE */
 410+ $conditions,
385411 __METHOD__,
386412 /* options */ array(
387413 'ORDER BY' => 'ar_timestamp' )
388414 );
389 - if( $dbw->numRows( $result ) < count( $timestamps ) ) {
390 - wfDebug( __METHOD__.": couldn't find all requested rows\n" );
391 - return false;
 415+ $ret = $dbw->resultObject( $result );
 416+
 417+ $rev_count = $dbw->numRows( $result );
 418+ if( $rev_count ) {
 419+ # We need to seek around as just using DESC in the ORDER BY
 420+ # would leave the revisions inserted in the wrong order
 421+ $first = $ret->fetchObject();
 422+ $ret->seek( $rev_count - 1 );
 423+ $last = $ret->fetchObject();
 424+ // We don't handle well changing the top revision's settings
 425+ if( !$Unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
 426+ wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
 427+ return false;
 428+ }
 429+ $ret->seek( 0 );
392430 }
393431
 432+ if( $makepage ) {
 433+ $newid = $article->insertOn( $dbw );
 434+ $pageId = $newid;
 435+ }
 436+
394437 $revision = null;
395438 $restored = 0;
396 -
397 - while( $row = $dbw->fetchObject( $result ) ) {
 439+
 440+ while( $row = $ret->fetchObject() ) {
398441 if( $row->ar_text_id ) {
399442 // Revision was deleted in 1.5+; text is in
400443 // the regular text table, use the reference.
@@ -416,12 +459,14 @@
417460 'timestamp' => $row->ar_timestamp,
418461 'minor_edit' => $row->ar_minor_edit,
419462 'text_id' => $row->ar_text_id,
420 - 'len' => $row->ar_len
 463+ 'deleted' => $Unsuppress ? 0 : $row->ar_deleted,
 464+ 'len' => $row->ar_len
421465 ) );
422466 $revision->insertOn( $dbw );
423467 $restored++;
424468 }
425 -
 469+
 470+ # If there were any revisions restored...
426471 if( $revision ) {
427472 // Attach the latest revision to the page...
428473 $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
@@ -430,7 +475,7 @@
431476 // Update site stats, link tables, etc
432477 $article->createUpdates( $revision );
433478 }
434 -
 479+
435480 if( $newid ) {
436481 wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
437482 Article::onArticleCreate( $this->title );
@@ -438,17 +483,22 @@
439484 wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
440485 Article::onArticleEdit( $this->title );
441486 }
442 - } else {
443 - # Something went terribly wrong!
444487 }
445488
446489 # Now that it's safely stored, take it out of the archive
447490 $dbw->delete( 'archive',
448 - /* WHERE */ array(
449 - 'ar_namespace' => $this->title->getNamespace(),
450 - 'ar_title' => $this->title->getDBkey(),
451 - $oldones ),
 491+ /* WHERE */
 492+ $conditions,
452493 __METHOD__ );
 494+ # Update any revision left to reflect the page they belong to.
 495+ # If a page was deleted, and a new one created over it, then deleted,
 496+ # selective restore acts as a way to seperate the two. Nevertheless, we
 497+ # still want the rest to be restorable, in case some mistake was made.
 498+ $dbw->update( 'archive',
 499+ array( 'ar_page_id' => $newid ),
 500+ array( 'ar_namespace' => $this->title->getNamespace(),
 501+ 'ar_title' => $this->title->getDBkey() ),
 502+ __METHOD__ );
453503
454504 return $restored;
455505 }
@@ -473,71 +523,92 @@
474524 $time = $request->getVal( 'timestamp' );
475525 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
476526 $this->mFile = $request->getVal( 'file' );
 527+ $this->mDiff = $request->getVal( 'diff' );
 528+ $this->mOldid = $request->getVal( 'oldid' );
477529
478 - $posted = $request->wasPosted() &&
479 - $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
 530+ $posted = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
480531 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
481532 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
482533 $this->mComment = $request->getText( 'wpComment' );
 534+ $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'oversight' );
483535
484536 if( $par != "" ) {
485537 $this->mTarget = $par;
 538+ $_GET['target'] = $par; // hack for Pager
486539 }
487 - if ( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
 540+ if( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
488541 $this->mAllowed = true;
489542 } else {
490543 $this->mAllowed = false;
491544 $this->mTimestamp = '';
492545 $this->mRestore = false;
493546 }
494 - if ( $this->mTarget !== "" ) {
 547+ if( $this->mTarget !== "" ) {
495548 $this->mTargetObj = Title::newFromURL( $this->mTarget );
496549 } else {
497550 $this->mTargetObj = NULL;
498551 }
499552 if( $this->mRestore ) {
500 - $timestamps = array();
501 - $this->mFileVersions = array();
502 - foreach( $_REQUEST as $key => $val ) {
503 - $matches = array();
504 - if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
505 - array_push( $timestamps, $matches[1] );
506 - }
507 -
508 - if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
509 - $this->mFileVersions[] = intval( $matches[1] );
510 - }
 553+ $this->mFileTimestamp = $request->getVal('imgrestorepoint');
 554+ $this->mPageTimestamp = $request->getVal('restorepoint');
 555+ }
 556+ $this->preCacheMessages();
 557+ }
 558+
 559+ /**
 560+ * As we use the same small set of messages in various methods and that
 561+ * they are called often, we call them once and save them in $this->message
 562+ */
 563+ function preCacheMessages() {
 564+ // Precache various messages
 565+ if( !isset( $this->message ) ) {
 566+ foreach( explode(' ', 'last rev-delundel' ) as $msg ) {
 567+ $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
511568 }
512 - rsort( $timestamps );
513 - $this->mTargetTimestamp = $timestamps;
514569 }
515570 }
516571
517572 function execute() {
518 - global $wgOut;
519 - if ( $this->mAllowed ) {
520 - $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
 573+ global $wgOut, $wgUser;
 574+ if( $this->mAllowed ) {
 575+ $wgOut->setPagetitle( wfMsgHtml( "undeletepage" ) );
521576 } else {
522 - $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
 577+ $wgOut->setPagetitle( wfMsgHtml( "viewdeletedpage" ) );
523578 }
524579
525580 if( is_null( $this->mTargetObj ) ) {
526 - $this->showSearchForm();
 581+ # Not all users can just browse every deleted page from the list
 582+ if( $wgUser->isAllowed( 'browsearchive' ) ) {
 583+ $this->showSearchForm();
527584
528 - # List undeletable articles
529 - if( $this->mSearchPrefix ) {
530 - $result = PageArchive::listPagesByPrefix(
531 - $this->mSearchPrefix );
532 - $this->showList( $result );
 585+ # List undeletable articles
 586+ if( $this->mSearchPrefix ) {
 587+ $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
 588+ $this->showList( $result );
 589+ }
 590+ } else {
 591+ $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
533592 }
534593 return;
535594 }
536595 if( $this->mTimestamp !== '' ) {
537596 return $this->showRevision( $this->mTimestamp );
538597 }
 598+
 599+ if( $this->mDiff && $this->mOldid )
 600+ return $this->showDiff( $this->mDiff, $this->mOldid );
 601+
539602 if( $this->mFile !== null ) {
540 - return $this->showFile( $this->mFile );
 603+ $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
 604+ // Check if user is allowed to see this file
 605+ if( !$file->userCan( File::DELETED_FILE ) ) {
 606+ $wgOut->permissionRequired( 'hiderevision' );
 607+ return false;
 608+ } else {
 609+ return $this->showFile( $this->mFile );
 610+ }
541611 }
 612+
542613 if( $this->mRestore && $this->mAction == "submit" ) {
543614 return $this->undelete();
544615 }
@@ -565,7 +636,8 @@
566637 '</form>' );
567638 }
568639
569 - /* private */ function showList( $result ) {
 640+ // Generic list of deleted pages
 641+ private function showList( $result ) {
570642 global $wgLang, $wgContLang, $wgUser, $wgOut;
571643
572644 if( $result->numRows() == 0 ) {
@@ -593,7 +665,7 @@
594666 return true;
595667 }
596668
597 - /* private */ function showRevision( $timestamp ) {
 669+ private function showRevision( $timestamp ) {
598670 global $wgLang, $wgUser, $wgOut;
599671 $self = SpecialPage::getTitleFor( 'Undelete' );
600672 $skin = $wgUser->getSkin();
@@ -604,14 +676,24 @@
605677 $rev = $archive->getRevision( $timestamp );
606678
607679 if( !$rev ) {
608 - $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) );
 680+ $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) );
609681 return;
610682 }
611683
 684+ if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
 685+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
 686+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
 687+ return;
 688+ } else {
 689+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
 690+ $wgOut->addHTML( '<br/>' );
 691+ // and we are allowed to see...
 692+ }
 693+ }
 694+
612695 $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
613696
614 - $link = $skin->makeKnownLinkObj(
615 - $self,
 697+ $link = $skin->makeKnownLinkObj( $self,
616698 htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
617699 'target=' . $this->mTargetObj->getPrefixedUrl()
618700 );
@@ -625,7 +707,7 @@
626708
627709 if( $this->mPreview ) {
628710 $wgOut->addHtml( "<hr />\n" );
629 - $wgOut->addWikiTextTitleTidy( $rev->getText(), $this->mTargetObj, false );
 711+ $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, false );
630712 }
631713
632714 $wgOut->addHtml(
@@ -633,7 +715,7 @@
634716 'readonly' => 'readonly',
635717 'cols' => intval( $wgUser->getOption( 'cols' ) ),
636718 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
637 - $rev->getText() . "\n" ) .
 719+ $rev->revText() . "\n" ) .
638720 wfOpenElement( 'div' ) .
639721 wfOpenElement( 'form', array(
640722 'method' => 'post',
@@ -660,11 +742,74 @@
661743 wfCloseElement( 'form' ) .
662744 wfCloseElement( 'div' ) );
663745 }
 746+
 747+ /**
 748+ * Show the changes between two deleted revisions
 749+ */
 750+ private function showDiff( $newid, $oldid ) {
 751+ global $wgOut, $wgUser, $wgLang;
664752
 753+ if( is_null($this->mTargetObj) )
 754+ return;
 755+ $skin = $wgUser->getSkin();
 756+
 757+ $archive = new PageArchive( $this->mTargetObj );
 758+ $oldRev = $archive->getRevision( null, $oldid );
 759+ $newRev = $archive->getRevision( null, $newid );
 760+
 761+ if( !$oldRev || !$newRev )
 762+ return;
 763+
 764+ $oldTitle = $this->mTargetObj->getPrefixedText();
 765+ $wgOut->addHtml( "<center><h3>$oldTitle</h3></center>" );
 766+
 767+ $oldminor = $newminor = '';
 768+
 769+ if($oldRev->mMinorEdit == 1) {
 770+ $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
 771+ wfMsg( 'minoreditletter') ) . ' ';
 772+ }
 773+
 774+ if($newRev->mMinorEdit == 1) {
 775+ $newminor = wfElement( 'span', array( 'class' => 'minor' ),
 776+ wfMsg( 'minoreditletter') ) . ' ';
 777+ }
 778+
 779+ $ot = $wgLang->timeanddate( $oldRev->getTimestamp(), true );
 780+ $nt = $wgLang->timeanddate( $newRev->getTimestamp(), true );
 781+ $oldHeader = htmlspecialchars( wfMsg( 'revisionasof', $ot ) ) . "<br />" .
 782+ $skin->revUserTools( $oldRev, true ) . "<br />" .
 783+ $oldminor . $skin->revComment( $oldRev, false ) . "<br />";
 784+ $newHeader = htmlspecialchars( wfMsg( 'revisionasof', $nt ) ) . "<br />" .
 785+ $skin->revUserTools( $newRev, true ) . " <br />" .
 786+ $newminor . $skin->revComment( $newRev, false ) . "<br />";
 787+
 788+ $otext = $oldRev->revText();
 789+ $ntext = $newRev->revText();
 790+
 791+ $wgOut->addStyle( 'common/diff.css' );
 792+ $wgOut->addHtml(
 793+ "<div>" .
 794+ "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
 795+ "<col class='diff-marker' />" .
 796+ "<col class='diff-content' />" .
 797+ "<col class='diff-marker' />" .
 798+ "<col class='diff-content' />" .
 799+ "<tr>" .
 800+ "<td colspan='2' width='50%' align='center' class='diff-otitle'>" . $oldHeader . "</td>" .
 801+ "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" . $newHeader . "</td>" .
 802+ "</tr>" .
 803+ DifferenceEngine::generateDiffBody( $otext, $ntext ) .
 804+ "</table>" .
 805+ "</div>\n" );
 806+
 807+ return true;
 808+ }
 809+
665810 /**
666811 * Show a deleted file version requested by the visitor.
667812 */
668 - function showFile( $key ) {
 813+ private function showFile( $key ) {
669814 global $wgOut, $wgRequest;
670815 $wgOut->disable();
671816
@@ -680,47 +825,35 @@
681826 $store->stream( $key );
682827 }
683828
684 - /* private */ function showHistory() {
 829+ private function showHistory() {
685830 global $wgLang, $wgContLang, $wgUser, $wgOut;
686831
687 - $sk = $wgUser->getSkin();
688 - if ( $this->mAllowed ) {
 832+ $this->sk = $wgUser->getSkin();
 833+ if( $this->mAllowed ) {
689834 $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
690835 } else {
691836 $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
692837 }
 838+
 839+ $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
693840
694841 $archive = new PageArchive( $this->mTargetObj );
695 - /*
696 - $text = $archive->getLastRevisionText();
697 - if( is_null( $text ) ) {
698 - $wgOut->addWikiText( wfMsg( "nohistory" ) );
699 - return;
700 - }
701 - */
702 - if ( $this->mAllowed ) {
703 - $wgOut->addWikiText( wfMsg( "undeletehistory" ) );
 842+
 843+ if( $this->mAllowed ) {
 844+ $wgOut->addWikiText( '<p>' . wfMsgHtml( "undeletehistory" ) . '</p>' );
 845+ $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
704846 } else {
705 - $wgOut->addWikiText( wfMsg( "undeletehistorynoadmin" ) );
 847+ $wgOut->addWikiText( wfMsgHtml( "undeletehistorynoadmin" ) );
706848 }
707849
708850 # List all stored revisions
709 - $revisions = $archive->listRevisions();
 851+ $revisions = new UndeleteRevisionsPager( $this, array(), $this->mTargetObj );
710852 $files = $archive->listFiles();
711853
712 - $haveRevisions = $revisions && $revisions->numRows() > 0;
 854+ $haveRevisions = $revisions && $revisions->getNumRows() > 0;
713855 $haveFiles = $files && $files->numRows() > 0;
714856
715857 # Batch existence check on user and talk pages
716 - if( $haveRevisions ) {
717 - $batch = new LinkBatch();
718 - while( $row = $revisions->fetchObject() ) {
719 - $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
720 - $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
721 - }
722 - $batch->execute();
723 - $revisions->seek( 0 );
724 - }
725858 if( $haveFiles ) {
726859 $batch = new LinkBatch();
727860 while( $row = $files->fetchObject() ) {
@@ -731,7 +864,7 @@
732865 $files->seek( 0 );
733866 }
734867
735 - if ( $this->mAllowed ) {
 868+ if( $this->mAllowed ) {
736869 $titleObj = SpecialPage::getTitleFor( "Undelete" );
737870 $action = $titleObj->getLocalURL( "action=submit" );
738871 # Start the form here
@@ -739,20 +872,6 @@
740873 $wgOut->addHtml( $top );
741874 }
742875
743 - # Show relevant lines from the deletion log:
744 - $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
745 - $logViewer = new LogViewer(
746 - new LogReader(
747 - new FauxRequest(
748 - array(
749 - 'page' => $this->mTargetObj->getPrefixedText(),
750 - 'type' => 'delete'
751 - )
752 - )
753 - ), LogViewer::NO_ACTION_LINK
754 - );
755 - $logViewer->showList( $wgOut );
756 -
757876 if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
758877 # Format the user-visible controls (comment field, submission button)
759878 # in a nice little table
@@ -778,6 +897,10 @@
779898 <td>" .
780899 Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
781900 Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
 901+ Xml::openElement( 'p' ) .
 902+ Xml::check( 'wpUnsuppress', $this->mUnsuppress, array('id' => 'mw-undelete-unsupress') ) . ' ' .
 903+ Xml::label( wfMsgHtml('revdelete-unsuppress'), 'mw-undelete-unsupress' ) .
 904+ Xml::closeElement( 'p' ) .
782905 "</td>
783906 </tr>" .
784907 Xml::closeElement( 'table' ) .
@@ -786,59 +909,38 @@
787910 $wgOut->addHtml( $table );
788911 }
789912
790 - $wgOut->addHTML( "<h2>" . htmlspecialchars( wfMsg( "history" ) ) . "</h2>\n" );
 913+ $wgOut->addHTML( "<h2 id=\"pagehistory\">" . wfMsgHtml( "history" ) . "</h2>\n" );
791914
792915 if( $haveRevisions ) {
793 - # The page's stored (deleted) history:
794 - $wgOut->addHTML("<ul>");
795 - $target = urlencode( $this->mTarget );
796 - while( $row = $revisions->fetchObject() ) {
797 - $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
798 - if ( $this->mAllowed ) {
799 - $checkBox = Xml::check( "ts$ts" );
800 - $pageLink = $sk->makeKnownLinkObj( $titleObj,
801 - $wgLang->timeanddate( $ts, true ),
802 - "target=$target&timestamp=$ts" );
803 - } else {
804 - $checkBox = '';
805 - $pageLink = $wgLang->timeanddate( $ts, true );
806 - }
807 - $userLink = $sk->userLink( $row->ar_user, $row->ar_user_text ) . $sk->userToolLinks( $row->ar_user, $row->ar_user_text );
808 - $stxt = '';
809 - if (!is_null($size = $row->ar_len)) {
810 - if ($size == 0) {
811 - $stxt = wfMsgHtml('historyempty');
812 - } else {
813 - $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
814 - }
815 - }
816 - $comment = $sk->commentBlock( $row->ar_comment );
817 - $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $stxt $comment</li>\n" );
818 -
819 - }
820 - $revisions->free();
821 - $wgOut->addHTML("</ul>");
 916+ $wgOut->addHTML( '<p>' . wfMsgHtml( "restorepoint" ) . '</p>' );
 917+ $wgOut->addHTML( $revisions->getNavigationBar() );
 918+ $wgOut->addHTML( "<ul>" );
 919+ $wgOut->addHTML( "<li>" . wfRadio( "restorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
 920+ $wgOut->addHTML( $revisions->getBody() );
 921+ $wgOut->addHTML( "</ul>" );
 922+ $wgOut->addHTML( $revisions->getNavigationBar() );
822923 } else {
823924 $wgOut->addWikiText( wfMsg( "nohistory" ) );
824925 }
825 -
826926 if( $haveFiles ) {
827 - $wgOut->addHtml( "<h2>" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
 927+ $wgOut->addHtml( "<h2 id=\"filehistory\">" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
 928+ $wgOut->addHTML( wfMsgHtml( "restorepoint" ) );
828929 $wgOut->addHtml( "<ul>" );
 930+ $wgOut->addHTML( "<li>" . wfRadio( "imgrestorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
829931 while( $row = $files->fetchObject() ) {
 932+ $file = ArchivedFile::newFromRow( $row );
 933+
830934 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
831 - if ( $this->mAllowed && $row->fa_storage_key ) {
832 - $checkBox = Xml::check( "fileid" . $row->fa_id );
 935+ if( $this->mAllowed && $row->fa_storage_key ) {
 936+ $checkBox = wfRadio( "imgrestorepoint", $ts, false );
833937 $key = urlencode( $row->fa_storage_key );
834938 $target = urlencode( $this->mTarget );
835 - $pageLink = $sk->makeKnownLinkObj( $titleObj,
836 - $wgLang->timeanddate( $ts, true ),
837 - "target=$target&file=$key" );
 939+ $pageLink = $this->getFileLink( $file, $titleObj, $ts, $target, $key );
838940 } else {
839941 $checkBox = '';
840942 $pageLink = $wgLang->timeanddate( $ts, true );
841943 }
842 - $userLink = $sk->userLink( $row->fa_user, $row->fa_user_text ) . $sk->userToolLinks( $row->fa_user, $row->fa_user_text );
 944+ $userLink = $this->getFileUser( $file );
843945 $data =
844946 wfMsgHtml( 'widthheight',
845947 $wgLang->formatNum( $row->fa_width ),
@@ -846,14 +948,40 @@
847949 ' (' .
848950 wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
849951 ')';
850 - $comment = $sk->commentBlock( $row->fa_description );
851 - $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $data $comment</li>\n" );
 952+ $comment = $this->getFileComment( $file );
 953+ $rd='';
 954+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
 955+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 956+ if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
 957+ // If revision was hidden from sysops
 958+ $del = $this->message['rev-delundel'];
 959+ } else {
 960+ $del = $this->sk->makeKnownLinkObj( $revdel,
 961+ $this->message['rev-delundel'],
 962+ 'target=' . urlencode( $this->mTarget ) .
 963+ '&fileid=' . urlencode( $row->fa_id ) );
 964+ // Bolden oversighted content
 965+ if( $file->isDeleted( File::DELETED_RESTRICTED ) )
 966+ $del = "<strong>$del</strong>";
 967+ }
 968+ $rd = "<tt>(<small>$del</small>)</tt>";
 969+ }
 970+ $wgOut->addHTML( "<li>$checkBox $rd $pageLink . . $userLink $data $comment</li>\n" );
852971 }
853972 $files->free();
854973 $wgOut->addHTML( "</ul>" );
855974 }
856975
857 - if ( $this->mAllowed ) {
 976+ # Show relevant lines from the deletion log:
 977+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
 978+ $logViewer = new LogViewer(
 979+ new LogReader(
 980+ new FauxRequest(
 981+ array( 'page' => $this->mTargetObj->getPrefixedText(),
 982+ 'type' => 'delete' ) ) ) );
 983+ $logViewer->showList( $wgOut );
 984+
 985+ if( $this->mAllowed ) {
858986 # Slip in the hidden controls here
859987 $misc = Xml::hidden( 'target', $this->mTarget );
860988 $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
@@ -863,23 +991,152 @@
864992
865993 return true;
866994 }
 995+
 996+ function formatRevisionRow( $row ) {
 997+ global $wgUser, $wgLang;
 998+
 999+ $rev = new Revision( array(
 1000+ 'page' => $this->mTargetObj->getArticleId(),
 1001+ 'id' => $row->ar_rev_id,
 1002+ 'comment' => $row->ar_comment,
 1003+ 'user' => $row->ar_user,
 1004+ 'user_text' => $row->ar_user_text,
 1005+ 'timestamp' => $row->ar_timestamp,
 1006+ 'minor_edit' => $row->ar_minor_edit,
 1007+ 'text_id' => $row->ar_text_id,
 1008+ 'deleted' => $row->ar_deleted,
 1009+ 'len' => $row->ar_len) );
 1010+
 1011+ $stxt = '';
 1012+ $last = $this->message['last'];
 1013+
 1014+ if( $this->mAllowed ) {
 1015+ $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
 1016+ $checkBox = wfRadio( "restorepoint", $ts, false );
 1017+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
 1018+ $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $this->mTarget );
 1019+ # Last link
 1020+ if( !$rev->userCan( Revision::DELETED_TEXT ) )
 1021+ $last = $this->message['last'];
 1022+ else if( isset($this->prevId[$row->ar_rev_id]) )
 1023+ $last = $this->sk->makeKnownLinkObj( $titleObj, $this->message['last'], "target=" . $this->mTarget .
 1024+ "&diff=" . $row->ar_rev_id . "&oldid=" . $this->prevId[$row->ar_rev_id] );
 1025+ } else {
 1026+ $checkBox = '';
 1027+ $pageLink = $wgLang->timeanddate( $ts, true );
 1028+ }
 1029+ $userLink = $this->sk->revUserTools( $rev );
 1030+
 1031+ if(!is_null($size = $row->ar_len)) {
 1032+ if($size == 0)
 1033+ $stxt = wfMsgHtml('historyempty');
 1034+ else
 1035+ $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
 1036+ }
 1037+ $comment = $this->sk->revComment( $rev );
 1038+ $revd='';
 1039+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
 1040+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 1041+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
 1042+ // If revision was hidden from sysops
 1043+ $del = $this->message['rev-delundel'];
 1044+ } else {
 1045+ $del = $this->sk->makeKnownLinkObj( $revdel,
 1046+ $this->message['rev-delundel'],
 1047+ 'target=' . urlencode( $this->mTarget ) .
 1048+ '&artimestamp=' . urlencode( $row->ar_timestamp ) );
 1049+ // Bolden oversighted content
 1050+ if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
 1051+ $del = "<strong>$del</strong>";
 1052+ }
 1053+ $revd = "<tt>(<small>$del</small>)</tt>";
 1054+ }
 1055+
 1056+ return "<li>$checkBox $revd ($last) $pageLink . . $userLink $stxt $comment</li>";
 1057+ }
8671058
 1059+ /**
 1060+ * Fetch revision text link if it's available to all users
 1061+ * @return string
 1062+ */
 1063+ function getPageLink( $rev, $titleObj, $ts, $target ) {
 1064+ global $wgLang;
 1065+
 1066+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
 1067+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
 1068+ } else {
 1069+ $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
 1070+ if( $rev->isDeleted(Revision::DELETED_TEXT) )
 1071+ $link = '<span class="history-deleted">' . $link . '</span>';
 1072+ return $link;
 1073+ }
 1074+ }
 1075+
 1076+ /**
 1077+ * Fetch image view link if it's available to all users
 1078+ * @return string
 1079+ */
 1080+ function getFileLink( $file, $titleObj, $ts, $target, $key ) {
 1081+ global $wgLang;
 1082+
 1083+ if( !$file->userCan(File::DELETED_FILE) ) {
 1084+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
 1085+ } else {
 1086+ $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&file=$key" );
 1087+ if( $file->isDeleted(File::DELETED_FILE) )
 1088+ $link = '<span class="history-deleted">' . $link . '</span>';
 1089+ return $link;
 1090+ }
 1091+ }
 1092+
 1093+ /**
 1094+ * Fetch file's user id if it's available to this user
 1095+ * @return string
 1096+ */
 1097+ function getFileUser( $file ) {
 1098+ if( !$file->userCan(File::DELETED_USER) ) {
 1099+ return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
 1100+ } else {
 1101+ $link = $this->sk->userLink( $file->user, $file->userText ) .
 1102+ $this->sk->userToolLinks( $file->user, $file->userText );
 1103+ if( $file->isDeleted(File::DELETED_USER) )
 1104+ $link = '<span class="history-deleted">' . $link . '</span>';
 1105+ return $link;
 1106+ }
 1107+ }
 1108+
 1109+ /**
 1110+ * Fetch file upload comment if it's available to this user
 1111+ * @return string
 1112+ */
 1113+ function getFileComment( $file ) {
 1114+ if( !$file->userCan(File::DELETED_COMMENT) ) {
 1115+ return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
 1116+ } else {
 1117+ $link = $this->sk->commentBlock( $file->description );
 1118+ if( $file->isDeleted(File::DELETED_COMMENT) )
 1119+ $link = '<span class="history-deleted">' . $link . '</span>';
 1120+ return $link;
 1121+ }
 1122+ }
 1123+
8681124 function undelete() {
8691125 global $wgOut, $wgUser;
8701126 if( !is_null( $this->mTargetObj ) ) {
8711127 $archive = new PageArchive( $this->mTargetObj );
8721128
8731129 $ok = $archive->undelete(
874 - $this->mTargetTimestamp,
 1130+ $this->mPageTimestamp,
8751131 $this->mComment,
876 - $this->mFileVersions );
877 -
 1132+ $this->mFileTimestamp,
 1133+ $this->mUnsuppress );
8781134 if( $ok ) {
8791135 $skin = $wgUser->getSkin();
880 - $link = $skin->makeKnownLinkObj( $this->mTargetObj );
 1136+ $link = $skin->makeKnownLinkObj( $this->mTargetObj, $this->mTargetObj->getPrefixedText(), 'redirect=no' );
8811137 $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
8821138 } else {
8831139 $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
 1140+ $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
8841141 }
8851142
8861143 // Show file deletion warnings and errors
@@ -894,4 +1151,61 @@
8951152 }
8961153 }
8971154
 1155+class UndeleteRevisionsPager extends ReverseChronologicalPager {
 1156+ public $mForm, $mConds;
8981157
 1158+ function __construct( $form, $conds = array(), $title ) {
 1159+ $this->mForm = $form;
 1160+ $this->mConds = $conds;
 1161+ $this->title = $title;
 1162+ parent::__construct();
 1163+ }
 1164+
 1165+ function getStartBody() {
 1166+ wfProfileIn( __METHOD__ );
 1167+ # Do a link batch query
 1168+ $this->mResult->seek( 0 );
 1169+ $batch = new LinkBatch();
 1170+ # Give some pointers to make (last) links
 1171+ $this->mForm->prevId = array();
 1172+ while( $row = $this->mResult->fetchObject() ) {
 1173+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
 1174+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
 1175+
 1176+ $rev_id = isset($rev_id) ? $rev_id : $row->ar_rev_id;
 1177+ if( $rev_id > $row->ar_rev_id )
 1178+ $this->mForm->prevId[$rev_id] = $row->ar_rev_id;
 1179+ else if( $rev_id < $row->ar_rev_id )
 1180+ $this->mForm->prevId[$row->ar_rev_id] = $rev_id;
 1181+
 1182+ $rev_id = $row->ar_rev_id;
 1183+ }
 1184+
 1185+ $batch->execute();
 1186+ $this->mResult->seek( 0 );
 1187+
 1188+ wfProfileOut( __METHOD__ );
 1189+ return '';
 1190+ }
 1191+
 1192+ function formatRow( $row ) {
 1193+ $block = new Block;
 1194+ return $this->mForm->formatRevisionRow( $row );
 1195+ }
 1196+
 1197+ function getQueryInfo() {
 1198+ $conds = $this->mConds;
 1199+ $conds['ar_namespace'] = $this->title->getNamespace();
 1200+ $conds['ar_title'] = $this->title->getDBkey();
 1201+ return array(
 1202+ 'tables' => array('archive'),
 1203+ 'fields' => array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment',
 1204+ 'ar_rev_id', 'ar_text_id', 'ar_len', 'ar_deleted' ),
 1205+ 'conds' => $conds
 1206+ );
 1207+ }
 1208+
 1209+ function getIndexField() {
 1210+ return 'ar_timestamp';
 1211+ }
 1212+}
Index: trunk/phase3/includes/Title.php
@@ -2445,6 +2445,38 @@
24462446 }
24472447
24482448 /**
 2449+ * Checks if the deleted history of another page can be merged into the same title as $this
 2450+ * - Selects for update, so don't call it unless you mean business
 2451+ */
 2452+ public function isValidRestoreOverTarget() {
 2453+
 2454+ $fname = 'Title::isValidRestoreOverTarget';
 2455+ $dbw = wfGetDB( DB_MASTER );
 2456+
 2457+ # Is it a redirect?
 2458+ $page_is_redirect = $dbw->selectField( 'page', 'page_is_redirect',
 2459+ array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ),
 2460+ $fname, 'FOR UPDATE' );
 2461+
 2462+ if ( !$page_is_redirect ) {
 2463+ # Not a redirect
 2464+ wfDebug( __METHOD__ . ": not a redirect\n" );
 2465+ return false;
 2466+ }
 2467+
 2468+ # Does the article have a history?
 2469+ $row = $dbw->selectRow( array('page','revision'),
 2470+ array( 'rev_id' ),
 2471+ array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform,
 2472+ 'page_id=rev_page AND page_latest != rev_id'
 2473+ ), $fname, 'FOR UPDATE'
 2474+ );
 2475+
 2476+ # Return true if there was no history
 2477+ return $row === false;
 2478+ }
 2479+
 2480+ /**
24492481 * Can this title be added to a user's watchlist?
24502482 *
24512483 * @return bool
Index: trunk/phase3/includes/RecentChange.php
@@ -25,6 +25,11 @@
2626 * rc_patrolled boolean whether or not someone has marked this edit as patrolled
2727 * rc_old_len integer byte length of the text before the edit
2828 * rc_new_len the same after the edit
 29+ * rc_deleted partial deletion
 30+ * rc_logid the log_id value for this log entry (or zero)
 31+ * rc_log_type the log type (or null)
 32+ * rc_log_action the log action (or null)
 33+ * rc_params log params
2934 *
3035 * mExtra:
3136 * prefixedDBkey prefixed db key, used by external app via msg queue
@@ -295,7 +300,12 @@
296301 'rc_patrolled' => 0,
297302 'rc_new' => 0, # obsolete
298303 'rc_old_len' => $oldSize,
299 - 'rc_new_len' => $newSize
 304+ 'rc_new_len' => $newSize,
 305+ 'rc_deleted' => 0,
 306+ 'rc_logid' => 0,
 307+ 'rc_log_type' => null,
 308+ 'rc_log_action' => '',
 309+ 'rc_params' => ''
300310 );
301311
302312 $rc->mExtra = array(
@@ -316,11 +326,9 @@
317327 public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default',
318328 $ip='', $size = 0, $newId = 0 )
319329 {
320 - if ( !$ip ) {
 330+ if( !$ip ) {
321331 $ip = wfGetIP();
322 - if ( !$ip ) {
323 - $ip = '';
324 - }
 332+ if( !$ip ) $ip = '';
325333 }
326334 if ( $bot === 'default' ) {
327335 $bot = $user->isAllowed( 'bot' );
@@ -345,9 +353,14 @@
346354 'rc_moved_to_title' => '',
347355 'rc_ip' => $ip,
348356 'rc_patrolled' => 0,
349 - 'rc_new' => 1, # obsolete
 357+ 'rc_new' => 1, # obsolete
350358 'rc_old_len' => 0,
351 - 'rc_new_len' => $size
 359+ 'rc_new_len' => $size,
 360+ 'rc_deleted' => 0,
 361+ 'rc_logid' => 0,
 362+ 'rc_log_type' => null,
 363+ 'rc_log_action' => '',
 364+ 'rc_params' => ''
352365 );
353366
354367 $rc->mExtra = array(
@@ -363,11 +376,9 @@
364377 # Makes an entry in the database corresponding to a rename
365378 public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
366379 {
367 - if ( !$ip ) {
 380+ if( !$ip ) {
368381 $ip = wfGetIP();
369 - if ( !$ip ) {
370 - $ip = '';
371 - }
 382+ if( !$ip ) $ip = '';
372383 }
373384
374385 $rc = new RecentChange;
@@ -392,6 +403,11 @@
393404 'rc_patrolled' => 1,
394405 'rc_old_len' => NULL,
395406 'rc_new_len' => NULL,
 407+ 'rc_deleted' => 0,
 408+ 'rc_logid' => 0, # notifyMove not used anymore
 409+ 'rc_log_type' => null,
 410+ 'rc_log_action' => '',
 411+ 'rc_params' => ''
396412 );
397413
398414 $rc->mExtra = array(
@@ -410,30 +426,27 @@
411427 RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
412428 }
413429
414 - # A log entry is different to an edit in that previous revisions are
415 - # not kept
 430+ # A log entry is different to an edit in that previous revisions are not kept
416431 public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
417 - $type, $action, $target, $logComment, $params )
 432+ $type, $action, $target, $logComment, $params, $newId=0 )
418433 {
419 - if ( !$ip ) {
 434+ if( !$ip ) {
420435 $ip = wfGetIP();
421 - if ( !$ip ) {
422 - $ip = '';
423 - }
 436+ if( !$ip ) $ip = '';
424437 }
425438
426439 $rc = new RecentChange;
427440 $rc->mAttribs = array(
428441 'rc_timestamp' => $timestamp,
429442 'rc_cur_time' => $timestamp,
430 - 'rc_namespace' => $title->getNamespace(),
431 - 'rc_title' => $title->getDBkey(),
 443+ 'rc_namespace' => $target->getNamespace(),
 444+ 'rc_title' => $target->getDBkey(),
432445 'rc_type' => RC_LOG,
433446 'rc_minor' => 0,
434 - 'rc_cur_id' => $title->getArticleID(),
 447+ 'rc_cur_id' => $target->getArticleID(),
435448 'rc_user' => $user->getID(),
436449 'rc_user_text' => $user->getName(),
437 - 'rc_comment' => $comment,
 450+ 'rc_comment' => $logComment,
438451 'rc_this_oldid' => 0,
439452 'rc_last_oldid' => 0,
440453 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0,
@@ -444,6 +457,11 @@
445458 'rc_new' => 0, # obsolete
446459 'rc_old_len' => NULL,
447460 'rc_new_len' => NULL,
 461+ 'rc_deleted' => 0,
 462+ 'rc_logid' => $newId,
 463+ 'rc_log_type' => $type,
 464+ 'rc_log_action' => $action,
 465+ 'rc_params' => $params
448466 );
449467 $rc->mExtra = array(
450468 'prefixedDBkey' => $title->getPrefixedDBkey(),
@@ -490,6 +508,11 @@
491509 'rc_new' => $row->page_is_new, # obsolete
492510 'rc_old_len' => $row->rc_old_len,
493511 'rc_new_len' => $row->rc_new_len,
 512+ 'rc_deleted' => $row->rc_deleted,
 513+ 'rc_logid' => $row->rc_logid,
 514+ 'rc_log_type' => $row->rc_log_type,
 515+ 'rc_log_action' => $row->rc_log_action,
 516+ 'rc_params' => $row->rc_params
494517 );
495518
496519 $this->mExtra = array();
Index: trunk/phase3/includes/SpecialSpecialpages.php
@@ -16,10 +16,13 @@
1717 $sk = $wgUser->getSkin();
1818
1919 /** Pages available to all */
20 - wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk );
 20+ wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk, false );
2121
2222 /** Restricted special pages */
23 - wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk );
 23+ wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk, false );
 24+
 25+ /** Restricted logs */
 26+ wfSpecialSpecialpages_gen( SpecialPage::getRestrictedLogs(), 'restrictedlheading', $sk, true );
2427 }
2528
2629 /**
@@ -27,9 +30,10 @@
2831 * @param $pages the list of pages
2932 * @param $heading header to be used
3033 * @param $sk skin object ???
 34+ * @param $islog, is this for a list of log types?
3135 */
32 -function wfSpecialSpecialpages_gen($pages,$heading,$sk) {
33 - global $wgOut, $wgSortSpecialPages;
 36+function wfSpecialSpecialpages_gen( $pages, $heading, $sk, $islog=false ) {
 37+ global $wgOut, $wgUser, $wgSortSpecialPages;
3438
3539 if( count( $pages ) == 0 ) {
3640 # Yeah, that was pointless. Thanks for coming.
@@ -38,9 +42,13 @@
3943
4044 /** Put them into a sortable array */
4145 $sortedPages = array();
42 - foreach ( $pages as $page ) {
43 - if ( $page->isListed() ) {
44 - $sortedPages[$page->getDescription()] = $page->getTitle();
 46+ if( $islog ) {
 47+ $sortedPages = $pages;
 48+ } else {
 49+ foreach ( $pages as $page ) {
 50+ if ( $page->isListed() ) {
 51+ $sortedPages[$page->getDescription()] = $page->getTitle();
 52+ }
4553 }
4654 }
4755
Index: trunk/phase3/includes/DefaultSettings.php
@@ -170,10 +170,16 @@
171171 * $wgFileStore['deleted']['directory'] = '/var/wiki/private/deleted';
172172 *
173173 */
 174+// For deleted images, gererally were all versions of the image are discarded
174175 $wgFileStore = array();
175176 $wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
176177 $wgFileStore['deleted']['url'] = null; // Private
177178 $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
 179+// For revisions of images marked as hidden
 180+// These are kept even if $wgSaveDeletedFiles is set to false
 181+$wgFileStore['hidden']['directory'] = false;// Defaults to $wgUploadDirectory/hidden
 182+$wgFileStore['hidden']['url'] = null; // Private
 183+$wgFileStore['hidden']['hash'] = 3; // 3-level subdirectory split
178184
179185 /**#@+
180186 * File repository structures
@@ -1060,6 +1066,7 @@
10611067 $wgGroupPermissions['sysop']['block'] = true;
10621068 $wgGroupPermissions['sysop']['createaccount'] = true;
10631069 $wgGroupPermissions['sysop']['delete'] = true;
 1070+$wgGroupPermissions['sysop']['browsearchive'] = true; // can see the deleted page list
10641071 $wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text
10651072 $wgGroupPermissions['sysop']['editinterface'] = true;
10661073 $wgGroupPermissions['sysop']['editusercssjs'] = true;
@@ -1079,15 +1086,22 @@
10801087 $wgGroupPermissions['sysop']['autoconfirmed'] = true;
10811088 $wgGroupPermissions['sysop']['upload_by_url'] = true;
10821089 $wgGroupPermissions['sysop']['ipblock-exempt'] = true;
 1090+$wgGroupPermissions['sysop']['deleterevision'] = true;
10831091 $wgGroupPermissions['sysop']['blockemail'] = true;
 1092+$wgGroupPermissions['sysop']['mergehistory'] = true;
10841093
10851094 // Permission to change users' group assignments
10861095 $wgGroupPermissions['bureaucrat']['userrights'] = true;
10871096
1088 -// Experimental permissions, not ready for production use
1089 -//$wgGroupPermissions['sysop']['deleterevision'] = true;
1090 -//$wgGroupPermissions['bureaucrat']['hiderevision'] = true;
 1097+// To hide usernames
 1098+$wgGroupPermissions['oversight']['hideuser'] = true;
 1099+// To see hidden revs and unhide revs hidden from Sysops
 1100+$wgGroupPermissions['oversight']['hiderevision'] = true;
 1101+// For private log access
 1102+$wgGroupPermissions['oversight']['oversight'] = true;
10911103
 1104+$wgAllowLogDeletion = false;
 1105+
10921106 /**
10931107 * The developer group is deprecated, but can be activated if need be
10941108 * to use the 'lockdb' and 'unlockdb' special pages. Those require
@@ -2243,9 +2257,21 @@
22442258 'move',
22452259 'import',
22462260 'patrol',
 2261+ 'merge',
 2262+ 'oversight',
22472263 );
22482264
22492265 /**
 2266+ * This restricts log access to those who have a certain right
 2267+ * Users without this will not see it in the option menu and can not view it
 2268+ * Restricted logs are not added to recent changes
 2269+ * Logs should remain non-transcludable
 2270+ */
 2271+$wgLogRestrictions = array(
 2272+ 'oversight' => 'oversight'
 2273+);
 2274+
 2275+/**
22502276 * Lists the message key string for each log type. The localized messages
22512277 * will be listed in the user interface.
22522278 *
@@ -2261,6 +2287,8 @@
22622288 'move' => 'movelogpage',
22632289 'import' => 'importlogpage',
22642290 'patrol' => 'patrol-log-page',
 2291+ 'merge' => 'mergelog',
 2292+ 'oversight' => 'oversightlog',
22652293 );
22662294
22672295 /**
@@ -2279,6 +2307,8 @@
22802308 'move' => 'movelogpagetext',
22812309 'import' => 'importlogpagetext',
22822310 'patrol' => 'patrol-log-header',
 2311+ 'merge' => 'mergelogpagetext',
 2312+ 'oversight' => 'overlogpagetext',
22832313 );
22842314
22852315 /**
@@ -2288,22 +2318,29 @@
22892319 * Extensions with custom log types may add to this array.
22902320 */
22912321 $wgLogActions = array(
2292 - 'block/block' => 'blocklogentry',
2293 - 'block/unblock' => 'unblocklogentry',
2294 - 'protect/protect' => 'protectedarticle',
2295 - 'protect/modify' => 'modifiedarticleprotection',
2296 - 'protect/unprotect' => 'unprotectedarticle',
2297 - 'rights/rights' => 'rightslogentry',
2298 - 'delete/delete' => 'deletedarticle',
2299 - 'delete/restore' => 'undeletedarticle',
2300 - 'delete/revision' => 'revdelete-logentry',
2301 - 'upload/upload' => 'uploadedimage',
2302 - 'upload/overwrite' => 'overwroteimage',
2303 - 'upload/revert' => 'uploadedimage',
2304 - 'move/move' => '1movedto2',
2305 - 'move/move_redir' => '1movedto2_redir',
2306 - 'import/upload' => 'import-logentry-upload',
2307 - 'import/interwiki' => 'import-logentry-interwiki',
 2322+ 'block/block' => 'blocklogentry',
 2323+ 'block/unblock' => 'unblocklogentry',
 2324+ 'protect/protect' => 'protectedarticle',
 2325+ 'protect/modify' => 'modifiedarticleprotection',
 2326+ 'protect/unprotect' => 'unprotectedarticle',
 2327+ 'rights/rights' => 'rightslogentry',
 2328+ 'delete/delete' => 'deletedarticle',
 2329+ 'delete/restore' => 'undeletedarticle',
 2330+ 'delete/revision' => 'revdelete-logentry',
 2331+ 'delete/event' => 'logdelete-logentry',
 2332+ 'upload/upload' => 'uploadedimage',
 2333+ 'upload/overwrite' => 'overwroteimage',
 2334+ 'upload/revert' => 'uploadedimage',
 2335+ 'move/move' => '1movedto2',
 2336+ 'move/move_redir' => '1movedto2_redir',
 2337+ 'import/upload' => 'import-logentry-upload',
 2338+ 'import/interwiki' => 'import-logentry-interwiki',
 2339+ 'merge/merge' => 'pagemerge-logentry',
 2340+ 'oversight/revision' => 'revdelete-logentry',
 2341+ 'oversight/file' => 'revdelete-logentry',
 2342+ 'oversight/event' => 'logdelete-logentry',
 2343+ 'oversight/delete' => 'suppressedarticle',
 2344+ 'oversight/block' => 'blocklogentry',
23082345 );
23092346
23102347 /**
Index: trunk/phase3/includes/SpecialRecentchanges.php
@@ -408,7 +408,7 @@
409409 rcFormatDiff( $obj ),
410410 $title->getFullURL(),
411411 $obj->rc_timestamp,
412 - $obj->rc_user_text,
 412+ ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text,
413413 $talkpage->getFullURL()
414414 );
415415 $feed->outItem( $item );
@@ -617,15 +617,18 @@
618618 return rcFormatDiffRow( $titleObj,
619619 $row->rc_last_oldid, $row->rc_this_oldid,
620620 $timestamp,
621 - $row->rc_comment );
 621+ ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment,
 622+ ($row->rc_deleted & LogViewer::DELETED_ACTION) ? wfMsgHtml('rev-deleted-event') : $row->rc_actiontext );
622623 }
623624
624 -function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) {
 625+function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
625626 global $wgFeedDiffCutoff, $wgContLang, $wgUser;
626627 $fname = 'rcFormatDiff';
627628 wfProfileIn( $fname );
628629
629630 $skin = $wgUser->getSkin();
 631+ # log enties
 632+ if( $actiontext ) $comment = "$actiontext $comment";
630633 $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
631634
632635 //NOTE: Check permissions for anonymous users, not current user.
Index: trunk/phase3/includes/Linker.php
@@ -442,6 +442,7 @@
443443 * @param boolean $thumb shows image as thumbnail in a frame
444444 * @param string $manualthumb image name for the manual thumbnail
445445 * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
 446+ * @param string $time, timestamp (for image versioning)
446447 * @return string
447448 */
448449 function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
@@ -871,10 +872,13 @@
872873 /**
873874 * Generate a user link if the current user is allowed to view it
874875 * @param $rev Revision object.
 876+ * @param $isPublic, bool, show only if all users can see it
875877 * @return string HTML
876878 */
877 - function revUserLink( $rev ) {
878 - if( $rev->userCan( Revision::DELETED_USER ) ) {
 879+ function revUserLink( $rev, $isPublic = false ) {
 880+ if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
 881+ $link = wfMsgHtml( 'rev-deleted-user' );
 882+ } else if( $rev->userCan( Revision::DELETED_USER ) ) {
879883 $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
880884 } else {
881885 $link = wfMsgHtml( 'rev-deleted-user' );
@@ -884,21 +888,74 @@
885889 }
886890 return $link;
887891 }
 892+
 893+ /**
 894+ * Generate a user link if the current user is allowed to view it
 895+ * @param $event, log row item.
 896+ * @param $isPublic, bool, show only if all users can see it
 897+ * @return string HTML
 898+ */
 899+ function logUserLink( $event, $isPublic = false ) {
 900+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) && $isPublic ) {
 901+ $link = wfMsgHtml( 'rev-deleted-user' );
 902+ } else if( LogViewer::userCan( $event, LogViewer::DELETED_USER ) ) {
 903+ if ( isset($event->user_name) ) {
 904+ $link = $this->userLink( $event->log_user, $event->user_name );
 905+ } else {
 906+ $user = $event->log_user;
 907+ $link = $this->userLink( $event->log_user, User::whoIs( $user ) );
 908+ }
 909+ } else {
 910+ $link = wfMsgHtml( 'rev-deleted-user' );
 911+ }
 912+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) ) {
 913+ return '<span class="history-deleted">' . $link . '</span>';
 914+ }
 915+ return $link;
 916+ }
888917
889918 /**
890919 * Generate a user tool link cluster if the current user is allowed to view it
891920 * @param $rev Revision object.
 921+ * @param $isPublic, bool, show only if all users can see it
892922 * @return string HTML
893923 */
894 - function revUserTools( $rev ) {
895 - if( $rev->userCan( Revision::DELETED_USER ) ) {
 924+ function revUserTools( $rev, $isPublic = false ) {
 925+ if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
 926+ $link = wfMsgHtml( 'rev-deleted-user' );
 927+ } else if( $rev->userCan( Revision::DELETED_USER ) ) {
896928 $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
897 - ' ' .
898 - $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
 929+ ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
899930 } else {
900931 $link = wfMsgHtml( 'rev-deleted-user' );
901932 }
902933 if( $rev->isDeleted( Revision::DELETED_USER ) ) {
 934+ return ' <span class="history-deleted">' . $link . '</span>';
 935+ }
 936+ return " $link";
 937+ }
 938+
 939+ /**
 940+ * Generate a user tool link cluster if the current user is allowed to view it
 941+ * @param $event, log item.
 942+ * @param $isPublic, bool, show only if all users can see it
 943+ * @return string HTML
 944+ */
 945+ function logUserTools( $event, $isPublic = false ) {
 946+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) && $isPublic ) {
 947+ $link = wfMsgHtml( 'rev-deleted-user' );
 948+ } else if( LogViewer::userCan( $event, LogViewer::DELETED_USER ) ) {
 949+ if( isset($event->user_name) ) {
 950+ $link = $this->userLink( $event->log_user, $event->user_name ) .
 951+ $this->userToolLinksRedContribs( $event->log_user, $event->user_name );
 952+ } else {
 953+ $link = $this->userLink( $event->log_user, $event->user_name ) .
 954+ $this->userToolLinksRedContribs( $event->log_user, User::whoIs($event->log_user) );
 955+ }
 956+ } else {
 957+ $link = wfMsgHtml( 'rev-deleted-user' );
 958+ }
 959+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) ) {
903960 return '<span class="history-deleted">' . $link . '</span>';
904961 }
905962 return $link;
@@ -1056,20 +1113,43 @@
10571114 *
10581115 * @param Revision $rev
10591116 * @param bool $local Whether section links should refer to local page
 1117+ * @param $isPublic, show only if all users can see it
10601118 * @return string HTML
10611119 */
1062 - function revComment( Revision $rev, $local = false ) {
1063 - if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
 1120+ function revComment( Revision $rev, $local = false, $isPublic = false ) {
 1121+ if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
 1122+ $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
 1123+ } else if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
10641124 $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local );
10651125 } else {
1066 - $block = " <span class=\"comment\">" .
1067 - wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
 1126+ $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
10681127 }
10691128 if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
10701129 return " <span class=\"history-deleted\">$block</span>";
10711130 }
10721131 return $block;
10731132 }
 1133+
 1134+ /**
 1135+ * Wrap and format the given event's comment block, if the current
 1136+ * user is allowed to view it.
 1137+ *
 1138+ * @param Revision $rev
 1139+ * @return string HTML
 1140+ */
 1141+ function logComment( $event, $isPublic = false ) {
 1142+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_COMMENT ) && $isPublic ) {
 1143+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
 1144+ } else if( LogViewer::userCan( $event, LogViewer::DELETED_COMMENT ) ) {
 1145+ $block = $this->commentBlock( LogViewer::getRawComment( $event ) );
 1146+ } else {
 1147+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
 1148+ }
 1149+ if( LogViewer::isDeleted( $event, LogViewer::DELETED_COMMENT ) ) {
 1150+ return "<span class=\"history-deleted\">$block</span>";
 1151+ }
 1152+ return $block;
 1153+ }
10741154
10751155 /** @todo document */
10761156 function tocIndent() {
Index: trunk/phase3/includes/PageHistory.php
@@ -37,7 +37,21 @@
3838 $this->mTitle =& $article->mTitle;
3939 $this->mNotificationTimestamp = NULL;
4040 $this->mSkin = $wgUser->getSkin();
 41+ $this->preCacheMessages();
4142 }
 43+
 44+ /**
 45+ * As we use the same small set of messages in various methods and that
 46+ * they are called often, we call them once and save them in $this->message
 47+ */
 48+ function preCacheMessages() {
 49+ // Precache various messages
 50+ if( !isset( $this->message ) ) {
 51+ foreach( explode(' ', 'cur last rev-delundel' ) as $msg ) {
 52+ $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
 53+ }
 54+ }
 55+ }
4256
4357 /**
4458 * Print the history page for an article.
@@ -189,35 +203,31 @@
190204 $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
191205 $link = $this->revLink( $rev );
192206
193 - $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() )
194 - . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() );
195 -
196207 $s .= "($curlink) ($lastlink) $arbitrary";
197208
198209 if( $wgUser->isAllowed( 'deleterevision' ) ) {
199210 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
200211 if( $firstInList ) {
201 - // We don't currently handle well changing the top revision's settings
202 - $del = wfMsgHtml( 'rev-delundel' );
 212+ // We don't currently handle well changing the top revision's settings
 213+ $del = $this->message['rev-delundel'];
203214 } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
204215 // If revision was hidden from sysops
205 - $del = wfMsgHtml( 'rev-delundel' );
 216+ $del = $this->message['rev-delundel'];
206217 } else {
207218 $del = $this->mSkin->makeKnownLinkObj( $revdel,
208 - wfMsg( 'rev-delundel' ),
 219+ $this->message['rev-delundel'],
209220 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
210221 '&oldid=' . urlencode( $rev->getId() ) );
 222+ // Bolden oversighted content
 223+ if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
 224+ $del = "<strong>$del</strong>";
211225 }
212 - $s .= " (<small>$del</small>) ";
 226+ $s .= " <tt>(<small>$del</small>)</tt> ";
213227 }
214228
215229 $s .= " $link";
216 - #getUser is safe, but this avoids making the invalid untargeted contribs links
217 - if( $row->rev_deleted & Revision::DELETED_USER ) {
218 - $user = '<span class="history-deleted">' . wfMsg('rev-deleted-user') . '</span>';
219 - }
220 - $s .= " <span class='history-user'>$user</span>";
221 -
 230+ $s .= ' '.$this->mSkin->revUserTools( $rev, true);
 231+
222232 if( $row->rev_minor_edit ) {
223233 $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
224234 }
@@ -243,7 +253,7 @@
244254 }
245255 #add blurb about text having been deleted
246256 if( $row->rev_deleted & Revision::DELETED_TEXT ) {
247 - $s .= ' ' . wfMsgHtml( 'deletedrev' );
 257+ $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
248258 }
249259
250260 $tools = array();
@@ -292,7 +302,7 @@
293303
294304 /** @todo document */
295305 function curLink( $rev, $latest ) {
296 - $cur = wfMsgExt( 'cur', array( 'escape') );
 306+ $cur = $this->message['cur'];
297307 if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
298308 return $cur;
299309 } else {
@@ -305,7 +315,7 @@
306316
307317 /** @todo document */
308318 function lastLink( $rev, $next, $counter ) {
309 - $last = wfMsgExt( 'last', array( 'escape' ) );
 319+ $last = $this->message['last'];
310320 if ( is_null( $next ) ) {
311321 # Probably no next row
312322 return $last;
Index: trunk/phase3/includes/SpecialContributions.php
@@ -171,7 +171,7 @@
172172 }
173173 $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
174174
175 - $comment = $wgContLang->getDirMark() . $sk->revComment( $rev );
 175+ $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
176176 $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
177177
178178 if( $this->target == 'newbies' ) {
Index: trunk/phase3/includes/SpecialPage.php
@@ -131,7 +131,7 @@
132132 'Log' => array( 'SpecialPage', 'Log' ),
133133 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ),
134134 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
135 - 'Import' => array( 'SpecialPage', "Import", 'import' ),
 135+ 'Import' => array( 'SpecialPage', 'Import', 'import' ),
136136 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
137137 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
138138 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ),
@@ -147,6 +147,7 @@
148148 'Mytalk' => array( 'SpecialMytalk' ),
149149 'Mycontributions' => array( 'SpecialMycontributions' ),
150150 'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ),
 151+ 'MergeHistory' => array( 'SpecialPage', 'Mergehistory', 'mergehistory' ),
151152 );
152153
153154 static public $mAliases;
@@ -379,6 +380,28 @@
380381 }
381382 return $pages;
382383 }
 384+
 385+ /**
 386+ * Return categorised listable log pages which are available
 387+ * for the current user, but not for everyone
 388+ * @static
 389+ */
 390+ static function getRestrictedLogs() {
 391+ global $wgUser, $wgLogRestrictions, $wgLogNames;
 392+
 393+ $pages = array();
 394+
 395+ if ( isset($wgLogRestrictions) ) {
 396+ foreach ( $wgLogRestrictions as $type => $restriction ) {
 397+ $page = SpecialPage::getTitleFor( 'Log', $type );
 398+ if ( $restriction !='' && $restriction !='*' && $wgUser->isAllowed( $restriction ) ) {
 399+ $name = wfMsgHtml( $wgLogNames[$type] );
 400+ $pages[$name] = $page;
 401+ }
 402+ }
 403+ }
 404+ return $pages;
 405+ }
383406
384407 /**
385408 * Execute a special page path.
Index: trunk/phase3/includes/LogPage.php
@@ -50,7 +50,7 @@
5151 function saveContent() {
5252 if( wfReadOnly() ) return false;
5353
54 - global $wgUser;
 54+ global $wgUser, $wgLogRestrictions;
5555 $fname = 'LogPage::saveContent';
5656
5757 $dbw = wfGetDB( DB_MASTER );
@@ -68,20 +68,18 @@
6969 'log_comment' => $this->comment,
7070 'log_params' => $this->params
7171 );
72 -
73 - # log_id doesn't exist on Wikimedia servers yet, and it's a tricky
74 - # schema update to do. Hack it for now to ignore the field on MySQL.
75 - if ( !is_null( $log_id ) ) {
76 - $data['log_id'] = $log_id;
77 - }
7872 $dbw->insert( 'logging', $data, $fname );
 73+ $newId = $dbw->insertId();
7974
8075 # And update recentchanges
81 - if ( $this->updateRecentChanges ) {
82 - $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
83 - $rcComment = $this->getRcComment();
84 - RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
85 - $this->type, $this->action, $this->target, $this->comment, $this->params );
 76+ if( $this->updateRecentChanges ) {
 77+ # Don't add private logs to RC!
 78+ if( !isset($wgLogRestrictions[$this->type]) || $wgLogRestrictions[$this->type]=='*' ) {
 79+ $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
 80+ $rcComment = $this->getRcComment();
 81+ RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
 82+ $this->type, $this->action, $this->target, $this->comment, $this->params, $newId );
 83+ }
8684 }
8785 return true;
8886 }
@@ -133,7 +131,7 @@
134132 */
135133 static function logHeader( $type ) {
136134 global $wgLogHeaders;
137 - return wfMsg( $wgLogHeaders[$type] );
 135+ return wfMsgHtml( $wgLogHeaders[$type] );
138136 }
139137
140138 /**
@@ -173,6 +171,11 @@
174172 $text = $wgContLang->ucfirst( $title->getText() );
175173 $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
176174 break;
 175+ case 'merge':
 176+ $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
 177+ $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
 178+ $params[1] = $wgLang->timeanddate( $params[1] );
 179+ break;
177180 default:
178181 $titleLink = $skin->makeLinkObj( $title );
179182 }
@@ -199,7 +202,7 @@
200203 }
201204 } else {
202205 array_unshift( $params, $titleLink );
203 - if ( $key == 'block/block' ) {
 206+ if ( $key == 'block/block' || $key == 'oversight/block' ) {
204207 if ( $skin ) {
205208 $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
206209 } else {
Index: trunk/phase3/includes/filerepo/File.php
@@ -380,6 +380,17 @@
381381 return $this->getPath() && file_exists( $this->path );
382382 }
383383
 384+ /**
 385+ * Returns true if file exists in the repository and can be included in a page.
 386+ * It would be unsafe to include private images, making public thumbnails inadvertently
 387+ *
 388+ * @return boolean Whether file exists in the repository and is includable.
 389+ * @public
 390+ */
 391+ function isVisible() {
 392+ return $this->exists();
 393+ }
 394+
384395 function getTransformScript() {
385396 if ( !isset( $this->transformScript ) ) {
386397 $this->transformScript = false;
@@ -600,7 +611,7 @@
601612 * STUB
602613 * Overridden by LocalFile
603614 */
604 - function purgeCache( $archiveFiles = array() ) {}
 615+ function purgeCache() {}
605616
606617 /**
607618 * Purge the file description page, but don't go after
@@ -912,14 +923,13 @@
913924 *
914925 * May throw database exceptions on error.
915926 *
916 - * @param $versions set of record ids of deleted items to restore,
917 - * or empty to restore all revisions.
 927+ * @param string $timestamp, restore all revisions since this time
918928 * @return the number of file revisions restored if successful,
919929 * or false on failure
920930 * STUB
921931 * Overridden by LocalFile
922932 */
923 - function restore( $versions=array(), $Unsuppress=false ) {
 933+ function restore( $timestamp = 0, $Unsuppress=false ) {
924934 $this->readOnlyError();
925935 }
926936
Index: trunk/phase3/includes/filerepo/ArchivedFile.php
@@ -5,24 +5,49 @@
66 */
77 class ArchivedFile
88 {
 9+ function ArchivedFile( $title, $id=0, $key='' ) {
 10+ if( !is_object( $title ) ) {
 11+ throw new MWException( 'ArchivedFile constructor given bogus title.' );
 12+ }
 13+ $this->id = -1;
 14+ $this->title = $title;
 15+ $this->name = $title->getDBKey();
 16+ $this->group = '';
 17+ $this->key = '';
 18+ $this->size = 0;
 19+ $this->bits = 0;
 20+ $this->width = 0;
 21+ $this->height = 0;
 22+ $this->metaData = '';
 23+ $this->mime = "unknown/unknown";
 24+ $this->type = '';
 25+ $this->description = '';
 26+ $this->user = 0;
 27+ $this->userText = '';
 28+ $this->timestamp = NULL;
 29+ $this->deleted = 0;
 30+ # BC, load if these are specified
 31+ if( $id || $key ) {
 32+ $this->load();
 33+ }
 34+ }
 35+
936 /**
10 - * Returns a file object from the filearchive table
11 - * @param $title, the corresponding image page title
12 - * @param $id, the image id, a unique key
13 - * @param $key, optional storage key
 37+ * Loads a file object from the filearchive table
1438 * @return ResultWrapper
1539 */
16 - function ArchivedFile( $title, $id=0, $key='' ) {
17 - if( !is_object( $title ) ) {
 40+ function load() {
 41+ if( !is_object( $this->title ) ) {
1842 throw new MWException( 'ArchivedFile constructor given bogus title.' );
1943 }
20 - $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'";
21 - if( $title->getNamespace() == NS_IMAGE ) {
 44+ $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'";
 45+ if( $this->title->getNamespace() == NS_IMAGE ) {
2246 $dbr = wfGetDB( DB_SLAVE );
2347 $res = $dbr->select( 'filearchive',
2448 array(
2549 'fa_id',
2650 'fa_name',
 51+ 'fa_archive_name',
2752 'fa_storage_key',
2853 'fa_storage_group',
2954 'fa_size',
@@ -39,7 +64,7 @@
4065 'fa_timestamp',
4166 'fa_deleted' ),
4267 array(
43 - 'fa_name' => $title->getDbKey(),
 68+ 'fa_name' => $this->title->getDBKey(),
4469 $conds ),
4570 __METHOD__,
4671 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -52,22 +77,23 @@
5378 $row = $ret->fetchObject();
5479
5580 // initialize fields for filestore image object
56 - $this->mId = intval($row->fa_id);
57 - $this->mName = $row->fa_name;
58 - $this->mGroup = $row->fa_storage_group;
59 - $this->mKey = $row->fa_storage_key;
60 - $this->mSize = $row->fa_size;
61 - $this->mBits = $row->fa_bits;
62 - $this->mWidth = $row->fa_width;
63 - $this->mHeight = $row->fa_height;
64 - $this->mMetaData = $row->fa_metadata;
65 - $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime";
66 - $this->mType = $row->fa_media_type;
67 - $this->mDescription = $row->fa_description;
68 - $this->mUser = $row->fa_user;
69 - $this->mUserText = $row->fa_user_text;
70 - $this->mTimestamp = $row->fa_timestamp;
71 - $this->mDeleted = $row->fa_deleted;
 81+ $this->id = intval($row->fa_id);
 82+ $this->name = $row->fa_name;
 83+ $this->archive_name = $row->fa_archive_name;
 84+ $this->group = $row->fa_storage_group;
 85+ $this->key = $row->fa_storage_key;
 86+ $this->size = $row->fa_size;
 87+ $this->bits = $row->fa_bits;
 88+ $this->width = $row->fa_width;
 89+ $this->height = $row->fa_height;
 90+ $this->metaData = $row->fa_metadata;
 91+ $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
 92+ $this->type = $row->fa_media_type;
 93+ $this->description = $row->fa_description;
 94+ $this->user = $row->fa_user;
 95+ $this->userText = $row->fa_user_text;
 96+ $this->timestamp = $row->fa_timestamp;
 97+ $this->deleted = $row->fa_deleted;
7298 } else {
7399 throw new MWException( 'This title does not correspond to an image page.' );
74100 return;
@@ -76,12 +102,40 @@
77103 }
78104
79105 /**
 106+ * Loads a file object from the filearchive table
 107+ * @return ResultWrapper
 108+ */
 109+ public static function newFromRow( $row ) {
 110+ $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) );
 111+
 112+ $file->id = intval($row->fa_id);
 113+ $file->name = $row->fa_name;
 114+ $file->archive_name = $row->fa_archive_name;
 115+ $file->group = $row->fa_storage_group;
 116+ $file->key = $row->fa_storage_key;
 117+ $file->size = $row->fa_size;
 118+ $file->bits = $row->fa_bits;
 119+ $file->width = $row->fa_width;
 120+ $file->height = $row->fa_height;
 121+ $file->metaData = $row->fa_metadata;
 122+ $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
 123+ $file->type = $row->fa_media_type;
 124+ $file->description = $row->fa_description;
 125+ $file->user = $row->fa_user;
 126+ $file->userText = $row->fa_user_text;
 127+ $file->timestamp = $row->fa_timestamp;
 128+ $file->deleted = $row->fa_deleted;
 129+
 130+ return $file;
 131+ }
 132+
 133+ /**
80134 * int $field one of DELETED_* bitfield constants
81135 * for file or revision rows
82136 * @return bool
83137 */
84138 function isDeleted( $field ) {
85 - return ($this->mDeleted & $field) == $field;
 139+ return ($this->deleted & $field) == $field;
86140 }
87141
88142 /**
@@ -91,18 +145,16 @@
92146 * @return bool
93147 */
94148 function userCan( $field ) {
95 - if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) {
 149+ if( isset($this->deleted) && ($this->deleted & $field) == $field ) {
96150 // images
97151 global $wgUser;
98 - $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
 152+ $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
99153 ? 'hiderevision'
100154 : 'deleterevision';
101 - wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
 155+ wfDebug( "Checking for $permission due to $field match on $this->deleted\n" );
102156 return $wgUser->isAllowed( $permission );
103157 } else {
104158 return true;
105159 }
106160 }
107161 }
108 -
109 -
Index: trunk/phase3/includes/filerepo/FileRepo.php
@@ -6,6 +6,7 @@
77 */
88 abstract class FileRepo {
99 const DELETE_SOURCE = 1;
 10+ const FIND_PRIVATE = 1;
1011 const OVERWRITE = 2;
1112 const OVERWRITE_SAME = 4;
1213
@@ -76,20 +77,23 @@
7778 *
7879 * @param mixed $time 14-character timestamp, or false for the current version
7980 */
80 - function findFile( $title, $time = false ) {
 81+ function findFile( $title, $time = false, $flags = 0 ) {
8182 # First try the current version of the file to see if it precedes the timestamp
8283 $img = $this->newFile( $title );
83 - if ( !$img ) {
84 - return false;
85 - }
86 - if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
 84+ # Check if the image exists and is of the specified timestamp
 85+ if ( $img->exists() && ( !$time || $img->getTimestamp()==$time ) ) {
8786 return $img;
8887 }
8988 # Now try an old version of the file
9089 $img = $this->newFile( $title, $time );
9190 if ( $img->exists() ) {
92 - return $img;
 91+ if( !$img->isDeleted(File::DELETED_FILE) ) {
 92+ return $img;
 93+ } else if( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
 94+ return $img;
 95+ }
9396 }
 97+ return false;
9498 }
9599
96100 /**
Index: trunk/phase3/includes/filerepo/OldLocalFile.php
@@ -56,6 +56,10 @@
5757 function isOld() {
5858 return true;
5959 }
 60+
 61+ function isVisible() {
 62+ return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
 63+ }
6064
6165 /**
6266 * Try to load file metadata from memcached. Returns true on success.
@@ -179,10 +183,8 @@
180184 function getCacheFields( $prefix = 'img_' ) {
181185 $fields = parent::getCacheFields( $prefix );
182186 $fields[] = $prefix . 'archive_name';
183 -
184 - // XXX: Temporary hack before schema update
185 - //$fields = array_diff( $fields, array(
186 - // 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) );
 187+ $fields[] = $prefix . 'deleted';
 188+
187189 return $fields;
188190 }
189191
@@ -226,7 +228,35 @@
227229 );
228230 wfProfileOut( __METHOD__ );
229231 }
 232+
 233+ /**
 234+ * int $field one of DELETED_* bitfield constants
 235+ * for file or revision rows
 236+ * @return bool
 237+ */
 238+ function isDeleted( $field ) {
 239+ return ($this->deleted & $field) == $field;
 240+ }
 241+
 242+ /**
 243+ * Determine if the current user is allowed to view a particular
 244+ * field of this FileStore image file, if it's marked as deleted.
 245+ * @param int $field
 246+ * @return bool
 247+ */
 248+ function userCan( $field ) {
 249+ if( isset($this->deleted) && ($this->deleted & $field) == $field ) {
 250+ global $wgUser;
 251+ $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
 252+ ? 'hiderevision'
 253+ : 'deleterevision';
 254+ wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
 255+ return $wgUser->isAllowed( $permission );
 256+ } else {
 257+ return true;
 258+ }
 259+ }
 260+
230261 }
231262
232263
233 -
Index: trunk/phase3/includes/filerepo/LocalFile.php
@@ -46,7 +46,8 @@
4747 $sha1, # SHA-1 base 36 content hash
4848 $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
4949 $upgraded, # Whether the row was upgraded on load
50 - $locked; # True if the image row is locked
 50+ $locked, # True if the image row is locked
 51+ $deleted; # Bitfield akin to rev_deleted
5152
5253 /**#@-*/
5354
@@ -236,8 +237,13 @@
237238 $this->$name = $value;
238239 }
239240 $this->fileExists = true;
240 - // Check for rows from a previous schema, quietly upgrade them
241 - $this->maybeUpgradeRow();
 241+ // Check if the file is hidden...
 242+ if( $this->isDeleted(File::DELETED_FILE) ) {
 243+ $this->fileExists = false; // treat as not existing
 244+ } else {
 245+ // Check for rows from a previous schema, quietly upgrade them
 246+ $this->maybeUpgradeRow();
 247+ }
242248 }
243249
244250 /**
@@ -572,7 +578,9 @@
573579 $this->historyRes = $dbr->select( 'image',
574580 array(
575581 '*',
576 - "'' AS oi_archive_name"
 582+ "'' AS oi_archive_name",
 583+ '0 as oi_deleted',
 584+ 'img_sha1'
577585 ),
578586 array( 'img_name' => $this->title->getDBkey() ),
579587 __METHOD__
@@ -741,7 +749,7 @@
742750 'oi_media_type' => 'img_media_type',
743751 'oi_major_mime' => 'img_major_mime',
744752 'oi_minor_mime' => 'img_minor_mime',
745 - 'oi_sha1' => 'img_sha1',
 753+ 'oi_sha1' => 'img_sha1'
746754 ), array( 'img_name' => $this->getName() ), __METHOD__
747755 );
748756
@@ -857,9 +865,9 @@
858866 * @param $reason
859867 * @return FileRepoStatus object.
860868 */
861 - function delete( $reason ) {
 869+ function delete( $reason, $suppress=false ) {
862870 $this->lock();
863 - $batch = new LocalFileDeleteBatch( $this, $reason );
 871+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
864872 $batch->addCurrent();
865873
866874 # Get old version relative paths
@@ -895,9 +903,9 @@
896904 * @throws MWException or FSException on database or filestore failure
897905 * @return FileRepoStatus object.
898906 */
899 - function deleteOld( $archiveName, $reason ) {
 907+ function deleteOld( $archiveName, $reason, $suppress=false ) {
900908 $this->lock();
901 - $batch = new LocalFileDeleteBatch( $this, $reason );
 909+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
902910 $batch->addOld( $archiveName );
903911 $status = $batch->execute();
904912 $this->unlock();
@@ -908,22 +916,21 @@
909917 return $status;
910918 }
911919
912 - /**
 920+ /*
913921 * Restore all or specified deleted revisions to the given file.
914922 * Permissions and logging are left to the caller.
915923 *
916924 * May throw database exceptions on error.
917925 *
918 - * @param $versions set of record ids of deleted items to restore,
919 - * or empty to restore all revisions.
 926+ * @param string $timestamp, restore all revisions since this time
920927 * @return FileRepoStatus
921928 */
922 - function restore( $versions = array(), $unsuppress = false ) {
 929+ function restore( $timestamp = 0, $unsuppress = false ) {
923930 $batch = new LocalFileRestoreBatch( $this );
924 - if ( !$versions ) {
 931+ if ( !$timestamp ) {
925932 $batch->addAll();
926933 } else {
927 - $batch->addIds( $versions );
 934+ $batch->addAll( $timestamp );
928935 }
929936 $status = $batch->execute();
930937 if ( !$status->ok ) {
@@ -1103,9 +1110,10 @@
11041111 var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
11051112 var $status;
11061113
1107 - function __construct( File $file, $reason = '' ) {
 1114+ function __construct( File $file, $reason = '', $suppress=false ) {
11081115 $this->file = $file;
11091116 $this->reason = $reason;
 1117+ $this->suppress = $suppress;
11101118 $this->status = $file->repo->newGood();
11111119 }
11121120
@@ -1186,6 +1194,16 @@
11871195 $dotExt = $ext === '' ? '' : ".$ext";
11881196 $encExt = $dbw->addQuotes( $dotExt );
11891197 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1198+
 1199+ // Bitfields to further suppress the content
 1200+ if ( $this->suppress ) {
 1201+ $bitfield = 0;
 1202+ // This should be 15...
 1203+ $bitfield |= Revision::DELETED_TEXT;
 1204+ $bitfield |= Revision::DELETED_COMMENT;
 1205+ $bitfield |= Revision::DELETED_USER;
 1206+ $bitfield |= Revision::DELETED_RESTRICTED;
 1207+ }
11901208
11911209 if ( $deleteCurrent ) {
11921210 $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
@@ -1197,7 +1215,7 @@
11981216 'fa_deleted_user' => $encUserId,
11991217 'fa_deleted_timestamp' => $encTimestamp,
12001218 'fa_deleted_reason' => $encReason,
1201 - 'fa_deleted' => 0,
 1219+ 'fa_deleted' => $this->suppress ? $bitfield : 0,
12021220
12031221 'fa_name' => 'img_name',
12041222 'fa_archive_name' => 'NULL',
@@ -1228,7 +1246,7 @@
12291247 'fa_deleted_user' => $encUserId,
12301248 'fa_deleted_timestamp' => $encTimestamp,
12311249 'fa_deleted_reason' => $encReason,
1232 - 'fa_deleted' => 0,
 1250+ 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
12331251
12341252 'fa_name' => 'oi_name',
12351253 'fa_archive_name' => 'oi_archive_name',
@@ -1271,7 +1289,25 @@
12721290 wfProfileIn( __METHOD__ );
12731291
12741292 $this->file->lock();
1275 -
 1293+ // Use revisiondelete to handle private files
 1294+ $privateFiles = array();
 1295+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1296+ $dbw = $this->file->repo->getMasterDB();
 1297+ $revisionDeleter = new RevisionDeleter( $dbw );
 1298+ if( !empty( $oldRels ) ) {
 1299+ $res = $dbw->select( 'oldimage',
 1300+ array( 'oi_archive_name', 'oi_sha1' ),
 1301+ array( 'oi_name' => $this->file->getName(),
 1302+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
 1303+ 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
 1304+ __METHOD__ );
 1305+ while( $row = $dbw->fetchObject( $res ) ) {
 1306+ $title = $this->file->getTitle();
 1307+ $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $row->oi_archive_name );
 1308+ $oimage->sha1 = $row->oi_sha1;
 1309+ $privateFiles[$row->oi_archive_name] = $oimage;
 1310+ }
 1311+ }
12761312 // Prepare deletion batch
12771313 $hashes = $this->getHashes();
12781314 $this->deletionBatch = array();
@@ -1279,7 +1315,8 @@
12801316 $dotExt = $ext === '' ? '' : ".$ext";
12811317 foreach ( $this->srcRels as $name => $srcRel ) {
12821318 // Skip files that have no hash (missing source)
1283 - if ( isset( $hashes[$name] ) ) {
 1319+ // Move private files using revisiondelete
 1320+ if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
12841321 $hash = $hashes[$name];
12851322 $key = $hash . $dotExt;
12861323 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
@@ -1308,6 +1345,19 @@
13091346 $this->file->unlockAndRollback();
13101347 return $this->status;
13111348 }
 1349+
 1350+ // Delete image/oldimage rows
 1351+ $this->doDBDeletes();
 1352+
 1353+ // Move private files to deletion archives
 1354+ $revisionDeleter = new RevisionDeleter( $dbw );
 1355+ foreach( $privateFiles as $name => $oimage ) {
 1356+ $ok = $revisionDeleter->moveImageFromFileRepos( $oimage, 'hidden', 'deleted' );
 1357+ if( $ok )
 1358+ $status->successCount++;
 1359+ else
 1360+ $status->failCount++;
 1361+ }
13121362
13131363 // Purge squid
13141364 if ( $wgUseSquid ) {
@@ -1319,9 +1369,6 @@
13201370 SquidUpdate::purge( $urls );
13211371 }
13221372
1323 - // Delete image/oldimage rows
1324 - $this->doDBDeletes();
1325 -
13261373 // Commit and return
13271374 $this->file->unlock();
13281375 wfProfileOut( __METHOD__ );
@@ -1335,12 +1382,13 @@
13361383 * Helper class for file undeletion
13371384 */
13381385 class LocalFileRestoreBatch {
1339 - var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
 1386+ var $file, $cleanupBatch, $ids, $all;
13401387
1341 - function __construct( File $file ) {
 1388+ function __construct( File $file, $unsuppress = false ) {
13421389 $this->file = $file;
13431390 $this->cleanupBatch = $this->ids = array();
13441391 $this->ids = array();
 1392+ $this->unsuppress = $unsuppress;
13451393 }
13461394
13471395 /**
@@ -1359,9 +1407,11 @@
13601408
13611409 /**
13621410 * Add all revisions of the file
 1411+ * Can be all from $timestamp if given
13631412 */
1364 - function addAll() {
 1413+ function addAll( $timestamp = false ) {
13651414 $this->all = true;
 1415+ $this->timestamp = $timestamp;
13661416 }
13671417
13681418 /**
@@ -1382,11 +1432,16 @@
13831433 $dbw = $this->file->repo->getMasterDB();
13841434 $status = $this->file->repo->newGood();
13851435
 1436+ $revisionDeleter = new RevisionDeleter( $dbw );
 1437+ $privateFiles = array();
 1438+
13861439 // Fetch all or selected archived revisions for the file,
13871440 // sorted from the most recent to the oldest.
13881441 $conditions = array( 'fa_name' => $this->file->getName() );
13891442 if( !$this->all ) {
13901443 $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
 1444+ } else if( $this->timestamp ) {
 1445+ $conditions[] = "fa_timestamp >= {$this->timestamp}";
13911446 }
13921447
13931448 $result = $dbw->select( 'filearchive', '*',
@@ -1403,12 +1458,7 @@
14041459 $archiveNames = array();
14051460 while( $row = $dbw->fetchObject( $result ) ) {
14061461 $idsPresent[] = $row->fa_id;
1407 - if ( $this->unsuppress ) {
1408 - // Currently, fa_deleted flags fall off upon restore, lets be careful about this
1409 - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
1410 - // Skip restoring file revisions that the user cannot restore
1411 - continue;
1412 - }
 1462+
14131463 if ( $row->fa_name != $this->file->getName() ) {
14141464 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
14151465 $status->failCount++;
@@ -1446,6 +1496,11 @@
14471497 }
14481498
14491499 if ( $first && !$exists ) {
 1500+ // The live (current) version cannot be hidden!
 1501+ if( $row->fa_deleted ) {
 1502+ $this->file->unlock();
 1503+ return $status;
 1504+ }
14501505 // This revision will be published as the new current version
14511506 $destRel = $this->file->getRel();
14521507 $insertCurrent = array(
@@ -1492,13 +1547,21 @@
14931548 'oi_media_type' => $props['media_type'],
14941549 'oi_major_mime' => $props['major_mime'],
14951550 'oi_minor_mime' => $props['minor_mime'],
1496 - 'oi_deleted' => $row->fa_deleted,
 1551+ 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
14971552 'oi_sha1' => $sha1 );
14981553 }
14991554
15001555 $deleteIds[] = $row->fa_id;
1501 - $storeBatch[] = array( $deletedUrl, 'public', $destRel );
1502 - $this->cleanupBatch[] = $row->fa_storage_key;
 1556+ // Use revisiondelete to handle private files
 1557+ if( $row->fa_deleted & File::DELETED_FILE ) {
 1558+ $title = $this->file->getTitle();
 1559+ $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $archiveName );
 1560+ $oimage->sha1 = $sha1;
 1561+ $privateFiles[$archiveName] = $oimage;
 1562+ } else {
 1563+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
 1564+ $this->cleanupBatch[] = $row->fa_storage_key;
 1565+ }
15031566 $first = false;
15041567 }
15051568 unset( $result );
@@ -1538,6 +1601,16 @@
15391602 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
15401603 __METHOD__ );
15411604 }
 1605+
 1606+ // Immediatly move to private files to hidden directory
 1607+ $revisionDeleter = new RevisionDeleter( $dbw );
 1608+ foreach ( $privateFiles as $oimage ) {
 1609+ $ok = $revisionDeleter->moveImageFromFileRepos( $oimage, 'deleted', 'hidden' );
 1610+ if( $ok )
 1611+ $status->successCount++;
 1612+ else
 1613+ $status->failCount++;
 1614+ }
15421615
15431616 if( $status->successCount > 0 ) {
15441617 if( !$exists ) {
Index: trunk/phase3/includes/filerepo/FSRepo.php
@@ -6,7 +6,7 @@
77 */
88
99 class FSRepo extends FileRepo {
10 - var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
 10+ var $directory, $deletedDir, $hiddenDir, $url, $hashLevels, $deletedHashLevels;
1111 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
1212 var $oldFileFactory = false;
1313 var $pathDisclosureProtection = 'simple';
@@ -22,7 +22,10 @@
2323 $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
2424 $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
2525 $info['deletedHashLevels'] : $this->hashLevels;
 26+ $this->hiddenHashLevels = isset( $info['hiddenHashLevels'] ) ?
 27+ $info['hiddenHashLevels'] : $this->hashLevels;
2628 $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
 29+ $this->hiddenDir = isset( $info['hiddenDir'] ) ? $info['hiddenDir'] : false;
2730 }
2831
2932 /**
@@ -55,6 +58,8 @@
5659 return $this->directory;
5760 case 'temp':
5861 return "{$this->directory}/temp";
 62+ case 'hidden':
 63+ return $this->hiddenDir;
5964 case 'deleted':
6065 return $this->deletedDir;
6166 default:
@@ -71,6 +76,8 @@
7277 return $this->url;
7378 case 'temp':
7479 return "{$this->url}/temp";
 80+ case 'hidden':
 81+ return false; // no public URL
7582 case 'deleted':
7683 return false; // no public URL
7784 default:
@@ -417,7 +424,7 @@
418425 $status->error( 'filedeleteerror', $srcPath );
419426 $good = false;
420427 }
421 - } else{
 428+ } else {
422429 if ( !@rename( $srcPath, $archivePath ) ) {
423430 $status->error( 'filerenameerror', $srcPath, $archivePath );
424431 $good = false;
Index: trunk/phase3/includes/EditPage.php
@@ -982,9 +982,13 @@
983983 }
984984 if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
985985 // Let sysop know that this will make private content public if saved
986 - if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
 986+
 987+ if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
 988+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
 989+ } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
987990 $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
988991 }
 992+
989993 if( !$this->mArticle->mRevision->isCurrent() ) {
990994 $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
991995 $wgOut->addWikiText( wfMsg( 'editingold' ) );
Index: trunk/phase3/includes/SpecialBlockip.php
@@ -227,34 +227,35 @@
228228 </td>
229229 </tr>
230230 ");
231 - // Allow some users to hide name from block log, blocklist and listusers
232 - if ( $wgUser->isAllowed( 'hideuser' ) ) {
 231+
 232+ global $wgSysopEmailBans;
 233+ if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
233234 $wgOut->addHTML("
234235 <tr>
235236 <td>&nbsp;</td>
236237 <td>
237 - " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
238 - 'wpHideName', 'wpHideName', $this->BlockHideName,
239 - array( 'tabindex' => '9' ) ) . "
 238+ " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
 239+ 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
 240+ array( 'tabindex' => '10' )) . "
240241 </td>
241242 </tr>
242243 ");
243244 }
244245
245 - global $wgSysopEmailBans;
246 -
247 - if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
 246+ // Allow some users to hide name from block log, blocklist and listusers
 247+ if ( $wgUser->isAllowed( 'hideuser' ) ) {
248248 $wgOut->addHTML("
249249 <tr id='wpEnableEmailBan'>
250250 <td>&nbsp;</td>
251251 <td>
252 - " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
253 - 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
254 - array( 'tabindex' => '10' )) . "
 252+ " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
 253+ 'wpHideName', 'wpHideName', $this->BlockHideName,
 254+ array( 'tabindex' => '9' ) ) . "
255255 </td>
256256 </tr>
257257 ");
258258 }
 259+
259260 $wgOut->addHTML("
260261 <tr>
261262 <td style='padding-top: 1em'>&nbsp;</td>
Index: trunk/phase3/includes/SpecialWatchlist.php
@@ -161,7 +161,8 @@
162162 $andLatest='';
163163 $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
164164 } else {
165 - $andLatest= 'AND rc_this_oldid=page_latest';
 165+ # Top log Ids for a page are not stored
 166+ $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
166167 $limitWatchlist = '';
167168 }
168169
@@ -364,4 +365,4 @@
365366 $count = floor( $count / 2 );
366367
367368 return( $count );
368 -}
\ No newline at end of file
 369+}
Index: trunk/phase3/includes/SpecialMergeHistory.php
@@ -0,0 +1,418 @@
 2+<?php
 3+
 4+/**
 5+ * Special page allowing users with the appropriate permissions to
 6+ * merge article histories, with some restrictions
 7+ *
 8+ * @addtogroup SpecialPage
 9+ */
 10+
 11+/**
 12+ * Constructor
 13+ */
 14+function wfSpecialMergehistory( $par ) {
 15+ global $wgRequest;
 16+
 17+ $form = new MergehistoryForm( $wgRequest, $par );
 18+ $form->execute();
 19+}
 20+
 21+/**
 22+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
 23+ * permissions to view and restore deleted content.
 24+ * @addtogroup SpecialPage
 25+ */
 26+class MergehistoryForm {
 27+ var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
 28+ var $mTargetObj, $mDestObj;
 29+
 30+ function MergehistoryForm( $request, $par = "" ) {
 31+ global $wgUser;
 32+
 33+ $this->mAction = $request->getVal( 'action' );
 34+ $this->mTarget = $request->getVal( 'target' );
 35+ $this->mDest = $request->getVal( 'dest' );
 36+
 37+ $this->mTargetID = intval( $request->getVal( 'targetID' ) );
 38+ $this->mDestID = intval( $request->getVal( 'destID' ) );
 39+ $this->mTimestamp = $request->getVal( 'mergepoint' );
 40+ $this->mComment = $request->getText( 'wpComment' );
 41+
 42+ $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
 43+ // target page
 44+ if( $this->mTarget !== "" ) {
 45+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
 46+ } else {
 47+ $this->mTargetObj = NULL;
 48+ }
 49+ # Destination
 50+ if( $this->mDest !== "" ) {
 51+ $this->mDestObj = Title::newFromURL( $this->mDest );
 52+ } else {
 53+ $this->mDestObj = NULL;
 54+ }
 55+
 56+ $this->preCacheMessages();
 57+ }
 58+
 59+ /**
 60+ * As we use the same small set of messages in various methods and that
 61+ * they are called often, we call them once and save them in $this->message
 62+ */
 63+ function preCacheMessages() {
 64+ // Precache various messages
 65+ if( !isset( $this->message ) ) {
 66+ $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
 67+ }
 68+ }
 69+
 70+ function execute() {
 71+ global $wgOut, $wgUser;
 72+
 73+ $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
 74+
 75+ if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
 76+ return $this->merge();
 77+ }
 78+
 79+ if( is_object($this->mTargetObj) && is_object($this->mDestObj) ) {
 80+ return $this->showHistory();
 81+ }
 82+
 83+ return $this->showMergeForm();
 84+ }
 85+
 86+ function showMergeForm() {
 87+ global $wgOut, $wgScript;
 88+
 89+ $wgOut->addWikiText( wfMsg( 'mergehistory-header' ) );
 90+
 91+ $wgOut->addHtml(
 92+ Xml::openElement( 'form', array(
 93+ 'method' => 'get',
 94+ 'action' => $wgScript ) ) .
 95+ '<fieldset>' .
 96+ Xml::element( 'legend', array(),
 97+ wfMsg( 'mergehistory-box' ) ) .
 98+ Xml::hidden( 'title',
 99+ SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
 100+ Xml::openElement( 'table' ) .
 101+ "<tr>
 102+ <td>".Xml::Label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
 103+ <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
 104+ </tr><tr>
 105+ <td>".Xml::Label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
 106+ <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
 107+ </tr><tr><td>" .
 108+ Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
 109+ "</td></tr>" .
 110+ Xml::closeElement( 'table' ) .
 111+ '</fieldset>' .
 112+ '</form>' );
 113+ }
 114+
 115+ private function showHistory() {
 116+ global $wgLang, $wgContLang, $wgUser, $wgOut;
 117+
 118+ $this->sk = $wgUser->getSkin();
 119+
 120+ $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
 121+
 122+ $this->showMergeForm();
 123+
 124+ # List all stored revisions
 125+ $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
 126+ $haveRevisions = $revisions && $revisions->getNumRows() > 0;
 127+
 128+ $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
 129+ $action = $titleObj->getLocalURL( "action=submit" );
 130+ # Start the form here
 131+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
 132+ $wgOut->addHtml( $top );
 133+
 134+ if( $haveRevisions ) {
 135+ # Format the user-visible controls (comment field, submission button)
 136+ # in a nice little table
 137+ $align = $wgContLang->isRtl() ? 'left' : 'right';
 138+ $table =
 139+ Xml::openElement( 'fieldset' ) .
 140+ Xml::openElement( 'table' ) .
 141+ "<tr>
 142+ <td colspan='2'>" .
 143+ wfMsgExt( 'mergehistory-merge', array('parseinline'),
 144+ $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
 145+ "</td>
 146+ </tr>
 147+ <tr>
 148+ <td align='$align'>" .
 149+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
 150+ "</td>
 151+ <td>" .
 152+ Xml::input( 'wpComment', 50, $this->mComment ) .
 153+ "</td>
 154+ </tr>
 155+ <tr>
 156+ <td>&nbsp;</td>
 157+ <td>" .
 158+ Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
 159+ "</td>
 160+ </tr>" .
 161+ Xml::closeElement( 'table' ) .
 162+ Xml::closeElement( 'fieldset' );
 163+
 164+ $wgOut->addHtml( $table );
 165+ }
 166+
 167+ $wgOut->addHTML( "<h2 id=\"mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
 168+
 169+ if( $haveRevisions ) {
 170+ $wgOut->addHTML( $revisions->getNavigationBar() );
 171+ $wgOut->addHTML( "<ul>" );
 172+ $wgOut->addHTML( $revisions->getBody() );
 173+ $wgOut->addHTML( "</ul>" );
 174+ $wgOut->addHTML( $revisions->getNavigationBar() );
 175+ } else {
 176+ $wgOut->addWikiText( wfMsg( "mergehistory-empty" ) );
 177+ }
 178+
 179+ # Show relevant lines from the deletion log:
 180+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
 181+ $logViewer = new LogViewer(
 182+ new LogReader(
 183+ new FauxRequest(
 184+ array( 'page' => $this->mTargetObj->getPrefixedText(),
 185+ 'type' => 'merge' ) ) ) );
 186+ $logViewer->showList( $wgOut );
 187+
 188+ # Slip in the hidden controls here
 189+ # When we submit, go by page ID to avoid some nasty but unlikely collisions.
 190+ # Such would happen if a page was renamed after the form loaded, but before submit
 191+ $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
 192+ $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
 193+ $misc .= Xml::hidden( 'target', $this->mTarget );
 194+ $misc .= Xml::hidden( 'dest', $this->mDest );
 195+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
 196+ $misc .= Xml::closeElement( 'form' );
 197+ $wgOut->addHtml( $misc );
 198+
 199+ return true;
 200+ }
 201+
 202+ function formatRevisionRow( $row ) {
 203+ global $wgUser, $wgLang;
 204+
 205+ $rev = new Revision( $row );
 206+
 207+ $stxt = '';
 208+ $last = $this->message['last'];
 209+
 210+ $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
 211+ $checkBox = wfRadio( "mergepoint", $ts, false );
 212+
 213+ $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(), $wgLang->timeanddate( $ts ), 'oldid=' . $rev->getID() );
 214+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
 215+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
 216+ }
 217+
 218+ # Last link
 219+ if( !$rev->userCan( Revision::DELETED_TEXT ) )
 220+ $last = $this->message['last'];
 221+ else if( isset($this->prevId[$row->rev_id]) )
 222+ $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'],
 223+ "&diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );
 224+
 225+ $userLink = $this->sk->userLink( $rev->getUser(), $rev->getUserText() )
 226+ . $this->sk->userToolLinks( $rev->getUser(), $rev->getUserText() );
 227+
 228+ if(!is_null($size = $row->rev_len)) {
 229+ if($size == 0)
 230+ $stxt = wfMsgHtml('historyempty');
 231+ else
 232+ $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
 233+ }
 234+ $comment = $this->sk->revComment( $rev );
 235+
 236+ return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
 237+ }
 238+
 239+ /**
 240+ * Fetch revision text link if it's available to all users
 241+ * @return string
 242+ */
 243+ function getPageLink( $row, $titleObj, $ts, $target ) {
 244+ global $wgLang;
 245+
 246+ if( !$this->userCan($row, Revision::DELETED_TEXT) ) {
 247+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
 248+ } else {
 249+ $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
 250+ if( $this->isDeleted($row, Revision::DELETED_TEXT) )
 251+ $link = '<span class="history-deleted">' . $link . '</span>';
 252+ return $link;
 253+ }
 254+ }
 255+
 256+ /**
 257+ * Fetch revision's user id if it's available to this user
 258+ * @return string
 259+ */
 260+ function getUser( $row ) {
 261+ if( !$this->userCan($row, Revision::DELETED_USER) ) {
 262+ return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
 263+ } else {
 264+ $link = $this->sk->userLink( $row->rev_user, $row->rev_user_text ) . $this->sk->userToolLinks( $row->rev_user, $row->rev_user_text );
 265+ if( $this->isDeleted($row, Revision::DELETED_USER) )
 266+ $link = '<span class="history-deleted">' . $link . '</span>';
 267+ return $link;
 268+ }
 269+ }
 270+
 271+ /**
 272+ * Fetch revision comment if it's available to this user
 273+ * @return string
 274+ */
 275+ function getComment( $row ) {
 276+ if( !$this->userCan($row, Revision::DELETED_COMMENT) ) {
 277+ return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
 278+ } else {
 279+ $link = $this->sk->commentBlock( $row->rev_comment );
 280+ if( $this->isDeleted($row, Revision::DELETED_COMMENT) )
 281+ $link = '<span class="history-deleted">' . $link . '</span>';
 282+ return $link;
 283+ }
 284+ }
 285+
 286+ function merge() {
 287+ global $wgOut, $wgUser;
 288+ # Get the titles directly from the IDs, in case the target page params
 289+ # were spoofed. The queries are done based on the IDs, so it's best to
 290+ # keep it consistent...
 291+ $targetTitle = Title::newFromID( $this->mTargetID );
 292+ $destTitle = Title::newFromID( $this->mDestID );
 293+ if( is_null($targetTitle) || is_null($destTitle) )
 294+ return false; // validate these
 295+ # Verify that this timestamp is valid
 296+ # Must be older than the destination page
 297+ $dbw = wfGetDB( DB_MASTER );
 298+ $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
 299+ array('rev_page' => $this->mDestID ),
 300+ __METHOD__ );
 301+ # Destination page must exist with revisions
 302+ if( !$maxtimestamp ) {
 303+ $wgOut->addHtml( wfMsg('mergehistory-fail') );
 304+ return false;
 305+ }
 306+ # Leave the latest version no matter what
 307+ $lasttime = $dbw->selectField( array('page','revision'),
 308+ 'rev_timestamp',
 309+ array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
 310+ __METHOD__ );
 311+ # Take the most restrictive of the twain
 312+ $maxtimestamp = ($lasttime < $maxtimestamp) ? $lasttime : $maxtimestamp;
 313+ if( $this->mTimestamp && $this->mTimestamp >= $maxtimestamp ) {
 314+ $wgOut->addHtml( wfMsg('mergehistory-fail') );
 315+ return false;
 316+ }
 317+ # Update the revisions
 318+ if( $this->mTimestamp )
 319+ $timewhere = "rev_timestamp <= {$this->mTimestamp}";
 320+ else
 321+ $timewhere = '1 = 1';
 322+
 323+ $dbw->update( 'revision',
 324+ array( 'rev_page' => $this->mDestID ),
 325+ array( 'rev_page' => $this->mTargetID,
 326+ "rev_timestamp < {$maxtimestamp}",
 327+ $timewhere ),
 328+ __METHOD__ );
 329+ # Check if this did anything
 330+ $count = $dbw->affectedRows();
 331+ if( !$count ) {
 332+ $wgOut->addHtml( wfMsg('mergehistory-fail') );
 333+ return false;
 334+ }
 335+ # Update our logs
 336+ $log = new LogPage( 'merge' );
 337+ $log->addEntry( 'merge', $targetTitle, $this->mComment,
 338+ array($destTitle->getPrefixedText(),$this->mTimestamp) );
 339+
 340+ $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
 341+ $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
 342+
 343+ wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
 344+
 345+ return true;
 346+ }
 347+}
 348+
 349+class MergeHistoryPager extends ReverseChronologicalPager {
 350+ public $mForm, $mConds;
 351+
 352+ function __construct( $form, $conds = array(), $title, $title2 ) {
 353+ $this->mForm = $form;
 354+ $this->mConds = $conds;
 355+ $this->title = $title;
 356+ $this->articleID = $title->getArticleID();
 357+
 358+ $dbw = wfGetDB( DB_SLAVE );
 359+ $maxtimestamp = $dbw->selectField('revision', 'MIN(rev_timestamp)',
 360+ array('rev_page' => $title2->getArticleID() ),
 361+ __METHOD__ );
 362+ $maxtimestamp = $maxtimestamp ? $maxtimestamp : 0;
 363+ $this->maxTimestamp = $maxtimestamp;
 364+
 365+ parent::__construct();
 366+ }
 367+
 368+ function getStartBody() {
 369+ wfProfileIn( __METHOD__ );
 370+ # Do a link batch query
 371+ $this->mResult->seek( 0 );
 372+ $batch = new LinkBatch();
 373+ # Give some pointers to make (last) links
 374+ $this->mForm->prevId = array();
 375+ while( $row = $this->mResult->fetchObject() ) {
 376+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
 377+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
 378+
 379+ $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
 380+ if( $rev_id > $row->rev_id )
 381+ $this->mForm->prevId[$rev_id] = $row->rev_id;
 382+ else if( $rev_id < $row->rev_id )
 383+ $this->mForm->prevId[$row->rev_id] = $rev_id;
 384+
 385+ $rev_id = $row->rev_id;
 386+ }
 387+
 388+ $batch->execute();
 389+ $this->mResult->seek( 0 );
 390+
 391+ wfProfileOut( __METHOD__ );
 392+ return '';
 393+ }
 394+
 395+ function formatRow( $row ) {
 396+ $block = new Block;
 397+ return $this->mForm->formatRevisionRow( $row );
 398+ }
 399+
 400+ function getQueryInfo() {
 401+ $conds = $this->mConds;
 402+ $conds['rev_page'] = $this->articleID;
 403+ $conds[] = "rev_timestamp < {$this->maxTimestamp}";
 404+ # Skip the latest one, as that could cause problems
 405+ if( $page = $this->title->getLatestRevID() )
 406+ $conds[] = "rev_id != {$page}";
 407+
 408+ return array(
 409+ 'tables' => array('revision'),
 410+ 'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
 411+ 'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
 412+ 'conds' => $conds
 413+ );
 414+ }
 415+
 416+ function getIndexField() {
 417+ return 'rev_timestamp';
 418+ }
 419+}
Index: trunk/phase3/includes/Revision.php
@@ -794,7 +794,7 @@
795795 * @param bool $minor
796796 * @return Revision
797797 */
798 - function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
 798+ static function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
799799 $fname = 'Revision::newNullRevision';
800800 wfProfileIn( $fname );
801801
Index: trunk/phase3/includes/DifferenceEngine.php
@@ -48,7 +48,7 @@
4949 # Show diff between revision $old and the previous one.
5050 # Get previous one from DB.
5151 #
52 - $this->mNewid = intval($old);
 52+ $this->mNewid = intval($old);
5353
5454 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
5555
@@ -64,6 +64,13 @@
6565 $this->mNewid = 0;
6666 }
6767
 68+ } else if( 'cur' === $new ) {
 69+ # Show diff between revision $old and the current one.
 70+ # Get previous one from DB.
 71+ #
 72+ $this->mNewid = $this->mTitle->getLatestRevID();
 73+
 74+ $this->mOldid = intval($old);
6875 } else {
6976 $this->mOldid = intval($old);
7077 $this->mNewid = intval($new);
@@ -220,14 +227,49 @@
221228 $newminor = wfElement( 'span', array( 'class' => 'minor' ),
222229 wfMsg( 'minoreditletter') ) . ' ';
223230 }
 231+
 232+ $rdel = ''; $ldel = '';
 233+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
 234+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 235+ if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
 236+ // If revision was hidden from sysops
 237+ $ldel = wfMsgHtml('rev-delundel');
 238+ } else {
 239+ $ldel = $sk->makeKnownLinkObj( $revdel,
 240+ wfMsgHtml('rev-delundel'),
 241+ 'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
 242+ '&oldid=' . urlencode( $this->mOldRev->getId() ) );
 243+ // Bolden oversighted content
 244+ if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
 245+ $ldel = "<strong>$ldel</strong>";
 246+ }
 247+ $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
 248+ // We don't currently handle well changing the top revision's settings
 249+ if( $this->mNewRev->isCurrent() ) {
 250+ // If revision was hidden from sysops
 251+ $rdel = wfMsgHtml('rev-delundel');
 252+ } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
 253+ // If revision was hidden from sysops
 254+ $rdel = wfMsgHtml('rev-delundel');
 255+ } else {
 256+ $rdel = $sk->makeKnownLinkObj( $revdel,
 257+ wfMsgHtml('rev-delundel'),
 258+ 'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
 259+ '&oldid=' . urlencode( $this->mNewRev->getId() ) );
 260+ // Bolden oversighted content
 261+ if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
 262+ $rdel = "<strong>$rdel</strong>";
 263+ }
 264+ $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
 265+ }
224266
225 - $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' .
226 - '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" .
227 - '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</div>" .
228 - '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
229 - $newHeader = '<div id="mw-diff-ntitle1"><strong>' .$this->mNewtitle . '</strong></div>' .
230 - '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev ) . " $rollback</div>" .
231 - '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" .
 267+ $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
 268+ '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
 269+ '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
 270+ '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
 271+ $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
 272+ '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
 273+ '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
232274 '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
233275
234276 $this->showDiff( $oldHeader, $newHeader );
@@ -248,8 +290,10 @@
249291
250292 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
251293 #add deleted rev tag if needed
252 - if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
 294+ if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
253295 $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
 296+ } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
 297+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
254298 }
255299
256300 if( !$this->mNewRev->isCurrent() ) {
@@ -394,20 +438,25 @@
395439 } // don't try to load but save the result
396440 }
397441
398 - #loadtext is permission safe, this just clears out the diff
 442+ // Loadtext is permission safe, this just clears out the diff
399443 if ( !$this->loadText() ) {
400444 wfProfileOut( $fname );
401445 return false;
402446 } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
403 - return '';
 447+ return '';
404448 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
405 - return '';
 449+ return '';
406450 }
407451
408452 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
409453
410454 // Save to cache for 7 days
411 - if ( $key !== false && $difftext !== false ) {
 455+ // Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it!
 456+ if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
 457+ wfIncrStats( 'diff_uncacheable' );
 458+ } else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
 459+ wfIncrStats( 'diff_uncacheable' );
 460+ } else if ( $key !== false && $difftext !== false ) {
412461 wfIncrStats( 'diff_cache_miss' );
413462 $wgMemc->set( $key, $difftext, 7*86400 );
414463 } else {
@@ -539,15 +588,9 @@
540589 /**
541590 * Add the header to a diff body
542591 */
543 - function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
 592+ static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
544593 global $wgOut;
545 -
546 - if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
547 - $otitle = '<span class="history-deleted">'.$otitle.'</span>';
548 - }
549 - if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
550 - $ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
551 - }
 594+
552595 $header = "
553596 <table class='diff'>
554597 <col class='diff-marker' />
@@ -600,10 +643,10 @@
601644 : Revision::newFromTitle( $this->mTitle );
602645 if( !$this->mNewRev instanceof Revision )
603646 return false;
604 -
 647+
605648 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
606649 $this->mNewid = $this->mNewRev->getId();
607 -
 650+
608651 // Set assorted variables
609652 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
610653 $this->mNewPage = $this->mNewRev->getTitle();
@@ -623,6 +666,11 @@
624667 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
625668 . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
626669 }
 670+ if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
 671+ $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
 672+ } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
 673+ $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
 674+ }
627675
628676 // Load the old revision object
629677 $this->mOldRev = false;
@@ -650,12 +698,20 @@
651699 $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
652700 $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
653701 $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
654 - $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) )
655 - . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
 702+ $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
656703
 704+ $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
 705+ . "(<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
657706 // Add an "undo" link
658707 $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
659 - $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
 708+ if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) )
 709+ $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
 710+
 711+ if ( !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
 712+ $this->mOldtitle = "<span class='history-deleted'>{$this->mOldPagetitle}</span>";
 713+ } else if ( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
 714+ $this->mOldtitle = '<span class="history-deleted">'.$this->mOldtitle.'</span>';
 715+ }
660716 }
661717
662718 return true;
@@ -676,7 +732,6 @@
677733 return false;
678734 }
679735 if ( $this->mOldRev ) {
680 - // FIXME: permission tests
681736 $this->mOldtext = $this->mOldRev->revText();
682737 if ( $this->mOldtext === false ) {
683738 return false;
Index: trunk/phase3/includes/SpecialIpblocklist.php
@@ -322,13 +322,13 @@
323323 $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
324324 $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
325325 }
326 -
 326+
327327 $comment = $sk->commentBlock( $block->mReason );
328 -
 328+
329329 $s = "{$line} $comment";
330330 if ( $block->mHideName )
331331 $s = '<span class="history-deleted">' . $s . '</span>';
332 -
 332+
333333 wfProfileOut( __METHOD__ );
334334 return "<li>$s $unblocklink</li>\n";
335335 }
Index: trunk/phase3/includes/SpecialLog.php
@@ -50,7 +50,24 @@
5151 $this->db = wfGetDB( DB_SLAVE );
5252 $this->setupQuery( $request );
5353 }
 54+
 55+ /**
 56+ * Returns a row of log data
 57+ * @param Title $title
 58+ * @param integer $logid, optional
 59+ * @private
 60+ */
 61+ function newRowFromID( $logid ) {
 62+ $fname = 'LogReader::newFromTitle';
5463
 64+ $dbr = wfGetDB( DB_SLAVE );
 65+ $row = $dbr->selectRow( 'logging', array('*'),
 66+ array('log_id' => intval($logid) ),
 67+ $fname );
 68+
 69+ return $row;
 70+ }
 71+
5572 /**
5673 * Basic setup and applies the limiting factors from the WebRequest object.
5774 * @param WebRequest $request
@@ -80,10 +97,31 @@
8198
8299 /**
83100 * Set the log reader to return only entries of the given type.
 101+ * Type restrictions enforced here
84102 * @param string $type A log type ('upload', 'delete', etc)
85103 * @private
86104 */
87105 function limitType( $type ) {
 106+ global $wgLogRestrictions, $wgUser;
 107+ // Reset the array, clears extra "where" clauses when $par is used
 108+ $this->whereClauses = $hiddenLogs = array();
 109+ // Exclude logs this user can't see
 110+ if( isset($wgLogRestrictions) ) {
 111+ if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed( $wgLogRestrictions[$type] ) )
 112+ return false;
 113+ // Don't show private logs to unpriviledged users or
 114+ // when not specifically requested.
 115+ foreach( $wgLogRestrictions as $logtype => $right ) {
 116+ if( !$wgUser->isAllowed( $right ) || empty($type) ) {
 117+ $safetype = $this->db->strencode( $logtype );
 118+ $hiddenLogs[] = "'$safetype'";
 119+ }
 120+ }
 121+ if( !empty($hiddenLogs) ) {
 122+ $this->whereClauses[] = 'log_type NOT IN('.implode(',',$hiddenLogs).')';
 123+ }
 124+ }
 125+
88126 if( empty( $type ) ) {
89127 return false;
90128 }
@@ -161,11 +199,16 @@
162200 * @private
163201 */
164202 function getQuery() {
 203+ global $wgAllowLogDeletion;
 204+
165205 $logging = $this->db->tableName( "logging" );
166206 $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp,
167 - log_user, user_name,
168 - log_namespace, log_title, page_id,
169 - log_comment, log_params FROM $logging ";
 207+ log_user, user_name, log_namespace, log_title, page_id,
 208+ log_comment, log_params, log_deleted ";
 209+ if( $wgAllowLogDeletion )
 210+ $sql .= ", log_id ";
 211+
 212+ $sql .= "FROM $logging ";
170213 if( !empty( $this->joinClauses ) ) {
171214 $sql .= implode( ' ', $this->joinClauses );
172215 }
@@ -233,7 +276,7 @@
234277 $this->db->freeResult( $res );
235278 return $ret;
236279 }
237 -
 280+
238281 }
239282
240283 /**
@@ -241,8 +284,12 @@
242285 * @addtogroup SpecialPage
243286 */
244287 class LogViewer {
 288+ const DELETED_ACTION = 1;
 289+ const DELETED_COMMENT = 2;
 290+ const DELETED_USER = 4;
 291+ const DELETED_RESTRICTED = 8;
 292+
245293 const NO_ACTION_LINK = 1;
246 -
247294 /**
248295 * @var LogReader $reader
249296 */
@@ -259,8 +306,22 @@
260307 global $wgUser;
261308 $this->skin = $wgUser->getSkin();
262309 $this->reader =& $reader;
 310+ $this->preCacheMessages();
263311 $this->flags = $flags;
264312 }
 313+
 314+ /**
 315+ * As we use the same small set of messages in various methods and that
 316+ * they are called often, we call them once and save them in $this->message
 317+ */
 318+ function preCacheMessages() {
 319+ // Precache various messages
 320+ if( !isset( $this->message ) ) {
 321+ foreach( explode(' ', 'viewpagelogs revhistory filehist rev-delundel' ) as $msg ) {
 322+ $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
 323+ }
 324+ }
 325+ }
265326
266327 /**
267328 * Take over the whole output page in $wgOut with the log display.
@@ -278,8 +339,84 @@
279340 $this->showError( $wgOut );
280341 }
281342 }
 343+
 344+ /**
 345+ * Fetch event's user id if it's available to all users
 346+ * @return int
 347+ */
 348+ static function getUser( $event ) {
 349+ if( $this->isDeleted( $event, Revision::DELETED_USER ) ) {
 350+ return 0;
 351+ } else {
 352+ return $event->log_user;
 353+ }
 354+ }
282355
283356 /**
 357+ * Fetch event's user id without regard for the current user's permissions
 358+ * @return string
 359+ */
 360+ static function getRawUser( $event ) {
 361+ return $event->log_user;
 362+ }
 363+
 364+ /**
 365+ * Fetch event's username if it's available to all users
 366+ * @return string
 367+ */
 368+ static function getUserText( $event ) {
 369+ if( $this->isDeleted( $event, Revision::DELETED_USER ) ) {
 370+ return "";
 371+ } else {
 372+ if ( isset($event->user_name) ) {
 373+ return $event->user_name;
 374+ } else {
 375+ return User::whoIs( $event->log_user );
 376+ }
 377+ }
 378+ }
 379+
 380+ /**
 381+ * Fetch event's username without regard for view restrictions
 382+ * @return string
 383+ */
 384+ static function getRawUserText( $event ) {
 385+ if ( isset($event->user_name) ) {
 386+ return $event->user_name;
 387+ } else {
 388+ return User::whoIs( $event->log_user );
 389+ }
 390+ }
 391+
 392+ /**
 393+ * Fetch event comment if it's available to all users
 394+ * @return string
 395+ */
 396+ static function getComment( $event ) {
 397+ if( $this->isDeleted( $event, Revision::DELETED_COMMENT ) ) {
 398+ return "";
 399+ } else {
 400+ return $event->log_comment;
 401+ }
 402+ }
 403+
 404+ /**
 405+ * Fetch event comment without regard for the current user's permissions
 406+ * @return string
 407+ */
 408+ static function getRawComment( $event ) {
 409+ return $event->log_comment;
 410+ }
 411+
 412+ /**
 413+ * Returns the title of the page associated with this entry.
 414+ * @return Title
 415+ */
 416+ static function getTitle( $event ) {
 417+ return Title::makeTitle( $event->log_namespace, $event->log_title );
 418+ }
 419+
 420+ /**
284421 * Load the data from the linked LogReader
285422 * Preload the link cache
286423 * Initialise numResults
@@ -363,54 +500,154 @@
364501 } else {
365502 $linkCache->addBadLinkObj( $title );
366503 }
 504+ // User links
 505+ $userLink = $this->skin->logUserTools( $s, true );
 506+ // Comment
 507+ if( $s->log_action == 'create2' ) {
 508+ $comment = ''; // Suppress from old account creations, useless and can contain incorrect links
 509+ } else if( $s->log_deleted & self::DELETED_COMMENT ) {
 510+ $comment = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
 511+ } else {
 512+ $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment );
 513+ }
 514+
 515+ $paramArray = LogPage::extractParams( $s->log_params );
 516+ $revert = ''; $del = '';
 517+
 518+ // Some user can hide log items and have review links
 519+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
 520+ $del = $this->showhideLinks( $s, $title );
 521+ }
 522+
 523+ // Show restore/unprotect/unblock
 524+ $revert = $this->showReviewLinks( $s, $title, $paramArray );
 525+
 526+ // Event description
 527+ if ( $s->log_deleted & self::DELETED_ACTION )
 528+ $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 529+ else
 530+ $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true );
 531+
 532+ return "<li><tt>$del</tt> $time $userLink $action $comment $revert</li>";
 533+ }
367534
368 - $userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinksRedContribs( $s->log_user, $s->user_name );
369 - $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment );
370 - $paramArray = LogPage::extractParams( $s->log_params );
 535+ /**
 536+ * @param $s, row object
 537+ * @param $s, title object
 538+ * @private
 539+ */
 540+ function showhideLinks( $s, $title ) {
 541+ global $wgAllowLogDeletion;
 542+
 543+ if( !$wgAllowLogDeletion )
 544+ return "";
 545+
 546+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 547+ // If event was hidden from sysops
 548+ if( !self::userCan( $s, Revision::DELETED_RESTRICTED ) ) {
 549+ $del = $this->message['rev-delundel'];
 550+ } else if( $s->log_type == 'oversight' ) {
 551+ return ''; // No one should be hiding from the oversight log
 552+ } else {
 553+ $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'], 'logid='.$s->log_id );
 554+ // Bolden oversighted content
 555+ if( self::isDeleted( $s, Revision::DELETED_RESTRICTED ) )
 556+ $del = "<strong>$del</strong>";
 557+ }
 558+ return "(<small>$del</small>)";
 559+ }
 560+
 561+ /**
 562+ * @param $s, row object
 563+ * @param $title, title object
 564+ * @param $s, param array
 565+ * @private
 566+ */
 567+ function showReviewLinks( $s, $title, $paramArray ) {
 568+ global $wgUser;
 569+
371570 $revert = '';
372 - // show revertmove link
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() ) ) . ')';
 571+ // Don't give away the page name
 572+ if( self::isDeleted($s,self::DELETED_ACTION) )
 573+ return $revert;
389574
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 );
 575+ if( $this->flags & self::NO_ACTION_LINK ) {
 576+ return $revert;
 577+ }
 578+ // Show revertmove link
 579+ if( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
 580+ $destTitle = Title::newFromText( $paramArray[0] );
 581+ if ( $destTitle ) {
 582+ $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
 583+ wfMsg( 'revertmove' ),
 584+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
 585+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
 586+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
 587+ '&wpMovetalk=0' );
 588+ }
 589+ // show undelete link
 590+ } else if( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
 591+ $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
 592+ wfMsg( 'undeletebtn' ) ,
 593+ 'target='. urlencode( $title->getPrefixedDBkey() ) );
 594+ // show unblock link
 595+ } elseif( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
 596+ $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
 597+ wfMsg( 'unblocklink' ),
 598+ 'action=unblock&ip=' . urlencode( $s->log_title ) );
 599+ // show change protection link
 600+ } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
 601+ $revert = $this->skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' );
 602+ // show user tool links for self created users
 603+ // TODO: The extension should be handling this, get it out of core!
 604+ } elseif ( $s->log_action == 'create2' ) {
 605+ if( isset( $paramArray[0] ) ) {
 606+ $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
 607+ } else {
 608+ # Fall back to a blue contributions link
 609+ $revert = $this->skin->userToolLinks( 1, $s->log_title );
 610+ }
 611+ // If an edit was hidden from a page give a review link to the history
 612+ } elseif ( $s->log_action == 'merge' ) {
 613+ $merge = SpecialPage::getTitleFor( 'Mergehistory' );
 614+ $revert = $this->skin->makeKnownLinkObj( $merge, wfMsg('revertmerge'),
 615+ wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedText() ) ) );
 616+ // If an edit was hidden from a page give a review link to the history
 617+ } else if( ($s->log_action=='revision') && $wgUser->isAllowed( 'deleterevision' ) ) {
 618+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 619+ // Different revision types use different URL params...
 620+ $subtype = isset($paramArray[2]) ? $paramArray[1] : '';
 621+ // Link to each hidden object ID, $paramArray[1] is the url param.
 622+ // Don't number by IDs because of their size.
 623+ // We may often just have one, in which case it's easier to not...
 624+ $Ids = explode( ',', $paramArray[2] );
 625+ if( count($Ids) == 1 ) {
 626+ $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
 627+ wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $Ids[0] ) ) );
 628+ } else {
 629+ $revert .= wfMsgHtml('revdel-restore').':';
 630+ foreach( $Ids as $n => $id ) {
 631+ $revert .= ' '.$this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
 632+ wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $id ) ) );
406633 }
407 - # Suppress $comment from old entries, not needed and can contain incorrect links
408 - $comment = '';
409634 }
 635+ // Hidden log items, give review link
 636+ } else if( ($s->log_action=='event') && $wgUser->isAllowed( 'deleterevision' ) ) {
 637+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 638+ $revert .= wfMsgHtml('revdel-restore');
 639+ $Ids = explode( ',', $paramArray[0] );
 640+ if( count($Ids) == 1 ) {
 641+ $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
 642+ wfArrayToCGI( array('logid' => $Ids[0] ) ) );
 643+ } else {
 644+ foreach( $Ids as $n => $id ) {
 645+ $revert .= $this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
 646+ wfArrayToCGI( array('logid' => $id ) ) );
 647+ }
 648+ }
410649 }
411 -
412 - $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true );
413 - $out = "<li>$time $userLink $action $comment $revert</li>\n";
414 - return $out;
 650+ $revert = ($revert == '') ? "" : "&nbsp;&nbsp;&nbsp;($revert) ";
 651+ return $revert;
415652 }
416653
417654 /**
@@ -451,6 +688,8 @@
452689 * @private
453690 */
454691 function getTypeMenu() {
 692+ global $wgLogRestrictions, $wgUser;
 693+
455694 $out = "<select name='type'>\n";
456695
457696 $validTypes = LogPage::validTypes();
@@ -468,7 +707,14 @@
469708 // Third pass generates sorted XHTML content
470709 foreach( $m as $text => $type ) {
471710 $selected = ($type == $this->reader->queryType());
472 - $out .= Xml::option( $text, $type, $selected ) . "\n";
 711+ // Restricted types
 712+ if ( isset($wgLogRestrictions[$type]) ) {
 713+ if ( $wgUser->isAllowed( $wgLogRestrictions[$type] ) ) {
 714+ $out .= Xml::option( $text, $type, $selected ) . "\n";
 715+ }
 716+ } else {
 717+ $out .= Xml::option( $text, $type, $selected ) . "\n";
 718+ }
473719 }
474720
475721 $out .= '</select>';
@@ -524,7 +770,41 @@
525771 $this->numResults < $limit);
526772 $out->addHTML( '<p>' . $html . '</p>' );
527773 }
 774+
 775+ /**
 776+ * Determine if the current user is allowed to view a particular
 777+ * field of this event, if it's marked as deleted.
 778+ * @param int $field
 779+ * @return bool
 780+ */
 781+ public static function userCan( $event, $field ) {
 782+ if( ( $event->log_deleted & $field ) == $field ) {
 783+ global $wgUser;
 784+ $permission = ( $event->log_deleted & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED
 785+ ? 'hiderevision'
 786+ : 'deleterevision';
 787+ wfDebug( "Checking for $permission due to $field match on $event->log_deleted\n" );
 788+ return $wgUser->isAllowed( $permission );
 789+ } else {
 790+ return true;
 791+ }
 792+ }
 793+
 794+ /**
 795+ * int $field one of DELETED_* bitfield constants
 796+ * @return bool
 797+ */
 798+ public static function isDeleted( $event, $field ) {
 799+ return ($event->log_deleted & $field) == $field;
 800+ }
528801 }
529802
 803+/**
 804+ * Aliases for backwards compatibility with 1.6
 805+ */
 806+define( 'MW_LOG_DELETED_ACTION', LogViewer::DELETED_ACTION );
 807+define( 'MW_LOG_DELETED_USER', LogViewer::DELETED_USER );
 808+define( 'MW_LOG_DELETED_COMMENT', LogViewer::DELETED_COMMENT );
 809+define( 'MW_LOG_DELETED_RESTRICTED', LogViewer::DELETED_RESTRICTED );
530810
531811
Index: trunk/phase3/includes/Article.php
@@ -390,6 +390,7 @@
391391 // We should instead work with the Revision object when we need it...
392392 $this->mContent = $revision->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : "";
393393 //$this->mContent = $revision->getText();
 394+ $this->mContent = $revision->revText(); // Loads if user is allowed
394395
395396 $this->mUser = $revision->getUser();
396397 $this->mUserText = $revision->getUserText();
@@ -1069,7 +1070,6 @@
10701071 $result = $dbw->affectedRows() != 0;
10711072
10721073 if ($result) {
1073 - // FIXME: Should the result from updateRedirectOn() be returned instead?
10741074 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
10751075 }
10761076
@@ -1494,6 +1494,7 @@
14951495 *
14961496 * @param boolean $noRedir Add redirect=no
14971497 * @param string $sectionAnchor section to redirect to, including "#"
 1498+ * @param string $extraq, extra query params
14981499 */
14991500 function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) {
15001501 global $wgOut;
@@ -1689,7 +1690,7 @@
16901691 * @return bool true on success
16911692 */
16921693 function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) {
1693 - global $wgUser, $wgRestrictionTypes, $wgContLang;
 1694+ global $wgUser, $wgRestrictionTypes, $wgContLang, $wgGroupPermissions;
16941695
16951696 $id = $this->mTitle->getArticleID();
16961697 if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) {
@@ -1719,7 +1720,6 @@
17201721
17211722 # If nothing's changed, do nothing
17221723 if( $changed ) {
1723 - global $wgGroupPermissions;
17241724 if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
17251725
17261726 $dbw = wfGetDB( DB_MASTER );
@@ -1832,6 +1832,8 @@
18331833 $confirm = $wgRequest->wasPosted() &&
18341834 $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
18351835 $reason = $wgRequest->getText( 'wpReason' );
 1836+ # Flag to hide all contents of the archived revisions
 1837+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('deleterevision');
18361838
18371839 # This code desperately needs to be totally rewritten
18381840
@@ -1856,7 +1858,7 @@
18571859 }
18581860
18591861 if( $confirm ) {
1860 - $this->doDelete( $reason );
 1862+ $this->doDelete( $reason, $suppress );
18611863 if( $wgRequest->getCheck( 'wpWatch' ) ) {
18621864 $this->doWatch();
18631865 } elseif( $this->mTitle->userIsWatching() ) {
@@ -2002,7 +2004,14 @@
20032005 $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) );
20042006 $token = htmlspecialchars( $wgUser->editToken() );
20052007 $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) );
2006 -
 2008+ if ( $wgUser->isAllowed( 'deleterevision' ) ) {
 2009+ $supress = "<tr><td>&nbsp;</td><td>";
 2010+ $supress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
 2011+ $supress .= "</td></tr>";
 2012+ } else {
 2013+ $supress = '';
 2014+ }
 2015+
20072016 $wgOut->addHTML( "
20082017 <form id='deleteconfirm' method='post' action=\"{$formaction}\">
20092018 <table border='0'>
@@ -2014,6 +2023,7 @@
20152024 <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" />
20162025 </td>
20172026 </tr>
 2027+ $supress
20182028 <tr>
20192029 <td>&nbsp;</td>
20202030 <td>$watch</td>
@@ -2051,12 +2061,12 @@
20522062 /**
20532063 * Perform a deletion and output success or failure messages
20542064 */
2055 - function doDelete( $reason ) {
 2065+ function doDelete( $reason, $suppress = false ) {
20562066 global $wgOut, $wgUser;
20572067 wfDebug( __METHOD__."\n" );
20582068
20592069 if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
2060 - if ( $this->doDeleteArticle( $reason ) ) {
 2070+ if ( $this->doDeleteArticle( $reason, $suppress ) ) {
20612071 $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
20622072
20632073 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
@@ -2069,7 +2079,7 @@
20702080 $wgOut->returnToMain( false );
20712081 wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
20722082 } else {
2073 - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
 2083+ $wgOut->showFatalError( wfMsg( 'cannotdelete' ).'<br/>'.wfMsg('cannotdelete-merge') );
20742084 }
20752085 }
20762086 }
@@ -2079,7 +2089,7 @@
20802090 * Deletes the article with database consistency, writes logs, purges caches
20812091 * Returns success
20822092 */
2083 - function doDeleteArticle( $reason ) {
 2093+ function doDeleteArticle( $reason, $suppress = false ) {
20842094 global $wgUseSquid, $wgDeferredUpdateList;
20852095 global $wgUseTrackbacks;
20862096
@@ -2093,10 +2103,30 @@
20942104 if ( $t == '' || $id == 0 ) {
20952105 return false;
20962106 }
 2107+ // Do not fuck up histories by merging them in annoying, unrevertable ways
 2108+ // This page id should match any deleted ones (excepting NULL values)
 2109+ $otherpages = $dbw->selectField( 'archive', 'COUNT(*)',
 2110+ array('ar_namespace' => $ns, 'ar_title' => $t,
 2111+ 'ar_page_id IS NOT NULL', "ar_page_id != $id" ),
 2112+ __METHOD__ );
 2113+ if( $otherpages )
 2114+ return false;
20972115
20982116 $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 );
20992117 array_push( $wgDeferredUpdateList, $u );
21002118
 2119+ // Bitfields to further supress the content
 2120+ if ( $suppress ) {
 2121+ $bitfield = 0;
 2122+ // This should be 15...
 2123+ $bitfield |= Revision::DELETED_TEXT;
 2124+ $bitfield |= Revision::DELETED_COMMENT;
 2125+ $bitfield |= Revision::DELETED_USER;
 2126+ $bitfield |= Revision::DELETED_RESTRICTED;
 2127+ } else {
 2128+ $bitfield = 'rev_deleted';
 2129+ }
 2130+
21012131 // For now, shunt the revision data into the archive table.
21022132 // Text is *not* removed from the text table; bulk storage
21032133 // is left intact to avoid breaking block-compression or
@@ -2122,6 +2152,7 @@
21232153 'ar_flags' => '\'\'', // MySQL's "strict mode"...
21242154 'ar_len' => 'rev_len',
21252155 'ar_page_id' => 'page_id',
 2156+ 'ar_deleted' => $bitfield
21262157 ), array(
21272158 'page_id' => $id,
21282159 'page_id = rev_page'
@@ -2162,8 +2193,9 @@
21632194 # Clear caches
21642195 Article::onArticleDelete( $this->mTitle );
21652196
2166 - # Log the deletion
2167 - $log = new LogPage( 'delete' );
 2197+ # Log the deletion, if the page was suppressed, log it at Oversight instead
 2198+ $logtype = ($suppress) ? 'oversight' : 'delete';
 2199+ $log = new LogPage( $logtype );
21682200 $log->addEntry( 'delete', $this->mTitle, $reason );
21692201
21702202 # Clear the cached article id so the interface doesn't act like we exist
@@ -2262,8 +2294,13 @@
22632295 );
22642296 }
22652297
 2298+ $target = Revision::newFromId( $s->rev_id );
 2299+ # Revision *must* be public and we don't well handle deleted edits on top
 2300+ if ( $target->isDeleted(REVISION::DELETED_TEXT) ) {
 2301+ $wgOut->setPageTitle( wfMsg('rollbackfailed') );
 2302+ $wgOut->addHTML( wfMsg( 'missingarticle' ) );
 2303+ }
22662304 # Get the edit summary
2267 - $target = Revision::newFromId( $s->rev_id );
22682305 if( empty( $summary ) )
22692306 $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
22702307
@@ -2524,8 +2561,28 @@
25252562 ? wfMsg( 'diff' )
25262563 : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid );
25272564
2528 - $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() )
2529 - . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() );
 2565+ $cdel='';
 2566+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
 2567+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 2568+ if( $revision->isCurrent() ) {
 2569+ // We don't handle top deleted edits too well
 2570+ $cdel = wfMsgHtml('rev-delundel');
 2571+ } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
 2572+ // If revision was hidden from sysops
 2573+ $cdel = wfMsgHtml('rev-delundel');
 2574+ } else {
 2575+ $cdel = $sk->makeKnownLinkObj( $revdel,
 2576+ wfMsgHtml('rev-delundel'),
 2577+ 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
 2578+ '&oldid=' . urlencode( $oldid ) );
 2579+ // Bolden oversighted content
 2580+ if( $revision->isDeleted( Revision::DELETED_RESTRICTED ) )
 2581+ $cdel = "<strong>$cdel</strong>";
 2582+ }
 2583+ $cdel = "(<small>$cdel</small>) ";
 2584+ }
 2585+
 2586+ $userlinks = $sk->revUserTools( $revision, true );
25302587
25312588 $m = wfMsg( 'revision-info-current' );
25322589 $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
@@ -2533,7 +2590,8 @@
25342591 : 'revision-info';
25352592
25362593 $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" .
2537 - "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
 2594+
 2595+ "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
25382596 $wgOut->setSubtitle( $r );
25392597 }
25402598
Index: trunk/phase3/includes/FileDeleteForm.php
@@ -46,8 +46,14 @@
4747 return;
4848 }
4949
50 - $this->oldimage = $wgRequest->getText( 'oldimage', false );
 50+ # Use revision delete
 51+ # $this->oldimage = $wgRequest->getText( 'oldimage', false );
 52+
 53+ $this->oldimage = false;
5154 $token = $wgRequest->getText( 'wpEditToken' );
 55+ # Flag to hide all contents of the archived revisions
 56+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('deleterevision');
 57+
5258 if( $this->oldimage && !$this->isValidOldSpec() ) {
5359 $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
5460 return;
@@ -65,7 +71,7 @@
6672 if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
6773 $comment = $wgRequest->getText( 'wpReason' );
6874 if( $this->oldimage ) {
69 - $status = $this->file->deleteOld( $this->oldimage, $comment );
 75+ $status = $this->file->deleteOld( $this->oldimage, $comment, $suppress );
7076 if( $status->ok ) {
7177 // Need to do a log item
7278 $log = new LogPage( 'delete' );
@@ -75,7 +81,7 @@
7682 $log->addEntry( 'delete', $this->title, $logComment );
7783 }
7884 } else {
79 - $status = $this->file->delete( $comment );
 85+ $status = $this->file->delete( $comment, $suppress );
8086 if( $status->ok ) {
8187 // Need to delete the associated article
8288 $article = new Article( $this->title );
Index: trunk/phase3/includes/ImagePage.php
@@ -416,22 +416,23 @@
417417
418418 if ( $line ) {
419419 $list = new ImageHistoryList( $sk, $this->img );
 420+ // Our top image
420421 $file = $this->repo->newFileFromRow( $line );
421422 $dims = $file->getDimensionsString();
422423 $s = $list->beginImageHistoryList() .
423424 $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp),
424 - $this->mTitle->getDBkey(), $line->img_user,
425 - $line->img_user_text, $line->img_size, $line->img_description,
426 - $dims
 425+ $this->mTitle->getDBkey(), $line->img_user,
 426+ $line->img_user_text, $line->img_size, $line->img_description, $dims,
 427+ $line->oi_deleted, $line->img_sha1
427428 );
428 -
 429+ // old image versions
429430 while ( $line = $this->img->nextHistoryLine() ) {
430431 $file = $this->repo->newFileFromRow( $line );
431432 $dims = $file->getDimensionsString();
432433 $s .= $list->imageHistoryLine( false, $line->oi_timestamp,
433434 $line->oi_archive_name, $line->oi_user,
434435 $line->oi_user_text, $line->oi_size, $line->oi_description,
435 - $dims
 436+ $dims, $line->oi_deleted, $line->oi_sha1
436437 );
437438 }
438439 $s .= $list->endImageHistoryList();
@@ -550,7 +551,7 @@
551552 . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
552553 . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
553554 . '<tr><td></td>'
554 - . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '<td></td>' : '' )
 555+ . ( $this->img->isLocal() && $wgUser->isAllowed( 'deleterevision' ) ? '<td></td>' : '' )
555556 . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
556557 . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
557558 . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
@@ -563,30 +564,48 @@
564565 return "</table>\n";
565566 }
566567
567 - public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) {
568 - global $wgUser, $wgLang, $wgContLang;
 568+ public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims,
 569+ $deleted, $sha1 ) {
 570+ global $wgUser, $wgLang, $wgContLang, $wgTitle;
569571 $local = $this->img->isLocal();
570 - $row = '';
 572+ $row = '<td>';
571573
572574 // Deletion link
573 - if( $local && $wgUser->isAllowed( 'delete' ) ) {
574 - $row .= '<td>';
 575+ if( $iscur && $local && $wgUser->isAllowed( 'delete' ) ) {
575576 $q = array();
576577 $q[] = 'action=delete';
577 - if( !$iscur )
578 - $q[] = 'oldimage=' . urlencode( $img );
 578+ $q[] = 'image=' . $this->title->getPartialUrl();
579579 $row .= '(' . $this->skin->makeKnownLinkObj(
580580 $this->title,
581581 wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
582582 implode( '&', $q )
583583 ) . ')';
584 - $row .= '</td>';
 584+ $row .= '</td><td>';
585585 }
586586
 587+ if( !$iscur && $local && $wgUser->isAllowed( 'deleterevision' ) ) {
 588+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 589+ if( !$this->userCan($deleted,Image::DELETED_RESTRICTED) ) {
 590+ // If file was hidden from sysops
 591+ $del = wfMsgHtml( 'rev-delundel' );
 592+ } else {
 593+ // If the file was hidden, link to sha-1
 594+ list($ts,$name) = explode('!',$img,2);
 595+ $del = $this->skin->makeKnownLinkObj( $revdel, wfMsg( 'rev-delundel' ),
 596+ 'target=' . urlencode( $wgTitle->getPrefixedText() ) .
 597+ '&oldimage=' . urlencode( $ts ) );
 598+ // Bolden oversighted content
 599+ if( $this->isDeleted($deleted,Image::DELETED_RESTRICTED) )
 600+ $del = "<strong>$del</strong>";
 601+ }
 602+ $row .= "<tt>(<small>$del</small>)</tt></td><td> ";
 603+ }
 604+
587605 // Reversion link/current indicator
588 - $row .= '<td>';
589606 if( $iscur ) {
590 - $row .= '(' . wfMsgHtml( 'filehist-current' ) . ')';
 607+ $row .= ' (' . wfMsgHtml( 'filehist-current' ) . ')';
 608+ } elseif( $this->isDeleted($deleted,Image::DELETED_FILE) ) {
 609+ $row .= '(' . wfMsgHtml('filehist-revert') . ')';
591610 } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
592611 $q = array();
593612 $q[] = 'action=revert';
@@ -602,18 +621,32 @@
603622
604623 // Date/time and image link
605624 $row .= '<td>';
606 - $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
607 - $row .= Xml::element(
608 - 'a',
609 - array( 'href' => $url ),
610 - $wgLang->timeAndDate( $timestamp, true )
611 - );
 625+ if( !$this->userCan($deleted,Image::DELETED_FILE) ) {
 626+ # Don't link to unviewable files
 627+ $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>';
 628+ } else if( $this->isDeleted($deleted,Image::DELETED_FILE) ) {
 629+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
 630+ # Make a link to review the image
 631+ $url = $this->skin->makeKnownLinkObj( $revdel, $wgLang->timeAndDate( $timestamp, true ),
 632+ "target=".$wgTitle->getPrefixedText()."&file=$sha1.".$this->img->getExtension() );
 633+ $row .= '<span class="history-deleted">'.$url.'</span>';
 634+ } else {
 635+ $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
 636+ $row .= Xml::element( 'a',
 637+ array( 'href' => $url ),
 638+ $wgLang->timeAndDate( $timestamp, true ) );
 639+ }
 640+
612641 $row .= '</td>';
613642
614643 // Uploading user
615644 $row .= '<td>';
616645 if( $local ) {
617 - $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
 646+ // Hide deleted usernames
 647+ if( $this->isDeleted($deleted,Image::DELETED_USER) )
 648+ $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
 649+ else
 650+ $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
618651 } else {
619652 $row .= htmlspecialchars( $usertext );
620653 }
@@ -625,10 +658,45 @@
626659 // File size
627660 $row .= '<td class="mw-imagepage-filesize">' . $this->skin->formatSize( $size ) . '</td>';
628661
629 - // Comment
630 - $row .= '<td>' . $this->skin->formatComment( $description, $this->title ) . '</td>';
 662+ // Don't show deleted descriptions
 663+ if ( $this->isDeleted($deleted,Image::DELETED_COMMENT) )
 664+ $row .= '<td><span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span></td>';
 665+ else
 666+ $row .= '<td>' . $this->skin->commentBlock( $description, $this->title ) . '</td>';
631667
632668 return "<tr>{$row}</tr>\n";
633669 }
 670+
 671+ /**
 672+ * int $field one of DELETED_* bitfield constants
 673+ * for file or revision rows
 674+ * @param int $bitfield
 675+ * @param int $field
 676+ * @return bool
 677+ */
 678+ function isDeleted( $bitfield, $field ) {
 679+ return ($bitfield & $field) == $field;
 680+ }
 681+
 682+ /**
 683+ * Determine if the current user is allowed to view a particular
 684+ * field of this FileStore image file, if it's marked as deleted.
 685+ * @param int $bitfield
 686+ * @param int $field
 687+ * @return bool
 688+ */
 689+ function userCan( $bitfield, $field ) {
 690+ if( ($bitfield & $field) == $field ) {
 691+ // images
 692+ global $wgUser;
 693+ $permission = ( $bitfield & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
 694+ ? 'hiderevision'
 695+ : 'deleterevision';
 696+ wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
 697+ return $wgUser->isAllowed( $permission );
 698+ } else {
 699+ return true;
 700+ }
 701+ }
634702
635703 }
Index: trunk/phase3/includes/SpecialRevisiondelete.php
@@ -1,38 +1,58 @@
22 <?php
33
44 /**
5 - * Not quite ready for production use yet; need to fix up the restricted mode,
6 - * and provide for preservation across delete/undelete of the page.
 5+ * Special page allowing users with the appropriate permissions to view
 6+ * and hide revisions. Log items can also be hidden.
77 *
8 - * To try this out, set up extra permissions something like:
9 - * $wgGroupPermissions['sysop']['deleterevision'] = true;
10 - * $wgGroupPermissions['bureaucrat']['hiderevision'] = true;
 8+ * @addtogroup SpecialPage
119 */
1210
1311 function wfSpecialRevisiondelete( $par = null ) {
14 - global $wgOut, $wgRequest;
15 -
16 - $target = $wgRequest->getVal( 'target' );
17 - $oldid = $wgRequest->getIntArray( 'oldid' );
18 -
 12+ global $wgOut, $wgRequest, $wgUser, $wgAllowLogDeletion;
 13+ # Handle our many different possible input types
 14+ $target = $wgRequest->getText( 'target' );
 15+ $oldid = $wgRequest->getArray( 'oldid' );
 16+ $artimestamp = $wgRequest->getArray( 'artimestamp' );
 17+ $logid = $wgAllowLogDeletion ? $wgRequest->getArray( 'logid' ) : '';
 18+ $image = $wgRequest->getArray( 'oldimage' );
 19+ $fileid = $wgRequest->getArray( 'fileid' );
 20+ # For reviewing deleted files...
 21+ $file = $wgRequest->getVal( 'file' );
 22+ # If this is a revision, then we need a target page
1923 $page = Title::newFromUrl( $target );
20 -
21 - if( is_null( $page ) ) {
22 - $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
 24+ if( is_null($page) && is_null($logid) ) {
 25+ $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
2326 return;
2427 }
 28+ # Only one target set at a time please!
 29+ $inputs = !empty($file) + !empty($oldid) + !empty($logid) + !empty($artimestamp) +
 30+ !empty($fileid) + !empty($image);
2531
26 - if( is_null( $oldid ) ) {
 32+ if( $inputs > 1 || $inputs==0 ) {
2733 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
2834 return;
2935 }
30 -
31 - $form = new RevisionDeleteForm( $wgRequest );
 36+ # Either submit or create our form
 37+ $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $image, $file );
3238 if( $wgRequest->wasPosted() ) {
3339 $form->submit( $wgRequest );
34 - } else {
35 - $form->show( $wgRequest );
 40+ } else if( $oldid || $artimestamp ) {
 41+ $form->showRevs( $wgRequest );
 42+ } else if( $fileid || $image ) {
 43+ $form->showImages( $wgRequest );
 44+ } else if( $logid ) {
 45+ $form->showEvents( $wgRequest );
3646 }
 47+ # Show relevant lines from the deletion log
 48+ # This will show even if said ID does not exist...might be helpful
 49+ if( !is_null($page) ) {
 50+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
 51+ $logViewer = new LogViewer(
 52+ new LogReader(
 53+ new FauxRequest(
 54+ array( 'page' => $page->getPrefixedText(), 'type' => 'delete' ) ) ) );
 55+ $logViewer->showList( $wgOut );
 56+ }
3757 }
3858
3959 /**
@@ -42,54 +62,410 @@
4363 class RevisionDeleteForm {
4464 /**
4565 * @param Title $page
46 - * @param int $oldid
 66+ * @param array $oldids
 67+ * @param array $logids
 68+ * @param array $artimestamps
 69+ * @param array $fileids
 70+ * @param array $oldimages
 71+ * @param string $file
4772 */
48 - function __construct( $request ) {
 73+ function __construct( $page, $oldids=null, $logids=null, $artimestamps=null, $fileids=null, $oldimages=null, $file=null ) {
4974 global $wgUser;
 75+
 76+ $this->page = $page;
 77+ $this->skin = $wgUser->getSkin();
5078
51 - $target = $request->getVal( 'target' );
52 - $this->page = Title::newFromUrl( $target );
53 -
54 - $this->revisions = $request->getIntArray( 'oldid', array() );
55 -
56 - $this->skin = $wgUser->getSkin();
 79+ // For reviewing deleted files
 80+ if( $file ) {
 81+ $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
 82+ $oimage->load();
 83+ // Check if user is allowed to see this file
 84+ if( !$oimage->userCan(File::DELETED_FILE) ) {
 85+ $wgOut->permissionRequired( 'hiderevision' );
 86+ return false;
 87+ } else {
 88+ return $this->showFile( $file );
 89+ }
 90+ }
 91+ // At this point, we should only have one of these
 92+ if( $oldids ) {
 93+ $this->revisions = $oldids;
 94+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
 95+ $this->deletetype='oldid';
 96+ } else if( $artimestamps ) {
 97+ $this->archrevs = $artimestamps;
 98+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
 99+ $this->deletetype='artimestamp';
 100+ } else if( $oldimages ) {
 101+ $this->ofiles = $oldimages;
 102+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
 103+ $this->deletetype='oldimage';
 104+ } else if( $fileids ) {
 105+ $this->afiles = $fileids;
 106+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
 107+ $this->deletetype='fileid';
 108+ } else if( $logids ) {
 109+ $this->events = $logids;
 110+ $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogViewer::DELETED_ACTION );
 111+ $this->deletetype='logid';
 112+ }
 113+ // Our checkbox messages depends one what we are doing,
 114+ // e.g. we don't hide "text" for logs or images
57115 $this->checks = array(
58 - array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT ),
 116+ $hide_content_name,
59117 array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
60118 array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ),
61119 array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED ) );
62120 }
63121
64122 /**
 123+ * Show a deleted file version requested by the visitor.
 124+ */
 125+ function showFile( $key ) {
 126+ global $wgOut, $wgRequest;
 127+ $wgOut->disable();
 128+
 129+ # We mustn't allow the output to be Squid cached, otherwise
 130+ # if an admin previews a deleted image, and it's cached, then
 131+ # a user without appropriate permissions can toddle off and
 132+ # nab the image, and Squid will serve it
 133+ $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
 134+ $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
 135+ $wgRequest->response()->header( 'Pragma: no-cache' );
 136+
 137+ $store = FileStore::get( 'hidden' );
 138+ $store->stream( $key );
 139+ }
 140+
 141+ /**
 142+ * This lets a user set restrictions for live and archived revisions
65143 * @param WebRequest $request
66144 */
67 - function show( $request ) {
68 - global $wgOut, $wgUser;
 145+ function showRevs( $request ) {
 146+ global $wgOut, $wgUser, $action;
69147
70 - $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) );
 148+ $UserAllowed = true;
71149
 150+ $count = ($this->deletetype=='oldid') ?
 151+ count($this->revisions) : count($this->archrevs);
 152+ $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'),
 153+ $this->page->getPrefixedText(), $count ) );
 154+
 155+ $bitfields = 0;
72156 $wgOut->addHtml( "<ul>" );
73 - foreach( $this->revisions as $revid ) {
74 - $rev = Revision::newFromTitle( $this->page, $revid );
75 - if( !isset( $rev ) ) {
 157+
 158+ $where = $revObjs = array();
 159+ $dbr = wfGetDB( DB_SLAVE );
 160+ // Live revisions...
 161+ if( $this->deletetype=='oldid' ) {
 162+ // Run through and pull all our data in one query
 163+ foreach( $this->revisions as $revid ) {
 164+ $where[] = intval($revid);
 165+ }
 166+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
 167+ $result = $dbr->select( 'revision', '*',
 168+ array( 'rev_page' => $this->page->getArticleID(),
 169+ $whereClause ),
 170+ __METHOD__ );
 171+ while( $row = $dbr->fetchObject( $result ) ) {
 172+ $revObjs[$row->rev_id] = new Revision( $row );
 173+ }
 174+ foreach( $this->revisions as $revid ) {
 175+ // Hiding top revisison is bad
 176+ if( !is_object($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
 177+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
 178+ return;
 179+ } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
 180+ // If a rev is hidden from sysops
 181+ if( $action != 'submit') {
 182+ $wgOut->permissionRequired( 'hiderevision' );
 183+ return;
 184+ }
 185+ $UserAllowed = false;
 186+ }
 187+ $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
 188+ $bitfields |= $revObjs[$revid]->mDeleted;
 189+ }
 190+ // The archives...
 191+ } else {
 192+ // Run through and pull all our data in one query
 193+ foreach( $this->archrevs as $timestamp ) {
 194+ $where[] = $dbr->addQuotes( $timestamp );
 195+ }
 196+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
 197+ $result = $dbr->select( 'archive', '*',
 198+ array( 'ar_namespace' => $this->page->getNamespace(),
 199+ 'ar_title' => $this->page->getDBKey(),
 200+ $whereClause ),
 201+ __METHOD__ );
 202+ while( $row = $dbr->fetchObject( $result ) ) {
 203+ $revObjs[$row->ar_timestamp] = new Revision( array(
 204+ 'page' => $this->page->getArticleId(),
 205+ 'id' => $row->ar_rev_id,
 206+ 'text' => $row->ar_text_id,
 207+ 'comment' => $row->ar_comment,
 208+ 'user' => $row->ar_user,
 209+ 'user_text' => $row->ar_user_text,
 210+ 'timestamp' => $row->ar_timestamp,
 211+ 'minor_edit' => $row->ar_minor_edit,
 212+ 'text_id' => $row->ar_text_id,
 213+ 'deleted' => $row->ar_deleted,
 214+ 'len' => $row->ar_len) );
 215+ }
 216+ foreach( $this->archrevs as $timestamp ) {
 217+ if( !is_object($revObjs[$timestamp]) ) {
 218+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
 219+ return;
 220+ }
 221+ }
 222+ foreach( $revObjs as $rev ) {
 223+ if( !$rev->userCan(Revision::DELETED_RESTRICTED) ) {
 224+ //if a rev is hidden from sysops
 225+ if( $action != 'submit') {
 226+ $wgOut->permissionRequired( 'hiderevision' );
 227+ return;
 228+ }
 229+ $UserAllowed = false;
 230+ }
 231+ $wgOut->addHtml( $this->historyLine( $rev ) );
 232+ $bitfields |= $rev->mDeleted;
 233+ }
 234+ }
 235+ $wgOut->addHtml( "</ul>" );
 236+
 237+ $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
 238+ //Normal sysops can always see what they did, but can't always change it
 239+ if( !$UserAllowed ) return;
 240+
 241+ $items = array(
 242+ wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
 243+ wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
 244+ $hidden = array(
 245+ wfHidden( 'wpEditToken', $wgUser->editToken() ),
 246+ wfHidden( 'target', $this->page->getPrefixedText() ),
 247+ wfHidden( 'type', $this->deletetype ) );
 248+ if( $this->deletetype=='oldid' ) {
 249+ foreach( $revObjs as $rev )
 250+ $hidden[] = wfHidden( 'oldid[]', $rev->getID() );
 251+ } else {
 252+ foreach( $revObjs as $rev )
 253+ $hidden[] = wfHidden( 'artimestamp[]', $rev->getTimestamp() );
 254+ }
 255+ $special = SpecialPage::getTitleFor( 'Revisiondelete' );
 256+ $wgOut->addHtml( wfElement( 'form', array(
 257+ 'method' => 'post',
 258+ 'action' => $special->getLocalUrl( 'action=submit' ) ),
 259+ null ) );
 260+
 261+ $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
 262+ // FIXME: all items checked for just one rev are checked, even if not set for the others
 263+ foreach( $this->checks as $item ) {
 264+ list( $message, $name, $field ) = $item;
 265+ $wgOut->addHtml( "<div>" .
 266+ wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
 267+ "</div>\n" );
 268+ }
 269+ $wgOut->addHtml( '</fieldset>' );
 270+ foreach( $items as $item ) {
 271+ $wgOut->addHtml( '<p>' . $item . '</p>' );
 272+ }
 273+ foreach( $hidden as $item ) {
 274+ $wgOut->addHtml( $item );
 275+ }
 276+
 277+ $wgOut->addHtml( '</form>' );
 278+ }
 279+
 280+ /**
 281+ * This lets a user set restrictions for archived images
 282+ * @param WebRequest $request
 283+ */
 284+ function showImages( $request ) {
 285+ global $wgOut, $wgUser, $action;
 286+
 287+ $UserAllowed = true;
 288+
 289+ $count = ($this->deletetype=='oldimage') ? count($this->ofiles) : count($this->afiles);
 290+ $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'), $this->page->getPrefixedText(), $count ) );
 291+
 292+ $bitfields = 0;
 293+ $wgOut->addHtml( "<ul>" );
 294+
 295+ $where = $filesObjs = array();
 296+ $dbr = wfGetDB( DB_SLAVE );
 297+ // Live old revisions...
 298+ if( $this->deletetype=='oldimage' ) {
 299+ // Run through and pull all our data in one query
 300+ foreach( $this->ofiles as $timestamp ) {
 301+ $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
 302+ }
 303+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
 304+ $result = $dbr->select( 'oldimage', '*',
 305+ array( 'oi_name' => $this->page->getDbKey(),
 306+ $whereClause ),
 307+ __METHOD__ );
 308+ while( $row = $dbr->fetchObject( $result ) ) {
 309+ $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
 310+ $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
 311+ $filesObjs[$row->oi_archive_name]->userText = $row->oi_user_text;
 312+ }
 313+ // Check through our images
 314+ foreach( $this->ofiles as $timestamp ) {
 315+ $archivename = $timestamp.'!'.$this->page->getDbKey();
 316+ if( !isset($filesObjs[$archivename]) ) {
 317+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
 318+ return;
 319+ }
 320+ }
 321+ foreach( $filesObjs as $file ) {
 322+ if( !isset($file) ) {
 323+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
 324+ return;
 325+ } else if( !$file->userCan(File::DELETED_RESTRICTED) ) {
 326+ // If a rev is hidden from sysops
 327+ if( $action != 'submit' ) {
 328+ $wgOut->permissionRequired( 'hiderevision' );
 329+ return;
 330+ }
 331+ $UserAllowed = false;
 332+ }
 333+ // Inject history info
 334+ $wgOut->addHtml( $this->uploadLine( $file ) );
 335+ $bitfields |= $file->deleted;
 336+ }
 337+ // Archived files...
 338+ } else {
 339+ // Run through and pull all our data in one query
 340+ foreach( $this->afiles as $id ) {
 341+ $where[] = intval($id);
 342+ }
 343+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
 344+ $result = $dbr->select( 'filearchive', '*',
 345+ array( 'fa_name' => $this->page->getDbKey(),
 346+ $whereClause ),
 347+ __METHOD__ );
 348+ while( $row = $dbr->fetchObject( $result ) ) {
 349+ $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
 350+ }
 351+
 352+ foreach( $this->afiles as $fileid ) {
 353+ if( !isset($filesObjs[$fileid]) ) {
 354+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
 355+ return;
 356+ } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
 357+ // If a rev is hidden from sysops
 358+ if( $action != 'submit' ) {
 359+ $wgOut->permissionRequired( 'hiderevision' );
 360+ return;
 361+ }
 362+ $UserAllowed = false;
 363+ }
 364+ // Inject history info
 365+ $wgOut->addHtml( $this->uploadLine( $filesObjs[$fileid] ) );
 366+ $bitfields |= $filesObjs[$fileid]->deleted;
 367+ }
 368+ }
 369+ $wgOut->addHtml( "</ul>" );
 370+
 371+ $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
 372+ //Normal sysops can always see what they did, but can't always change it
 373+ if( !$UserAllowed ) return;
 374+
 375+ $items = array(
 376+ wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
 377+ wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
 378+ $hidden = array(
 379+ wfHidden( 'wpEditToken', $wgUser->editToken() ),
 380+ wfHidden( 'target', $this->page->getPrefixedText() ),
 381+ wfHidden( 'type', $this->deletetype ) );
 382+ if( $this->deletetype=='oldimage' ) {
 383+ foreach( $this->ofiles as $filename )
 384+ $hidden[] = wfHidden( 'oldimage[]', $filename );
 385+ } else {
 386+ foreach( $this->afiles as $fileid )
 387+ $hidden[] = wfHidden( 'fileid[]', $fileid );
 388+ }
 389+ $special = SpecialPage::getTitleFor( 'Revisiondelete' );
 390+ $wgOut->addHtml( wfElement( 'form', array(
 391+ 'method' => 'post',
 392+ 'action' => $special->getLocalUrl( 'action=submit' ) ),
 393+ null ) );
 394+
 395+ $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
 396+ // FIXME: all items checked for just one file are checked, even if not set for the others
 397+ foreach( $this->checks as $item ) {
 398+ list( $message, $name, $field ) = $item;
 399+ $wgOut->addHtml( '<div>' .
 400+ wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
 401+ '</div>' );
 402+ }
 403+ $wgOut->addHtml( '</fieldset>' );
 404+ foreach( $items as $item ) {
 405+ $wgOut->addHtml( '<p>' . $item . '</p>' );
 406+ }
 407+ foreach( $hidden as $item ) {
 408+ $wgOut->addHtml( $item );
 409+ }
 410+
 411+ $wgOut->addHtml( '</form>' );
 412+ }
 413+
 414+ /**
 415+ * This lets a user set restrictions for log items
 416+ * @param WebRequest $request
 417+ */
 418+ function showEvents( $request ) {
 419+ global $wgOut, $wgUser, $action;
 420+
 421+ $UserAllowed = true;
 422+ $wgOut->addWikiText( wfMsgExt( 'logdelete-selected', array('parsemag'), count($this->events) ) );
 423+
 424+ $bitfields = 0;
 425+ $wgOut->addHtml( "<ul>" );
 426+
 427+ $where = $logRows = array();
 428+ $dbr = wfGetDB( DB_SLAVE );
 429+ // Run through and pull all our data in one query
 430+ foreach( $this->events as $logid ) {
 431+ $where[] = intval($logid);
 432+ }
 433+ $whereClause = 'log_id IN(' . implode(',',$where) . ')';
 434+ $result = $dbr->select( 'logging', '*',
 435+ array( $whereClause ),
 436+ __METHOD__ );
 437+ while( $row = $dbr->fetchObject( $result ) ) {
 438+ $logRows[$row->log_id] = $row;
 439+ }
 440+ foreach( $this->events as $logid ) {
 441+ // Don't hide from oversight log!!!
 442+ if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='oversight' ) {
76443 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
77444 return;
 445+ } else if( !LogViewer::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
 446+ // If an event is hidden from sysops
 447+ if( $action != 'submit') {
 448+ $wgOut->permissionRequired( 'hiderevision' );
 449+ return;
 450+ }
 451+ $UserAllowed = false;
78452 }
79 - $wgOut->addHtml( $this->historyLine( $rev ) );
80 - $bitfields[] = $rev->mDeleted; // FIXME
 453+ $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
 454+ $bitfields |= $logRows[$logid]->log_deleted;
81455 }
82456 $wgOut->addHtml( "</ul>" );
83 -
84 - $wgOut->addWikiText( wfMsg( 'revdelete-text' ) );
 457+
 458+ $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
 459+ //Normal sysops can always see what they did, but can't always change it
 460+ if( !$UserAllowed ) return;
85461
86462 $items = array(
87 - wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
88 - wfSubmitButton( wfMsg( 'revdelete-submit' ) ) );
 463+ wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
 464+ wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
89465 $hidden = array(
90466 wfHidden( 'wpEditToken', $wgUser->editToken() ),
91 - wfHidden( 'target', $this->page->getPrefixedText() ) );
92 - foreach( $this->revisions as $revid ) {
93 - $hidden[] = wfHidden( 'oldid[]', $revid );
 467+ wfHidden( 'type', $this->deletetype ) );
 468+ foreach( $this->events as $logid ) {
 469+ $hidden[] = wfHidden( 'logid[]', $logid );
94470 }
95471
96472 $special = SpecialPage::getTitleFor( 'Revisiondelete' );
@@ -99,10 +475,11 @@
100476 null ) );
101477
102478 $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
 479+ // FIXME: all items checked for just on event are checked, even if not set for the others
103480 foreach( $this->checks as $item ) {
104481 list( $message, $name, $field ) = $item;
105482 $wgOut->addHtml( '<div>' .
106 - wfCheckLabel( wfMsg( $message), $name, $name, $rev->isDeleted( $field ) ) .
 483+ wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
107484 '</div>' );
108485 }
109486 $wgOut->addHtml( '</fieldset>' );
@@ -123,32 +500,211 @@
124501 function historyLine( $rev ) {
125502 global $wgContLang;
126503 $date = $wgContLang->timeanddate( $rev->getTimestamp() );
 504+
 505+ $difflink=''; $del = '';
 506+ // Live revisions
 507+ if( $this->deletetype=='oldid' ) {
 508+ $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
 509+ 'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
 510+ $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
 511+ } else {
 512+ // Archived revisions
 513+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
 514+ $target = $this->page->getPrefixedText();
 515+ $revlink = $this->skin->makeLinkObj( $undelete, $date, "target=$target&timestamp=" . $rev->getTimestamp() );
 516+ }
 517+
 518+ if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
 519+ $revlink = '<span class="history-deleted">'.$revlink.'</span>';
 520+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
 521+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
 522+ $revlink = '<span class="history-deleted">'.$date.'</span>';
 523+ }
 524+ }
 525+
127526 return
128 - "<li>" .
129 - $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ) .
130 - " " .
131 - $this->skin->revUserLink( $rev ) .
132 - " " .
133 - $this->skin->revComment( $rev ) .
134 - "</li>";
 527+ "<li> $difflink $revlink " . $this->skin->revUserLink( $rev ) . " " . $this->skin->revComment( $rev ) . "$del</li>";
135528 }
136529
137530 /**
 531+ * @param File $file
 532+ * This can work for old or archived revisions
 533+ * @returns string
 534+ */
 535+ function uploadLine( $file ) {
 536+ global $wgContLang, $wgTitle;
 537+
 538+ $target = $this->page->getPrefixedText();
 539+ $date = $wgContLang->timeanddate( $file->timestamp, true );
 540+
 541+ $del = '';
 542+ // Special:Undelete for viewing archived images
 543+ if( $this->deletetype=='fileid' ) {
 544+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
 545+ $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file=$file->key" );
 546+ // Revisiondelete for viewing images
 547+ } else {
 548+ # Hidden files...
 549+ if( $file->isDeleted(File::DELETED_FILE) ) {
 550+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
 551+ if( !$file->userCan(File::DELETED_FILE) ) {
 552+ $pageLink = $date;
 553+ } else {
 554+ $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
 555+ "target=$target&file=$file->sha1.".$file->getExtension() );
 556+ }
 557+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
 558+ # Regular files...
 559+ } else {
 560+ $url = $file->getUrlRel();
 561+ $pageLink = "<a href=\"{$url}\">{$date}</a>";
 562+ }
 563+ }
 564+
 565+ $data = wfMsgHtml( 'widthheight',
 566+ $wgContLang->formatNum( $file->width ),
 567+ $wgContLang->formatNum( $file->height ) ) .
 568+ ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->size ) ) . ')';
 569+
 570+ return "<li> $pageLink " . $this->fileUserLink( $file ) . " $data " . $this->fileComment( $file ) . "$del</li>";
 571+ }
 572+
 573+ /**
 574+ * @param Array $event row
 575+ * @returns string
 576+ */
 577+ function logLine( $event ) {
 578+ global $wgContLang;
 579+
 580+ $date = $wgContLang->timeanddate( $event->log_timestamp );
 581+ $paramArray = LogPage::extractParams( $event->log_params );
 582+
 583+ if( !LogViewer::userCan($event,LogViewer::DELETED_ACTION) ) {
 584+ $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 585+ } else {
 586+ $title = Title::makeTitle( $event->log_namespace, $event->log_title );
 587+ $action = LogPage::actionText( $event->log_type, $event->log_action, $title, $this->skin, $paramArray, true, true );
 588+ if( $event->log_deleted & LogViewer::DELETED_ACTION )
 589+ $action = '<span class="history-deleted">' . $action . '</span>';
 590+ }
 591+ return
 592+ "<li>$date" . " " . $this->skin->logUserLink( $event ) . " $action " . $this->skin->logComment( $event ) . "</li>";
 593+ }
 594+
 595+ /**
 596+ * Generate a user link if the current user is allowed to view it
 597+ * @param ArchivedFile $file
 598+ * @param $isPublic, bool, show only if all users can see it
 599+ * @return string HTML
 600+ */
 601+ function fileUserLink( $file, $isPublic = false ) {
 602+ if( $file->isDeleted( File::DELETED_USER ) && $isPublic ) {
 603+ $link = wfMsgHtml( 'rev-deleted-user' );
 604+ } else if( $file->userCan( File::DELETED_USER ) ) {
 605+ $link = $this->skin->userLink( $file->user, $file->userText );
 606+ } else {
 607+ $link = wfMsgHtml( 'rev-deleted-user' );
 608+ }
 609+ if( $file->isDeleted( File::DELETED_USER ) ) {
 610+ return '<span class="history-deleted">' . $link . '</span>';
 611+ }
 612+ return $link;
 613+ }
 614+
 615+ /**
 616+ * Generate a user tool link cluster if the current user is allowed to view it
 617+ * @param ArchivedFile $file
 618+ * @param $isPublic, bool, show only if all users can see it
 619+ * @return string HTML
 620+ */
 621+ function fileUserTools( $file, $isPublic = false ) {
 622+ if( $file->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
 623+ $link = wfMsgHtml( 'rev-deleted-user' );
 624+ } else if( $file->userCan( Revision::DELETED_USER ) ) {
 625+ $link = $this->skin->userLink( $file->user, $file->userText ) .
 626+ $this->userToolLinks( $file->user, $file->userText );
 627+ } else {
 628+ $link = wfMsgHtml( 'rev-deleted-user' );
 629+ }
 630+ if( $file->isDeleted( Revision::DELETED_USER ) ) {
 631+ return '<span class="history-deleted">' . $link . '</span>';
 632+ }
 633+ return $link;
 634+ }
 635+
 636+ /**
 637+ * Wrap and format the given file's comment block, if the current
 638+ * user is allowed to view it.
 639+ *
 640+ * @param ArchivedFile $file
 641+ * @return string HTML
 642+ */
 643+ function fileComment( $file, $isPublic = false ) {
 644+ if( $file->isDeleted( File::DELETED_COMMENT ) && $isPublic ) {
 645+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
 646+ } else if( $file->userCan( File::DELETED_COMMENT ) ) {
 647+ $block = $this->skin->commentBlock( $file->description );
 648+ } else {
 649+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
 650+ }
 651+ if( $file->isDeleted( File::DELETED_COMMENT ) ) {
 652+ return "<span class=\"history-deleted\">$block</span>";
 653+ }
 654+ return $block;
 655+ }
 656+
 657+ /**
138658 * @param WebRequest $request
139659 */
140660 function submit( $request ) {
141661 $bitfield = $this->extractBitfield( $request );
142662 $comment = $request->getText( 'wpReason' );
143 - if( $this->save( $bitfield, $comment ) ) {
144 - return $this->success( $request );
145 - } else {
146 - return $this->show( $request );
 663+
 664+ $this->target = $request->getText( 'target' );
 665+ $this->title = Title::newFromURL( $this->target );
 666+
 667+ if( $this->save( $bitfield, $comment, $this->title ) ) {
 668+ $this->success( $request );
 669+ } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
 670+ return $this->showRevs( $request );
 671+ } else if( $request->getCheck( 'logid' ) ) {
 672+ return $this->showLogs( $request );
 673+ } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
 674+ return $this->showImages( $request );
147675 }
148676 }
149677
150678 function success( $request ) {
151679 global $wgOut;
152 - $wgOut->addWikiText( 'woo' );
 680+
 681+ $wgOut->setPagetitle( wfMsgHtml( 'actioncomplete' ) );
 682+ # Give a link to the log for this page
 683+ $logtitle = SpecialPage::getTitleFor( 'Log' );
 684+ $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
 685+ wfArrayToCGI( array('page' => $this->target ) ) );
 686+ # Give a link to the page history
 687+ $histlink = $this->skin->makeKnownLinkObj( $this->title, wfMsgHtml( 'revhistory' ),
 688+ wfArrayToCGI( array('action' => 'history' ) ) );
 689+ # Link to deleted edits
 690+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
 691+ $dellink = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'undeleterevs' ),
 692+ wfArrayToCGI( array('target' => $this->target) ) );
 693+ # Logs themselves don't have histories or archived revisions
 694+ if( !is_null($this->title) && $this->title->getNamespace() > -1)
 695+ $wgOut->setSubtitle( '<p>'.$histlink.' / '.$loglink.' / '.$dellink.'</p>' );
 696+
 697+ if( $this->deletetype=='logid' ) {
 698+ $wgOut->addWikiText( wfMsgHtml('logdelete-success'), false );
 699+ $this->showEvents( $request );
 700+ } else if( $this->deletetype=='oldid' || $this->deletetype=='artimestamp' ) {
 701+ $wgOut->addWikiText( wfMsgHtml('revdelete-success'), false );
 702+ $this->showRevs( $request );
 703+ } else if( $this->deletetype=='fileid' ) {
 704+ $wgOut->addWikiText( wfMsgHtml('revdelete-success'), false );
 705+ $this->showImages( $request );
 706+ } else if( $this->deletetype=='oldimage' ) {
 707+ $this->showImages( $request );
 708+ }
153709 }
154710
155711 /**
@@ -167,10 +723,26 @@
168724 return $bitfield;
169725 }
170726
171 - function save( $bitfield, $reason ) {
 727+ function save( $bitfield, $reason, $title ) {
172728 $dbw = wfGetDB( DB_MASTER );
 729+
 730+ // Don't allow simply locking the interface for no reason
 731+ if( $bitfield == Revision::DELETED_RESTRICTED )
 732+ $bitfield = 0;
 733+
173734 $deleter = new RevisionDeleter( $dbw );
174 - $deleter->setVisibility( $this->revisions, $bitfield, $reason );
 735+ // By this point, only one of the below should be set
 736+ if( isset($this->revisions) ) {
 737+ return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
 738+ } else if( isset($this->archrevs) ) {
 739+ return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
 740+ } else if( isset($this->ofiles) ) {
 741+ return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
 742+ } else if( isset($this->afiles) ) {
 743+ return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
 744+ } else if( isset($this->events) ) {
 745+ return $deleter->setEventVisibility( $this->events, $bitfield, $reason );
 746+ }
175747 }
176748 }
177749
@@ -180,42 +752,510 @@
181753 */
182754 class RevisionDeleter {
183755 function __construct( $db ) {
184 - $this->db = $db;
 756+ $this->dbw = $db;
185757 }
186758
187759 /**
 760+ * @param $title, the page these events apply to
188761 * @param array $items list of revision ID numbers
189762 * @param int $bitfield new rev_deleted value
190763 * @param string $comment Comment for log records
191764 */
192 - function setVisibility( $items, $bitfield, $comment ) {
193 - $pages = array();
 765+ function setRevVisibility( $title, $items, $bitfield, $comment ) {
 766+ global $wgOut;
194767
 768+ $userAllowedAll = $success = true;
 769+ $revIDs = array();
 770+ $revCount = 0;
 771+ // Run through and pull all our data in one query
 772+ foreach( $items as $revid ) {
 773+ $where[] = intval($revid);
 774+ }
 775+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
 776+ $result = $this->dbw->select( 'revision', '*',
 777+ array( 'rev_page' => $title->getArticleID(),
 778+ $whereClause ),
 779+ __METHOD__ );
 780+ while( $row = $this->dbw->fetchObject( $result ) ) {
 781+ $revObjs[$row->rev_id] = new Revision( $row );
 782+ }
195783 // To work!
196784 foreach( $items as $revid ) {
197 - $rev = Revision::newFromId( $revid );
198 - if( !isset( $rev ) ) {
199 - return false;
 785+ if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
 786+ $success = false;
 787+ continue; // Must exist
 788+ } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
 789+ $userAllowedAll=false;
 790+ continue;
200791 }
201 - $this->updateRevision( $rev, $bitfield );
202 - $this->updateRecentChanges( $rev, $bitfield );
 792+ // For logging, maintain a count of revisions
 793+ if( $revObjs[$revid]->mDeleted != $bitfield ) {
 794+ $revCount++;
 795+ $revIDs[]=$revid;
 796+
 797+ $this->updateRevision( $revObjs[$revid], $bitfield );
 798+ $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
 799+ }
 800+ }
 801+ // Clear caches...
 802+ // Don't log or touch if nothing changed
 803+ if( $revCount > 0 ) {
 804+ $this->updatePage( $title );
 805+ $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
 806+ $comment, $title, 'oldid', $revIDs );
 807+ }
 808+ // Where all revs allowed to be set?
 809+ if( !$userAllowedAll ) {
 810+ //FIXME: still might be confusing???
 811+ $wgOut->permissionRequired( 'hiderevision' );
 812+ return false;
 813+ }
 814+
 815+ return $success;
 816+ }
 817+
 818+ /**
 819+ * @param $title, the page these events apply to
 820+ * @param array $items list of revision ID numbers
 821+ * @param int $bitfield new rev_deleted value
 822+ * @param string $comment Comment for log records
 823+ */
 824+ function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
 825+ global $wgOut;
 826+
 827+ $userAllowedAll = $success = true;
 828+ $count = 0;
 829+ $Id_set = array();
 830+ // Run through and pull all our data in one query
 831+ foreach( $items as $timestamp ) {
 832+ $where[] = $this->dbw->addQuotes( $timestamp );
 833+ }
 834+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
 835+ $result = $this->dbw->select( 'archive', '*',
 836+ array( 'ar_namespace' => $title->getNamespace(),
 837+ 'ar_title' => $title->getDBKey(),
 838+ $whereClause ),
 839+ __METHOD__ );
 840+ while( $row = $this->dbw->fetchObject( $result ) ) {
 841+ $revObjs[$row->ar_timestamp] = new Revision( array(
 842+ 'page' => $title->getArticleId(),
 843+ 'id' => $row->ar_rev_id,
 844+ 'text' => $row->ar_text_id,
 845+ 'comment' => $row->ar_comment,
 846+ 'user' => $row->ar_user,
 847+ 'user_text' => $row->ar_user_text,
 848+ 'timestamp' => $row->ar_timestamp,
 849+ 'minor_edit' => $row->ar_minor_edit,
 850+ 'text_id' => $row->ar_text_id,
 851+ 'deleted' => $row->ar_deleted,
 852+ 'len' => $row->ar_len) );
 853+ }
 854+ // To work!
 855+ foreach( $items as $timestamp ) {
 856+ // This will only select the first revision with this timestamp.
 857+ // Since they are all selected/deleted at once, we can just check the
 858+ // permissions of one. UPDATE is done via timestamp, so all revs are set.
 859+ if( !is_object($revObjs[$timestamp]) ) {
 860+ $success = false;
 861+ continue; // Must exist
 862+ } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
 863+ $userAllowedAll=false;
 864+ continue;
 865+ }
 866+ // Which revisions did we change anything about?
 867+ if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
 868+ $Id_set[]=$timestamp;
 869+ $count++;
 870+
 871+ $this->updateArchive( $revObjs[$timestamp], $bitfield );
 872+ }
 873+ }
 874+ // For logging, maintain a count of revisions
 875+ if( $count > 0 ) {
 876+ $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
 877+ $comment, $title, 'artimestamp', $Id_set );
 878+ }
 879+ // Where all revs allowed to be set?
 880+ if( !$userAllowedAll ) {
 881+ $wgOut->permissionRequired( 'hiderevision' );
 882+ return false;
 883+ }
 884+
 885+ return $success;
 886+ }
 887+
 888+ /**
 889+ * @param $title, the page these events apply to
 890+ * @param array $items list of revision ID numbers
 891+ * @param int $bitfield new rev_deleted value
 892+ * @param string $comment Comment for log records
 893+ */
 894+ function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
 895+ global $wgOut;
 896+
 897+ $userAllowedAll = $success = true;
 898+ $count = 0;
 899+ $set = array();
 900+ // Run through and pull all our data in one query
 901+ foreach( $items as $timestamp ) {
 902+ $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
 903+ }
 904+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
 905+ $result = $this->dbw->select( 'oldimage', '*',
 906+ array( 'oi_name' => $title->getDbKey(),
 907+ $whereClause ),
 908+ __METHOD__ );
 909+ while( $row = $this->dbw->fetchObject( $result ) ) {
 910+ $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
 911+ $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
 912+ $filesObjs[$row->oi_archive_name]->userText = $row->oi_user_text;
 913+ }
 914+ // To work!
 915+ foreach( $items as $timestamp ) {
 916+ $archivename = $timestamp.'!'.$title->getDbKey();
 917+ if( !isset($filesObjs[$archivename]) ) {
 918+ $success = false;
 919+ continue; // Must exist
 920+ } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
 921+ $userAllowedAll=false;
 922+ continue;
 923+ }
203924
204 - // For logging, maintain a count of revisions per page
205 - $pageid = $rev->getPage();
206 - if( isset( $pages[$pageid] ) ) {
207 - $pages[$pageid]++;
 925+ $transaction = true;
 926+ // Which revisions did we change anything about?
 927+ if( $filesObjs[$archivename]->deleted != $bitfield ) {
 928+ $count++;
 929+
 930+ $this->dbw->begin();
 931+ $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
 932+ // If this image is currently hidden...
 933+ if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
 934+ if( $bitfield & File::DELETED_FILE ) {
 935+ # Leave it alone if we are not changing this...
 936+ $set[]=$name;
 937+ $transaction = true;
 938+ } else {
 939+ # We are moving this out
 940+ $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
 941+ $set[]=$transaction;
 942+ }
 943+ // Is it just now becoming hidden?
 944+ } else if( $bitfield & File::DELETED_FILE ) {
 945+ $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
 946+ $set[]=$transaction;
 947+ } else {
 948+ $set[]=$name;
 949+ }
 950+ // If our file operations fail, then revert back the db
 951+ if( $transaction==false ) {
 952+ $this->dbw->rollback();
 953+ return false;
 954+ }
 955+ $this->dbw->commit();
 956+ // Purge page/history
 957+ $filesObjs[$archivename]->purgeCache();
 958+ $filesObjs[$archivename]->purgeHistory();
 959+ // Invalidate cache for all pages using this file
 960+ $update = new HTMLCacheUpdate( $oimage->getTitle(), 'imagelinks' );
 961+ $update->doUpdate();
 962+ }
 963+ }
 964+
 965+ // Log if something was changed
 966+ if( $count > 0 ) {
 967+ $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
 968+ $comment, $title, 'oldimage', $set );
 969+ }
 970+ // Where all revs allowed to be set?
 971+ if( !$userAllowedAll ) {
 972+ $wgOut->permissionRequired( 'hiderevision' );
 973+ return false;
 974+ }
 975+
 976+ return $success;
 977+ }
 978+
 979+ /**
 980+ * @param $title, the page these events apply to
 981+ * @param array $items list of revision ID numbers
 982+ * @param int $bitfield new rev_deleted value
 983+ * @param string $comment Comment for log records
 984+ */
 985+ function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
 986+ global $wgOut;
 987+
 988+ $userAllowedAll = $success = true;
 989+ $count = 0;
 990+ $Id_set = array();
 991+
 992+ // Run through and pull all our data in one query
 993+ foreach( $items as $id ) {
 994+ $where[] = intval($id);
 995+ }
 996+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
 997+ $result = $this->dbw->select( 'filearchive', '*',
 998+ array( 'fa_name' => $title->getDbKey(),
 999+ $whereClause ),
 1000+ __METHOD__ );
 1001+ while( $row = $this->dbw->fetchObject( $result ) ) {
 1002+ $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
 1003+ }
 1004+ // To work!
 1005+ foreach( $items as $fileid ) {
 1006+ if( !isset($filesObjs[$fileid]) ) {
 1007+ $success = false;
 1008+ continue; // Must exist
 1009+ } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
 1010+ $userAllowedAll=false;
 1011+ continue;
 1012+ }
 1013+ // Which revisions did we change anything about?
 1014+ if( $filesObjs[$fileid]->deleted != $bitfield ) {
 1015+ $Id_set[]=$fileid;
 1016+ $count++;
 1017+
 1018+ $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
 1019+ }
 1020+ }
 1021+ // Log if something was changed
 1022+ if( $count > 0 ) {
 1023+ $this->updateLog( $title, $count, $bitfield, $comment,
 1024+ $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
 1025+ }
 1026+ // Where all revs allowed to be set?
 1027+ if( !$userAllowedAll ) {
 1028+ $wgOut->permissionRequired( 'hiderevision' );
 1029+ return false;
 1030+ }
 1031+
 1032+ return $success;
 1033+ }
 1034+
 1035+ /**
 1036+ * @param $title, the page these events apply to
 1037+ * @param array $items list of log ID numbers
 1038+ * @param int $bitfield new log_deleted value
 1039+ * @param string $comment Comment for log records
 1040+ */
 1041+ function setEventVisibility( $items, $bitfield, $comment ) {
 1042+ global $wgOut;
 1043+
 1044+ $userAllowedAll = $success = true;
 1045+ $logs_count = array();
 1046+ $logs_Ids = array();
 1047+
 1048+ // Run through and pull all our data in one query
 1049+ foreach( $items as $logid ) {
 1050+ $where[] = intval($logid);
 1051+ }
 1052+ $whereClause = 'log_id IN(' . implode(',',$where) . ')';
 1053+ $result = $this->dbw->select( 'logging', '*',
 1054+ array( $whereClause ),
 1055+ __METHOD__ );
 1056+ while( $row = $this->dbw->fetchObject( $result ) ) {
 1057+ $logRows[$row->log_id] = $row;
 1058+ }
 1059+ // To work!
 1060+ foreach( $items as $logid ) {
 1061+ if( !isset($logRows[$logid]) ) {
 1062+ $success = false;
 1063+ continue; // Must exist
 1064+ } else if( !LogViewer::userCan($logRows[$logid], Revision::DELETED_RESTRICTED)
 1065+ || $logRows[$logid]->log_type=='oversight' ) {
 1066+ // Don't hide from oversight log!!!
 1067+ $userAllowedAll=false;
 1068+ continue;
 1069+ }
 1070+ $logtype = $logRows[$logid]->log_type;
 1071+ // For logging, maintain a count of events per log type
 1072+ if( !isset( $logs_count[$logtype] ) ) {
 1073+ $logs_count[$logtype]=0;
 1074+ $logs_Ids[$logtype]=array();
 1075+ }
 1076+ // Which logs did we change anything about?
 1077+ if( $logRows[$logid]->log_deleted != $bitfield ) {
 1078+ $logs_Ids[$logtype][]=$logid;
 1079+ $logs_count[$logtype]++;
 1080+
 1081+ $this->updateLogs( $logRows[$logid], $bitfield );
 1082+ $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
 1083+ }
 1084+ }
 1085+ foreach( $logs_count as $logtype => $count ) {
 1086+ //Don't log or touch if nothing changed
 1087+ if( $count > 0 ) {
 1088+ $target = SpecialPage::getTitleFor( 'Log', $logtype );
 1089+ $this->updateLog( $target, $count, $bitfield, $logRows[$logid]->log_deleted,
 1090+ $comment, $target, 'logid', $logs_Ids[$logtype] );
 1091+ }
 1092+ }
 1093+ // Where all revs allowed to be set?
 1094+ if( !$userAllowedAll ) {
 1095+ $wgOut->permissionRequired( 'hiderevision' );
 1096+ return false;
 1097+ }
 1098+
 1099+ return $success;
 1100+ }
 1101+
 1102+ /**
 1103+ * Moves an image to a safe private location
 1104+ * Caller is responsible for clearing caches
 1105+ * @param File $oimage
 1106+ * @returns string, timestamp on success, false on failure
 1107+ */
 1108+ function makeOldImagePrivate( $oimage ) {
 1109+ global $wgFileStore, $wgUseSquid;
 1110+
 1111+ $transaction = new FSTransaction();
 1112+ if( !FileStore::lock() ) {
 1113+ wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
 1114+ return false;
 1115+ }
 1116+ $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
 1117+ // Dupe the file into the file store
 1118+ if( file_exists( $oldpath ) ) {
 1119+ // Is our directory configured?
 1120+ if( $store = FileStore::get( 'hidden' ) ) {
 1121+ if( !$oimage->sha1 )
 1122+ $oimage->upgradeRow();
 1123+
 1124+ $key = $oimage->sha1.'.'.$oimage->getExtension();
 1125+ $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
2081126 } else {
209 - $pages[$pageid] = 1;
 1127+ $group = null;
 1128+ $key = null;
 1129+ $transaction = false; // Return an error and do nothing
2101130 }
 1131+ } else {
 1132+ wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
 1133+ $group = null;
 1134+ $key = '';
 1135+ $transaction = new FSTransaction(); // empty
2111136 }
 1137+
 1138+ if( $transaction === false ) {
 1139+ // Fail to restore?
 1140+ wfDebug( __METHOD__.": import to file store failed, aborting\n" );
 1141+ throw new MWException( "Could not archive and delete file $oldpath" );
 1142+ return false;
 1143+ }
2121144
213 - // Clear caches...
214 - foreach( $pages as $pageid => $count ) {
215 - $title = Title::newFromId( $pageid );
216 - $this->updatePage( $title );
217 - $this->updateLog( $title, $count, $bitfield, $comment );
 1145+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
 1146+ $transaction->commit();
 1147+ FileStore::unlock();
 1148+
 1149+ $m = explode('!',$oimage->archive_name,2);
 1150+ $timestamp = $m[0];
 1151+
 1152+ return $timestamp;
 1153+ }
 1154+
 1155+ /**
 1156+ * Moves an image from a safe private location
 1157+ * Caller is responsible for clearing caches
 1158+ * @param File $oimage
 1159+ * @returns string, timestamp on success, false on failure
 1160+ */
 1161+ function makeOldImagePublic( $oimage ) {
 1162+ global $wgFileStore;
 1163+
 1164+ $transaction = new FSTransaction();
 1165+ if( !FileStore::lock() ) {
 1166+ wfDebug( __METHOD__." could not acquire filestore lock\n" );
 1167+ return false;
2181168 }
2191169
 1170+ $store = FileStore::get( 'hidden' );
 1171+ if( !$store ) {
 1172+ wfDebug( __METHOD__.": skipping row with no file.\n" );
 1173+ return false;
 1174+ }
 1175+
 1176+ $key = $oimage->sha1.'.'.$oimage->getExtension();
 1177+ $destDir = $oimage->getArchivePath();
 1178+ if( !is_dir( $destDir ) ) {
 1179+ wfMkdirParents( $destDir );
 1180+ }
 1181+ $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
 1182+ // Check if any other stored revisions use this file;
 1183+ // if so, we shouldn't remove the file from the hidden
 1184+ // archives so they will still work.
 1185+ $useCount = $this->dbw->selectField( 'oldimage','COUNT(*)',
 1186+ array( 'oi_sha1' => $oimage->sha1,
 1187+ 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
 1188+ __METHOD__ );
 1189+
 1190+ if( $useCount == 0 ) {
 1191+ wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
 1192+ $flags = FileStore::DELETE_ORIGINAL;
 1193+ } else {
 1194+ $flags = 0;
 1195+ }
 1196+ $transaction->add( $store->export( $key, $destPath, $flags ) );
 1197+
 1198+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
 1199+ $transaction->commit();
 1200+ FileStore::unlock();
 1201+
 1202+ $m = explode('!',$oimage->archive_name,2);
 1203+ $timestamp = $m[0];
 1204+
 1205+ return $timestamp;
 1206+ }
 1207+
 1208+ /**
 1209+ * Moves an image from a safe private location to deleted archives
 1210+ * Groups should be 'deleted' and 'hidden'
 1211+ * @param File $oimage
 1212+ * @param string $group1, old group
 1213+ * @param string $group2, new group
 1214+ * @returns bool, success
 1215+ */
 1216+ function moveImageFromFileRepos( $oimage, $group1, $group2 ) {
 1217+ global $wgFileStore;
 1218+
 1219+ $transaction = new FSTransaction();
 1220+ if( !FileStore::lock() ) {
 1221+ wfDebug( __METHOD__." could not acquire filestore lock\n" );
 1222+ return false;
 1223+ }
 1224+
 1225+ $storeOld = FileStore::get( $group1 );
 1226+ if( !$storeOld ) {
 1227+ wfDebug( __METHOD__.": skipping row with no file.\n" );
 1228+ return false;
 1229+ }
 1230+ $key = $oimage->sha1.'.'.$oimage->getExtension();
 1231+
 1232+ $oldPath = $storeOld->filePath( $key );
 1233+ // Check if any other stored revisions use this file;
 1234+ // if so, we shouldn't remove the file from the hidden
 1235+ // archives so they will still work.
 1236+ if( $group1=='hidden' ) {
 1237+ $useCount = $this->dbw->selectField( 'oldimage','COUNT(*)',
 1238+ array( 'oi_sha1' => $oimage->sha1 ),
 1239+ __METHOD__ );
 1240+ } else if( $group1=='deleted' ) {
 1241+ $useCount = $this->dbw->selectField( 'filearchive','COUNT(*)',
 1242+ array( 'fa_storage_key' => $key, 'fa_storage_group' => 'deleted' ),
 1243+ __METHOD__ );
 1244+ }
 1245+
 1246+ if( $useCount == 0 ) {
 1247+ wfDebug( __METHOD__.": nothing else using $key, will deleting after\n" );
 1248+ $flags = FileStore::DELETE_ORIGINAL;
 1249+ } else {
 1250+ $flags = 0;
 1251+ }
 1252+
 1253+ $storeNew = FileStore::get( $group2 );
 1254+ $transaction->add( $storeNew->insert( $key, $oldPath, $flags ) );
 1255+
 1256+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
 1257+ $transaction->commit();
 1258+ FileStore::unlock();
 1259+
2201260 return true;
2211261 }
2221262
@@ -225,29 +1265,87 @@
2261266 * @param int $bitfield new rev_deleted bitfield value
2271267 */
2281268 function updateRevision( $rev, $bitfield ) {
229 - $this->db->update( 'revision',
 1269+ $this->dbw->update( 'revision',
2301270 array( 'rev_deleted' => $bitfield ),
2311271 array( 'rev_id' => $rev->getId() ),
2321272 'RevisionDeleter::updateRevision' );
2331273 }
2341274
2351275 /**
 1276+ * Update the revision's rev_deleted field
 1277+ * @param Revision $rev
 1278+ * @param int $bitfield new rev_deleted bitfield value
 1279+ */
 1280+ function updateArchive( $rev, $bitfield ) {
 1281+ $this->dbw->update( 'archive',
 1282+ array( 'ar_deleted' => $bitfield ),
 1283+ array( 'ar_rev_id' => $rev->getId() ),
 1284+ 'RevisionDeleter::updateArchive' );
 1285+ }
 1286+
 1287+ /**
 1288+ * Update the images's oi_deleted field
 1289+ * @param File $oimage
 1290+ * @param int $bitfield new rev_deleted bitfield value
 1291+ */
 1292+ function updateOldFiles( $oimage, $bitfield ) {
 1293+ $this->dbw->update( 'oldimage',
 1294+ array( 'oi_deleted' => $bitfield ),
 1295+ array( 'oi_archive_name' => $oimage->archive_name ),
 1296+ 'RevisionDeleter::updateOldFiles' );
 1297+ }
 1298+
 1299+ /**
 1300+ * Update the images's fa_deleted field
 1301+ * @param ArchivedFile $file
 1302+ * @param int $bitfield new rev_deleted bitfield value
 1303+ */
 1304+ function updateArchFiles( $file, $bitfield ) {
 1305+ $this->dbw->update( 'filearchive',
 1306+ array( 'fa_deleted' => $bitfield ),
 1307+ array( 'fa_id' => $file->id ),
 1308+ 'RevisionDeleter::updateArchFiles' );
 1309+ }
 1310+
 1311+ /**
 1312+ * Update the logging log_deleted field
 1313+ * @param Row $event
 1314+ * @param int $bitfield new rev_deleted bitfield value
 1315+ */
 1316+ function updateLogs( $event, $bitfield ) {
 1317+ $this->dbw->update( 'logging',
 1318+ array( 'log_deleted' => $bitfield ),
 1319+ array( 'log_id' => $event->log_id ),
 1320+ 'RevisionDeleter::updateLogs' );
 1321+ }
 1322+
 1323+ /**
2361324 * Update the revision's recentchanges record if fields have been hidden
2371325 * @param Revision $rev
2381326 * @param int $bitfield new rev_deleted bitfield value
2391327 */
240 - function updateRecentChanges( $rev, $bitfield ) {
241 - $this->db->update( 'recentchanges',
242 - array(
243 - 'rc_user' => ($bitfield & Revision::DELETED_USER) ? 0 : $rev->getUser(),
244 - 'rc_user_text' => ($bitfield & Revision::DELETED_USER) ? wfMsg( 'rev-deleted-user' ) : $rev->getUserText(),
245 - 'rc_comment' => ($bitfield & Revision::DELETED_COMMENT) ? wfMsg( 'rev-deleted-comment' ) : $rev->getComment() ),
246 - array(
247 - 'rc_this_oldid' => $rev->getId() ),
248 - 'RevisionDeleter::updateRecentChanges' );
 1328+ function updateRecentChangesEdits( $rev, $bitfield ) {
 1329+ $this->dbw->update( 'recentchanges',
 1330+ array( 'rc_deleted' => $bitfield,
 1331+ 'rc_patrolled' => 1 ),
 1332+ array( 'rc_this_oldid' => $rev->getId() ),
 1333+ 'RevisionDeleter::updateRecentChangesEdits' );
2491334 }
2501335
2511336 /**
 1337+ * Update the revision's recentchanges record if fields have been hidden
 1338+ * @param Row $event
 1339+ * @param int $bitfield new rev_deleted bitfield value
 1340+ */
 1341+ function updateRecentChangesLog( $event, $bitfield ) {
 1342+ $this->dbw->update( 'recentchanges',
 1343+ array( 'rc_deleted' => $bitfield,
 1344+ 'rc_patrolled' => 1 ),
 1345+ array( 'rc_logid' => $event->log_id ),
 1346+ 'RevisionDeleter::updateRecentChangesLog' );
 1347+ }
 1348+
 1349+ /**
2521350 * Touch the page's cache invalidation timestamp; this forces cached
2531351 * history views to refresh, so any newly hidden or shown fields will
2541352 * update properly.
@@ -255,21 +1353,39 @@
2561354 */
2571355 function updatePage( $title ) {
2581356 $title->invalidateCache();
 1357+ $title->purgeSquid();
 1358+
 1359+ // Extensions that require referencing previous revisions may need this
 1360+ wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
2591361 }
2601362
2611363 /**
2621364 * Record a log entry on the action
263 - * @param Title $title
 1365+ * @param Title $title, page where item was removed from
2641366 * @param int $count the number of revisions altered for this page
265 - * @param int $bitfield the new rev_deleted value
 1367+ * @param int $nbitfield the new _deleted value
 1368+ * @param int $obitfield the old _deleted value
2661369 * @param string $comment
 1370+ * @param Title $target, the relevant page
 1371+ * @param string $param, URL param
 1372+ * @param Array $items
2671373 */
268 - function updateLog( $title, $count, $bitfield, $comment ) {
269 - $log = new LogPage( 'delete' );
270 - $reason = "changed $count revisions to $bitfield";
271 - $reason .= ": $comment";
272 - $log->addEntry( 'revision', $title, $reason );
 1374+ function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
 1375+ // Put things hidden from sysops in the oversight log
 1376+ $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'oversight' : 'delete';
 1377+ $log = new LogPage( $logtype );
 1378+ // FIXME: do this better
 1379+ if( $param=='logid' ) {
 1380+ $params = array( implode( ',', $items) );
 1381+ $reason = wfMsgExt('logdelete-logaction', array('parsemag'), $count, $nbitfield );
 1382+ if($comment) $reason .= ": $comment";
 1383+ $log->addEntry( 'event', $title, $reason, $params );
 1384+ } else {
 1385+ // Add params for effected page and ids
 1386+ $params = array( $target->getPrefixedText(), $param, implode( ',', $items) );
 1387+ $reason = wfMsgExt('revdelete-logaction', array('parsemag'), $count, $nbitfield );
 1388+ if($comment) $reason .= ": $comment";
 1389+ $log->addEntry( 'revision', $title, $reason, $params );
 1390+ }
2731391 }
2741392 }
275 -
276 -
Index: trunk/phase3/includes/Setup.php
@@ -57,8 +57,10 @@
5858 if ( empty( $wgFileStore['deleted']['directory'] ) ) {
5959 $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
6060 }
 61+if ( empty( $wgFileStore['hidden']['directory'] ) ) {
 62+ $wgFileStore['hidden']['directory'] = "{$wgUploadDirectory}/hidden";
 63+}
6164
62 -
6365 /**
6466 * Initialise $wgLocalFileRepo from backwards-compatible settings
6567 */
@@ -73,7 +75,9 @@
7476 'transformVia404' => !$wgGenerateThumbnailOnParse,
7577 'initialCapital' => $wgCapitalLinks,
7678 'deletedDir' => $wgFileStore['deleted']['directory'],
77 - 'deletedHashLevels' => $wgFileStore['deleted']['hash']
 79+ 'deletedHashLevels' => $wgFileStore['deleted']['hash'],
 80+ 'hiddenDir' => $wgFileStore['hidden']['directory'],
 81+ 'hiddenHashLevels' => $wgFileStore['hidden']['hash']
7882 );
7983 }
8084 /**
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -782,6 +782,8 @@
783783 'formerror' => 'Error: could not submit form',
784784 'badarticleerror' => 'This action cannot be performed on this page.',
785785 'cannotdelete' => 'Could not delete the page or file specified. (It may have already been deleted by someone else.)',
 786+'cannotdelete-merge' => 'Pages cannot be deleted if a different page already has archived revisions under the same title. This can
 787+happen if you move a page over another one and then delete it.',
786788 'badtitle' => 'Bad title',
787789 'badtitletext' => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.',
788790 'perfdisabled' => 'Sorry! This feature has been temporarily disabled because it slows the database down to the point that no one can use the wiki.',
@@ -1146,10 +1148,11 @@
11471149 </div>',
11481150 'rev-delundel' => 'show/hide',
11491151 'revisiondelete' => 'Delete/undelete revisions',
1150 -'revdelete-nooldid-title' => 'No target revision',
1151 -'revdelete-nooldid-text' => 'You have not specified target revision or revisions to perform this function on.',
1152 -'revdelete-selected' => "{{PLURAL:$2|Selected revision|Selected revisions}} of '''$1:'''",
1153 -'logdelete-selected' => "{{PLURAL:$2|Selected log event|Selected log events}} for '''$1:'''",
 1152+'revdelete-nooldid-title' => 'Invalid target revision',
 1153+'revdelete-nooldid-text' => 'You have either not specified a target revision(s) to perform this
 1154+function, the specified revision does not exist, or you are attempting to hide the current revision.',
 1155+'revdelete-selected' => "{{PLURAL:$2|Selected revision|Selected revisions}} of [[:$1]]:",
 1156+'logdelete-selected' => "{{PLURAL:$1|Selected log event|Selected log events}}:",
11541157 'revdelete-text' => 'Deleted revisions and events will still appear in the page history and logs,
11551158 but parts of their content will be inaccessible to the public.
11561159
@@ -1160,7 +1163,7 @@
11611164 'revdelete-hide-name' => 'Hide action and target',
11621165 'revdelete-hide-comment' => 'Hide edit comment',
11631166 'revdelete-hide-user' => "Hide editor's username/IP",
1164 -'revdelete-hide-restricted' => 'Apply these restrictions to sysops as well as others',
 1167+'revdelete-hide-restricted' => 'Apply these restrictions to Sysops and lock this interface',
11651168 'revdelete-suppress' => 'Suppress data from sysops as well as others',
11661169 'revdelete-hide-image' => 'Hide file content',
11671170 'revdelete-unsuppress' => 'Remove restrictions on restored revisions',
@@ -1169,15 +1172,44 @@
11701173 'revdelete-logentry' => 'changed revision visibility of [[$1]]',
11711174 'logdelete-logentry' => 'changed event visibility of [[$1]]',
11721175 'revdelete-logaction' => '$1 {{PLURAL:$1|revision|revisions}} set to mode $2',
1173 -'logdelete-logaction' => '$1 {{PLURAL:$1|event|events}} to [[$3]] set to mode $2',
1174 -'revdelete-success' => 'Revision visibility successfully set.',
1175 -'logdelete-success' => 'Event visibility successfully set.',
 1176+'logdelete-logaction' => '$1 {{PLURAL:$1|event|events}} set to mode $2',
 1177+'revdelete-success' => "'''Revision visibility successfully set.'''",
 1178+'logdelete-success' => "'''Log visibility successfully set.'''",
 1179+'revdel-restore' => 'Change visiblity',
11761180
11771181 # Oversight log
1178 -'oversightlog' => 'Oversight log',
1179 -'overlogpagetext' => 'Below is a list of the most recent deletions and blocks involving content
1180 -hidden from Sysops. See the [[Special:Ipblocklist|IP block list]] for the list of currently operational bans and blocks.',
 1182+'oversightlog' => 'Suppression log',
 1183+'overlogpagetext' => 'Below is a list of the most recent deletions and blocks involving items
 1184+hidden from Sysops. Automatically blocked IP addresses are not listed. See the [[Special:Ipblocklist|IP block list]]
 1185+for the list of currently operational bans and blocks.
11811186
 1187+Blocked users listed here can cannot edit their talk pages and thus can only communicate via email. Their accounts
 1188+will remain hidden only as long as they are blocked.',
 1189+
 1190+# History merging
 1191+'mergehistory' => 'Merge page histories',
 1192+'mergehistory-header' => 'This page lets you merge revisions of the history of one source page into a newer page.
 1193+Please make sure that this change will maintain historical page continuity.
 1194+
 1195+At least the current revision of the source page must be left.',
 1196+'mergehistory-box' => 'Merge revisions of two pages:',
 1197+'mergehistory-from' => 'Source page:',
 1198+'mergehistory-into' => 'Destination page:',
 1199+'mergehistory-list' => 'Mergeable edit history',
 1200+'mergehistory-merge' => 'The following revisions of [[:$1|$1]] can be merged into [[:$2|$2]]. Use the radio
 1201+button column to merge in only the revisions created at or before the specified time. Note that you will have to
 1202+reselect any options if you use the navigation links.',
 1203+'mergehistory-go' => 'Show mergeable edits',
 1204+'mergehistory-submit' => 'Merge revisions',
 1205+'mergehistory-empty' => 'No revisions can be merged',
 1206+'mergehistory-success' => '$3 revisions of [[:$1]] successfully merged into [[:$2]].',
 1207+'mergehistory-fail' => 'Unable to perform history merge, please recheck the page and time parameters.',
 1208+
 1209+'mergelog' => 'Merge log',
 1210+'pagemerge-logentry' => 'merged $1 into $2 (revisions up to $3)',
 1211+'revertmerge' => 'Unmerge',
 1212+'mergelogpagetext' => 'Below is a list of the most recent merges of one page history into another.',
 1213+
11821214 # Diffs
11831215 'history-title' => 'Revision history of "$1"',
11841216 'difference' => '(Difference between revisions)',
@@ -1322,6 +1354,11 @@
13231355 'grouppage-sysop' => '{{ns:project}}:Administrators',
13241356 'grouppage-bureaucrat' => '{{ns:project}}:Bureaucrats',
13251357
 1358+'oversight' => 'Oversight',
 1359+'group-oversight' => 'Oversights',
 1360+'group-oversight-member' => 'Oversight',
 1361+'grouppage-oversight' => '{{ns:project}}:Oversight',
 1362+
13261363 # User rights log
13271364 'rightslog' => 'User rights log',
13281365 'rightslogtext' => 'This is a log of changes to user rights.',
@@ -1665,6 +1702,7 @@
16661703 'specialpages-summary' => '', # only translate this message to other languages if you have to change it
16671704 'spheading' => 'Special pages for all users',
16681705 'restrictedpheading' => 'Restricted special pages',
 1706+'restrictedlheading' => 'Restricted logs',
16691707 'rclsub' => '(to pages linked from "$1")',
16701708 'newpages' => 'New pages',
16711709 'newpages-summary' => '', # only translate this message to other languages if you have to change it
@@ -1703,10 +1741,10 @@
17041742 'specialloguserlabel' => 'User:',
17051743 'speciallogtitlelabel' => 'Title:',
17061744 'log' => 'Logs',
1707 -'all-logs-page' => 'All logs',
 1745+'all-logs-page' => 'All public logs',
17081746 'log-search-legend' => 'Search for logs',
17091747 'log-search-submit' => 'Go',
1710 -'alllogstext' => 'Combined display of all available logs of {{SITENAME}}.
 1748+'alllogstext' => 'Combined display of all available public logs of {{SITENAME}}.
17111749 You can narrow down the view by selecting a log type, the user name, or the affected page.',
17121750 'logempty' => 'No matching items in log.',
17131751 'log-title-wildcard' => 'Search titles starting with this text',
@@ -1853,6 +1891,7 @@
18541892 'deletedtext' => '"$1" has been deleted.
18551893 See $2 for a record of recent deletions.',
18561894 'deletedarticle' => 'deleted "[[$1]]"',
 1895+'suppressedarticle' => 'suppressed "[[$1]]"',
18571896 'dellogpage' => 'Deletion log',
18581897 'dellogpagetext' => 'Below is a list of the most recent deletions.',
18591898 'deletionlog' => 'deletion log',
@@ -1873,6 +1912,7 @@
18741913 'sessionfailure' => 'There seems to be a problem with your login session;
18751914 this action has been canceled as a precaution against session hijacking.
18761915 Please hit "back" and reload the page you came from, then try again.',
 1916+
18771917 'protectlogpage' => 'Protection log',
18781918 'protectlogtext' => 'Below is a list of page locks and unlocks. See the [[Special:Protectedpages|protected pages list]] for the list of currently operational page protections.',
18791919 'protectedarticle' => 'protected "[[$1]]"',
@@ -1880,6 +1920,7 @@
18811921 'unprotectedarticle' => 'unprotected "[[$1]]"',
18821922 'protectsub' => '(Setting protection level for "$1")',
18831923 'confirmprotect' => 'Confirm protection',
 1924+'protect-fileonly' => 'Apply edit restrictions to file uploads only',
18841925 'protectcomment' => 'Comment:',
18851926 'protectexpiry' => 'Expires:',
18861927 'protect_expiry_invalid' => 'Expiry time is invalid.',
@@ -1910,6 +1951,7 @@
19111952 # Restrictions (nouns)
19121953 'restriction-edit' => 'Edit',
19131954 'restriction-move' => 'Move',
 1955+'restriction-upload' => 'Upload',
19141956
19151957 # Restriction levels
19161958 'restriction-level-sysop' => 'full protected',
@@ -1918,25 +1960,29 @@
19191961
19201962 # Undelete
19211963 'undelete' => 'View deleted pages',
 1964+'undeleterevs' => 'Deleted revisions',
19221965 'undeletepage' => 'View and restore deleted pages',
19231966 'viewdeletedpage' => 'View deleted pages',
 1967+'undeletepagetitle' => '\'\'\'The following consists of deleted revisions of [[:$1]]\'\'\'.',
19241968 'undeletepagetext' => 'The following pages have been deleted but are still in the archive and
19251969 can be restored. The archive may be periodically cleaned out.',
1926 -'undeleteextrahelp' => "To restore the entire page, leave all checkboxes deselected and
1927 -click '''''Restore'''''. To perform a selective restoration, check the boxes corresponding to the
1928 -revisions to be restored, and click '''''Restore'''''. Clicking '''''Reset''''' will clear the
1929 -comment field and all checkboxes.",
 1970+'undeleteextrahelp' => "To restore the entire page, leave all radios deselected and click '''''Restore'''''.
 1971+To perform a selective restoration, check the desired restore point below and click '''''Restore'''''.
 1972+Clicking '''''Reset''''' will reset this form. Note that you will have to reselect any options if you
 1973+use the navigation links.",
19301974 'undeleterevisions' => '$1 {{PLURAL:$1|revision|revisions}} archived',
19311975 'undeletehistory' => 'If you restore the page, all revisions will be restored to the history.
19321976 If a new page with the same name has been created since the deletion, the restored
19331977 revisions will appear in the prior history, and the current revision of the live page
1934 -will not be automatically replaced. Also note that restrictions on file revisions are lost upon restoration',
1935 -'undeleterevdel' => "Undeletion will not be performed if it will result in the top page revision being
1936 -partially deleted. In such cases, you must uncheck or unhide the newest deleted revisions. Revisions of files
1937 -that you don't have permission to view will not be restored.",
 1978+will not be automatically replaced.',
 1979+'undeleterevdel' => 'Undeletion will not be performed if either it would result in the top page/image revision
 1980+being restricted. Histories of different pages cannot be merged unless the live page is a redirect with no edit history.',
19381981 'undeletehistorynoadmin' => 'This article has been deleted. The reason for deletion is
19391982 shown in the summary below, along with details of the users who had edited this page
19401983 before deletion. The actual text of these deleted revisions is only available to administrators.',
 1984+'restorepoint' => 'Use the radio button column to restore only revisions from the specified time onwards.',
 1985+'restorenone' => '(select this button to restore none of these revisions)',
 1986+
19411987 'undelete-revision' => 'Deleted revision of $1 (as of $2) by $3:',
19421988 'undeleterevision-missing' => 'Invalid or missing revision. You may have a bad link, or the
19431989 revision may have been restored or removed from the archive.',

Follow-up revisions

RevisionCommit summaryAuthorDate
r26283Revert r26281 for the moment. Big patch, changes several existing practices. ...brion19:50, 1 October 2007
r26331Merged revisions 26280-26330 via svnmerge from...david22:28, 2 October 2007

Status & tagging log