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 
117  private $templateParser;
118 
120  private $actorMigration;
121 
123  private $commentFormatter;
124 
126  private $hookRunner;
127 
129  private $linkBatchFactory;
130 
132  private $namespaceInfo;
133 
135  private $revisionStore;
136 
138  private $formattedComments = [];
139 
141  private $revisions = [];
142 
157  public function __construct(
158  IContextSource $context,
159  array $options,
160  LinkRenderer $linkRenderer = null,
161  LinkBatchFactory $linkBatchFactory = null,
162  HookContainer $hookContainer = null,
163  ILoadBalancer $loadBalancer = null,
164  ActorMigration $actorMigration = null,
165  RevisionStore $revisionStore = null,
166  NamespaceInfo $namespaceInfo = null,
167  UserIdentity $targetUser = null,
168  CommentFormatter $commentFormatter = 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 
264  public function reallyDoQuery( $offset, $limit, $order ) {
265  list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
266  $offset,
267  $limit,
268  $order
269  );
270 
271  $options['MAX_EXECUTION_TIME'] =
272  $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
273  /*
274  * This hook will allow extensions to add in additional queries, so they can get their data
275  * in My Contributions as well. Extensions should append their results to the $data array.
276  *
277  * Extension queries have to implement the navbar requirement as well. They should
278  * - have a column aliased as $pager->getIndexField()
279  * - have LIMIT set
280  * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
281  * - have the ORDER BY specified based upon the details provided by the navbar
282  *
283  * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
284  *
285  * &$data: an array of results of all contribs queries
286  * $pager: the ContribsPager object hooked into
287  * $offset: see phpdoc above
288  * $limit: see phpdoc above
289  * $descending: see phpdoc above
290  */
291  $dbr = $this->getDatabase();
292  $data = [ $dbr->select(
293  $tables, $fields, $conds, $fname, $options, $join_conds
294  ) ];
295  if ( !$this->revisionsOnly ) {
296  $this->hookRunner->onContribsPager__reallyDoQuery(
297  $data, $this, $offset, $limit, $order );
298  }
299 
300  $result = [];
301 
302  // loop all results and collect them in an array
303  foreach ( $data as $query ) {
304  foreach ( $query as $i => $row ) {
305  // If the query results are in descending order, the indexes must also be in descending order
306  $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
307  // Left-pad with zeroes, because these values will be sorted as strings
308  $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT );
309  // use index column as key, allowing us to easily sort in PHP
310  $result[$row->{$this->getIndexField()} . "-$index"] = $row;
311  }
312  }
313 
314  // sort results
315  if ( $order === self::QUERY_ASCENDING ) {
316  ksort( $result );
317  } else {
318  krsort( $result );
319  }
320 
321  // enforce limit
322  $result = array_slice( $result, 0, $limit );
323 
324  // get rid of array keys
325  $result = array_values( $result );
326 
327  return new FakeResultWrapper( $result );
328  }
329 
339  private function getTargetTable() {
340  $dbr = $this->getDatabase();
341  $ipRangeConds = $this->targetUser->isRegistered()
342  ? null : $this->getIpRangeConds( $dbr, $this->target );
343  if ( $ipRangeConds ) {
344  return 'ip_changes';
345  }
346 
347  return 'revision';
348  }
349 
350  public function getQueryInfo() {
351  $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
352  $queryInfo = [
353  'tables' => $revQuery['tables'],
354  'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
355  'conds' => [],
356  'options' => [],
357  'join_conds' => $revQuery['joins'],
358  ];
359 
360  // WARNING: Keep this in sync with getTargetTable()!
361  $dbr = $this->getDatabase();
362  $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) : null;
363  if ( $ipRangeConds ) {
364  // Put ip_changes first (T284419)
365  array_unshift( $queryInfo['tables'], 'ip_changes' );
366  $queryInfo['join_conds']['revision'] = [
367  'JOIN', [ 'rev_id = ipc_rev_id' ]
368  ];
369  $queryInfo['conds'][] = $ipRangeConds;
370  } else {
371  // tables and joins are already handled by RevisionStore::getQueryInfo()
372  $conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $this->targetUser );
373  $queryInfo['conds'][] = $conds['conds'];
374  // Force the appropriate index to avoid bad query plans (T307295)
375  if ( isset( $conds['orconds']['newactor'] ) ) {
376  $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
377  }
378  }
379 
380  if ( $this->deletedOnly ) {
381  $queryInfo['conds'][] = 'rev_deleted != 0';
382  }
383 
384  if ( $this->topOnly ) {
385  $queryInfo['conds'][] = 'rev_id = page_latest';
386  }
387 
388  if ( $this->newOnly ) {
389  $queryInfo['conds'][] = 'rev_parent_id = 0';
390  }
391 
392  if ( $this->hideMinor ) {
393  $queryInfo['conds'][] = 'rev_minor_edit = 0';
394  }
395 
396  $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
397 
398  // Paranoia: avoid brute force searches (T19342)
399  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
400  $queryInfo['conds'][] = $dbr->bitAnd(
401  'rev_deleted', RevisionRecord::DELETED_USER
402  ) . ' = 0';
403  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
404  $queryInfo['conds'][] = $dbr->bitAnd(
405  'rev_deleted', RevisionRecord::SUPPRESSED_USER
406  ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
407  }
408 
409  // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
410  $indexField = $this->getIndexField();
411  if ( $indexField !== 'rev_timestamp' ) {
412  $queryInfo['fields'][] = $indexField;
413  }
414 
416  $queryInfo['tables'],
417  $queryInfo['fields'],
418  $queryInfo['conds'],
419  $queryInfo['join_conds'],
420  $queryInfo['options'],
421  $this->tagFilter
422  );
423 
424  $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
425 
426  return $queryInfo;
427  }
428 
429  protected function getNamespaceCond() {
430  if ( $this->namespace !== '' ) {
431  $dbr = $this->getDatabase();
432  $selectedNS = $dbr->addQuotes( $this->namespace );
433  $eq_op = $this->nsInvert ? '!=' : '=';
434  $bool_op = $this->nsInvert ? 'AND' : 'OR';
435 
436  if ( !$this->associated ) {
437  return [ "page_namespace $eq_op $selectedNS" ];
438  }
439 
440  $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
441 
442  return [
443  "page_namespace $eq_op $selectedNS " .
444  $bool_op .
445  " page_namespace $eq_op $associatedNS"
446  ];
447  }
448 
449  return [];
450  }
451 
458  private function getIpRangeConds( $db, $ip ) {
459  // First make sure it is a valid range and they are not outside the CIDR limit
460  if ( !$this->isQueryableRange( $ip ) ) {
461  return false;
462  }
463 
464  list( $start, $end ) = IPUtils::parseRange( $ip );
465 
466  return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
467  }
468 
476  public function isQueryableRange( $ipRange ) {
477  $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
478 
479  $bits = IPUtils::parseCIDR( $ipRange )[1];
480  if (
481  ( $bits === false ) ||
482  ( IPUtils::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
483  ( IPUtils::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
484  ) {
485  return false;
486  }
487 
488  return true;
489  }
490 
494  public function getIndexField() {
495  // The returned column is used for sorting and continuation, so we need to
496  // make sure to use the right denormalized column depending on which table is
497  // being targeted by the query to avoid bad query plans.
498  // See T200259, T204669, T220991, and T221380.
499  $target = $this->getTargetTable();
500  switch ( $target ) {
501  case 'revision':
502  return 'rev_timestamp';
503  case 'ip_changes':
504  return 'ipc_rev_timestamp';
505  default:
506  wfWarn(
507  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
508  );
509  return 'rev_timestamp';
510  }
511  }
512 
516  public function getTagFilter() {
517  return $this->tagFilter;
518  }
519 
523  public function getTarget() {
524  return $this->target;
525  }
526 
530  public function isNewOnly() {
531  return $this->newOnly;
532  }
533 
537  public function getNamespace() {
538  return $this->namespace;
539  }
540 
544  protected function getExtraSortFields() {
545  // The returned columns are used for sorting, so we need to make sure
546  // to use the right denormalized column depending on which table is
547  // being targeted by the query to avoid bad query plans.
548  // See T200259, T204669, T220991, and T221380.
549  $target = $this->getTargetTable();
550  switch ( $target ) {
551  case 'revision':
552  return [ 'rev_id' ];
553  case 'ip_changes':
554  return [ 'ipc_rev_id' ];
555  default:
556  wfWarn(
557  __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
558  );
559  return [ 'rev_id' ];
560  }
561  }
562 
563  protected function doBatchLookups() {
564  # Do a link batch query
565  $this->mResult->seek( 0 );
566  $parentRevIds = [];
567  $this->mParentLens = [];
568  $revisions = [];
569  $linkBatch = $this->linkBatchFactory->newLinkBatch();
570  $isIpRange = $this->isQueryableRange( $this->target );
571  # Give some pointers to make (last) links
572  foreach ( $this->mResult as $row ) {
573  if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
574  $parentRevIds[] = (int)$row->rev_parent_id;
575  }
576  if ( $this->revisionStore->isRevisionRow( $row ) ) {
577  $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
578  if ( $isIpRange ) {
579  // If this is an IP range, batch the IP's talk page
580  $linkBatch->add( NS_USER_TALK, $row->rev_user_text );
581  }
582  $linkBatch->add( $row->page_namespace, $row->page_title );
583  $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
584  }
585  }
586  # Fetch rev_len for revisions not already scanned above
587  $this->mParentLens += $this->revisionStore->getRevisionSizes(
588  array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
589  );
590  $linkBatch->execute();
591 
592  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
593  ->authority( $this->getAuthority() )
594  ->revisions( $revisions )
595  ->hideIfDeleted()
596  ->execute();
597 
598  # For performance, save the revision objects for later.
599  # The array is indexed by rev_id. doBatchLookups() may be called
600  # multiple times with different results, so merge the revisions array,
601  # ignoring any duplicates.
602  $this->revisions += $revisions;
603  }
604 
608  protected function getStartBody() {
609  return "<section class='mw-pager-body'>\n";
610  }
611 
615  protected function getEndBody() {
616  return "</section>\n";
617  }
618 
629  public function tryCreatingRevisionRecord( $row, $title = null ) {
630  if ( $row instanceof stdClass && isset( $row->rev_id )
631  && isset( $this->revisions[$row->rev_id] )
632  ) {
633  return $this->revisions[$row->rev_id];
634  } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
635  return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
636  } else {
637  return null;
638  }
639  }
640 
653  public function formatRow( $row ) {
654  $ret = '';
655  $classes = [];
656  $attribs = [];
657 
658  $linkRenderer = $this->getLinkRenderer();
659 
660  $page = null;
661  // Create a title for the revision if possible
662  // Rows from the hook may not include title information
663  if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
664  $page = Title::newFromRow( $row );
665  }
666  // Flow overrides the ContribsPager::reallyDoQuery hook, causing this
667  // function to be called with a special object for $row. It expects us
668  // skip formatting so that the row can be formatted by the
669  // ContributionsLineEnding hook below.
670  // FIXME: have some better way for extensions to provide formatted rows.
671  $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
672  if ( $revRecord ) {
673  $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
674  $attribs['data-mw-revid'] = $revRecord->getId();
675 
676  $link = $linkRenderer->makeLink(
677  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
678  $page,
679  $page->getPrefixedText(),
680  [ 'class' => 'mw-contributions-title' ],
681  $page->isRedirect() ? [ 'redirect' => 'no' ] : []
682  );
683  # Mark current revisions
684  $topmarktext = '';
685 
686  if ( $row->rev_id === $row->page_latest ) {
687  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
688  $classes[] = 'mw-contributions-current';
689  # Add rollback link
690  if ( !$row->page_is_new &&
691  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
692  $this->getAuthority()->probablyCan( 'rollback', $page ) &&
693  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
694  $this->getAuthority()->probablyCan( 'edit', $page )
695  ) {
696  $this->setPreventClickjacking( true );
697  $topmarktext .= ' ' . Linker::generateRollback(
698  $revRecord,
699  $this->getContext(),
700  [ 'noBrackets' ]
701  );
702  }
703  }
704  # Is there a visible previous revision?
705  if ( $revRecord->getParentId() !== 0 &&
706  $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
707  ) {
708  $difftext = $linkRenderer->makeKnownLink(
709  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
710  $page,
711  new HtmlArmor( $this->messages['diff'] ),
712  [ 'class' => 'mw-changeslist-diff' ],
713  [
714  'diff' => 'prev',
715  'oldid' => $row->rev_id
716  ]
717  );
718  } else {
719  $difftext = $this->messages['diff'];
720  }
721  $histlink = $linkRenderer->makeKnownLink(
722  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
723  $page,
724  new HtmlArmor( $this->messages['hist'] ),
725  [ 'class' => 'mw-changeslist-history' ],
726  [ 'action' => 'history' ]
727  );
728 
729  if ( $row->rev_parent_id === null ) {
730  // For some reason rev_parent_id isn't populated for this row.
731  // Its rumoured this is true on wikipedia for some revisions (T36922).
732  // Next best thing is to have the total number of bytes.
733  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
734  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
735  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
736  } else {
737  $parentLen = 0;
738  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
739  $parentLen = $this->mParentLens[$row->rev_parent_id];
740  }
741 
742  $chardiff = ' <span class="mw-changeslist-separator"></span> ';
744  $parentLen,
745  $row->rev_len,
746  $this->getContext()
747  );
748  $chardiff .= ' <span class="mw-changeslist-separator"></span> ';
749  }
750 
751  $lang = $this->getLanguage();
752 
753  $comment = $this->formattedComments[$row->rev_id];
754 
755  if ( $comment === '' ) {
756  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
757  $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
758  }
759 
760  $comment = $lang->getDirMark() . $comment;
761 
762  $authority = $this->getAuthority();
763  $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
764 
765  # When querying for an IP range, we want to always show user and user talk links.
766  $userlink = '';
767  $revUser = $revRecord->getUser();
768  $revUserId = $revUser ? $revUser->getId() : 0;
769  $revUserText = $revUser ? $revUser->getName() : '';
770  if ( $this->isQueryableRange( $this->target ) ) {
771  $userlink = ' <span class="mw-changeslist-separator"></span> '
772  . $lang->getDirMark()
773  . Linker::userLink( $revUserId, $revUserText );
774  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
775  Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' ';
776  }
777 
778  $flags = [];
779  if ( $revRecord->getParentId() === 0 ) {
780  $flags[] = ChangesList::flag( 'newpage' );
781  }
782 
783  if ( $revRecord->isMinor() ) {
784  $flags[] = ChangesList::flag( 'minor' );
785  }
786 
787  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
788  $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
789  if ( $del !== '' ) {
790  $del .= ' ';
791  }
792 
793  // While it might be tempting to use a list here
794  // this would result in clutter and slows down navigating the content
795  // in assistive technology.
796  // See https://phabricator.wikimedia.org/T205581#4734812
797  $diffHistLinks = Html::rawElement( 'span',
798  [ 'class' => 'mw-changeslist-links' ],
799  // The spans are needed to ensure the dividing '|' elements are not
800  // themselves styled as links.
801  Html::rawElement( 'span', [], $difftext ) .
802  ' ' . // Space needed for separating two words.
803  Html::rawElement( 'span', [], $histlink )
804  );
805 
806  # Tags, if any.
807  list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
808  $row->ts_tags,
809  'contributions',
810  $this->getContext()
811  );
812  $classes = array_merge( $classes, $newClasses );
813 
814  $this->hookRunner->onSpecialContributions__formatRow__flags(
815  $this->getContext(), $row, $flags );
816 
817  $templateParams = [
818  'del' => $del,
819  'timestamp' => $d,
820  'diffHistLinks' => $diffHistLinks,
821  'charDifference' => $chardiff,
822  'flags' => $flags,
823  'articleLink' => $link,
824  'userlink' => $userlink,
825  'logText' => $comment,
826  'topmarktext' => $topmarktext,
827  'tagSummary' => $tagSummary,
828  ];
829 
830  # Denote if username is redacted for this edit
831  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
832  $templateParams['rev-deleted-user-contribs'] =
833  $this->msg( 'rev-deleted-user-contribs' )->escaped();
834  }
835 
836  $ret = $this->templateParser->processTemplate(
837  'SpecialContributionsLine',
838  $templateParams
839  );
840  }
841 
842  // Let extensions add data
843  $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
844  $attribs = array_filter( $attribs,
845  [ Sanitizer::class, 'isReservedDataAttribute' ],
846  ARRAY_FILTER_USE_KEY
847  );
848 
849  // TODO: Handle exceptions in the catch block above. Do any extensions rely on
850  // receiving empty rows?
851 
852  if ( $classes === [] && $attribs === [] && $ret === '' ) {
853  wfDebug( "Dropping Special:Contribution row that could not be formatted" );
854  return "<!-- Could not format Special:Contribution row. -->\n";
855  }
856  $attribs['class'] = $classes;
857 
858  // FIXME: The signature of the ContributionsLineEnding hook makes it
859  // very awkward to move this LI wrapper into the template.
860  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
861  }
862 
867  protected function getSqlComment() {
868  if ( $this->namespace || $this->deletedOnly ) {
869  // potentially slow, see CR r58153
870  return 'contributions page filtered for namespace or RevisionDeleted edits';
871  } else {
872  return 'contributions page unfiltered';
873  }
874  }
875 
879  protected function preventClickjacking() {
880  $this->setPreventClickjacking( true );
881  }
882 
887  protected function setPreventClickjacking( bool $enable ) {
888  $this->preventClickjacking = $enable;
889  }
890 
894  public function getPreventClickjacking() {
895  return $this->preventClickjacking;
896  }
897 
904  public static function processDateFilter( array $opts ) {
905  $start = $opts['start'] ?? '';
906  $end = $opts['end'] ?? '';
907  $year = $opts['year'] ?? '';
908  $month = $opts['month'] ?? '';
909 
910  if ( $start !== '' && $end !== '' && $start > $end ) {
911  $temp = $start;
912  $start = $end;
913  $end = $temp;
914  }
915 
916  // If year/month legacy filtering options are set, convert them to display the new stamp
917  if ( $year !== '' || $month !== '' ) {
918  // Reuse getDateCond logic, but subtract a day because
919  // the endpoints of our date range appear inclusive
920  // but the internal end offsets are always exclusive
921  $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
922  $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
923  $legacyDateTime = $legacyDateTime->modify( '-1 day' );
924 
925  // Clear the new timestamp range options if used and
926  // replace with the converted legacy timestamp
927  $start = '';
928  $end = $legacyDateTime->format( 'Y-m-d' );
929  }
930 
931  $opts['start'] = $start;
932  $opts['end'] = $end;
933 
934  return $opts;
935  }
936 }
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
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:906
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:193
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:249
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition: Linker.php:1071
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:2109
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1790
static formatRevisionSize( $size)
Definition: Linker.php:1579
static userTalkLink( $userId, $userText)
Definition: Linker.php:1210
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:566
Class that generates HTML anchor link elements for pages.
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 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:573
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:39
Create and track the database connections and transactions 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