MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
29 
30 class ChangeTags {
34  public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
39  public const TAG_NEW_REDIRECT = 'mw-new-redirect';
43  public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
47  public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
51  public const TAG_BLANK = 'mw-blank';
55  public const TAG_REPLACE = 'mw-replace';
63  public const TAG_ROLLBACK = 'mw-rollback';
70  public const TAG_UNDO = 'mw-undo';
76  public const TAG_MANUAL_REVERT = 'mw-manual-revert';
84  public const TAG_REVERTED = 'mw-reverted';
85 
90 
94  public const BYPASS_MAX_USAGE_CHECK = 1;
95 
101  private const MAX_DELETE_USES = 5000;
102 
106  private const DEFINED_SOFTWARE_TAGS = [
107  'mw-contentmodelchange',
108  'mw-new-redirect',
109  'mw-removed-redirect',
110  'mw-changed-redirect-target',
111  'mw-blank',
112  'mw-replace',
113  'mw-rollback',
114  'mw-undo',
115  'mw-manual-revert',
116  'mw-reverted',
117  ];
118 
129  public static $avoidReopeningTablesForTesting = false;
130 
138  public static function getSoftwareTags( $all = false ) {
139  global $wgSoftwareTags;
140  $softwareTags = [];
141 
142  if ( !is_array( $wgSoftwareTags ) ) {
143  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
144  Please refer to documentation for the list of tags you can enable' );
145  return $softwareTags;
146  }
147 
148  $availableSoftwareTags = !$all ?
149  array_keys( array_filter( $wgSoftwareTags ) ) :
150  array_keys( $wgSoftwareTags );
151 
152  $softwareTags = array_intersect(
153  $availableSoftwareTags,
154  self::DEFINED_SOFTWARE_TAGS
155  );
156 
157  return $softwareTags;
158  }
159 
174  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
175  if ( !$tags ) {
176  return [ '', [] ];
177  }
178  if ( !$context ) {
179  $context = RequestContext::getMain();
180  }
181 
182  $classes = [];
183 
184  $tags = explode( ',', $tags );
185  $displayTags = [];
186  foreach ( $tags as $tag ) {
187  if ( !$tag ) {
188  continue;
189  }
190  $description = self::tagDescription( $tag, $context );
191  if ( $description === false ) {
192  continue;
193  }
194  $displayTags[] = Xml::tags(
195  'span',
196  [ 'class' => 'mw-tag-marker ' .
197  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
198  $description
199  );
200  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
201  }
202 
203  if ( !$displayTags ) {
204  return [ '', [] ];
205  }
206 
207  $markers = $context->msg( 'tag-list-wrapper' )
208  ->numParams( count( $displayTags ) )
209  ->rawParams( implode( ' ', $displayTags ) )
210  ->parse();
211  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
212 
213  return [ $markers, $classes ];
214  }
215 
229  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
230  $msg = $context->msg( "tag-$tag" );
231  if ( !$msg->exists() ) {
232  // No such message
233  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
234  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
235  // so extract the language from $msg and use that.
236  // The language doesn't really matter, but we need to set it to avoid requesting
237  // the user's language from session-less entry points (T227233)
238  ->inLanguage( $msg->getLanguage() );
239 
240  }
241  if ( $msg->isDisabled() ) {
242  // The message exists but is disabled, hide the tag.
243  return false;
244  }
245 
246  // Message exists and isn't disabled, use it.
247  return $msg;
248  }
249 
263  public static function tagDescription( $tag, MessageLocalizer $context ) {
264  $msg = self::tagShortDescriptionMessage( $tag, $context );
265  return $msg ? $msg->parse() : false;
266  }
267 
280  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
281  $msg = $context->msg( "tag-$tag-description" );
282  if ( !$msg->exists() ) {
283  return false;
284  }
285  if ( $msg->isDisabled() ) {
286  // The message exists but is disabled, hide the description.
287  return false;
288  }
289 
290  // Message exists and isn't disabled, use it.
291  return $msg;
292  }
293 
308  public static function addTags( $tags, $rc_id = null, $rev_id = null,
309  $log_id = null, $params = null, RecentChange $rc = null
310  ) {
311  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
312  return (bool)$result[0];
313  }
314 
345  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
346  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
347  User $user = null
348  ) {
349  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
350  $tagsToRemove = array_filter( (array)$tagsToRemove );
351 
352  if ( !$rc_id && !$rev_id && !$log_id ) {
353  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
354  'specified when adding or removing a tag from a change!' );
355  }
356 
357  $dbw = wfGetDB( DB_MASTER );
358 
359  // Might as well look for rcids and so on.
360  if ( !$rc_id ) {
361  // Info might be out of date, somewhat fractionally, on replica DB.
362  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
363  // so use that relation to avoid full table scans.
364  if ( $log_id ) {
365  $rc_id = $dbw->selectField(
366  [ 'logging', 'recentchanges' ],
367  'rc_id',
368  [
369  'log_id' => $log_id,
370  'rc_timestamp = log_timestamp',
371  'rc_logid = log_id'
372  ],
373  __METHOD__
374  );
375  } elseif ( $rev_id ) {
376  $rc_id = $dbw->selectField(
377  [ 'revision', 'recentchanges' ],
378  'rc_id',
379  [
380  'rev_id' => $rev_id,
381  'rc_this_oldid = rev_id'
382  ],
383  __METHOD__
384  );
385  }
386  } elseif ( !$log_id && !$rev_id ) {
387  // Info might be out of date, somewhat fractionally, on replica DB.
388  $log_id = $dbw->selectField(
389  'recentchanges',
390  'rc_logid',
391  [ 'rc_id' => $rc_id ],
392  __METHOD__
393  );
394  $rev_id = $dbw->selectField(
395  'recentchanges',
396  'rc_this_oldid',
397  [ 'rc_id' => $rc_id ],
398  __METHOD__
399  );
400  }
401 
402  if ( $log_id && !$rev_id ) {
403  $rev_id = $dbw->selectField(
404  'log_search',
405  'ls_value',
406  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
407  __METHOD__
408  );
409  } elseif ( !$log_id && $rev_id ) {
410  $log_id = $dbw->selectField(
411  'log_search',
412  'ls_log_id',
413  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
414  __METHOD__
415  );
416  }
417 
418  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
419 
420  // add tags
421  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
422  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
423 
424  // remove tags
425  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
426  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
427 
428  sort( $prevTags );
429  sort( $newTags );
430  if ( $prevTags == $newTags ) {
431  return [ [], [], $prevTags ];
432  }
433 
434  // insert a row into change_tag for each new tag
435  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
436  if ( count( $tagsToAdd ) ) {
437  $changeTagMapping = [];
438  foreach ( $tagsToAdd as $tag ) {
439  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
440  }
441  $fname = __METHOD__;
442  // T207881: update the counts at the end of the transaction
443  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) {
444  $dbw->update(
445  'change_tag_def',
446  [ 'ctd_count = ctd_count + 1' ],
447  [ 'ctd_name' => $tagsToAdd ],
448  $fname
449  );
450  }, $fname );
451 
452  $tagsRows = [];
453  foreach ( $tagsToAdd as $tag ) {
454  // Filter so we don't insert NULLs as zero accidentally.
455  // Keep in mind that $rc_id === null means "I don't care/know about the
456  // rc_id, just delete $tag on this revision/log entry". It doesn't
457  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
458  $tagsRows[] = array_filter(
459  [
460  'ct_rc_id' => $rc_id,
461  'ct_log_id' => $log_id,
462  'ct_rev_id' => $rev_id,
463  'ct_params' => $params,
464  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
465  ]
466  );
467 
468  }
469 
470  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
471  }
472 
473  // delete from change_tag
474  if ( count( $tagsToRemove ) ) {
475  $fname = __METHOD__;
476  foreach ( $tagsToRemove as $tag ) {
477  $conds = array_filter(
478  [
479  'ct_rc_id' => $rc_id,
480  'ct_log_id' => $log_id,
481  'ct_rev_id' => $rev_id,
482  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
483  ]
484  );
485  $dbw->delete( 'change_tag', $conds, __METHOD__ );
486  if ( $dbw->affectedRows() ) {
487  // T207881: update the counts at the end of the transaction
488  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) {
489  $dbw->update(
490  'change_tag_def',
491  [ 'ctd_count = ctd_count - 1' ],
492  [ 'ctd_name' => $tag ],
493  $fname
494  );
495 
496  $dbw->delete(
497  'change_tag_def',
498  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
499  $fname
500  );
501  }, $fname );
502  }
503  }
504  }
505 
506  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
507  $rc_id, $rev_id, $log_id, $params, $rc, $user );
508 
509  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
510  }
511 
523  public static function getTagsWithData(
524  IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
525  ) {
526  $conds = array_filter(
527  [
528  'ct_rc_id' => $rc_id,
529  'ct_rev_id' => $rev_id,
530  'ct_log_id' => $log_id,
531  ]
532  );
533 
534  $result = $db->select(
535  'change_tag',
536  [ 'ct_tag_id', 'ct_params' ],
537  $conds,
538  __METHOD__
539  );
540 
541  $tags = [];
542  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
543  foreach ( $result as $row ) {
544  $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
545  $tags[$tagName] = $row->ct_params;
546  }
547 
548  return $tags;
549  }
550 
561  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
562  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
563  }
564 
575  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
576  $lang = RequestContext::getMain()->getLanguage();
577  $tags = array_values( $tags );
578  $count = count( $tags );
579  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
580  $lang->commaList( $tags ), $count );
581  $status->value = $tags;
582  return $status;
583  }
584 
598  public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
599  if ( $user !== null ) {
600  if ( !MediaWikiServices::getInstance()->getPermissionManager()
601  ->userHasRight( $user, 'applychangetags' )
602  ) {
603  return Status::newFatal( 'tags-apply-no-permission' );
604  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
605  return Status::newFatal( 'tags-apply-blocked', $user->getName() );
606  }
607  }
608 
609  // to be applied, a tag has to be explicitly defined
610  $allowedTags = self::listExplicitlyDefinedTags();
611  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
612  $disallowedTags = array_diff( $tags, $allowedTags );
613  if ( $disallowedTags ) {
614  return self::restrictedTagError( 'tags-apply-not-allowed-one',
615  'tags-apply-not-allowed-multi', $disallowedTags );
616  }
617 
618  return Status::newGood();
619  }
620 
641  public static function addTagsAccompanyingChangeWithChecks(
642  array $tags, $rc_id, $rev_id, $log_id, $params, User $user
643  ) {
644  // are we allowed to do this?
645  $result = self::canAddTagsAccompanyingChange( $tags, $user );
646  if ( !$result->isOK() ) {
647  $result->value = null;
648  return $result;
649  }
650 
651  // do it!
652  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
653 
654  return Status::newGood( true );
655  }
656 
671  public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
672  User $user = null
673  ) {
674  if ( $user !== null ) {
675  if ( !MediaWikiServices::getInstance()->getPermissionManager()
676  ->userHasRight( $user, 'changetags' )
677  ) {
678  return Status::newFatal( 'tags-update-no-permission' );
679  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
680  return Status::newFatal( 'tags-update-blocked', $user->getName() );
681  }
682  }
683 
684  if ( $tagsToAdd ) {
685  // to be added, a tag has to be explicitly defined
686  // @todo Allow extensions to define tags that can be applied by users...
687  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
688  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
689  if ( $diff ) {
690  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
691  'tags-update-add-not-allowed-multi', $diff );
692  }
693  }
694 
695  if ( $tagsToRemove ) {
696  // to be removed, a tag must not be defined by an extension, or equivalently it
697  // has to be either explicitly defined or not defined at all
698  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
699  $softwareDefinedTags = self::listSoftwareDefinedTags();
700  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
701  if ( $intersect ) {
702  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
703  'tags-update-remove-not-allowed-multi', $intersect );
704  }
705  }
706 
707  return Status::newGood();
708  }
709 
740  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
741  $rc_id, $rev_id, $log_id, $params, $reason, User $user
742  ) {
743  if ( $tagsToAdd === null ) {
744  $tagsToAdd = [];
745  }
746  if ( $tagsToRemove === null ) {
747  $tagsToRemove = [];
748  }
749  if ( !$tagsToAdd && !$tagsToRemove ) {
750  // no-op, don't bother
751  return Status::newGood( (object)[
752  'logId' => null,
753  'addedTags' => [],
754  'removedTags' => [],
755  ] );
756  }
757 
758  // are we allowed to do this?
759  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
760  if ( !$result->isOK() ) {
761  $result->value = null;
762  return $result;
763  }
764 
765  // basic rate limiting
766  if ( $user->pingLimiter( 'changetag' ) ) {
767  return Status::newFatal( 'actionthrottledtext' );
768  }
769 
770  // do it!
771  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
772  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
773  if ( !$tagsAdded && !$tagsRemoved ) {
774  // no-op, don't log it
775  return Status::newGood( (object)[
776  'logId' => null,
777  'addedTags' => [],
778  'removedTags' => [],
779  ] );
780  }
781 
782  // log it
783  $logEntry = new ManualLogEntry( 'tag', 'update' );
784  $logEntry->setPerformer( $user );
785  $logEntry->setComment( $reason );
786 
787  // find the appropriate target page
788  if ( $rev_id ) {
789  $revisionRecord = MediaWikiServices::getInstance()
790  ->getRevisionLookup()
791  ->getRevisionById( $rev_id );
792  if ( $revisionRecord ) {
793  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
794  }
795  } elseif ( $log_id ) {
796  // This function is from revision deletion logic and has nothing to do with
797  // change tags, but it appears to be the only other place in core where we
798  // perform logged actions on log items.
799  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
800  }
801 
802  if ( !$logEntry->getTarget() ) {
803  // target is required, so we have to set something
804  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
805  }
806 
807  $logParams = [
808  '4::revid' => $rev_id,
809  '5::logid' => $log_id,
810  '6:list:tagsAdded' => $tagsAdded,
811  '7:number:tagsAddedCount' => count( $tagsAdded ),
812  '8:list:tagsRemoved' => $tagsRemoved,
813  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
814  'initialTags' => $initialTags,
815  ];
816  $logEntry->setParameters( $logParams );
817  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
818 
819  $dbw = wfGetDB( DB_MASTER );
820  $logId = $logEntry->insert( $dbw );
821  // Only send this to UDP, not RC, similar to patrol events
822  $logEntry->publish( $logId, 'udp' );
823 
824  return Status::newGood( (object)[
825  'logId' => $logId,
826  'addedTags' => $tagsAdded,
827  'removedTags' => $tagsRemoved,
828  ] );
829  }
830 
851  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
852  &$join_conds, &$options, $filter_tag = ''
853  ) {
854  global $wgUseTagFilter;
855 
856  // Normalize to arrays
857  $tables = (array)$tables;
858  $fields = (array)$fields;
859  $conds = (array)$conds;
860  $options = (array)$options;
861 
862  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
863 
864  // Figure out which ID field to use
865  if ( in_array( 'recentchanges', $tables ) ) {
866  $join_cond = 'ct_rc_id=rc_id';
867  } elseif ( in_array( 'logging', $tables ) ) {
868  $join_cond = 'ct_log_id=log_id';
869  } elseif ( in_array( 'revision', $tables ) ) {
870  $join_cond = 'ct_rev_id=rev_id';
871  } elseif ( in_array( 'archive', $tables ) ) {
872  $join_cond = 'ct_rev_id=ar_rev_id';
873  } else {
874  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
875  }
876 
877  if ( $wgUseTagFilter && $filter_tag ) {
878  // Somebody wants to filter on a tag.
879  // Add an INNER JOIN on change_tag
880 
881  $tagTable = 'change_tag';
882  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
883  $db = wfGetDB( DB_REPLICA );
884 
885  if ( $db->getType() === 'mysql' ) {
886  // When filtering by tag, we are using the change_tag table twice:
887  // Once in a join for filtering, and once in a sub-query to list all
888  // tags for each revision. This does not work with temporary tables
889  // on some versions of MySQL, which causes phpunit tests to fail.
890  // As a hacky workaround, we copy the temporary table, and join
891  // against the copy. It is acknowledge that this is quite horrific.
892  // Discuss at T256006.
893 
894  $tagTable = 'change_tag_for_display_query';
895  $db->query(
896  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
897  . ' LIKE ' . $db->tableName( 'change_tag' )
898  );
899  $db->query(
900  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
901  . ' SELECT * FROM ' . $db->tableName( 'change_tag' )
902  );
903  }
904  }
905 
906  $tables[] = $tagTable;
907  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
908  $filterTagIds = [];
909  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
910  foreach ( (array)$filter_tag as $filterTagName ) {
911  try {
912  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
913  } catch ( NameTableAccessException $exception ) {
914  // Return nothing.
915  $conds[] = '0=1';
916  break;
917  }
918  }
919 
920  if ( $filterTagIds !== [] ) {
921  $conds['ct_tag_id'] = $filterTagIds;
922  }
923 
924  if (
925  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
926  !in_array( 'DISTINCT', $options )
927  ) {
928  $options[] = 'DISTINCT';
929  }
930  }
931  }
932 
941  public static function makeTagSummarySubquery( $tables ) {
942  // Normalize to arrays
943  $tables = (array)$tables;
944 
945  // Figure out which ID field to use
946  if ( in_array( 'recentchanges', $tables ) ) {
947  $join_cond = 'ct_rc_id=rc_id';
948  } elseif ( in_array( 'logging', $tables ) ) {
949  $join_cond = 'ct_log_id=log_id';
950  } elseif ( in_array( 'revision', $tables ) ) {
951  $join_cond = 'ct_rev_id=rev_id';
952  } elseif ( in_array( 'archive', $tables ) ) {
953  $join_cond = 'ct_rev_id=ar_rev_id';
954  } else {
955  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
956  }
957 
958  $tagTables = [ 'change_tag', 'change_tag_def' ];
959  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
960  $field = 'ctd_name';
961 
962  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
963  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
964  );
965  }
966 
978  public static function buildTagFilterSelector(
979  $selected = '', $ooui = false, IContextSource $context = null
980  ) {
981  if ( !$context ) {
982  $context = RequestContext::getMain();
983  }
984 
985  $config = $context->getConfig();
986  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
987  return [];
988  }
989 
990  $data = [
992  'label',
993  [ 'for' => 'tagfilter' ],
994  $context->msg( 'tag-filter' )->parse()
995  )
996  ];
997 
998  if ( $ooui ) {
999  $data[] = new OOUI\TextInputWidget( [
1000  'id' => 'tagfilter',
1001  'name' => 'tagfilter',
1002  'value' => $selected,
1003  'classes' => 'mw-tagfilter-input',
1004  ] );
1005  } else {
1006  $data[] = Xml::input(
1007  'tagfilter',
1008  20,
1009  $selected,
1010  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
1011  );
1012  }
1013 
1014  return $data;
1015  }
1016 
1025  public static function defineTag( $tag ) {
1026  $dbw = wfGetDB( DB_MASTER );
1027  $tagDef = [
1028  'ctd_name' => $tag,
1029  'ctd_user_defined' => 1,
1030  'ctd_count' => 0
1031  ];
1032  $dbw->upsert(
1033  'change_tag_def',
1034  $tagDef,
1035  'ctd_name',
1036  [ 'ctd_user_defined' => 1 ],
1037  __METHOD__
1038  );
1039 
1040  // clear the memcache of defined tags
1042  }
1043 
1052  public static function undefineTag( $tag ) {
1053  $dbw = wfGetDB( DB_MASTER );
1054 
1055  $dbw->update(
1056  'change_tag_def',
1057  [ 'ctd_user_defined' => 0 ],
1058  [ 'ctd_name' => $tag ],
1059  __METHOD__
1060  );
1061 
1062  $dbw->delete(
1063  'change_tag_def',
1064  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1065  __METHOD__
1066  );
1067 
1068  // clear the memcache of defined tags
1070  }
1071 
1086  protected static function logTagManagementAction( $action, $tag, $reason,
1087  User $user, $tagCount = null, array $logEntryTags = []
1088  ) {
1089  $dbw = wfGetDB( DB_MASTER );
1090 
1091  $logEntry = new ManualLogEntry( 'managetags', $action );
1092  $logEntry->setPerformer( $user );
1093  // target page is not relevant, but it has to be set, so we just put in
1094  // the title of Special:Tags
1095  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1096  $logEntry->setComment( $reason );
1097 
1098  $params = [ '4::tag' => $tag ];
1099  if ( $tagCount !== null ) {
1100  $params['5:number:count'] = $tagCount;
1101  }
1102  $logEntry->setParameters( $params );
1103  $logEntry->setRelations( [ 'Tag' => $tag ] );
1104  $logEntry->addTags( $logEntryTags );
1105 
1106  $logId = $logEntry->insert( $dbw );
1107  $logEntry->publish( $logId );
1108  return $logId;
1109  }
1110 
1120  public static function canActivateTag( $tag, User $user = null ) {
1121  if ( $user !== null ) {
1122  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1123  ->userHasRight( $user, 'managechangetags' )
1124  ) {
1125  return Status::newFatal( 'tags-manage-no-permission' );
1126  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1127  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1128  }
1129  }
1130 
1131  // defined tags cannot be activated (a defined tag is either extension-
1132  // defined, in which case the extension chooses whether or not to active it;
1133  // or user-defined, in which case it is considered active)
1134  $definedTags = self::listDefinedTags();
1135  if ( in_array( $tag, $definedTags ) ) {
1136  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1137  }
1138 
1139  // non-existing tags cannot be activated
1140  $tagUsage = self::tagUsageStatistics();
1141  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1142  return Status::newFatal( 'tags-activate-not-found', $tag );
1143  }
1144 
1145  return Status::newGood();
1146  }
1147 
1165  public static function activateTagWithChecks( $tag, $reason, User $user,
1166  $ignoreWarnings = false, array $logEntryTags = []
1167  ) {
1168  // are we allowed to do this?
1169  $result = self::canActivateTag( $tag, $user );
1170  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1171  $result->value = null;
1172  return $result;
1173  }
1174 
1175  // do it!
1176  self::defineTag( $tag );
1177 
1178  // log it
1179  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
1180  null, $logEntryTags );
1181 
1182  return Status::newGood( $logId );
1183  }
1184 
1194  public static function canDeactivateTag( $tag, User $user = null ) {
1195  if ( $user !== null ) {
1196  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1197  ->userHasRight( $user, 'managechangetags' )
1198  ) {
1199  return Status::newFatal( 'tags-manage-no-permission' );
1200  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1201  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1202  }
1203  }
1204 
1205  // only explicitly-defined tags can be deactivated
1206  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1207  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1208  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1209  }
1210  return Status::newGood();
1211  }
1212 
1230  public static function deactivateTagWithChecks( $tag, $reason, User $user,
1231  $ignoreWarnings = false, array $logEntryTags = []
1232  ) {
1233  // are we allowed to do this?
1234  $result = self::canDeactivateTag( $tag, $user );
1235  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1236  $result->value = null;
1237  return $result;
1238  }
1239 
1240  // do it!
1241  self::undefineTag( $tag );
1242 
1243  // log it
1244  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
1245  null, $logEntryTags );
1246 
1247  return Status::newGood( $logId );
1248  }
1249 
1257  public static function isTagNameValid( $tag ) {
1258  // no empty tags
1259  if ( $tag === '' ) {
1260  return Status::newFatal( 'tags-create-no-name' );
1261  }
1262 
1263  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1264  // pipe (used as a delimiter between multiple tags in
1265  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1266  // MediaWiki namespace)
1267  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1268  || strpos( $tag, '/' ) !== false ) {
1269  return Status::newFatal( 'tags-create-invalid-chars' );
1270  }
1271 
1272  // could the MediaWiki namespace description messages be created?
1273  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1274  if ( $title === null ) {
1275  return Status::newFatal( 'tags-create-invalid-title-chars' );
1276  }
1277 
1278  return Status::newGood();
1279  }
1280 
1293  public static function canCreateTag( $tag, User $user = null ) {
1294  if ( $user !== null ) {
1295  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1296  ->userHasRight( $user, 'managechangetags' )
1297  ) {
1298  return Status::newFatal( 'tags-manage-no-permission' );
1299  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1300  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1301  }
1302  }
1303 
1304  $status = self::isTagNameValid( $tag );
1305  if ( !$status->isGood() ) {
1306  return $status;
1307  }
1308 
1309  // does the tag already exist?
1310  $tagUsage = self::tagUsageStatistics();
1311  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1312  return Status::newFatal( 'tags-create-already-exists', $tag );
1313  }
1314 
1315  // check with hooks
1316  $canCreateResult = Status::newGood();
1317  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1318  return $canCreateResult;
1319  }
1320 
1340  public static function createTagWithChecks( $tag, $reason, User $user,
1341  $ignoreWarnings = false, array $logEntryTags = []
1342  ) {
1343  // are we allowed to do this?
1344  $result = self::canCreateTag( $tag, $user );
1345  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1346  $result->value = null;
1347  return $result;
1348  }
1349 
1350  // do it!
1351  self::defineTag( $tag );
1352 
1353  // log it
1354  $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1355  null, $logEntryTags );
1356 
1357  return Status::newGood( $logId );
1358  }
1359 
1372  public static function deleteTagEverywhere( $tag ) {
1373  $dbw = wfGetDB( DB_MASTER );
1374  $dbw->startAtomic( __METHOD__ );
1375 
1376  // fetch tag id, this must be done before calling undefineTag(), see T225564
1377  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1378 
1379  // set ctd_user_defined = 0
1380  self::undefineTag( $tag );
1381 
1382  // delete from change_tag
1383  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1384  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1385  $dbw->endAtomic( __METHOD__ );
1386 
1387  // give extensions a chance
1388  $status = Status::newGood();
1389  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1390  // let's not allow error results, as the actual tag deletion succeeded
1391  if ( !$status->isOK() ) {
1392  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1393  $status->setOK( true );
1394  }
1395 
1396  // clear the memcache of defined tags
1398 
1399  return $status;
1400  }
1401 
1414  public static function canDeleteTag( $tag, User $user = null, int $flags = 0 ) {
1415  $tagUsage = self::tagUsageStatistics();
1416 
1417  if ( $user !== null ) {
1418  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1419  ->userHasRight( $user, 'deletechangetags' )
1420  ) {
1421  return Status::newFatal( 'tags-delete-no-permission' );
1422  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1423  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1424  }
1425  }
1426 
1427  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1428  return Status::newFatal( 'tags-delete-not-found', $tag );
1429  }
1430 
1431  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1432  isset( $tagUsage[$tag] ) &&
1433  $tagUsage[$tag] > self::MAX_DELETE_USES
1434  ) {
1435  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1436  }
1437 
1438  $softwareDefined = self::listSoftwareDefinedTags();
1439  if ( in_array( $tag, $softwareDefined ) ) {
1440  // extension-defined tags can't be deleted unless the extension
1441  // specifically allows it
1442  $status = Status::newFatal( 'tags-delete-not-allowed' );
1443  } else {
1444  // user-defined tags are deletable unless otherwise specified
1445  $status = Status::newGood();
1446  }
1447 
1448  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1449  return $status;
1450  }
1451 
1469  public static function deleteTagWithChecks( $tag, $reason, User $user,
1470  $ignoreWarnings = false, array $logEntryTags = []
1471  ) {
1472  // are we allowed to do this?
1473  $result = self::canDeleteTag( $tag, $user );
1474  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1475  $result->value = null;
1476  return $result;
1477  }
1478 
1479  // store the tag usage statistics
1480  $tagUsage = self::tagUsageStatistics();
1481  $hitcount = $tagUsage[$tag] ?? 0;
1482 
1483  // do it!
1484  $deleteResult = self::deleteTagEverywhere( $tag );
1485  if ( !$deleteResult->isOK() ) {
1486  return $deleteResult;
1487  }
1488 
1489  // log it
1490  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1491  $hitcount, $logEntryTags );
1492 
1493  $deleteResult->value = $logId;
1494  return $deleteResult;
1495  }
1496 
1503  public static function listSoftwareActivatedTags() {
1504  // core active tags
1505  $tags = self::getSoftwareTags();
1506  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1507  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1508  return $tags;
1509  }
1510  $hookRunner = new HookRunner( $hookContainer );
1511  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1512  return $cache->getWithSetCallback(
1513  $cache->makeKey( 'active-tags' ),
1514  WANObjectCache::TTL_MINUTE * 5,
1515  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1516  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1517 
1518  // Ask extensions which tags they consider active
1519  $hookRunner->onChangeTagsListActive( $tags );
1520  return $tags;
1521  },
1522  [
1523  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1524  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1525  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1526  ]
1527  );
1528  }
1529 
1536  public static function listDefinedTags() {
1538  $tags2 = self::listSoftwareDefinedTags();
1539  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1540  }
1541 
1550  public static function listExplicitlyDefinedTags() {
1551  $fname = __METHOD__;
1552 
1553  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1554  return $cache->getWithSetCallback(
1555  $cache->makeKey( 'valid-tags-db' ),
1556  WANObjectCache::TTL_MINUTE * 5,
1557  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1558  $dbr = wfGetDB( DB_REPLICA );
1559 
1560  $setOpts += Database::getCacheSetOptions( $dbr );
1561 
1562  $tags = $dbr->selectFieldValues(
1563  'change_tag_def',
1564  'ctd_name',
1565  [ 'ctd_user_defined' => 1 ],
1566  $fname
1567  );
1568 
1569  return array_filter( array_unique( $tags ) );
1570  },
1571  [
1572  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1573  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1574  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1575  ]
1576  );
1577  }
1578 
1588  public static function listSoftwareDefinedTags() {
1589  // core defined tags
1590  $tags = self::getSoftwareTags( true );
1591  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1592  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1593  return $tags;
1594  }
1595  $hookRunner = new HookRunner( $hookContainer );
1596  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1597  return $cache->getWithSetCallback(
1598  $cache->makeKey( 'valid-tags-hook' ),
1599  WANObjectCache::TTL_MINUTE * 5,
1600  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1601  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1602 
1603  $hookRunner->onListDefinedTags( $tags );
1604  return array_filter( array_unique( $tags ) );
1605  },
1606  [
1607  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1608  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1609  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1610  ]
1611  );
1612  }
1613 
1619  public static function purgeTagCacheAll() {
1620  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1621 
1622  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1623  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1624  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1625  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1626 
1627  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1628  }
1629 
1636  public static function tagUsageStatistics() {
1637  $fname = __METHOD__;
1638 
1639  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1640  return $cache->getWithSetCallback(
1641  $cache->makeKey( 'tags-usage-statistics' ),
1642  WANObjectCache::TTL_MINUTE * 5,
1643  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1644  $dbr = wfGetDB( DB_REPLICA );
1645  $res = $dbr->select(
1646  'change_tag_def',
1647  [ 'ctd_name', 'ctd_count' ],
1648  [],
1649  $fname,
1650  [ 'ORDER BY' => 'ctd_count DESC' ]
1651  );
1652 
1653  $out = [];
1654  foreach ( $res as $row ) {
1655  $out[$row->ctd_name] = $row->ctd_count;
1656  }
1657 
1658  return $out;
1659  },
1660  [
1661  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1662  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1663  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1664  ]
1665  );
1666  }
1667 
1682  public static function showTagEditingUI( User $user ) {
1683  return MediaWikiServices::getInstance()->getPermissionManager()
1684  ->userHasRight( $user, 'changetags' ) &&
1685  (bool)self::listExplicitlyDefinedTags();
1686  }
1687 }
ChangeTags\REVERT_TAGS
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:89
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
ChangeTags\makeTagSummarySubquery
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:941
$wgUseTagFilter
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
Definition: DefaultSettings.php:7463
ChangeTags\DEFINED_SOFTWARE_TAGS
const DEFINED_SOFTWARE_TAGS
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:106
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:361
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:71
ChangeTags\purgeTagCacheAll
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
Definition: ChangeTags.php:1619
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:166
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
ChangeTags\TAG_REMOVED_REDIRECT
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
Definition: ChangeTags.php:43
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:74
ChangeTags\logTagManagementAction
static logTagManagementAction( $action, $tag, $reason, User $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
Definition: ChangeTags.php:1086
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:106
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:976
ChangeTags\showTagEditingUI
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
Definition: ChangeTags.php:1682
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:978
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1654
$res
$res
Definition: testCompression.php:57
ChangeTags\restrictedTagError
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:575
ChangeTags\isTagNameValid
static isTagNameValid( $tag)
Is the tag name valid?
Definition: ChangeTags.php:1257
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MessageLocalizer
Interface for localizing messages in MediaWiki.
Definition: MessageLocalizer.php:29
ChangeTags\BYPASS_MAX_USAGE_CHECK
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
Definition: ChangeTags.php:94
$dbr
$dbr
Definition: testCompression.php:54
ChangeTags\tagShortDescriptionMessage
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:229
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1588
ChangeTags\TAG_CONTENT_MODEL_CHANGE
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
Definition: ChangeTags.php:34
ChangeTags\deleteTagEverywhere
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
Definition: ChangeTags.php:1372
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:851
ChangeTags\getTags
static getTags(IDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID.
Definition: ChangeTags.php:561
MWException
MediaWiki exception.
Definition: MWException.php:29
ChangeTags\undefineTag
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
Definition: ChangeTags.php:1052
MessageLocalizer\msg
msg( $key,... $params)
This is the method for getting translated interface messages.
ChangeTags
Definition: ChangeTags.php:30
ChangeTags\TAG_CHANGED_REDIRECT_TARGET
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
Definition: ChangeTags.php:47
getPermissionManager
getPermissionManager()
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2466
ChangeTags\TAG_MANUAL_REVERT
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
Definition: ChangeTags.php:76
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1536
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:138
$title
$title
Definition: testCompression.php:38
ChangeTags\MAX_DELETE_USES
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
Definition: ChangeTags.php:101
ChangeTags\defineTag
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
Definition: ChangeTags.php:1025
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
$wgSoftwareTags
array $wgSoftwareTags
List of core tags to enable.
Definition: DefaultSettings.php:7481
DB_MASTER
const DB_MASTER
Definition: defines.php:26
ChangeTags\canActivateTag
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:1120
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:914
ChangeTags\TAG_BLANK
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
Definition: ChangeTags.php:51
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1550
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1104
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:132
ChangeTags\canDeleteTag
static canDeleteTag( $tag, User $user=null, int $flags=0)
Is it OK to allow the user to delete this tag?
Definition: ChangeTags.php:1414
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
ChangeTags\deactivateTagWithChecks
static deactivateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1230
ChangeTags\TAG_NEW_REDIRECT
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
Definition: ChangeTags.php:39
RevDelLogList\suggestTarget
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
Definition: RevDelLogList.php:74
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:454
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1503
ChangeTags\TAG_REPLACE
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
Definition: ChangeTags.php:55
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:55
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:598
$cache
$cache
Definition: mcc.php:33
ChangeTags\canCreateTag
static canCreateTag( $tag, User $user=null)
Is it OK to allow the user to create this tag?
Definition: ChangeTags.php:1293
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
ChangeTags\updateTags
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, User $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
Definition: ChangeTags.php:345
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
Wikimedia\Rdbms\IDatabase\select
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:43
ChangeTags\tagUsageStatistics
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
Definition: ChangeTags.php:1636
ChangeTags\TAG_REVERTED
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:84
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:280
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1080
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
ChangeTags\$avoidReopeningTablesForTesting
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:129
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
ChangeTags\TAG_ROLLBACK
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
Definition: ChangeTags.php:63
ChangeTags\addTagsAccompanyingChangeWithChecks
static addTagsAccompanyingChangeWithChecks(array $tags, $rc_id, $rev_id, $log_id, $params, User $user)
Adds tags to a given change, checking whether it is allowed first, but without adding a log entry.
Definition: ChangeTags.php:641
ChangeTags\createTagWithChecks
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
Definition: ChangeTags.php:1340
ChangeTags\canUpdateTags
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, User $user=null)
Is it OK to allow the user to adds and remove the given tags to/from a change?
Definition: ChangeTags.php:671
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:280
ChangeTags\deleteTagWithChecks
static deleteTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1469
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:174
ChangeTags\TAG_UNDO
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
Definition: ChangeTags.php:70
ChangeTags\updateTagsWithChecks
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $reason, User $user)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
Definition: ChangeTags.php:740
ChangeTags\getTagsWithData
static getTagsWithData(IDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID,...
Definition: ChangeTags.php:523
ChangeTags\addTags
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:308
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:263
ChangeTags\activateTagWithChecks
static activateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1165
ChangeTags\canDeactivateTag
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
Definition: ChangeTags.php:1194