MediaWiki master
ContributionsPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use ChangesList;
25use ChangeTags;
26use HtmlArmor;
27use InvalidArgumentException;
28use MapCacheLRU;
48use stdClass;
51
57
59 public $mGroupByDate = true;
60
64 private $messages;
65
69 protected $isArchive;
70
74 protected $target;
75
79 private $namespace;
80
84 private $tagFilter;
85
89 private $tagInvert;
90
94 private $nsInvert;
95
100 private $associated;
101
105 private $deletedOnly;
106
110 private $topOnly;
111
115 private $newOnly;
116
120 private $hideMinor;
121
126 private $revisionsOnly;
127
129 private $preventClickjacking = false;
130
134 private $mParentLens;
135
137 protected $targetUser;
138
139 private TemplateParser $templateParser;
140 private CommentFormatter $commentFormatter;
141 private HookRunner $hookRunner;
142 private LinkBatchFactory $linkBatchFactory;
143 private NamespaceInfo $namespaceInfo;
145
147 private $formattedComments = [];
148
150 private $revisions = [];
151
153 private $tagsCache;
154
159 protected string $revisionIdField = 'rev_id';
160 protected string $revisionParentIdField = 'rev_parent_id';
161 protected string $revisionTimestampField = 'rev_timestamp';
162 protected string $revisionLengthField = 'rev_len';
163 protected string $revisionDeletedField = 'rev_deleted';
164 protected string $revisionMinorField = 'rev_minor_edit';
165 protected string $userNameField = 'rev_user_text';
166 protected string $pageNamespaceField = 'page_namespace';
167 protected string $pageTitleField = 'page_title';
168
181 public function __construct(
182 LinkRenderer $linkRenderer,
183 LinkBatchFactory $linkBatchFactory,
184 HookContainer $hookContainer,
186 NamespaceInfo $namespaceInfo,
187 CommentFormatter $commentFormatter,
188 UserFactory $userFactory,
189 IContextSource $context,
190 array $options,
192 ) {
193 $this->isArchive = $options['isArchive'] ?? false;
194
195 // Set ->target before calling parent::__construct() so
196 // parent can call $this->getIndexField() and get the right result. Set
197 // the rest too just to keep things simple.
198 if ( $targetUser ) {
199 $this->target = $options['target'] ?? $targetUser->getName();
200 $this->targetUser = $targetUser;
201 } else {
202 // Use target option
203 // It's possible for the target to be empty. This is used by
204 // ContribsPagerTest and does not cause newFromName() to return
205 // false. It's probably not used by any production code.
206 $this->target = $options['target'] ?? '';
207 // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
208 $this->targetUser = $userFactory->newFromName(
209 $this->target, UserRigorOptions::RIGOR_NONE
210 );
211 if ( !$this->targetUser ) {
212 // This can happen if the target contained "#". Callers
213 // typically pass user input through title normalization to
214 // avoid it.
215 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
216 'broken to use even with validation disabled.' );
217 }
218 }
219
220 $this->namespace = $options['namespace'] ?? '';
221 $this->tagFilter = $options['tagfilter'] ?? false;
222 $this->tagInvert = $options['tagInvert'] ?? false;
223 $this->nsInvert = $options['nsInvert'] ?? false;
224 $this->associated = $options['associated'] ?? false;
225
226 $this->deletedOnly = !empty( $options['deletedOnly'] );
227 $this->topOnly = !empty( $options['topOnly'] );
228 $this->newOnly = !empty( $options['newOnly'] );
229 $this->hideMinor = !empty( $options['hideMinor'] );
230 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
231
232 parent::__construct( $context, $linkRenderer );
233
234 $msgs = [
235 'diff',
236 'hist',
237 'pipe-separator',
238 'uctop',
239 'changeslist-nocomment',
240 'undeleteviewlink',
241 'undeleteviewlink',
242 'deletionlog',
243 ];
244
245 foreach ( $msgs as $msg ) {
246 $this->messages[$msg] = $this->msg( $msg )->escaped();
247 }
248
249 // Date filtering: use timestamp if available
250 $startTimestamp = '';
251 $endTimestamp = '';
252 if ( isset( $options['start'] ) && $options['start'] ) {
253 $startTimestamp = $options['start'] . ' 00:00:00';
254 }
255 if ( isset( $options['end'] ) && $options['end'] ) {
256 $endTimestamp = $options['end'] . ' 23:59:59';
257 }
258 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
259
260 $this->templateParser = new TemplateParser();
261 $this->linkBatchFactory = $linkBatchFactory;
262 $this->hookRunner = new HookRunner( $hookContainer );
263 $this->revisionStore = $revisionStore;
264 $this->namespaceInfo = $namespaceInfo;
265 $this->commentFormatter = $commentFormatter;
266 $this->tagsCache = new MapCacheLRU( 50 );
267 }
268
269 public function getDefaultQuery() {
270 $query = parent::getDefaultQuery();
271 $query['target'] = $this->target;
272
273 return $query;
274 }
275
285 public function reallyDoQuery( $offset, $limit, $order ) {
286 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo(
287 $offset,
288 $limit,
289 $order
290 );
291
292 $options['MAX_EXECUTION_TIME'] =
294 /*
295 * This hook will allow extensions to add in additional queries, so they can get their data
296 * in My Contributions as well. Extensions should append their results to the $data array.
297 *
298 * Extension queries have to implement the navbar requirement as well. They should
299 * - have a column aliased as $pager->getIndexField()
300 * - have LIMIT set
301 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
302 * - have the ORDER BY specified based upon the details provided by the navbar
303 *
304 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
305 *
306 * &$data: an array of results of all contribs queries
307 * $pager: the ContribsPager object hooked into
308 * $offset: see phpdoc above
309 * $limit: see phpdoc above
310 * $descending: see phpdoc above
311 */
312 $dbr = $this->getDatabase();
313 $data = [ $dbr->newSelectQueryBuilder()
314 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
315 ->fields( $fields )
316 ->conds( $conds )
317 ->caller( $fname )
318 ->options( $options )
319 ->joinConds( $join_conds )
320 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
321 ->fetchResultSet() ];
322 if ( !$this->revisionsOnly ) {
323 // These hooks were moved from ContribsPager and DeletedContribsPager. For backwards
324 // compatability, they keep the same names. But they should be run for any contributions
325 // pager, otherwise the entries from extensions would be missing.
326 $reallyDoQueryHook = $this->isArchive ?
327 'onDeletedContribsPager__reallyDoQuery' :
328 'onContribsPager__reallyDoQuery';
329 // TODO: Range offsets are fairly important and all handlers should take care of it.
330 // If this hook will be replaced (e.g. unified with the DeletedContribsPager one),
331 // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577).
332 $this->hookRunner->$reallyDoQueryHook( $data, $this, $offset, $limit, $order );
333 }
334
335 $result = [];
336
337 // loop all results and collect them in an array
338 foreach ( $data as $query ) {
339 foreach ( $query as $i => $row ) {
340 // If the query results are in descending order, the indexes must also be in descending order
341 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
342 // Left-pad with zeroes, because these values will be sorted as strings
343 $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
344 // use index column as key, allowing us to easily sort in PHP
345 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
346 }
347 }
348
349 // sort results
350 if ( $order === self::QUERY_ASCENDING ) {
351 ksort( $result );
352 } else {
353 krsort( $result );
354 }
355
356 // enforce limit
357 $result = array_slice( $result, 0, $limit );
358
359 // get rid of array keys
360 $result = array_values( $result );
361
362 return new FakeResultWrapper( $result );
363 }
364
371 abstract protected function getRevisionQuery();
372
373 public function getQueryInfo() {
374 $queryInfo = $this->getRevisionQuery();
375
376 if ( $this->deletedOnly ) {
377 $queryInfo['conds'][] = $this->revisionDeletedField . ' != 0';
378 }
379
380 if ( !$this->isArchive && $this->topOnly ) {
381 $queryInfo['conds'][] = $this->revisionIdField . ' = page_latest';
382 }
383
384 if ( $this->newOnly ) {
385 $queryInfo['conds'][] = $this->revisionParentIdField . ' = 0';
386 }
387
388 if ( $this->hideMinor ) {
389 $queryInfo['conds'][] = $this->revisionMinorField . ' = 0';
390 }
391
392 $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
393
394 // Paranoia: avoid brute force searches (T19342)
395 $dbr = $this->getDatabase();
396 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
397 $queryInfo['conds'][] = $dbr->bitAnd(
398 $this->revisionDeletedField, RevisionRecord::DELETED_USER
399 ) . ' = 0';
400 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
401 $queryInfo['conds'][] = $dbr->bitAnd(
402 $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER
403 ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
404 }
405
406 // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
407 $indexField = $this->getIndexField();
408 if ( $indexField !== $this->revisionTimestampField ) {
409 $queryInfo['fields'][] = $indexField;
410 }
411
413 $queryInfo['tables'],
414 $queryInfo['fields'],
415 $queryInfo['conds'],
416 $queryInfo['join_conds'],
417 $queryInfo['options'],
418 $this->tagFilter,
419 $this->tagInvert,
420 );
421
422 if ( !$this->isArchive ) {
423 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
424 }
425
426 return $queryInfo;
427 }
428
429 protected function getNamespaceCond() {
430 if ( $this->namespace !== '' ) {
431 $dbr = $this->getDatabase();
432 $namespaces = [ $this->namespace ];
433 $eq_op = $this->nsInvert ? '!=' : '=';
434 if ( $this->associated ) {
435 $namespaces[] = $this->namespaceInfo->getAssociated( $this->namespace );
436 }
437 return [ $dbr->expr( $this->pageNamespaceField, $eq_op, $namespaces ) ];
438 }
439
440 return [];
441 }
442
446 public function getTagFilter() {
447 return $this->tagFilter;
448 }
449
453 public function getTagInvert() {
454 return $this->tagInvert;
455 }
456
460 public function getTarget() {
461 return $this->target;
462 }
463
467 public function isNewOnly() {
468 return $this->newOnly;
469 }
470
474 public function getNamespace() {
475 return $this->namespace;
476 }
477
478 protected function doBatchLookups() {
479 # Do a link batch query
480 $this->mResult->seek( 0 );
481 $parentRevIds = [];
482 $this->mParentLens = [];
483 $revisions = [];
484 $linkBatch = $this->linkBatchFactory->newLinkBatch();
485 # Give some pointers to make (last) links
486 foreach ( $this->mResult as $row ) {
487 if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) {
488 $parentRevIds[] = (int)$row->{$this->revisionParentIdField};
489 }
490 if ( $this->revisionStore->isRevisionRow( $row, $this->isArchive ? 'archive' : 'revision' ) ) {
491 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
492 if ( $this->target !== $row->{$this->userNameField} ) {
493 // If the target does not match the author, batch the author's talk page
494 $linkBatch->add( NS_USER_TALK, $row->{$this->userNameField} );
495 }
496 $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
497 $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row );
498 }
499 }
500 // Fetch rev_len/ar_len for revisions not already scanned above
501 // TODO: is it possible to make this fully abstract?
502 if ( $this->isArchive ) {
503 $parentRevIds = array_diff( $parentRevIds, array_keys( $this->mParentLens ) );
504 if ( $parentRevIds ) {
505 $result = $this->revisionStore
506 ->newArchiveSelectQueryBuilder( $this->getDatabase() )
507 ->clearFields()
508 ->fields( [ $this->revisionIdField, $this->revisionLengthField ] )
509 ->where( [ $this->revisionIdField => $parentRevIds ] )
510 ->caller( __METHOD__ )
511 ->fetchResultSet();
512 foreach ( $result as $row ) {
513 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
514 }
515 }
516 }
517 $this->mParentLens += $this->revisionStore->getRevisionSizes(
518 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
519 );
520 $linkBatch->execute();
521
522 $revisionBatch = $this->commentFormatter->createRevisionBatch()
523 ->authority( $this->getAuthority() )
524 ->revisions( $revisions );
525
526 if ( !$this->isArchive ) {
527 // Only show public comments, because this page might be public
528 $revisionBatch = $revisionBatch->hideIfDeleted();
529 }
530
531 $this->formattedComments = $revisionBatch->execute();
532
533 # For performance, save the revision objects for later.
534 # The array is indexed by rev_id. doBatchLookups() may be called
535 # multiple times with different results, so merge the revisions array,
536 # ignoring any duplicates.
537 $this->revisions += $revisions;
538 }
539
543 protected function getStartBody() {
544 return "<section class='mw-pager-body'>\n";
545 }
546
550 protected function getEndBody() {
551 return "</section>\n";
552 }
553
564 public function tryCreatingRevisionRecord( $row, $title = null ) {
565 if ( $row instanceof stdClass && isset( $row->{$this->revisionIdField} )
566 && isset( $this->revisions[$row->{$this->revisionIdField}] )
567 ) {
568 return $this->revisions[$row->{$this->revisionIdField}];
569 }
570
571 if (
572 $this->isArchive &&
573 $this->revisionStore->isRevisionRow( $row, 'archive' )
574 ) {
575 return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title );
576 }
577
578 if (
579 !$this->isArchive &&
580 $this->revisionStore->isRevisionRow( $row )
581 ) {
582 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
583 }
584
585 return null;
586 }
587
595 public function createRevisionRecord( $row, $title = null ) {
596 if ( $this->isArchive ) {
597 return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title );
598 }
599
600 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
601 }
602
615 public function formatRow( $row ) {
616 $ret = '';
617 $classes = [];
618 $attribs = [];
619 $authority = $this->getAuthority();
620 $language = $this->getLanguage();
621
622 $linkRenderer = $this->getLinkRenderer();
623
624 $page = null;
625 // Create a title for the revision if possible
626 // Rows from the hook may not include title information
627 if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) {
628 $page = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
629 }
630
631 $dir = $language->getDir();
632
633 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
634 // function to be called with a special object for $row. It expects us
635 // skip formatting so that the row can be formatted by the
636 // ContributionsLineEnding hook below.
637 // FIXME: have some better way for extensions to provide formatted rows.
638 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
639 if ( $revRecord && $page ) {
640 $revRecord = $this->createRevisionRecord( $row, $page );
641 $attribs['data-mw-revid'] = $revRecord->getId();
642
643 $link = Html::rawElement( 'bdi', [ 'dir' => $dir ], $linkRenderer->makeLink(
644 $page,
645 $page->getPrefixedText(),
646 [ 'class' => 'mw-contributions-title' ],
647 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
648 ) );
649 # Mark current revisions
650 $topmarktext = '';
651
652 // Add links for seeing history, diff, etc.
653 if ( $this->isArchive ) {
654 // Add the same links as DeletedContribsPager::formatRevisionRow
655 $undelete = SpecialPage::getTitleFor( 'Undelete' );
656 if ( $authority->isAllowed( 'deletedtext' ) ) {
657 $last = $linkRenderer->makeKnownLink(
658 $undelete,
659 new HtmlArmor( $this->messages['diff'] ),
660 [],
661 [
662 'target' => $page->getPrefixedText(),
663 'timestamp' => $revRecord->getTimestamp(),
664 'diff' => 'prev'
665 ]
666 );
667 } else {
668 $last = $this->messages['diff'];
669 }
670
671 $logs = SpecialPage::getTitleFor( 'Log' );
672 $dellog = $linkRenderer->makeKnownLink(
673 $logs,
674 new HtmlArmor( $this->messages['deletionlog'] ),
675 [],
676 [
677 'type' => 'delete',
678 'page' => $page->getPrefixedText()
679 ]
680 );
681
682 $reviewlink = $linkRenderer->makeKnownLink(
683 SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
684 new HtmlArmor( $this->messages['undeleteviewlink'] )
685 );
686
687 $diffHistLinks = Html::rawElement(
688 'span',
689 [ 'class' => 'mw-deletedcontribs-tools' ],
690 $this->msg( 'parentheses' )->rawParams( $language->pipeList(
691 [ $last, $dellog, $reviewlink ] ) )->escaped()
692 );
693
694 $date = $language->userTimeAndDate(
695 $revRecord->getTimestamp(),
696 $this->getUser()
697 );
698
699 if ( $authority->isAllowed( 'undelete' ) &&
700 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $authority )
701 ) {
702 $dateLink = $linkRenderer->makeKnownLink(
703 SpecialPage::getTitleFor( 'Undelete' ),
704 $date,
705 [ 'class' => 'mw-changeslist-date' ],
706 [
707 'target' => $page->getPrefixedText(),
708 'timestamp' => $revRecord->getTimestamp()
709 ]
710 );
711 } else {
712 $dateLink = htmlspecialchars( $date );
713 }
714 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
715 $class = Linker::getRevisionDeletedClass( $revRecord );
716 $dateLink = Html::rawElement(
717 'span',
718 [ 'class' => $class ],
719 $dateLink
720 );
721 }
722
723 } else {
724 $pagerTools = new PagerTools(
725 $revRecord,
726 null,
727 $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new,
728 $this->hookRunner,
729 $page,
730 $this->getContext(),
731 $this->getLinkRenderer()
732 );
733 if ( $row->{$this->revisionIdField} === $row->page_latest ) {
734 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
735 $classes[] = 'mw-contributions-current';
736 }
737 if ( $pagerTools->shouldPreventClickjacking() ) {
738 $this->setPreventClickjacking( true );
739 }
740 $topmarktext .= $pagerTools->toHTML();
741 # Is there a visible previous revision?
742 if ( $revRecord->getParentId() !== 0 &&
743 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $authority )
744 ) {
745 $difftext = $linkRenderer->makeKnownLink(
746 $page,
747 new HtmlArmor( $this->messages['diff'] ),
748 [ 'class' => 'mw-changeslist-diff' ],
749 [
750 'diff' => 'prev',
751 'oldid' => $row->{$this->revisionIdField},
752 ]
753 );
754 } else {
755 $difftext = $this->messages['diff'];
756 }
757 $histlink = $linkRenderer->makeKnownLink(
758 $page,
759 new HtmlArmor( $this->messages['hist'] ),
760 [ 'class' => 'mw-changeslist-history' ],
761 [ 'action' => 'history' ]
762 );
763
764 // While it might be tempting to use a list here
765 // this would result in clutter and slows down navigating the content
766 // in assistive technology.
767 // See https://phabricator.wikimedia.org/T205581#4734812
768 $diffHistLinks = Html::rawElement( 'span',
769 [ 'class' => 'mw-changeslist-links' ],
770 // The spans are needed to ensure the dividing '|' elements are not
771 // themselves styled as links.
772 Html::rawElement( 'span', [], $difftext ) .
773 ' ' . // Space needed for separating two words.
774 Html::rawElement( 'span', [], $histlink )
775 );
776
777 $dateLink = ChangesList::revDateLink( $revRecord, $authority, $language, $page );
778 }
779
780 if ( $row->{$this->revisionParentIdField} === null ) {
781 // For some reason rev_parent_id isn't populated for this row.
782 // Its rumoured this is true on wikipedia for some revisions (T36922).
783 // Next best thing is to have the total number of bytes.
784 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
785 $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} );
786 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
787 } else {
788 $parentLen = 0;
789 if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) {
790 $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}];
791 }
792
793 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
795 $parentLen,
796 $row->{$this->revisionLengthField},
797 $this->getContext()
798 );
799 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
800 }
801
802 $comment = $this->formattedComments[$row->{$this->revisionIdField}];
803
804 if ( $comment === '' ) {
805 $defaultComment = $this->messages['changeslist-nocomment'];
806 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
807 }
808
809 $comment = Html::rawElement( 'bdi', [ 'dir' => $dir ], $comment );
810
811 // When the author is different from the target, always show user and user talk links
812 $userlink = '';
813 $revUser = $revRecord->getUser();
814 $revUserId = $revUser ? $revUser->getId() : 0;
815 $revUserText = $revUser ? $revUser->getName() : '';
816 if ( $this->target !== $revUserText ) {
817 $userlink = ' <span class="mw-changeslist-separator"></span> '
818 . Html::rawElement( 'bdi', [ 'dir' => $dir ],
819 Linker::userLink( $revUserId, $revUserText ) );
820 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
821 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
822 }
823
824 $flags = [];
825 if ( $revRecord->getParentId() === 0 ) {
826 $flags[] = ChangesList::flag( 'newpage' );
827 }
828
829 if ( $revRecord->isMinor() ) {
830 $flags[] = ChangesList::flag( 'minor' );
831 }
832
833 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
834 if ( $del !== '' ) {
835 $del .= ' ';
836 }
837
838 # Tags, if any. Save some time using a cache.
839 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
840 $this->tagsCache->makeKey(
841 $row->ts_tags ?? '',
842 $this->getUser()->getName(),
843 $language->getCode()
844 ),
846 $row->ts_tags,
847 null,
848 $this->getContext()
849 )
850 );
851 $classes = array_merge( $classes, $newClasses );
852
853 if ( !$this->isArchive ) {
854 $this->hookRunner->onSpecialContributions__formatRow__flags(
855 $this->getContext(), $row, $flags );
856 }
857
858 $templateParams = [
859 'del' => $del,
860 'timestamp' => $dateLink,
861 'diffHistLinks' => $diffHistLinks,
862 'charDifference' => $chardiff,
863 'flags' => $flags,
864 'articleLink' => $link,
865 'userlink' => $userlink,
866 'logText' => $comment,
867 'topmarktext' => $topmarktext,
868 'tagSummary' => $tagSummary,
869 ];
870
871 # Denote if username is redacted for this edit
872 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
873 $templateParams['rev-deleted-user-contribs'] =
874 $this->msg( 'rev-deleted-user-contribs' )->escaped();
875 }
876
877 $ret = $this->templateParser->processTemplate(
878 'SpecialContributionsLine',
879 $templateParams
880 );
881 }
882
883 // Let extensions add data
884 $lineEndingsHook = $this->isArchive ?
885 'onDeletedContributionsLineEnding' :
886 'onContributionsLineEnding';
887 $this->hookRunner->$lineEndingsHook( $this, $ret, $row, $classes, $attribs );
888 $attribs = array_filter( $attribs,
889 [ Sanitizer::class, 'isReservedDataAttribute' ],
890 ARRAY_FILTER_USE_KEY
891 );
892
893 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
894 // receiving empty rows?
895
896 if ( $classes === [] && $attribs === [] && $ret === '' ) {
897 wfDebug( "Dropping ContributionsSpecialPage row that could not be formatted" );
898 return "<!-- Could not format ContributionsSpecialPage row. -->\n";
899 }
900 $attribs['class'] = $classes;
901
902 // FIXME: The signature of the ContributionsLineEnding hook makes it
903 // very awkward to move this LI wrapper into the template.
904 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
905 }
906
911 protected function getSqlComment() {
912 if ( $this->namespace || $this->deletedOnly ) {
913 // potentially slow, see CR r58153
914 return 'contributions page filtered for namespace or RevisionDeleted edits';
915 } else {
916 return 'contributions page unfiltered';
917 }
918 }
919
923 protected function preventClickjacking() {
924 $this->setPreventClickjacking( true );
925 }
926
931 protected function setPreventClickjacking( bool $enable ) {
932 $this->preventClickjacking = $enable;
933 }
934
938 public function getPreventClickjacking() {
939 return $this->preventClickjacking;
940 }
941
942}
const NS_USER_TALK
Definition Defines.php:68
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Recent changes tagging.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Base class for lists of recent changes shown on special pages.
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null, $className='')
Render the date and time of a revision in the current user language based on whether the user is able...
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Store key-value entries in a size-limited in-memory LRU cache.
This is the main service interface for converting single-line comments from various DB comment fields...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:63
A class containing constants representing the names of configuration variables.
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
Pager for Special:Contributions.
getRevisionQuery()
Get queryInfo for the main query selecting revisions, not including filtering on namespace,...
getQueryInfo()
Provides all parameters needed for the main paged query.
string $revisionIdField
Field names for various attributes.
reallyDoQuery( $offset, $limit, $order)
This method basically executes the exact same code as the parent class, though with a hook added,...
getEndBody()
Hook into getBody() for the end of the list.to overridestring
createRevisionRecord( $row, $title=null)
Create a revision record from a $row that models a revision.
string $target
User name, or a string describing an IP address range.
getSqlComment()
Overwrite Pager function and return a helpful comment.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
tryCreatingRevisionRecord( $row, $title=null)
If the object looks like a revision row, or corresponds to a previously cached revision,...
bool $isArchive
Get revisions from the archive table (if true) or the revision table (if false)
__construct(LinkRenderer $linkRenderer, LinkBatchFactory $linkBatchFactory, HookContainer $hookContainer, RevisionStore $revisionStore, NamespaceInfo $namespaceInfo, CommentFormatter $commentFormatter, UserFactory $userFactory, IContextSource $context, array $options, ?UserIdentity $targetUser)
formatRow( $row)
Generates each row in the contributions list.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
getStartBody()
Hook into getBody(), allows text to be inserted at the start.This will be called even if there are no...
getDatabase()
Get the Database object in use.
getIndexField()
Returns the name of the index field.
Pager for filtering by a range of dates.
getDateRangeCond( $startTime, $endTime)
Set and return a date range condition using timestamps provided by the user.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.For b/c, query direction is true for ascending and fal...
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Page revision base class.
Service for looking up page revisions.
Parent class for all special pages.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:78
Creates User objects.
newFromName(string $name, string $validate=self::RIGOR_VALID)
Factory method for creating users by name, replacing static User::newFromName.
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Result wrapper for grabbing data queried from an IDatabase object.