MediaWiki REL1_39
ContribsPager.php
Go to the documentation of this file.
1<?php
33use Wikimedia\IPUtils;
38
44
45 public $mGroupByDate = true;
46
50 private $messages;
51
55 private $target;
56
60 private $namespace;
61
65 private $tagFilter;
66
70 private $nsInvert;
71
76 private $associated;
77
81 private $deletedOnly;
82
86 private $topOnly;
87
91 private $newOnly;
92
96 private $hideMinor;
97
102 private $revisionsOnly;
103
104 private $preventClickjacking = false;
105
109 private $mParentLens;
110
112 private $targetUser;
113
117 private $templateParser;
118
120 private $actorMigration;
121
123 private $commentFormatter;
124
126 private $hookRunner;
127
129 private $linkBatchFactory;
130
132 private $namespaceInfo;
133
135 private $revisionStore;
136
138 private $formattedComments = [];
139
141 private $revisions = [];
142
157 public function __construct(
158 IContextSource $context,
159 array $options,
160 LinkRenderer $linkRenderer = null,
161 LinkBatchFactory $linkBatchFactory = null,
162 HookContainer $hookContainer = null,
163 ILoadBalancer $loadBalancer = null,
164 ActorMigration $actorMigration = null,
165 RevisionStore $revisionStore = null,
166 NamespaceInfo $namespaceInfo = null,
167 UserIdentity $targetUser = null,
168 CommentFormatter $commentFormatter = null
169 ) {
170 // Class is used directly in extensions - T266484
171 $services = MediaWikiServices::getInstance();
172 $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
173
174 // Set ->target before calling parent::__construct() so
175 // parent can call $this->getIndexField() and get the right result. Set
176 // the rest too just to keep things simple.
177 if ( $targetUser ) {
178 $this->target = $options['target'] ?? $targetUser->getName();
179 $this->targetUser = $targetUser;
180 } else {
181 // Use target option
182 // It's possible for the target to be empty. This is used by
183 // ContribsPagerTest and does not cause newFromName() to return
184 // false. It's probably not used by any production code.
185 $this->target = $options['target'] ?? '';
186 // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
187 $this->targetUser = $services->getUserFactory()->newFromName(
188 $this->target, UserRigorOptions::RIGOR_NONE
189 );
190 if ( !$this->targetUser ) {
191 // This can happen if the target contained "#". Callers
192 // typically pass user input through title normalization to
193 // avoid it.
194 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
195 'broken to use even with validation disabled.' );
196 }
197 }
198
199 $this->namespace = $options['namespace'] ?? '';
200 $this->tagFilter = $options['tagfilter'] ?? false;
201 $this->nsInvert = $options['nsInvert'] ?? false;
202 $this->associated = $options['associated'] ?? false;
203
204 $this->deletedOnly = !empty( $options['deletedOnly'] );
205 $this->topOnly = !empty( $options['topOnly'] );
206 $this->newOnly = !empty( $options['newOnly'] );
207 $this->hideMinor = !empty( $options['hideMinor'] );
208 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
209
210 // Most of this code will use the 'contributions' group DB, which can map to replica DBs
211 // with extra user based indexes or partitioning by user.
212 // Set database before parent constructor to avoid setting it there with wfGetDB
213 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'contributions' );
214 // Needed by call to getIndexField -> getTargetTable from parent constructor
215 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
216 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
217
218 $msgs = [
219 'diff',
220 'hist',
221 'pipe-separator',
222 'uctop'
223 ];
224
225 foreach ( $msgs as $msg ) {
226 $this->messages[$msg] = $this->msg( $msg )->escaped();
227 }
228
229 // Date filtering: use timestamp if available
230 $startTimestamp = '';
231 $endTimestamp = '';
232 if ( isset( $options['start'] ) && $options['start'] ) {
233 $startTimestamp = $options['start'] . ' 00:00:00';
234 }
235 if ( isset( $options['end'] ) && $options['end'] ) {
236 $endTimestamp = $options['end'] . ' 23:59:59';
237 }
238 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
239
240 $this->templateParser = new TemplateParser();
241 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
242 $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
243 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
244 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
245 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
246 }
247
248 public function getDefaultQuery() {
249 $query = parent::getDefaultQuery();
250 $query['target'] = $this->target;
251
252 return $query;
253 }
254
264 public function reallyDoQuery( $offset, $limit, $order ) {
265 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
266 $offset,
267 $limit,
268 $order
269 );
270
271 $options['MAX_EXECUTION_TIME'] =
272 $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
273 /*
274 * This hook will allow extensions to add in additional queries, so they can get their data
275 * in My Contributions as well. Extensions should append their results to the $data array.
276 *
277 * Extension queries have to implement the navbar requirement as well. They should
278 * - have a column aliased as $pager->getIndexField()
279 * - have LIMIT set
280 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
281 * - have the ORDER BY specified based upon the details provided by the navbar
282 *
283 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
284 *
285 * &$data: an array of results of all contribs queries
286 * $pager: the ContribsPager object hooked into
287 * $offset: see phpdoc above
288 * $limit: see phpdoc above
289 * $descending: see phpdoc above
290 */
291 $dbr = $this->getDatabase();
292 $data = [ $dbr->select(
293 $tables, $fields, $conds, $fname, $options, $join_conds
294 ) ];
295 if ( !$this->revisionsOnly ) {
296 $this->hookRunner->onContribsPager__reallyDoQuery(
297 $data, $this, $offset, $limit, $order );
298 }
299
300 $result = [];
301
302 // loop all results and collect them in an array
303 foreach ( $data as $query ) {
304 foreach ( $query as $i => $row ) {
305 // If the query results are in descending order, the indexes must also be in descending order
306 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
307 // Left-pad with zeroes, because these values will be sorted as strings
308 $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
309 // use index column as key, allowing us to easily sort in PHP
310 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
311 }
312 }
313
314 // sort results
315 if ( $order === self::QUERY_ASCENDING ) {
316 ksort( $result );
317 } else {
318 krsort( $result );
319 }
320
321 // enforce limit
322 $result = array_slice( $result, 0, $limit );
323
324 // get rid of array keys
325 $result = array_values( $result );
326
327 return new FakeResultWrapper( $result );
328 }
329
339 private function getTargetTable() {
340 $dbr = $this->getDatabase();
341 $ipRangeConds = $this->targetUser->isRegistered()
342 ? null : $this->getIpRangeConds( $dbr, $this->target );
343 if ( $ipRangeConds ) {
344 return 'ip_changes';
345 }
346
347 return 'revision';
348 }
349
350 public function getQueryInfo() {
351 $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
352 $queryInfo = [
353 'tables' => $revQuery['tables'],
354 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
355 'conds' => [],
356 'options' => [],
357 'join_conds' => $revQuery['joins'],
358 ];
359
360 // WARNING: Keep this in sync with getTargetTable()!
361 $dbr = $this->getDatabase();
362 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
363 if ( $ipRangeConds ) {
364 // Put ip_changes first (T284419)
365 array_unshift( $queryInfo['tables'], 'ip_changes' );
366 $queryInfo['join_conds']['revision'] = [
367 'JOIN', [ 'rev_id = ipc_rev_id' ]
368 ];
369 $queryInfo['conds'][] = $ipRangeConds;
370 } else {
371 // tables and joins are already handled by RevisionStore::getQueryInfo()
372 $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
373 $queryInfo['conds'][] = $conds['conds'];
374 // Force the appropriate index to avoid bad query plans (T189026 and T307295)
375 if ( isset( $conds['orconds']['actor'] ) ) {
376 $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
377 }
378 if ( isset( $conds['orconds']['newactor'] ) ) {
379 $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
380 }
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 );
426
427 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
428
429 return $queryInfo;
430 }
431
432 protected function getNamespaceCond() {
433 if ( $this->namespace !== '' ) {
434 $dbr = $this->getDatabase();
435 $selectedNS = $dbr->addQuotes( $this->namespace );
436 $eq_op = $this->nsInvert ? '!=' : '=';
437 $bool_op = $this->nsInvert ? 'AND' : 'OR';
438
439 if ( !$this->associated ) {
440 return [ "page_namespace $eq_op $selectedNS" ];
441 }
442
443 $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
444
445 return [
446 "page_namespace $eq_op $selectedNS " .
447 $bool_op .
448 " page_namespace $eq_op $associatedNS"
449 ];
450 }
451
452 return [];
453 }
454
461 private function getIpRangeConds( $db, $ip ) {
462 // First make sure it is a valid range and they are not outside the CIDR limit
463 if ( !$this->isQueryableRange( $ip ) ) {
464 return false;
465 }
466
467 list( $start, $end ) = IPUtils::parseRange( $ip );
468
469 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
470 }
471
479 public function isQueryableRange( $ipRange ) {
480 $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
481
482 $bits = IPUtils::parseCIDR( $ipRange )[1];
483 if (
484 ( $bits === false ) ||
485 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
486 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
487 ) {
488 return false;
489 }
490
491 return true;
492 }
493
497 public function getIndexField() {
498 // The returned column is used for sorting and continuation, so we need to
499 // make sure to use the right denormalized column depending on which table is
500 // being targeted by the query to avoid bad query plans.
501 // See T200259, T204669, T220991, and T221380.
502 $target = $this->getTargetTable();
503 switch ( $target ) {
504 case 'revision':
505 return 'rev_timestamp';
506 case 'ip_changes':
507 return 'ipc_rev_timestamp';
508 default:
509 wfWarn(
510 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
511 );
512 return 'rev_timestamp';
513 }
514 }
515
519 public function getTagFilter() {
520 return $this->tagFilter;
521 }
522
526 public function getTarget() {
527 return $this->target;
528 }
529
533 public function isNewOnly() {
534 return $this->newOnly;
535 }
536
540 public function getNamespace() {
541 return $this->namespace;
542 }
543
547 protected function getExtraSortFields() {
548 // The returned columns are used for sorting, so we need to make sure
549 // to use the right denormalized column depending on which table is
550 // being targeted by the query to avoid bad query plans.
551 // See T200259, T204669, T220991, and T221380.
552 $target = $this->getTargetTable();
553 switch ( $target ) {
554 case 'revision':
555 return [ 'rev_id' ];
556 case 'ip_changes':
557 return [ 'ipc_rev_id' ];
558 default:
559 wfWarn(
560 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
561 );
562 return [ 'rev_id' ];
563 }
564 }
565
566 protected function doBatchLookups() {
567 # Do a link batch query
568 $this->mResult->seek( 0 );
569 $parentRevIds = [];
570 $this->mParentLens = [];
571 $revisions = [];
572 $linkBatch = $this->linkBatchFactory->newLinkBatch();
573 $isIpRange = $this->isQueryableRange( $this->target );
574 # Give some pointers to make (last) links
575 foreach ( $this->mResult as $row ) {
576 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
577 $parentRevIds[] = (int)$row->rev_parent_id;
578 }
579 if ( $this->revisionStore->isRevisionRow( $row ) ) {
580 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
581 if ( $isIpRange ) {
582 // If this is an IP range, batch the IP's talk page
583 $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
584 }
585 $linkBatch->add( $row->page_namespace, $row->page_title );
586 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
587 }
588 }
589 # Fetch rev_len for revisions not already scanned above
590 $this->mParentLens += $this->revisionStore->getRevisionSizes(
591 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
592 );
593 $linkBatch->execute();
594
595 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
596 ->authority( $this->getAuthority() )
597 ->revisions( $revisions )
598 ->hideIfDeleted()
599 ->execute();
600
601 # For performance, save the revision objects for later.
602 # The array is indexed by rev_id. doBatchLookups() may be called
603 # multiple times with different results, so merge the revisions array,
604 # ignoring any duplicates.
605 $this->revisions += $revisions;
606 }
607
611 protected function getStartBody() {
612 return "<section class='mw-pager-body'>\n";
613 }
614
618 protected function getEndBody() {
619 return "</section>\n";
620 }
621
632 public function tryCreatingRevisionRecord( $row, $title = null ) {
633 if ( $row instanceof stdClass && isset( $row->rev_id )
634 && isset( $this->revisions[$row->rev_id] )
635 ) {
636 return $this->revisions[$row->rev_id];
637 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
638 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
639 } else {
640 return null;
641 }
642 }
643
656 public function formatRow( $row ) {
657 $ret = '';
658 $classes = [];
659 $attribs = [];
660
661 $linkRenderer = $this->getLinkRenderer();
662
663 $page = null;
664 // Create a title for the revision if possible
665 // Rows from the hook may not include title information
666 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
667 $page = Title::newFromRow( $row );
668 }
669 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
670 // function to be called with a special object for $row. It expects us
671 // skip formatting so that the row can be formatted by the
672 // ContributionsLineEnding hook below.
673 // FIXME: have some better way for extensions to provide formatted rows.
674 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
675 if ( $revRecord ) {
676 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
677 $attribs['data-mw-revid'] = $revRecord->getId();
678
679 $link = $linkRenderer->makeLink(
680 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
681 $page,
682 $page->getPrefixedText(),
683 [ 'class' => 'mw-contributions-title' ],
684 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
685 );
686 # Mark current revisions
687 $topmarktext = '';
688
689 if ( $row->rev_id === $row->page_latest ) {
690 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
691 $classes[] = 'mw-contributions-current';
692 # Add rollback link
693 if ( !$row->page_is_new &&
694 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
695 $this->getAuthority()->probablyCan( 'rollback', $page ) &&
696 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
697 $this->getAuthority()->probablyCan( 'edit', $page )
698 ) {
699 $this->setPreventClickjacking( true );
700 $topmarktext .= ' ' . Linker::generateRollback(
701 $revRecord,
702 $this->getContext(),
703 [ 'noBrackets' ]
704 );
705 }
706 }
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 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
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 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
726 $page,
727 new HtmlArmor( $this->messages['hist'] ),
728 [ 'class' => 'mw-changeslist-history' ],
729 [ 'action' => 'history' ]
730 );
731
732 if ( $row->rev_parent_id === null ) {
733 // For some reason rev_parent_id isn't populated for this row.
734 // Its rumoured this is true on wikipedia for some revisions (T36922).
735 // Next best thing is to have the total number of bytes.
736 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
737 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
738 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
739 } else {
740 $parentLen = 0;
741 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
742 $parentLen = $this->mParentLens[$row->rev_parent_id];
743 }
744
745 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
746 $chardiff .= ChangesList::showCharacterDifference(
747 $parentLen,
748 $row->rev_len,
749 $this->getContext()
750 );
751 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
752 }
753
754 $lang = $this->getLanguage();
755
756 $comment = $this->formattedComments[$row->rev_id];
757
758 if ( $comment === '' ) {
759 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
760 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
761 }
762
763 $comment = $lang->getDirMark() . $comment;
764
765 $authority = $this->getAuthority();
766 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
767
768 # When querying for an IP range, we want to always show user and user talk links.
769 $userlink = '';
770 $revUser = $revRecord->getUser();
771 $revUserId = $revUser ? $revUser->getId() : 0;
772 $revUserText = $revUser ? $revUser->getName() : '';
773 if ( $this->isQueryableRange( $this->target ) ) {
774 $userlink = ' <span class="mw-changeslist-separator"></span> '
775 . $lang->getDirMark()
776 . Linker::userLink( $revUserId, $revUserText );
777 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
778 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
779 }
780
781 $flags = [];
782 if ( $revRecord->getParentId() === 0 ) {
783 $flags[] = ChangesList::flag( 'newpage' );
784 }
785
786 if ( $revRecord->isMinor() ) {
787 $flags[] = ChangesList::flag( 'minor' );
788 }
789
790 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
791 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
792 if ( $del !== '' ) {
793 $del .= ' ';
794 }
795
796 // While it might be tempting to use a list here
797 // this would result in clutter and slows down navigating the content
798 // in assistive technology.
799 // See https://phabricator.wikimedia.org/T205581#4734812
800 $diffHistLinks = Html::rawElement( 'span',
801 [ 'class' => 'mw-changeslist-links' ],
802 // The spans are needed to ensure the dividing '|' elements are not
803 // themselves styled as links.
804 Html::rawElement( 'span', [], $difftext ) .
805 ' ' . // Space needed for separating two words.
806 Html::rawElement( 'span', [], $histlink )
807 );
808
809 # Tags, if any.
810 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
811 $row->ts_tags,
812 'contributions',
813 $this->getContext()
814 );
815 $classes = array_merge( $classes, $newClasses );
816
817 $this->hookRunner->onSpecialContributions__formatRow__flags(
818 $this->getContext(), $row, $flags );
819
820 $templateParams = [
821 'del' => $del,
822 'timestamp' => $d,
823 'diffHistLinks' => $diffHistLinks,
824 'charDifference' => $chardiff,
825 'flags' => $flags,
826 'articleLink' => $link,
827 'userlink' => $userlink,
828 'logText' => $comment,
829 'topmarktext' => $topmarktext,
830 'tagSummary' => $tagSummary,
831 ];
832
833 # Denote if username is redacted for this edit
834 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
835 $templateParams['rev-deleted-user-contribs'] =
836 $this->msg( 'rev-deleted-user-contribs' )->escaped();
837 }
838
839 $ret = $this->templateParser->processTemplate(
840 'SpecialContributionsLine',
841 $templateParams
842 );
843 }
844
845 // Let extensions add data
846 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
847 $attribs = array_filter( $attribs,
848 [ Sanitizer::class, 'isReservedDataAttribute' ],
849 ARRAY_FILTER_USE_KEY
850 );
851
852 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
853 // receiving empty rows?
854
855 if ( $classes === [] && $attribs === [] && $ret === '' ) {
856 wfDebug( "Dropping Special:Contribution row that could not be formatted" );
857 return "<!-- Could not format Special:Contribution row. -->\n";
858 }
859 $attribs['class'] = $classes;
860
861 // FIXME: The signature of the ContributionsLineEnding hook makes it
862 // very awkward to move this LI wrapper into the template.
863 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
864 }
865
870 protected function getSqlComment() {
871 if ( $this->namespace || $this->deletedOnly ) {
872 // potentially slow, see CR r58153
873 return 'contributions page filtered for namespace or RevisionDeleted edits';
874 } else {
875 return 'contributions page unfiltered';
876 }
877 }
878
882 protected function preventClickjacking() {
883 $this->setPreventClickjacking( true );
884 }
885
890 protected function setPreventClickjacking( bool $enable ) {
891 $this->preventClickjacking = $enable;
892 }
893
897 public function getPreventClickjacking() {
898 return $this->preventClickjacking;
899 }
900
907 public static function processDateFilter( array $opts ) {
908 $start = $opts['start'] ?? '';
909 $end = $opts['end'] ?? '';
910 $year = $opts['year'] ?? '';
911 $month = $opts['month'] ?? '';
912
913 if ( $start !== '' && $end !== '' && $start > $end ) {
914 $temp = $start;
915 $start = $end;
916 $end = $temp;
917 }
918
919 // If year/month legacy filtering options are set, convert them to display the new stamp
920 if ( $year !== '' || $month !== '' ) {
921 // Reuse getDateCond logic, but subtract a day because
922 // the endpoints of our date range appear inclusive
923 // but the internal end offsets are always exclusive
924 $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
925 $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
926 $legacyDateTime = $legacyDateTime->modify( '-1 day' );
927
928 // Clear the new timestamp range options if used and
929 // replace with the converted legacy timestamp
930 $start = '';
931 $end = $legacyDateTime->format( 'Y-m-d' );
932 }
933
934 $opts['start'] = $start;
935 $opts['end'] = $end;
936
937 return $opts;
938 }
939}
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()
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
static formatSummaryRow( $tags, $page, 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()
Pager for Special:Contributions.
static processDateFilter(array $opts)
Set up date filter options, given request data.
tryCreatingRevisionRecord( $row, $title=null)
If the object looks like a revision row, or corresponds to a previously cached revision,...
getQueryInfo()
Provides all parameters needed for the main paged query.
__construct(IContextSource $context, array $options, LinkRenderer $linkRenderer=null, LinkBatchFactory $linkBatchFactory=null, HookContainer $hookContainer=null, ILoadBalancer $loadBalancer=null, ActorMigration $actorMigration=null, RevisionStore $revisionStore=null, NamespaceInfo $namespaceInfo=null, UserIdentity $targetUser=null, CommentFormatter $commentFormatter=null)
FIXME List services first T266484 / T290405.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
isQueryableRange( $ipRange)
Is the given IP a range and within the CIDR limit?
getSqlComment()
Overwrite Pager function and return a helpful comment.
formatRow( $row)
Generates each row in the contributions list.
reallyDoQuery( $offset, $limit, $order)
This method basically executes the exact same code as the parent class, though with a hook added,...
getStartBody()
Hook into getBody(), allows text to be inserted at the start.This will be called even if there are no...
getEndBody()
Hook into getBody() for the end of the list.to overridestring
setPreventClickjacking(bool $enable)
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
getDatabase()
Get the Database object in use.
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition Linker.php:1114
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2153
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1834
static formatRevisionSize( $size)
Definition Linker.php:1623
static userTalkLink( $userId, $userText)
Definition Linker.php:1254
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...
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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...
Pager for filtering by a range of dates.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
getDateRangeCond( $startStamp, $endStamp)
Set and return a date range condition using timestamps provided by the user.
static getOffsetDate( $year, $month, $day=-1)
Core logic of determining the mOffset timestamp such that we can get all items with a timestamp up to...
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
Create and track the database connections and transactions for a given database cluster.
Result wrapper for grabbing data queried from an IDatabase object.
if(!isset( $args[0])) $lang