00001 <?php
00018 class PageHistory {
00019 const DIR_PREV = 0;
00020 const DIR_NEXT = 1;
00021
00022 var $mArticle, $mTitle, $mSkin;
00023 var $lastdate;
00024 var $linesonpage;
00025 var $mLatestId = null;
00026
00027 private $mOldIdChecked = 0;
00028
00035 function __construct( $article ) {
00036 global $wgUser;
00037 $this->mArticle =& $article;
00038 $this->mTitle =& $article->mTitle;
00039 $this->mSkin = $wgUser->getSkin();
00040 $this->preCacheMessages();
00041 }
00042
00043 function getArticle() {
00044 return $this->mArticle;
00045 }
00046
00047 function getTitle() {
00048 return $this->mTitle;
00049 }
00050
00055 function preCacheMessages() {
00056
00057 if( !isset( $this->message ) ) {
00058 foreach( explode(' ', 'cur last rev-delundel' ) as $msg ) {
00059 $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
00060 }
00061 }
00062 }
00063
00069 function history() {
00070 global $wgOut, $wgRequest, $wgTitle, $wgScript;
00071
00072
00073
00074
00075 if( $wgOut->checkLastModified( $this->mArticle->getTouched() ) )
00076 return;
00077
00078 wfProfileIn( __METHOD__ );
00079
00080
00081
00082
00083 $wgOut->setPageTitle( wfMsg( 'history-title', $this->mTitle->getPrefixedText() ) );
00084 $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
00085 $wgOut->setArticleFlag( false );
00086 $wgOut->setArticleRelated( true );
00087 $wgOut->setRobotPolicy( 'noindex,nofollow' );
00088 $wgOut->setSyndicated( true );
00089 $wgOut->setFeedAppendQuery( 'action=history' );
00090 $wgOut->addScriptFile( 'history.js' );
00091
00092 $logPage = SpecialPage::getTitleFor( 'Log' );
00093 $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ),
00094 'page=' . $this->mTitle->getPrefixedUrl() );
00095 $wgOut->setSubtitle( $logLink );
00096
00097 $feedType = $wgRequest->getVal( 'feed' );
00098 if( $feedType ) {
00099 wfProfileOut( __METHOD__ );
00100 return $this->feed( $feedType );
00101 }
00102
00103
00104
00105
00106 if( !$this->mTitle->exists() ) {
00107 $wgOut->addWikiMsg( 'nohistory' );
00108 wfProfileOut( __METHOD__ );
00109 return;
00110 }
00111
00115 $year = $wgRequest->getInt( 'year' );
00116 $month = $wgRequest->getInt( 'month' );
00117 $tagFilter = $wgRequest->getVal( 'tagfilter' );
00118 $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
00119
00120 $action = htmlspecialchars( $wgScript );
00121 $wgOut->addHTML(
00122 "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" .
00123 Xml::fieldset( wfMsg( 'history-fieldset-title' ), false, array( 'id' => 'mw-history-search' ) ) .
00124 Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" .
00125 Xml::hidden( 'action', 'history' ) . "\n" .
00126 xml::dateMenu( $year, $month ) . ' ' .
00127 ( $tagSelector ? ( implode( ' ', $tagSelector ) . ' ' ) : '' ) .
00128 Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
00129 '</fieldset></form>'
00130 );
00131
00132 wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) );
00133
00137 $pager = new PageHistoryPager( $this, $year, $month, $tagFilter );
00138 $this->linesonpage = $pager->getNumRows();
00139 $wgOut->addHTML(
00140 $pager->getNavigationBar() .
00141 $this->beginHistoryList() .
00142 $pager->getBody() .
00143 $this->endHistoryList() .
00144 $pager->getNavigationBar()
00145 );
00146
00147 wfProfileOut( __METHOD__ );
00148 }
00149
00155 function beginHistoryList() {
00156 global $wgTitle, $wgScript, $wgEnableHtmlDiff;
00157 $this->lastdate = '';
00158 $s = wfMsgExt( 'histlegend', array( 'parse') );
00159 $s .= Xml::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-history-compare' ) );
00160 $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
00161 if( $wgEnableHtmlDiff ) {
00162 $s .= $this->submitButton( wfMsg( 'visualcomparison'),
00163 array(
00164 'name' => 'htmldiff',
00165 'class' => 'historysubmit',
00166 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
00167 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00168 )
00169 );
00170 $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
00171 array(
00172 'class' => 'historysubmit',
00173 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
00174 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00175 )
00176 );
00177 } else {
00178 $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
00179 array(
00180 'class' => 'historysubmit',
00181 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
00182 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00183 )
00184 );
00185 }
00186 $s .= '<ul id="pagehistory">' . "\n";
00187 return $s;
00188 }
00189
00195 function endHistoryList() {
00196 global $wgEnableHtmlDiff;
00197 $s = '</ul>';
00198 if( $wgEnableHtmlDiff ) {
00199 $s .= $this->submitButton( wfMsg( 'visualcomparison'),
00200 array(
00201 'name' => 'htmldiff',
00202 'class' => 'historysubmit',
00203 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
00204 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00205 )
00206 );
00207 $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
00208 array(
00209 'class' => 'historysubmit',
00210 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
00211 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00212 )
00213 );
00214 } else {
00215 $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
00216 array(
00217 'class' => 'historysubmit',
00218 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
00219 'title' => wfMsg( 'tooltip-compareselectedversions' ),
00220 )
00221 );
00222 }
00223 $s .= '</form>';
00224 return $s;
00225 }
00226
00233 function submitButton($message, $attributes = array() ) {
00234 # Disable submit button if history has 1 revision only
00235 if( $this->linesonpage > 1 ) {
00236 return Xml::submitButton( $message , $attributes );
00237 } else {
00238 return '';
00239 }
00240 }
00241
00255 function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
00256 global $wgUser, $wgLang;
00257 $rev = new Revision( $row );
00258 $rev->setTitle( $this->mTitle );
00259
00260 $curlink = $this->curLink( $rev, $latest );
00261 $lastlink = $this->lastLink( $rev, $next, $counter );
00262 $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
00263 $link = $this->revLink( $rev );
00264 $classes = array();
00265
00266 $s = "($curlink) ($lastlink) $arbitrary";
00267
00268 if( $wgUser->isAllowed( 'deleterevision' ) ) {
00269 if( $latest ) {
00270
00271 $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
00272 } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
00273
00274 $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
00275 } else {
00276 $query = array( 'target' => $this->mTitle->getPrefixedDbkey(),
00277 'oldid' => $rev->getId()
00278 );
00279 $del = $this->mSkin->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
00280 }
00281 $s .= " $del ";
00282 }
00283
00284 $s .= " $link";
00285 $s .= " <span class='history-user'>" . $this->mSkin->revUserTools( $rev, true ) . "</span>";
00286
00287 if( $rev->isMinor() ) {
00288 $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
00289 }
00290
00291 if( !is_null( $size = $rev->getSize() ) && !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
00292 $s .= ' ' . $this->mSkin->formatRevisionSize( $size );
00293 }
00294
00295 $s .= $this->mSkin->revComment( $rev, false, true );
00296
00297 if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) {
00298 $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
00299 }
00300 if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
00301 $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
00302 }
00303
00304 $tools = array();
00305
00306 if( !is_null( $next ) && is_object( $next ) ) {
00307 if( $latest && $this->mTitle->userCan( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) {
00308 $tools[] = '<span class="mw-rollback-link">'.$this->mSkin->buildRollbackLink( $rev ).'</span>';
00309 }
00310
00311 if( $this->mTitle->quickUserCan( 'edit' ) && !$rev->isDeleted( Revision::DELETED_TEXT ) &&
00312 !$next->rev_deleted & Revision::DELETED_TEXT )
00313 {
00314 # Create undo tooltip for the first (=latest) line only
00315 $undoTooltip = $latest
00316 ? array( 'title' => wfMsg( 'tooltip-undo' ) )
00317 : array();
00318 $undolink = $this->mSkin->link(
00319 $this->mTitle,
00320 wfMsgHtml( 'editundo' ),
00321 $undoTooltip,
00322 array( 'action' => 'edit', 'undoafter' => $next->rev_id, 'undo' => $rev->getId() ),
00323 array( 'known', 'noclasses' )
00324 );
00325 $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
00326 }
00327 }
00328
00329 if( $tools ) {
00330 $s .= ' (' . $wgLang->pipeList( $tools ) . ')';
00331 }
00332
00333 # Tags
00334 list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' );
00335 $classes = array_merge( $classes, $newClasses );
00336 $s .= " $tagSummary";
00337
00338 wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) );
00339
00340 $classes = implode( ' ', $classes );
00341
00342 return "<li class=\"$classes\">$s</li>\n";
00343 }
00344
00350 function revLink( $rev ) {
00351 global $wgLang;
00352 $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
00353 if( !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
00354 $link = $this->mSkin->makeKnownLinkObj( $this->mTitle, $date, "oldid=" . $rev->getId() );
00355 } else {
00356 $link = '<span class="history-deleted">' . $date . '</span>';
00357 }
00358 return $link;
00359 }
00360
00367 function curLink( $rev, $latest ) {
00368 $cur = $this->message['cur'];
00369 if( $latest || $rev->isDeleted( Revision::DELETED_TEXT ) ) {
00370 return $cur;
00371 } else {
00372 return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur,
00373 'diff=' . $this->mTitle->getLatestRevID() . "&oldid=" . $rev->getId() );
00374 }
00375 }
00376
00384 function lastLink( $prevRev, $next, $counter ) {
00385 $last = $this->message['last'];
00386 # $next may either be a Row, null, or "unkown"
00387 $nextRev = is_object($next) ? new Revision( $next ) : $next;
00388 if( is_null($next) ) {
00389 # Probably no next row
00390 return $last;
00391 } elseif( $next === 'unknown' ) {
00392 # Next row probably exists but is unknown, use an oldid=prev link
00393 return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
00394 "diff=" . $prevRev->getId() . "&oldid=prev" );
00395 } elseif( $prevRev->isDeleted(Revision::DELETED_TEXT) || $nextRev->isDeleted(Revision::DELETED_TEXT) ) {
00396 return $last;
00397 } else {
00398 return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
00399 "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" );
00400 }
00401 }
00402
00411 function diffButtons( $rev, $firstInList, $counter ) {
00412 if( $this->linesonpage > 1 ) {
00413 $radio = array( 'type' => 'radio', 'value' => $rev->getId() );
00415 if( $firstInList ) {
00416 $first = Xml::element( 'input',
00417 array_merge( $radio, array( 'style' => 'visibility:hidden', 'name' => 'oldid' ) )
00418 );
00419 $checkmark = array( 'checked' => 'checked' );
00420 } else {
00421 # Check visibility of old revisions
00422 if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
00423 $radio['disabled'] = 'disabled';
00424 $checkmark = array();
00425 } else if( $counter == 2 || !$this->mOldIdChecked ) {
00426 $checkmark = array( 'checked' => 'checked' );
00427 $this->mOldIdChecked = $rev->getId();
00428 } else {
00429 $checkmark = array();
00430 }
00431 $first = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'oldid' ) ) );
00432 $checkmark = array();
00433 }
00434 $second = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'diff' ) ) );
00435 return $first . $second;
00436 } else {
00437 return '';
00438 }
00439 }
00440
00446 function fetchRevisions($limit, $offset, $direction) {
00447 $dbr = wfGetDB( DB_SLAVE );
00448
00449 if( $direction == PageHistory::DIR_PREV )
00450 list($dirs, $oper) = array("ASC", ">=");
00451 else
00452 list($dirs, $oper) = array("DESC", "<=");
00453
00454 if( $offset )
00455 $offsets = array("rev_timestamp $oper '$offset'");
00456 else
00457 $offsets = array();
00458
00459 $page_id = $this->mTitle->getArticleID();
00460
00461 return $dbr->select( 'revision',
00462 Revision::selectFields(),
00463 array_merge(array("rev_page=$page_id"), $offsets),
00464 __METHOD__,
00465 array( 'ORDER BY' => "rev_timestamp $dirs",
00466 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
00467 );
00468 }
00469
00474 function feed( $type ) {
00475 global $wgFeedClasses, $wgRequest, $wgFeedLimit;
00476 if( !FeedUtils::checkFeedOutput($type) ) {
00477 return;
00478 }
00479
00480 $feed = new $wgFeedClasses[$type](
00481 $this->mTitle->getPrefixedText() . ' - ' .
00482 wfMsgForContent( 'history-feed-title' ),
00483 wfMsgForContent( 'history-feed-description' ),
00484 $this->mTitle->getFullUrl( 'action=history' ) );
00485
00486
00487
00488 $limit = $wgRequest->getInt( 'limit', 10 );
00489 if( $limit > $wgFeedLimit || $limit < 1 ) {
00490 $limit = 10;
00491 }
00492 $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT);
00493
00494 $feed->outHeader();
00495 if( $items ) {
00496 foreach( $items as $row ) {
00497 $feed->outItem( $this->feedItem( $row ) );
00498 }
00499 } else {
00500 $feed->outItem( $this->feedEmpty() );
00501 }
00502 $feed->outFooter();
00503 }
00504
00505 function feedEmpty() {
00506 global $wgOut;
00507 return new FeedItem(
00508 wfMsgForContent( 'nohistory' ),
00509 $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ),
00510 $this->mTitle->getFullUrl(),
00511 wfTimestamp( TS_MW ),
00512 '',
00513 $this->mTitle->getTalkPage()->getFullUrl() );
00514 }
00515
00524 function feedItem( $row ) {
00525 $rev = new Revision( $row );
00526 $rev->setTitle( $this->mTitle );
00527 $text = FeedUtils::formatDiffRow( $this->mTitle,
00528 $this->mTitle->getPreviousRevisionID( $rev->getId() ),
00529 $rev->getId(),
00530 $rev->getTimestamp(),
00531 $rev->getComment() );
00532
00533 if( $rev->getComment() == '' ) {
00534 global $wgContLang;
00535 $title = wfMsgForContent( 'history-feed-item-nocomment',
00536 $rev->getUserText(),
00537 $wgContLang->timeanddate( $rev->getTimestamp() ) );
00538 } else {
00539 $title = $rev->getUserText() . wfMsgForContent( 'colon-separator' ) . FeedItem::stripComment( $rev->getComment() );
00540 }
00541
00542 return new FeedItem(
00543 $title,
00544 $text,
00545 $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ),
00546 $rev->getTimestamp(),
00547 $rev->getUserText(),
00548 $this->mTitle->getTalkPage()->getFullUrl() );
00549 }
00550 }
00551
00552
00556 class PageHistoryPager extends ReverseChronologicalPager {
00557 public $mLastRow = false, $mPageHistory, $mTitle;
00558
00559 function __construct( $pageHistory, $year='', $month='', $tagFilter = '' ) {
00560 parent::__construct();
00561 $this->mPageHistory = $pageHistory;
00562 $this->mTitle =& $this->mPageHistory->mTitle;
00563 $this->tagFilter = $tagFilter;
00564 $this->getDateCond( $year, $month );
00565 }
00566
00567 function getQueryInfo() {
00568 $queryInfo = array(
00569 'tables' => array('revision'),
00570 'fields' => array_merge( Revision::selectFields(), array('ts_tags') ),
00571 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ),
00572 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ),
00573 'join_conds' => array( 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ),
00574 );
00575 ChangeTags::modifyDisplayQuery( $queryInfo['tables'],
00576 $queryInfo['fields'],
00577 $queryInfo['conds'],
00578 $queryInfo['join_conds'],
00579 $queryInfo['options'],
00580 $this->tagFilter );
00581 wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) );
00582 return $queryInfo;
00583 }
00584
00585 function getIndexField() {
00586 return 'rev_timestamp';
00587 }
00588
00589 function formatRow( $row ) {
00590 if( $this->mLastRow ) {
00591 $latest = $this->mCounter == 1 && $this->mIsFirst;
00592 $firstInList = $this->mCounter == 1;
00593 $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++,
00594 $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
00595 } else {
00596 $s = '';
00597 }
00598 $this->mLastRow = $row;
00599 return $s;
00600 }
00601
00602 function getStartBody() {
00603 $this->mLastRow = false;
00604 $this->mCounter = 1;
00605 return '';
00606 }
00607
00608 function getEndBody() {
00609 if( $this->mLastRow ) {
00610 $latest = $this->mCounter == 1 && $this->mIsFirst;
00611 $firstInList = $this->mCounter == 1;
00612 if( $this->mIsBackwards ) {
00613 # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
00614 if( $this->mOffset == '' ) {
00615 $next = null;
00616 } else {
00617 $next = 'unknown';
00618 }
00619 } else {
00620 # The next row is the past-the-end row
00621 $next = $this->mPastTheEndRow;
00622 }
00623 $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++,
00624 $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
00625 } else {
00626 $s = '';
00627 }
00628 return $s;
00629 }
00630 }