MediaWiki master
ContribsPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use ChangesList;
25use ChangeTags;
26use DateTime;
27use HtmlArmor;
28use InvalidArgumentException;
29use MapCacheLRU;
49use stdClass;
50use Wikimedia\IPUtils;
55
61
62 public $mGroupByDate = true;
63
67 private $messages;
68
72 private $target;
73
77 private $namespace;
78
82 private $tagFilter;
83
87 private $tagInvert;
88
92 private $nsInvert;
93
98 private $associated;
99
103 private $deletedOnly;
104
108 private $topOnly;
109
113 private $newOnly;
114
118 private $hideMinor;
119
124 private $revisionsOnly;
125
126 private $preventClickjacking = false;
127
131 private $mParentLens;
132
134 private $targetUser;
135
137 private CommentFormatter $commentFormatter;
138 private HookRunner $hookRunner;
139 private LinkBatchFactory $linkBatchFactory;
140 private NamespaceInfo $namespaceInfo;
141 private RevisionStore $revisionStore;
142
144 private $formattedComments = [];
145
147 private $revisions = [];
148
150 private $tagsCache;
151
165 public function __construct(
166 IContextSource $context,
167 array $options,
168 LinkRenderer $linkRenderer = null,
169 LinkBatchFactory $linkBatchFactory = null,
170 HookContainer $hookContainer = null,
171 IConnectionProvider $dbProvider = null,
172 RevisionStore $revisionStore = null,
173 NamespaceInfo $namespaceInfo = null,
174 UserIdentity $targetUser = null,
175 CommentFormatter $commentFormatter = null
176 ) {
177 // Class is used directly in extensions - T266484
178 $services = MediaWikiServices::getInstance();
179 $dbProvider ??= $services->getConnectionProvider();
180
181 // Set ->target before calling parent::__construct() so
182 // parent can call $this->getIndexField() and get the right result. Set
183 // the rest too just to keep things simple.
184 if ( $targetUser ) {
185 $this->target = $options['target'] ?? $targetUser->getName();
186 $this->targetUser = $targetUser;
187 } else {
188 // Use target option
189 // It's possible for the target to be empty. This is used by
190 // ContribsPagerTest and does not cause newFromName() to return
191 // false. It's probably not used by any production code.
192 $this->target = $options['target'] ?? '';
193 // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
194 $this->targetUser = $services->getUserFactory()->newFromName(
195 $this->target, UserRigorOptions::RIGOR_NONE
196 );
197 if ( !$this->targetUser ) {
198 // This can happen if the target contained "#". Callers
199 // typically pass user input through title normalization to
200 // avoid it.
201 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
202 'broken to use even with validation disabled.' );
203 }
204 }
205
206 $this->namespace = $options['namespace'] ?? '';
207 $this->tagFilter = $options['tagfilter'] ?? false;
208 $this->tagInvert = $options['tagInvert'] ?? false;
209 $this->nsInvert = $options['nsInvert'] ?? false;
210 $this->associated = $options['associated'] ?? false;
211
212 $this->deletedOnly = !empty( $options['deletedOnly'] );
213 $this->topOnly = !empty( $options['topOnly'] );
214 $this->newOnly = !empty( $options['newOnly'] );
215 $this->hideMinor = !empty( $options['hideMinor'] );
216 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
217
218 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
219
220 $msgs = [
221 'diff',
222 'hist',
223 'pipe-separator',
224 'uctop',
225 'changeslist-nocomment',
226 ];
227
228 foreach ( $msgs as $msg ) {
229 $this->messages[$msg] = $this->msg( $msg )->escaped();
230 }
231
232 // Date filtering: use timestamp if available
233 $startTimestamp = '';
234 $endTimestamp = '';
235 if ( isset( $options['start'] ) && $options['start'] ) {
236 $startTimestamp = $options['start'] . ' 00:00:00';
237 }
238 if ( isset( $options['end'] ) && $options['end'] ) {
239 $endTimestamp = $options['end'] . ' 23:59:59';
240 }
241 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
242
243 $this->templateParser = new TemplateParser();
244 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
245 $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
246 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
247 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
248 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
249 $this->tagsCache = new MapCacheLRU( 50 );
250 }
251
252 public function getDefaultQuery() {
253 $query = parent::getDefaultQuery();
254 $query['target'] = $this->target;
255
256 return $query;
257 }
258
268 public function reallyDoQuery( $offset, $limit, $order ) {
269 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo(
270 $offset,
271 $limit,
272 $order
273 );
274
275 $options['MAX_EXECUTION_TIME'] =
277 /*
278 * This hook will allow extensions to add in additional queries, so they can get their data
279 * in My Contributions as well. Extensions should append their results to the $data array.
280 *
281 * Extension queries have to implement the navbar requirement as well. They should
282 * - have a column aliased as $pager->getIndexField()
283 * - have LIMIT set
284 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
285 * - have the ORDER BY specified based upon the details provided by the navbar
286 *
287 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
288 *
289 * &$data: an array of results of all contribs queries
290 * $pager: the ContribsPager object hooked into
291 * $offset: see phpdoc above
292 * $limit: see phpdoc above
293 * $descending: see phpdoc above
294 */
295 $dbr = $this->getDatabase();
296 $data = [ $dbr->select(
297 $tables, $fields, $conds, $fname, $options, $join_conds
298 ) ];
299 if ( !$this->revisionsOnly ) {
300 // TODO: Range offsets are fairly important and all handlers should take care of it.
301 // If this hook will be replaced (e.g. unified with the DeletedContribsPager one),
302 // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577).
303 $this->hookRunner->onContribsPager__reallyDoQuery(
304 $data, $this, $offset, $limit, $order );
305 }
306
307 $result = [];
308
309 // loop all results and collect them in an array
310 foreach ( $data as $query ) {
311 foreach ( $query as $i => $row ) {
312 // If the query results are in descending order, the indexes must also be in descending order
313 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
314 // Left-pad with zeroes, because these values will be sorted as strings
315 $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
316 // use index column as key, allowing us to easily sort in PHP
317 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
318 }
319 }
320
321 // sort results
322 if ( $order === self::QUERY_ASCENDING ) {
323 ksort( $result );
324 } else {
325 krsort( $result );
326 }
327
328 // enforce limit
329 $result = array_slice( $result, 0, $limit );
330
331 // get rid of array keys
332 $result = array_values( $result );
333
334 return new FakeResultWrapper( $result );
335 }
336
346 private function getTargetTable() {
347 $dbr = $this->getDatabase();
348 $ipRangeConds = $this->targetUser->isRegistered()
349 ? null : $this->getIpRangeConds( $dbr, $this->target );
350 if ( $ipRangeConds ) {
351 return 'ip_changes';
352 }
353
354 return 'revision';
355 }
356
357 public function getQueryInfo() {
358 $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
359 $queryInfo = [
360 'tables' => $revQuery['tables'],
361 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
362 'conds' => [],
363 'options' => [],
364 'join_conds' => $revQuery['joins'],
365 ];
366
367 // WARNING: Keep this in sync with getTargetTable()!
368 $dbr = $this->getDatabase();
369 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
370 if ( $ipRangeConds ) {
371 // Put ip_changes first (T284419)
372 array_unshift( $queryInfo['tables'], 'ip_changes' );
373 $queryInfo['join_conds']['revision'] = [
374 'JOIN', [ 'rev_id = ipc_rev_id' ]
375 ];
376 $queryInfo['conds'][] = $ipRangeConds;
377 } else {
378 $queryInfo['conds']['actor_name'] = $this->targetUser->getName();
379 // Force the appropriate index to avoid bad query plans (T307295)
380 $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
381 }
382
383 if ( $this->deletedOnly ) {
384 $queryInfo['conds'][] = 'rev_deleted != 0';
385 }
386
387 if ( $this->topOnly ) {
388 $queryInfo['conds'][] = 'rev_id = page_latest';
389 }
390
391 if ( $this->newOnly ) {
392 $queryInfo['conds'][] = 'rev_parent_id = 0';
393 }
394
395 if ( $this->hideMinor ) {
396 $queryInfo['conds'][] = 'rev_minor_edit = 0';
397 }
398
399 $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
400
401 // Paranoia: avoid brute force searches (T19342)
402 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
403 $queryInfo['conds'][] = $dbr->bitAnd(
404 'rev_deleted', RevisionRecord::DELETED_USER
405 ) . ' = 0';
406 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
407 $queryInfo['conds'][] = $dbr->bitAnd(
408 'rev_deleted', RevisionRecord::SUPPRESSED_USER
409 ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
410 }
411
412 // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
413 $indexField = $this->getIndexField();
414 if ( $indexField !== 'rev_timestamp' ) {
415 $queryInfo['fields'][] = $indexField;
416 }
417
419 $queryInfo['tables'],
420 $queryInfo['fields'],
421 $queryInfo['conds'],
422 $queryInfo['join_conds'],
423 $queryInfo['options'],
424 $this->tagFilter,
425 $this->tagInvert,
426 );
427
428 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
429
430 return $queryInfo;
431 }
432
433 protected function getNamespaceCond() {
434 if ( $this->namespace !== '' ) {
435 $dbr = $this->getDatabase();
436 $selectedNS = $dbr->addQuotes( $this->namespace );
437 $eq_op = $this->nsInvert ? '!=' : '=';
438 $bool_op = $this->nsInvert ? 'AND' : 'OR';
439
440 if ( !$this->associated ) {
441 return [ "page_namespace $eq_op $selectedNS" ];
442 }
443
444 $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
445
446 return [
447 "page_namespace $eq_op $selectedNS " .
448 $bool_op .
449 " page_namespace $eq_op $associatedNS"
450 ];
451 }
452
453 return [];
454 }
455
462 private function getIpRangeConds( $db, $ip ) {
463 // First make sure it is a valid range and they are not outside the CIDR limit
464 if ( !self::isQueryableRange( $ip, $this->getConfig() ) ) {
465 return false;
466 }
467
468 [ $start, $end ] = IPUtils::parseRange( $ip );
469
470 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
471 }
472
482 public static function isQueryableRange( $ipRange, $config ) {
483 $limits = $config->get( MainConfigNames::RangeContributionsCIDRLimit );
484
485 $bits = IPUtils::parseCIDR( $ipRange )[1];
486 if (
487 ( $bits === false ) ||
488 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
489 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
490 ) {
491 return false;
492 }
493
494 return true;
495 }
496
500 public function getIndexField() {
501 // The returned column is used for sorting and continuation, so we need to
502 // make sure to use the right denormalized column depending on which table is
503 // being targeted by the query to avoid bad query plans.
504 // See T200259, T204669, T220991, and T221380.
505 $target = $this->getTargetTable();
506 switch ( $target ) {
507 case 'revision':
508 return 'rev_timestamp';
509 case 'ip_changes':
510 return 'ipc_rev_timestamp';
511 default:
512 wfWarn(
513 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
514 );
515 return 'rev_timestamp';
516 }
517 }
518
522 public function getTagFilter() {
523 return $this->tagFilter;
524 }
525
529 public function getTagInvert() {
530 return $this->tagInvert;
531 }
532
536 public function getTarget() {
537 return $this->target;
538 }
539
543 public function isNewOnly() {
544 return $this->newOnly;
545 }
546
550 public function getNamespace() {
551 return $this->namespace;
552 }
553
557 protected function getExtraSortFields() {
558 // The returned columns are used for sorting, so we need to make sure
559 // to use the right denormalized column depending on which table is
560 // being targeted by the query to avoid bad query plans.
561 // See T200259, T204669, T220991, and T221380.
562 $target = $this->getTargetTable();
563 switch ( $target ) {
564 case 'revision':
565 return [ 'rev_id' ];
566 case 'ip_changes':
567 return [ 'ipc_rev_id' ];
568 default:
569 wfWarn(
570 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
571 );
572 return [ 'rev_id' ];
573 }
574 }
575
576 protected function doBatchLookups() {
577 # Do a link batch query
578 $this->mResult->seek( 0 );
579 $parentRevIds = [];
580 $this->mParentLens = [];
581 $revisions = [];
582 $linkBatch = $this->linkBatchFactory->newLinkBatch();
583 # Give some pointers to make (last) links
584 foreach ( $this->mResult as $row ) {
585 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
586 $parentRevIds[] = (int)$row->rev_parent_id;
587 }
588 if ( $this->revisionStore->isRevisionRow( $row ) ) {
589 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
590 if ( $this->target !== $row->rev_user_text ) {
591 // If the target does not match the author, batch the author's talk page
592 $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
593 }
594 $linkBatch->add( $row->page_namespace, $row->page_title );
595 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
596 }
597 }
598 # Fetch rev_len for revisions not already scanned above
599 $this->mParentLens += $this->revisionStore->getRevisionSizes(
600 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
601 );
602 $linkBatch->execute();
603
604 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
605 ->authority( $this->getAuthority() )
606 ->revisions( $revisions )
607 ->hideIfDeleted()
608 ->execute();
609
610 # For performance, save the revision objects for later.
611 # The array is indexed by rev_id. doBatchLookups() may be called
612 # multiple times with different results, so merge the revisions array,
613 # ignoring any duplicates.
614 $this->revisions += $revisions;
615 }
616
620 protected function getStartBody() {
621 return "<section class='mw-pager-body'>\n";
622 }
623
627 protected function getEndBody() {
628 return "</section>\n";
629 }
630
641 public function tryCreatingRevisionRecord( $row, $title = null ) {
642 if ( $row instanceof stdClass && isset( $row->rev_id )
643 && isset( $this->revisions[$row->rev_id] )
644 ) {
645 return $this->revisions[$row->rev_id];
646 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
647 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
648 } else {
649 return null;
650 }
651 }
652
665 public function formatRow( $row ) {
666 $ret = '';
667 $classes = [];
668 $attribs = [];
669
670 $linkRenderer = $this->getLinkRenderer();
671
672 $page = null;
673 // Create a title for the revision if possible
674 // Rows from the hook may not include title information
675 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
676 $page = Title::newFromRow( $row );
677 }
678 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
679 // function to be called with a special object for $row. It expects us
680 // skip formatting so that the row can be formatted by the
681 // ContributionsLineEnding hook below.
682 // FIXME: have some better way for extensions to provide formatted rows.
683 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
684 if ( $revRecord && $page ) {
685 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
686 $attribs['data-mw-revid'] = $revRecord->getId();
687
688 $link = $linkRenderer->makeLink(
689 $page,
690 $page->getPrefixedText(),
691 [ 'class' => 'mw-contributions-title' ],
692 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
693 );
694 # Mark current revisions
695 $topmarktext = '';
696
697 $pagerTools = new PagerTools(
698 $revRecord,
699 null,
700 $row->rev_id === $row->page_latest && !$row->page_is_new,
701 $this->hookRunner,
702 $page,
703 $this->getContext(),
704 $this->getLinkRenderer()
705 );
706 if ( $row->rev_id === $row->page_latest ) {
707 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
708 $classes[] = 'mw-contributions-current';
709 }
710 if ( $pagerTools->shouldPreventClickjacking() ) {
711 $this->setPreventClickjacking( true );
712 }
713 $topmarktext .= $pagerTools->toHTML();
714 # Is there a visible previous revision?
715 if ( $revRecord->getParentId() !== 0 &&
716 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
717 ) {
718 $difftext = $linkRenderer->makeKnownLink(
719 $page,
720 new HtmlArmor( $this->messages['diff'] ),
721 [ 'class' => 'mw-changeslist-diff' ],
722 [
723 'diff' => 'prev',
724 'oldid' => $row->rev_id
725 ]
726 );
727 } else {
728 $difftext = $this->messages['diff'];
729 }
730 $histlink = $linkRenderer->makeKnownLink(
731 $page,
732 new HtmlArmor( $this->messages['hist'] ),
733 [ 'class' => 'mw-changeslist-history' ],
734 [ 'action' => 'history' ]
735 );
736
737 if ( $row->rev_parent_id === null ) {
738 // For some reason rev_parent_id isn't populated for this row.
739 // Its rumoured this is true on wikipedia for some revisions (T36922).
740 // Next best thing is to have the total number of bytes.
741 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
742 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
743 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
744 } else {
745 $parentLen = 0;
746 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
747 $parentLen = $this->mParentLens[$row->rev_parent_id];
748 }
749
750 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
751 $chardiff .= ChangesList::showCharacterDifference(
752 $parentLen,
753 $row->rev_len,
754 $this->getContext()
755 );
756 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
757 }
758
759 $lang = $this->getLanguage();
760
761 $comment = $this->formattedComments[$row->rev_id];
762
763 if ( $comment === '' ) {
764 $defaultComment = $this->messages['changeslist-nocomment'];
765 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
766 }
767
768 $comment = $lang->getDirMark() . $comment;
769
770 $authority = $this->getAuthority();
771 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
772
773 // When the author is different from the target, always show user and user talk links
774 $userlink = '';
775 $revUser = $revRecord->getUser();
776 $revUserId = $revUser ? $revUser->getId() : 0;
777 $revUserText = $revUser ? $revUser->getName() : '';
778 if ( $this->target !== $revUserText ) {
779 $userlink = ' <span class="mw-changeslist-separator"></span> '
780 . $lang->getDirMark()
781 . Linker::userLink( $revUserId, $revUserText );
782 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
783 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
784 }
785
786 $flags = [];
787 if ( $revRecord->getParentId() === 0 ) {
788 $flags[] = ChangesList::flag( 'newpage' );
789 }
790
791 if ( $revRecord->isMinor() ) {
792 $flags[] = ChangesList::flag( 'minor' );
793 }
794
795 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
796 if ( $del !== '' ) {
797 $del .= ' ';
798 }
799
800 // While it might be tempting to use a list here
801 // this would result in clutter and slows down navigating the content
802 // in assistive technology.
803 // See https://phabricator.wikimedia.org/T205581#4734812
804 $diffHistLinks = Html::rawElement( 'span',
805 [ 'class' => 'mw-changeslist-links' ],
806 // The spans are needed to ensure the dividing '|' elements are not
807 // themselves styled as links.
808 Html::rawElement( 'span', [], $difftext ) .
809 ' ' . // Space needed for separating two words.
810 Html::rawElement( 'span', [], $histlink )
811 );
812
813 # Tags, if any. Save some time using a cache.
814 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
815 $this->tagsCache->makeKey(
816 $row->ts_tags ?? '',
817 $this->getUser()->getName(),
818 $lang->getCode()
819 ),
821 $row->ts_tags,
822 null,
823 $this->getContext()
824 )
825 );
826 $classes = array_merge( $classes, $newClasses );
827
828 $this->hookRunner->onSpecialContributions__formatRow__flags(
829 $this->getContext(), $row, $flags );
830
831 $templateParams = [
832 'del' => $del,
833 'timestamp' => $d,
834 'diffHistLinks' => $diffHistLinks,
835 'charDifference' => $chardiff,
836 'flags' => $flags,
837 'articleLink' => $link,
838 'userlink' => $userlink,
839 'logText' => $comment,
840 'topmarktext' => $topmarktext,
841 'tagSummary' => $tagSummary,
842 ];
843
844 # Denote if username is redacted for this edit
845 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
846 $templateParams['rev-deleted-user-contribs'] =
847 $this->msg( 'rev-deleted-user-contribs' )->escaped();
848 }
849
850 $ret = $this->templateParser->processTemplate(
851 'SpecialContributionsLine',
852 $templateParams
853 );
854 }
855
856 // Let extensions add data
857 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
858 $attribs = array_filter( $attribs,
859 [ Sanitizer::class, 'isReservedDataAttribute' ],
860 ARRAY_FILTER_USE_KEY
861 );
862
863 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
864 // receiving empty rows?
865
866 if ( $classes === [] && $attribs === [] && $ret === '' ) {
867 wfDebug( "Dropping Special:Contribution row that could not be formatted" );
868 return "<!-- Could not format Special:Contribution row. -->\n";
869 }
870 $attribs['class'] = $classes;
871
872 // FIXME: The signature of the ContributionsLineEnding hook makes it
873 // very awkward to move this LI wrapper into the template.
874 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
875 }
876
881 protected function getSqlComment() {
882 if ( $this->namespace || $this->deletedOnly ) {
883 // potentially slow, see CR r58153
884 return 'contributions page filtered for namespace or RevisionDeleted edits';
885 } else {
886 return 'contributions page unfiltered';
887 }
888 }
889
893 protected function preventClickjacking() {
894 $this->setPreventClickjacking( true );
895 }
896
901 protected function setPreventClickjacking( bool $enable ) {
902 $this->preventClickjacking = $enable;
903 }
904
908 public function getPreventClickjacking() {
909 return $this->preventClickjacking;
910 }
911
918 public static function processDateFilter( array $opts ) {
919 $start = $opts['start'] ?? '';
920 $end = $opts['end'] ?? '';
921 $year = $opts['year'] ?? '';
922 $month = $opts['month'] ?? '';
923
924 if ( $start !== '' && $end !== '' && $start > $end ) {
925 $temp = $start;
926 $start = $end;
927 $end = $temp;
928 }
929
930 // If year/month legacy filtering options are set, convert them to display the new stamp
931 if ( $year !== '' || $month !== '' ) {
932 // Reuse getDateCond logic, but subtract a day because
933 // the endpoints of our date range appear inclusive
934 // but the internal end offsets are always exclusive
935 $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
936 $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
937 $legacyDateTime = $legacyDateTime->modify( '-1 day' );
938
939 // Clear the new timestamp range options if used and
940 // replace with the converted legacy timestamp
941 $start = '';
942 $end = $legacyDateTime->format( 'Y-m-d' );
943 }
944
945 $opts['start'] = $start;
946 $opts['end'] = $end;
947
948 return $opts;
949 }
950}
951
956class_alias( ContribsPager::class, 'ContribsPager' );
getAuthority()
const NS_USER_TALK
Definition Defines.php:67
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
$templateParser
getContext()
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.
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:65
A class containing constants representing the names of configuration variables.
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
const RangeContributionsCIDRLimit
Name constant for the RangeContributionsCIDRLimit 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.
tryCreatingRevisionRecord( $row, $title=null)
If the object looks like a revision row, or corresponds to a previously cached revision,...
static isQueryableRange( $ipRange, $config)
Is the given IP a range and within the CIDR limit?
formatRow( $row)
Generates each row in the contributions list.
getQueryInfo()
Provides all parameters needed for the main paged query.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
static processDateFilter(array $opts)
Set up date filter options, given request data.
reallyDoQuery( $offset, $limit, $order)
This method basically executes the exact same code as the parent class, though with a hook added,...
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
getStartBody()
Hook into getBody(), allows text to be inserted at the start.This will be called even if there are no...
__construct(IContextSource $context, array $options, LinkRenderer $linkRenderer=null, LinkBatchFactory $linkBatchFactory=null, HookContainer $hookContainer=null, IConnectionProvider $dbProvider=null, RevisionStore $revisionStore=null, NamespaceInfo $namespaceInfo=null, UserIdentity $targetUser=null, CommentFormatter $commentFormatter=null)
FIXME List services first T266484 / T290405.
getSqlComment()
Overwrite Pager function and return a helpful comment.
getEndBody()
Hook into getBody() for the end of the list.to overridestring
getDatabase()
Get the Database object in use.
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...
static getOffsetDate( $year, $month, $day=-1)
Core logic of determining the offset timestamp such that we can get all items with a timestamp up to ...
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Page revision base class.
Service for looking up page revisions.
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
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for configuration instances.
Definition Config.php:32
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.
Provide primary and replica IDatabase connections.
A database connection without write operations.
Result wrapper for grabbing data queried from an IDatabase object.