MediaWiki 1.43.1
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;
49use stdClass;
52
58
60 public $mGroupByDate = true;
61
65 protected $messages;
66
70 protected $isArchive;
71
75 protected $target;
76
80 private $namespace;
81
85 private $tagFilter;
86
90 private $tagInvert;
91
95 private $nsInvert;
96
101 private $associated;
102
106 private $deletedOnly;
107
111 private $topOnly;
112
116 private $newOnly;
117
121 private $hideMinor;
122
127 private $revisionsOnly;
128
130 private $preventClickjacking = false;
131
132 protected ?Title $currentPage;
134
138 private $mParentLens;
139
141 protected $targetUser;
142
147
148 private CommentFormatter $commentFormatter;
149 private HookRunner $hookRunner;
150 private LinkBatchFactory $linkBatchFactory;
151 private NamespaceInfo $namespaceInfo;
153
155 private $formattedComments = [];
156
158 private $revisions = [];
159
161 private $tagsCache;
162
167 protected string $revisionIdField = 'rev_id';
168 protected string $revisionParentIdField = 'rev_parent_id';
169 protected string $revisionTimestampField = 'rev_timestamp';
170 protected string $revisionLengthField = 'rev_len';
171 protected string $revisionDeletedField = 'rev_deleted';
172 protected string $revisionMinorField = 'rev_minor_edit';
173 protected string $userNameField = 'rev_user_text';
174 protected string $pageNamespaceField = 'page_namespace';
175 protected string $pageTitleField = 'page_title';
176
189 public function __construct(
190 LinkRenderer $linkRenderer,
191 LinkBatchFactory $linkBatchFactory,
192 HookContainer $hookContainer,
194 NamespaceInfo $namespaceInfo,
195 CommentFormatter $commentFormatter,
196 UserFactory $userFactory,
197 IContextSource $context,
198 array $options,
200 ) {
201 $this->isArchive = $options['isArchive'] ?? false;
202
203 // Set ->target before calling parent::__construct() so
204 // parent can call $this->getIndexField() and get the right result. Set
205 // the rest too just to keep things simple.
206 if ( $targetUser ) {
207 $this->target = $options['target'] ?? $targetUser->getName();
208 $this->targetUser = $targetUser;
209 } else {
210 // Use target option
211 // It's possible for the target to be empty. This is used by
212 // ContribsPagerTest and does not cause newFromName() to return
213 // false. It's probably not used by any production code.
214 $this->target = $options['target'] ?? '';
215 // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
216 $this->targetUser = $userFactory->newFromName(
217 $this->target, UserRigorOptions::RIGOR_NONE
218 );
219 if ( !$this->targetUser ) {
220 // This can happen if the target contained "#". Callers
221 // typically pass user input through title normalization to
222 // avoid it.
223 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
224 'broken to use even with validation disabled.' );
225 }
226 }
227
228 $this->namespace = $options['namespace'] ?? '';
229 $this->tagFilter = $options['tagfilter'] ?? false;
230 $this->tagInvert = $options['tagInvert'] ?? false;
231 $this->nsInvert = $options['nsInvert'] ?? false;
232 $this->associated = $options['associated'] ?? false;
233
234 $this->deletedOnly = !empty( $options['deletedOnly'] );
235 $this->topOnly = !empty( $options['topOnly'] );
236 $this->newOnly = !empty( $options['newOnly'] );
237 $this->hideMinor = !empty( $options['hideMinor'] );
238 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
239
240 parent::__construct( $context, $linkRenderer );
241
242 $msgs = [
243 'diff',
244 'hist',
245 'pipe-separator',
246 'uctop',
247 'changeslist-nocomment',
248 'undeleteviewlink',
249 'undeleteviewlink',
250 'deletionlog',
251 ];
252
253 foreach ( $msgs as $msg ) {
254 $this->messages[$msg] = $this->msg( $msg )->escaped();
255 }
256
257 // Date filtering: use timestamp if available
258 $startTimestamp = '';
259 $endTimestamp = '';
260 if ( isset( $options['start'] ) && $options['start'] ) {
261 $startTimestamp = $options['start'] . ' 00:00:00';
262 }
263 if ( isset( $options['end'] ) && $options['end'] ) {
264 $endTimestamp = $options['end'] . ' 23:59:59';
265 }
266 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
267
268 $this->templateParser = new TemplateParser();
269 $this->linkBatchFactory = $linkBatchFactory;
270 $this->hookRunner = new HookRunner( $hookContainer );
271 $this->revisionStore = $revisionStore;
272 $this->namespaceInfo = $namespaceInfo;
273 $this->commentFormatter = $commentFormatter;
274 $this->tagsCache = new MapCacheLRU( 50 );
275 }
276
277 public function getDefaultQuery() {
278 $query = parent::getDefaultQuery();
279 $query['target'] = $this->target;
280
281 return $query;
282 }
283
293 public function reallyDoQuery( $offset, $limit, $order ) {
294 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo(
295 $offset,
296 $limit,
297 $order
298 );
299
300 $options['MAX_EXECUTION_TIME'] =
302 /*
303 * This hook will allow extensions to add in additional queries, so they can get their data
304 * in My Contributions as well. Extensions should append their results to the $data array.
305 *
306 * Extension queries have to implement the navbar requirement as well. They should
307 * - have a column aliased as $pager->getIndexField()
308 * - have LIMIT set
309 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
310 * - have the ORDER BY specified based upon the details provided by the navbar
311 *
312 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
313 *
314 * &$data: an array of results of all contribs queries
315 * $pager: the ContribsPager object hooked into
316 * $offset: see phpdoc above
317 * $limit: see phpdoc above
318 * $descending: see phpdoc above
319 */
320 $dbr = $this->getDatabase();
321 $data = [ $dbr->newSelectQueryBuilder()
322 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
323 ->fields( $fields )
324 ->conds( $conds )
325 ->caller( $fname )
326 ->options( $options )
327 ->joinConds( $join_conds )
328 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
329 ->fetchResultSet() ];
330 if ( !$this->revisionsOnly ) {
331 // These hooks were moved from ContribsPager and DeletedContribsPager. For backwards
332 // compatability, they keep the same names. But they should be run for any contributions
333 // pager, otherwise the entries from extensions would be missing.
334 $reallyDoQueryHook = $this->isArchive ?
335 'onDeletedContribsPager__reallyDoQuery' :
336 'onContribsPager__reallyDoQuery';
337 // TODO: Range offsets are fairly important and all handlers should take care of it.
338 // If this hook will be replaced (e.g. unified with the DeletedContribsPager one),
339 // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577).
340 $this->hookRunner->$reallyDoQueryHook( $data, $this, $offset, $limit, $order );
341 }
342
343 $result = [];
344
345 // loop all results and collect them in an array
346 foreach ( $data as $query ) {
347 foreach ( $query as $i => $row ) {
348 // If the query results are in descending order, the indexes must also be in descending order
349 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
350 // Left-pad with zeroes, because these values will be sorted as strings
351 $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
352 // use index column as key, allowing us to easily sort in PHP
353 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
354 }
355 }
356
357 // sort results
358 if ( $order === self::QUERY_ASCENDING ) {
359 ksort( $result );
360 } else {
361 krsort( $result );
362 }
363
364 // enforce limit
365 $result = array_slice( $result, 0, $limit );
366
367 // get rid of array keys
368 $result = array_values( $result );
369
370 return new FakeResultWrapper( $result );
371 }
372
379 abstract protected function getRevisionQuery();
380
381 public function getQueryInfo() {
382 $queryInfo = $this->getRevisionQuery();
383
384 if ( $this->deletedOnly ) {
385 $queryInfo['conds'][] = $this->revisionDeletedField . ' != 0';
386 }
387
388 if ( !$this->isArchive && $this->topOnly ) {
389 $queryInfo['conds'][] = $this->revisionIdField . ' = page_latest';
390 }
391
392 if ( $this->newOnly ) {
393 $queryInfo['conds'][] = $this->revisionParentIdField . ' = 0';
394 }
395
396 if ( $this->hideMinor ) {
397 $queryInfo['conds'][] = $this->revisionMinorField . ' = 0';
398 }
399
400 $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
401
402 // Paranoia: avoid brute force searches (T19342)
403 $dbr = $this->getDatabase();
404 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
405 $queryInfo['conds'][] = $dbr->bitAnd(
406 $this->revisionDeletedField, RevisionRecord::DELETED_USER
407 ) . ' = 0';
408 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
409 $queryInfo['conds'][] = $dbr->bitAnd(
410 $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER
411 ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
412 }
413
414 // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
415 $indexField = $this->getIndexField();
416 if ( $indexField !== $this->revisionTimestampField ) {
417 $queryInfo['fields'][] = $indexField;
418 }
419
420 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
421 $queryInfo['tables'],
422 $queryInfo['fields'],
423 $queryInfo['conds'],
424 $queryInfo['join_conds'],
425 $queryInfo['options'],
426 $this->tagFilter,
427 $this->tagInvert,
428 );
429
430 if ( !$this->isArchive ) {
431 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
432 }
433
434 return $queryInfo;
435 }
436
437 protected function getNamespaceCond() {
438 if ( $this->namespace !== '' ) {
439 $dbr = $this->getDatabase();
440 $namespaces = [ $this->namespace ];
441 $eq_op = $this->nsInvert ? '!=' : '=';
442 if ( $this->associated ) {
443 $namespaces[] = $this->namespaceInfo->getAssociated( $this->namespace );
444 }
445 return [ $dbr->expr( $this->pageNamespaceField, $eq_op, $namespaces ) ];
446 }
447
448 return [];
449 }
450
454 public function getTagFilter() {
455 return $this->tagFilter;
456 }
457
461 public function getTagInvert() {
462 return $this->tagInvert;
463 }
464
468 public function getTarget() {
469 return $this->target;
470 }
471
475 public function isNewOnly() {
476 return $this->newOnly;
477 }
478
482 public function getNamespace() {
483 return $this->namespace;
484 }
485
486 protected function doBatchLookups() {
487 # Do a link batch query
488 $this->mResult->seek( 0 );
489 $parentRevIds = [];
490 $this->mParentLens = [];
491 $revisions = [];
492 $linkBatch = $this->linkBatchFactory->newLinkBatch();
493 # Give some pointers to make (last) links
494 foreach ( $this->mResult as $row ) {
495 $revisionRecord = $this->tryCreatingRevisionRecord( $row );
496 if ( !$revisionRecord ) {
497 continue;
498 }
499 if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) {
500 $parentRevIds[] = (int)$row->{$this->revisionParentIdField};
501 }
502 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
503 if ( $this->target !== $row->{$this->userNameField} ) {
504 // If the target does not match the author, batch the author's talk page
505 $linkBatch->add( NS_USER_TALK, $row->{$this->userNameField} );
506 }
507 $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
508 $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row );
509 }
510 // Fetch rev_len/ar_len for revisions not already scanned above
511 // TODO: is it possible to make this fully abstract?
512 if ( $this->isArchive ) {
513 $parentRevIds = array_diff( $parentRevIds, array_keys( $this->mParentLens ) );
514 if ( $parentRevIds ) {
515 $result = $this->revisionStore
516 ->newArchiveSelectQueryBuilder( $this->getDatabase() )
517 ->clearFields()
518 ->fields( [ $this->revisionIdField, $this->revisionLengthField ] )
519 ->where( [ $this->revisionIdField => $parentRevIds ] )
520 ->caller( __METHOD__ )
521 ->fetchResultSet();
522 foreach ( $result as $row ) {
523 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
524 }
525 }
526 }
527 $this->mParentLens += $this->revisionStore->getRevisionSizes(
528 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
529 );
530 $linkBatch->execute();
531
532 $revisionBatch = $this->commentFormatter->createRevisionBatch()
533 ->authority( $this->getAuthority() )
534 ->revisions( $revisions );
535
536 if ( !$this->isArchive ) {
537 // Only show public comments, because this page might be public
538 $revisionBatch = $revisionBatch->hideIfDeleted();
539 }
540
541 $this->formattedComments = $revisionBatch->execute();
542
543 # For performance, save the revision objects for later.
544 # The array is indexed by rev_id. doBatchLookups() may be called
545 # multiple times with different results, so merge the revisions array,
546 # ignoring any duplicates.
547 $this->revisions += $revisions;
548 }
549
553 protected function getStartBody() {
554 return "<section class='mw-pager-body'>\n";
555 }
556
560 protected function getEndBody() {
561 return "</section>\n";
562 }
563
574 public function tryCreatingRevisionRecord( $row, $title = null ) {
575 if ( $row instanceof stdClass && isset( $row->{$this->revisionIdField} )
576 && isset( $this->revisions[$row->{$this->revisionIdField}] )
577 ) {
578 return $this->revisions[$row->{$this->revisionIdField}];
579 }
580
581 if (
582 $this->isArchive &&
583 $this->revisionStore->isRevisionRow( $row, 'archive' )
584 ) {
585 return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title );
586 }
587
588 if (
589 !$this->isArchive &&
590 $this->revisionStore->isRevisionRow( $row )
591 ) {
592 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
593 }
594
595 return null;
596 }
597
605 public function createRevisionRecord( $row, $title = null ) {
606 if ( $this->isArchive ) {
607 return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title );
608 }
609
610 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
611 }
612
619 protected function populateAttributes( $row, &$attributes ) {
620 $attributes['data-mw-revid'] = $this->currentRevRecord->getId();
621 }
622
629 protected function formatArticleLink( $row ) {
630 if ( !$this->currentPage ) {
631 return '';
632 }
633 $dir = $this->getLanguage()->getDir();
634 return Html::rawElement( 'bdi', [ 'dir' => $dir ], $this->getLinkRenderer()->makeLink(
635 $this->currentPage,
636 $this->currentPage->getPrefixedText(),
637 [ 'class' => 'mw-contributions-title' ],
638 $this->currentPage->isRedirect() ? [ 'redirect' => 'no' ] : []
639 ) );
640 }
641
648 protected function formatDiffHistLinks( $row ) {
649 if ( !$this->currentPage || !$this->currentRevRecord ) {
650 return '';
651 }
652 if ( $this->isArchive ) {
653 // Add the same links as DeletedContribsPager::formatRevisionRow
654 $undelete = SpecialPage::getTitleFor( 'Undelete' );
655 if ( $this->getAuthority()->isAllowed( 'deletedtext' ) ) {
656 $last = $this->getLinkRenderer()->makeKnownLink(
657 $undelete,
658 new HtmlArmor( $this->messages['diff'] ),
659 [],
660 [
661 'target' => $this->currentPage->getPrefixedText(),
662 'timestamp' => $this->currentRevRecord->getTimestamp(),
663 'diff' => 'prev'
664 ]
665 );
666 } else {
667 $last = $this->messages['diff'];
668 }
669
670 $logs = SpecialPage::getTitleFor( 'Log' );
671 $dellog = $this->getLinkRenderer()->makeKnownLink(
672 $logs,
673 new HtmlArmor( $this->messages['deletionlog'] ),
674 [],
675 [
676 'type' => 'delete',
677 'page' => $this->currentPage->getPrefixedText()
678 ]
679 );
680
681 $reviewlink = $this->getLinkRenderer()->makeKnownLink(
682 SpecialPage::getTitleFor( 'Undelete', $this->currentPage->getPrefixedDBkey() ),
683 new HtmlArmor( $this->messages['undeleteviewlink'] )
684 );
685
686 return Html::rawElement(
687 'span',
688 [ 'class' => 'mw-deletedcontribs-tools' ],
689 $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList(
690 [ $last, $dellog, $reviewlink ] ) )->escaped()
691 );
692 } else {
693 # Is there a visible previous revision?
694 if ( $this->currentRevRecord->getParentId() !== 0 &&
695 $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
696 ) {
697 $difftext = $this->getLinkRenderer()->makeKnownLink(
698 $this->currentPage,
699 new HtmlArmor( $this->messages['diff'] ),
700 [ 'class' => 'mw-changeslist-diff' ],
701 [
702 'diff' => 'prev',
703 'oldid' => $row->{$this->revisionIdField},
704 ]
705 );
706 } else {
707 $difftext = $this->messages['diff'];
708 }
709 $histlink = $this->getLinkRenderer()->makeKnownLink(
710 $this->currentPage,
711 new HtmlArmor( $this->messages['hist'] ),
712 [ 'class' => 'mw-changeslist-history' ],
713 [ 'action' => 'history' ]
714 );
715
716 // While it might be tempting to use a list here
717 // this would result in clutter and slows down navigating the content
718 // in assistive technology.
719 // See https://phabricator.wikimedia.org/T205581#4734812
720 return Html::rawElement( 'span',
721 [ 'class' => 'mw-changeslist-links' ],
722 // The spans are needed to ensure the dividing '|' elements are not
723 // themselves styled as links.
724 Html::rawElement( 'span', [], $difftext ) .
725 ' ' . // Space needed for separating two words.
726 Html::rawElement( 'span', [], $histlink )
727 );
728 }
729 }
730
737 protected function formatDateLink( $row ) {
738 if ( !$this->currentPage || !$this->currentRevRecord ) {
739 return '';
740 }
741 if ( $this->isArchive ) {
742 $date = $this->getLanguage()->userTimeAndDate(
743 $this->currentRevRecord->getTimestamp(),
744 $this->getUser()
745 );
746
747 if ( $this->getAuthority()->isAllowed( 'undelete' ) &&
748 $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
749 ) {
750 $dateLink = $this->getLinkRenderer()->makeKnownLink(
751 SpecialPage::getTitleFor( 'Undelete' ),
752 $date,
753 [ 'class' => 'mw-changeslist-date' ],
754 [
755 'target' => $this->currentPage->getPrefixedText(),
756 'timestamp' => $this->currentRevRecord->getTimestamp()
757 ]
758 );
759 } else {
760 $dateLink = htmlspecialchars( $date );
761 }
762 if ( $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
763 $class = Linker::getRevisionDeletedClass( $this->currentRevRecord );
764 $dateLink = Html::rawElement(
765 'span',
766 [ 'class' => $class ],
767 $dateLink
768 );
769 }
770 } else {
771 $dateLink = ChangesList::revDateLink(
772 $this->currentRevRecord,
773 $this->getAuthority(),
774 $this->getLanguage(),
775 $this->currentPage
776 );
777 }
778 return $dateLink;
779 }
780
788 protected function formatTopMarkText( $row, &$classes ) {
789 if ( !$this->currentPage || !$this->currentRevRecord ) {
790 return '';
791 }
792 $topmarktext = '';
793 if ( !$this->isArchive ) {
794 $pagerTools = new PagerTools(
795 $this->currentRevRecord,
796 null,
797 $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new,
798 $this->hookRunner,
799 $this->currentPage,
800 $this->getContext(),
801 $this->getLinkRenderer()
802 );
803 if ( $row->{$this->revisionIdField} === $row->page_latest ) {
804 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
805 $classes[] = 'mw-contributions-current';
806 }
807 if ( $pagerTools->shouldPreventClickjacking() ) {
808 $this->setPreventClickjacking( true );
809 }
810 $topmarktext .= $pagerTools->toHTML();
811 }
812 return $topmarktext;
813 }
814
821 protected function formatCharDiff( $row ) {
822 if ( $row->{$this->revisionParentIdField} === null ) {
823 // For some reason rev_parent_id isn't populated for this row.
824 // Its rumoured this is true on wikipedia for some revisions (T36922).
825 // Next best thing is to have the total number of bytes.
826 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
827 $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} );
828 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
829 } else {
830 $parentLen = 0;
831 if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) {
832 $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}];
833 }
834
835 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
837 $parentLen,
838 $row->{$this->revisionLengthField},
839 $this->getContext()
840 );
841 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
842 }
843 return $chardiff;
844 }
845
852 protected function formatComment( $row ) {
853 $comment = $this->formattedComments[$row->{$this->revisionIdField}];
854
855 if ( $comment === '' ) {
856 $defaultComment = $this->messages['changeslist-nocomment'];
857 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
858 }
859
860 // Don't wrap result of this with <bdi> or any other element, see T377555
861 return $comment;
862 }
863
870 protected function formatUserLink( $row ) {
871 if ( !$this->currentRevRecord ) {
872 return '';
873 }
874 $dir = $this->getLanguage()->getDir();
875
876 // When the author is different from the target, always show user and user talk links
877 $userlink = '';
878 $revUser = $this->currentRevRecord->getUser();
879 $revUserId = $revUser ? $revUser->getId() : 0;
880 $revUserText = $revUser ? $revUser->getName() : '';
881 if ( $this->target !== $revUserText ) {
882 $userlink = ' <span class="mw-changeslist-separator"></span> '
883 . Html::rawElement( 'bdi', [ 'dir' => $dir ],
884 Linker::userLink( $revUserId, $revUserText ) );
885 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
886 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
887 }
888 return $userlink;
889 }
890
895 protected function formatFlags( $row ) {
896 if ( !$this->currentRevRecord ) {
897 return [];
898 }
899 $flags = [];
900 if ( $this->currentRevRecord->getParentId() === 0 ) {
901 $flags[] = ChangesList::flag( 'newpage' );
902 }
903
904 if ( $this->currentRevRecord->isMinor() ) {
905 $flags[] = ChangesList::flag( 'minor' );
906 }
907 return $flags;
908 }
909
916 protected function formatVisibilityLink( $row ) {
917 if ( !$this->currentPage || !$this->currentRevRecord ) {
918 return '';
919 }
920 $del = Linker::getRevDeleteLink(
921 $this->getAuthority(),
922 $this->currentRevRecord,
923 $this->currentPage
924 );
925 if ( $del !== '' ) {
926 $del .= ' ';
927 }
928 return $del;
929 }
930
936 protected function formatTags( $row, &$classes ) {
937 # Tags, if any. Save some time using a cache.
938 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
939 $this->tagsCache->makeKey(
940 $row->ts_tags ?? '',
941 $this->getUser()->getName(),
942 $this->getLanguage()->getCode()
943 ),
945 $row->ts_tags,
946 null,
947 $this->getContext()
948 )
949 );
950 $classes = array_merge( $classes, $newClasses );
951 return $tagSummary;
952 }
953
960 public function revisionUserIsDeleted( $row ) {
961 return $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_USER );
962 }
963
976 public function formatRow( $row ) {
977 $ret = '';
978 $classes = [];
979 $attribs = [];
980
981 $this->currentPage = null;
982 $this->currentRevRecord = null;
983
984 // Create a title for the revision if possible
985 // Rows from the hook may not include title information
986 if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) {
987 $this->currentPage = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
988 }
989
990 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
991 // function to be called with a special object for $row. It expects us
992 // skip formatting so that the row can be formatted by the
993 // ContributionsLineEnding hook below.
994 // FIXME: have some better way for extensions to provide formatted rows.
995 $this->currentRevRecord = $this->tryCreatingRevisionRecord( $row, $this->currentPage );
996 if ( $this->revisionsOnly || ( $this->currentRevRecord && $this->currentPage ) ) {
997 $this->populateAttributes( $row, $attribs );
998
999 $templateParams = $this->getTemplateParams( $row, $classes );
1000 $ret = $this->getProcessedTemplate( $templateParams );
1001 }
1002
1003 // Let extensions add data
1004 $lineEndingsHook = $this->isArchive ?
1005 'onDeletedContributionsLineEnding' :
1006 'onContributionsLineEnding';
1007 $this->hookRunner->$lineEndingsHook( $this, $ret, $row, $classes, $attribs );
1008 $attribs = array_filter( $attribs,
1009 [ Sanitizer::class, 'isReservedDataAttribute' ],
1010 ARRAY_FILTER_USE_KEY
1011 );
1012
1013 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
1014 // receiving empty rows?
1015
1016 if ( $classes === [] && $attribs === [] && $ret === '' ) {
1017 wfDebug( "Dropping ContributionsSpecialPage row that could not be formatted" );
1018 return "<!-- Could not format ContributionsSpecialPage row. -->\n";
1019 }
1020 $attribs['class'] = $classes;
1021
1022 // FIXME: The signature of the ContributionsLineEnding hook makes it
1023 // very awkward to move this LI wrapper into the template.
1024 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
1025 }
1026
1037 public function getTemplateParams( $row, &$classes ) {
1038 $link = $this->formatArticleLink( $row );
1039 $topmarktext = $this->formatTopMarkText( $row, $classes );
1040 $diffHistLinks = $this->formatDiffHistLinks( $row );
1041 $dateLink = $this->formatDateLink( $row );
1042 $chardiff = $this->formatCharDiff( $row );
1043 $comment = $this->formatComment( $row );
1044 $userlink = $this->formatUserLink( $row );
1045 $flags = $this->formatFlags( $row );
1046 $del = $this->formatVisibilityLink( $row );
1047 $tagSummary = $this->formatTags( $row, $classes );
1048
1049 if ( !$this->isArchive ) {
1050 $this->hookRunner->onSpecialContributions__formatRow__flags(
1051 $this->getContext(), $row, $flags );
1052 }
1053
1054 $templateParams = [
1055 'del' => $del,
1056 'timestamp' => $dateLink,
1057 'diffHistLinks' => $diffHistLinks,
1058 'charDifference' => $chardiff,
1059 'flags' => $flags,
1060 'articleLink' => $link,
1061 'userlink' => $userlink,
1062 'logText' => $comment,
1063 'topmarktext' => $topmarktext,
1064 'tagSummary' => $tagSummary,
1065 ];
1066
1067 # Denote if username is redacted for this edit
1068 if ( $this->revisionUserIsDeleted( $row ) ) {
1069 $templateParams['rev-deleted-user-contribs'] =
1070 $this->msg( 'rev-deleted-user-contribs' )->escaped();
1071 }
1072
1073 return $templateParams;
1074 }
1075
1085 public function getProcessedTemplate( $templateParams ) {
1086 return $this->templateParser->processTemplate(
1087 'SpecialContributionsLine',
1088 $templateParams
1089 );
1090 }
1091
1096 protected function getSqlComment() {
1097 if ( $this->namespace || $this->deletedOnly ) {
1098 // potentially slow, see CR r58153
1099 return 'contributions page filtered for namespace or RevisionDeleted edits';
1100 } else {
1101 return 'contributions page unfiltered';
1102 }
1103 }
1104
1108 protected function preventClickjacking() {
1109 $this->setPreventClickjacking( true );
1110 }
1111
1116 protected function setPreventClickjacking( bool $enable ) {
1117 $this->preventClickjacking = $enable;
1118 }
1119
1123 public function getPreventClickjacking() {
1124 return $this->preventClickjacking;
1125 }
1126
1127}
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 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.
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:57
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()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
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.
getTemplateParams( $row, &$classes)
Generate array of template parameters to pass to the template for rendering.
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,...
revisionUserIsDeleted( $row)
Check whether the revision author is deleted.
formatTopMarkText( $row, &$classes)
Format annotation and add extra class if a row represents a latest revision.
getEndBody()
Hook into getBody() for the end of the list.to overridestring
getProcessedTemplate( $templateParams)
Return the processed template.
createRevisionRecord( $row, $title=null)
Create a revision record from a $row that models a revision.
formatCharDiff( $row)
Format annotation to show the size of a diff.
string $target
User name, or a string describing an IP address range.
formatDateLink( $row)
Format a date link.
getSqlComment()
Overwrite Pager function and return a helpful comment.
string[] $messages
Local cache for escaped messages.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
formatArticleLink( $row)
Format a link to an article.
formatVisibilityLink( $row)
Format link for changing visibility.
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)
formatDiffHistLinks( $row)
Format diff and history links.
TemplateParser $templateParser
Set to protected to allow subclasses access for overrides.
formatUserLink( $row)
Format a user link.
populateAttributes( $row, &$attributes)
Populate the HTML attributes.
formatRow( $row)
Generates each row in the contributions list.
formatComment( $row)
Format a comment for a revision.
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.
Generate a set of tools for a revision.
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.