From: Umherirrender Date: Fri, 8 Mar 2019 20:56:38 +0000 (+0100) Subject: Move class HistoryPager to own file X-Git-Tag: 1.34.0-rc.0~2571^2 X-Git-Url: http://git.cyclocoop.org/%22.htmlspecialchars%28%24url_syndic%29.%22?a=commitdiff_plain;h=11ce0ab99b61febbd0844a5b5892510ba37ce034;p=lhc%2Fweb%2Fwiklou.git Move class HistoryPager to own file Change-Id: Ie12e79e6f32225442b5bd794826f8c760b2c02d6 --- diff --git a/.phpcs.xml b/.phpcs.xml index 5a639adb99..14db0887de 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -207,7 +207,6 @@ Whitelist existing violations, but enable the sniff to prevent any new occurrences. --> - */includes/actions/HistoryAction\.php */includes/api/ApiErrorFormatter\.php */includes/api/ApiImport\.php */includes/api/ApiMessage\.php diff --git a/autoload.php b/autoload.php index be377373e9..64a1d9591d 100644 --- a/autoload.php +++ b/autoload.php @@ -630,7 +630,7 @@ $wgAutoloadLocalClasses = [ 'HistoryBlob' => __DIR__ . '/includes/HistoryBlob.php', 'HistoryBlobCurStub' => __DIR__ . '/includes/HistoryBlob.php', 'HistoryBlobStub' => __DIR__ . '/includes/HistoryBlob.php', - 'HistoryPager' => __DIR__ . '/includes/actions/HistoryAction.php', + 'HistoryPager' => __DIR__ . '/includes/actions/pagers/HistoryPager.php', 'Hooks' => __DIR__ . '/includes/Hooks.php', 'Html' => __DIR__ . '/includes/Html.php', 'HtmlArmor' => __DIR__ . '/includes/libs/HtmlArmor.php', diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 39bc830ddc..fbf43e0f3d 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -2,8 +2,6 @@ /** * Page history * - * Split off from Article.php and Skin.php, 2003-12-22 - * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -382,607 +380,3 @@ class HistoryAction extends FormlessAction { ); } } - -/** - * @ingroup Pager - * @ingroup Actions - */ -class HistoryPager extends ReverseChronologicalPager { - /** - * @var bool|stdClass - */ - public $lastRow = false; - - public $counter, $historyPage, $buttons, $conds; - - protected $oldIdChecked; - - protected $preventClickjacking = false; - /** - * @var array - */ - protected $parentLens; - - /** @var bool Whether to show the tag editing UI */ - protected $showTagEditUI; - - /** @var string */ - private $tagFilter; - - /** - * @param HistoryAction $historyPage - * @param string $year - * @param string $month - * @param string $tagFilter - * @param array $conds - */ - public function __construct( - HistoryAction $historyPage, - $year = '', - $month = '', - $tagFilter = '', - array $conds = [] - ) { - parent::__construct( $historyPage->getContext() ); - $this->historyPage = $historyPage; - $this->tagFilter = $tagFilter; - $this->getDateCond( $year, $month ); - $this->conds = $conds; - $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() ); - } - - // For hook compatibility... - function getArticle() { - return $this->historyPage->getArticle(); - } - - function getSqlComment() { - if ( $this->conds ) { - return 'history page filtered'; // potentially slow, see CR r58153 - } else { - return 'history page unfiltered'; - } - } - - function getQueryInfo() { - $revQuery = Revision::getQueryInfo( [ 'user' ] ); - $queryInfo = [ - 'tables' => $revQuery['tables'], - 'fields' => $revQuery['fields'], - 'conds' => array_merge( - [ 'rev_page' => $this->getWikiPage()->getId() ], - $this->conds ), - 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ], - 'join_conds' => $revQuery['joins'], - ]; - ChangeTags::modifyDisplayQuery( - $queryInfo['tables'], - $queryInfo['fields'], - $queryInfo['conds'], - $queryInfo['join_conds'], - $queryInfo['options'], - $this->tagFilter - ); - - // Avoid PHP 7.1 warning of passing $this by reference - $historyPager = $this; - Hooks::run( 'PageHistoryPager::getQueryInfo', [ &$historyPager, &$queryInfo ] ); - - return $queryInfo; - } - - function getIndexField() { - return 'rev_timestamp'; - } - - /** - * @param stdClass $row - * @return string - */ - function formatRow( $row ) { - if ( $this->lastRow ) { - $latest = ( $this->counter == 1 && $this->mIsFirst ); - $firstInList = $this->counter == 1; - $this->counter++; - - $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' ) - ? $this->getTitle()->getNotificationTimestamp( $this->getUser() ) - : false; - - $s = $this->historyLine( - $this->lastRow, $row, $notifTimestamp, $latest, $firstInList ); - } else { - $s = ''; - } - $this->lastRow = $row; - - return $s; - } - - protected function doBatchLookups() { - if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) { - return; - } - - # Do a link batch query - $this->mResult->seek( 0 ); - $batch = new LinkBatch(); - $revIds = []; - foreach ( $this->mResult as $row ) { - if ( $row->rev_parent_id ) { - $revIds[] = $row->rev_parent_id; - } - if ( $row->user_name !== null ) { - $batch->add( NS_USER, $row->user_name ); - $batch->add( NS_USER_TALK, $row->user_name ); - } else { # for anons or usernames of imported revisions - $batch->add( NS_USER, $row->rev_user_text ); - $batch->add( NS_USER_TALK, $row->rev_user_text ); - } - } - $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds ); - $batch->execute(); - $this->mResult->seek( 0 ); - } - - /** - * Creates begin of history list with a submit button - * - * @return string HTML output - */ - protected function getStartBody() { - $this->lastRow = false; - $this->counter = 1; - $this->oldIdChecked = 0; - - $this->getOutput()->wrapWikiMsg( "
\n$1\n
", 'histlegend' ); - $s = Html::openElement( 'form', [ 'action' => wfScript(), - 'id' => 'mw-history-compare' ] ) . "\n"; - $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n"; - $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; - $s .= Html::hidden( 'type', 'revision' ) . "\n"; - - // Button container stored in $this->buttons for re-use in getEndBody() - $this->buttons = '
'; - $className = 'historysubmit mw-history-compareselectedversions-button'; - $attrs = [ 'class' => $className ] - + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ); - $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), - $attrs - ) . "\n"; - - $user = $this->getUser(); - $actionButtons = ''; - if ( $user->isAllowed( 'deleterevision' ) ) { - $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); - } - if ( $this->showTagEditUI ) { - $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' ); - } - if ( $actionButtons ) { - $this->buttons .= Xml::tags( 'div', [ 'class' => - 'mw-history-revisionactions' ], $actionButtons ); - } - - if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { - $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); - } - - $this->buttons .= '
'; - - $s .= $this->buttons; - $s .= '\n"; - # Add second buttons only if there is more than one rev - if ( $this->getNumRows() > 2 ) { - $s .= $this->buttons; - } - $s .= ''; - - return $s; - } - - /** - * Creates a submit button - * - * @param string $message Text of the submit button, will be escaped - * @param array $attributes - * @return string HTML output for the submit button - */ - function submitButton( $message, $attributes = [] ) { - # Disable submit button if history has 1 revision only - if ( $this->getNumRows() > 1 ) { - return Html::submitButton( $message, $attributes ); - } else { - return ''; - } - } - - /** - * Returns a row from the history printout. - * - * @todo document some more, and maybe clean up the code (some params redundant?) - * - * @param stdClass $row The database row corresponding to the previous line. - * @param mixed $next The database row corresponding to the next line - * (chronologically previous) - * @param bool|string $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, $notificationtimestamp = false, - $latest = false, $firstInList = false ) { - $rev = new Revision( $row, 0, $this->getTitle() ); - - if ( is_object( $next ) ) { - $prevRev = new Revision( $next, 0, $this->getTitle() ); - } else { - $prevRev = null; - } - - $curlink = $this->curLink( $rev, $latest ); - $lastlink = $this->lastLink( $rev, $next ); - $curLastlinks = $curlink . $this->historyPage->message['pipe-separator'] . $lastlink; - $histLinks = Html::rawElement( - 'span', - [ 'class' => 'mw-history-histlinks' ], - $this->msg( 'parentheses' )->rawParams( $curLastlinks )->escaped() - ); - - $diffButtons = $this->diffButtons( $rev, $firstInList ); - $s = $histLinks . $diffButtons; - - $link = $this->revLink( $rev ); - $classes = []; - - $del = ''; - $user = $this->getUser(); - $canRevDelete = $user->isAllowed( 'deleterevision' ); - // Show checkboxes for each revision, to allow for revision deletion and - // change tags - if ( $canRevDelete || $this->showTagEditUI ) { - $this->preventClickjacking(); - // If revision was hidden from sysops and we don't need the checkbox - // for anything else, disable it - if ( !$this->showTagEditUI && !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { - $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); - // Otherwise, enable the checkbox... - } else { - $del = Xml::check( 'showhiderevisions', false, - [ 'name' => 'ids[' . $rev->getId() . ']' ] ); - } - // User can only view deleted revisions... - } elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) { - // If revision was hidden from sysops, disable the link - if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { - $del = Linker::revDeleteLinkDisabled( false ); - // Otherwise, show the link... - } else { - $query = [ 'type' => 'revision', - 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() ]; - $del .= Linker::revDeleteLink( $query, - $rev->isDeleted( Revision::DELETED_RESTRICTED ), false ); - } - } - if ( $del ) { - $s .= " $del "; - } - - $lang = $this->getLanguage(); - $dirmark = $lang->getDirMark(); - - $s .= " $link"; - $s .= $dirmark; - $s .= " " . - Linker::revUserTools( $rev, true ) . ""; - $s .= $dirmark; - - if ( $rev->isMinor() ) { - $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() ); - } - - # Sometimes rev_len isn't populated - if ( $rev->getSize() !== null ) { - # Size is always public data - $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0; - $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() ); - $fSize = Linker::formatRevisionSize( $rev->getSize() ); - $s .= ' . . ' . "$fSize $sDiff"; - } - - # Text following the character difference is added just before running hooks - $s2 = Linker::revComment( $rev, false, true ); - - if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) { - $s2 .= ' ' . $this->msg( 'updatedmarker' )->escaped() . ''; - $classes[] = 'mw-history-line-updated'; - } - - $tools = []; - - # Rollback and undo links - if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) { - if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) { - // Get a rollback link without the brackets - $rollbackLink = Linker::generateRollback( - $rev, - $this->getContext(), - [ 'verify', 'noBrackets' ] - ); - if ( $rollbackLink ) { - $this->preventClickjacking(); - $tools[] = $rollbackLink; - } - } - - if ( !$rev->isDeleted( Revision::DELETED_TEXT ) - && !$prevRev->isDeleted( Revision::DELETED_TEXT ) - ) { - # Create undo tooltip for the first (=latest) line only - $undoTooltip = $latest - ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ] - : []; - $undolink = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( - $this->getTitle(), - $this->msg( 'editundo' )->text(), - $undoTooltip, - [ - 'action' => 'edit', - 'undoafter' => $prevRev->getId(), - 'undo' => $rev->getId() - ] - ); - $tools[] = "{$undolink}"; - } - } - // Allow extension to add their own links here - Hooks::run( 'HistoryRevisionTools', [ $rev, &$tools, $prevRev, $user ] ); - - if ( $tools ) { - $s2 .= ' ' . $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped(); - } - - # Tags - list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( - $row->ts_tags, - 'history', - $this->getContext() - ); - $classes = array_merge( $classes, $newClasses ); - if ( $tagSummary !== '' ) { - $s2 .= " $tagSummary"; - } - - # Include separator between character difference and following text - if ( $s2 !== '' ) { - $s .= ' . . ' . $s2; - } - - $attribs = [ 'data-mw-revid' => $rev->getId() ]; - - Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes, &$attribs ] ); - $attribs = array_filter( $attribs, - [ Sanitizer::class, 'isReservedDataAttribute' ], - ARRAY_FILTER_USE_KEY - ); - - if ( $classes ) { - $attribs['class'] = implode( ' ', $classes ); - } - - return Xml::tags( 'li', $attribs, $s ) . "\n"; - } - - /** - * Create a link to view this revision of the page - * - * @param Revision $rev - * @return string - */ - function revLink( $rev ) { - $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $this->getUser() ); - if ( $rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( - $this->getTitle(), - $date, - [ 'class' => 'mw-changeslist-date' ], - [ 'oldid' => $rev->getId() ] - ); - } else { - $link = htmlspecialchars( $date ); - } - if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $link = "$link"; - } - - return $link; - } - - /** - * Create a diff-to-current link for this revision for this page - * - * @param Revision $rev - * @param bool $latest This is the latest revision of the page? - * @return string - */ - function curLink( $rev, $latest ) { - $cur = $this->historyPage->message['cur']; - if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - return $cur; - } else { - return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( - $this->getTitle(), - new HtmlArmor( $cur ), - [], - [ - 'diff' => $this->getWikiPage()->getLatest(), - 'oldid' => $rev->getId() - ] - ); - } - } - - /** - * Create a diff-to-previous link for this revision for this page. - * - * @param Revision $prevRev The revision being displayed - * @param stdClass|string|null $next The next revision in list (that is - * the previous one in chronological order). - * May either be a row, "unknown" or null. - * @return string - */ - function lastLink( $prevRev, $next ) { - $last = $this->historyPage->message['last']; - - if ( $next === null ) { - # Probably no next row - return $last; - } - - $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - if ( $next === 'unknown' ) { - # Next row probably exists but is unknown, use an oldid=prev link - return $linkRenderer->makeKnownLink( - $this->getTitle(), - new HtmlArmor( $last ), - [], - [ - 'diff' => $prevRev->getId(), - 'oldid' => 'prev' - ] - ); - } - - $nextRev = new Revision( $next ); - - if ( !$prevRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) - || !$nextRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) - ) { - return $last; - } - - return $linkRenderer->makeKnownLink( - $this->getTitle(), - new HtmlArmor( $last ), - [], - [ - 'diff' => $prevRev->getId(), - 'oldid' => $next->rev_id - ] - ); - } - - /** - * Create radio buttons for page history - * - * @param Revision $rev - * @param bool $firstInList Is this version the first one? - * - * @return string HTML output for the radio buttons - */ - function diffButtons( $rev, $firstInList ) { - if ( $this->getNumRows() > 1 ) { - $id = $rev->getId(); - $radio = [ 'type' => 'radio', 'value' => $id ]; - /** @todo Move title texts to javascript */ - if ( $firstInList ) { - $first = Xml::element( 'input', - array_merge( $radio, [ - 'style' => 'visibility:hidden', - 'name' => 'oldid', - 'id' => 'mw-oldid-null' ] ) - ); - $checkmark = [ 'checked' => 'checked' ]; - } else { - # Check visibility of old revisions - if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - $radio['disabled'] = 'disabled'; - $checkmark = []; // We will check the next possible one - } elseif ( !$this->oldIdChecked ) { - $checkmark = [ 'checked' => 'checked' ]; - $this->oldIdChecked = $id; - } else { - $checkmark = []; - } - $first = Xml::element( 'input', - array_merge( $radio, $checkmark, [ - 'name' => 'oldid', - 'id' => "mw-oldid-$id" ] ) ); - $checkmark = []; - } - $second = Xml::element( 'input', - array_merge( $radio, $checkmark, [ - 'name' => 'diff', - 'id' => "mw-diff-$id" ] ) ); - - return $first . $second; - } else { - return ''; - } - } - - /** - * This is called if a write operation is possible from the generated HTML - * @param bool $enable - */ - function preventClickjacking( $enable = true ) { - $this->preventClickjacking = $enable; - } - - /** - * Get the "prevent clickjacking" flag - * @return bool - */ - function getPreventClickjacking() { - return $this->preventClickjacking; - } - -} diff --git a/includes/actions/pagers/HistoryPager.php b/includes/actions/pagers/HistoryPager.php new file mode 100644 index 0000000000..a59597fea9 --- /dev/null +++ b/includes/actions/pagers/HistoryPager.php @@ -0,0 +1,628 @@ +getContext() ); + $this->historyPage = $historyPage; + $this->tagFilter = $tagFilter; + $this->getDateCond( $year, $month ); + $this->conds = $conds; + $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() ); + } + + // For hook compatibility... + function getArticle() { + return $this->historyPage->getArticle(); + } + + function getSqlComment() { + if ( $this->conds ) { + return 'history page filtered'; // potentially slow, see CR r58153 + } else { + return 'history page unfiltered'; + } + } + + function getQueryInfo() { + $revQuery = Revision::getQueryInfo( [ 'user' ] ); + $queryInfo = [ + 'tables' => $revQuery['tables'], + 'fields' => $revQuery['fields'], + 'conds' => array_merge( + [ 'rev_page' => $this->getWikiPage()->getId() ], + $this->conds ), + 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ], + 'join_conds' => $revQuery['joins'], + ]; + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + $this->tagFilter + ); + + // Avoid PHP 7.1 warning of passing $this by reference + $historyPager = $this; + Hooks::run( 'PageHistoryPager::getQueryInfo', [ &$historyPager, &$queryInfo ] ); + + return $queryInfo; + } + + function getIndexField() { + return 'rev_timestamp'; + } + + /** + * @param stdClass $row + * @return string + */ + function formatRow( $row ) { + if ( $this->lastRow ) { + $latest = ( $this->counter == 1 && $this->mIsFirst ); + $firstInList = $this->counter == 1; + $this->counter++; + + $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' ) + ? $this->getTitle()->getNotificationTimestamp( $this->getUser() ) + : false; + + $s = $this->historyLine( + $this->lastRow, $row, $notifTimestamp, $latest, $firstInList ); + } else { + $s = ''; + } + $this->lastRow = $row; + + return $s; + } + + protected function doBatchLookups() { + if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) { + return; + } + + # Do a link batch query + $this->mResult->seek( 0 ); + $batch = new LinkBatch(); + $revIds = []; + foreach ( $this->mResult as $row ) { + if ( $row->rev_parent_id ) { + $revIds[] = $row->rev_parent_id; + } + if ( $row->user_name !== null ) { + $batch->add( NS_USER, $row->user_name ); + $batch->add( NS_USER_TALK, $row->user_name ); + } else { # for anons or usernames of imported revisions + $batch->add( NS_USER, $row->rev_user_text ); + $batch->add( NS_USER_TALK, $row->rev_user_text ); + } + } + $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds ); + $batch->execute(); + $this->mResult->seek( 0 ); + } + + /** + * Creates begin of history list with a submit button + * + * @return string HTML output + */ + protected function getStartBody() { + $this->lastRow = false; + $this->counter = 1; + $this->oldIdChecked = 0; + + $this->getOutput()->wrapWikiMsg( "
\n$1\n
", 'histlegend' ); + $s = Html::openElement( 'form', [ 'action' => wfScript(), + 'id' => 'mw-history-compare' ] ) . "\n"; + $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n"; + $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; + $s .= Html::hidden( 'type', 'revision' ) . "\n"; + + // Button container stored in $this->buttons for re-use in getEndBody() + $this->buttons = '
'; + $className = 'historysubmit mw-history-compareselectedversions-button'; + $attrs = [ 'class' => $className ] + + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ); + $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), + $attrs + ) . "\n"; + + $user = $this->getUser(); + $actionButtons = ''; + if ( $user->isAllowed( 'deleterevision' ) ) { + $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); + } + if ( $this->showTagEditUI ) { + $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' ); + } + if ( $actionButtons ) { + $this->buttons .= Xml::tags( 'div', [ 'class' => + 'mw-history-revisionactions' ], $actionButtons ); + } + + if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { + $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); + } + + $this->buttons .= '
'; + + $s .= $this->buttons; + $s .= '\n"; + # Add second buttons only if there is more than one rev + if ( $this->getNumRows() > 2 ) { + $s .= $this->buttons; + } + $s .= ''; + + return $s; + } + + /** + * Creates a submit button + * + * @param string $message Text of the submit button, will be escaped + * @param array $attributes + * @return string HTML output for the submit button + */ + function submitButton( $message, $attributes = [] ) { + # Disable submit button if history has 1 revision only + if ( $this->getNumRows() > 1 ) { + return Html::submitButton( $message, $attributes ); + } else { + return ''; + } + } + + /** + * Returns a row from the history printout. + * + * @todo document some more, and maybe clean up the code (some params redundant?) + * + * @param stdClass $row The database row corresponding to the previous line. + * @param mixed $next The database row corresponding to the next line + * (chronologically previous) + * @param bool|string $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, $notificationtimestamp = false, + $latest = false, $firstInList = false ) { + $rev = new Revision( $row, 0, $this->getTitle() ); + + if ( is_object( $next ) ) { + $prevRev = new Revision( $next, 0, $this->getTitle() ); + } else { + $prevRev = null; + } + + $curlink = $this->curLink( $rev, $latest ); + $lastlink = $this->lastLink( $rev, $next ); + $curLastlinks = $curlink . $this->historyPage->message['pipe-separator'] . $lastlink; + $histLinks = Html::rawElement( + 'span', + [ 'class' => 'mw-history-histlinks' ], + $this->msg( 'parentheses' )->rawParams( $curLastlinks )->escaped() + ); + + $diffButtons = $this->diffButtons( $rev, $firstInList ); + $s = $histLinks . $diffButtons; + + $link = $this->revLink( $rev ); + $classes = []; + + $del = ''; + $user = $this->getUser(); + $canRevDelete = $user->isAllowed( 'deleterevision' ); + // Show checkboxes for each revision, to allow for revision deletion and + // change tags + if ( $canRevDelete || $this->showTagEditUI ) { + $this->preventClickjacking(); + // If revision was hidden from sysops and we don't need the checkbox + // for anything else, disable it + if ( !$this->showTagEditUI && !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { + $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); + // Otherwise, enable the checkbox... + } else { + $del = Xml::check( 'showhiderevisions', false, + [ 'name' => 'ids[' . $rev->getId() . ']' ] ); + } + // User can only view deleted revisions... + } elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) { + // If revision was hidden from sysops, disable the link + if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { + $del = Linker::revDeleteLinkDisabled( false ); + // Otherwise, show the link... + } else { + $query = [ 'type' => 'revision', + 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() ]; + $del .= Linker::revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), false ); + } + } + if ( $del ) { + $s .= " $del "; + } + + $lang = $this->getLanguage(); + $dirmark = $lang->getDirMark(); + + $s .= " $link"; + $s .= $dirmark; + $s .= " " . + Linker::revUserTools( $rev, true ) . ""; + $s .= $dirmark; + + if ( $rev->isMinor() ) { + $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() ); + } + + # Sometimes rev_len isn't populated + if ( $rev->getSize() !== null ) { + # Size is always public data + $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0; + $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() ); + $fSize = Linker::formatRevisionSize( $rev->getSize() ); + $s .= ' . . ' . "$fSize $sDiff"; + } + + # Text following the character difference is added just before running hooks + $s2 = Linker::revComment( $rev, false, true ); + + if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) { + $s2 .= ' ' . $this->msg( 'updatedmarker' )->escaped() . ''; + $classes[] = 'mw-history-line-updated'; + } + + $tools = []; + + # Rollback and undo links + if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) { + if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) { + // Get a rollback link without the brackets + $rollbackLink = Linker::generateRollback( + $rev, + $this->getContext(), + [ 'verify', 'noBrackets' ] + ); + if ( $rollbackLink ) { + $this->preventClickjacking(); + $tools[] = $rollbackLink; + } + } + + if ( !$rev->isDeleted( Revision::DELETED_TEXT ) + && !$prevRev->isDeleted( Revision::DELETED_TEXT ) + ) { + # Create undo tooltip for the first (=latest) line only + $undoTooltip = $latest + ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ] + : []; + $undolink = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + $this->getTitle(), + $this->msg( 'editundo' )->text(), + $undoTooltip, + [ + 'action' => 'edit', + 'undoafter' => $prevRev->getId(), + 'undo' => $rev->getId() + ] + ); + $tools[] = "{$undolink}"; + } + } + // Allow extension to add their own links here + Hooks::run( 'HistoryRevisionTools', [ $rev, &$tools, $prevRev, $user ] ); + + if ( $tools ) { + $s2 .= ' ' . $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped(); + } + + # Tags + list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( + $row->ts_tags, + 'history', + $this->getContext() + ); + $classes = array_merge( $classes, $newClasses ); + if ( $tagSummary !== '' ) { + $s2 .= " $tagSummary"; + } + + # Include separator between character difference and following text + if ( $s2 !== '' ) { + $s .= ' . . ' . $s2; + } + + $attribs = [ 'data-mw-revid' => $rev->getId() ]; + + Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes, &$attribs ] ); + $attribs = array_filter( $attribs, + [ Sanitizer::class, 'isReservedDataAttribute' ], + ARRAY_FILTER_USE_KEY + ); + + if ( $classes ) { + $attribs['class'] = implode( ' ', $classes ); + } + + return Xml::tags( 'li', $attribs, $s ) . "\n"; + } + + /** + * Create a link to view this revision of the page + * + * @param Revision $rev + * @return string + */ + function revLink( $rev ) { + $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $this->getUser() ); + if ( $rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + $this->getTitle(), + $date, + [ 'class' => 'mw-changeslist-date' ], + [ 'oldid' => $rev->getId() ] + ); + } else { + $link = htmlspecialchars( $date ); + } + if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $link = "$link"; + } + + return $link; + } + + /** + * Create a diff-to-current link for this revision for this page + * + * @param Revision $rev + * @param bool $latest This is the latest revision of the page? + * @return string + */ + function curLink( $rev, $latest ) { + $cur = $this->historyPage->message['cur']; + if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + return $cur; + } else { + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + $this->getTitle(), + new HtmlArmor( $cur ), + [], + [ + 'diff' => $this->getWikiPage()->getLatest(), + 'oldid' => $rev->getId() + ] + ); + } + } + + /** + * Create a diff-to-previous link for this revision for this page. + * + * @param Revision $prevRev The revision being displayed + * @param stdClass|string|null $next The next revision in list (that is + * the previous one in chronological order). + * May either be a row, "unknown" or null. + * @return string + */ + function lastLink( $prevRev, $next ) { + $last = $this->historyPage->message['last']; + + if ( $next === null ) { + # Probably no next row + return $last; + } + + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + if ( $next === 'unknown' ) { + # Next row probably exists but is unknown, use an oldid=prev link + return $linkRenderer->makeKnownLink( + $this->getTitle(), + new HtmlArmor( $last ), + [], + [ + 'diff' => $prevRev->getId(), + 'oldid' => 'prev' + ] + ); + } + + $nextRev = new Revision( $next ); + + if ( !$prevRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) + || !$nextRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) + ) { + return $last; + } + + return $linkRenderer->makeKnownLink( + $this->getTitle(), + new HtmlArmor( $last ), + [], + [ + 'diff' => $prevRev->getId(), + 'oldid' => $next->rev_id + ] + ); + } + + /** + * Create radio buttons for page history + * + * @param Revision $rev + * @param bool $firstInList Is this version the first one? + * + * @return string HTML output for the radio buttons + */ + function diffButtons( $rev, $firstInList ) { + if ( $this->getNumRows() > 1 ) { + $id = $rev->getId(); + $radio = [ 'type' => 'radio', 'value' => $id ]; + /** @todo Move title texts to javascript */ + if ( $firstInList ) { + $first = Xml::element( 'input', + array_merge( $radio, [ + 'style' => 'visibility:hidden', + 'name' => 'oldid', + 'id' => 'mw-oldid-null' ] ) + ); + $checkmark = [ 'checked' => 'checked' ]; + } else { + # Check visibility of old revisions + if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + $radio['disabled'] = 'disabled'; + $checkmark = []; // We will check the next possible one + } elseif ( !$this->oldIdChecked ) { + $checkmark = [ 'checked' => 'checked' ]; + $this->oldIdChecked = $id; + } else { + $checkmark = []; + } + $first = Xml::element( 'input', + array_merge( $radio, $checkmark, [ + 'name' => 'oldid', + 'id' => "mw-oldid-$id" ] ) ); + $checkmark = []; + } + $second = Xml::element( 'input', + array_merge( $radio, $checkmark, [ + 'name' => 'diff', + 'id' => "mw-diff-$id" ] ) ); + + return $first . $second; + } else { + return ''; + } + } + + /** + * This is called if a write operation is possible from the generated HTML + * @param bool $enable + */ + function preventClickjacking( $enable = true ) { + $this->preventClickjacking = $enable; + } + + /** + * Get the "prevent clickjacking" flag + * @return bool + */ + function getPreventClickjacking() { + return $this->preventClickjacking; + } + +}