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