Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/includes/pager/ContributionsPager.php
Ðазад
<?php /** * 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 * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup Pager */ namespace MediaWiki\Pager; use ChangesList; use ChangeTags; use HtmlArmor; use InvalidArgumentException; use MapCacheLRU; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\CommentFormatter\CommentFormatter; use MediaWiki\Context\IContextSource; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Html\Html; use MediaWiki\Html\TemplateParser; use MediaWiki\Linker\Linker; use MediaWiki\Linker\LinkRenderer; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Parser\Sanitizer; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; use MediaWiki\SpecialPage\SpecialPage; use MediaWiki\Title\NamespaceInfo; use MediaWiki\Title\Title; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserRigorOptions; use stdClass; use Wikimedia\Rdbms\FakeResultWrapper; use Wikimedia\Rdbms\IResultWrapper; /** * Pager for Special:Contributions * @ingroup Pager */ abstract class ContributionsPager extends RangeChronologicalPager { /** @inheritDoc */ public $mGroupByDate = true; /** * @var string[] Local cache for escaped messages */ protected $messages; /** * @var bool Get revisions from the archive table (if true) or the revision table (if false) */ protected $isArchive; /** * @var string User name, or a string describing an IP address range */ protected $target; /** * @var string|int A single namespace number, or an empty string for all namespaces */ private $namespace; /** * @var string[]|false Name of tag to filter, or false to ignore tags */ private $tagFilter; /** * @var bool Set to true to invert the tag selection */ private $tagInvert; /** * @var bool Set to true to invert the namespace selection */ private $nsInvert; /** * @var bool Set to true to show both the subject and talk namespace, no matter which got * selected */ private $associated; /** * @var bool Set to true to show only deleted revisions */ private $deletedOnly; /** * @var bool Set to true to show only latest (a.k.a. current) revisions */ private $topOnly; /** * @var bool Set to true to show only new pages */ private $newOnly; /** * @var bool Set to true to hide edits marked as minor by the user */ private $hideMinor; /** * @var bool Set to true to only include mediawiki revisions. * (restricts extensions from executing additional queries to include their own contributions) */ private $revisionsOnly; /** @var bool */ private $preventClickjacking = false; protected ?Title $currentPage; protected ?RevisionRecord $currentRevRecord; /** * @var array */ private $mParentLens; /** @var UserIdentity */ protected $targetUser; /** * Set to protected to allow subclasses access for overrides */ protected TemplateParser $templateParser; private CommentFormatter $commentFormatter; private HookRunner $hookRunner; private LinkBatchFactory $linkBatchFactory; private NamespaceInfo $namespaceInfo; protected RevisionStore $revisionStore; /** @var string[] */ private $formattedComments = []; /** @var RevisionRecord[] Cached revisions by ID */ private $revisions = []; /** @var MapCacheLRU */ private $tagsCache; /** * Field names for various attributes. These may be overridden in a subclass, * for example for getting revisions from the archive table. */ protected string $revisionIdField = 'rev_id'; protected string $revisionParentIdField = 'rev_parent_id'; protected string $revisionTimestampField = 'rev_timestamp'; protected string $revisionLengthField = 'rev_len'; protected string $revisionDeletedField = 'rev_deleted'; protected string $revisionMinorField = 'rev_minor_edit'; protected string $userNameField = 'rev_user_text'; protected string $pageNamespaceField = 'page_namespace'; protected string $pageTitleField = 'page_title'; /** * @param LinkRenderer $linkRenderer * @param LinkBatchFactory $linkBatchFactory * @param HookContainer $hookContainer * @param RevisionStore $revisionStore * @param NamespaceInfo $namespaceInfo * @param CommentFormatter $commentFormatter * @param UserFactory $userFactory * @param IContextSource $context * @param array $options * @param UserIdentity|null $targetUser */ public function __construct( LinkRenderer $linkRenderer, LinkBatchFactory $linkBatchFactory, HookContainer $hookContainer, RevisionStore $revisionStore, NamespaceInfo $namespaceInfo, CommentFormatter $commentFormatter, UserFactory $userFactory, IContextSource $context, array $options, ?UserIdentity $targetUser ) { $this->isArchive = $options['isArchive'] ?? false; // Set ->target before calling parent::__construct() so // parent can call $this->getIndexField() and get the right result. Set // the rest too just to keep things simple. if ( $targetUser ) { $this->target = $options['target'] ?? $targetUser->getName(); $this->targetUser = $targetUser; } else { // Use target option // It's possible for the target to be empty. This is used by // ContribsPagerTest and does not cause newFromName() to return // false. It's probably not used by any production code. $this->target = $options['target'] ?? ''; // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null $this->targetUser = $userFactory->newFromName( $this->target, UserRigorOptions::RIGOR_NONE ); if ( !$this->targetUser ) { // This can happen if the target contained "#". Callers // typically pass user input through title normalization to // avoid it. throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' . 'broken to use even with validation disabled.' ); } } $this->namespace = $options['namespace'] ?? ''; $this->tagFilter = $options['tagfilter'] ?? false; $this->tagInvert = $options['tagInvert'] ?? false; $this->nsInvert = $options['nsInvert'] ?? false; $this->associated = $options['associated'] ?? false; $this->deletedOnly = !empty( $options['deletedOnly'] ); $this->topOnly = !empty( $options['topOnly'] ); $this->newOnly = !empty( $options['newOnly'] ); $this->hideMinor = !empty( $options['hideMinor'] ); $this->revisionsOnly = !empty( $options['revisionsOnly'] ); parent::__construct( $context, $linkRenderer ); $msgs = [ 'diff', 'hist', 'pipe-separator', 'uctop', 'changeslist-nocomment', 'undeleteviewlink', 'undeleteviewlink', 'deletionlog', ]; foreach ( $msgs as $msg ) { $this->messages[$msg] = $this->msg( $msg )->escaped(); } // Date filtering: use timestamp if available $startTimestamp = ''; $endTimestamp = ''; if ( isset( $options['start'] ) && $options['start'] ) { $startTimestamp = $options['start'] . ' 00:00:00'; } if ( isset( $options['end'] ) && $options['end'] ) { $endTimestamp = $options['end'] . ' 23:59:59'; } $this->getDateRangeCond( $startTimestamp, $endTimestamp ); $this->templateParser = new TemplateParser(); $this->linkBatchFactory = $linkBatchFactory; $this->hookRunner = new HookRunner( $hookContainer ); $this->revisionStore = $revisionStore; $this->namespaceInfo = $namespaceInfo; $this->commentFormatter = $commentFormatter; $this->tagsCache = new MapCacheLRU( 50 ); } public function getDefaultQuery() { $query = parent::getDefaultQuery(); $query['target'] = $this->target; return $query; } /** * This method basically executes the exact same code as the parent class, though with * a hook added, to allow extensions to add additional queries. * * @param string $offset Index offset, inclusive * @param int $limit Exact query limit * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING * @return IResultWrapper */ public function reallyDoQuery( $offset, $limit, $order ) { [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo( $offset, $limit, $order ); $options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ); /* * This hook will allow extensions to add in additional queries, so they can get their data * in My Contributions as well. Extensions should append their results to the $data array. * * Extension queries have to implement the navbar requirement as well. They should * - have a column aliased as $pager->getIndexField() * - have LIMIT set * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset * - have the ORDER BY specified based upon the details provided by the navbar * * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY * * &$data: an array of results of all contribs queries * $pager: the ContribsPager object hooked into * $offset: see phpdoc above * $limit: see phpdoc above * $descending: see phpdoc above */ $dbr = $this->getDatabase(); $data = [ $dbr->newSelectQueryBuilder() ->tables( is_array( $tables ) ? $tables : [ $tables ] ) ->fields( $fields ) ->conds( $conds ) ->caller( $fname ) ->options( $options ) ->joinConds( $join_conds ) ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) ) ->fetchResultSet() ]; if ( !$this->revisionsOnly ) { // These hooks were moved from ContribsPager and DeletedContribsPager. For backwards // compatability, they keep the same names. But they should be run for any contributions // pager, otherwise the entries from extensions would be missing. $reallyDoQueryHook = $this->isArchive ? 'onDeletedContribsPager__reallyDoQuery' : 'onContribsPager__reallyDoQuery'; // TODO: Range offsets are fairly important and all handlers should take care of it. // If this hook will be replaced (e.g. unified with the DeletedContribsPager one), // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577). $this->hookRunner->$reallyDoQueryHook( $data, $this, $offset, $limit, $order ); } $result = []; // loop all results and collect them in an array foreach ( $data as $query ) { foreach ( $query as $i => $row ) { // If the query results are in descending order, the indexes must also be in descending order $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i; // Left-pad with zeroes, because these values will be sorted as strings $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT ); // use index column as key, allowing us to easily sort in PHP $result[$row->{$this->getIndexField()} . "-$index"] = $row; } } // sort results if ( $order === self::QUERY_ASCENDING ) { ksort( $result ); } else { krsort( $result ); } // enforce limit $result = array_slice( $result, 0, $limit ); // get rid of array keys $result = array_values( $result ); return new FakeResultWrapper( $result ); } /** * Get queryInfo for the main query selecting revisions, not including * filtering on namespace, date, etc. * * @return array */ abstract protected function getRevisionQuery(); public function getQueryInfo() { $queryInfo = $this->getRevisionQuery(); if ( $this->deletedOnly ) { $queryInfo['conds'][] = $this->revisionDeletedField . ' != 0'; } if ( !$this->isArchive && $this->topOnly ) { $queryInfo['conds'][] = $this->revisionIdField . ' = page_latest'; } if ( $this->newOnly ) { $queryInfo['conds'][] = $this->revisionParentIdField . ' = 0'; } if ( $this->hideMinor ) { $queryInfo['conds'][] = $this->revisionMinorField . ' = 0'; } $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() ); // Paranoia: avoid brute force searches (T19342) $dbr = $this->getDatabase(); if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { $queryInfo['conds'][] = $dbr->bitAnd( $this->revisionDeletedField, RevisionRecord::DELETED_USER ) . ' = 0'; } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { $queryInfo['conds'][] = $dbr->bitAnd( $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER ) . ' != ' . RevisionRecord::SUPPRESSED_USER; } // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it. $indexField = $this->getIndexField(); if ( $indexField !== $this->revisionTimestampField ) { $queryInfo['fields'][] = $indexField; } MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery( $queryInfo['tables'], $queryInfo['fields'], $queryInfo['conds'], $queryInfo['join_conds'], $queryInfo['options'], $this->tagFilter, $this->tagInvert, ); if ( !$this->isArchive ) { $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo ); } return $queryInfo; } protected function getNamespaceCond() { if ( $this->namespace !== '' ) { $dbr = $this->getDatabase(); $namespaces = [ $this->namespace ]; $eq_op = $this->nsInvert ? '!=' : '='; if ( $this->associated ) { $namespaces[] = $this->namespaceInfo->getAssociated( $this->namespace ); } return [ $dbr->expr( $this->pageNamespaceField, $eq_op, $namespaces ) ]; } return []; } /** * @return false|string[] */ public function getTagFilter() { return $this->tagFilter; } /** * @return bool */ public function getTagInvert() { return $this->tagInvert; } /** * @return string */ public function getTarget() { return $this->target; } /** * @return bool */ public function isNewOnly() { return $this->newOnly; } /** * @return int|string */ public function getNamespace() { return $this->namespace; } protected function doBatchLookups() { # Do a link batch query $this->mResult->seek( 0 ); $parentRevIds = []; $this->mParentLens = []; $revisions = []; $linkBatch = $this->linkBatchFactory->newLinkBatch(); # Give some pointers to make (last) links foreach ( $this->mResult as $row ) { $revisionRecord = $this->tryCreatingRevisionRecord( $row ); if ( !$revisionRecord ) { continue; } if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) { $parentRevIds[] = (int)$row->{$this->revisionParentIdField}; } $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField}; if ( $this->target !== $row->{$this->userNameField} ) { // If the target does not match the author, batch the author's talk page $linkBatch->add( NS_USER_TALK, $row->{$this->userNameField} ); } $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} ); $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row ); } // Fetch rev_len/ar_len for revisions not already scanned above // TODO: is it possible to make this fully abstract? if ( $this->isArchive ) { $parentRevIds = array_diff( $parentRevIds, array_keys( $this->mParentLens ) ); if ( $parentRevIds ) { $result = $this->revisionStore ->newArchiveSelectQueryBuilder( $this->getDatabase() ) ->clearFields() ->fields( [ $this->revisionIdField, $this->revisionLengthField ] ) ->where( [ $this->revisionIdField => $parentRevIds ] ) ->caller( __METHOD__ ) ->fetchResultSet(); foreach ( $result as $row ) { $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField}; } } } $this->mParentLens += $this->revisionStore->getRevisionSizes( array_diff( $parentRevIds, array_keys( $this->mParentLens ) ) ); $linkBatch->execute(); $revisionBatch = $this->commentFormatter->createRevisionBatch() ->authority( $this->getAuthority() ) ->revisions( $revisions ); if ( !$this->isArchive ) { // Only show public comments, because this page might be public $revisionBatch = $revisionBatch->hideIfDeleted(); } $this->formattedComments = $revisionBatch->execute(); # For performance, save the revision objects for later. # The array is indexed by rev_id. doBatchLookups() may be called # multiple times with different results, so merge the revisions array, # ignoring any duplicates. $this->revisions += $revisions; } /** * @inheritDoc */ protected function getStartBody() { return "<section class='mw-pager-body'>\n"; } /** * @inheritDoc */ protected function getEndBody() { return "</section>\n"; } /** * If the object looks like a revision row, or corresponds to a previously * cached revision, return the RevisionRecord. Otherwise, return null. * * @since 1.35 * * @param mixed $row * @param Title|null $title * @return RevisionRecord|null */ public function tryCreatingRevisionRecord( $row, $title = null ) { if ( $row instanceof stdClass && isset( $row->{$this->revisionIdField} ) && isset( $this->revisions[$row->{$this->revisionIdField}] ) ) { return $this->revisions[$row->{$this->revisionIdField}]; } if ( $this->isArchive && $this->revisionStore->isRevisionRow( $row, 'archive' ) ) { return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title ); } if ( !$this->isArchive && $this->revisionStore->isRevisionRow( $row ) ) { return $this->revisionStore->newRevisionFromRow( $row, 0, $title ); } return null; } /** * Create a revision record from a $row that models a revision. * * @param mixed $row * @param Title|null $title * @return RevisionRecord */ public function createRevisionRecord( $row, $title = null ) { if ( $this->isArchive ) { return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title ); } return $this->revisionStore->newRevisionFromRow( $row, 0, $title ); } /** * Populate the HTML attributes. * * @param mixed $row * @param string[] &$attributes */ protected function populateAttributes( $row, &$attributes ) { $attributes['data-mw-revid'] = $this->currentRevRecord->getId(); } /** * Format a link to an article. * * @param mixed $row * @return string */ protected function formatArticleLink( $row ) { if ( !$this->currentPage ) { return ''; } $dir = $this->getLanguage()->getDir(); return Html::rawElement( 'bdi', [ 'dir' => $dir ], $this->getLinkRenderer()->makeLink( $this->currentPage, $this->currentPage->getPrefixedText(), [ 'class' => 'mw-contributions-title' ], $this->currentPage->isRedirect() ? [ 'redirect' => 'no' ] : [] ) ); } /** * Format diff and history links. * * @param mixed $row * @return string */ protected function formatDiffHistLinks( $row ) { if ( !$this->currentPage || !$this->currentRevRecord ) { return ''; } if ( $this->isArchive ) { // Add the same links as DeletedContribsPager::formatRevisionRow $undelete = SpecialPage::getTitleFor( 'Undelete' ); if ( $this->getAuthority()->isAllowed( 'deletedtext' ) ) { $last = $this->getLinkRenderer()->makeKnownLink( $undelete, new HtmlArmor( $this->messages['diff'] ), [], [ 'target' => $this->currentPage->getPrefixedText(), 'timestamp' => $this->currentRevRecord->getTimestamp(), 'diff' => 'prev' ] ); } else { $last = $this->messages['diff']; } $logs = SpecialPage::getTitleFor( 'Log' ); $dellog = $this->getLinkRenderer()->makeKnownLink( $logs, new HtmlArmor( $this->messages['deletionlog'] ), [], [ 'type' => 'delete', 'page' => $this->currentPage->getPrefixedText() ] ); $reviewlink = $this->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Undelete', $this->currentPage->getPrefixedDBkey() ), new HtmlArmor( $this->messages['undeleteviewlink'] ) ); return Html::rawElement( 'span', [ 'class' => 'mw-deletedcontribs-tools' ], $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( [ $last, $dellog, $reviewlink ] ) )->escaped() ); } else { # Is there a visible previous revision? if ( $this->currentRevRecord->getParentId() !== 0 && $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { $difftext = $this->getLinkRenderer()->makeKnownLink( $this->currentPage, new HtmlArmor( $this->messages['diff'] ), [ 'class' => 'mw-changeslist-diff' ], [ 'diff' => 'prev', 'oldid' => $row->{$this->revisionIdField}, ] ); } else { $difftext = $this->messages['diff']; } $histlink = $this->getLinkRenderer()->makeKnownLink( $this->currentPage, new HtmlArmor( $this->messages['hist'] ), [ 'class' => 'mw-changeslist-history' ], [ 'action' => 'history' ] ); // While it might be tempting to use a list here // this would result in clutter and slows down navigating the content // in assistive technology. // See https://phabricator.wikimedia.org/T205581#4734812 return Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ], // The spans are needed to ensure the dividing '|' elements are not // themselves styled as links. Html::rawElement( 'span', [], $difftext ) . ' ' . // Space needed for separating two words. Html::rawElement( 'span', [], $histlink ) ); } } /** * Format a date link. * * @param mixed $row * @return string */ protected function formatDateLink( $row ) { if ( !$this->currentPage || !$this->currentRevRecord ) { return ''; } if ( $this->isArchive ) { $date = $this->getLanguage()->userTimeAndDate( $this->currentRevRecord->getTimestamp(), $this->getUser() ); if ( $this->getAuthority()->isAllowed( 'undelete' ) && $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { $dateLink = $this->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Undelete' ), $date, [ 'class' => 'mw-changeslist-date' ], [ 'target' => $this->currentPage->getPrefixedText(), 'timestamp' => $this->currentRevRecord->getTimestamp() ] ); } else { $dateLink = htmlspecialchars( $date ); } if ( $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { $class = Linker::getRevisionDeletedClass( $this->currentRevRecord ); $dateLink = Html::rawElement( 'span', [ 'class' => $class ], $dateLink ); } } else { $dateLink = ChangesList::revDateLink( $this->currentRevRecord, $this->getAuthority(), $this->getLanguage(), $this->currentPage ); } return $dateLink; } /** * Format annotation and add extra class if a row represents a latest revision. * * @param mixed $row * @param string[] &$classes * @return string */ protected function formatTopMarkText( $row, &$classes ) { if ( !$this->currentPage || !$this->currentRevRecord ) { return ''; } $topmarktext = ''; if ( !$this->isArchive ) { $pagerTools = new PagerTools( $this->currentRevRecord, null, $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new, $this->hookRunner, $this->currentPage, $this->getContext(), $this->getLinkRenderer() ); if ( $row->{$this->revisionIdField} === $row->page_latest ) { $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; $classes[] = 'mw-contributions-current'; } if ( $pagerTools->shouldPreventClickjacking() ) { $this->setPreventClickjacking( true ); } $topmarktext .= $pagerTools->toHTML(); } return $topmarktext; } /** * Format annotation to show the size of a diff. * * @param mixed $row * @return string */ protected function formatCharDiff( $row ) { if ( $row->{$this->revisionParentIdField} === null ) { // For some reason rev_parent_id isn't populated for this row. // Its rumoured this is true on wikipedia for some revisions (T36922). // Next best thing is to have the total number of bytes. $chardiff = ' <span class="mw-changeslist-separator"></span> '; $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} ); $chardiff .= ' <span class="mw-changeslist-separator"></span> '; } else { $parentLen = 0; if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) { $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}]; } $chardiff = ' <span class="mw-changeslist-separator"></span> '; $chardiff .= ChangesList::showCharacterDifference( $parentLen, $row->{$this->revisionLengthField}, $this->getContext() ); $chardiff .= ' <span class="mw-changeslist-separator"></span> '; } return $chardiff; } /** * Format a comment for a revision. * * @param mixed $row * @return string */ protected function formatComment( $row ) { $comment = $this->formattedComments[$row->{$this->revisionIdField}]; if ( $comment === '' ) { $defaultComment = $this->messages['changeslist-nocomment']; $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>"; } // Don't wrap result of this with <bdi> or any other element, see T377555 return $comment; } /** * Format a user link. * * @param mixed $row * @return string */ protected function formatUserLink( $row ) { if ( !$this->currentRevRecord ) { return ''; } $dir = $this->getLanguage()->getDir(); // When the author is different from the target, always show user and user talk links $userlink = ''; $revUser = $this->currentRevRecord->getUser(); $revUserId = $revUser ? $revUser->getId() : 0; $revUserText = $revUser ? $revUser->getName() : ''; if ( $this->target !== $revUserText ) { $userlink = ' <span class="mw-changeslist-separator"></span> ' . Html::rawElement( 'bdi', [ 'dir' => $dir ], Linker::userLink( $revUserId, $revUserText ) ); $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' '; } return $userlink; } /** * @param mixed $row * @return string[] */ protected function formatFlags( $row ) { if ( !$this->currentRevRecord ) { return []; } $flags = []; if ( $this->currentRevRecord->getParentId() === 0 ) { $flags[] = ChangesList::flag( 'newpage' ); } if ( $this->currentRevRecord->isMinor() ) { $flags[] = ChangesList::flag( 'minor' ); } return $flags; } /** * Format link for changing visibility. * * @param mixed $row * @return string */ protected function formatVisibilityLink( $row ) { if ( !$this->currentPage || !$this->currentRevRecord ) { return ''; } $del = Linker::getRevDeleteLink( $this->getAuthority(), $this->currentRevRecord, $this->currentPage ); if ( $del !== '' ) { $del .= ' '; } return $del; } /** * @param mixed $row * @param string[] &$classes * @return string */ protected function formatTags( $row, &$classes ) { # Tags, if any. Save some time using a cache. [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback( $this->tagsCache->makeKey( $row->ts_tags ?? '', $this->getUser()->getName(), $this->getLanguage()->getCode() ), fn () => ChangeTags::formatSummaryRow( $row->ts_tags, null, $this->getContext() ) ); $classes = array_merge( $classes, $newClasses ); return $tagSummary; } /** * Check whether the revision author is deleted * * @param mixed $row * @return bool */ public function revisionUserIsDeleted( $row ) { return $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_USER ); } /** * Generates each row in the contributions list. * * Contributions which are marked "top" are currently on top of the history. * For these contributions, a [rollback] link is shown for users with roll- * back privileges. The rollback link restores the most recent version that * was not written by the target user. * * @todo This would probably look a lot nicer in a table. * @param stdClass|mixed $row * @return string */ public function formatRow( $row ) { $ret = ''; $classes = []; $attribs = []; $this->currentPage = null; $this->currentRevRecord = null; // Create a title for the revision if possible // Rows from the hook may not include title information if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) { $this->currentPage = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} ); } // Flow overrides the ContribsPager::reallyDoQuery hook, causing this // function to be called with a special object for $row. It expects us // skip formatting so that the row can be formatted by the // ContributionsLineEnding hook below. // FIXME: have some better way for extensions to provide formatted rows. $this->currentRevRecord = $this->tryCreatingRevisionRecord( $row, $this->currentPage ); if ( $this->revisionsOnly || ( $this->currentRevRecord && $this->currentPage ) ) { $this->populateAttributes( $row, $attribs ); $templateParams = $this->getTemplateParams( $row, $classes ); $ret = $this->getProcessedTemplate( $templateParams ); } // Let extensions add data $lineEndingsHook = $this->isArchive ? 'onDeletedContributionsLineEnding' : 'onContributionsLineEnding'; $this->hookRunner->$lineEndingsHook( $this, $ret, $row, $classes, $attribs ); $attribs = array_filter( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ], ARRAY_FILTER_USE_KEY ); // TODO: Handle exceptions in the catch block above. Do any extensions rely on // receiving empty rows? if ( $classes === [] && $attribs === [] && $ret === '' ) { wfDebug( "Dropping ContributionsSpecialPage row that could not be formatted" ); return "<!-- Could not format ContributionsSpecialPage row. -->\n"; } $attribs['class'] = $classes; // FIXME: The signature of the ContributionsLineEnding hook makes it // very awkward to move this LI wrapper into the template. return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** * Generate array of template parameters to pass to the template for rendering. * Function can be overriden by classes to add/remove their own parameters. * * @since 1.43 * * @param stdClass|mixed $row * @param string[] &$classes * @return mixed[] */ public function getTemplateParams( $row, &$classes ) { $link = $this->formatArticleLink( $row ); $topmarktext = $this->formatTopMarkText( $row, $classes ); $diffHistLinks = $this->formatDiffHistLinks( $row ); $dateLink = $this->formatDateLink( $row ); $chardiff = $this->formatCharDiff( $row ); $comment = $this->formatComment( $row ); $userlink = $this->formatUserLink( $row ); $flags = $this->formatFlags( $row ); $del = $this->formatVisibilityLink( $row ); $tagSummary = $this->formatTags( $row, $classes ); if ( !$this->isArchive ) { $this->hookRunner->onSpecialContributions__formatRow__flags( $this->getContext(), $row, $flags ); } $templateParams = [ 'del' => $del, 'timestamp' => $dateLink, 'diffHistLinks' => $diffHistLinks, 'charDifference' => $chardiff, 'flags' => $flags, 'articleLink' => $link, 'userlink' => $userlink, 'logText' => $comment, 'topmarktext' => $topmarktext, 'tagSummary' => $tagSummary, ]; # Denote if username is redacted for this edit if ( $this->revisionUserIsDeleted( $row ) ) { $templateParams['rev-deleted-user-contribs'] = $this->msg( 'rev-deleted-user-contribs' )->escaped(); } return $templateParams; } /** * Return the processed template. Function can be overriden by classes * to provide their own template parser. * * @since 1.43 * * @param string[] $templateParams * @return string */ public function getProcessedTemplate( $templateParams ) { return $this->templateParser->processTemplate( 'SpecialContributionsLine', $templateParams ); } /** * Overwrite Pager function and return a helpful comment * @return string */ protected function getSqlComment() { if ( $this->namespace || $this->deletedOnly ) { // potentially slow, see CR r58153 return 'contributions page filtered for namespace or RevisionDeleted edits'; } else { return 'contributions page unfiltered'; } } /** * @deprecated since 1.38, use ::setPreventClickjacking() instead */ protected function preventClickjacking() { $this->setPreventClickjacking( true ); } /** * @param bool $enable * @since 1.38 */ protected function setPreventClickjacking( bool $enable ) { $this->preventClickjacking = $enable; } /** * @return bool */ public function getPreventClickjacking() { return $this->preventClickjacking; } }
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка