MediaWiki  master
ContribsPager.php
Go to the documentation of this file.
1 <?php
32 use Wikimedia\IPUtils;
37 
43 
47  private $messages;
48 
52  private $target;
53 
57  private $namespace;
58 
62  private $tagFilter;
63 
67  private $nsInvert;
68 
73  private $associated;
74 
78  private $deletedOnly;
79 
83  private $topOnly;
84 
88  private $newOnly;
89 
93  private $hideMinor;
94 
99  private $revisionsOnly;
100 
101  private $preventClickjacking = false;
102 
106  private $mParentLens;
107 
109  private $targetUser;
110 
115 
118 
120  private $hookRunner;
121 
124 
126  private $revisionStore;
127 
129  private $namespaceInfo;
130 
133 
135  private $formattedComments = [];
136 
138  private $revisions = [];
139 
153  public function __construct(
155  array $options,
158  HookContainer $hookContainer = null,
159  ILoadBalancer $loadBalancer = null,
163  UserIdentity $targetUser = null,
165  ) {
166  // Class is used directly in extensions - T266484
167  $services = MediaWikiServices::getInstance();
168  $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
169 
170  // Set ->target before calling parent::__construct() so
171  // parent can call $this->getIndexField() and get the right result. Set
172  // the rest too just to keep things simple.
173  if ( $targetUser ) {
174  $this->target = $options['target'] ?? $targetUser->getName();
175  $this->targetUser = $targetUser;
176  } else {
177  // Use target option
178  // It's possible for the target to be empty. This is used by
179  // ContribsPagerTest and does not cause newFromName() to return
180  // false. It's probably not used by any production code.
181  $this->target = $options['target'] ?? '';
182  $this->targetUser = $services->getUserFactory()->newFromName(
183  $this->target, UserFactory::RIGOR_NONE
184  );
185  if ( !$this->targetUser ) {
186  // This can happen if the target contained "#". Callers
187  // typically pass user input through title normalization to
188  // avoid it.
189  throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
190  'broken to use even with validation disabled.' );
191  }
192  }
193 
194  $this->namespace = $options['namespace'] ?? '';
195  $this->tagFilter = $options['tagfilter'] ?? false;
196  $this->nsInvert = $options['nsInvert'] ?? false;
197  $this->associated = $options['associated'] ?? false;
198 
199  $this->deletedOnly = !empty( $options['deletedOnly'] );
200  $this->topOnly = !empty( $options['topOnly'] );
201  $this->newOnly = !empty( $options['newOnly'] );
202  $this->hideMinor = !empty( $options['hideMinor'] );
203  $this->revisionsOnly = !empty( $options['revisionsOnly'] );
204 
205  // Most of this code will use the 'contributions' group DB, which can map to replica DBs
206  // with extra user based indexes or partioning by user.
207  // Set database before parent constructor to avoid setting it there with wfGetDB
208  $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'contributions' );
209  // Needed by call to getIndexField -> getTargetTable from parent constructor
210  $this->actorMigration = $actorMigration ?? $services->getActorMigration();
211  parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
212 
213  $msgs = [
214  'diff',
215  'hist',
216  'pipe-separator',
217  'uctop'
218  ];
219 
220  foreach ( $msgs as $msg ) {
221  $this->messages[$msg] = $this->msg( $msg )->escaped();
222  }
223 
224  // Date filtering: use timestamp if available
225  $startTimestamp = '';
226  $endTimestamp = '';
227  if ( isset( $options['start'] ) && $options['start'] ) {
228  $startTimestamp = $options['start'] . ' 00:00:00';
229  }
230  if ( isset( $options['end'] ) && $options['end'] ) {
231  $endTimestamp = $options['end'] . ' 23:59:59';
232  }
233  $this->getDateRangeCond( $startTimestamp, $endTimestamp );
234 
235  $this->templateParser = new TemplateParser();
236  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
237  $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
238  $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
239  $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
240  $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
241  }
242 
243  public function getDefaultQuery() {
244  $query = parent::getDefaultQuery();
245  $query['target'] = $this->target;
246 
247  return $query;
248  }
249 
257  public function getNavigationBar() {
258  return Html::rawElement( 'p', [ 'class' => 'mw-pager-navigation-bar' ],
259  parent::getNavigationBar()
260  );
261  }
262 
272  public function reallyDoQuery( $offset, $limit, $order ) {
273  list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
274  $offset,
275  $limit,
276  $order
277  );
278 
279  /*
280  * This hook will allow extensions to add in additional queries, so they can get their data
281  * in My Contributions as well. Extensions should append their results to the $data array.
282  *
283  * Extension queries have to implement the navbar requirement as well. They should
284  * - have a column aliased as $pager->getIndexField()
285  * - have LIMIT set
286  * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
287  * - have the ORDER BY specified based upon the details provided by the navbar
288  *
289  * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
290  *
291  * &$data: an array of results of all contribs queries
292  * $pager: the ContribsPager object hooked into
293  * $offset: see phpdoc above
294  * $limit: see phpdoc above
295  * $descending: see phpdoc above
296  */
297  $dbr = $this->getDatabase();
298  $data = [ $dbr->select(
299  $tables, $fields, $conds, $fname, $options, $join_conds
300  ) ];
301  if ( !$this->revisionsOnly ) {
302  $this->hookRunner->onContribsPager__reallyDoQuery(
303  $data, $this, $offset, $limit, $order );
304  }
305 
306  $result = [];
307 
308  // loop all results and collect them in an array
309  foreach ( $data as $query ) {
310  foreach ( $query as $i => $row ) {
311  // If the query results are in descending order, the indexes must also be in descending order
312  $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
313  // Left-pad with zeroes, because these values will be sorted as strings
314  $index = str_pad( $index, strlen( $limit ), '0', STR_PAD_LEFT );
315  // use index column as key, allowing us to easily sort in PHP
316  $result[$row->{$this->getIndexField()} . "-$index"] = $row;
317  }
318  }
319 
320  // sort results
321  if ( $order === self::QUERY_ASCENDING ) {
322  ksort( $result );
323  } else {
324  krsort( $result );
325  }
326 
327  // enforce limit
328  $result = array_slice( $result, 0, $limit );
329 
330  // get rid of array keys
331  $result = array_values( $result );
332 
333  return new FakeResultWrapper( $result );
334  }
335 
345  private function getTargetTable() {
346  $dbr = $this->getDatabase();
347  $ipRangeConds = $this->targetUser->isRegistered()
348  ? null : $this->getIpRangeConds( $dbr, $this->target );
349  if ( $ipRangeConds ) {
350  return 'ip_changes';
351  } else {
352  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
353  if ( isset( $conds['orconds']['actor'] ) ) {
354  return 'revision_actor_temp';
355  }
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 (T189026)
386  if ( isset( $conds['orconds']['actor'] ) ) {
387  $queryInfo['options']['USE INDEX']['temp_rev_user'] = '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  );
434 
435  $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
436 
437  return $queryInfo;
438  }
439 
440  protected function getNamespaceCond() {
441  if ( $this->namespace !== '' ) {
442  $dbr = $this->getDatabase();
443  $selectedNS = $dbr->addQuotes( $this->namespace );
444  $eq_op = $this->nsInvert ? '!=' : '=';
445  $bool_op = $this->nsInvert ? 'AND' : 'OR';
446 
447  if ( !$this->associated ) {
448  return [ "page_namespace $eq_op $selectedNS" ];
449  }
450 
451  $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
452 
453  return [
454  "page_namespace $eq_op $selectedNS " .
455  $bool_op .
456  " page_namespace $eq_op $associatedNS"
457  ];
458  }
459 
460  return [];
461  }
462 
469  private function getIpRangeConds( $db, $ip ) {
470  // First make sure it is a valid range and they are not outside the CIDR limit
471  if ( !$this->isQueryableRange( $ip ) ) {
472  return false;
473  }
474 
475  list( $start, $end ) = IPUtils::parseRange( $ip );
476 
477  return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
478  }
479 
487  public function isQueryableRange( $ipRange ) {
488  $limits = $this->getConfig()->get( '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  case 'revision_actor_temp':
517  return 'revactor_timestamp';
518  default:
519  wfWarn(
520  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
521  );
522  return 'rev_timestamp';
523  }
524  }
525 
529  public function getTagFilter() {
530  return $this->tagFilter;
531  }
532 
536  public function getTarget() {
537  return $this->target;
538  }
539 
543  public function isNewOnly() {
544  return $this->newOnly;
545  }
546 
550  public function getNamespace() {
551  return $this->namespace;
552  }
553 
557  protected function getExtraSortFields() {
558  // The returned columns are used for sorting, so we need to make sure
559  // to use the right denormalized column depending on which table is
560  // being targeted by the query to avoid bad query plans.
561  // See T200259, T204669, T220991, and T221380.
562  $target = $this->getTargetTable();
563  switch ( $target ) {
564  case 'revision':
565  return [ 'rev_id' ];
566  case 'ip_changes':
567  return [ 'ipc_rev_id' ];
568  case 'revision_actor_temp':
569  return [ 'revactor_rev' ];
570  default:
571  wfWarn(
572  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
573  );
574  return [ 'rev_id' ];
575  }
576  }
577 
578  protected function doBatchLookups() {
579  # Do a link batch query
580  $this->mResult->seek( 0 );
581  $parentRevIds = [];
582  $this->mParentLens = [];
583  $revisions = [];
584  $linkBatch = $this->linkBatchFactory->newLinkBatch();
585  $isIpRange = $this->isQueryableRange( $this->target );
586  # Give some pointers to make (last) links
587  foreach ( $this->mResult as $row ) {
588  if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
589  $parentRevIds[] = (int)$row->rev_parent_id;
590  }
591  if ( $this->revisionStore->isRevisionRow( $row ) ) {
592  $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
593  if ( $isIpRange ) {
594  // If this is an IP range, batch the IP's talk page
595  $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
596  }
597  $linkBatch->add( $row->page_namespace, $row->page_title );
598  $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
599  }
600  }
601  # Fetch rev_len for revisions not already scanned above
602  $this->mParentLens += $this->revisionStore->getRevisionSizes(
603  array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
604  );
605  $linkBatch->execute();
606 
607  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
608  ->authority( $this->getAuthority() )
609  ->revisions( $revisions )
610  ->hideIfDeleted()
611  ->execute();
612 
613  # For performance, save the revision objects for later.
614  # The array is indexed by rev_id. doBatchLookups() may be called
615  # multiple times with different results, so merge the revisions array,
616  # ignoring any duplicates.
617  $this->revisions += $revisions;
618  }
619 
623  protected function getStartBody() {
624  return "<ul class=\"mw-contributions-list\">\n";
625  }
626 
630  protected function getEndBody() {
631  return "</ul>\n";
632  }
633 
644  public function tryCreatingRevisionRecord( $row, $title = null ) {
645  if ( $row instanceof stdClass && isset( $row->rev_id )
646  && isset( $this->revisions[$row->rev_id] )
647  ) {
648  return $this->revisions[$row->rev_id];
649  } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
650  return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
651  } else {
652  return null;
653  }
654  }
655 
668  public function formatRow( $row ) {
669  $ret = '';
670  $classes = [];
671  $attribs = [];
672 
673  $linkRenderer = $this->getLinkRenderer();
674 
675  $page = null;
676  // Create a title for the revision if possible
677  // Rows from the hook may not include title information
678  if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
679  $page = Title::newFromRow( $row );
680  }
681  // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
682  // function to be called with a special object for $row. It expects us
683  // skip formatting so that the row can be formatted by the
684  // ContributionsLineEnding hook below.
685  // FIXME: have some better way for extensions to provide formatted rows.
686  $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
687  if ( $revRecord ) {
688  $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
689  $attribs['data-mw-revid'] = $revRecord->getId();
690 
691  $link = $linkRenderer->makeLink(
692  $page,
693  $page->getPrefixedText(),
694  [ 'class' => 'mw-contributions-title' ],
695  $page->isRedirect() ? [ 'redirect' => 'no' ] : []
696  );
697  # Mark current revisions
698  $topmarktext = '';
699  $user = $this->getUser();
700 
701  if ( $row->rev_id === $row->page_latest ) {
702  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
703  $classes[] = 'mw-contributions-current';
704  # Add rollback link
705  if ( !$row->page_is_new &&
706  $this->getAuthority()->probablyCan( 'rollback', $page ) &&
707  $this->getAuthority()->probablyCan( 'edit', $page )
708  ) {
709  $this->setPreventClickjacking( true );
710  $topmarktext .= ' ' . Linker::generateRollback(
711  $revRecord,
712  $this->getContext(),
713  [ 'noBrackets' ]
714  );
715  }
716  }
717  # Is there a visible previous revision?
718  if ( $revRecord->getParentId() !== 0 &&
719  $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
720  ) {
721  $difftext = $linkRenderer->makeKnownLink(
722  $page,
723  new HtmlArmor( $this->messages['diff'] ),
724  [ 'class' => 'mw-changeslist-diff' ],
725  [
726  'diff' => 'prev',
727  'oldid' => $row->rev_id
728  ]
729  );
730  } else {
731  $difftext = $this->messages['diff'];
732  }
733  $histlink = $linkRenderer->makeKnownLink(
734  $page,
735  new HtmlArmor( $this->messages['hist'] ),
736  [ 'class' => 'mw-changeslist-history' ],
737  [ 'action' => 'history' ]
738  );
739 
740  if ( $row->rev_parent_id === null ) {
741  // For some reason rev_parent_id isn't populated for this row.
742  // Its rumoured this is true on wikipedia for some revisions (T36922).
743  // Next best thing is to have the total number of bytes.
744  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
745  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
746  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
747  } else {
748  $parentLen = 0;
749  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
750  $parentLen = $this->mParentLens[$row->rev_parent_id];
751  }
752 
753  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
755  $parentLen,
756  $row->rev_len,
757  $this->getContext()
758  );
759  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
760  }
761 
762  $lang = $this->getLanguage();
763 
764  $comment = $lang->getDirMark() . (
765  $this->formattedComments[$row->rev_id] ??
766  $this->commentFormatter->formatRevision(
767  $revRecord, $user, false, true, false
768  ) );
769  $d = ChangesList::revDateLink( $revRecord, $user, $lang, $page );
770 
771  # When querying for an IP range, we want to always show user and user talk links.
772  $userlink = '';
773  $revUser = $revRecord->getUser();
774  $revUserId = $revUser ? $revUser->getId() : 0;
775  $revUserText = $revUser ? $revUser->getName() : '';
776  if ( $this->isQueryableRange( $this->target ) ) {
777  $userlink = ' <span class="mw-changeslist-separator"></span> '
778  . $lang->getDirMark()
779  . Linker::userLink( $revUserId, $revUserText );
780  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
781  Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
782  }
783 
784  $flags = [];
785  if ( $revRecord->getParentId() === 0 ) {
786  $flags[] = ChangesList::flag( 'newpage' );
787  }
788 
789  if ( $revRecord->isMinor() ) {
790  $flags[] = ChangesList::flag( 'minor' );
791  }
792 
793  $del = Linker::getRevDeleteLink( $user, $revRecord, $page );
794  if ( $del !== '' ) {
795  $del .= ' ';
796  }
797 
798  // While it might be tempting to use a list here
799  // this would result in clutter and slows down navigating the content
800  // in assistive technology.
801  // See https://phabricator.wikimedia.org/T205581#4734812
802  $diffHistLinks = Html::rawElement( 'span',
803  [ 'class' => 'mw-changeslist-links' ],
804  // The spans are needed to ensure the dividing '|' elements are not
805  // themselves styled as links.
806  Html::rawElement( 'span', [], $difftext ) .
807  ' ' . // Space needed for separating two words.
808  Html::rawElement( 'span', [], $histlink )
809  );
810 
811  # Tags, if any.
812  list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
813  $row->ts_tags,
814  'contributions',
815  $this->getContext()
816  );
817  $classes = array_merge( $classes, $newClasses );
818 
819  $this->hookRunner->onSpecialContributions__formatRow__flags(
820  $this->getContext(), $row, $flags );
821 
822  $templateParams = [
823  'del' => $del,
824  'timestamp' => $d,
825  'diffHistLinks' => $diffHistLinks,
826  'charDifference' => $chardiff,
827  'flags' => $flags,
828  'articleLink' => $link,
829  'userlink' => $userlink,
830  'logText' => $comment,
831  'topmarktext' => $topmarktext,
832  'tagSummary' => $tagSummary,
833  ];
834 
835  # Denote if username is redacted for this edit
836  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
837  $templateParams['rev-deleted-user-contribs'] =
838  $this->msg( 'rev-deleted-user-contribs' )->escaped();
839  }
840 
841  $ret = $this->templateParser->processTemplate(
842  'SpecialContributionsLine',
843  $templateParams
844  );
845  }
846 
847  // Let extensions add data
848  $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
849  $attribs = array_filter( $attribs,
850  [ Sanitizer::class, 'isReservedDataAttribute' ],
851  ARRAY_FILTER_USE_KEY
852  );
853 
854  // TODO: Handle exceptions in the catch block above. Do any extensions rely on
855  // receiving empty rows?
856 
857  if ( $classes === [] && $attribs === [] && $ret === '' ) {
858  wfDebug( "Dropping Special:Contribution row that could not be formatted" );
859  return "<!-- Could not format Special:Contribution row. -->\n";
860  }
861  $attribs['class'] = $classes;
862 
863  // FIXME: The signature of the ContributionsLineEnding hook makes it
864  // very awkward to move this LI wrapper into the template.
865  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
866  }
867 
872  protected function getSqlComment() {
873  if ( $this->namespace || $this->deletedOnly ) {
874  // potentially slow, see CR r58153
875  return 'contributions page filtered for namespace or RevisionDeleted edits';
876  } else {
877  return 'contributions page unfiltered';
878  }
879  }
880 
884  protected function preventClickjacking() {
885  $this->setPreventClickjacking( true );
886  }
887 
892  protected function setPreventClickjacking( bool $enable ) {
893  $this->preventClickjacking = $enable;
894  }
895 
899  public function getPreventClickjacking() {
900  return $this->preventClickjacking;
901  }
902 
909  public static function processDateFilter( array $opts ) {
910  $start = $opts['start'] ?? '';
911  $end = $opts['end'] ?? '';
912  $year = $opts['year'] ?? '';
913  $month = $opts['month'] ?? '';
914 
915  if ( $start !== '' && $end !== '' && $start > $end ) {
916  $temp = $start;
917  $start = $end;
918  $end = $temp;
919  }
920 
921  // If year/month legacy filtering options are set, convert them to display the new stamp
922  if ( $year !== '' || $month !== '' ) {
923  // Reuse getDateCond logic, but subtract a day because
924  // the endpoints of our date range appear inclusive
925  // but the internal end offsets are always exclusive
926  $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
927  $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
928  $legacyDateTime = $legacyDateTime->modify( '-1 day' );
929 
930  // Clear the new timestamp range options if used and
931  // replace with the converted legacy timestamp
932  $start = '';
933  $end = $legacyDateTime->format( 'Y-m-d' );
934  }
935 
936  $opts['start'] = $start;
937  $opts['end'] = $end;
938 
939  return $opts;
940  }
941 }
ReverseChronologicalPager\getOffsetDate
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...
Definition: ReverseChronologicalPager.php:129
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:39
ContribsPager\getNamespace
getNamespace()
Definition: ContribsPager.php:550
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
Linker\userTalkLink
static userTalkLink( $userId, $userText)
Definition: Linker.php:1202
Linker\userLink
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition: Linker.php:1064
ContribsPager\$topOnly
bool $topOnly
Set to true to show only latest (a.k.a.
Definition: ContribsPager.php:83
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:88
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
ContribsPager\$formattedComments
string[] $formattedComments
Definition: ContribsPager.php:135
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:43
ContribsPager\getDefaultQuery
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
Definition: ContribsPager.php:243
IndexPager\getDatabase
getDatabase()
Get the Database object in use.
Definition: IndexPager.php:248
getAuthority
getAuthority()
ContribsPager\__construct
__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)
Definition: ContribsPager.php:153
ContribsPager\getSqlComment
getSqlComment()
Overwrite Pager function and return a helpful comment.
Definition: ContribsPager.php:872
ContribsPager\formatRow
formatRow( $row)
Generates each row in the contributions list.
Definition: ContribsPager.php:668
ContribsPager\$hookRunner
HookRunner $hookRunner
Definition: ContribsPager.php:120
IndexPager\$linkRenderer
LinkRenderer $linkRenderer
Definition: IndexPager.php:167
ContribsPager\$targetUser
UserIdentity $targetUser
Definition: ContribsPager.php:109
ContribsPager\reallyDoQuery
reallyDoQuery( $offset, $limit, $order)
This method basically executes the exact same code as the parent class, though with a hook added,...
Definition: ContribsPager.php:272
ContribsPager\$commentFormatter
CommentFormatter $commentFormatter
Definition: ContribsPager.php:132
ContribsPager\tryCreatingRevisionRecord
tryCreatingRevisionRecord( $row, $title=null)
If the object looks like a revision row, or corresponds to a previously cached revision,...
Definition: ContribsPager.php:644
ActorMigration
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Definition: ActorMigration.php:15
MediaWiki\CommentFormatter\CommentFormatter
This is the main service interface for converting single-line comments from various DB comment fields...
Definition: CommentFormatter.php:16
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Definition: FakeResultWrapper.php:12
ContribsPager\$target
string $target
User name, or a string describing an IP address range.
Definition: ContribsPager.php:52
getContext
getContext()
ContribsPager\doBatchLookups
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
Definition: ContribsPager.php:578
$revQuery
$revQuery
Definition: testCompression.php:56
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
ContribsPager\processDateFilter
static processDateFilter(array $opts)
Set up date filter options, given request data.
Definition: ContribsPager.php:909
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
$dbr
$dbr
Definition: testCompression.php:54
ContribsPager\$namespaceInfo
NamespaceInfo $namespaceInfo
Definition: ContribsPager.php:129
ContribsPager\getPreventClickjacking
getPreventClickjacking()
Definition: ContribsPager.php:899
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:891
ContribsPager\$linkBatchFactory
LinkBatchFactory $linkBatchFactory
Definition: ContribsPager.php:117
ContribsPager\getNamespaceCond
getNamespaceCond()
Definition: ContribsPager.php:440
ContribsPager\setPreventClickjacking
setPreventClickjacking(bool $enable)
Definition: ContribsPager.php:892
ContribsPager\getTarget
getTarget()
Definition: ContribsPager.php:536
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:26
ContribsPager\getStartBody
getStartBody()
Definition: ContribsPager.php:623
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:579
RangeChronologicalPager\getDateRangeCond
getDateRangeCond( $startStamp, $endStamp)
Set and return a date range condition using timestamps provided by the user.
Definition: RangeChronologicalPager.php:46
MediaWiki\Cache\LinkBatchFactory
Definition: LinkBatchFactory.php:39
ContribsPager\$deletedOnly
bool $deletedOnly
Set to true to show only deleted revisions.
Definition: ContribsPager.php:78
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:273
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
ContribsPager\isNewOnly
isNewOnly()
Definition: ContribsPager.php:543
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ContribsPager\$actorMigration
ActorMigration $actorMigration
Definition: ContribsPager.php:123
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:886
RangeChronologicalPager
Pager for filtering by a range of dates.
Definition: RangeChronologicalPager.php:28
Linker\generateRollback
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1757
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
ContribsPager
Pager for Special:Contributions.
Definition: ContribsPager.php:42
ContribsPager\isQueryableRange
isQueryableRange( $ipRange)
Is the given IP a range and within the CIDR limit?
Definition: ContribsPager.php:487
ContribsPager\$revisions
RevisionRecord[] $revisions
Cached revisions by ID.
Definition: ContribsPager.php:138
ContribsPager\getNavigationBar
getNavigationBar()
Wrap the navigation bar in a p element with identifying class.
Definition: ContribsPager.php:257
ContribsPager\$preventClickjacking
$preventClickjacking
Definition: ContribsPager.php:101
ContribsPager\getExtraSortFields
getExtraSortFields()
Definition: ContribsPager.php:557
ContribsPager\$templateParser
TemplateParser $templateParser
Definition: ContribsPager.php:114
Linker\formatRevisionSize
static formatRevisionSize( $size)
Definition: Linker.php:1546
ChangesList\showCharacterDifference
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
Definition: ChangesList.php:347
ContribsPager\$tagFilter
string false $tagFilter
Name of tag to filter, or false to ignore tags.
Definition: ContribsPager.php:62
RangeChronologicalPager\buildQueryInfo
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
Definition: RangeChronologicalPager.php:107
ChangesList\revDateLink
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null)
Render the date and time of a revision in the current user language based on whether the user is able...
Definition: ChangesList.php:439
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
ContribsPager\$newOnly
bool $newOnly
Set to true to show only new pages.
Definition: ContribsPager.php:88
ContribsPager\$hideMinor
bool $hideMinor
Set to true to hide edits marked as minor by the user.
Definition: ContribsPager.php:93
ContribsPager\$associated
bool $associated
Set to true to show both the subject and talk namespace, no matter which got selected.
Definition: ContribsPager.php:73
ContribsPager\getTargetTable
getTargetTable()
Return the table targeted for ordering and continuation.
Definition: ContribsPager.php:345
ContribsPager\$revisionsOnly
bool $revisionsOnly
Set to true to only include mediawiki revisions.
Definition: ContribsPager.php:99
ContribsPager\$revisionStore
RevisionStore $revisionStore
Definition: ContribsPager.php:126
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
ContribsPager\getIpRangeConds
getIpRangeConds( $db, $ip)
Get SQL conditions for an IP range, if applicable.
Definition: ContribsPager.php:469
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1035
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:557
ContribsPager\getEndBody
getEndBody()
Definition: ContribsPager.php:630
ContribsPager\getQueryInfo
getQueryInfo()
Provides all parameters needed for the main paged query.
Definition: ContribsPager.php:361
ContribsPager\$messages
string[] $messages
Local cache for escaped messages.
Definition: ContribsPager.php:47
ContribsPager\$nsInvert
bool $nsInvert
Set to true to invert the namespace selection.
Definition: ContribsPager.php:67
ContribsPager\$namespace
string int $namespace
A single namespace number, or an empty string for all namespaces.
Definition: ContribsPager.php:57
Linker\getRevDeleteLink
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:2086
ContribsPager\preventClickjacking
preventClickjacking()
Definition: ContribsPager.php:884
TemplateParser
Definition: TemplateParser.php:27
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
ContribsPager\getIndexField
getIndexField()
Definition: ContribsPager.php:505
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:194
ContribsPager\$mParentLens
array $mParentLens
Definition: ContribsPager.php:106
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
ContribsPager\getTagFilter
getTagFilter()
Definition: ContribsPager.php:529