MediaWiki  master
ContribsPager.php
Go to the documentation of this file.
1 <?php
35 use Wikimedia\IPUtils;
40 
46 
47  public $mGroupByDate = true;
48 
52  private $messages;
53 
57  private $target;
58 
62  private $namespace;
63 
67  private $tagFilter;
68 
72  private $tagInvert;
73 
77  private $nsInvert;
78 
83  private $associated;
84 
88  private $deletedOnly;
89 
93  private $topOnly;
94 
98  private $newOnly;
99 
103  private $hideMinor;
104 
109  private $revisionsOnly;
110 
111  private $preventClickjacking = false;
112 
116  private $mParentLens;
117 
119  private $targetUser;
120 
124  private $templateParser;
125 
127  private $actorMigration;
128 
130  private $commentFormatter;
131 
133  private $hookRunner;
134 
136  private $linkBatchFactory;
137 
139  private $namespaceInfo;
140 
142  private $revisionStore;
143 
145  private $formattedComments = [];
146 
148  private $revisions = [];
149 
164  public function __construct(
165  IContextSource $context,
166  array $options,
167  LinkRenderer $linkRenderer = null,
168  LinkBatchFactory $linkBatchFactory = null,
169  HookContainer $hookContainer = null,
170  ILoadBalancer $loadBalancer = null,
171  ActorMigration $actorMigration = null,
172  RevisionStore $revisionStore = null,
173  NamespaceInfo $namespaceInfo = null,
174  UserIdentity $targetUser = null,
175  CommentFormatter $commentFormatter = null
176  ) {
177  // Class is used directly in extensions - T266484
178  $services = MediaWikiServices::getInstance();
179  $loadBalancer ??= $services->getDBLoadBalancer();
180 
181  // Set ->target before calling parent::__construct() so
182  // parent can call $this->getIndexField() and get the right result. Set
183  // the rest too just to keep things simple.
184  if ( $targetUser ) {
185  $this->target = $options['target'] ?? $targetUser->getName();
186  $this->targetUser = $targetUser;
187  } else {
188  // Use target option
189  // It's possible for the target to be empty. This is used by
190  // ContribsPagerTest and does not cause newFromName() to return
191  // false. It's probably not used by any production code.
192  $this->target = $options['target'] ?? '';
193  // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
194  $this->targetUser = $services->getUserFactory()->newFromName(
195  $this->target, UserRigorOptions::RIGOR_NONE
196  );
197  if ( !$this->targetUser ) {
198  // This can happen if the target contained "#". Callers
199  // typically pass user input through title normalization to
200  // avoid it.
201  throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
202  'broken to use even with validation disabled.' );
203  }
204  }
205 
206  $this->namespace = $options['namespace'] ?? '';
207  $this->tagFilter = $options['tagfilter'] ?? false;
208  $this->tagInvert = $options['tagInvert'] ?? false;
209  $this->nsInvert = $options['nsInvert'] ?? false;
210  $this->associated = $options['associated'] ?? false;
211 
212  $this->deletedOnly = !empty( $options['deletedOnly'] );
213  $this->topOnly = !empty( $options['topOnly'] );
214  $this->newOnly = !empty( $options['newOnly'] );
215  $this->hideMinor = !empty( $options['hideMinor'] );
216  $this->revisionsOnly = !empty( $options['revisionsOnly'] );
217 
218  // Most of this code will use the 'contributions' group DB, which can map to replica DBs
219  // with extra user based indexes or partitioning by user.
220  // Set database before parent constructor to avoid setting it there with wfGetDB
221  $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'contributions' );
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  $this->hookRunner->onContribsPager__reallyDoQuery(
305  $data, $this, $offset, $limit, $order );
306  }
307 
308  $result = [];
309 
310  // loop all results and collect them in an array
311  foreach ( $data as $query ) {
312  foreach ( $query as $i => $row ) {
313  // If the query results are in descending order, the indexes must also be in descending order
314  $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
315  // Left-pad with zeroes, because these values will be sorted as strings
316  $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
317  // use index column as key, allowing us to easily sort in PHP
318  $result[$row->{$this->getIndexField()} . "-$index"] = $row;
319  }
320  }
321 
322  // sort results
323  if ( $order === self::QUERY_ASCENDING ) {
324  ksort( $result );
325  } else {
326  krsort( $result );
327  }
328 
329  // enforce limit
330  $result = array_slice( $result, 0, $limit );
331 
332  // get rid of array keys
333  $result = array_values( $result );
334 
335  return new FakeResultWrapper( $result );
336  }
337 
347  private function getTargetTable() {
348  $dbr = $this->getDatabase();
349  $ipRangeConds = $this->targetUser->isRegistered()
350  ? null : $this->getIpRangeConds( $dbr, $this->target );
351  if ( $ipRangeConds ) {
352  return 'ip_changes';
353  }
354 
355  return 'revision';
356  }
357 
358  public function getQueryInfo() {
359  $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
360  $queryInfo = [
361  'tables' => $revQuery['tables'],
362  'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
363  'conds' => [],
364  'options' => [],
365  'join_conds' => $revQuery['joins'],
366  ];
367 
368  // WARNING: Keep this in sync with getTargetTable()!
369  $dbr = $this->getDatabase();
370  $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
371  if ( $ipRangeConds ) {
372  // Put ip_changes first (T284419)
373  array_unshift( $queryInfo['tables'], 'ip_changes' );
374  $queryInfo['join_conds']['revision'] = [
375  'JOIN', [ 'rev_id = ipc_rev_id' ]
376  ];
377  $queryInfo['conds'][] = $ipRangeConds;
378  } else {
379  // tables and joins are already handled by RevisionStore::getQueryInfo()
380  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
381  $queryInfo['conds'][] = $conds['conds'];
382  // Force the appropriate index to avoid bad query plans (T307295)
383  if ( isset( $conds['orconds']['newactor'] ) ) {
384  $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
385  }
386  }
387 
388  if ( $this->deletedOnly ) {
389  $queryInfo['conds'][] = 'rev_deleted != 0';
390  }
391 
392  if ( $this->topOnly ) {
393  $queryInfo['conds'][] = 'rev_id = page_latest';
394  }
395 
396  if ( $this->newOnly ) {
397  $queryInfo['conds'][] = 'rev_parent_id = 0';
398  }
399 
400  if ( $this->hideMinor ) {
401  $queryInfo['conds'][] = 'rev_minor_edit = 0';
402  }
403 
404  $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
405 
406  // Paranoia: avoid brute force searches (T19342)
407  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
408  $queryInfo['conds'][] = $dbr->bitAnd(
409  'rev_deleted', RevisionRecord::DELETED_USER
410  ) . ' = 0';
411  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
412  $queryInfo['conds'][] = $dbr->bitAnd(
413  'rev_deleted', RevisionRecord::SUPPRESSED_USER
414  ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
415  }
416 
417  // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
418  $indexField = $this->getIndexField();
419  if ( $indexField !== 'rev_timestamp' ) {
420  $queryInfo['fields'][] = $indexField;
421  }
422 
424  $queryInfo['tables'],
425  $queryInfo['fields'],
426  $queryInfo['conds'],
427  $queryInfo['join_conds'],
428  $queryInfo['options'],
429  $this->tagFilter,
430  $this->tagInvert,
431  );
432 
433  $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
434 
435  return $queryInfo;
436  }
437 
438  protected function getNamespaceCond() {
439  if ( $this->namespace !== '' ) {
440  $dbr = $this->getDatabase();
441  $selectedNS = $dbr->addQuotes( $this->namespace );
442  $eq_op = $this->nsInvert ? '!=' : '=';
443  $bool_op = $this->nsInvert ? 'AND' : 'OR';
444 
445  if ( !$this->associated ) {
446  return [ "page_namespace $eq_op $selectedNS" ];
447  }
448 
449  $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
450 
451  return [
452  "page_namespace $eq_op $selectedNS " .
453  $bool_op .
454  " page_namespace $eq_op $associatedNS"
455  ];
456  }
457 
458  return [];
459  }
460 
467  private function getIpRangeConds( $db, $ip ) {
468  // First make sure it is a valid range and they are not outside the CIDR limit
469  if ( !$this->isQueryableRange( $ip ) ) {
470  return false;
471  }
472 
473  [ $start, $end ] = IPUtils::parseRange( $ip );
474 
475  return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
476  }
477 
485  public function isQueryableRange( $ipRange ) {
486  $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
487 
488  $bits = IPUtils::parseCIDR( $ipRange )[1];
489  if (
490  ( $bits === false ) ||
491  ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
492  ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
493  ) {
494  return false;
495  }
496 
497  return true;
498  }
499 
503  public function getIndexField() {
504  // The returned column is used for sorting and continuation, so we need to
505  // make sure to use the right denormalized column depending on which table is
506  // being targeted by the query to avoid bad query plans.
507  // See T200259, T204669, T220991, and T221380.
508  $target = $this->getTargetTable();
509  switch ( $target ) {
510  case 'revision':
511  return 'rev_timestamp';
512  case 'ip_changes':
513  return 'ipc_rev_timestamp';
514  default:
515  wfWarn(
516  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
517  );
518  return 'rev_timestamp';
519  }
520  }
521 
525  public function getTagFilter() {
526  return $this->tagFilter;
527  }
528 
532  public function getTarget() {
533  return $this->target;
534  }
535 
539  public function isNewOnly() {
540  return $this->newOnly;
541  }
542 
546  public function getNamespace() {
547  return $this->namespace;
548  }
549 
553  protected function getExtraSortFields() {
554  // The returned columns are used for sorting, so we need to make sure
555  // to use the right denormalized column depending on which table is
556  // being targeted by the query to avoid bad query plans.
557  // See T200259, T204669, T220991, and T221380.
558  $target = $this->getTargetTable();
559  switch ( $target ) {
560  case 'revision':
561  return [ 'rev_id' ];
562  case 'ip_changes':
563  return [ 'ipc_rev_id' ];
564  default:
565  wfWarn(
566  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
567  );
568  return [ 'rev_id' ];
569  }
570  }
571 
572  protected function doBatchLookups() {
573  # Do a link batch query
574  $this->mResult->seek( 0 );
575  $parentRevIds = [];
576  $this->mParentLens = [];
577  $revisions = [];
578  $linkBatch = $this->linkBatchFactory->newLinkBatch();
579  $isIpRange = $this->isQueryableRange( $this->target );
580  # Give some pointers to make (last) links
581  foreach ( $this->mResult as $row ) {
582  if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
583  $parentRevIds[] = (int)$row->rev_parent_id;
584  }
585  if ( $this->revisionStore->isRevisionRow( $row ) ) {
586  $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
587  if ( $isIpRange ) {
588  // If this is an IP range, batch the IP's talk page
589  $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
590  }
591  $linkBatch->add( $row->page_namespace, $row->page_title );
592  $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
593  }
594  }
595  # Fetch rev_len for revisions not already scanned above
596  $this->mParentLens += $this->revisionStore->getRevisionSizes(
597  array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
598  );
599  $linkBatch->execute();
600 
601  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
602  ->authority( $this->getAuthority() )
603  ->revisions( $revisions )
604  ->hideIfDeleted()
605  ->execute();
606 
607  # For performance, save the revision objects for later.
608  # The array is indexed by rev_id. doBatchLookups() may be called
609  # multiple times with different results, so merge the revisions array,
610  # ignoring any duplicates.
611  $this->revisions += $revisions;
612  }
613 
617  protected function getStartBody() {
618  return "<section class='mw-pager-body'>\n";
619  }
620 
624  protected function getEndBody() {
625  return "</section>\n";
626  }
627 
638  public function tryCreatingRevisionRecord( $row, $title = null ) {
639  if ( $row instanceof stdClass && isset( $row->rev_id )
640  && isset( $this->revisions[$row->rev_id] )
641  ) {
642  return $this->revisions[$row->rev_id];
643  } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
644  return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
645  } else {
646  return null;
647  }
648  }
649 
662  public function formatRow( $row ) {
663  $ret = '';
664  $classes = [];
665  $attribs = [];
666 
667  $linkRenderer = $this->getLinkRenderer();
668 
669  $page = null;
670  // Create a title for the revision if possible
671  // Rows from the hook may not include title information
672  if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
673  $page = Title::newFromRow( $row );
674  }
675  // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
676  // function to be called with a special object for $row. It expects us
677  // skip formatting so that the row can be formatted by the
678  // ContributionsLineEnding hook below.
679  // FIXME: have some better way for extensions to provide formatted rows.
680  $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
681  if ( $revRecord && $page ) {
682  $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
683  $attribs['data-mw-revid'] = $revRecord->getId();
684 
685  $link = $linkRenderer->makeLink(
686  $page,
687  $page->getPrefixedText(),
688  [ 'class' => 'mw-contributions-title' ],
689  $page->isRedirect() ? [ 'redirect' => 'no' ] : []
690  );
691  # Mark current revisions
692  $topmarktext = '';
693 
694  $pagerTools = new PagerTools(
695  $revRecord,
696  null,
697  $row->rev_id === $row->page_latest && !$row->page_is_new,
698  $this->hookRunner,
699  $page,
700  $this->getContext(),
701  $this->getLinkRenderer()
702  );
703  if ( $row->rev_id === $row->page_latest ) {
704  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
705  $classes[] = 'mw-contributions-current';
706  }
707  if ( $pagerTools->shouldPreventClickjacking() ) {
708  $this->setPreventClickjacking( true );
709  }
710  $topmarktext .= $pagerTools->toHTML();
711  # Is there a visible previous revision?
712  if ( $revRecord->getParentId() !== 0 &&
713  $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
714  ) {
715  $difftext = $linkRenderer->makeKnownLink(
716  $page,
717  new HtmlArmor( $this->messages['diff'] ),
718  [ 'class' => 'mw-changeslist-diff' ],
719  [
720  'diff' => 'prev',
721  'oldid' => $row->rev_id
722  ]
723  );
724  } else {
725  $difftext = $this->messages['diff'];
726  }
727  $histlink = $linkRenderer->makeKnownLink(
728  $page,
729  new HtmlArmor( $this->messages['hist'] ),
730  [ 'class' => 'mw-changeslist-history' ],
731  [ 'action' => 'history' ]
732  );
733 
734  if ( $row->rev_parent_id === null ) {
735  // For some reason rev_parent_id isn't populated for this row.
736  // Its rumoured this is true on wikipedia for some revisions (T36922).
737  // Next best thing is to have the total number of bytes.
738  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
739  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
740  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
741  } else {
742  $parentLen = 0;
743  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
744  $parentLen = $this->mParentLens[$row->rev_parent_id];
745  }
746 
747  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
749  $parentLen,
750  $row->rev_len,
751  $this->getContext()
752  );
753  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
754  }
755 
756  $lang = $this->getLanguage();
757 
758  $comment = $this->formattedComments[$row->rev_id];
759 
760  if ( $comment === '' ) {
761  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
762  $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
763  }
764 
765  $comment = $lang->getDirMark() . $comment;
766 
767  $authority = $this->getAuthority();
768  $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
769 
770  # When querying for an IP range, we want to always show user and user talk links.
771  $userlink = '';
772  $revUser = $revRecord->getUser();
773  $revUserId = $revUser ? $revUser->getId() : 0;
774  $revUserText = $revUser ? $revUser->getName() : '';
775  if ( $this->isQueryableRange( $this->target ) ) {
776  $userlink = ' <span class="mw-changeslist-separator"></span> '
777  . $lang->getDirMark()
778  . Linker::userLink( $revUserId, $revUserText );
779  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
780  Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
781  }
782 
783  $flags = [];
784  if ( $revRecord->getParentId() === 0 ) {
785  $flags[] = ChangesList::flag( 'newpage' );
786  }
787 
788  if ( $revRecord->isMinor() ) {
789  $flags[] = ChangesList::flag( 'minor' );
790  }
791 
792  $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
793  if ( $del !== '' ) {
794  $del .= ' ';
795  }
796 
797  // While it might be tempting to use a list here
798  // this would result in clutter and slows down navigating the content
799  // in assistive technology.
800  // See https://phabricator.wikimedia.org/T205581#4734812
801  $diffHistLinks = Html::rawElement( 'span',
802  [ 'class' => 'mw-changeslist-links' ],
803  // The spans are needed to ensure the dividing '|' elements are not
804  // themselves styled as links.
805  Html::rawElement( 'span', [], $difftext ) .
806  ' ' . // Space needed for separating two words.
807  Html::rawElement( 'span', [], $histlink )
808  );
809 
810  # Tags, if any.
811  [ $tagSummary, $newClasses ] = ChangeTags::formatSummaryRow(
812  $row->ts_tags,
813  'contributions',
814  $this->getContext()
815  );
816  $classes = array_merge( $classes, $newClasses );
817 
818  $this->hookRunner->onSpecialContributions__formatRow__flags(
819  $this->getContext(), $row, $flags );
820 
821  $templateParams = [
822  'del' => $del,
823  'timestamp' => $d,
824  'diffHistLinks' => $diffHistLinks,
825  'charDifference' => $chardiff,
826  'flags' => $flags,
827  'articleLink' => $link,
828  'userlink' => $userlink,
829  'logText' => $comment,
830  'topmarktext' => $topmarktext,
831  'tagSummary' => $tagSummary,
832  ];
833 
834  # Denote if username is redacted for this edit
835  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
836  $templateParams['rev-deleted-user-contribs'] =
837  $this->msg( 'rev-deleted-user-contribs' )->escaped();
838  }
839 
840  $ret = $this->templateParser->processTemplate(
841  'SpecialContributionsLine',
842  $templateParams
843  );
844  }
845 
846  // Let extensions add data
847  $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
848  $attribs = array_filter( $attribs,
849  [ Sanitizer::class, 'isReservedDataAttribute' ],
850  ARRAY_FILTER_USE_KEY
851  );
852 
853  // TODO: Handle exceptions in the catch block above. Do any extensions rely on
854  // receiving empty rows?
855 
856  if ( $classes === [] && $attribs === [] && $ret === '' ) {
857  wfDebug( "Dropping Special:Contribution row that could not be formatted" );
858  return "<!-- Could not format Special:Contribution row. -->\n";
859  }
860  $attribs['class'] = $classes;
861 
862  // FIXME: The signature of the ContributionsLineEnding hook makes it
863  // very awkward to move this LI wrapper into the template.
864  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
865  }
866 
871  protected function getSqlComment() {
872  if ( $this->namespace || $this->deletedOnly ) {
873  // potentially slow, see CR r58153
874  return 'contributions page filtered for namespace or RevisionDeleted edits';
875  } else {
876  return 'contributions page unfiltered';
877  }
878  }
879 
883  protected function preventClickjacking() {
884  $this->setPreventClickjacking( true );
885  }
886 
891  protected function setPreventClickjacking( bool $enable ) {
892  $this->preventClickjacking = $enable;
893  }
894 
898  public function getPreventClickjacking() {
899  return $this->preventClickjacking;
900  }
901 
908  public static function processDateFilter( array $opts ) {
909  $start = $opts['start'] ?? '';
910  $end = $opts['end'] ?? '';
911  $year = $opts['year'] ?? '';
912  $month = $opts['month'] ?? '';
913 
914  if ( $start !== '' && $end !== '' && $start > $end ) {
915  $temp = $start;
916  $start = $end;
917  $end = $temp;
918  }
919 
920  // If year/month legacy filtering options are set, convert them to display the new stamp
921  if ( $year !== '' || $month !== '' ) {
922  // Reuse getDateCond logic, but subtract a day because
923  // the endpoints of our date range appear inclusive
924  // but the internal end offsets are always exclusive
925  $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
926  $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
927  $legacyDateTime = $legacyDateTime->modify( '-1 day' );
928 
929  // Clear the new timestamp range options if used and
930  // replace with the converted legacy timestamp
931  $start = '';
932  $end = $legacyDateTime->format( 'Y-m-d' );
933  }
934 
935  $opts['start'] = $start;
936  $opts['end'] = $end;
937 
938  return $opts;
939  }
940 }
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()
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:902
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:196
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...
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
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.@stable to overridestring
setPreventClickjacking(bool $enable)
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
getDatabase()
Get the Database object in use.
Definition: IndexPager.php:248
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...
Definition: HookRunner.php:560
Class that generates HTML anchor link elements for pages.
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Page revision base class.
Service for looking up page revisions.
This is 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.
Definition: PagerTools.php:13
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 ...
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:576
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:40
This class is a delegate to ILBFactory for a given database cluster.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang
$revQuery