MediaWiki  master
ContribsPager.php
Go to the documentation of this file.
1 <?php
38 use Wikimedia\IPUtils;
43 
49 
50  public $mGroupByDate = true;
51 
55  private $messages;
56 
60  private $target;
61 
65  private $namespace;
66 
70  private $tagFilter;
71 
75  private $tagInvert;
76 
80  private $nsInvert;
81 
86  private $associated;
87 
91  private $deletedOnly;
92 
96  private $topOnly;
97 
101  private $newOnly;
102 
106  private $hideMinor;
107 
112  private $revisionsOnly;
113 
114  private $preventClickjacking = false;
115 
119  private $mParentLens;
120 
122  private $targetUser;
123 
127  private $templateParser;
128 
130  private $actorMigration;
131 
133  private $commentFormatter;
134 
136  private $hookRunner;
137 
139  private $linkBatchFactory;
140 
142  private $namespaceInfo;
143 
145  private $revisionStore;
146 
148  private $formattedComments = [];
149 
151  private $revisions = [];
152 
167  public function __construct(
168  IContextSource $context,
169  array $options,
170  LinkRenderer $linkRenderer = null,
171  LinkBatchFactory $linkBatchFactory = null,
172  HookContainer $hookContainer = null,
173  IConnectionProvider $dbProvider = null,
174  ActorMigration $actorMigration = null,
175  RevisionStore $revisionStore = null,
176  NamespaceInfo $namespaceInfo = null,
177  UserIdentity $targetUser = null,
178  CommentFormatter $commentFormatter = null
179  ) {
180  // Class is used directly in extensions - T266484
181  $services = MediaWikiServices::getInstance();
182  $dbProvider ??= $services->getDBLoadBalancerFactory();
183 
184  // Set ->target before calling parent::__construct() so
185  // parent can call $this->getIndexField() and get the right result. Set
186  // the rest too just to keep things simple.
187  if ( $targetUser ) {
188  $this->target = $options['target'] ?? $targetUser->getName();
189  $this->targetUser = $targetUser;
190  } else {
191  // Use target option
192  // It's possible for the target to be empty. This is used by
193  // ContribsPagerTest and does not cause newFromName() to return
194  // false. It's probably not used by any production code.
195  $this->target = $options['target'] ?? '';
196  // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
197  $this->targetUser = $services->getUserFactory()->newFromName(
198  $this->target, UserRigorOptions::RIGOR_NONE
199  );
200  if ( !$this->targetUser ) {
201  // This can happen if the target contained "#". Callers
202  // typically pass user input through title normalization to
203  // avoid it.
204  throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
205  'broken to use even with validation disabled.' );
206  }
207  }
208 
209  $this->namespace = $options['namespace'] ?? '';
210  $this->tagFilter = $options['tagfilter'] ?? false;
211  $this->tagInvert = $options['tagInvert'] ?? false;
212  $this->nsInvert = $options['nsInvert'] ?? false;
213  $this->associated = $options['associated'] ?? false;
214 
215  $this->deletedOnly = !empty( $options['deletedOnly'] );
216  $this->topOnly = !empty( $options['topOnly'] );
217  $this->newOnly = !empty( $options['newOnly'] );
218  $this->hideMinor = !empty( $options['hideMinor'] );
219  $this->revisionsOnly = !empty( $options['revisionsOnly'] );
220 
221  // Needed by call to getIndexField -> getTargetTable from parent constructor
222  $this->actorMigration = $actorMigration ?? $services->getActorMigration();
223  parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
224 
225  $msgs = [
226  'diff',
227  'hist',
228  'pipe-separator',
229  'uctop'
230  ];
231 
232  foreach ( $msgs as $msg ) {
233  $this->messages[$msg] = $this->msg( $msg )->escaped();
234  }
235 
236  // Date filtering: use timestamp if available
237  $startTimestamp = '';
238  $endTimestamp = '';
239  if ( isset( $options['start'] ) && $options['start'] ) {
240  $startTimestamp = $options['start'] . ' 00:00:00';
241  }
242  if ( isset( $options['end'] ) && $options['end'] ) {
243  $endTimestamp = $options['end'] . ' 23:59:59';
244  }
245  $this->getDateRangeCond( $startTimestamp, $endTimestamp );
246 
247  $this->templateParser = new TemplateParser();
248  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
249  $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
250  $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
251  $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
252  $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
253  }
254 
255  public function getDefaultQuery() {
256  $query = parent::getDefaultQuery();
257  $query['target'] = $this->target;
258 
259  return $query;
260  }
261 
271  public function reallyDoQuery( $offset, $limit, $order ) {
272  [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo(
273  $offset,
274  $limit,
275  $order
276  );
277 
278  $options['MAX_EXECUTION_TIME'] =
279  $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
280  /*
281  * This hook will allow extensions to add in additional queries, so they can get their data
282  * in My Contributions as well. Extensions should append their results to the $data array.
283  *
284  * Extension queries have to implement the navbar requirement as well. They should
285  * - have a column aliased as $pager->getIndexField()
286  * - have LIMIT set
287  * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
288  * - have the ORDER BY specified based upon the details provided by the navbar
289  *
290  * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
291  *
292  * &$data: an array of results of all contribs queries
293  * $pager: the ContribsPager object hooked into
294  * $offset: see phpdoc above
295  * $limit: see phpdoc above
296  * $descending: see phpdoc above
297  */
298  $dbr = $this->getDatabase();
299  $data = [ $dbr->select(
300  $tables, $fields, $conds, $fname, $options, $join_conds
301  ) ];
302  if ( !$this->revisionsOnly ) {
303  // TODO: Range offsets are fairly important and all handlers should take care of it.
304  // If this hook will be replaced (e.g. unified with the DeletedContribsPager one),
305  // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577).
306  $this->hookRunner->onContribsPager__reallyDoQuery(
307  $data, $this, $offset, $limit, $order );
308  }
309 
310  $result = [];
311 
312  // loop all results and collect them in an array
313  foreach ( $data as $query ) {
314  foreach ( $query as $i => $row ) {
315  // If the query results are in descending order, the indexes must also be in descending order
316  $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
317  // Left-pad with zeroes, because these values will be sorted as strings
318  $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
319  // use index column as key, allowing us to easily sort in PHP
320  $result[$row->{$this->getIndexField()} . "-$index"] = $row;
321  }
322  }
323 
324  // sort results
325  if ( $order === self::QUERY_ASCENDING ) {
326  ksort( $result );
327  } else {
328  krsort( $result );
329  }
330 
331  // enforce limit
332  $result = array_slice( $result, 0, $limit );
333 
334  // get rid of array keys
335  $result = array_values( $result );
336 
337  return new FakeResultWrapper( $result );
338  }
339 
349  private function getTargetTable() {
350  $dbr = $this->getDatabase();
351  $ipRangeConds = $this->targetUser->isRegistered()
352  ? null : $this->getIpRangeConds( $dbr, $this->target );
353  if ( $ipRangeConds ) {
354  return 'ip_changes';
355  }
356 
357  return 'revision';
358  }
359 
360  public function getQueryInfo() {
361  $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
362  $queryInfo = [
363  'tables' => $revQuery['tables'],
364  'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
365  'conds' => [],
366  'options' => [],
367  'join_conds' => $revQuery['joins'],
368  ];
369 
370  // WARNING: Keep this in sync with getTargetTable()!
371  $dbr = $this->getDatabase();
372  $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
373  if ( $ipRangeConds ) {
374  // Put ip_changes first (T284419)
375  array_unshift( $queryInfo['tables'], 'ip_changes' );
376  $queryInfo['join_conds']['revision'] = [
377  'JOIN', [ 'rev_id = ipc_rev_id' ]
378  ];
379  $queryInfo['conds'][] = $ipRangeConds;
380  } else {
381  // tables and joins are already handled by RevisionStore::getQueryInfo()
382  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
383  $queryInfo['conds'][] = $conds['conds'];
384  // Force the appropriate index to avoid bad query plans (T307295)
385  if ( isset( $conds['orconds']['newactor'] ) ) {
386  $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
387  }
388  }
389 
390  if ( $this->deletedOnly ) {
391  $queryInfo['conds'][] = 'rev_deleted != 0';
392  }
393 
394  if ( $this->topOnly ) {
395  $queryInfo['conds'][] = 'rev_id = page_latest';
396  }
397 
398  if ( $this->newOnly ) {
399  $queryInfo['conds'][] = 'rev_parent_id = 0';
400  }
401 
402  if ( $this->hideMinor ) {
403  $queryInfo['conds'][] = 'rev_minor_edit = 0';
404  }
405 
406  $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
407 
408  // Paranoia: avoid brute force searches (T19342)
409  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
410  $queryInfo['conds'][] = $dbr->bitAnd(
411  'rev_deleted', RevisionRecord::DELETED_USER
412  ) . ' = 0';
413  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
414  $queryInfo['conds'][] = $dbr->bitAnd(
415  'rev_deleted', RevisionRecord::SUPPRESSED_USER
416  ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
417  }
418 
419  // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
420  $indexField = $this->getIndexField();
421  if ( $indexField !== 'rev_timestamp' ) {
422  $queryInfo['fields'][] = $indexField;
423  }
424 
426  $queryInfo['tables'],
427  $queryInfo['fields'],
428  $queryInfo['conds'],
429  $queryInfo['join_conds'],
430  $queryInfo['options'],
431  $this->tagFilter,
432  $this->tagInvert,
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  [ $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( MainConfigNames::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  default:
517  wfWarn(
518  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
519  );
520  return 'rev_timestamp';
521  }
522  }
523 
527  public function getTagFilter() {
528  return $this->tagFilter;
529  }
530 
534  public function getTarget() {
535  return $this->target;
536  }
537 
541  public function isNewOnly() {
542  return $this->newOnly;
543  }
544 
548  public function getNamespace() {
549  return $this->namespace;
550  }
551 
555  protected function getExtraSortFields() {
556  // The returned columns are used for sorting, so we need to make sure
557  // to use the right denormalized column depending on which table is
558  // being targeted by the query to avoid bad query plans.
559  // See T200259, T204669, T220991, and T221380.
560  $target = $this->getTargetTable();
561  switch ( $target ) {
562  case 'revision':
563  return [ 'rev_id' ];
564  case 'ip_changes':
565  return [ 'ipc_rev_id' ];
566  default:
567  wfWarn(
568  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
569  );
570  return [ 'rev_id' ];
571  }
572  }
573 
574  protected function doBatchLookups() {
575  # Do a link batch query
576  $this->mResult->seek( 0 );
577  $parentRevIds = [];
578  $this->mParentLens = [];
579  $revisions = [];
580  $linkBatch = $this->linkBatchFactory->newLinkBatch();
581  $isIpRange = $this->isQueryableRange( $this->target );
582  # Give some pointers to make (last) links
583  foreach ( $this->mResult as $row ) {
584  if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
585  $parentRevIds[] = (int)$row->rev_parent_id;
586  }
587  if ( $this->revisionStore->isRevisionRow( $row ) ) {
588  $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
589  if ( $isIpRange ) {
590  // If this is an IP range, batch the IP's talk page
591  $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
592  }
593  $linkBatch->add( $row->page_namespace, $row->page_title );
594  $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
595  }
596  }
597  # Fetch rev_len for revisions not already scanned above
598  $this->mParentLens += $this->revisionStore->getRevisionSizes(
599  array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
600  );
601  $linkBatch->execute();
602 
603  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
604  ->authority( $this->getAuthority() )
605  ->revisions( $revisions )
606  ->hideIfDeleted()
607  ->execute();
608 
609  # For performance, save the revision objects for later.
610  # The array is indexed by rev_id. doBatchLookups() may be called
611  # multiple times with different results, so merge the revisions array,
612  # ignoring any duplicates.
613  $this->revisions += $revisions;
614  }
615 
619  protected function getStartBody() {
620  return "<section class='mw-pager-body'>\n";
621  }
622 
626  protected function getEndBody() {
627  return "</section>\n";
628  }
629 
640  public function tryCreatingRevisionRecord( $row, $title = null ) {
641  if ( $row instanceof stdClass && isset( $row->rev_id )
642  && isset( $this->revisions[$row->rev_id] )
643  ) {
644  return $this->revisions[$row->rev_id];
645  } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
646  return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
647  } else {
648  return null;
649  }
650  }
651 
664  public function formatRow( $row ) {
665  $ret = '';
666  $classes = [];
667  $attribs = [];
668 
669  $linkRenderer = $this->getLinkRenderer();
670 
671  $page = null;
672  // Create a title for the revision if possible
673  // Rows from the hook may not include title information
674  if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
675  $page = Title::newFromRow( $row );
676  }
677  // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
678  // function to be called with a special object for $row. It expects us
679  // skip formatting so that the row can be formatted by the
680  // ContributionsLineEnding hook below.
681  // FIXME: have some better way for extensions to provide formatted rows.
682  $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
683  if ( $revRecord && $page ) {
684  $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
685  $attribs['data-mw-revid'] = $revRecord->getId();
686 
687  $link = $linkRenderer->makeLink(
688  $page,
689  $page->getPrefixedText(),
690  [ 'class' => 'mw-contributions-title' ],
691  $page->isRedirect() ? [ 'redirect' => 'no' ] : []
692  );
693  # Mark current revisions
694  $topmarktext = '';
695 
696  $pagerTools = new PagerTools(
697  $revRecord,
698  null,
699  $row->rev_id === $row->page_latest && !$row->page_is_new,
700  $this->hookRunner,
701  $page,
702  $this->getContext(),
703  $this->getLinkRenderer()
704  );
705  if ( $row->rev_id === $row->page_latest ) {
706  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
707  $classes[] = 'mw-contributions-current';
708  }
709  if ( $pagerTools->shouldPreventClickjacking() ) {
710  $this->setPreventClickjacking( true );
711  }
712  $topmarktext .= $pagerTools->toHTML();
713  # Is there a visible previous revision?
714  if ( $revRecord->getParentId() !== 0 &&
715  $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
716  ) {
717  $difftext = $linkRenderer->makeKnownLink(
718  $page,
719  new HtmlArmor( $this->messages['diff'] ),
720  [ 'class' => 'mw-changeslist-diff' ],
721  [
722  'diff' => 'prev',
723  'oldid' => $row->rev_id
724  ]
725  );
726  } else {
727  $difftext = $this->messages['diff'];
728  }
729  $histlink = $linkRenderer->makeKnownLink(
730  $page,
731  new HtmlArmor( $this->messages['hist'] ),
732  [ 'class' => 'mw-changeslist-history' ],
733  [ 'action' => 'history' ]
734  );
735 
736  if ( $row->rev_parent_id === null ) {
737  // For some reason rev_parent_id isn't populated for this row.
738  // Its rumoured this is true on wikipedia for some revisions (T36922).
739  // Next best thing is to have the total number of bytes.
740  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
741  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
742  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
743  } else {
744  $parentLen = 0;
745  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
746  $parentLen = $this->mParentLens[$row->rev_parent_id];
747  }
748 
749  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
751  $parentLen,
752  $row->rev_len,
753  $this->getContext()
754  );
755  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
756  }
757 
758  $lang = $this->getLanguage();
759 
760  $comment = $this->formattedComments[$row->rev_id];
761 
762  if ( $comment === '' ) {
763  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
764  $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
765  }
766 
767  $comment = $lang->getDirMark() . $comment;
768 
769  $authority = $this->getAuthority();
770  $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
771 
772  # When querying for an IP range, we want to always show user and user talk links.
773  $userlink = '';
774  $revUser = $revRecord->getUser();
775  $revUserId = $revUser ? $revUser->getId() : 0;
776  $revUserText = $revUser ? $revUser->getName() : '';
777  if ( $this->isQueryableRange( $this->target ) ) {
778  $userlink = ' <span class="mw-changeslist-separator"></span> '
779  . $lang->getDirMark()
780  . Linker::userLink( $revUserId, $revUserText );
781  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
782  Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
783  }
784 
785  $flags = [];
786  if ( $revRecord->getParentId() === 0 ) {
787  $flags[] = ChangesList::flag( 'newpage' );
788  }
789 
790  if ( $revRecord->isMinor() ) {
791  $flags[] = ChangesList::flag( 'minor' );
792  }
793 
794  $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
795  if ( $del !== '' ) {
796  $del .= ' ';
797  }
798 
799  // While it might be tempting to use a list here
800  // this would result in clutter and slows down navigating the content
801  // in assistive technology.
802  // See https://phabricator.wikimedia.org/T205581#4734812
803  $diffHistLinks = Html::rawElement( 'span',
804  [ 'class' => 'mw-changeslist-links' ],
805  // The spans are needed to ensure the dividing '|' elements are not
806  // themselves styled as links.
807  Html::rawElement( 'span', [], $difftext ) .
808  ' ' . // Space needed for separating two words.
809  Html::rawElement( 'span', [], $histlink )
810  );
811 
812  # Tags, if any.
813  [ $tagSummary, $newClasses ] = ChangeTags::formatSummaryRow(
814  $row->ts_tags,
815  null,
816  $this->getContext()
817  );
818  $classes = array_merge( $classes, $newClasses );
819 
820  $this->hookRunner->onSpecialContributions__formatRow__flags(
821  $this->getContext(), $row, $flags );
822 
823  $templateParams = [
824  'del' => $del,
825  'timestamp' => $d,
826  'diffHistLinks' => $diffHistLinks,
827  'charDifference' => $chardiff,
828  'flags' => $flags,
829  'articleLink' => $link,
830  'userlink' => $userlink,
831  'logText' => $comment,
832  'topmarktext' => $topmarktext,
833  'tagSummary' => $tagSummary,
834  ];
835 
836  # Denote if username is redacted for this edit
837  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
838  $templateParams['rev-deleted-user-contribs'] =
839  $this->msg( 'rev-deleted-user-contribs' )->escaped();
840  }
841 
842  $ret = $this->templateParser->processTemplate(
843  'SpecialContributionsLine',
844  $templateParams
845  );
846  }
847 
848  // Let extensions add data
849  $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
850  $attribs = array_filter( $attribs,
851  [ Sanitizer::class, 'isReservedDataAttribute' ],
852  ARRAY_FILTER_USE_KEY
853  );
854 
855  // TODO: Handle exceptions in the catch block above. Do any extensions rely on
856  // receiving empty rows?
857 
858  if ( $classes === [] && $attribs === [] && $ret === '' ) {
859  wfDebug( "Dropping Special:Contribution row that could not be formatted" );
860  return "<!-- Could not format Special:Contribution row. -->\n";
861  }
862  $attribs['class'] = $classes;
863 
864  // FIXME: The signature of the ContributionsLineEnding hook makes it
865  // very awkward to move this LI wrapper into the template.
866  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
867  }
868 
873  protected function getSqlComment() {
874  if ( $this->namespace || $this->deletedOnly ) {
875  // potentially slow, see CR r58153
876  return 'contributions page filtered for namespace or RevisionDeleted edits';
877  } else {
878  return 'contributions page unfiltered';
879  }
880  }
881 
885  protected function preventClickjacking() {
886  $this->setPreventClickjacking( true );
887  }
888 
893  protected function setPreventClickjacking( bool $enable ) {
894  $this->preventClickjacking = $enable;
895  }
896 
900  public function getPreventClickjacking() {
901  return $this->preventClickjacking;
902  }
903 
910  public static function processDateFilter( array $opts ) {
911  $start = $opts['start'] ?? '';
912  $end = $opts['end'] ?? '';
913  $year = $opts['year'] ?? '';
914  $month = $opts['month'] ?? '';
915 
916  if ( $start !== '' && $end !== '' && $start > $end ) {
917  $temp = $start;
918  $start = $end;
919  $end = $temp;
920  }
921 
922  // If year/month legacy filtering options are set, convert them to display the new stamp
923  if ( $year !== '' || $month !== '' ) {
924  // Reuse getDateCond logic, but subtract a day because
925  // the endpoints of our date range appear inclusive
926  // but the internal end offsets are always exclusive
927  $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
928  $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
929  $legacyDateTime = $legacyDateTime->modify( '-1 day' );
930 
931  // Clear the new timestamp range options if used and
932  // replace with the converted legacy timestamp
933  $start = '';
934  $end = $legacyDateTime->format( 'Y-m-d' );
935  }
936 
937  $opts['start'] = $start;
938  $opts['end'] = $end;
939 
940  return $opts;
941  }
942 }
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'))
Definition: WebStart.php:88
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:845
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:162
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.
__construct(IContextSource $context, array $options, LinkRenderer $linkRenderer=null, LinkBatchFactory $linkBatchFactory=null, HookContainer $hookContainer=null, IConnectionProvider $dbProvider=null, ActorMigration $actorMigration=null, RevisionStore $revisionStore=null, NamespaceInfo $namespaceInfo=null, UserIdentity $targetUser=null, CommentFormatter $commentFormatter=null)
FIXME List services first T266484 / T290405.
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.
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
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:565
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition: Linker.php:67
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Page revision base class.
Service for looking up page revisions.
Represents a title within MediaWiki.
Definition: Title.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Generate a set of tools for a revision.
Definition: PagerTools.php:14
Pager for filtering by a range of dates.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.For b/c, query direction is true for ascending and fal...
getDateRangeCond( $startTime, $endTime)
Set and return a date range condition using timestamps provided by the user.
static getOffsetDate( $year, $month, $day=-1)
Core logic of determining the offset timestamp such that we can get all items with a timestamp up to ...
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Provide primary and replica IDatabase connections.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
Result wrapper for grabbing data queried from an IDatabase object.
if(!isset( $args[0])) $lang
$revQuery