X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FPageHistory.php;h=b1cf41f0de08cfca59b99cd58509b965bde54e39;hb=72a4abe588a7511ed5a92a5e30f889c60d824c1a;hp=754a2d1d4ebe744d253f3b681ec520e8004b34b7;hpb=798270581d38271fa87b2744fa157f77f2d2db80;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/PageHistory.php b/includes/PageHistory.php index 754a2d1d4e..b1cf41f0de 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -1,231 +1,592 @@ history() to print the + * history. + * */ class PageHistory { + const DIR_PREV = 0; + const DIR_NEXT = 1; + var $mArticle, $mTitle, $mSkin; - var $lastline, $lastdate; + var $lastdate; var $linesonpage; - function PageHistory( $article ) { + var $mNotificationTimestamp; + var $mLatestId = null; + + /** + * Construct a new PageHistory. + * + * @param Article $article + * @returns nothing + */ + function __construct($article) { + global $wgUser; + $this->mArticle =& $article; $this->mTitle =& $article->mTitle; + $this->mNotificationTimestamp = NULL; + $this->mSkin = $wgUser->getSkin(); } - # This shares a lot of issues (and code) with Recent Changes - + /** + * Print the history page for an article. + * + * @returns nothing + */ function history() { - global $wgUser, $wgOut, $wgLang, $wgShowUpdatedMarker; + global $wgOut, $wgRequest, $wgTitle; - # If page hasn't changed, client can cache this + /* + * Allow client caching. + */ - if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) ){ - # Client cache fresh and headers sent, nothing more to do. + if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) ) + /* Client cache fresh and headers sent, nothing more to do. */ return; - } + $fname = 'PageHistory::history'; wfProfileIn( $fname ); - $wgOut->setPageTitle( $this->mTitle->getPRefixedText() ); - $wgOut->setSubtitle( wfMsg( 'revhistory' ) ); + /* + * Setup page variables. + */ + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSyndicated( true ); - if( $this->mTitle->getArticleID() == 0 ) { - $wgOut->addHTML( wfMsg( 'nohistory' ) ); - wfProfileOut( $fname ); - return; - } + $logPage = SpecialPage::getTitleFor( 'Log' ); + $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); - list( $limit, $offset ) = wfCheckLimits(); - - /* We have to draw the latest revision from 'cur' */ - $rawlimit = $limit; - $rawoffset = $offset - 1; - if( 0 == $offset ) { - $rawlimit--; - $rawoffset = 0; - } - /* Check one extra row to see whether we need to show 'next' and diff links */ - $limitplus = $rawlimit + 1; - - $namespace = $this->mTitle->getNamespace(); - $title = $this->mTitle->getText(); - $uid = $wgUser->getID(); - $db =& wfGetDB( DB_SLAVE ); - if ($wgShowUpdatedMarker && $wgUser->getOption( 'showupdated' )) { - $dbr =& wfGetDB( DB_MASTER ); - $row = $dbr->selectRow( 'watchlist', - array( 'wl_notificationtimestamp' ), - array( 'wl_namespace' => $namespace, 'wl_title' => $this->mTitle->getDBkey(), 'wl_user' => $wgUser->getID() ), - $fname ); - $notificationtimestamp = $row->wl_notificationtimestamp; - } else $notificationtimestamp = false ; - - $use_index = $db->useIndexClause( 'name_title_timestamp' ); - $oldtable = $db->tableName( 'old' ); - - $sql = "SELECT old_id,old_user," . - "old_comment,old_user_text,old_timestamp,old_minor_edit ". - "FROM $oldtable $use_index " . - "WHERE old_namespace={$namespace} AND " . - "old_title='" . $db->strencode( $this->mTitle->getDBkey() ) . "' " . - "ORDER BY inverse_timestamp".$db->limitResult($limitplus,$rawoffset); - $res = $db->query( $sql, $fname ); - - $revs = $db->numRows( $res ); - - if( $revs < $limitplus ) // the sql above tries to fetch one extra - $this->linesonpage = $revs; - else - $this->linesonpage = $revs - 1; + $subtitle = wfMsgHtml( 'revhistory' ) . '
' . $logLink; + $wgOut->setSubtitle( $subtitle ); - $atend = ($revs < $limitplus); + $feedType = $wgRequest->getVal( 'feed' ); + if( $feedType ) { + wfProfileOut( $fname ); + return $this->feed( $feedType ); + } - $this->mSkin = $wgUser->getSkin(); - $numbar = wfViewPrevNext( - $offset, $limit, - $this->mTitle->getPrefixedText(), - 'action=history', $atend ); - $s = $numbar; - if($this->linesonpage > 0) { - $submitpart1 = 'submitbuttonhtml1 = $submitpart1 . ' />'; - $this->submitbuttonhtml2 = $submitpart1 . ' id="historysubmit" />'; - } - $s .= $this->beginHistoryList(); - $counter = 1; - if( $offset == 0 ){ - $this->linesonpage++; - $s .= $this->historyLine( - $this->mArticle->getTimestamp(), - $this->mArticle->getUser(), - $this->mArticle->getUserText(), $namespace, - $title, 0, $this->mArticle->getComment(), - ( $this->mArticle->getMinorEdit() > 0 ), - $counter++, - $notificationtimestamp - ); + /* + * Fail if article doesn't exist. + */ + if( !$this->mTitle->exists() ) { + $wgOut->addWikiText( wfMsg( 'nohistory' ) ); + wfProfileOut( $fname ); + return; } - while ( $line = $db->fetchObject( $res ) ) { - $s .= $this->historyLine( - $line->old_timestamp, $line->old_user, - $line->old_user_text, $namespace, - $title, $line->old_id, - $line->old_comment, ( $line->old_minor_edit > 0 ), - $counter++, - $notificationtimestamp - ); + + + /* + * "go=first" means to jump to the last (earliest) history page. + * This is deprecated, it no longer appears in the user interface + */ + if ( $wgRequest->getText("go") == 'first' ) { + $limit = $wgRequest->getInt( 'limit', 50 ); + $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); + return; } - $s .= $this->endHistoryList( !$atend ); - $s .= $numbar; - $wgOut->addHTML( $s ); + + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); + + /** + * Do the list + */ + $pager = new PageHistoryPager( $this ); + $this->linesonpage = $pager->getNumRows(); + $wgOut->addHTML( + $pager->getNavigationBar() . + $this->beginHistoryList() . + $pager->getBody() . + $this->endHistoryList() . + $pager->getNavigationBar() + ); wfProfileOut( $fname ); } + /** @todo document */ function beginHistoryList() { global $wgTitle; - $this->lastdate = $this->lastline = ''; - $s = '

' . wfMsg( 'histlegend' ) . '

'; + $this->lastdate = ''; + $s = wfMsgExt( 'histlegend', array( 'parse') ); $s .= '
'; $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey()); + + // The following line is SUPPOSED to have double-quotes around the + // $prefixedkey variable, because htmlspecialchars() doesn't escape + // single-quotes. + // + // On at least two occasions people have changed it to single-quotes, + // which creates invalid HTML and incorrect display of the resulting + // link. + // + // Please do not break this a third time. Thank you for your kind + // consideration and cooperation. + // $s .= "\n"; - $s .= !empty($this->submitbuttonhtml1) ? $this->submitbuttonhtml1."\n":''; - $s .= ''; + $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); $s .= '
'; return $s; } - function historyLine( $ts, $u, $ut, $ns, $ttl, $oid, $c, $isminor, $counter = '', $notificationtimestamp = false ) { - global $wgLang, $wgContLang; + /** @todo document */ + function submitButton( $bits = array() ) { + return ( $this->linesonpage > 0 ) + ? wfElement( 'input', array_merge( $bits, + array( + 'class' => 'historysubmit', + 'type' => 'submit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', + 'value' => wfMsg( 'compareselectedversions' ), + ) ) ) + : ''; + } - static $message; - if( !isset( $message ) ) { - foreach( explode( ' ', 'cur last selectolderversionfordiff selectnewerversionfordiff minoreditletter' ) as $msg ) { - $message[$msg] = wfMsg( $msg ); + /** + * Returns a row from the history printout. + * + * @todo document some more, and maybe clean up the code (some params redundant?) + * + * @param object $row The database row corresponding to the line (or is it the previous line?). + * @param object $next The database row corresponding to the next line (or is it this one?). + * @param int $counter Apparently a counter of what row number we're at, counted from the top row = 1. + * @param $notificationtimestamp + * @param bool $latest Whether this row corresponds to the page's latest revision. + * @param bool $firstInList Whether this row corresponds to the first displayed on this history page. + * @return string HTML output for the row + */ + function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) { + global $wgUser, $wgLang; + $rev = new Revision( $row ); + $rev->setTitle( $this->mTitle ); + + $s = '
  • '; + $curlink = $this->curLink( $rev, $latest ); + $lastlink = $this->lastLink( $rev, $next, $counter ); + $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); + $link = $this->revLink( $rev ); + + $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() ) + . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() ); + + $s .= "($curlink) ($lastlink) $arbitrary"; + + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + if( $firstInList ) { + // We don't currently handle well changing the top revision's settings + $del = wfMsgHtml( 'rev-delundel' ); + } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $del = wfMsgHtml( 'rev-delundel' ); + } else { + $del = $this->mSkin->makeKnownLinkObj( $revdel, + wfMsg( 'rev-delundel' ), + 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $rev->getId() ) ); } + $s .= " ($del) "; } - if ( $oid && $this->lastline ) { - $ret = preg_replace( "/!OLDID!([0-9]+)!/", $this->mSkin->makeKnownLinkObj( - $this->mTitle, $message['last'], "diff=\\1&oldid={$oid}",'' ,'' ,' tabindex="'.$counter.'"' ), $this->lastline ); + $s .= " $link"; + #getUser is safe, but this avoids making the invalid untargeted contribs links + if( $row->rev_deleted & Revision::DELETED_USER ) { + $user = '' . wfMsg('rev-deleted-user') . ''; + } + $s .= " $user"; + + if( $row->rev_minor_edit ) { + $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); + } + + if (!is_null($size = $rev->getSize())) { + if ($size == 0) + $stxt = wfMsgHtml('historyempty'); + else + $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); + $s .= " $stxt"; + } + + #getComment is safe, but this is better formatted + if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + $s .= " " . + wfMsgHtml( 'rev-deleted-comment' ) . ""; } else { - $ret = ''; + $s .= $this->mSkin->revComment( $rev ); + } + + if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) { + $s .= ' ' . wfMsgHtml( 'updatedmarker' ) . ''; } - $dt = $wgLang->timeanddate( $ts, true ); + #add blurb about text having been deleted + if( $row->rev_deleted & Revision::DELETED_TEXT ) { + $s .= ' ' . wfMsgHtml( 'deletedrev' ); + } + if( $wgUser->isAllowed( 'rollback' ) && $latest ) { + $s .= ' '.$this->mSkin->generateRollback( $rev ); + } + + wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) ); + + $s .= "
  • \n"; - if ( $oid ) { - $q = 'oldid='.$oid; + return $s; + } + + /** @todo document */ + function revLink( $rev ) { + global $wgLang; + $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true ); + if( $rev->userCan( Revision::DELETED_TEXT ) ) { + $link = $this->mSkin->makeKnownLinkObj( + $this->mTitle, $date, "oldid=" . $rev->getId() ); } else { - $q = ''; + $link = $date; + } + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + return '' . $link . ''; } - $link = $this->mSkin->makeKnownLinkObj( $this->mTitle, $dt, $q ); + return $link; + } - if ( 0 == $u ) { - $contribsPage =& Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $ul = $this->mSkin->makeKnownLinkObj( $contribsPage, - htmlspecialchars( $ut ), 'target=' . urlencode( $ut ) ); + /** @todo document */ + function curLink( $rev, $latest ) { + $cur = wfMsgExt( 'cur', array( 'escape') ); + if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { + return $cur; } else { - $userPage =& Title::makeTitle( NS_USER, $ut ); - $ul = $this->mSkin->makeLinkObj( $userPage , htmlspecialchars( $ut ) ); + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, $cur, + 'diff=' . $this->getLatestID() . + "&oldid=" . $rev->getId() ); } + } - $s = '
  • '; - if ( $oid ) { - $curlink = $this->mSkin->makeKnownLinkObj( $this->mTitle, $message['cur'], - 'diff=0&oldid='.$oid ); + /** @todo document */ + function lastLink( $rev, $next, $counter ) { + $last = wfMsgExt( 'last', array( 'escape' ) ); + if ( is_null( $next ) ) { + # Probably no next row + return $last; + } elseif ( $next === 'unknown' ) { + # Next row probably exists but is unknown, use an oldid=prev link + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid=prev" ); + } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) { + return $last; } else { - $curlink = $message['cur']; + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid={$next->rev_id}" + /*, + '', + '', + "tabindex={$counter}"*/ ); } - $arbitrary = ''; + } + + /** @todo document */ + function diffButtons( $rev, $firstInList, $counter ) { if( $this->linesonpage > 1) { - # XXX: move title texts to javascript - $checkmark = ''; - if ( !$oid ) { - $arbitrary = ''; - $checkmark = ' checked="checked"'; + $radio = array( + 'type' => 'radio', + 'value' => $rev->getId(), +# do we really need to flood this on every item? +# 'title' => wfMsgHtml( 'selectolderversionfordiff' ) + ); + + if( !$rev->userCan( Revision::DELETED_TEXT ) ) { + $radio['disabled'] = 'disabled'; + } + + /** @todo: move title texts to javascript */ + if ( $firstInList ) { + $first = wfElement( 'input', array_merge( + $radio, + array( + 'style' => 'visibility:hidden', + 'name' => 'oldid' ) ) ); + $checkmark = array( 'checked' => 'checked' ); } else { - if( $counter == 2 ) $checkmark = ' checked="checked"'; - $arbitrary = ''; - $checkmark = ''; + if( $counter == 2 ) { + $checkmark = array( 'checked' => 'checked' ); + } else { + $checkmark = array(); + } + $first = wfElement( 'input', array_merge( + $radio, + $checkmark, + array( 'name' => 'oldid' ) ) ); + $checkmark = array(); } - $arbitrary .= ''; + $second = wfElement( 'input', array_merge( + $radio, + $checkmark, + array( 'name' => 'diff' ) ) ); + return $first . $second; + } else { + return ''; + } + } + + /** @todo document */ + function getLatestId() { + if( is_null( $this->mLatestId ) ) { + $id = $this->mTitle->getArticleID(); + $db = wfGetDB(DB_SLAVE); + $this->mLatestId = $db->selectField( 'page', + "page_latest", + array( 'page_id' => $id ), + 'PageHistory::getLatestID' ); + } + return $this->mLatestId; + } + + /** + * Fetch an array of revisions, specified by a given limit, offset and + * direction. This is now only used by the feeds. It was previously + * used by the main UI but that's now handled by the pager. + */ + function fetchRevisions($limit, $offset, $direction) { + $fname = 'PageHistory::fetchRevisions'; + + $dbr = wfGetDB( DB_SLAVE ); + + if ($direction == PageHistory::DIR_PREV) + list($dirs, $oper) = array("ASC", ">="); + else /* $direction == PageHistory::DIR_NEXT */ + list($dirs, $oper) = array("DESC", "<="); + + if ($offset) + $offsets = array("rev_timestamp $oper '$offset'"); + else + $offsets = array(); + + $page_id = $this->mTitle->getArticleID(); + + $res = $dbr->select( + 'revision', + Revision::selectFields(), + array_merge(array("rev_page=$page_id"), $offsets), + $fname, + array('ORDER BY' => "rev_timestamp $dirs", + 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit) + ); + + $result = array(); + while (($obj = $dbr->fetchObject($res)) != NULL) + $result[] = $obj; + + return $result; + } + + /** @todo document */ + function getNotificationTimestamp() { + global $wgUser, $wgShowUpdatedMarker; + $fname = 'PageHistory::getNotficationTimestamp'; + + if ($this->mNotificationTimestamp !== NULL) + return $this->mNotificationTimestamp; + + if ($wgUser->isAnon() || !$wgShowUpdatedMarker) + return $this->mNotificationTimestamp = false; + + $dbr = wfGetDB(DB_SLAVE); + + $this->mNotificationTimestamp = $dbr->selectField( + 'watchlist', + 'wl_notificationtimestamp', + array( 'wl_namespace' => $this->mTitle->getNamespace(), + 'wl_title' => $this->mTitle->getDBkey(), + 'wl_user' => $wgUser->getID() + ), + $fname); + + // Don't use the special value reserved for telling whether the field is filled + if ( is_null( $this->mNotificationTimestamp ) ) { + $this->mNotificationTimestamp = false; } - $s .= "({$curlink}) (!OLDID!{$oid}!) $arbitrary {$link} {$ul}"; - $s .= $isminor ? ' '.$message['minoreditletter'].'': '' ; + return $this->mNotificationTimestamp; + } + + /** + * Output a subscription feed listing recent edits to this page. + * @param string $type + */ + function feed( $type ) { + require_once 'SpecialRecentchanges.php'; + + global $wgFeedClasses; + if( !isset( $wgFeedClasses[$type] ) ) { + global $wgOut; + $wgOut->addWikiText( wfMsg( 'feed-invalid' ) ); + return; + } + + $feed = new $wgFeedClasses[$type]( + $this->mTitle->getPrefixedText() . ' - ' . + wfMsgForContent( 'history-feed-title' ), + wfMsgForContent( 'history-feed-description' ), + $this->mTitle->getFullUrl( 'action=history' ) ); - if ( '' != $c && '*' != $c ) { - $c = $this->mSkin->formatcomment( $c, $this->mTitle ); - $s .= " ($c)"; + $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT); + $feed->outHeader(); + if( $items ) { + foreach( $items as $row ) { + $feed->outItem( $this->feedItem( $row ) ); + } + } else { + $feed->outItem( $this->feedEmpty() ); } - if ($notificationtimestamp && ($ts >= $notificationtimestamp)) { - $s .= wfMsg( 'updatedmarker' ); + $feed->outFooter(); + } + + function feedEmpty() { + global $wgOut; + return new FeedItem( + wfMsgForContent( 'nohistory' ), + $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ), + $this->mTitle->getFullUrl(), + wfTimestamp( TS_MW ), + '', + $this->mTitle->getTalkPage()->getFullUrl() ); + } + + /** + * Generate a FeedItem object from a given revision table row + * Borrows Recent Changes' feed generation functions for formatting; + * includes a diff to the previous revision (if any). + * + * @param $row + * @return FeedItem + */ + function feedItem( $row ) { + $rev = new Revision( $row ); + $rev->setTitle( $this->mTitle ); + $text = rcFormatDiffRow( $this->mTitle, + $this->mTitle->getPreviousRevisionID( $rev->getId() ), + $rev->getId(), + $rev->getTimestamp(), + $rev->getComment() ); + + if( $rev->getComment() == '' ) { + global $wgContLang; + $title = wfMsgForContent( 'history-feed-item-nocomment', + $rev->getUserText(), + $wgContLang->timeanddate( $rev->getTimestamp() ) ); + } else { + $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() ); } - $s .= '
  • '; - $this->lastline = $s; - return $ret; + return new FeedItem( + $title, + $text, + $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ), + $rev->getTimestamp(), + $rev->getUserText(), + $this->mTitle->getTalkPage()->getFullUrl() ); + } + + /** + * Quickie hack... strip out wikilinks to more legible form from the comment. + */ + function stripComment( $text ) { + return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); + } +} + + +/** + * @addtogroup Pager + */ +class PageHistoryPager extends ReverseChronologicalPager { + public $mLastRow = false, $mPageHistory; + + function __construct( $pageHistory ) { + parent::__construct(); + $this->mPageHistory = $pageHistory; + } + + function getQueryInfo() { + return array( + 'tables' => 'revision', + 'fields' => Revision::selectFields(), + 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), + 'options' => array( 'USE INDEX' => 'page_timestamp' ) + ); } + function getIndexField() { + return 'rev_timestamp'; + } + + function formatRow( $row ) { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == ''; + $firstInList = $this->mCounter == 1; + $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, + $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + } else { + $s = ''; + } + $this->mLastRow = $row; + return $s; + } + + function getStartBody() { + $this->mLastRow = false; + $this->mCounter = 1; + return ''; + } + + function getEndBody() { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == 0; + $firstInList = $this->mCounter == 1; + if ( $this->mIsBackwards ) { + # Next row is unknown, but for UI reasons, probably exists if an offset has been specified + if ( $this->mOffset == '' ) { + $next = null; + } else { + $next = 'unknown'; + } + } else { + # The next row is the past-the-end row + $next = $this->mPastTheEndRow; + } + $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++, + $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + } else { + $s = ''; + } + return $s; + } } ?>