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;
29use InvalidArgumentException;
30use 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->getDBLoadBalancerFactory();
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 getTarget() {
530 return $this->target;
531 }
532
536 public function isNewOnly() {
537 return $this->newOnly;
538 }
539
543 public function getNamespace() {
544 return $this->namespace;
545 }
546
550 protected function getExtraSortFields() {
551 // The returned columns are used for sorting, so we need to make sure
552 // to use the right denormalized column depending on which table is
553 // being targeted by the query to avoid bad query plans.
554 // See T200259, T204669, T220991, and T221380.
555 $target = $this->getTargetTable();
556 switch ( $target ) {
557 case 'revision':
558 return [ 'rev_id' ];
559 case 'ip_changes':
560 return [ 'ipc_rev_id' ];
561 default:
562 wfWarn(
563 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
564 );
565 return [ 'rev_id' ];
566 }
567 }
568
569 protected function doBatchLookups() {
570 # Do a link batch query
571 $this->mResult->seek( 0 );
572 $parentRevIds = [];
573 $this->mParentLens = [];
574 $revisions = [];
575 $linkBatch = $this->linkBatchFactory->newLinkBatch();
576 $isIpRange = self::isQueryableRange( $this->target, $this->getConfig() );
577 # Give some pointers to make (last) links
578 foreach ( $this->mResult as $row ) {
579 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
580 $parentRevIds[] = (int)$row->rev_parent_id;
581 }
582 if ( $this->revisionStore->isRevisionRow( $row ) ) {
583 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
584 if ( $isIpRange ) {
585 // If this is an IP range, batch the IP's talk page
586 $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
587 }
588 $linkBatch->add( $row->page_namespace, $row->page_title );
589 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
590 }
591 }
592 # Fetch rev_len for revisions not already scanned above
593 $this->mParentLens += $this->revisionStore->getRevisionSizes(
594 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
595 );
596 $linkBatch->execute();
597
598 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
599 ->authority( $this->getAuthority() )
600 ->revisions( $revisions )
601 ->hideIfDeleted()
602 ->execute();
603
604 # For performance, save the revision objects for later.
605 # The array is indexed by rev_id. doBatchLookups() may be called
606 # multiple times with different results, so merge the revisions array,
607 # ignoring any duplicates.
608 $this->revisions += $revisions;
609 }
610
614 protected function getStartBody() {
615 return "<section class='mw-pager-body'>\n";
616 }
617
621 protected function getEndBody() {
622 return "</section>\n";
623 }
624
635 public function tryCreatingRevisionRecord( $row, $title = null ) {
636 if ( $row instanceof stdClass && isset( $row->rev_id )
637 && isset( $this->revisions[$row->rev_id] )
638 ) {
639 return $this->revisions[$row->rev_id];
640 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
641 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
642 } else {
643 return null;
644 }
645 }
646
659 public function formatRow( $row ) {
660 $ret = '';
661 $classes = [];
662 $attribs = [];
663
664 $linkRenderer = $this->getLinkRenderer();
665
666 $page = null;
667 // Create a title for the revision if possible
668 // Rows from the hook may not include title information
669 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
670 $page = Title::newFromRow( $row );
671 }
672 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
673 // function to be called with a special object for $row. It expects us
674 // skip formatting so that the row can be formatted by the
675 // ContributionsLineEnding hook below.
676 // FIXME: have some better way for extensions to provide formatted rows.
677 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
678 if ( $revRecord && $page ) {
679 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
680 $attribs['data-mw-revid'] = $revRecord->getId();
681
682 $link = $linkRenderer->makeLink(
683 $page,
684 $page->getPrefixedText(),
685 [ 'class' => 'mw-contributions-title' ],
686 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
687 );
688 # Mark current revisions
689 $topmarktext = '';
690
691 $pagerTools = new PagerTools(
692 $revRecord,
693 null,
694 $row->rev_id === $row->page_latest && !$row->page_is_new,
695 $this->hookRunner,
696 $page,
697 $this->getContext(),
698 $this->getLinkRenderer()
699 );
700 if ( $row->rev_id === $row->page_latest ) {
701 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
702 $classes[] = 'mw-contributions-current';
703 }
704 if ( $pagerTools->shouldPreventClickjacking() ) {
705 $this->setPreventClickjacking( true );
706 }
707 $topmarktext .= $pagerTools->toHTML();
708 # Is there a visible previous revision?
709 if ( $revRecord->getParentId() !== 0 &&
710 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
711 ) {
712 $difftext = $linkRenderer->makeKnownLink(
713 $page,
714 new HtmlArmor( $this->messages['diff'] ),
715 [ 'class' => 'mw-changeslist-diff' ],
716 [
717 'diff' => 'prev',
718 'oldid' => $row->rev_id
719 ]
720 );
721 } else {
722 $difftext = $this->messages['diff'];
723 }
724 $histlink = $linkRenderer->makeKnownLink(
725 $page,
726 new HtmlArmor( $this->messages['hist'] ),
727 [ 'class' => 'mw-changeslist-history' ],
728 [ 'action' => 'history' ]
729 );
730
731 if ( $row->rev_parent_id === null ) {
732 // For some reason rev_parent_id isn't populated for this row.
733 // Its rumoured this is true on wikipedia for some revisions (T36922).
734 // Next best thing is to have the total number of bytes.
735 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
736 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
737 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
738 } else {
739 $parentLen = 0;
740 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
741 $parentLen = $this->mParentLens[$row->rev_parent_id];
742 }
743
744 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
745 $chardiff .= ChangesList::showCharacterDifference(
746 $parentLen,
747 $row->rev_len,
748 $this->getContext()
749 );
750 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
751 }
752
753 $lang = $this->getLanguage();
754
755 $comment = $this->formattedComments[$row->rev_id];
756
757 if ( $comment === '' ) {
758 $defaultComment = $this->messages['changeslist-nocomment'];
759 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
760 }
761
762 $comment = $lang->getDirMark() . $comment;
763
764 $authority = $this->getAuthority();
765 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
766
767 # When querying for an IP range, we want to always show user and user talk links.
768 $userlink = '';
769 $revUser = $revRecord->getUser();
770 $revUserId = $revUser ? $revUser->getId() : 0;
771 $revUserText = $revUser ? $revUser->getName() : '';
772 if ( self::isQueryableRange( $this->target, $this->getConfig() ) ) {
773 $userlink = ' <span class="mw-changeslist-separator"></span> '
774 . $lang->getDirMark()
775 . Linker::userLink( $revUserId, $revUserText );
776 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
777 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
778 }
779
780 $flags = [];
781 if ( $revRecord->getParentId() === 0 ) {
782 $flags[] = ChangesList::flag( 'newpage' );
783 }
784
785 if ( $revRecord->isMinor() ) {
786 $flags[] = ChangesList::flag( 'minor' );
787 }
788
789 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
790 if ( $del !== '' ) {
791 $del .= ' ';
792 }
793
794 // While it might be tempting to use a list here
795 // this would result in clutter and slows down navigating the content
796 // in assistive technology.
797 // See https://phabricator.wikimedia.org/T205581#4734812
798 $diffHistLinks = Html::rawElement( 'span',
799 [ 'class' => 'mw-changeslist-links' ],
800 // The spans are needed to ensure the dividing '|' elements are not
801 // themselves styled as links.
802 Html::rawElement( 'span', [], $difftext ) .
803 ' ' . // Space needed for separating two words.
804 Html::rawElement( 'span', [], $histlink )
805 );
806
807 # Tags, if any. Save some time using a cache.
808 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
809 $this->tagsCache->makeKey(
810 $row->ts_tags ?? '',
811 $this->getUser()->getName(),
812 $lang->getCode()
813 ),
815 $row->ts_tags,
816 null,
817 $this->getContext()
818 )
819 );
820 $classes = array_merge( $classes, $newClasses );
821
822 $this->hookRunner->onSpecialContributions__formatRow__flags(
823 $this->getContext(), $row, $flags );
824
825 $templateParams = [
826 'del' => $del,
827 'timestamp' => $d,
828 'diffHistLinks' => $diffHistLinks,
829 'charDifference' => $chardiff,
830 'flags' => $flags,
831 'articleLink' => $link,
832 'userlink' => $userlink,
833 'logText' => $comment,
834 'topmarktext' => $topmarktext,
835 'tagSummary' => $tagSummary,
836 ];
837
838 # Denote if username is redacted for this edit
839 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
840 $templateParams['rev-deleted-user-contribs'] =
841 $this->msg( 'rev-deleted-user-contribs' )->escaped();
842 }
843
844 $ret = $this->templateParser->processTemplate(
845 'SpecialContributionsLine',
846 $templateParams
847 );
848 }
849
850 // Let extensions add data
851 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
852 $attribs = array_filter( $attribs,
853 [ Sanitizer::class, 'isReservedDataAttribute' ],
854 ARRAY_FILTER_USE_KEY
855 );
856
857 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
858 // receiving empty rows?
859
860 if ( $classes === [] && $attribs === [] && $ret === '' ) {
861 wfDebug( "Dropping Special:Contribution row that could not be formatted" );
862 return "<!-- Could not format Special:Contribution row. -->\n";
863 }
864 $attribs['class'] = $classes;
865
866 // FIXME: The signature of the ContributionsLineEnding hook makes it
867 // very awkward to move this LI wrapper into the template.
868 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
869 }
870
875 protected function getSqlComment() {
876 if ( $this->namespace || $this->deletedOnly ) {
877 // potentially slow, see CR r58153
878 return 'contributions page filtered for namespace or RevisionDeleted edits';
879 } else {
880 return 'contributions page unfiltered';
881 }
882 }
883
887 protected function preventClickjacking() {
888 $this->setPreventClickjacking( true );
889 }
890
895 protected function setPreventClickjacking( bool $enable ) {
896 $this->preventClickjacking = $enable;
897 }
898
902 public function getPreventClickjacking() {
903 return $this->preventClickjacking;
904 }
905
912 public static function processDateFilter( array $opts ) {
913 $start = $opts['start'] ?? '';
914 $end = $opts['end'] ?? '';
915 $year = $opts['year'] ?? '';
916 $month = $opts['month'] ?? '';
917
918 if ( $start !== '' && $end !== '' && $start > $end ) {
919 $temp = $start;
920 $start = $end;
921 $end = $temp;
922 }
923
924 // If year/month legacy filtering options are set, convert them to display the new stamp
925 if ( $year !== '' || $month !== '' ) {
926 // Reuse getDateCond logic, but subtract a day because
927 // the endpoints of our date range appear inclusive
928 // but the internal end offsets are always exclusive
929 $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
930 $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
931 $legacyDateTime = $legacyDateTime->modify( '-1 day' );
932
933 // Clear the new timestamp range options if used and
934 // replace with the converted legacy timestamp
935 $start = '';
936 $end = $legacyDateTime->format( 'Y-m-d' );
937 }
938
939 $opts['start'] = $start;
940 $opts['end'] = $end;
941
942 return $opts;
943 }
944}
945
950class_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.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
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...
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: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:79
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 configuration instances.
Definition Config.php:32
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.