MediaWiki  master
ContribsPager.php
Go to the documentation of this file.
1 <?php
33 use Wikimedia\IPUtils;
38 
44 
45  public $mGroupByDate = true;
46 
50  private $messages;
51 
55  private $target;
56 
60  private $namespace;
61 
65  private $tagFilter;
66 
70  private $nsInvert;
71 
76  private $associated;
77 
81  private $deletedOnly;
82 
86  private $topOnly;
87 
91  private $newOnly;
92 
96  private $hideMinor;
97 
102  private $revisionsOnly;
103 
104  private $preventClickjacking = false;
105 
109  private $mParentLens;
110 
112  private $targetUser;
113 
118 
121 
124 
126  private $hookRunner;
127 
130 
132  private $namespaceInfo;
133 
135  private $revisionStore;
136 
138  private $formattedComments = [];
139 
141  private $revisions = [];
142 
157  public function __construct(
159  array $options,
162  HookContainer $hookContainer = null,
163  ILoadBalancer $loadBalancer = null,
167  UserIdentity $targetUser = null,
169  ) {
170  // Class is used directly in extensions - T266484
171  $services = MediaWikiServices::getInstance();
172  $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
173 
174  // Set ->target before calling parent::__construct() so
175  // parent can call $this->getIndexField() and get the right result. Set
176  // the rest too just to keep things simple.
177  if ( $targetUser ) {
178  $this->target = $options['target'] ?? $targetUser->getName();
179  $this->targetUser = $targetUser;
180  } else {
181  // Use target option
182  // It's possible for the target to be empty. This is used by
183  // ContribsPagerTest and does not cause newFromName() to return
184  // false. It's probably not used by any production code.
185  $this->target = $options['target'] ?? '';
186  // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null
187  $this->targetUser = $services->getUserFactory()->newFromName(
188  $this->target, UserRigorOptions::RIGOR_NONE
189  );
190  if ( !$this->targetUser ) {
191  // This can happen if the target contained "#". Callers
192  // typically pass user input through title normalization to
193  // avoid it.
194  throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' .
195  'broken to use even with validation disabled.' );
196  }
197  }
198 
199  $this->namespace = $options['namespace'] ?? '';
200  $this->tagFilter = $options['tagfilter'] ?? false;
201  $this->nsInvert = $options['nsInvert'] ?? false;
202  $this->associated = $options['associated'] ?? false;
203 
204  $this->deletedOnly = !empty( $options['deletedOnly'] );
205  $this->topOnly = !empty( $options['topOnly'] );
206  $this->newOnly = !empty( $options['newOnly'] );
207  $this->hideMinor = !empty( $options['hideMinor'] );
208  $this->revisionsOnly = !empty( $options['revisionsOnly'] );
209 
210  // Most of this code will use the 'contributions' group DB, which can map to replica DBs
211  // with extra user based indexes or partitioning by user.
212  // Set database before parent constructor to avoid setting it there with wfGetDB
213  $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'contributions' );
214  // Needed by call to getIndexField -> getTargetTable from parent constructor
215  $this->actorMigration = $actorMigration ?? $services->getActorMigration();
216  parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
217 
218  $msgs = [
219  'diff',
220  'hist',
221  'pipe-separator',
222  'uctop'
223  ];
224 
225  foreach ( $msgs as $msg ) {
226  $this->messages[$msg] = $this->msg( $msg )->escaped();
227  }
228 
229  // Date filtering: use timestamp if available
230  $startTimestamp = '';
231  $endTimestamp = '';
232  if ( isset( $options['start'] ) && $options['start'] ) {
233  $startTimestamp = $options['start'] . ' 00:00:00';
234  }
235  if ( isset( $options['end'] ) && $options['end'] ) {
236  $endTimestamp = $options['end'] . ' 23:59:59';
237  }
238  $this->getDateRangeCond( $startTimestamp, $endTimestamp );
239 
240  $this->templateParser = new TemplateParser();
241  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
242  $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
243  $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
244  $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
245  $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
246  }
247 
248  public function getDefaultQuery() {
249  $query = parent::getDefaultQuery();
250  $query['target'] = $this->target;
251 
252  return $query;
253  }
254 
262  public function getNavigationBar() {
263  return Html::rawElement( 'p', [ 'class' => 'mw-pager-navigation-bar' ],
264  parent::getNavigationBar()
265  );
266  }
267 
277  public function reallyDoQuery( $offset, $limit, $order ) {
278  list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
279  $offset,
280  $limit,
281  $order
282  );
283 
284  $options['MAX_EXECUTION_TIME'] =
285  $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
286  /*
287  * This hook will allow extensions to add in additional queries, so they can get their data
288  * in My Contributions as well. Extensions should append their results to the $data array.
289  *
290  * Extension queries have to implement the navbar requirement as well. They should
291  * - have a column aliased as $pager->getIndexField()
292  * - have LIMIT set
293  * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
294  * - have the ORDER BY specified based upon the details provided by the navbar
295  *
296  * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
297  *
298  * &$data: an array of results of all contribs queries
299  * $pager: the ContribsPager object hooked into
300  * $offset: see phpdoc above
301  * $limit: see phpdoc above
302  * $descending: see phpdoc above
303  */
304  $dbr = $this->getDatabase();
305  $data = [ $dbr->select(
306  $tables, $fields, $conds, $fname, $options, $join_conds
307  ) ];
308  if ( !$this->revisionsOnly ) {
309  $this->hookRunner->onContribsPager__reallyDoQuery(
310  $data, $this, $offset, $limit, $order );
311  }
312 
313  $result = [];
314 
315  // loop all results and collect them in an array
316  foreach ( $data as $query ) {
317  foreach ( $query as $i => $row ) {
318  // If the query results are in descending order, the indexes must also be in descending order
319  $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
320  // Left-pad with zeroes, because these values will be sorted as strings
321  $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
322  // use index column as key, allowing us to easily sort in PHP
323  $result[$row->{$this->getIndexField()} . "-$index"] = $row;
324  }
325  }
326 
327  // sort results
328  if ( $order === self::QUERY_ASCENDING ) {
329  ksort( $result );
330  } else {
331  krsort( $result );
332  }
333 
334  // enforce limit
335  $result = array_slice( $result, 0, $limit );
336 
337  // get rid of array keys
338  $result = array_values( $result );
339 
340  return new FakeResultWrapper( $result );
341  }
342 
352  private function getTargetTable() {
353  $dbr = $this->getDatabase();
354  $ipRangeConds = $this->targetUser->isRegistered()
355  ? null : $this->getIpRangeConds( $dbr, $this->target );
356  if ( $ipRangeConds ) {
357  return 'ip_changes';
358  } else {
359  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
360  if ( isset( $conds['orconds']['actor'] ) ) {
361  return 'revision_actor_temp';
362  }
363  }
364 
365  return 'revision';
366  }
367 
368  public function getQueryInfo() {
369  $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
370  $queryInfo = [
371  'tables' => $revQuery['tables'],
372  'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
373  'conds' => [],
374  'options' => [],
375  'join_conds' => $revQuery['joins'],
376  ];
377 
378  // WARNING: Keep this in sync with getTargetTable()!
379  $dbr = $this->getDatabase();
380  $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
381  if ( $ipRangeConds ) {
382  // Put ip_changes first (T284419)
383  array_unshift( $queryInfo['tables'], 'ip_changes' );
384  $queryInfo['join_conds']['revision'] = [
385  'JOIN', [ 'rev_id = ipc_rev_id' ]
386  ];
387  $queryInfo['conds'][] = $ipRangeConds;
388  } else {
389  // tables and joins are already handled by RevisionStore::getQueryInfo()
390  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
391  $queryInfo['conds'][] = $conds['conds'];
392  // Force the appropriate index to avoid bad query plans (T189026 and T307295)
393  if ( isset( $conds['orconds']['actor'] ) ) {
394  $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
395  }
396  if ( isset( $conds['orconds']['newactor'] ) ) {
397  $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
398  }
399  }
400 
401  if ( $this->deletedOnly ) {
402  $queryInfo['conds'][] = 'rev_deleted != 0';
403  }
404 
405  if ( $this->topOnly ) {
406  $queryInfo['conds'][] = 'rev_id = page_latest';
407  }
408 
409  if ( $this->newOnly ) {
410  $queryInfo['conds'][] = 'rev_parent_id = 0';
411  }
412 
413  if ( $this->hideMinor ) {
414  $queryInfo['conds'][] = 'rev_minor_edit = 0';
415  }
416 
417  $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
418 
419  // Paranoia: avoid brute force searches (T19342)
420  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
421  $queryInfo['conds'][] = $dbr->bitAnd(
422  'rev_deleted', RevisionRecord::DELETED_USER
423  ) . ' = 0';
424  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
425  $queryInfo['conds'][] = $dbr->bitAnd(
426  'rev_deleted', RevisionRecord::SUPPRESSED_USER
427  ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
428  }
429 
430  // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
431  $indexField = $this->getIndexField();
432  if ( $indexField !== 'rev_timestamp' ) {
433  $queryInfo['fields'][] = $indexField;
434  }
435 
437  $queryInfo['tables'],
438  $queryInfo['fields'],
439  $queryInfo['conds'],
440  $queryInfo['join_conds'],
441  $queryInfo['options'],
442  $this->tagFilter
443  );
444 
445  $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
446 
447  return $queryInfo;
448  }
449 
450  protected function getNamespaceCond() {
451  if ( $this->namespace !== '' ) {
452  $dbr = $this->getDatabase();
453  $selectedNS = $dbr->addQuotes( $this->namespace );
454  $eq_op = $this->nsInvert ? '!=' : '=';
455  $bool_op = $this->nsInvert ? 'AND' : 'OR';
456 
457  if ( !$this->associated ) {
458  return [ "page_namespace $eq_op $selectedNS" ];
459  }
460 
461  $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
462 
463  return [
464  "page_namespace $eq_op $selectedNS " .
465  $bool_op .
466  " page_namespace $eq_op $associatedNS"
467  ];
468  }
469 
470  return [];
471  }
472 
479  private function getIpRangeConds( $db, $ip ) {
480  // First make sure it is a valid range and they are not outside the CIDR limit
481  if ( !$this->isQueryableRange( $ip ) ) {
482  return false;
483  }
484 
485  list( $start, $end ) = IPUtils::parseRange( $ip );
486 
487  return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
488  }
489 
497  public function isQueryableRange( $ipRange ) {
498  $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
499 
500  $bits = IPUtils::parseCIDR( $ipRange )[1];
501  if (
502  ( $bits === false ) ||
503  ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
504  ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
505  ) {
506  return false;
507  }
508 
509  return true;
510  }
511 
515  public function getIndexField() {
516  // The returned column is used for sorting and continuation, so we need to
517  // make sure to use the right denormalized column depending on which table is
518  // being targeted by the query to avoid bad query plans.
519  // See T200259, T204669, T220991, and T221380.
520  $target = $this->getTargetTable();
521  switch ( $target ) {
522  case 'revision':
523  return 'rev_timestamp';
524  case 'ip_changes':
525  return 'ipc_rev_timestamp';
526  case 'revision_actor_temp':
527  return 'revactor_timestamp';
528  default:
529  wfWarn(
530  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
531  );
532  return 'rev_timestamp';
533  }
534  }
535 
539  public function getTagFilter() {
540  return $this->tagFilter;
541  }
542 
546  public function getTarget() {
547  return $this->target;
548  }
549 
553  public function isNewOnly() {
554  return $this->newOnly;
555  }
556 
560  public function getNamespace() {
561  return $this->namespace;
562  }
563 
567  protected function getExtraSortFields() {
568  // The returned columns are used for sorting, so we need to make sure
569  // to use the right denormalized column depending on which table is
570  // being targeted by the query to avoid bad query plans.
571  // See T200259, T204669, T220991, and T221380.
572  $target = $this->getTargetTable();
573  switch ( $target ) {
574  case 'revision':
575  return [ 'rev_id' ];
576  case 'ip_changes':
577  return [ 'ipc_rev_id' ];
578  case 'revision_actor_temp':
579  return [ 'revactor_rev' ];
580  default:
581  wfWarn(
582  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
583  );
584  return [ 'rev_id' ];
585  }
586  }
587 
588  protected function doBatchLookups() {
589  # Do a link batch query
590  $this->mResult->seek( 0 );
591  $parentRevIds = [];
592  $this->mParentLens = [];
593  $revisions = [];
594  $linkBatch = $this->linkBatchFactory->newLinkBatch();
595  $isIpRange = $this->isQueryableRange( $this->target );
596  # Give some pointers to make (last) links
597  foreach ( $this->mResult as $row ) {
598  if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
599  $parentRevIds[] = (int)$row->rev_parent_id;
600  }
601  if ( $this->revisionStore->isRevisionRow( $row ) ) {
602  $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
603  if ( $isIpRange ) {
604  // If this is an IP range, batch the IP's talk page
605  $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
606  }
607  $linkBatch->add( $row->page_namespace, $row->page_title );
608  $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
609  }
610  }
611  # Fetch rev_len for revisions not already scanned above
612  $this->mParentLens += $this->revisionStore->getRevisionSizes(
613  array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
614  );
615  $linkBatch->execute();
616 
617  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
618  ->authority( $this->getAuthority() )
619  ->revisions( $revisions )
620  ->hideIfDeleted()
621  ->execute();
622 
623  # For performance, save the revision objects for later.
624  # The array is indexed by rev_id. doBatchLookups() may be called
625  # multiple times with different results, so merge the revisions array,
626  # ignoring any duplicates.
627  $this->revisions += $revisions;
628  }
629 
633  protected function getStartBody() {
634  return "<section class='mw-pager-body'>\n";
635  }
636 
640  protected function getEndBody() {
641  return "</section>\n";
642  }
643 
654  public function tryCreatingRevisionRecord( $row, $title = null ) {
655  if ( $row instanceof stdClass && isset( $row->rev_id )
656  && isset( $this->revisions[$row->rev_id] )
657  ) {
658  return $this->revisions[$row->rev_id];
659  } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
660  return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
661  } else {
662  return null;
663  }
664  }
665 
678  public function formatRow( $row ) {
679  $ret = '';
680  $classes = [];
681  $attribs = [];
682 
683  $linkRenderer = $this->getLinkRenderer();
684 
685  $page = null;
686  // Create a title for the revision if possible
687  // Rows from the hook may not include title information
688  if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
689  $page = Title::newFromRow( $row );
690  }
691  // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
692  // function to be called with a special object for $row. It expects us
693  // skip formatting so that the row can be formatted by the
694  // ContributionsLineEnding hook below.
695  // FIXME: have some better way for extensions to provide formatted rows.
696  $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
697  if ( $revRecord ) {
698  $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
699  $attribs['data-mw-revid'] = $revRecord->getId();
700 
701  $link = $linkRenderer->makeLink(
702  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
703  $page,
704  $page->getPrefixedText(),
705  [ 'class' => 'mw-contributions-title' ],
706  $page->isRedirect() ? [ 'redirect' => 'no' ] : []
707  );
708  # Mark current revisions
709  $topmarktext = '';
710  $user = $this->getUser();
711 
712  if ( $row->rev_id === $row->page_latest ) {
713  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
714  $classes[] = 'mw-contributions-current';
715  # Add rollback link
716  if ( !$row->page_is_new &&
717  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
718  $this->getAuthority()->probablyCan( 'rollback', $page ) &&
719  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
720  $this->getAuthority()->probablyCan( 'edit', $page )
721  ) {
722  $this->setPreventClickjacking( true );
723  $topmarktext .= ' ' . Linker::generateRollback(
724  $revRecord,
725  $this->getContext(),
726  [ 'noBrackets' ]
727  );
728  }
729  }
730  # Is there a visible previous revision?
731  if ( $revRecord->getParentId() !== 0 &&
732  $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
733  ) {
734  $difftext = $linkRenderer->makeKnownLink(
735  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
736  $page,
737  new HtmlArmor( $this->messages['diff'] ),
738  [ 'class' => 'mw-changeslist-diff' ],
739  [
740  'diff' => 'prev',
741  'oldid' => $row->rev_id
742  ]
743  );
744  } else {
745  $difftext = $this->messages['diff'];
746  }
747  $histlink = $linkRenderer->makeKnownLink(
748  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
749  $page,
750  new HtmlArmor( $this->messages['hist'] ),
751  [ 'class' => 'mw-changeslist-history' ],
752  [ 'action' => 'history' ]
753  );
754 
755  if ( $row->rev_parent_id === null ) {
756  // For some reason rev_parent_id isn't populated for this row.
757  // Its rumoured this is true on wikipedia for some revisions (T36922).
758  // Next best thing is to have the total number of bytes.
759  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
760  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
761  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
762  } else {
763  $parentLen = 0;
764  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
765  $parentLen = $this->mParentLens[$row->rev_parent_id];
766  }
767 
768  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
770  $parentLen,
771  $row->rev_len,
772  $this->getContext()
773  );
774  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
775  }
776 
777  $lang = $this->getLanguage();
778 
779  $comment = $this->formattedComments[$row->rev_id];
780 
781  if ( $comment === '' ) {
782  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
783  $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
784  }
785 
786  $comment = $lang->getDirMark() . $comment;
787 
788  $d = ChangesList::revDateLink( $revRecord, $user, $lang, $page );
789 
790  # When querying for an IP range, we want to always show user and user talk links.
791  $userlink = '';
792  $revUser = $revRecord->getUser();
793  $revUserId = $revUser ? $revUser->getId() : 0;
794  $revUserText = $revUser ? $revUser->getName() : '';
795  if ( $this->isQueryableRange( $this->target ) ) {
796  $userlink = ' <span class="mw-changeslist-separator"></span> '
797  . $lang->getDirMark()
798  . Linker::userLink( $revUserId, $revUserText );
799  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
800  Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
801  }
802 
803  $flags = [];
804  if ( $revRecord->getParentId() === 0 ) {
805  $flags[] = ChangesList::flag( 'newpage' );
806  }
807 
808  if ( $revRecord->isMinor() ) {
809  $flags[] = ChangesList::flag( 'minor' );
810  }
811 
812  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
813  $del = Linker::getRevDeleteLink( $user, $revRecord, $page );
814  if ( $del !== '' ) {
815  $del .= ' ';
816  }
817 
818  // While it might be tempting to use a list here
819  // this would result in clutter and slows down navigating the content
820  // in assistive technology.
821  // See https://phabricator.wikimedia.org/T205581#4734812
822  $diffHistLinks = Html::rawElement( 'span',
823  [ 'class' => 'mw-changeslist-links' ],
824  // The spans are needed to ensure the dividing '|' elements are not
825  // themselves styled as links.
826  Html::rawElement( 'span', [], $difftext ) .
827  ' ' . // Space needed for separating two words.
828  Html::rawElement( 'span', [], $histlink )
829  );
830 
831  # Tags, if any.
832  list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
833  $row->ts_tags,
834  'contributions',
835  $this->getContext()
836  );
837  $classes = array_merge( $classes, $newClasses );
838 
839  $this->hookRunner->onSpecialContributions__formatRow__flags(
840  $this->getContext(), $row, $flags );
841 
842  $templateParams = [
843  'del' => $del,
844  'timestamp' => $d,
845  'diffHistLinks' => $diffHistLinks,
846  'charDifference' => $chardiff,
847  'flags' => $flags,
848  'articleLink' => $link,
849  'userlink' => $userlink,
850  'logText' => $comment,
851  'topmarktext' => $topmarktext,
852  'tagSummary' => $tagSummary,
853  ];
854 
855  # Denote if username is redacted for this edit
856  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
857  $templateParams['rev-deleted-user-contribs'] =
858  $this->msg( 'rev-deleted-user-contribs' )->escaped();
859  }
860 
861  $ret = $this->templateParser->processTemplate(
862  'SpecialContributionsLine',
863  $templateParams
864  );
865  }
866 
867  // Let extensions add data
868  $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
869  $attribs = array_filter( $attribs,
870  [ Sanitizer::class, 'isReservedDataAttribute' ],
871  ARRAY_FILTER_USE_KEY
872  );
873 
874  // TODO: Handle exceptions in the catch block above. Do any extensions rely on
875  // receiving empty rows?
876 
877  if ( $classes === [] && $attribs === [] && $ret === '' ) {
878  wfDebug( "Dropping Special:Contribution row that could not be formatted" );
879  return "<!-- Could not format Special:Contribution row. -->\n";
880  }
881  $attribs['class'] = $classes;
882 
883  // FIXME: The signature of the ContributionsLineEnding hook makes it
884  // very awkward to move this LI wrapper into the template.
885  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
886  }
887 
892  protected function getSqlComment() {
893  if ( $this->namespace || $this->deletedOnly ) {
894  // potentially slow, see CR r58153
895  return 'contributions page filtered for namespace or RevisionDeleted edits';
896  } else {
897  return 'contributions page unfiltered';
898  }
899  }
900 
904  protected function preventClickjacking() {
905  $this->setPreventClickjacking( true );
906  }
907 
912  protected function setPreventClickjacking( bool $enable ) {
913  $this->preventClickjacking = $enable;
914  }
915 
919  public function getPreventClickjacking() {
920  return $this->preventClickjacking;
921  }
922 
929  public static function processDateFilter( array $opts ) {
930  $start = $opts['start'] ?? '';
931  $end = $opts['end'] ?? '';
932  $year = $opts['year'] ?? '';
933  $month = $opts['month'] ?? '';
934 
935  if ( $start !== '' && $end !== '' && $start > $end ) {
936  $temp = $start;
937  $start = $end;
938  $end = $temp;
939  }
940 
941  // If year/month legacy filtering options are set, convert them to display the new stamp
942  if ( $year !== '' || $month !== '' ) {
943  // Reuse getDateCond logic, but subtract a day because
944  // the endpoints of our date range appear inclusive
945  // but the internal end offsets are always exclusive
946  $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
947  $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
948  $legacyDateTime = $legacyDateTime->modify( '-1 day' );
949 
950  // Clear the new timestamp range options if used and
951  // replace with the converted legacy timestamp
952  $start = '';
953  $end = $legacyDateTime->format( 'Y-m-d' );
954  }
955 
956  $opts['start'] = $start;
957  $opts['end'] = $end;
958 
959  return $opts;
960  }
961 }
getUser()
getAuthority()
const NS_USER_TALK
Definition: Defines.php:67
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
getContext()
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:900
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:182
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()
IContextSource $context
Pager for Special:Contributions.
UserIdentity $targetUser
RevisionStore $revisionStore
TemplateParser $templateParser
string $target
User name, or a string describing an IP address range.
ActorMigration $actorMigration
bool $revisionsOnly
Set to true to only include mediawiki revisions.
string int $namespace
A single namespace number, or an empty string for all namespaces.
string[] false $tagFilter
Name of tag to filter, or false to ignore tags.
static processDateFilter(array $opts)
Set up date filter options, given request data.
getTargetTable()
Return the table targeted for ordering and continuation.
string[] $messages
Local cache for escaped messages.
tryCreatingRevisionRecord( $row, $title=null)
If the object looks like a revision row, or corresponds to a previously cached revision,...
LinkBatchFactory $linkBatchFactory
bool $associated
Set to true to show both the subject and talk namespace, no matter which got selected.
bool $nsInvert
Set to true to invert the namespace selection.
NamespaceInfo $namespaceInfo
getQueryInfo()
Provides all parameters needed for the main paged query.
getNavigationBar()
Wrap the navigation bar in a p element with identifying class.
HookRunner $hookRunner
bool $topOnly
Set to true to show only latest (a.k.a.
__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.
string[] $formattedComments
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,...
RevisionRecord[] $revisions
Cached revisions by ID.
bool $deletedOnly
Set to true to show only deleted revisions.
CommentFormatter $commentFormatter
bool $newOnly
Set to true to show only new pages.
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)
getIpRangeConds( $db, $ip)
Get SQL conditions for an IP range, if applicable.
bool $hideMinor
Set to true to hide edits marked as minor by the user.
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
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
LinkRenderer $linkRenderer
Definition: IndexPager.php:167
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition: Linker.php:1096
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:2139
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1814
static formatRevisionSize( $size)
Definition: Linker.php:1603
static userTalkLink( $userId, $userText)
Definition: Linker.php:1234
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:562
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Service for looking up page revisions.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Pager for filtering by a range of dates.
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
getDateRangeCond( $startStamp, $endStamp)
Set and return a date range condition using timestamps provided by the user.
static getOffsetDate( $year, $month, $day=-1)
Core logic of determining the mOffset timestamp such that we can get all items with a timestamp up to...
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:572
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
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition: defines.php:25
if(!isset( $args[0])) $lang
$revQuery