MediaWiki 1.40.4
ContribsPager.php
Go to the documentation of this file.
1<?php
38use Wikimedia\IPUtils;
43
49
50 public $mGroupByDate = true;
51
55 private $messages;
56
60 private $target;
61
65 private $namespace;
66
70 private $tagFilter;
71
75 private $tagInvert;
76
80 private $nsInvert;
81
86 private $associated;
87
91 private $deletedOnly;
92
96 private $topOnly;
97
101 private $newOnly;
102
106 private $hideMinor;
107
112 private $revisionsOnly;
113
114 private $preventClickjacking = false;
115
119 private $mParentLens;
120
122 private $targetUser;
123
127 private $templateParser;
128
130 private $actorMigration;
131
133 private $commentFormatter;
134
136 private $hookRunner;
137
139 private $linkBatchFactory;
140
142 private $namespaceInfo;
143
145 private $revisionStore;
146
148 private $formattedComments = [];
149
151 private $revisions = [];
152
167 public function __construct(
168 IContextSource $context,
169 array $options,
170 LinkRenderer $linkRenderer = null,
171 LinkBatchFactory $linkBatchFactory = null,
172 HookContainer $hookContainer = null,
173 ILoadBalancer $loadBalancer = null,
174 ActorMigration $actorMigration = null,
175 RevisionStore $revisionStore = null,
176 NamespaceInfo $namespaceInfo = null,
177 UserIdentity $targetUser = null,
178 CommentFormatter $commentFormatter = null
179 ) {
180 // Class is used directly in extensions - T266484
181 $services = MediaWikiServices::getInstance();
182 $loadBalancer ??= $services->getDBLoadBalancer();
183
184 // Set ->target before calling parent::__construct() so
185 // parent can call $this->getIndexField() and get the right result. Set
186 // the rest too just to keep things simple.
187 if ( $targetUser ) {
188 $this->target = $options['target'] ?? $targetUser->getName();
189 $this->targetUser = $targetUser;
190 } else {
191 // Use target option
192 // It's possible for the target to be empty. This is used by
193 // ContribsPagerTest and does not cause newFromName() to return
194 // false. It's probably not used by any production code.
195 $this->target = $options['target'] ?? '';
196 // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
197 $this->targetUser = $services->getUserFactory()->newFromName(
198 $this->target, UserRigorOptions::RIGOR_NONE
199 );
200 if ( !$this->targetUser ) {
201 // This can happen if the target contained "#". Callers
202 // typically pass user input through title normalization to
203 // avoid it.
204 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
205 'broken to use even with validation disabled.' );
206 }
207 }
208
209 $this->namespace = $options['namespace'] ?? '';
210 $this->tagFilter = $options['tagfilter'] ?? false;
211 $this->tagInvert = $options['tagInvert'] ?? false;
212 $this->nsInvert = $options['nsInvert'] ?? false;
213 $this->associated = $options['associated'] ?? false;
214
215 $this->deletedOnly = !empty( $options['deletedOnly'] );
216 $this->topOnly = !empty( $options['topOnly'] );
217 $this->newOnly = !empty( $options['newOnly'] );
218 $this->hideMinor = !empty( $options['hideMinor'] );
219 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
220
221 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
222 // Needed by call to getIndexField -> getTargetTable from parent constructor
223 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
224 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
225
226 $msgs = [
227 'diff',
228 'hist',
229 'pipe-separator',
230 'uctop'
231 ];
232
233 foreach ( $msgs as $msg ) {
234 $this->messages[$msg] = $this->msg( $msg )->escaped();
235 }
236
237 // Date filtering: use timestamp if available
238 $startTimestamp = '';
239 $endTimestamp = '';
240 if ( isset( $options['start'] ) && $options['start'] ) {
241 $startTimestamp = $options['start'] . ' 00:00:00';
242 }
243 if ( isset( $options['end'] ) && $options['end'] ) {
244 $endTimestamp = $options['end'] . ' 23:59:59';
245 }
246 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
247
248 $this->templateParser = new TemplateParser();
249 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
250 $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
251 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
252 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
253 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
254 }
255
256 public function getDefaultQuery() {
257 $query = parent::getDefaultQuery();
258 $query['target'] = $this->target;
259
260 return $query;
261 }
262
272 public function reallyDoQuery( $offset, $limit, $order ) {
273 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo(
274 $offset,
275 $limit,
276 $order
277 );
278
279 $options['MAX_EXECUTION_TIME'] =
280 $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
281 /*
282 * This hook will allow extensions to add in additional queries, so they can get their data
283 * in My Contributions as well. Extensions should append their results to the $data array.
284 *
285 * Extension queries have to implement the navbar requirement as well. They should
286 * - have a column aliased as $pager->getIndexField()
287 * - have LIMIT set
288 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
289 * - have the ORDER BY specified based upon the details provided by the navbar
290 *
291 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
292 *
293 * &$data: an array of results of all contribs queries
294 * $pager: the ContribsPager object hooked into
295 * $offset: see phpdoc above
296 * $limit: see phpdoc above
297 * $descending: see phpdoc above
298 */
299 $dbr = $this->getDatabase();
300 $data = [ $dbr->select(
301 $tables, $fields, $conds, $fname, $options, $join_conds
302 ) ];
303 if ( !$this->revisionsOnly ) {
304 // TODO: Range offsets are fairly important and all handlers should take care of it.
305 // If this hook will be replaced (e.g. unified with the DeletedContribsPager one),
306 // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577).
307 $this->hookRunner->onContribsPager__reallyDoQuery(
308 $data, $this, $offset, $limit, $order );
309 }
310
311 $result = [];
312
313 // loop all results and collect them in an array
314 foreach ( $data as $query ) {
315 foreach ( $query as $i => $row ) {
316 // If the query results are in descending order, the indexes must also be in descending order
317 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
318 // Left-pad with zeroes, because these values will be sorted as strings
319 $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
320 // use index column as key, allowing us to easily sort in PHP
321 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
322 }
323 }
324
325 // sort results
326 if ( $order === self::QUERY_ASCENDING ) {
327 ksort( $result );
328 } else {
329 krsort( $result );
330 }
331
332 // enforce limit
333 $result = array_slice( $result, 0, $limit );
334
335 // get rid of array keys
336 $result = array_values( $result );
337
338 return new FakeResultWrapper( $result );
339 }
340
350 private function getTargetTable() {
351 $dbr = $this->getDatabase();
352 $ipRangeConds = $this->targetUser->isRegistered()
353 ? null : $this->getIpRangeConds( $dbr, $this->target );
354 if ( $ipRangeConds ) {
355 return 'ip_changes';
356 }
357
358 return 'revision';
359 }
360
361 public function getQueryInfo() {
362 $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
363 $queryInfo = [
364 'tables' => $revQuery['tables'],
365 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
366 'conds' => [],
367 'options' => [],
368 'join_conds' => $revQuery['joins'],
369 ];
370
371 // WARNING: Keep this in sync with getTargetTable()!
372 $dbr = $this->getDatabase();
373 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
374 if ( $ipRangeConds ) {
375 // Put ip_changes first (T284419)
376 array_unshift( $queryInfo['tables'], 'ip_changes' );
377 $queryInfo['join_conds']['revision'] = [
378 'JOIN', [ 'rev_id = ipc_rev_id' ]
379 ];
380 $queryInfo['conds'][] = $ipRangeConds;
381 } else {
382 // tables and joins are already handled by RevisionStore::getQueryInfo()
383 $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
384 $queryInfo['conds'][] = $conds['conds'];
385 // Force the appropriate index to avoid bad query plans (T307295)
386 if ( isset( $conds['orconds']['newactor'] ) ) {
387 $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
388 }
389 }
390
391 if ( $this->deletedOnly ) {
392 $queryInfo['conds'][] = 'rev_deleted != 0';
393 }
394
395 if ( $this->topOnly ) {
396 $queryInfo['conds'][] = 'rev_id = page_latest';
397 }
398
399 if ( $this->newOnly ) {
400 $queryInfo['conds'][] = 'rev_parent_id = 0';
401 }
402
403 if ( $this->hideMinor ) {
404 $queryInfo['conds'][] = 'rev_minor_edit = 0';
405 }
406
407 $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
408
409 // Paranoia: avoid brute force searches (T19342)
410 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
411 $queryInfo['conds'][] = $dbr->bitAnd(
412 'rev_deleted', RevisionRecord::DELETED_USER
413 ) . ' = 0';
414 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
415 $queryInfo['conds'][] = $dbr->bitAnd(
416 'rev_deleted', RevisionRecord::SUPPRESSED_USER
417 ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
418 }
419
420 // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
421 $indexField = $this->getIndexField();
422 if ( $indexField !== 'rev_timestamp' ) {
423 $queryInfo['fields'][] = $indexField;
424 }
425
427 $queryInfo['tables'],
428 $queryInfo['fields'],
429 $queryInfo['conds'],
430 $queryInfo['join_conds'],
431 $queryInfo['options'],
432 $this->tagFilter,
433 $this->tagInvert,
434 );
435
436 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
437
438 return $queryInfo;
439 }
440
441 protected function getNamespaceCond() {
442 if ( $this->namespace !== '' ) {
443 $dbr = $this->getDatabase();
444 $selectedNS = $dbr->addQuotes( $this->namespace );
445 $eq_op = $this->nsInvert ? '!=' : '=';
446 $bool_op = $this->nsInvert ? 'AND' : 'OR';
447
448 if ( !$this->associated ) {
449 return [ "page_namespace $eq_op $selectedNS" ];
450 }
451
452 $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
453
454 return [
455 "page_namespace $eq_op $selectedNS " .
456 $bool_op .
457 " page_namespace $eq_op $associatedNS"
458 ];
459 }
460
461 return [];
462 }
463
470 private function getIpRangeConds( $db, $ip ) {
471 // First make sure it is a valid range and they are not outside the CIDR limit
472 if ( !$this->isQueryableRange( $ip ) ) {
473 return false;
474 }
475
476 [ $start, $end ] = IPUtils::parseRange( $ip );
477
478 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
479 }
480
488 public function isQueryableRange( $ipRange ) {
489 $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
490
491 $bits = IPUtils::parseCIDR( $ipRange )[1];
492 if (
493 ( $bits === false ) ||
494 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
495 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
496 ) {
497 return false;
498 }
499
500 return true;
501 }
502
506 public function getIndexField() {
507 // The returned column is used for sorting and continuation, so we need to
508 // make sure to use the right denormalized column depending on which table is
509 // being targeted by the query to avoid bad query plans.
510 // See T200259, T204669, T220991, and T221380.
511 $target = $this->getTargetTable();
512 switch ( $target ) {
513 case 'revision':
514 return 'rev_timestamp';
515 case 'ip_changes':
516 return 'ipc_rev_timestamp';
517 default:
518 wfWarn(
519 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
520 );
521 return 'rev_timestamp';
522 }
523 }
524
528 public function getTagFilter() {
529 return $this->tagFilter;
530 }
531
535 public function getTarget() {
536 return $this->target;
537 }
538
542 public function isNewOnly() {
543 return $this->newOnly;
544 }
545
549 public function getNamespace() {
550 return $this->namespace;
551 }
552
556 protected function getExtraSortFields() {
557 // The returned columns are used for sorting, so we need to make sure
558 // to use the right denormalized column depending on which table is
559 // being targeted by the query to avoid bad query plans.
560 // See T200259, T204669, T220991, and T221380.
561 $target = $this->getTargetTable();
562 switch ( $target ) {
563 case 'revision':
564 return [ 'rev_id' ];
565 case 'ip_changes':
566 return [ 'ipc_rev_id' ];
567 default:
568 wfWarn(
569 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
570 );
571 return [ 'rev_id' ];
572 }
573 }
574
575 protected function doBatchLookups() {
576 # Do a link batch query
577 $this->mResult->seek( 0 );
578 $parentRevIds = [];
579 $this->mParentLens = [];
580 $revisions = [];
581 $linkBatch = $this->linkBatchFactory->newLinkBatch();
582 $isIpRange = $this->isQueryableRange( $this->target );
583 # Give some pointers to make (last) links
584 foreach ( $this->mResult as $row ) {
585 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
586 $parentRevIds[] = (int)$row->rev_parent_id;
587 }
588 if ( $this->revisionStore->isRevisionRow( $row ) ) {
589 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
590 if ( $isIpRange ) {
591 // If this is an IP range, batch the IP's talk page
592 $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
593 }
594 $linkBatch->add( $row->page_namespace, $row->page_title );
595 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
596 }
597 }
598 # Fetch rev_len for revisions not already scanned above
599 $this->mParentLens += $this->revisionStore->getRevisionSizes(
600 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
601 );
602 $linkBatch->execute();
603
604 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
605 ->authority( $this->getAuthority() )
606 ->revisions( $revisions )
607 ->hideIfDeleted()
608 ->execute();
609
610 # For performance, save the revision objects for later.
611 # The array is indexed by rev_id. doBatchLookups() may be called
612 # multiple times with different results, so merge the revisions array,
613 # ignoring any duplicates.
614 $this->revisions += $revisions;
615 }
616
620 protected function getStartBody() {
621 return "<section class='mw-pager-body'>\n";
622 }
623
627 protected function getEndBody() {
628 return "</section>\n";
629 }
630
641 public function tryCreatingRevisionRecord( $row, $title = null ) {
642 if ( $row instanceof stdClass && isset( $row->rev_id )
643 && isset( $this->revisions[$row->rev_id] )
644 ) {
645 return $this->revisions[$row->rev_id];
646 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
647 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
648 } else {
649 return null;
650 }
651 }
652
665 public function formatRow( $row ) {
666 $ret = '';
667 $classes = [];
668 $attribs = [];
669
670 $linkRenderer = $this->getLinkRenderer();
671
672 $page = null;
673 // Create a title for the revision if possible
674 // Rows from the hook may not include title information
675 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
676 $page = Title::newFromRow( $row );
677 }
678 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
679 // function to be called with a special object for $row. It expects us
680 // skip formatting so that the row can be formatted by the
681 // ContributionsLineEnding hook below.
682 // FIXME: have some better way for extensions to provide formatted rows.
683 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
684 if ( $revRecord && $page ) {
685 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
686 $attribs['data-mw-revid'] = $revRecord->getId();
687
688 $link = $linkRenderer->makeLink(
689 $page,
690 $page->getPrefixedText(),
691 [ 'class' => 'mw-contributions-title' ],
692 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
693 );
694 # Mark current revisions
695 $topmarktext = '';
696
697 $pagerTools = new PagerTools(
698 $revRecord,
699 null,
700 $row->rev_id === $row->page_latest && !$row->page_is_new,
701 $this->hookRunner,
702 $page,
703 $this->getContext(),
704 $this->getLinkRenderer()
705 );
706 if ( $row->rev_id === $row->page_latest ) {
707 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
708 $classes[] = 'mw-contributions-current';
709 }
710 if ( $pagerTools->shouldPreventClickjacking() ) {
711 $this->setPreventClickjacking( true );
712 }
713 $topmarktext .= $pagerTools->toHTML();
714 # Is there a visible previous revision?
715 if ( $revRecord->getParentId() !== 0 &&
716 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
717 ) {
718 $difftext = $linkRenderer->makeKnownLink(
719 $page,
720 new HtmlArmor( $this->messages['diff'] ),
721 [ 'class' => 'mw-changeslist-diff' ],
722 [
723 'diff' => 'prev',
724 'oldid' => $row->rev_id
725 ]
726 );
727 } else {
728 $difftext = $this->messages['diff'];
729 }
730 $histlink = $linkRenderer->makeKnownLink(
731 $page,
732 new HtmlArmor( $this->messages['hist'] ),
733 [ 'class' => 'mw-changeslist-history' ],
734 [ 'action' => 'history' ]
735 );
736
737 if ( $row->rev_parent_id === null ) {
738 // For some reason rev_parent_id isn't populated for this row.
739 // Its rumoured this is true on wikipedia for some revisions (T36922).
740 // Next best thing is to have the total number of bytes.
741 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
742 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
743 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
744 } else {
745 $parentLen = 0;
746 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
747 $parentLen = $this->mParentLens[$row->rev_parent_id];
748 }
749
750 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
751 $chardiff .= ChangesList::showCharacterDifference(
752 $parentLen,
753 $row->rev_len,
754 $this->getContext()
755 );
756 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
757 }
758
759 $lang = $this->getLanguage();
760
761 $comment = $this->formattedComments[$row->rev_id];
762
763 if ( $comment === '' ) {
764 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
765 $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
766 }
767
768 $comment = $lang->getDirMark() . $comment;
769
770 $authority = $this->getAuthority();
771 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
772
773 # When querying for an IP range, we want to always show user and user talk links.
774 $userlink = '';
775 $revUser = $revRecord->getUser();
776 $revUserId = $revUser ? $revUser->getId() : 0;
777 $revUserText = $revUser ? $revUser->getName() : '';
778 if ( $this->isQueryableRange( $this->target ) ) {
779 $userlink = ' <span class="mw-changeslist-separator"></span> '
780 . $lang->getDirMark()
781 . Linker::userLink( $revUserId, $revUserText );
782 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
783 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
784 }
785
786 $flags = [];
787 if ( $revRecord->getParentId() === 0 ) {
788 $flags[] = ChangesList::flag( 'newpage' );
789 }
790
791 if ( $revRecord->isMinor() ) {
792 $flags[] = ChangesList::flag( 'minor' );
793 }
794
795 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
796 if ( $del !== '' ) {
797 $del .= ' ';
798 }
799
800 // While it might be tempting to use a list here
801 // this would result in clutter and slows down navigating the content
802 // in assistive technology.
803 // See https://phabricator.wikimedia.org/T205581#4734812
804 $diffHistLinks = Html::rawElement( 'span',
805 [ 'class' => 'mw-changeslist-links' ],
806 // The spans are needed to ensure the dividing '|' elements are not
807 // themselves styled as links.
808 Html::rawElement( 'span', [], $difftext ) .
809 ' ' . // Space needed for separating two words.
810 Html::rawElement( 'span', [], $histlink )
811 );
812
813 # Tags, if any.
814 [ $tagSummary, $newClasses ] = ChangeTags::formatSummaryRow(
815 $row->ts_tags,
816 null,
817 $this->getContext()
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}
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()
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.
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:55
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:67
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.
Represents a title within MediaWiki.
Definition Title.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Generate a set of tools for a revision.
Pager for filtering by a range of dates.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.For b/c, query direction is true for ascending and fal...
getDateRangeCond( $startTime, $endTime)
Set and return a date range condition using timestamps provided by the user.
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 ...
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:36
This class is a delegate to ILBFactory for a given database cluster.
Result wrapper for grabbing data queried from an IDatabase object.
if(!isset( $args[0])) $lang