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