MediaWiki REL1_39
ChangeTags.php
Go to the documentation of this file.
1<?php
33
38 public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
43 public const TAG_NEW_REDIRECT = 'mw-new-redirect';
47 public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
51 public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
55 public const TAG_BLANK = 'mw-blank';
59 public const TAG_REPLACE = 'mw-replace';
67 public const TAG_ROLLBACK = 'mw-rollback';
74 public const TAG_UNDO = 'mw-undo';
80 public const TAG_MANUAL_REVERT = 'mw-manual-revert';
88 public const TAG_REVERTED = 'mw-reverted';
92 public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
93
98
102 public const BYPASS_MAX_USAGE_CHECK = 1;
103
109 private const MAX_DELETE_USES = 5000;
110
114 private const DEFINED_SOFTWARE_TAGS = [
115 'mw-contentmodelchange',
116 'mw-new-redirect',
117 'mw-removed-redirect',
118 'mw-changed-redirect-target',
119 'mw-blank',
120 'mw-replace',
121 'mw-rollback',
122 'mw-undo',
123 'mw-manual-revert',
124 'mw-reverted',
125 'mw-server-side-upload',
126 ];
127
131 private const CHANGE_TAG = 'change_tag';
132
136 private const CHANGE_TAG_DEF = 'change_tag_def';
137
148 public static $avoidReopeningTablesForTesting = false;
149
157 public static function getSoftwareTags( $all = false ) {
158 $coreTags = MediaWikiServices::getInstance()->getMainConfig()->get(
159 MainConfigNames::SoftwareTags );
160 $softwareTags = [];
161
162 if ( !is_array( $coreTags ) ) {
163 wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
164 Please refer to documentation for the list of tags you can enable' );
165 return $softwareTags;
166 }
167
168 $availableSoftwareTags = !$all ?
169 array_keys( array_filter( $coreTags ) ) :
170 array_keys( $coreTags );
171
172 $softwareTags = array_intersect(
173 $availableSoftwareTags,
174 self::DEFINED_SOFTWARE_TAGS
175 );
176
177 return $softwareTags;
178 }
179
193 public static function formatSummaryRow( $tags, $page, MessageLocalizer $localizer = null ) {
194 if ( $tags === '' || $tags === null ) {
195 return [ '', [] ];
196 }
197 if ( !$localizer ) {
198 $localizer = RequestContext::getMain();
199 }
200
201 $classes = [];
202
203 $tags = explode( ',', $tags );
204 $order = array_flip( self::listDefinedTags() );
205 usort( $tags, static function ( $a, $b ) use ( $order ) {
206 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
207 } );
208
209 $displayTags = [];
210 foreach ( $tags as $tag ) {
211 if ( $tag === '' ) {
212 continue;
213 }
214 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
215 $description = self::tagDescription( $tag, $localizer );
216 if ( $description === false ) {
217 continue;
218 }
219 $displayTags[] = Xml::tags(
220 'span',
221 [ 'class' => 'mw-tag-marker ' .
222 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
223 $description
224 );
225 }
226
227 if ( !$displayTags ) {
228 return [ '', $classes ];
229 }
230
231 $markers = $localizer->msg( 'tag-list-wrapper' )
232 ->numParams( count( $displayTags ) )
233 ->rawParams( implode( ' ', $displayTags ) )
234 ->parse();
235 $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
236
237 return [ $markers, $classes ];
238 }
239
253 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
254 $msg = $context->msg( "tag-$tag" );
255 if ( !$msg->exists() ) {
256 // No such message
257 // Pass through ->msg(), even though it seems redundant, to avoid requesting
258 // the user's language from session-less entry points (T227233)
259 return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
260 }
261 if ( $msg->isDisabled() ) {
262 // The message exists but is disabled, hide the tag.
263 return false;
264 }
265
266 // Message exists and isn't disabled, use it.
267 return $msg;
268 }
269
283 public static function tagDescription( $tag, MessageLocalizer $context ) {
284 $msg = self::tagShortDescriptionMessage( $tag, $context );
285 return $msg ? $msg->parse() : false;
286 }
287
300 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
301 $msg = $context->msg( "tag-$tag-description" );
302 if ( !$msg->exists() ) {
303 return false;
304 }
305 if ( $msg->isDisabled() ) {
306 // The message exists but is disabled, hide the description.
307 return false;
308 }
309
310 // Message exists and isn't disabled, use it.
311 return $msg;
312 }
313
328 public static function addTags( $tags, $rc_id = null, $rev_id = null,
329 $log_id = null, $params = null, RecentChange $rc = null
330 ) {
331 $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
332 return (bool)$result[0];
333 }
334
365 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
366 &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
367 UserIdentity $user = null
368 ) {
369 $tagsToAdd = array_filter(
370 (array)$tagsToAdd, // Make sure we're submitting all tags...
371 static function ( $value ) {
372 return ( $value ?? '' ) !== '';
373 }
374 );
375 $tagsToRemove = array_filter(
376 (array)$tagsToRemove,
377 static function ( $value ) {
378 return ( $value ?? '' ) !== '';
379 }
380 );
381
382 if ( !$rc_id && !$rev_id && !$log_id ) {
383 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
384 'specified when adding or removing a tag from a change!' );
385 }
386
387 $dbw = wfGetDB( DB_PRIMARY );
388
389 // Might as well look for rcids and so on.
390 if ( !$rc_id ) {
391 // Info might be out of date, somewhat fractionally, on replica DB.
392 // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
393 // so use that relation to avoid full table scans.
394 if ( $log_id ) {
395 $rc_id = $dbw->newSelectQueryBuilder()
396 ->select( 'rc_id' )
397 ->from( 'logging' )
398 ->join( 'recentchanges', null, [
399 'rc_timestamp = log_timestamp',
400 'rc_logid = log_id'
401 ] )
402 ->where( [ 'log_id' => $log_id ] )
403 ->caller( __METHOD__ )
404 ->fetchField();
405 } elseif ( $rev_id ) {
406 $rc_id = $dbw->newSelectQueryBuilder()
407 ->select( 'rc_id' )
408 ->from( 'revision' )
409 ->join( 'recentchanges', null, [
410 'rc_this_oldid = rev_id'
411 ] )
412 ->where( [ 'rev_id' => $rev_id ] )
413 ->caller( __METHOD__ )
414 ->fetchField();
415 }
416 } elseif ( !$log_id && !$rev_id ) {
417 // Info might be out of date, somewhat fractionally, on replica DB.
418 $log_id = $dbw->newSelectQueryBuilder()
419 ->select( 'rc_logid' )
420 ->from( 'recentchanges' )
421 ->where( [ 'rc_id' => $rc_id ] )
422 ->caller( __METHOD__ )
423 ->fetchField();
424 $rev_id = $dbw->newSelectQueryBuilder()
425 ->select( 'rc_this_oldid' )
426 ->from( 'recentchanges' )
427 ->where( [ 'rc_id' => $rc_id ] )
428 ->caller( __METHOD__ )
429 ->fetchField();
430 }
431
432 if ( $log_id && !$rev_id ) {
433 $rev_id = $dbw->newSelectQueryBuilder()
434 ->select( 'ls_value' )
435 ->from( 'log_search' )
436 ->where( [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ] )
437 ->caller( __METHOD__ )
438 ->fetchField();
439 } elseif ( !$log_id && $rev_id ) {
440 $log_id = $dbw->newSelectQueryBuilder()
441 ->select( 'ls_log_id' )
442 ->from( 'log_search' )
443 ->where( [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ] )
444 ->caller( __METHOD__ )
445 ->fetchField();
446 }
447
448 $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
449
450 // add tags
451 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
452 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
453
454 // remove tags
455 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
456 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
457
458 sort( $prevTags );
459 sort( $newTags );
460 if ( $prevTags == $newTags ) {
461 return [ [], [], $prevTags ];
462 }
463
464 // insert a row into change_tag for each new tag
465 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
466 if ( count( $tagsToAdd ) ) {
467 $changeTagMapping = [];
468 foreach ( $tagsToAdd as $tag ) {
469 $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
470 }
471 $fname = __METHOD__;
472 // T207881: update the counts at the end of the transaction
473 $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
474 $dbw->update(
475 self::CHANGE_TAG_DEF,
476 [ 'ctd_count = ctd_count + 1' ],
477 [ 'ctd_name' => $tagsToAdd ],
478 $fname
479 );
480 }, $fname );
481
482 $tagsRows = [];
483 foreach ( $tagsToAdd as $tag ) {
484 // Filter so we don't insert NULLs as zero accidentally.
485 // Keep in mind that $rc_id === null means "I don't care/know about the
486 // rc_id, just delete $tag on this revision/log entry". It doesn't
487 // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
488 $tagsRows[] = array_filter(
489 [
490 'ct_rc_id' => $rc_id,
491 'ct_log_id' => $log_id,
492 'ct_rev_id' => $rev_id,
493 'ct_params' => $params,
494 'ct_tag_id' => $changeTagMapping[$tag] ?? null,
495 ]
496 );
497
498 }
499
500 $dbw->insert( self::CHANGE_TAG, $tagsRows, __METHOD__, [ 'IGNORE' ] );
501 }
502
503 // delete from change_tag
504 if ( count( $tagsToRemove ) ) {
505 $fname = __METHOD__;
506 foreach ( $tagsToRemove as $tag ) {
507 $conds = array_filter(
508 [
509 'ct_rc_id' => $rc_id,
510 'ct_log_id' => $log_id,
511 'ct_rev_id' => $rev_id,
512 'ct_tag_id' => $changeTagDefStore->getId( $tag ),
513 ]
514 );
515 $dbw->delete( self::CHANGE_TAG, $conds, __METHOD__ );
516 if ( $dbw->affectedRows() ) {
517 // T207881: update the counts at the end of the transaction
518 $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
519 $dbw->update(
520 self::CHANGE_TAG_DEF,
521 [ 'ctd_count = ctd_count - 1' ],
522 [ 'ctd_name' => $tag ],
523 $fname
524 );
525
526 $dbw->delete(
527 self::CHANGE_TAG_DEF,
528 [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
529 $fname
530 );
531 }, $fname );
532 }
533 }
534 }
535
536 $userObj = $user ? MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ) : null;
537 Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
538 $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
539
540 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
541 }
542
555 public static function getTagsWithData(
556 IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
557 ) {
558 if ( !$rc_id && !$rev_id && !$log_id ) {
559 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
560 'specified when loading tags from a change!' );
561 }
562
563 $conds = array_filter(
564 [
565 'ct_rc_id' => $rc_id,
566 'ct_rev_id' => $rev_id,
567 'ct_log_id' => $log_id,
568 ]
569 );
570 $result = $db->newSelectQueryBuilder()
571 ->select( [ 'ct_tag_id', 'ct_params' ] )
572 ->from( self::CHANGE_TAG )
573 ->where( $conds )
574 ->caller( __METHOD__ )
575 ->fetchResultSet();
576
577 $tags = [];
578 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
579 foreach ( $result as $row ) {
580 $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
581 $tags[$tagName] = $row->ct_params;
582 }
583
584 return $tags;
585 }
586
597 public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
598 return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
599 }
600
611 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
612 $lang = RequestContext::getMain()->getLanguage();
613 $tags = array_values( $tags );
614 $count = count( $tags );
615 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
616 $lang->commaList( $tags ), $count );
617 $status->value = $tags;
618 return $status;
619 }
620
635 public static function canAddTagsAccompanyingChange(
636 array $tags,
637 Authority $performer = null,
638 $checkBlock = true
639 ) {
640 $user = null;
641 if ( $performer !== null ) {
642 if ( !$performer->isAllowed( 'applychangetags' ) ) {
643 return Status::newFatal( 'tags-apply-no-permission' );
644 }
645
646 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
647 return Status::newFatal(
648 'tags-apply-blocked',
649 $performer->getUser()->getName()
650 );
651 }
652
653 // ChangeTagsAllowedAdd hook still needs a full User object
654 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
655 }
656
657 // to be applied, a tag has to be explicitly defined
658 $allowedTags = self::listExplicitlyDefinedTags();
659 Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
660 $disallowedTags = array_diff( $tags, $allowedTags );
661 if ( $disallowedTags ) {
662 return self::restrictedTagError( 'tags-apply-not-allowed-one',
663 'tags-apply-not-allowed-multi', $disallowedTags );
664 }
665
666 return Status::newGood();
667 }
668
690 array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
691 ) {
692 // are we allowed to do this?
693 $result = self::canAddTagsAccompanyingChange( $tags, $performer );
694 if ( !$result->isOK() ) {
695 $result->value = null;
696 return $result;
697 }
698
699 // do it!
700 self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
701
702 return Status::newGood( true );
703 }
704
719 public static function canUpdateTags(
720 array $tagsToAdd,
721 array $tagsToRemove,
722 Authority $performer = null
723 ) {
724 if ( $performer !== null ) {
725 if ( !$performer->isAllowed( 'changetags' ) ) {
726 return Status::newFatal( 'tags-update-no-permission' );
727 }
728
729 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
730 return Status::newFatal(
731 'tags-update-blocked',
732 $performer->getUser()->getName()
733 );
734 }
735 }
736
737 if ( $tagsToAdd ) {
738 // to be added, a tag has to be explicitly defined
739 // @todo Allow extensions to define tags that can be applied by users...
740 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
741 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
742 if ( $diff ) {
743 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
744 'tags-update-add-not-allowed-multi', $diff );
745 }
746 }
747
748 if ( $tagsToRemove ) {
749 // to be removed, a tag must not be defined by an extension, or equivalently it
750 // has to be either explicitly defined or not defined at all
751 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
752 $softwareDefinedTags = self::listSoftwareDefinedTags();
753 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
754 if ( $intersect ) {
755 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
756 'tags-update-remove-not-allowed-multi', $intersect );
757 }
758 }
759
760 return Status::newGood();
761 }
762
793 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
794 $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
795 ) {
796 if ( $tagsToAdd === null ) {
797 $tagsToAdd = [];
798 }
799 if ( $tagsToRemove === null ) {
800 $tagsToRemove = [];
801 }
802 if ( !$tagsToAdd && !$tagsToRemove ) {
803 // no-op, don't bother
804 return Status::newGood( (object)[
805 'logId' => null,
806 'addedTags' => [],
807 'removedTags' => [],
808 ] );
809 }
810
811 // are we allowed to do this?
812 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
813 if ( !$result->isOK() ) {
814 $result->value = null;
815 return $result;
816 }
817
818 // basic rate limiting
819 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
820 if ( $user->pingLimiter( 'changetag' ) ) {
821 return Status::newFatal( 'actionthrottledtext' );
822 }
823
824 // do it!
825 list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
826 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
827 if ( !$tagsAdded && !$tagsRemoved ) {
828 // no-op, don't log it
829 return Status::newGood( (object)[
830 'logId' => null,
831 'addedTags' => [],
832 'removedTags' => [],
833 ] );
834 }
835
836 // log it
837 $logEntry = new ManualLogEntry( 'tag', 'update' );
838 $logEntry->setPerformer( $performer->getUser() );
839 $logEntry->setComment( $reason );
840
841 // find the appropriate target page
842 if ( $rev_id ) {
843 $revisionRecord = MediaWikiServices::getInstance()
844 ->getRevisionLookup()
845 ->getRevisionById( $rev_id );
846 if ( $revisionRecord ) {
847 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
848 }
849 } elseif ( $log_id ) {
850 // This function is from revision deletion logic and has nothing to do with
851 // change tags, but it appears to be the only other place in core where we
852 // perform logged actions on log items.
853 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
854 }
855
856 if ( !$logEntry->getTarget() ) {
857 // target is required, so we have to set something
858 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
859 }
860
861 $logParams = [
862 '4::revid' => $rev_id,
863 '5::logid' => $log_id,
864 '6:list:tagsAdded' => $tagsAdded,
865 '7:number:tagsAddedCount' => count( $tagsAdded ),
866 '8:list:tagsRemoved' => $tagsRemoved,
867 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
868 'initialTags' => $initialTags,
869 ];
870 $logEntry->setParameters( $logParams );
871 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
872
873 $dbw = wfGetDB( DB_PRIMARY );
874 $logId = $logEntry->insert( $dbw );
875 // Only send this to UDP, not RC, similar to patrol events
876 $logEntry->publish( $logId, 'udp' );
877
878 return Status::newGood( (object)[
879 'logId' => $logId,
880 'addedTags' => $tagsAdded,
881 'removedTags' => $tagsRemoved,
882 ] );
883 }
884
906 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
907 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
908 ) {
909 $useTagFilter = MediaWikiServices::getInstance()->getMainConfig()->get(
910 MainConfigNames::UseTagFilter );
911
912 // Normalize to arrays
913 $tables = (array)$tables;
914 $fields = (array)$fields;
915 $conds = (array)$conds;
916 $options = (array)$options;
917
918 $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
919
920 // Figure out which ID field to use
921 if ( in_array( 'recentchanges', $tables ) ) {
922 $join_cond = 'ct_rc_id=rc_id';
923 } elseif ( in_array( 'logging', $tables ) ) {
924 $join_cond = 'ct_log_id=log_id';
925 } elseif ( in_array( 'revision', $tables ) ) {
926 $join_cond = 'ct_rev_id=rev_id';
927 } elseif ( in_array( 'archive', $tables ) ) {
928 $join_cond = 'ct_rev_id=ar_rev_id';
929 } else {
930 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
931 }
932
933 if ( !$useTagFilter ) {
934 return;
935 }
936
937 if ( !is_array( $filter_tag ) ) {
938 // some callers provide false or null
939 $filter_tag = (string)$filter_tag;
940 }
941
942 if ( $filter_tag !== [] && $filter_tag !== '' ) {
943 // Somebody wants to filter on a tag.
944 // Add an INNER JOIN on change_tag
945 $tagTable = self::getDisplayTableName();
946 $filterTagIds = [];
947 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
948 foreach ( (array)$filter_tag as $filterTagName ) {
949 try {
950 $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
951 } catch ( NameTableAccessException $exception ) {
952 }
953 }
954
955 if ( $exclude ) {
956 if ( $filterTagIds !== [] ) {
957 $tables[] = $tagTable;
958 $join_conds[$tagTable] = [
959 'LEFT JOIN',
960 [ $join_cond, 'ct_tag_id' => $filterTagIds ]
961 ];
962 $conds[] = "ct_tag_id IS NULL";
963 }
964 } else {
965 $tables[] = $tagTable;
966 $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
967 if ( $filterTagIds !== [] ) {
968 $conds['ct_tag_id'] = $filterTagIds;
969 } else {
970 // all tags were invalid, return nothing
971 $conds[] = '0=1';
972 }
973
974 if (
975 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
976 !in_array( 'DISTINCT', $options )
977 ) {
978 $options[] = 'DISTINCT';
979 }
980 }
981 }
982 }
983
990 public static function getDisplayTableName() {
991 $tagTable = self::CHANGE_TAG;
992 if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
993 $db = wfGetDB( DB_REPLICA );
994
995 if ( $db->getType() === 'mysql' ) {
996 // When filtering by tag, we are using the change_tag table twice:
997 // Once in a join for filtering, and once in a sub-query to list all
998 // tags for each revision. This does not work with temporary tables
999 // on some versions of MySQL, which causes phpunit tests to fail.
1000 // As a hacky workaround, we copy the temporary table, and join
1001 // against the copy. It is acknowledged that this is quite horrific.
1002 // Discuss at T256006.
1003
1004 $tagTable = 'change_tag_for_display_query';
1005 if ( !$db->tableExists( $tagTable ) ) {
1006 $db->query(
1007 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
1008 . ' LIKE ' . $db->tableName( self::CHANGE_TAG ),
1009 __METHOD__
1010 );
1011 $db->query(
1012 'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
1013 . ' SELECT * FROM ' . $db->tableName( self::CHANGE_TAG ),
1014 __METHOD__
1015 );
1016 }
1017 }
1018 }
1019 return $tagTable;
1020 }
1021
1030 public static function makeTagSummarySubquery( $tables ) {
1031 // Normalize to arrays
1032 $tables = (array)$tables;
1033
1034 // Figure out which ID field to use
1035 if ( in_array( 'recentchanges', $tables ) ) {
1036 $join_cond = 'ct_rc_id=rc_id';
1037 } elseif ( in_array( 'logging', $tables ) ) {
1038 $join_cond = 'ct_log_id=log_id';
1039 } elseif ( in_array( 'revision', $tables ) ) {
1040 $join_cond = 'ct_rev_id=rev_id';
1041 } elseif ( in_array( 'archive', $tables ) ) {
1042 $join_cond = 'ct_rev_id=ar_rev_id';
1043 } else {
1044 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
1045 }
1046
1047 $tagTables = [ self::CHANGE_TAG, self::CHANGE_TAG_DEF ];
1048 $join_cond_ts_tags = [ self::CHANGE_TAG_DEF => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
1049 $field = 'ctd_name';
1050
1051 return wfGetDB( DB_REPLICA )->buildGroupConcatField(
1052 ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
1053 );
1054 }
1055
1067 public static function buildTagFilterSelector(
1068 $selected = '', $ooui = false, IContextSource $context = null
1069 ) {
1070 if ( !$context ) {
1071 $context = RequestContext::getMain();
1072 }
1073
1074 $config = $context->getConfig();
1075 if ( !$config->get( MainConfigNames::UseTagFilter ) ||
1076 !count( self::listDefinedTags() ) ) {
1077 return [];
1078 }
1079
1080 $tags = self::getChangeTagList( $context, $context->getLanguage() );
1081 $autocomplete = [];
1082 foreach ( $tags as $tagInfo ) {
1083 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
1084 }
1085
1086 $data = [
1087 Html::rawElement(
1088 'label',
1089 [ 'for' => 'tagfilter' ],
1090 $context->msg( 'tag-filter' )->parse()
1091 )
1092 ];
1093
1094 if ( $ooui ) {
1095 $options = Xml::listDropDownOptionsOoui( $autocomplete );
1096
1097 $data[] = new OOUI\ComboBoxInputWidget( [
1098 'id' => 'tagfilter',
1099 'name' => 'tagfilter',
1100 'value' => $selected,
1101 'classes' => 'mw-tagfilter-input',
1102 'options' => $options,
1103 ] );
1104 } else {
1105 $datalist = new XmlSelect( false, 'tagfilter-datalist' );
1106 $datalist->setTagName( 'datalist' );
1107 $datalist->addOptions( $autocomplete );
1108
1109 $data[] = Xml::input(
1110 'tagfilter',
1111 20,
1112 $selected,
1113 [
1114 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline',
1115 'id' => 'tagfilter',
1116 'list' => 'tagfilter-datalist',
1117 ]
1118 ) . $datalist->getHTML();
1119 }
1120
1121 return $data;
1122 }
1123
1132 public static function defineTag( $tag ) {
1133 $dbw = wfGetDB( DB_PRIMARY );
1134 $tagDef = [
1135 'ctd_name' => $tag,
1136 'ctd_user_defined' => 1,
1137 'ctd_count' => 0
1138 ];
1139 $dbw->upsert(
1140 self::CHANGE_TAG_DEF,
1141 $tagDef,
1142 'ctd_name',
1143 [ 'ctd_user_defined' => 1 ],
1144 __METHOD__
1145 );
1146
1147 // clear the memcache of defined tags
1149 }
1150
1159 public static function undefineTag( $tag ) {
1160 $dbw = wfGetDB( DB_PRIMARY );
1161
1162 $dbw->update(
1163 self::CHANGE_TAG_DEF,
1164 [ 'ctd_user_defined' => 0 ],
1165 [ 'ctd_name' => $tag ],
1166 __METHOD__
1167 );
1168
1169 $dbw->delete(
1170 self::CHANGE_TAG_DEF,
1171 [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1172 __METHOD__
1173 );
1174
1175 // clear the memcache of defined tags
1177 }
1178
1193 protected static function logTagManagementAction( string $action, string $tag, string $reason,
1194 UserIdentity $user, $tagCount = null, array $logEntryTags = []
1195 ) {
1196 $dbw = wfGetDB( DB_PRIMARY );
1197
1198 $logEntry = new ManualLogEntry( 'managetags', $action );
1199 $logEntry->setPerformer( $user );
1200 // target page is not relevant, but it has to be set, so we just put in
1201 // the title of Special:Tags
1202 $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1203 $logEntry->setComment( $reason );
1204
1205 $params = [ '4::tag' => $tag ];
1206 if ( $tagCount !== null ) {
1207 $params['5:number:count'] = $tagCount;
1208 }
1209 $logEntry->setParameters( $params );
1210 $logEntry->setRelations( [ 'Tag' => $tag ] );
1211 $logEntry->addTags( $logEntryTags );
1212
1213 $logId = $logEntry->insert( $dbw );
1214 $logEntry->publish( $logId );
1215 return $logId;
1216 }
1217
1227 public static function canActivateTag( $tag, Authority $performer = null ) {
1228 if ( $performer !== null ) {
1229 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1230 return Status::newFatal( 'tags-manage-no-permission' );
1231 }
1232 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1233 return Status::newFatal(
1234 'tags-manage-blocked',
1235 $performer->getUser()->getName()
1236 );
1237 }
1238 }
1239
1240 // defined tags cannot be activated (a defined tag is either extension-
1241 // defined, in which case the extension chooses whether or not to active it;
1242 // or user-defined, in which case it is considered active)
1243 $definedTags = self::listDefinedTags();
1244 if ( in_array( $tag, $definedTags ) ) {
1245 return Status::newFatal( 'tags-activate-not-allowed', $tag );
1246 }
1247
1248 // non-existing tags cannot be activated
1249 $tagUsage = self::tagUsageStatistics();
1250 if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1251 return Status::newFatal( 'tags-activate-not-found', $tag );
1252 }
1253
1254 return Status::newGood();
1255 }
1256
1274 public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
1275 bool $ignoreWarnings = false, array $logEntryTags = []
1276 ) {
1277 // are we allowed to do this?
1278 $result = self::canActivateTag( $tag, $performer );
1279 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1280 $result->value = null;
1281 return $result;
1282 }
1283
1284 // do it!
1285 self::defineTag( $tag );
1286
1287 // log it
1288 $logId = self::logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1289 null, $logEntryTags );
1290
1291 return Status::newGood( $logId );
1292 }
1293
1303 public static function canDeactivateTag( $tag, Authority $performer = null ) {
1304 if ( $performer !== null ) {
1305 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1306 return Status::newFatal( 'tags-manage-no-permission' );
1307 }
1308 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1309 return Status::newFatal(
1310 'tags-manage-blocked',
1311 $performer->getUser()->getName()
1312 );
1313 }
1314 }
1315
1316 // only explicitly-defined tags can be deactivated
1317 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1318 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1319 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1320 }
1321 return Status::newGood();
1322 }
1323
1341 public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
1342 bool $ignoreWarnings = false, array $logEntryTags = []
1343 ) {
1344 // are we allowed to do this?
1345 $result = self::canDeactivateTag( $tag, $performer );
1346 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1347 $result->value = null;
1348 return $result;
1349 }
1350
1351 // do it!
1352 self::undefineTag( $tag );
1353
1354 // log it
1355 $logId = self::logTagManagementAction( 'deactivate', $tag, $reason,
1356 $performer->getUser(), null, $logEntryTags );
1357
1358 return Status::newGood( $logId );
1359 }
1360
1368 public static function isTagNameValid( $tag ) {
1369 // no empty tags
1370 if ( $tag === '' ) {
1371 return Status::newFatal( 'tags-create-no-name' );
1372 }
1373
1374 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1375 // pipe (used as a delimiter between multiple tags in
1376 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1377 // MediaWiki namespace)
1378 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1379 || strpos( $tag, '/' ) !== false ) {
1380 return Status::newFatal( 'tags-create-invalid-chars' );
1381 }
1382
1383 // could the MediaWiki namespace description messages be created?
1384 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1385 if ( $title === null ) {
1386 return Status::newFatal( 'tags-create-invalid-title-chars' );
1387 }
1388
1389 return Status::newGood();
1390 }
1391
1404 public static function canCreateTag( $tag, Authority $performer = null ) {
1405 $user = null;
1406 if ( $performer !== null ) {
1407 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1408 return Status::newFatal( 'tags-manage-no-permission' );
1409 }
1410 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1411 return Status::newFatal(
1412 'tags-manage-blocked',
1413 $performer->getUser()->getName()
1414 );
1415 }
1416 // ChangeTagCanCreate hook still needs a full User object
1417 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1418 }
1419
1420 $status = self::isTagNameValid( $tag );
1421 if ( !$status->isGood() ) {
1422 return $status;
1423 }
1424
1425 // does the tag already exist?
1426 $tagUsage = self::tagUsageStatistics();
1427 if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1428 return Status::newFatal( 'tags-create-already-exists', $tag );
1429 }
1430
1431 // check with hooks
1432 $canCreateResult = Status::newGood();
1433 Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1434 return $canCreateResult;
1435 }
1436
1456 public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
1457 bool $ignoreWarnings = false, array $logEntryTags = []
1458 ) {
1459 // are we allowed to do this?
1460 $result = self::canCreateTag( $tag, $performer );
1461 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1462 $result->value = null;
1463 return $result;
1464 }
1465
1466 // do it!
1467 self::defineTag( $tag );
1468
1469 // log it
1470 $logId = self::logTagManagementAction( 'create', $tag, $reason,
1471 $performer->getUser(), null, $logEntryTags );
1472
1473 return Status::newGood( $logId );
1474 }
1475
1488 public static function deleteTagEverywhere( $tag ) {
1489 $dbw = wfGetDB( DB_PRIMARY );
1490 $dbw->startAtomic( __METHOD__ );
1491
1492 // fetch tag id, this must be done before calling undefineTag(), see T225564
1493 $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1494
1495 // set ctd_user_defined = 0
1496 self::undefineTag( $tag );
1497
1498 // delete from change_tag
1499 $dbw->delete( self::CHANGE_TAG, [ 'ct_tag_id' => $tagId ], __METHOD__ );
1500 $dbw->delete( self::CHANGE_TAG_DEF, [ 'ctd_name' => $tag ], __METHOD__ );
1501 $dbw->endAtomic( __METHOD__ );
1502
1503 // give extensions a chance
1504 $status = Status::newGood();
1505 Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1506 // let's not allow error results, as the actual tag deletion succeeded
1507 if ( !$status->isOK() ) {
1508 wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1509 $status->setOK( true );
1510 }
1511
1512 // clear the memcache of defined tags
1514
1515 return $status;
1516 }
1517
1530 public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1531 $tagUsage = self::tagUsageStatistics();
1532 $user = null;
1533 if ( $performer !== null ) {
1534 if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1535 return Status::newFatal( 'tags-delete-no-permission' );
1536 }
1537 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1538 return Status::newFatal(
1539 'tags-manage-blocked',
1540 $performer->getUser()->getName()
1541 );
1542 }
1543 // ChangeTagCanDelete hook still needs a full User object
1544 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1545 }
1546
1547 if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1548 return Status::newFatal( 'tags-delete-not-found', $tag );
1549 }
1550
1551 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1552 isset( $tagUsage[$tag] ) &&
1553 $tagUsage[$tag] > self::MAX_DELETE_USES
1554 ) {
1555 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1556 }
1557
1558 $softwareDefined = self::listSoftwareDefinedTags();
1559 if ( in_array( $tag, $softwareDefined ) ) {
1560 // extension-defined tags can't be deleted unless the extension
1561 // specifically allows it
1562 $status = Status::newFatal( 'tags-delete-not-allowed' );
1563 } else {
1564 // user-defined tags are deletable unless otherwise specified
1565 $status = Status::newGood();
1566 }
1567
1568 Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1569 return $status;
1570 }
1571
1589 public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1590 bool $ignoreWarnings = false, array $logEntryTags = []
1591 ) {
1592 // are we allowed to do this?
1593 $result = self::canDeleteTag( $tag, $performer );
1594 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1595 $result->value = null;
1596 return $result;
1597 }
1598
1599 // store the tag usage statistics
1600 $tagUsage = self::tagUsageStatistics();
1601 $hitcount = $tagUsage[$tag] ?? 0;
1602
1603 // do it!
1604 $deleteResult = self::deleteTagEverywhere( $tag );
1605 if ( !$deleteResult->isOK() ) {
1606 return $deleteResult;
1607 }
1608
1609 // log it
1610 $logId = self::logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1611 $hitcount, $logEntryTags );
1612
1613 $deleteResult->value = $logId;
1614 return $deleteResult;
1615 }
1616
1623 public static function listSoftwareActivatedTags() {
1624 // core active tags
1625 $tags = self::getSoftwareTags();
1626 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1627 if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1628 return $tags;
1629 }
1630 $hookRunner = new HookRunner( $hookContainer );
1631 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1632 return $cache->getWithSetCallback(
1633 $cache->makeKey( 'active-tags' ),
1634 WANObjectCache::TTL_MINUTE * 5,
1635 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1636 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1637
1638 // Ask extensions which tags they consider active
1639 $hookRunner->onChangeTagsListActive( $tags );
1640 return $tags;
1641 },
1642 [
1643 'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1644 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1645 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1646 ]
1647 );
1648 }
1649
1657 public static function listDefinedTags() {
1660 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1661 }
1662
1671 public static function listExplicitlyDefinedTags() {
1672 $fname = __METHOD__;
1673
1674 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1675 return $cache->getWithSetCallback(
1676 $cache->makeKey( 'valid-tags-db' ),
1677 WANObjectCache::TTL_MINUTE * 5,
1678 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1679 $dbr = wfGetDB( DB_REPLICA );
1680
1681 $setOpts += Database::getCacheSetOptions( $dbr );
1682 $tags = $dbr->newSelectQueryBuilder()
1683 ->select( 'ctd_name' )
1684 ->from( self::CHANGE_TAG_DEF )
1685 ->where( [ 'ctd_user_defined' => 1 ] )
1686 ->caller( $fname )
1687 ->fetchFieldValues();
1688
1689 return array_unique( $tags );
1690 },
1691 [
1692 'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1693 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1694 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1695 ]
1696 );
1697 }
1698
1708 public static function listSoftwareDefinedTags() {
1709 // core defined tags
1710 $tags = self::getSoftwareTags( true );
1711 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1712 if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1713 return $tags;
1714 }
1715 $hookRunner = new HookRunner( $hookContainer );
1716 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1717 return $cache->getWithSetCallback(
1718 $cache->makeKey( 'valid-tags-hook' ),
1719 WANObjectCache::TTL_MINUTE * 5,
1720 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1721 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1722
1723 $hookRunner->onListDefinedTags( $tags );
1724 return array_unique( $tags );
1725 },
1726 [
1727 'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1728 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1729 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1730 ]
1731 );
1732 }
1733
1739 public static function purgeTagCacheAll() {
1740 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1741
1742 $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1743 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1744 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1745 $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1746
1747 MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1748 }
1749
1756 public static function tagUsageStatistics() {
1757 $fname = __METHOD__;
1758
1759 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1760 return $cache->getWithSetCallback(
1761 $cache->makeKey( 'tags-usage-statistics' ),
1762 WANObjectCache::TTL_MINUTE * 5,
1763 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1764 $dbr = wfGetDB( DB_REPLICA );
1765 $res = $dbr->newSelectQueryBuilder()
1766 ->select( [ 'ctd_name', 'ctd_count' ] )
1767 ->from( self::CHANGE_TAG_DEF )
1768 ->orderBy( 'ctd_count', SelectQueryBuilder::SORT_DESC )
1769 ->caller( $fname )
1770 ->fetchResultSet();
1771
1772 $out = [];
1773 foreach ( $res as $row ) {
1774 $out[$row->ctd_name] = $row->ctd_count;
1775 }
1776
1777 return $out;
1778 },
1779 [
1780 'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1781 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1782 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1783 ]
1784 );
1785 }
1786
1791 private const TAG_DESC_CHARACTER_LIMIT = 120;
1792
1817 public static function getChangeTagListSummary( MessageLocalizer $localizer, Language $lang ) {
1818 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1819 return $cache->getWithSetCallback(
1820 $cache->makeKey( 'tags-list-summary', $lang->getCode() ),
1821 WANObjectCache::TTL_DAY,
1822 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
1823 $tagHitCounts = self::tagUsageStatistics();
1824
1825 $result = [];
1826 // Only list tags that are still actively defined
1827 foreach ( self::listDefinedTags() as $tagName ) {
1828 // Only list tags with more than 0 hits
1829 $hits = $tagHitCounts[$tagName] ?? 0;
1830 if ( $hits <= 0 ) {
1831 continue;
1832 }
1833
1834 $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1835 $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1836 // Don't cache the message object, use the correct MessageLocalizer to parse later.
1837 $result[] = [
1838 'name' => $tagName,
1839 'labelMsg' => (bool)$labelMsg,
1840 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1841 'descriptionMsg' => (bool)$descriptionMsg,
1842 'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1843 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1844 ];
1845 }
1846 return $result;
1847 }
1848 );
1849 }
1850
1863 public static function getChangeTagList( MessageLocalizer $localizer, Language $lang ) {
1864 $tags = self::getChangeTagListSummary( $localizer, $lang );
1865 foreach ( $tags as &$tagInfo ) {
1866 if ( $tagInfo['labelMsg'] ) {
1867 // Use localizer with the correct page title to parse plain message from the cache.
1868 $labelMsg = new RawMessage( $tagInfo['label'] );
1869 $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1870 } else {
1871 $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1872 }
1873 if ( $tagInfo['descriptionMsg'] ) {
1874 $descriptionMsg = new RawMessage( $tagInfo['description'] );
1875 $tagInfo['description'] = $lang->truncateForVisual(
1876 Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() ),
1877 self::TAG_DESC_CHARACTER_LIMIT
1878 );
1879 }
1880 unset( $tagInfo['labelMsg'] );
1881 unset( $tagInfo['descriptionMsg'] );
1882 }
1883
1884 // Instead of sorting by hit count (disabled for now), sort by display name
1885 usort( $tags, static function ( $a, $b ) {
1886 return strcasecmp( $a['label'], $b['label'] );
1887 } );
1888 return $tags;
1889 }
1890
1905 public static function showTagEditingUI( Authority $performer ) {
1906 return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1907 }
1908}
const NS_MEDIAWIKI
Definition Defines.php:72
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.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
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.
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
static logTagManagementAction(string $action, string $tag, string $reason, UserIdentity $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
static deactivateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static deleteTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
static getChangeTagListSummary(MessageLocalizer $localizer, Language $lang)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
static getChangeTagList(MessageLocalizer $localizer, Language $lang)
Get information about change tags for tag filter dropdown menus.
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
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.
static addTagsAccompanyingChangeWithChecks(array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer)
Adds tags to a given change, checking whether it is allowed first, but without adding a log entry.
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static getDisplayTableName()
Get the name of the change_tag table to use for modifyDisplayQuery().
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
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,...
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
static createTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
static isTagNameValid( $tag)
Is the tag name valid?
const REVERT_TAGS
List of tags which denote a revert of some sort.
static activateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, Authority $performer=null)
Is it OK to allow the user to adds and remove the given tags to/from a change?
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, UserIdentity $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
Base class for language-specific code.
Definition Language.php:53
MediaWiki exception.
Class for creating new log entries and inserting them into the database.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Exception representing a failure to look up a row from a name table.
static plaintextParam( $plaintext)
Definition Message.php:1266
Variant of the Message class.
Utility class for creating new RC entries.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
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,...
Note that none of the methods in this class are stable to override.
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:26
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
if(!isset( $args[0])) $lang