Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / includes / ChangeTags.php
blobd597d6d4737764a718a7d4896a4e2e64dd522975
1 <?php
2 /**
3 * Recent changes tagging.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
23 class ChangeTags {
24 /**
25 * Can't delete tags with more than this many uses. Similar in intent to
26 * the bigdelete user right
27 * @todo Use the job queue for tag deletion to avoid this restriction
29 const MAX_DELETE_USES = 5000;
31 /**
32 * Creates HTML for the given tags
34 * @param string $tags Comma-separated list of tags
35 * @param string $page A label for the type of action which is being displayed,
36 * for example: 'history', 'contributions' or 'newpages'
37 * @return array Array with two items: (html, classes)
38 * - html: String: HTML for displaying the tags (empty string when param $tags is empty)
39 * - classes: Array of strings: CSS classes used in the generated html, one class for each tag
41 public static function formatSummaryRow( $tags, $page ) {
42 global $wgLang;
44 $tags = explode( ',', $tags );
46 // XXX(Ori Livneh, 2014-11-08): remove once bug 73181 is resolved.
47 $tags = array_diff( $tags, array( 'HHVM', '' ) );
49 if ( !$tags ) {
50 return array( '', array() );
53 $classes = array();
55 $displayTags = array();
56 foreach ( $tags as $tag ) {
57 $displayTags[] = Xml::tags(
58 'span',
59 array( 'class' => 'mw-tag-marker ' .
60 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ),
61 self::tagDescription( $tag )
63 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
65 $markers = wfMessage( 'tag-list-wrapper' )
66 ->numParams( count( $displayTags ) )
67 ->rawParams( $wgLang->commaList( $displayTags ) )
68 ->parse();
69 $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
71 return array( $markers, $classes );
74 /**
75 * Get a short description for a tag
77 * @param string $tag Tag
79 * @return string Short description of the tag from "mediawiki:tag-$tag" if this message exists,
80 * html-escaped version of $tag otherwise
82 public static function tagDescription( $tag ) {
83 $msg = wfMessage( "tag-$tag" );
84 return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag );
87 /**
88 * Add tags to a change given its rc_id, rev_id and/or log_id
90 * @param string|array $tags Tags to add to the change
91 * @param int|null $rc_id The rc_id of the change to add the tags to
92 * @param int|null $rev_id The rev_id of the change to add the tags to
93 * @param int|null $log_id The log_id of the change to add the tags to
94 * @param string $params Params to put in the ct_params field of table 'change_tag'
96 * @throws MWException
97 * @return bool False if no changes are made, otherwise true
99 * @exception MWException When $rc_id, $rev_id and $log_id are all null
101 public static function addTags( $tags, $rc_id = null, $rev_id = null,
102 $log_id = null, $params = null
104 if ( !is_array( $tags ) ) {
105 $tags = array( $tags );
108 $tags = array_filter( $tags ); // Make sure we're submitting all tags...
110 if ( !$rc_id && !$rev_id && !$log_id ) {
111 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
112 'specified when adding a tag to a change!' );
115 $dbw = wfGetDB( DB_MASTER );
117 // Might as well look for rcids and so on.
118 if ( !$rc_id ) {
119 // Info might be out of date, somewhat fractionally, on slave.
120 if ( $log_id ) {
121 $rc_id = $dbw->selectField(
122 'recentchanges',
123 'rc_id',
124 array( 'rc_logid' => $log_id ),
125 __METHOD__
127 } elseif ( $rev_id ) {
128 $rc_id = $dbw->selectField(
129 'recentchanges',
130 'rc_id',
131 array( 'rc_this_oldid' => $rev_id ),
132 __METHOD__
135 } elseif ( !$log_id && !$rev_id ) {
136 // Info might be out of date, somewhat fractionally, on slave.
137 $log_id = $dbw->selectField(
138 'recentchanges',
139 'rc_logid',
140 array( 'rc_id' => $rc_id ),
141 __METHOD__
143 $rev_id = $dbw->selectField(
144 'recentchanges',
145 'rc_this_oldid',
146 array( 'rc_id' => $rc_id ),
147 __METHOD__
151 $tsConds = array_filter( array(
152 'ts_rc_id' => $rc_id,
153 'ts_rev_id' => $rev_id,
154 'ts_log_id' => $log_id )
157 // Update the summary row.
158 // $prevTags can be out of date on slaves, especially when addTags is called consecutively,
159 // causing loss of tags added recently in tag_summary table.
160 $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
161 $prevTags = $prevTags ? $prevTags : '';
162 $prevTags = array_filter( explode( ',', $prevTags ) );
163 $newTags = array_unique( array_merge( $prevTags, $tags ) );
164 sort( $prevTags );
165 sort( $newTags );
167 if ( $prevTags == $newTags ) {
168 // No change.
169 return false;
172 $dbw->replace(
173 'tag_summary',
174 array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
175 array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
176 __METHOD__
179 // Insert the tags rows.
180 $tagsRows = array();
181 foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
182 $tagsRows[] = array_filter(
183 array(
184 'ct_tag' => $tag,
185 'ct_rc_id' => $rc_id,
186 'ct_log_id' => $log_id,
187 'ct_rev_id' => $rev_id,
188 'ct_params' => $params
193 $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
195 self::purgeTagUsageCache();
196 return true;
200 * Applies all tags-related changes to a query.
201 * Handles selecting tags, and filtering.
202 * Needs $tables to be set up properly, so we can figure out which join conditions to use.
204 * @param string|array $tables Table names, see DatabaseBase::select
205 * @param string|array $fields Fields used in query, see DatabaseBase::select
206 * @param string|array $conds Conditions used in query, see DatabaseBase::select
207 * @param array $join_conds Join conditions, see DatabaseBase::select
208 * @param array $options Options, see Database::select
209 * @param bool|string $filter_tag Tag to select on
211 * @throws MWException When unable to determine appropriate JOIN condition for tagging
213 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
214 &$join_conds, &$options, $filter_tag = false ) {
215 global $wgRequest, $wgUseTagFilter;
217 if ( $filter_tag === false ) {
218 $filter_tag = $wgRequest->getVal( 'tagfilter' );
221 // Figure out which conditions can be done.
222 if ( in_array( 'recentchanges', $tables ) ) {
223 $join_cond = 'ct_rc_id=rc_id';
224 } elseif ( in_array( 'logging', $tables ) ) {
225 $join_cond = 'ct_log_id=log_id';
226 } elseif ( in_array( 'revision', $tables ) ) {
227 $join_cond = 'ct_rev_id=rev_id';
228 } elseif ( in_array( 'archive', $tables ) ) {
229 $join_cond = 'ct_rev_id=ar_rev_id';
230 } else {
231 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
234 $fields['ts_tags'] = wfGetDB( DB_SLAVE )->buildGroupConcatField(
235 ',', 'change_tag', 'ct_tag', $join_cond
238 if ( $wgUseTagFilter && $filter_tag ) {
239 // Somebody wants to filter on a tag.
240 // Add an INNER JOIN on change_tag
242 $tables[] = 'change_tag';
243 $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond );
244 $conds['ct_tag'] = $filter_tag;
249 * Build a text box to select a change tag
251 * @param string $selected Tag to select by default
252 * @param bool $fullForm
253 * - if false, then it returns an array of (label, form).
254 * - if true, it returns an entire form around the selector.
255 * @param Title $title Title object to send the form to.
256 * Used when, and only when $fullForm is true.
257 * @return string|array
258 * - if $fullForm is false: Array with
259 * - if $fullForm is true: String, html fragment
261 public static function buildTagFilterSelector( $selected = '',
262 $fullForm = false, Title $title = null
264 global $wgUseTagFilter;
266 if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
267 return $fullForm ? '' : array();
270 $data = array(
271 Html::rawElement(
272 'label',
273 array( 'for' => 'tagfilter' ),
274 wfMessage( 'tag-filter' )->parse()
276 Xml::input(
277 'tagfilter',
279 $selected,
280 array( 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' )
284 if ( !$fullForm ) {
285 return $data;
288 $html = implode( '&#160;', $data );
289 $html .= "\n" .
290 Xml::element(
291 'input',
292 array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() )
294 $html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() );
295 $html = Xml::tags(
296 'form',
297 array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ),
298 $html
301 return $html;
305 * Defines a tag in the valid_tag table, without checking that the tag name
306 * is valid.
307 * Extensions should NOT use this function; they can use the ListDefinedTags
308 * hook instead.
310 * @param string $tag Tag to create
311 * @since 1.25
313 public static function defineTag( $tag ) {
314 $dbw = wfGetDB( DB_MASTER );
315 $dbw->replace( 'valid_tag',
316 array( 'vt_tag' ),
317 array( 'vt_tag' => $tag ),
318 __METHOD__ );
320 // clear the memcache of defined tags
321 self::purgeTagCacheAll();
325 * Removes a tag from the valid_tag table. The tag may remain in use by
326 * extensions, and may still show up as 'defined' if an extension is setting
327 * it from the ListDefinedTags hook.
329 * @param string $tag Tag to remove
330 * @since 1.25
332 public static function undefineTag( $tag ) {
333 $dbw = wfGetDB( DB_MASTER );
334 $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ), __METHOD__ );
336 // clear the memcache of defined tags
337 self::purgeTagCacheAll();
341 * Writes a tag action into the tag management log.
343 * @param string $action
344 * @param string $tag
345 * @param string $reason
346 * @param User $user Who to attribute the action to
347 * @param int $tagCount For deletion only, how many usages the tag had before
348 * it was deleted.
349 * @since 1.25
351 protected static function logTagAction( $action, $tag, $reason, User $user,
352 $tagCount = null ) {
354 $dbw = wfGetDB( DB_MASTER );
356 $logEntry = new ManualLogEntry( 'managetags', $action );
357 $logEntry->setPerformer( $user );
358 // target page is not relevant, but it has to be set, so we just put in
359 // the title of Special:Tags
360 $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
361 $logEntry->setComment( $reason );
363 $params = array( '4::tag' => $tag );
364 if ( !is_null( $tagCount ) ) {
365 $params['5:number:count'] = $tagCount;
367 $logEntry->setParameters( $params );
368 $logEntry->setRelations( array( 'Tag' => $tag ) );
370 $logId = $logEntry->insert( $dbw );
371 $logEntry->publish( $logId );
372 return $logId;
376 * Is it OK to allow the user to activate this tag?
378 * @param string $tag Tag that you are interested in activating
379 * @param User|null $user User whose permission you wish to check, or null if
380 * you don't care (e.g. maintenance scripts)
381 * @return Status
382 * @since 1.25
384 public static function canActivateTag( $tag, User $user = null ) {
385 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
386 return Status::newFatal( 'tags-manage-no-permission' );
389 // non-existing tags cannot be activated
390 $tagUsage = self::tagUsageStatistics();
391 if ( !isset( $tagUsage[$tag] ) ) {
392 return Status::newFatal( 'tags-activate-not-found', $tag );
395 // defined tags cannot be activated (a defined tag is either extension-
396 // defined, in which case the extension chooses whether or not to active it;
397 // or user-defined, in which case it is considered active)
398 $definedTags = self::listDefinedTags();
399 if ( in_array( $tag, $definedTags ) ) {
400 return Status::newFatal( 'tags-activate-not-allowed', $tag );
403 return Status::newGood();
407 * Activates a tag, checking whether it is allowed first, and adding a log
408 * entry afterwards.
410 * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need
411 * to do that.
413 * @param string $tag
414 * @param string $reason
415 * @param User $user Who to give credit for the action
416 * @param bool $ignoreWarnings Can be used for API interaction, default false
417 * @return Status If successful, the Status contains the ID of the added log
418 * entry as its value
419 * @since 1.25
421 public static function activateTagWithChecks( $tag, $reason, User $user,
422 $ignoreWarnings = false ) {
424 // are we allowed to do this?
425 $result = self::canActivateTag( $tag, $user );
426 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
427 $result->value = null;
428 return $result;
431 // do it!
432 self::defineTag( $tag );
434 // log it
435 $logId = self::logTagAction( 'activate', $tag, $reason, $user );
436 return Status::newGood( $logId );
440 * Is it OK to allow the user to deactivate this tag?
442 * @param string $tag Tag that you are interested in deactivating
443 * @param User|null $user User whose permission you wish to check, or null if
444 * you don't care (e.g. maintenance scripts)
445 * @return Status
446 * @since 1.25
448 public static function canDeactivateTag( $tag, User $user = null ) {
449 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
450 return Status::newFatal( 'tags-manage-no-permission' );
453 // only explicitly-defined tags can be deactivated
454 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
455 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
456 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
458 return Status::newGood();
462 * Deactivates a tag, checking whether it is allowed first, and adding a log
463 * entry afterwards.
465 * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need
466 * to do that.
468 * @param string $tag
469 * @param string $reason
470 * @param User $user Who to give credit for the action
471 * @param bool $ignoreWarnings Can be used for API interaction, default false
472 * @return Status If successful, the Status contains the ID of the added log
473 * entry as its value
474 * @since 1.25
476 public static function deactivateTagWithChecks( $tag, $reason, User $user,
477 $ignoreWarnings = false ) {
479 // are we allowed to do this?
480 $result = self::canDeactivateTag( $tag, $user );
481 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
482 $result->value = null;
483 return $result;
486 // do it!
487 self::undefineTag( $tag );
489 // log it
490 $logId = self::logTagAction( 'deactivate', $tag, $reason, $user );
491 return Status::newGood( $logId );
495 * Is it OK to allow the user to create this tag?
497 * @param string $tag Tag that you are interested in creating
498 * @param User|null $user User whose permission you wish to check, or null if
499 * you don't care (e.g. maintenance scripts)
500 * @return Status
501 * @since 1.25
503 public static function canCreateTag( $tag, User $user = null ) {
504 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
505 return Status::newFatal( 'tags-manage-no-permission' );
508 // no empty tags
509 if ( $tag === '' ) {
510 return Status::newFatal( 'tags-create-no-name' );
513 // tags cannot contain commas (used as a delimiter in tag_summary table) or
514 // slashes (would break tag description messages in MediaWiki namespace)
515 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '/' ) !== false ) {
516 return Status::newFatal( 'tags-create-invalid-chars' );
519 // could the MediaWiki namespace description messages be created?
520 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
521 if ( is_null( $title ) ) {
522 return Status::newFatal( 'tags-create-invalid-title-chars' );
525 // does the tag already exist?
526 $tagUsage = self::tagUsageStatistics();
527 if ( isset( $tagUsage[$tag] ) ) {
528 return Status::newFatal( 'tags-create-already-exists', $tag );
531 // check with hooks
532 $canCreateResult = Status::newGood();
533 Hooks::run( 'ChangeTagCanCreate', array( $tag, $user, &$canCreateResult ) );
534 return $canCreateResult;
538 * Creates a tag by adding a row to the `valid_tag` table.
540 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
541 * do that.
543 * @param string $tag
544 * @param string $reason
545 * @param User $user Who to give credit for the action
546 * @param bool $ignoreWarnings Can be used for API interaction, default false
547 * @return Status If successful, the Status contains the ID of the added log
548 * entry as its value
549 * @since 1.25
551 public static function createTagWithChecks( $tag, $reason, User $user,
552 $ignoreWarnings = false ) {
554 // are we allowed to do this?
555 $result = self::canCreateTag( $tag, $user );
556 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
557 $result->value = null;
558 return $result;
561 // do it!
562 self::defineTag( $tag );
564 // log it
565 $logId = self::logTagAction( 'create', $tag, $reason, $user );
566 return Status::newGood( $logId );
570 * Permanently removes all traces of a tag from the DB. Good for removing
571 * misspelt or temporary tags.
573 * This function should be directly called by maintenance scripts only, never
574 * by user-facing code. See deleteTagWithChecks() for functionality that can
575 * safely be exposed to users.
577 * @param string $tag Tag to remove
578 * @return Status The returned status will be good unless a hook changed it
579 * @since 1.25
581 public static function deleteTagEverywhere( $tag ) {
582 $dbw = wfGetDB( DB_MASTER );
583 $dbw->begin( __METHOD__ );
585 // delete from valid_tag
586 self::undefineTag( $tag );
588 // find out which revisions use this tag, so we can delete from tag_summary
589 $result = $dbw->select( 'change_tag',
590 array( 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ),
591 array( 'ct_tag' => $tag ),
592 __METHOD__ );
593 foreach ( $result as $row ) {
594 if ( $row->ct_rev_id ) {
595 $field = 'ts_rev_id';
596 $fieldValue = $row->ct_rev_id;
597 } elseif ( $row->ct_log_id ) {
598 $field = 'ts_log_id';
599 $fieldValue = $row->ct_log_id;
600 } elseif ( $row->ct_rc_id ) {
601 $field = 'ts_rc_id';
602 $fieldValue = $row->ct_rc_id;
603 } else {
604 // don't know what's up; just skip it
605 continue;
608 // remove the tag from the relevant row of tag_summary
609 $tsResult = $dbw->selectField( 'tag_summary',
610 'ts_tags',
611 array( $field => $fieldValue ),
612 __METHOD__ );
613 $tsValues = explode( ',', $tsResult );
614 $tsValues = array_values( array_diff( $tsValues, array( $tag ) ) );
615 if ( !$tsValues ) {
616 // no tags left, so delete the row altogether
617 $dbw->delete( 'tag_summary',
618 array( $field => $fieldValue ),
619 __METHOD__ );
620 } else {
621 $dbw->update( 'tag_summary',
622 array( 'ts_tags' => implode( ',', $tsValues ) ),
623 array( $field => $fieldValue ),
624 __METHOD__ );
628 // delete from change_tag
629 $dbw->delete( 'change_tag', array( 'ct_tag' => $tag ), __METHOD__ );
631 $dbw->commit( __METHOD__ );
633 // give extensions a chance
634 $status = Status::newGood();
635 Hooks::run( 'ChangeTagAfterDelete', array( $tag, &$status ) );
636 // let's not allow error results, as the actual tag deletion succeeded
637 if ( !$status->isOK() ) {
638 wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
639 $status->ok = true;
642 // clear the memcache of defined tags
643 self::purgeTagCacheAll();
645 return $status;
649 * Is it OK to allow the user to delete this tag?
651 * @param string $tag Tag that you are interested in deleting
652 * @param User|null $user User whose permission you wish to check, or null if
653 * you don't care (e.g. maintenance scripts)
654 * @return Status
655 * @since 1.25
657 public static function canDeleteTag( $tag, User $user = null ) {
658 $tagUsage = self::tagUsageStatistics();
660 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
661 return Status::newFatal( 'tags-manage-no-permission' );
664 if ( !isset( $tagUsage[$tag] ) ) {
665 return Status::newFatal( 'tags-delete-not-found', $tag );
668 if ( $tagUsage[$tag] > self::MAX_DELETE_USES ) {
669 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
672 $extensionDefined = self::listExtensionDefinedTags();
673 if ( in_array( $tag, $extensionDefined ) ) {
674 // extension-defined tags can't be deleted unless the extension
675 // specifically allows it
676 $status = Status::newFatal( 'tags-delete-not-allowed' );
677 } else {
678 // user-defined tags are deletable unless otherwise specified
679 $status = Status::newGood();
682 Hooks::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) );
683 return $status;
687 * Deletes a tag, checking whether it is allowed first, and adding a log entry
688 * afterwards.
690 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
691 * do that.
693 * @param string $tag
694 * @param string $reason
695 * @param User $user Who to give credit for the action
696 * @param bool $ignoreWarnings Can be used for API interaction, default false
697 * @return Status If successful, the Status contains the ID of the added log
698 * entry as its value
699 * @since 1.25
701 public static function deleteTagWithChecks( $tag, $reason, User $user,
702 $ignoreWarnings = false ) {
704 // are we allowed to do this?
705 $result = self::canDeleteTag( $tag, $user );
706 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
707 $result->value = null;
708 return $result;
711 // store the tag usage statistics
712 $tagUsage = self::tagUsageStatistics();
714 // do it!
715 $deleteResult = self::deleteTagEverywhere( $tag );
716 if ( !$deleteResult->isOK() ) {
717 return $deleteResult;
720 // log it
721 $logId = self::logTagAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
722 $deleteResult->value = $logId;
723 return $deleteResult;
727 * Lists those tags which extensions report as being "active".
729 * @return array
730 * @since 1.25
732 public static function listExtensionActivatedTags() {
733 // Caching...
734 global $wgMemc;
735 $key = wfMemcKey( 'active-tags' );
736 $tags = $wgMemc->get( $key );
737 if ( $tags ) {
738 return $tags;
741 // ask extensions which tags they consider active
742 $extensionActive = array();
743 Hooks::run( 'ChangeTagsListActive', array( &$extensionActive ) );
745 // Short-term caching.
746 $wgMemc->set( $key, $extensionActive, 300 );
747 return $extensionActive;
751 * Basically lists defined tags which count even if they aren't applied to anything.
752 * It returns a union of the results of listExplicitlyDefinedTags() and
753 * listExtensionDefinedTags().
755 * @return string[] Array of strings: tags
757 public static function listDefinedTags() {
758 $tags1 = self::listExplicitlyDefinedTags();
759 $tags2 = self::listExtensionDefinedTags();
760 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
764 * Lists tags explicitly defined in the `valid_tag` table of the database.
765 * Tags in table 'change_tag' which are not in table 'valid_tag' are not
766 * included.
768 * Tries memcached first.
770 * @return string[] Array of strings: tags
771 * @since 1.25
773 public static function listExplicitlyDefinedTags() {
774 // Caching...
775 global $wgMemc;
776 $key = wfMemcKey( 'valid-tags-db' );
777 $tags = $wgMemc->get( $key );
778 if ( $tags ) {
779 return $tags;
782 $emptyTags = array();
784 // Some DB stuff
785 $dbr = wfGetDB( DB_SLAVE );
786 $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ );
787 foreach ( $res as $row ) {
788 $emptyTags[] = $row->vt_tag;
791 $emptyTags = array_filter( array_unique( $emptyTags ) );
793 // Short-term caching.
794 $wgMemc->set( $key, $emptyTags, 300 );
795 return $emptyTags;
799 * Lists tags defined by extensions using the ListDefinedTags hook.
800 * Extensions need only define those tags they deem to be in active use.
802 * Tries memcached first.
804 * @return string[] Array of strings: tags
805 * @since 1.25
807 public static function listExtensionDefinedTags() {
808 // Caching...
809 global $wgMemc;
810 $key = wfMemcKey( 'valid-tags-hook' );
811 $tags = $wgMemc->get( $key );
812 if ( $tags ) {
813 return $tags;
816 $emptyTags = array();
817 Hooks::run( 'ListDefinedTags', array( &$emptyTags ) );
818 $emptyTags = array_filter( array_unique( $emptyTags ) );
820 // Short-term caching.
821 $wgMemc->set( $key, $emptyTags, 300 );
822 return $emptyTags;
826 * Invalidates the short-term cache of defined tags used by the
827 * list*DefinedTags functions, as well as the tag statistics cache.
828 * @since 1.25
830 public static function purgeTagCacheAll() {
831 global $wgMemc;
832 $wgMemc->delete( wfMemcKey( 'active-tags' ) );
833 $wgMemc->delete( wfMemcKey( 'valid-tags-db' ) );
834 $wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) );
835 self::purgeTagUsageCache();
839 * Invalidates the tag statistics cache only.
840 * @since 1.25
842 public static function purgeTagUsageCache() {
843 global $wgMemc;
844 $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
848 * Returns a map of any tags used on the wiki to number of edits
849 * tagged with them, ordered descending by the hitcount.
851 * Keeps a short-term cache in memory, so calling this multiple times in the
852 * same request should be fine.
854 * @return array Array of string => int
856 public static function tagUsageStatistics() {
857 // Caching...
858 global $wgMemc;
859 $key = wfMemcKey( 'change-tag-statistics' );
860 $stats = $wgMemc->get( $key );
861 if ( $stats ) {
862 return $stats;
865 $out = array();
867 $dbr = wfGetDB( DB_SLAVE );
868 $res = $dbr->select(
869 'change_tag',
870 array( 'ct_tag', 'hitcount' => 'count(*)' ),
871 array(),
872 __METHOD__,
873 array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' )
876 foreach ( $res as $row ) {
877 $out[$row->ct_tag] = $row->hitcount;
879 foreach ( self::listDefinedTags() as $tag ) {
880 if ( !isset( $out[$tag] ) ) {
881 $out[$tag] = 0;
885 // Cache for a very short time
886 $wgMemc->set( $key, $out, 300 );
887 return $out;