MediaWiki REL1_37
ContribsPager.php
Go to the documentation of this file.
1<?php
31use Wikimedia\IPUtils;
36
42
46 private $messages;
47
51 private $target;
52
56 private $namespace;
57
61 private $tagFilter;
62
66 private $nsInvert;
67
72 private $associated;
73
77 private $deletedOnly;
78
82 private $topOnly;
83
87 private $newOnly;
88
92 private $hideMinor;
93
99
100 private $preventClickjacking = false;
101
106
108 private $targetUser;
109
114
117
119 private $hookRunner;
120
123
126
129
142 public function __construct(
143 IContextSource $context,
144 array $options,
145 LinkRenderer $linkRenderer = null,
146 LinkBatchFactory $linkBatchFactory = null,
147 HookContainer $hookContainer = null,
148 ILoadBalancer $loadBalancer = null,
149 ActorMigration $actorMigration = null,
150 RevisionStore $revisionStore = null,
151 NamespaceInfo $namespaceInfo = null,
152 UserIdentity $targetUser = null
153 ) {
154 // Class is used directly in extensions - T266484
155 $services = MediaWikiServices::getInstance();
156 $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
157
158 // Set ->target before calling parent::__construct() so
159 // parent can call $this->getIndexField() and get the right result. Set
160 // the rest too just to keep things simple.
161 if ( $targetUser ) {
162 $this->target = $options['target'] ?? $targetUser->getName();
163 $this->targetUser = $targetUser;
164 } else {
165 // Use target option
166 // It's possible for the target to be empty. This is used by
167 // ContribsPagerTest and does not cause newFromName() to return
168 // false. It's probably not used by any production code.
169 $this->target = $options['target'] ?? '';
170 $this->targetUser = $services->getUserFactory()->newFromName(
171 $this->target, UserFactory::RIGOR_NONE
172 );
173 if ( !$this->targetUser ) {
174 // This can happen if the target contained "#". Callers
175 // typically pass user input through title normalization to
176 // avoid it.
177 throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
178 'broken to use even with validation disabled.' );
179 }
180 }
181
182 $this->namespace = $options['namespace'] ?? '';
183 $this->tagFilter = $options['tagfilter'] ?? false;
184 $this->nsInvert = $options['nsInvert'] ?? false;
185 $this->associated = $options['associated'] ?? false;
186
187 $this->deletedOnly = !empty( $options['deletedOnly'] );
188 $this->topOnly = !empty( $options['topOnly'] );
189 $this->newOnly = !empty( $options['newOnly'] );
190 $this->hideMinor = !empty( $options['hideMinor'] );
191 $this->revisionsOnly = !empty( $options['revisionsOnly'] );
192
193 // Most of this code will use the 'contributions' group DB, which can map to replica DBs
194 // with extra user based indexes or partioning by user.
195 // Set database before parent constructor to avoid setting it there with wfGetDB
196 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'contributions' );
197 // Needed by call to getIndexField -> getTargetTable from parent constructor
198 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
199 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
200
201 $msgs = [
202 'diff',
203 'hist',
204 'pipe-separator',
205 'uctop'
206 ];
207
208 foreach ( $msgs as $msg ) {
209 $this->messages[$msg] = $this->msg( $msg )->escaped();
210 }
211
212 // Date filtering: use timestamp if available
213 $startTimestamp = '';
214 $endTimestamp = '';
215 if ( isset( $options['start'] ) && $options['start'] ) {
216 $startTimestamp = $options['start'] . ' 00:00:00';
217 }
218 if ( isset( $options['end'] ) && $options['end'] ) {
219 $endTimestamp = $options['end'] . ' 23:59:59';
220 }
221 $this->getDateRangeCond( $startTimestamp, $endTimestamp );
222
223 $this->templateParser = new TemplateParser();
224 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
225 $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
226 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
227 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
228 }
229
230 public function getDefaultQuery() {
231 $query = parent::getDefaultQuery();
232 $query['target'] = $this->target;
233
234 return $query;
235 }
236
244 public function getNavigationBar() {
245 return Html::rawElement( 'p', [ 'class' => 'mw-pager-navigation-bar' ],
246 parent::getNavigationBar()
247 );
248 }
249
259 public function reallyDoQuery( $offset, $limit, $order ) {
260 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
261 $offset,
262 $limit,
263 $order
264 );
265
266 $options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' );
267 /*
268 * This hook will allow extensions to add in additional queries, so they can get their data
269 * in My Contributions as well. Extensions should append their results to the $data array.
270 *
271 * Extension queries have to implement the navbar requirement as well. They should
272 * - have a column aliased as $pager->getIndexField()
273 * - have LIMIT set
274 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
275 * - have the ORDER BY specified based upon the details provided by the navbar
276 *
277 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
278 *
279 * &$data: an array of results of all contribs queries
280 * $pager: the ContribsPager object hooked into
281 * $offset: see phpdoc above
282 * $limit: see phpdoc above
283 * $descending: see phpdoc above
284 */
285 $dbr = $this->getDatabase();
286 $data = [ $dbr->select(
287 $tables, $fields, $conds, $fname, $options, $join_conds
288 ) ];
289 if ( !$this->revisionsOnly ) {
290 $this->hookRunner->onContribsPager__reallyDoQuery(
291 $data, $this, $offset, $limit, $order );
292 }
293
294 $result = [];
295
296 // loop all results and collect them in an array
297 foreach ( $data as $query ) {
298 foreach ( $query as $i => $row ) {
299 // If the query results are in descending order, the indexes must also be in descending order
300 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
301 // Left-pad with zeroes, because these values will be sorted as strings
302 $index = str_pad( $index, strlen( $limit ), '0', STR_PAD_LEFT );
303 // use index column as key, allowing us to easily sort in PHP
304 $result[$row->{$this->getIndexField()} . "-$index"] = $row;
305 }
306 }
307
308 // sort results
309 if ( $order === self::QUERY_ASCENDING ) {
310 ksort( $result );
311 } else {
312 krsort( $result );
313 }
314
315 // enforce limit
316 $result = array_slice( $result, 0, $limit );
317
318 // get rid of array keys
319 $result = array_values( $result );
320
321 return new FakeResultWrapper( $result );
322 }
323
333 private function getTargetTable() {
334 $dbr = $this->getDatabase();
335 $ipRangeConds = $this->targetUser->isRegistered()
336 ? null : $this->getIpRangeConds( $dbr, $this->target );
337 if ( $ipRangeConds ) {
338 return 'ip_changes';
339 } else {
340 $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
341 if ( isset( $conds['orconds']['actor'] ) ) {
342 return 'revision_actor_temp';
343 }
344 }
345
346 return 'revision';
347 }
348
349 public function getQueryInfo() {
350 $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
351 $queryInfo = [
352 'tables' => $revQuery['tables'],
353 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
354 'conds' => [],
355 'options' => [],
356 'join_conds' => $revQuery['joins'],
357 ];
358
359 // WARNING: Keep this in sync with getTargetTable()!
360 $dbr = $this->getDatabase();
361 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
362 if ( $ipRangeConds ) {
363 // Put ip_changes first (T284419)
364 array_unshift( $queryInfo['tables'], 'ip_changes' );
365 $queryInfo['join_conds']['revision'] = [
366 'JOIN', [ 'rev_id = ipc_rev_id' ]
367 ];
368 $queryInfo['conds'][] = $ipRangeConds;
369 } else {
370 // tables and joins are already handled by RevisionStore::getQueryInfo()
371 $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
372 $queryInfo['conds'][] = $conds['conds'];
373 // Force the appropriate index to avoid bad query plans (T189026)
374 if ( isset( $conds['orconds']['actor'] ) ) {
375 $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
376 }
377 }
378
379 if ( $this->deletedOnly ) {
380 $queryInfo['conds'][] = 'rev_deleted != 0';
381 }
382
383 if ( $this->topOnly ) {
384 $queryInfo['conds'][] = 'rev_id = page_latest';
385 }
386
387 if ( $this->newOnly ) {
388 $queryInfo['conds'][] = 'rev_parent_id = 0';
389 }
390
391 if ( $this->hideMinor ) {
392 $queryInfo['conds'][] = 'rev_minor_edit = 0';
393 }
394
395 $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
396
397 // Paranoia: avoid brute force searches (T19342)
398 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
399 $queryInfo['conds'][] = $dbr->bitAnd(
400 'rev_deleted', RevisionRecord::DELETED_USER
401 ) . ' = 0';
402 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
403 $queryInfo['conds'][] = $dbr->bitAnd(
404 'rev_deleted', RevisionRecord::SUPPRESSED_USER
405 ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
406 }
407
408 // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
409 $indexField = $this->getIndexField();
410 if ( $indexField !== 'rev_timestamp' ) {
411 $queryInfo['fields'][] = $indexField;
412 }
413
415 $queryInfo['tables'],
416 $queryInfo['fields'],
417 $queryInfo['conds'],
418 $queryInfo['join_conds'],
419 $queryInfo['options'],
420 $this->tagFilter
421 );
422
423 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
424
425 return $queryInfo;
426 }
427
428 protected function getNamespaceCond() {
429 if ( $this->namespace !== '' ) {
430 $dbr = $this->getDatabase();
431 $selectedNS = $dbr->addQuotes( $this->namespace );
432 $eq_op = $this->nsInvert ? '!=' : '=';
433 $bool_op = $this->nsInvert ? 'AND' : 'OR';
434
435 if ( !$this->associated ) {
436 return [ "page_namespace $eq_op $selectedNS" ];
437 }
438
439 $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
440
441 return [
442 "page_namespace $eq_op $selectedNS " .
443 $bool_op .
444 " page_namespace $eq_op $associatedNS"
445 ];
446 }
447
448 return [];
449 }
450
457 private function getIpRangeConds( $db, $ip ) {
458 // First make sure it is a valid range and they are not outside the CIDR limit
459 if ( !$this->isQueryableRange( $ip ) ) {
460 return false;
461 }
462
463 list( $start, $end ) = IPUtils::parseRange( $ip );
464
465 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
466 }
467
475 public function isQueryableRange( $ipRange ) {
476 $limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
477
478 $bits = IPUtils::parseCIDR( $ipRange )[1];
479 if (
480 ( $bits === false ) ||
481 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
482 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
483 ) {
484 return false;
485 }
486
487 return true;
488 }
489
493 public function getIndexField() {
494 // The returned column is used for sorting and continuation, so we need to
495 // make sure to use the right denormalized column depending on which table is
496 // being targeted by the query to avoid bad query plans.
497 // See T200259, T204669, T220991, and T221380.
498 $target = $this->getTargetTable();
499 switch ( $target ) {
500 case 'revision':
501 return 'rev_timestamp';
502 case 'ip_changes':
503 return 'ipc_rev_timestamp';
504 case 'revision_actor_temp':
505 return 'revactor_timestamp';
506 default:
507 wfWarn(
508 __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
509 );
510 return 'rev_timestamp';
511 }
512 }
513
517 public function getTagFilter() {
518 return $this->tagFilter;
519 }
520
524 public function getTarget() {
525 return $this->target;
526 }
527
531 public function isNewOnly() {
532 return $this->newOnly;
533 }
534
538 public function getNamespace() {
539 return $this->namespace;
540 }
541
545 protected function getExtraSortFields() {
546 // The returned columns are used for sorting, so we need to make sure
547 // to use the right denormalized column depending on which table is
548 // being targeted by the query to avoid bad query plans.
549 // See T200259, T204669, T220991, and T221380.
550 $target = $this->getTargetTable();
551 switch ( $target ) {
552 case 'revision':
553 return [ 'rev_id' ];
554 case 'ip_changes':
555 return [ 'ipc_rev_id' ];
556 case 'revision_actor_temp':
557 return [ 'revactor_rev' ];
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 $batch = $this->linkBatchFactory->newLinkBatch();
572 $isIpRange = $this->isQueryableRange( $this->target );
573 # Give some pointers to make (last) links
574 foreach ( $this->mResult as $row ) {
575 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
576 $parentRevIds[] = $row->rev_parent_id;
577 }
578 if ( isset( $row->rev_id ) ) {
579 $this->mParentLens[$row->rev_id] = $row->rev_len;
580 if ( $isIpRange ) {
581 // If this is an IP range, batch the IP's talk page
582 $batch->add( NS_USER_TALK, $row->rev_user_text );
583 }
584 $batch->add( $row->page_namespace, $row->page_title );
585 }
586 }
587 # Fetch rev_len for revisions not already scanned above
588 $this->mParentLens += $this->revisionStore->getRevisionSizes(
589 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
590 );
591 $batch->execute();
592 $this->mResult->seek( 0 );
593 }
594
598 protected function getStartBody() {
599 return "<ul class=\"mw-contributions-list\">\n";
600 }
601
605 protected function getEndBody() {
606 return "</ul>\n";
607 }
608
623 public function tryCreatingRevisionRecord( $row, $title = null ) {
624 if ( !$this->revisionStore->isRevisionRow( $row ) ) {
625 return null;
626 }
627 return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
628 }
629
642 public function formatRow( $row ) {
643 $ret = '';
644 $classes = [];
645 $attribs = [];
646
647 $linkRenderer = $this->getLinkRenderer();
648
649 $page = null;
650 // Create a title for the revision if possible
651 // Rows from the hook may not include title information
652 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
653 $page = Title::newFromRow( $row );
654 }
655 // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
656 // function to be called with a special object for $row. It expects us
657 // skip formatting so that the row can be formatted by the
658 // ContributionsLineEnding hook below.
659 // FIXME: have some better way for extensions to provide formatted rows.
660 if ( $this->revisionStore->isRevisionRow( $row ) ) {
661 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
662 $attribs['data-mw-revid'] = $revRecord->getId();
663
664 $link = $linkRenderer->makeLink(
665 $page,
666 $page->getPrefixedText(),
667 [ 'class' => 'mw-contributions-title' ],
668 $page->isRedirect() ? [ 'redirect' => 'no' ] : []
669 );
670 # Mark current revisions
671 $topmarktext = '';
672 $user = $this->getUser();
673
674 if ( $row->rev_id === $row->page_latest ) {
675 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
676 $classes[] = 'mw-contributions-current';
677 # Add rollback link
678 if ( !$row->page_is_new &&
679 $this->getAuthority()->probablyCan( 'rollback', $page ) &&
680 $this->getAuthority()->probablyCan( 'edit', $page )
681 ) {
682 $this->preventClickjacking();
683 $topmarktext .= ' ' . Linker::generateRollback(
684 $revRecord,
685 $this->getContext(),
686 [ 'noBrackets' ]
687 );
688 }
689 }
690 # Is there a visible previous revision?
691 if ( $revRecord->getParentId() !== 0 &&
692 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
693 ) {
694 $difftext = $linkRenderer->makeKnownLink(
695 $page,
696 new HtmlArmor( $this->messages['diff'] ),
697 [ 'class' => 'mw-changeslist-diff' ],
698 [
699 'diff' => 'prev',
700 'oldid' => $row->rev_id
701 ]
702 );
703 } else {
704 $difftext = $this->messages['diff'];
705 }
706 $histlink = $linkRenderer->makeKnownLink(
707 $page,
708 new HtmlArmor( $this->messages['hist'] ),
709 [ 'class' => 'mw-changeslist-history' ],
710 [ 'action' => 'history' ]
711 );
712
713 if ( $row->rev_parent_id === null ) {
714 // For some reason rev_parent_id isn't populated for this row.
715 // Its rumoured this is true on wikipedia for some revisions (T36922).
716 // Next best thing is to have the total number of bytes.
717 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
718 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
719 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
720 } else {
721 $parentLen = 0;
722 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
723 $parentLen = $this->mParentLens[$row->rev_parent_id];
724 }
725
726 $chardiff = ' <span class="mw-changeslist-separator"></span> ';
727 $chardiff .= ChangesList::showCharacterDifference(
728 $parentLen,
729 $row->rev_len,
730 $this->getContext()
731 );
732 $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
733 }
734
735 $lang = $this->getLanguage();
736 $comment = $lang->getDirMark() . Linker::revComment( $revRecord, false, true, false );
737 $d = ChangesList::revDateLink( $revRecord, $user, $lang, $page );
738
739 # When querying for an IP range, we want to always show user and user talk links.
740 $userlink = '';
741 $revUser = $revRecord->getUser();
742 $revUserId = $revUser ? $revUser->getId() : 0;
743 $revUserText = $revUser ? $revUser->getName() : '';
744 if ( $this->isQueryableRange( $this->target ) ) {
745 $userlink = ' <span class="mw-changeslist-separator"></span> '
746 . $lang->getDirMark()
747 . Linker::userLink( $revUserId, $revUserText );
748 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
749 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
750 }
751
752 $flags = [];
753 if ( $revRecord->getParentId() === 0 ) {
754 $flags[] = ChangesList::flag( 'newpage' );
755 }
756
757 if ( $revRecord->isMinor() ) {
758 $flags[] = ChangesList::flag( 'minor' );
759 }
760
761 $del = Linker::getRevDeleteLink( $user, $revRecord, $page );
762 if ( $del !== '' ) {
763 $del .= ' ';
764 }
765
766 // While it might be tempting to use a list here
767 // this would result in clutter and slows down navigating the content
768 // in assistive technology.
769 // See https://phabricator.wikimedia.org/T205581#4734812
770 $diffHistLinks = Html::rawElement( 'span',
771 [ 'class' => 'mw-changeslist-links' ],
772 // The spans are needed to ensure the dividing '|' elements are not
773 // themselves styled as links.
774 Html::rawElement( 'span', [], $difftext ) .
775 ' ' . // Space needed for separating two words.
776 Html::rawElement( 'span', [], $histlink )
777 );
778
779 # Tags, if any.
780 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
781 $row->ts_tags,
782 'contributions',
783 $this->getContext()
784 );
785 $classes = array_merge( $classes, $newClasses );
786
787 $this->hookRunner->onSpecialContributions__formatRow__flags(
788 $this->getContext(), $row, $flags );
789
790 $templateParams = [
791 'del' => $del,
792 'timestamp' => $d,
793 'diffHistLinks' => $diffHistLinks,
794 'charDifference' => $chardiff,
795 'flags' => $flags,
796 'articleLink' => $link,
797 'userlink' => $userlink,
798 'logText' => $comment,
799 'topmarktext' => $topmarktext,
800 'tagSummary' => $tagSummary,
801 ];
802
803 # Denote if username is redacted for this edit
804 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
805 $templateParams['rev-deleted-user-contribs'] =
806 $this->msg( 'rev-deleted-user-contribs' )->escaped();
807 }
808
809 $ret = $this->templateParser->processTemplate(
810 'SpecialContributionsLine',
811 $templateParams
812 );
813 }
814
815 // Let extensions add data
816 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
817 $attribs = array_filter( $attribs,
818 [ Sanitizer::class, 'isReservedDataAttribute' ],
819 ARRAY_FILTER_USE_KEY
820 );
821
822 // TODO: Handle exceptions in the catch block above. Do any extensions rely on
823 // receiving empty rows?
824
825 if ( $classes === [] && $attribs === [] && $ret === '' ) {
826 wfDebug( "Dropping Special:Contribution row that could not be formatted" );
827 return "<!-- Could not format Special:Contribution row. -->\n";
828 }
829 $attribs['class'] = $classes;
830
831 // FIXME: The signature of the ContributionsLineEnding hook makes it
832 // very awkward to move this LI wrapper into the template.
833 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
834 }
835
840 protected function getSqlComment() {
841 if ( $this->namespace || $this->deletedOnly ) {
842 // potentially slow, see CR r58153
843 return 'contributions page filtered for namespace or RevisionDeleted edits';
844 } else {
845 return 'contributions page unfiltered';
846 }
847 }
848
849 protected function preventClickjacking() {
850 $this->preventClickjacking = true;
851 }
852
856 public function getPreventClickjacking() {
857 return $this->preventClickjacking;
858 }
859
866 public static function processDateFilter( array $opts ) {
867 $start = $opts['start'] ?? '';
868 $end = $opts['end'] ?? '';
869 $year = $opts['year'] ?? '';
870 $month = $opts['month'] ?? '';
871
872 if ( $start !== '' && $end !== '' && $start > $end ) {
873 $temp = $start;
874 $start = $end;
875 $end = $temp;
876 }
877
878 // If year/month legacy filtering options are set, convert them to display the new stamp
879 if ( $year !== '' || $month !== '' ) {
880 // Reuse getDateCond logic, but subtract a day because
881 // the endpoints of our date range appear inclusive
882 // but the internal end offsets are always exclusive
883 $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
884 $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
885 $legacyDateTime = $legacyDateTime->modify( '-1 day' );
886
887 // Clear the new timestamp range options if used and
888 // replace with the converted legacy timestamp
889 $start = '';
890 $end = $legacyDateTime->format( 'Y-m-d' );
891 }
892
893 $opts['start'] = $start;
894 $opts['end'] = $end;
895
896 return $opts;
897 }
898}
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.
getContext()
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Pager for Special:Contributions.
UserIdentity $targetUser
RevisionStore $revisionStore
TemplateParser $templateParser
string $target
User name, or a string describing an IP address range.
ActorMigration $actorMigration
bool $revisionsOnly
Set to true to only include mediawiki revisions.
string int $namespace
A single namespace number, or an empty string for all namespaces.
static processDateFilter(array $opts)
Set up date filter options, given request data.
getTargetTable()
Return the table targeted for ordering and continuation.
string false $tagFilter
Name of tag to filter, or false to ignore tags.
string[] $messages
Local cache for escaped messages.
tryCreatingRevisionRecord( $row, $title=null)
Check whether the revision associated is valid for formatting.
LinkBatchFactory $linkBatchFactory
bool $associated
Set to true to show both the subject and talk namespace, no matter which got selected.
bool $nsInvert
Set to true to invert the namespace selection.
NamespaceInfo $namespaceInfo
__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)
getQueryInfo()
Provides all parameters needed for the main paged query.
getNavigationBar()
Wrap the navigation bar in a p element with identifying class.
HookRunner $hookRunner
bool $topOnly
Set to true to show only latest (a.k.a.
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,...
bool $deletedOnly
Set to true to show only deleted revisions.
bool $newOnly
Set to true to show only new pages.
getIpRangeConds( $db, $ip)
Get SQL conditions for an IP range, if applicable.
bool $hideMinor
Set to true to hide edits marked as minor by the user.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
getDatabase()
Get the Database object in use.
LinkRenderer $linkRenderer
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition Linker.php:1064
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1782
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:2360
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:2031
static formatRevisionSize( $size)
Definition Linker.php:1820
static userTalkLink( $userId, $userText)
Definition Linker.php:1202
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Service for looking up page revisions.
Creates User objects.
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.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
if(!isset( $args[0])) $lang