MediaWiki REL1_35
ChangeTags.php
Go to the documentation of this file.
1<?php
29
36 private const MAX_DELETE_USES = 5000;
37
41 public const BYPASS_MAX_USAGE_CHECK = 1;
42
46 private static $definedSoftwareTags = [
47 'mw-contentmodelchange',
48 'mw-new-redirect',
49 'mw-removed-redirect',
50 'mw-changed-redirect-target',
51 'mw-blank',
52 'mw-replace',
53 'mw-rollback',
54 'mw-undo',
55 ];
56
67 public static $avoidReopeningTablesForTesting = false;
68
76 public static function getSoftwareTags( $all = false ) {
77 global $wgSoftwareTags;
78 $softwareTags = [];
79
80 if ( !is_array( $wgSoftwareTags ) ) {
81 wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
82 Please refer to documentation for the list of tags you can enable' );
83 return $softwareTags;
84 }
85
86 $availableSoftwareTags = !$all ?
87 array_keys( array_filter( $wgSoftwareTags ) ) :
88 array_keys( $wgSoftwareTags );
89
90 $softwareTags = array_intersect(
91 $availableSoftwareTags,
92 self::$definedSoftwareTags
93 );
94
95 return $softwareTags;
96 }
97
111 public static function formatSummaryRow( $tags, $page, MessageLocalizer $localizer = null ) {
112 if ( $tags === '' || $tags === null ) {
113 return [ '', [] ];
114 }
115 if ( !$localizer ) {
116 $localizer = RequestContext::getMain();
117 }
118
119 $classes = [];
120
121 $tags = explode( ',', $tags );
122 $displayTags = [];
123 foreach ( $tags as $tag ) {
124 if ( $tag === '' ) {
125 continue;
126 }
127 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
128 $description = self::tagDescription( $tag, $localizer );
129 if ( $description === false ) {
130 continue;
131 }
132 $displayTags[] = Xml::tags(
133 'span',
134 [ 'class' => 'mw-tag-marker ' .
135 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
136 $description
137 );
138 }
139
140 if ( !$displayTags ) {
141 return [ '', $classes ];
142 }
143
144 $markers = $localizer->msg( 'tag-list-wrapper' )
145 ->numParams( count( $displayTags ) )
146 ->rawParams( implode( ' ', $displayTags ) )
147 ->parse();
148 $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
149
150 return [ $markers, $classes ];
151 }
152
166 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
167 $msg = $context->msg( "tag-$tag" );
168 if ( !$msg->exists() ) {
169 // No such message
170 return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
171 // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
172 // so extract the language from $msg and use that.
173 // The language doesn't really matter, but we need to set it to avoid requesting
174 // the user's language from session-less entry points (T227233)
175 ->inLanguage( $msg->getLanguage() );
176
177 }
178 if ( $msg->isDisabled() ) {
179 // The message exists but is disabled, hide the tag.
180 return false;
181 }
182
183 // Message exists and isn't disabled, use it.
184 return $msg;
185 }
186
200 public static function tagDescription( $tag, MessageLocalizer $context ) {
201 $msg = self::tagShortDescriptionMessage( $tag, $context );
202 return $msg ? $msg->parse() : false;
203 }
204
217 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
218 $msg = $context->msg( "tag-$tag-description" );
219 if ( !$msg->exists() ) {
220 return false;
221 }
222 if ( $msg->isDisabled() ) {
223 // The message exists but is disabled, hide the description.
224 return false;
225 }
226
227 // Message exists and isn't disabled, use it.
228 return $msg;
229 }
230
241 public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
242 wfDeprecated( __METHOD__, '1.35' );
243 // FIXME: Make this accept MessageLocalizer and Language instead of IContextSource
244
245 $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
246 // If there is no tag description, return empty string
247 if ( !$originalDesc ) {
248 return '';
249 }
250
251 $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
252
253 return $context->getLanguage()->truncateForVisual( $taglessDesc, $length );
254 }
255
270 public static function addTags( $tags, $rc_id = null, $rev_id = null,
271 $log_id = null, $params = null, RecentChange $rc = null
272 ) {
273 $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
274 return (bool)$result[0];
275 }
276
307 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
308 &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
309 User $user = null
310 ) {
311 $tagsToAdd = array_filter(
312 (array)$tagsToAdd, // Make sure we're submitting all tags...
313 static function ( $value ) {
314 return ( $value ?? '' ) !== '';
315 }
316 );
317 $tagsToRemove = array_filter(
318 (array)$tagsToRemove,
319 static function ( $value ) {
320 return ( $value ?? '' ) !== '';
321 }
322 );
323
324 if ( !$rc_id && !$rev_id && !$log_id ) {
325 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
326 'specified when adding or removing a tag from a change!' );
327 }
328
329 $dbw = wfGetDB( DB_MASTER );
330
331 // Might as well look for rcids and so on.
332 if ( !$rc_id ) {
333 // Info might be out of date, somewhat fractionally, on replica DB.
334 // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
335 // so use that relation to avoid full table scans.
336 if ( $log_id ) {
337 $rc_id = $dbw->selectField(
338 [ 'logging', 'recentchanges' ],
339 'rc_id',
340 [
341 'log_id' => $log_id,
342 'rc_timestamp = log_timestamp',
343 'rc_logid = log_id'
344 ],
345 __METHOD__
346 );
347 } elseif ( $rev_id ) {
348 $rc_id = $dbw->selectField(
349 [ 'revision', 'recentchanges' ],
350 'rc_id',
351 [
352 'rev_id' => $rev_id,
353 'rc_this_oldid = rev_id'
354 ],
355 __METHOD__
356 );
357 }
358 } elseif ( !$log_id && !$rev_id ) {
359 // Info might be out of date, somewhat fractionally, on replica DB.
360 $log_id = $dbw->selectField(
361 'recentchanges',
362 'rc_logid',
363 [ 'rc_id' => $rc_id ],
364 __METHOD__
365 );
366 $rev_id = $dbw->selectField(
367 'recentchanges',
368 'rc_this_oldid',
369 [ 'rc_id' => $rc_id ],
370 __METHOD__
371 );
372 }
373
374 if ( $log_id && !$rev_id ) {
375 $rev_id = $dbw->selectField(
376 'log_search',
377 'ls_value',
378 [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
379 __METHOD__
380 );
381 } elseif ( !$log_id && $rev_id ) {
382 $log_id = $dbw->selectField(
383 'log_search',
384 'ls_log_id',
385 [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
386 __METHOD__
387 );
388 }
389
390 $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
391
392 // add tags
393 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
394 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
395
396 // remove tags
397 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
398 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
399
400 sort( $prevTags );
401 sort( $newTags );
402 if ( $prevTags == $newTags ) {
403 return [ [], [], $prevTags ];
404 }
405
406 // insert a row into change_tag for each new tag
407 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
408 if ( count( $tagsToAdd ) ) {
409 $changeTagMapping = [];
410 foreach ( $tagsToAdd as $tag ) {
411 $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
412 }
413 $fname = __METHOD__;
414 // T207881: update the counts at the end of the transaction
415 $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) {
416 $dbw->update(
417 'change_tag_def',
418 [ 'ctd_count = ctd_count + 1' ],
419 [ 'ctd_name' => $tagsToAdd ],
420 $fname
421 );
422 }, $fname );
423
424 $tagsRows = [];
425 foreach ( $tagsToAdd as $tag ) {
426 // Filter so we don't insert NULLs as zero accidentally.
427 // Keep in mind that $rc_id === null means "I don't care/know about the
428 // rc_id, just delete $tag on this revision/log entry". It doesn't
429 // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
430 $tagsRows[] = array_filter(
431 [
432 'ct_rc_id' => $rc_id,
433 'ct_log_id' => $log_id,
434 'ct_rev_id' => $rev_id,
435 'ct_params' => $params,
436 'ct_tag_id' => $changeTagMapping[$tag] ?? null,
437 ]
438 );
439
440 }
441
442 $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
443 }
444
445 // delete from change_tag
446 if ( count( $tagsToRemove ) ) {
447 $fname = __METHOD__;
448 foreach ( $tagsToRemove as $tag ) {
449 $conds = array_filter(
450 [
451 'ct_rc_id' => $rc_id,
452 'ct_log_id' => $log_id,
453 'ct_rev_id' => $rev_id,
454 'ct_tag_id' => $changeTagDefStore->getId( $tag ),
455 ]
456 );
457 $dbw->delete( 'change_tag', $conds, __METHOD__ );
458 if ( $dbw->affectedRows() ) {
459 // T207881: update the counts at the end of the transaction
460 $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) {
461 $dbw->update(
462 'change_tag_def',
463 [ 'ctd_count = ctd_count - 1' ],
464 [ 'ctd_name' => $tag ],
465 $fname
466 );
467
468 $dbw->delete(
469 'change_tag_def',
470 [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
471 $fname
472 );
473 }, $fname );
474 }
475 }
476 }
477
478 Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
479 $rc_id, $rev_id, $log_id, $params, $rc, $user );
480
481 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
482 }
483
494 public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
495 $conds = array_filter(
496 [
497 'ct_rc_id' => $rc_id,
498 'ct_rev_id' => $rev_id,
499 'ct_log_id' => $log_id,
500 ]
501 );
502
503 $tagIds = $db->selectFieldValues(
504 'change_tag',
505 'ct_tag_id',
506 $conds,
507 __METHOD__
508 );
509
510 $tags = [];
511 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
512 foreach ( $tagIds as $tagId ) {
513 $tags[] = $changeTagDefStore->getName( (int)$tagId );
514 }
515
516 return $tags;
517 }
518
529 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
530 $lang = RequestContext::getMain()->getLanguage();
531 $tags = array_values( $tags );
532 $count = count( $tags );
533 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
534 $lang->commaList( $tags ), $count );
535 $status->value = $tags;
536 return $status;
537 }
538
552 public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
553 if ( $user !== null ) {
554 if ( !MediaWikiServices::getInstance()->getPermissionManager()
555 ->userHasRight( $user, 'applychangetags' )
556 ) {
557 return Status::newFatal( 'tags-apply-no-permission' );
558 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
559 return Status::newFatal( 'tags-apply-blocked', $user->getName() );
560 }
561 }
562
563 // to be applied, a tag has to be explicitly defined
564 $allowedTags = self::listExplicitlyDefinedTags();
565 Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
566 $disallowedTags = array_diff( $tags, $allowedTags );
567 if ( $disallowedTags ) {
568 return self::restrictedTagError( 'tags-apply-not-allowed-one',
569 'tags-apply-not-allowed-multi', $disallowedTags );
570 }
571
572 return Status::newGood();
573 }
574
596 array $tags, $rc_id, $rev_id, $log_id, $params, User $user
597 ) {
598 // are we allowed to do this?
599 $result = self::canAddTagsAccompanyingChange( $tags, $user );
600 if ( !$result->isOK() ) {
601 $result->value = null;
602 return $result;
603 }
604
605 // do it!
606 self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
607
608 return Status::newGood( true );
609 }
610
625 public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
626 User $user = null
627 ) {
628 if ( $user !== null ) {
629 if ( !MediaWikiServices::getInstance()->getPermissionManager()
630 ->userHasRight( $user, 'changetags' )
631 ) {
632 return Status::newFatal( 'tags-update-no-permission' );
633 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
634 return Status::newFatal( 'tags-update-blocked', $user->getName() );
635 }
636 }
637
638 if ( $tagsToAdd ) {
639 // to be added, a tag has to be explicitly defined
640 // @todo Allow extensions to define tags that can be applied by users...
641 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
642 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
643 if ( $diff ) {
644 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
645 'tags-update-add-not-allowed-multi', $diff );
646 }
647 }
648
649 if ( $tagsToRemove ) {
650 // to be removed, a tag must not be defined by an extension, or equivalently it
651 // has to be either explicitly defined or not defined at all
652 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
653 $softwareDefinedTags = self::listSoftwareDefinedTags();
654 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
655 if ( $intersect ) {
656 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
657 'tags-update-remove-not-allowed-multi', $intersect );
658 }
659 }
660
661 return Status::newGood();
662 }
663
694 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
695 $rc_id, $rev_id, $log_id, $params, $reason, User $user
696 ) {
697 if ( $tagsToAdd === null ) {
698 $tagsToAdd = [];
699 }
700 if ( $tagsToRemove === null ) {
701 $tagsToRemove = [];
702 }
703 if ( !$tagsToAdd && !$tagsToRemove ) {
704 // no-op, don't bother
705 return Status::newGood( (object)[
706 'logId' => null,
707 'addedTags' => [],
708 'removedTags' => [],
709 ] );
710 }
711
712 // are we allowed to do this?
713 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
714 if ( !$result->isOK() ) {
715 $result->value = null;
716 return $result;
717 }
718
719 // basic rate limiting
720 if ( $user->pingLimiter( 'changetag' ) ) {
721 return Status::newFatal( 'actionthrottledtext' );
722 }
723
724 // do it!
725 list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
726 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
727 if ( !$tagsAdded && !$tagsRemoved ) {
728 // no-op, don't log it
729 return Status::newGood( (object)[
730 'logId' => null,
731 'addedTags' => [],
732 'removedTags' => [],
733 ] );
734 }
735
736 // log it
737 $logEntry = new ManualLogEntry( 'tag', 'update' );
738 $logEntry->setPerformer( $user );
739 $logEntry->setComment( $reason );
740
741 // find the appropriate target page
742 if ( $rev_id ) {
743 $revisionRecord = MediaWikiServices::getInstance()
744 ->getRevisionLookup()
745 ->getRevisionById( $rev_id );
746 if ( $revisionRecord ) {
747 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
748 }
749 } elseif ( $log_id ) {
750 // This function is from revision deletion logic and has nothing to do with
751 // change tags, but it appears to be the only other place in core where we
752 // perform logged actions on log items.
753 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
754 }
755
756 if ( !$logEntry->getTarget() ) {
757 // target is required, so we have to set something
758 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
759 }
760
761 $logParams = [
762 '4::revid' => $rev_id,
763 '5::logid' => $log_id,
764 '6:list:tagsAdded' => $tagsAdded,
765 '7:number:tagsAddedCount' => count( $tagsAdded ),
766 '8:list:tagsRemoved' => $tagsRemoved,
767 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
768 'initialTags' => $initialTags,
769 ];
770 $logEntry->setParameters( $logParams );
771 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
772
773 $dbw = wfGetDB( DB_MASTER );
774 $logId = $logEntry->insert( $dbw );
775 // Only send this to UDP, not RC, similar to patrol events
776 $logEntry->publish( $logId, 'udp' );
777
778 return Status::newGood( (object)[
779 'logId' => $logId,
780 'addedTags' => $tagsAdded,
781 'removedTags' => $tagsRemoved,
782 ] );
783 }
784
805 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
806 &$join_conds, &$options, $filter_tag = ''
807 ) {
808 global $wgUseTagFilter;
809
810 // Normalize to arrays
811 $tables = (array)$tables;
812 $fields = (array)$fields;
813 $conds = (array)$conds;
814 $options = (array)$options;
815
816 $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
817
818 // Figure out which ID field to use
819 if ( in_array( 'recentchanges', $tables ) ) {
820 $join_cond = 'ct_rc_id=rc_id';
821 } elseif ( in_array( 'logging', $tables ) ) {
822 $join_cond = 'ct_log_id=log_id';
823 } elseif ( in_array( 'revision', $tables ) ) {
824 $join_cond = 'ct_rev_id=rev_id';
825 } elseif ( in_array( 'archive', $tables ) ) {
826 $join_cond = 'ct_rev_id=ar_rev_id';
827 } else {
828 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
829 }
830
831 if ( !$wgUseTagFilter ) {
832 return;
833 }
834
835 if ( !is_array( $filter_tag ) ) {
836 // some callers provide false or null
837 $filter_tag = (string)$filter_tag;
838 }
839
840 if ( $filter_tag !== [] && $filter_tag !== '' ) {
841 // Somebody wants to filter on a tag.
842 // Add an INNER JOIN on change_tag
843
844 $tagTable = 'change_tag';
845 if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
846 $db = wfGetDB( DB_REPLICA );
847
848 if ( $db->getType() === 'mysql' ) {
849 // When filtering by tag, we are using the change_tag table twice:
850 // Once in a join for filtering, and once in a sub-query to list all
851 // tags for each revision. This does not work with temporary tables
852 // on some versions of MySQL, which causes phpunit tests to fail.
853 // As a hacky workaround, we copy the temporary table, and join
854 // against the copy. It is acknowledge that this is quite horrific.
855 // Discuss at T256006.
856
857 $tagTable = 'change_tag_for_display_query';
858 $db->query(
859 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
860 . ' LIKE ' . $db->tableName( 'change_tag' )
861 );
862 $db->query(
863 'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
864 . ' SELECT * FROM ' . $db->tableName( 'change_tag' )
865 );
866 }
867 }
868
869 $tables[] = $tagTable;
870 $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
871 $filterTagIds = [];
872 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
873 foreach ( (array)$filter_tag as $filterTagName ) {
874 try {
875 $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
876 } catch ( NameTableAccessException $exception ) {
877 // Return nothing.
878 $conds[] = '0=1';
879 break;
880 }
881 }
882
883 if ( $filterTagIds !== [] ) {
884 $conds['ct_tag_id'] = $filterTagIds;
885 }
886
887 if (
888 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
889 !in_array( 'DISTINCT', $options )
890 ) {
891 $options[] = 'DISTINCT';
892 }
893 }
894 }
895
904 public static function makeTagSummarySubquery( $tables ) {
905 // Normalize to arrays
906 $tables = (array)$tables;
907
908 // Figure out which ID field to use
909 if ( in_array( 'recentchanges', $tables ) ) {
910 $join_cond = 'ct_rc_id=rc_id';
911 } elseif ( in_array( 'logging', $tables ) ) {
912 $join_cond = 'ct_log_id=log_id';
913 } elseif ( in_array( 'revision', $tables ) ) {
914 $join_cond = 'ct_rev_id=rev_id';
915 } elseif ( in_array( 'archive', $tables ) ) {
916 $join_cond = 'ct_rev_id=ar_rev_id';
917 } else {
918 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
919 }
920
921 $tagTables = [ 'change_tag', 'change_tag_def' ];
922 $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
923 $field = 'ctd_name';
924
925 return wfGetDB( DB_REPLICA )->buildGroupConcatField(
926 ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
927 );
928 }
929
941 public static function buildTagFilterSelector(
942 $selected = '', $ooui = false, IContextSource $context = null
943 ) {
944 if ( !$context ) {
945 $context = RequestContext::getMain();
946 }
947
948 $config = $context->getConfig();
949 if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
950 return [];
951 }
952
953 $data = [
954 Html::rawElement(
955 'label',
956 [ 'for' => 'tagfilter' ],
957 $context->msg( 'tag-filter' )->parse()
958 )
959 ];
960
961 if ( $ooui ) {
962 $data[] = new OOUI\TextInputWidget( [
963 'id' => 'tagfilter',
964 'name' => 'tagfilter',
965 'value' => $selected,
966 'classes' => 'mw-tagfilter-input',
967 ] );
968 } else {
969 $data[] = Xml::input(
970 'tagfilter',
971 20,
972 $selected,
973 [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
974 );
975 }
976
977 return $data;
978 }
979
988 public static function defineTag( $tag ) {
989 $dbw = wfGetDB( DB_MASTER );
990 $tagDef = [
991 'ctd_name' => $tag,
992 'ctd_user_defined' => 1,
993 'ctd_count' => 0
994 ];
995 $dbw->upsert(
996 'change_tag_def',
997 $tagDef,
998 'ctd_name',
999 [ 'ctd_user_defined' => 1 ],
1000 __METHOD__
1001 );
1002
1003 // clear the memcache of defined tags
1005 }
1006
1015 public static function undefineTag( $tag ) {
1016 $dbw = wfGetDB( DB_MASTER );
1017
1018 $dbw->update(
1019 'change_tag_def',
1020 [ 'ctd_user_defined' => 0 ],
1021 [ 'ctd_name' => $tag ],
1022 __METHOD__
1023 );
1024
1025 $dbw->delete(
1026 'change_tag_def',
1027 [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1028 __METHOD__
1029 );
1030
1031 // clear the memcache of defined tags
1033 }
1034
1049 protected static function logTagManagementAction( $action, $tag, $reason,
1050 User $user, $tagCount = null, array $logEntryTags = []
1051 ) {
1052 $dbw = wfGetDB( DB_MASTER );
1053
1054 $logEntry = new ManualLogEntry( 'managetags', $action );
1055 $logEntry->setPerformer( $user );
1056 // target page is not relevant, but it has to be set, so we just put in
1057 // the title of Special:Tags
1058 $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1059 $logEntry->setComment( $reason );
1060
1061 $params = [ '4::tag' => $tag ];
1062 if ( $tagCount !== null ) {
1063 $params['5:number:count'] = $tagCount;
1064 }
1065 $logEntry->setParameters( $params );
1066 $logEntry->setRelations( [ 'Tag' => $tag ] );
1067 $logEntry->addTags( $logEntryTags );
1068
1069 $logId = $logEntry->insert( $dbw );
1070 $logEntry->publish( $logId );
1071 return $logId;
1072 }
1073
1083 public static function canActivateTag( $tag, User $user = null ) {
1084 if ( $user !== null ) {
1085 if ( !MediaWikiServices::getInstance()->getPermissionManager()
1086 ->userHasRight( $user, 'managechangetags' )
1087 ) {
1088 return Status::newFatal( 'tags-manage-no-permission' );
1089 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1090 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1091 }
1092 }
1093
1094 // defined tags cannot be activated (a defined tag is either extension-
1095 // defined, in which case the extension chooses whether or not to active it;
1096 // or user-defined, in which case it is considered active)
1097 $definedTags = self::listDefinedTags();
1098 if ( in_array( $tag, $definedTags ) ) {
1099 return Status::newFatal( 'tags-activate-not-allowed', $tag );
1100 }
1101
1102 // non-existing tags cannot be activated
1103 $tagUsage = self::tagUsageStatistics();
1104 if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1105 return Status::newFatal( 'tags-activate-not-found', $tag );
1106 }
1107
1108 return Status::newGood();
1109 }
1110
1128 public static function activateTagWithChecks( $tag, $reason, User $user,
1129 $ignoreWarnings = false, array $logEntryTags = []
1130 ) {
1131 // are we allowed to do this?
1132 $result = self::canActivateTag( $tag, $user );
1133 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1134 $result->value = null;
1135 return $result;
1136 }
1137
1138 // do it!
1139 self::defineTag( $tag );
1140
1141 // log it
1142 $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
1143 null, $logEntryTags );
1144
1145 return Status::newGood( $logId );
1146 }
1147
1157 public static function canDeactivateTag( $tag, User $user = null ) {
1158 if ( $user !== null ) {
1159 if ( !MediaWikiServices::getInstance()->getPermissionManager()
1160 ->userHasRight( $user, 'managechangetags' )
1161 ) {
1162 return Status::newFatal( 'tags-manage-no-permission' );
1163 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1164 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1165 }
1166 }
1167
1168 // only explicitly-defined tags can be deactivated
1169 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1170 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1171 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1172 }
1173 return Status::newGood();
1174 }
1175
1193 public static function deactivateTagWithChecks( $tag, $reason, User $user,
1194 $ignoreWarnings = false, array $logEntryTags = []
1195 ) {
1196 // are we allowed to do this?
1197 $result = self::canDeactivateTag( $tag, $user );
1198 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1199 $result->value = null;
1200 return $result;
1201 }
1202
1203 // do it!
1204 self::undefineTag( $tag );
1205
1206 // log it
1207 $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
1208 null, $logEntryTags );
1209
1210 return Status::newGood( $logId );
1211 }
1212
1220 public static function isTagNameValid( $tag ) {
1221 // no empty tags
1222 if ( $tag === '' ) {
1223 return Status::newFatal( 'tags-create-no-name' );
1224 }
1225
1226 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1227 // pipe (used as a delimiter between multiple tags in
1228 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1229 // MediaWiki namespace)
1230 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1231 || strpos( $tag, '/' ) !== false ) {
1232 return Status::newFatal( 'tags-create-invalid-chars' );
1233 }
1234
1235 // could the MediaWiki namespace description messages be created?
1236 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1237 if ( $title === null ) {
1238 return Status::newFatal( 'tags-create-invalid-title-chars' );
1239 }
1240
1241 return Status::newGood();
1242 }
1243
1256 public static function canCreateTag( $tag, User $user = null ) {
1257 if ( $user !== null ) {
1258 if ( !MediaWikiServices::getInstance()->getPermissionManager()
1259 ->userHasRight( $user, 'managechangetags' )
1260 ) {
1261 return Status::newFatal( 'tags-manage-no-permission' );
1262 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1263 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1264 }
1265 }
1266
1267 $status = self::isTagNameValid( $tag );
1268 if ( !$status->isGood() ) {
1269 return $status;
1270 }
1271
1272 // does the tag already exist?
1273 $tagUsage = self::tagUsageStatistics();
1274 if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1275 return Status::newFatal( 'tags-create-already-exists', $tag );
1276 }
1277
1278 // check with hooks
1279 $canCreateResult = Status::newGood();
1280 Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1281 return $canCreateResult;
1282 }
1283
1303 public static function createTagWithChecks( $tag, $reason, User $user,
1304 $ignoreWarnings = false, array $logEntryTags = []
1305 ) {
1306 // are we allowed to do this?
1307 $result = self::canCreateTag( $tag, $user );
1308 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1309 $result->value = null;
1310 return $result;
1311 }
1312
1313 // do it!
1314 self::defineTag( $tag );
1315
1316 // log it
1317 $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1318 null, $logEntryTags );
1319
1320 return Status::newGood( $logId );
1321 }
1322
1335 public static function deleteTagEverywhere( $tag ) {
1336 $dbw = wfGetDB( DB_MASTER );
1337 $dbw->startAtomic( __METHOD__ );
1338
1339 // fetch tag id, this must be done before calling undefineTag(), see T225564
1340 $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1341
1342 // set ctd_user_defined = 0
1343 self::undefineTag( $tag );
1344
1345 // delete from change_tag
1346 $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1347 $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1348 $dbw->endAtomic( __METHOD__ );
1349
1350 // give extensions a chance
1351 $status = Status::newGood();
1352 Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1353 // let's not allow error results, as the actual tag deletion succeeded
1354 if ( !$status->isOK() ) {
1355 wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1356 $status->setOK( true );
1357 }
1358
1359 // clear the memcache of defined tags
1361
1362 return $status;
1363 }
1364
1377 public static function canDeleteTag( $tag, User $user = null, int $flags = 0 ) {
1378 $tagUsage = self::tagUsageStatistics();
1379
1380 if ( $user !== null ) {
1381 if ( !MediaWikiServices::getInstance()->getPermissionManager()
1382 ->userHasRight( $user, 'deletechangetags' )
1383 ) {
1384 return Status::newFatal( 'tags-delete-no-permission' );
1385 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1386 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1387 }
1388 }
1389
1390 if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1391 return Status::newFatal( 'tags-delete-not-found', $tag );
1392 }
1393
1394 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1395 isset( $tagUsage[$tag] ) &&
1396 $tagUsage[$tag] > self::MAX_DELETE_USES
1397 ) {
1398 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1399 }
1400
1401 $softwareDefined = self::listSoftwareDefinedTags();
1402 if ( in_array( $tag, $softwareDefined ) ) {
1403 // extension-defined tags can't be deleted unless the extension
1404 // specifically allows it
1405 $status = Status::newFatal( 'tags-delete-not-allowed' );
1406 } else {
1407 // user-defined tags are deletable unless otherwise specified
1408 $status = Status::newGood();
1409 }
1410
1411 Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1412 return $status;
1413 }
1414
1432 public static function deleteTagWithChecks( $tag, $reason, User $user,
1433 $ignoreWarnings = false, array $logEntryTags = []
1434 ) {
1435 // are we allowed to do this?
1436 $result = self::canDeleteTag( $tag, $user );
1437 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1438 $result->value = null;
1439 return $result;
1440 }
1441
1442 // store the tag usage statistics
1443 $tagUsage = self::tagUsageStatistics();
1444 $hitcount = $tagUsage[$tag] ?? 0;
1445
1446 // do it!
1447 $deleteResult = self::deleteTagEverywhere( $tag );
1448 if ( !$deleteResult->isOK() ) {
1449 return $deleteResult;
1450 }
1451
1452 // log it
1453 $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1454 $hitcount, $logEntryTags );
1455
1456 $deleteResult->value = $logId;
1457 return $deleteResult;
1458 }
1459
1466 public static function listSoftwareActivatedTags() {
1467 // core active tags
1468 $tags = self::getSoftwareTags();
1469 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1470 if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1471 return $tags;
1472 }
1473 $hookRunner = new HookRunner( $hookContainer );
1474 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1475 return $cache->getWithSetCallback(
1476 $cache->makeKey( 'active-tags' ),
1477 WANObjectCache::TTL_MINUTE * 5,
1478 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1479 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1480
1481 // Ask extensions which tags they consider active
1482 $hookRunner->onChangeTagsListActive( $tags );
1483 return $tags;
1484 },
1485 [
1486 'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1487 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1488 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1489 ]
1490 );
1491 }
1492
1499 public static function listDefinedTags() {
1502 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1503 }
1504
1513 public static function listExplicitlyDefinedTags() {
1514 $fname = __METHOD__;
1515
1516 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1517 return $cache->getWithSetCallback(
1518 $cache->makeKey( 'valid-tags-db' ),
1519 WANObjectCache::TTL_MINUTE * 5,
1520 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1521 $dbr = wfGetDB( DB_REPLICA );
1522
1523 $setOpts += Database::getCacheSetOptions( $dbr );
1524
1525 $tags = $dbr->selectFieldValues(
1526 'change_tag_def',
1527 'ctd_name',
1528 [ 'ctd_user_defined' => 1 ],
1529 $fname
1530 );
1531
1532 return array_unique( $tags );
1533 },
1534 [
1535 'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1536 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1537 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1538 ]
1539 );
1540 }
1541
1551 public static function listSoftwareDefinedTags() {
1552 // core defined tags
1553 $tags = self::getSoftwareTags( true );
1554 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1555 if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1556 return $tags;
1557 }
1558 $hookRunner = new HookRunner( $hookContainer );
1559 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1560 return $cache->getWithSetCallback(
1561 $cache->makeKey( 'valid-tags-hook' ),
1562 WANObjectCache::TTL_MINUTE * 5,
1563 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1564 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1565
1566 $hookRunner->onListDefinedTags( $tags );
1567 return array_unique( $tags );
1568 },
1569 [
1570 'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1571 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1572 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1573 ]
1574 );
1575 }
1576
1582 public static function purgeTagCacheAll() {
1583 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1584
1585 $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1586 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1587 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1588 $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1589
1590 MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1591 }
1592
1599 public static function tagUsageStatistics() {
1600 $fname = __METHOD__;
1601
1602 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1603 return $cache->getWithSetCallback(
1604 $cache->makeKey( 'tags-usage-statistics' ),
1605 WANObjectCache::TTL_MINUTE * 5,
1606 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1607 $dbr = wfGetDB( DB_REPLICA );
1608 $res = $dbr->select(
1609 'change_tag_def',
1610 [ 'ctd_name', 'ctd_count' ],
1611 [],
1612 $fname,
1613 [ 'ORDER BY' => 'ctd_count DESC' ]
1614 );
1615
1616 $out = [];
1617 foreach ( $res as $row ) {
1618 $out[$row->ctd_name] = $row->ctd_count;
1619 }
1620
1621 return $out;
1622 },
1623 [
1624 'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1625 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1626 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1627 ]
1628 );
1629 }
1630
1645 public static function showTagEditingUI( User $user ) {
1646 return MediaWikiServices::getInstance()->getPermissionManager()
1647 ->userHasRight( $user, 'changetags' ) &&
1648 (bool)self::listExplicitlyDefinedTags();
1649 }
1650}
getPermissionManager()
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
array $wgSoftwareTags
List of core tags to enable.
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
static truncateTagDescription( $tag, $length, IContextSource $context)
Get truncated message for the tag's long description.
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.
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
static canDeleteTag( $tag, User $user=null, int $flags=0)
Is it OK to allow the user to delete this tag?
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.
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
static logTagManagementAction( $action, $tag, $reason, User $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
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...
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
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.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
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 canCreateTag( $tag, User $user=null)
Is it OK to allow the user to create 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 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 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.
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
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,...
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
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 canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
static $definedSoftwareTags
A list of tags defined and used by MediaWiki itself.
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.
static isTagNameValid( $tag)
Is the tag name valid?
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, User $user=null)
Is it OK to allow the user to adds and remove the given tags tags to/from a change?
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
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...
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...
MediaWikiServices is the service locator for the application scope of MediaWiki.
Exception representing a failure to look up a row from a name table.
static plaintextParam( $plaintext)
Definition Message.php:1130
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,...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition User.php:1747
Relational database abstraction object.
Definition Database.php:50
const NS_MEDIAWIKI
Definition Defines.php:78
Interface for objects which can provide a MediaWiki context on request.
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:38
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
if(!isset( $args[0])) $lang