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
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;
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 ) {
45 return array( '', array() );
50 $tags = explode( ',', $tags );
51 $displayTags = array();
52 foreach ( $tags as $tag ) {
53 $displayTags[] = Xml
::tags(
55 array( 'class' => 'mw-tag-marker ' .
56 Sanitizer
::escapeClass( "mw-tag-marker-$tag" ) ),
57 self
::tagDescription( $tag )
59 $classes[] = Sanitizer
::escapeClass( "mw-tag-$tag" );
61 $markers = wfMessage( 'tag-list-wrapper' )
62 ->numParams( count( $displayTags ) )
63 ->rawParams( $wgLang->commaList( $displayTags ) )
65 $markers = Xml
::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
67 return array( $markers, $classes );
71 * Get a short description for a tag
73 * @param string $tag Tag
75 * @return string Short description of the tag from "mediawiki:tag-$tag" if this message exists,
76 * html-escaped version of $tag otherwise
78 public static function tagDescription( $tag ) {
79 $msg = wfMessage( "tag-$tag" );
80 return $msg->exists() ?
$msg->parse() : htmlspecialchars( $tag );
84 * Add tags to a change given its rc_id, rev_id and/or log_id
86 * @param string|array $tags Tags to add to the change
87 * @param int|null $rc_id The rc_id of the change to add the tags to
88 * @param int|null $rev_id The rev_id of the change to add the tags to
89 * @param int|null $log_id The log_id of the change to add the tags to
90 * @param string $params Params to put in the ct_params field of table 'change_tag'
93 * @return bool False if no changes are made, otherwise true
95 * @exception MWException When $rc_id, $rev_id and $log_id are all null
97 public static function addTags( $tags, $rc_id = null, $rev_id = null,
98 $log_id = null, $params = null
100 if ( !is_array( $tags ) ) {
101 $tags = array( $tags );
104 $tags = array_filter( $tags ); // Make sure we're submitting all tags...
106 if ( !$rc_id && !$rev_id && !$log_id ) {
107 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
108 'specified when adding a tag to a change!' );
111 $dbw = wfGetDB( DB_MASTER
);
113 // Might as well look for rcids and so on.
115 // Info might be out of date, somewhat fractionally, on slave.
117 $rc_id = $dbw->selectField(
120 array( 'rc_logid' => $log_id ),
123 } elseif ( $rev_id ) {
124 $rc_id = $dbw->selectField(
127 array( 'rc_this_oldid' => $rev_id ),
131 } elseif ( !$log_id && !$rev_id ) {
132 // Info might be out of date, somewhat fractionally, on slave.
133 $log_id = $dbw->selectField(
136 array( 'rc_id' => $rc_id ),
139 $rev_id = $dbw->selectField(
142 array( 'rc_id' => $rc_id ),
147 $tsConds = array_filter( array(
148 'ts_rc_id' => $rc_id,
149 'ts_rev_id' => $rev_id,
150 'ts_log_id' => $log_id )
153 // Update the summary row.
154 // $prevTags can be out of date on slaves, especially when addTags is called consecutively,
155 // causing loss of tags added recently in tag_summary table.
156 $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__
);
157 $prevTags = $prevTags ?
$prevTags : '';
158 $prevTags = array_filter( explode( ',', $prevTags ) );
159 $newTags = array_unique( array_merge( $prevTags, $tags ) );
163 if ( $prevTags == $newTags ) {
170 array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
171 array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
175 // Insert the tags rows.
177 foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
178 $tagsRows[] = array_filter(
181 'ct_rc_id' => $rc_id,
182 'ct_log_id' => $log_id,
183 'ct_rev_id' => $rev_id,
184 'ct_params' => $params
189 $dbw->insert( 'change_tag', $tagsRows, __METHOD__
, array( 'IGNORE' ) );
191 self
::purgeTagUsageCache();
196 * Applies all tags-related changes to a query.
197 * Handles selecting tags, and filtering.
198 * Needs $tables to be set up properly, so we can figure out which join conditions to use.
200 * @param string|array $tables Table names, see DatabaseBase::select
201 * @param string|array $fields Fields used in query, see DatabaseBase::select
202 * @param string|array $conds Conditions used in query, see DatabaseBase::select
203 * @param array $join_conds Join conditions, see DatabaseBase::select
204 * @param array $options Options, see Database::select
205 * @param bool|string $filter_tag Tag to select on
207 * @throws MWException When unable to determine appropriate JOIN condition for tagging
209 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
210 &$join_conds, &$options, $filter_tag = false ) {
211 global $wgRequest, $wgUseTagFilter;
213 if ( $filter_tag === false ) {
214 $filter_tag = $wgRequest->getVal( 'tagfilter' );
217 // Figure out which conditions can be done.
218 if ( in_array( 'recentchanges', $tables ) ) {
219 $join_cond = 'ct_rc_id=rc_id';
220 } elseif ( in_array( 'logging', $tables ) ) {
221 $join_cond = 'ct_log_id=log_id';
222 } elseif ( in_array( 'revision', $tables ) ) {
223 $join_cond = 'ct_rev_id=rev_id';
224 } elseif ( in_array( 'archive', $tables ) ) {
225 $join_cond = 'ct_rev_id=ar_rev_id';
227 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
230 $fields['ts_tags'] = wfGetDB( DB_SLAVE
)->buildGroupConcatField(
231 ',', 'change_tag', 'ct_tag', $join_cond
234 if ( $wgUseTagFilter && $filter_tag ) {
235 // Somebody wants to filter on a tag.
236 // Add an INNER JOIN on change_tag
238 $tables[] = 'change_tag';
239 $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond );
240 $conds['ct_tag'] = $filter_tag;
245 * Build a text box to select a change tag
247 * @param string $selected Tag to select by default
248 * @param bool $fullForm
249 * - if false, then it returns an array of (label, form).
250 * - if true, it returns an entire form around the selector.
251 * @param Title $title Title object to send the form to.
252 * Used when, and only when $fullForm is true.
253 * @return string|array
254 * - if $fullForm is false: Array with
255 * - if $fullForm is true: String, html fragment
257 public static function buildTagFilterSelector( $selected = '',
258 $fullForm = false, Title
$title = null
260 global $wgUseTagFilter;
262 if ( !$wgUseTagFilter ||
!count( self
::listDefinedTags() ) ) {
263 return $fullForm ?
'' : array();
269 array( 'for' => 'tagfilter' ),
270 wfMessage( 'tag-filter' )->parse()
276 array( 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' )
284 $html = implode( ' ', $data );
288 array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() )
290 $html .= "\n" . Html
::hidden( 'title', $title->getPrefixedText() );
293 array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ),
301 * Defines a tag in the valid_tag table, without checking that the tag name
303 * Extensions should NOT use this function; they can use the ListDefinedTags
306 * @param string $tag Tag to create
309 public static function defineTag( $tag ) {
310 $dbw = wfGetDB( DB_MASTER
);
311 $dbw->replace( 'valid_tag',
313 array( 'vt_tag' => $tag ),
316 // clear the memcache of defined tags
317 self
::purgeTagCacheAll();
321 * Removes a tag from the valid_tag table. The tag may remain in use by
322 * extensions, and may still show up as 'defined' if an extension is setting
323 * it from the ListDefinedTags hook.
325 * @param string $tag Tag to remove
328 public static function undefineTag( $tag ) {
329 $dbw = wfGetDB( DB_MASTER
);
330 $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ), __METHOD__
);
332 // clear the memcache of defined tags
333 self
::purgeTagCacheAll();
337 * Writes a tag action into the tag management log.
339 * @param string $action
341 * @param string $reason
342 * @param User $user Who to attribute the action to
343 * @param int $tagCount For deletion only, how many usages the tag had before
347 protected static function logTagAction( $action, $tag, $reason, User
$user,
350 $dbw = wfGetDB( DB_MASTER
);
352 $logEntry = new ManualLogEntry( 'managetags', $action );
353 $logEntry->setPerformer( $user );
354 // target page is not relevant, but it has to be set, so we just put in
355 // the title of Special:Tags
356 $logEntry->setTarget( Title
::newFromText( 'Special:Tags' ) );
357 $logEntry->setComment( $reason );
359 $params = array( '4::tag' => $tag );
360 if ( !is_null( $tagCount ) ) {
361 $params['5:number:count'] = $tagCount;
363 $logEntry->setParameters( $params );
364 $logEntry->setRelations( array( 'Tag' => $tag ) );
366 $logId = $logEntry->insert( $dbw );
367 $logEntry->publish( $logId );
372 * Is it OK to allow the user to activate this tag?
374 * @param string $tag Tag that you are interested in activating
375 * @param User|null $user User whose permission you wish to check, or null if
376 * you don't care (e.g. maintenance scripts)
380 public static function canActivateTag( $tag, User
$user = null ) {
381 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
382 return Status
::newFatal( 'tags-manage-no-permission' );
385 // non-existing tags cannot be activated
386 $tagUsage = self
::tagUsageStatistics();
387 if ( !isset( $tagUsage[$tag] ) ) {
388 return Status
::newFatal( 'tags-activate-not-found', $tag );
391 // defined tags cannot be activated (a defined tag is either extension-
392 // defined, in which case the extension chooses whether or not to active it;
393 // or user-defined, in which case it is considered active)
394 $definedTags = self
::listDefinedTags();
395 if ( in_array( $tag, $definedTags ) ) {
396 return Status
::newFatal( 'tags-activate-not-allowed', $tag );
399 return Status
::newGood();
403 * Activates a tag, checking whether it is allowed first, and adding a log
406 * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need
410 * @param string $reason
411 * @param User $user Who to give credit for the action
412 * @param bool $ignoreWarnings Can be used for API interaction, default false
413 * @return Status If successful, the Status contains the ID of the added log
417 public static function activateTagWithChecks( $tag, $reason, User
$user,
418 $ignoreWarnings = false ) {
420 // are we allowed to do this?
421 $result = self
::canActivateTag( $tag, $user );
422 if ( $ignoreWarnings ?
!$result->isOK() : !$result->isGood() ) {
423 $result->value
= null;
428 self
::defineTag( $tag );
431 $logId = self
::logTagAction( 'activate', $tag, $reason, $user );
432 return Status
::newGood( $logId );
436 * Is it OK to allow the user to deactivate this tag?
438 * @param string $tag Tag that you are interested in deactivating
439 * @param User|null $user User whose permission you wish to check, or null if
440 * you don't care (e.g. maintenance scripts)
444 public static function canDeactivateTag( $tag, User
$user = null ) {
445 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
446 return Status
::newFatal( 'tags-manage-no-permission' );
449 // only explicitly-defined tags can be deactivated
450 $explicitlyDefinedTags = self
::listExplicitlyDefinedTags();
451 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
452 return Status
::newFatal( 'tags-deactivate-not-allowed', $tag );
454 return Status
::newGood();
458 * Deactivates a tag, checking whether it is allowed first, and adding a log
461 * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need
465 * @param string $reason
466 * @param User $user Who to give credit for the action
467 * @param bool $ignoreWarnings Can be used for API interaction, default false
468 * @return Status If successful, the Status contains the ID of the added log
472 public static function deactivateTagWithChecks( $tag, $reason, User
$user,
473 $ignoreWarnings = false ) {
475 // are we allowed to do this?
476 $result = self
::canDeactivateTag( $tag, $user );
477 if ( $ignoreWarnings ?
!$result->isOK() : !$result->isGood() ) {
478 $result->value
= null;
483 self
::undefineTag( $tag );
486 $logId = self
::logTagAction( 'deactivate', $tag, $reason, $user );
487 return Status
::newGood( $logId );
491 * Is it OK to allow the user to create this tag?
493 * @param string $tag Tag that you are interested in creating
494 * @param User|null $user User whose permission you wish to check, or null if
495 * you don't care (e.g. maintenance scripts)
499 public static function canCreateTag( $tag, User
$user = null ) {
500 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
501 return Status
::newFatal( 'tags-manage-no-permission' );
506 return Status
::newFatal( 'tags-create-no-name' );
509 // tags cannot contain commas (used as a delimiter in tag_summary table) or
510 // slashes (would break tag description messages in MediaWiki namespace)
511 if ( strpos( $tag, ',' ) !== false ||
strpos( $tag, '/' ) !== false ) {
512 return Status
::newFatal( 'tags-create-invalid-chars' );
515 // could the MediaWiki namespace description messages be created?
516 $title = Title
::makeTitleSafe( NS_MEDIAWIKI
, "Tag-$tag-description" );
517 if ( is_null( $title ) ) {
518 return Status
::newFatal( 'tags-create-invalid-title-chars' );
521 // does the tag already exist?
522 $tagUsage = self
::tagUsageStatistics();
523 if ( isset( $tagUsage[$tag] ) ) {
524 return Status
::newFatal( 'tags-create-already-exists', $tag );
528 $canCreateResult = Status
::newGood();
529 Hooks
::run( 'ChangeTagCanCreate', array( $tag, $user, &$canCreateResult ) );
530 return $canCreateResult;
534 * Creates a tag by adding a row to the `valid_tag` table.
536 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
540 * @param string $reason
541 * @param User $user Who to give credit for the action
542 * @param bool $ignoreWarnings Can be used for API interaction, default false
543 * @return Status If successful, the Status contains the ID of the added log
547 public static function createTagWithChecks( $tag, $reason, User
$user,
548 $ignoreWarnings = false ) {
550 // are we allowed to do this?
551 $result = self
::canCreateTag( $tag, $user );
552 if ( $ignoreWarnings ?
!$result->isOK() : !$result->isGood() ) {
553 $result->value
= null;
558 self
::defineTag( $tag );
561 $logId = self
::logTagAction( 'create', $tag, $reason, $user );
562 return Status
::newGood( $logId );
566 * Permanently removes all traces of a tag from the DB. Good for removing
567 * misspelt or temporary tags.
569 * This function should be directly called by maintenance scripts only, never
570 * by user-facing code. See deleteTagWithChecks() for functionality that can
571 * safely be exposed to users.
573 * @param string $tag Tag to remove
574 * @return Status The returned status will be good unless a hook changed it
577 public static function deleteTagEverywhere( $tag ) {
578 $dbw = wfGetDB( DB_MASTER
);
579 $dbw->begin( __METHOD__
);
581 // delete from valid_tag
582 self
::undefineTag( $tag );
584 // find out which revisions use this tag, so we can delete from tag_summary
585 $result = $dbw->select( 'change_tag',
586 array( 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ),
587 array( 'ct_tag' => $tag ),
589 foreach ( $result as $row ) {
590 if ( $row->ct_rev_id
) {
591 $field = 'ts_rev_id';
592 $fieldValue = $row->ct_rev_id
;
593 } elseif ( $row->ct_log_id
) {
594 $field = 'ts_log_id';
595 $fieldValue = $row->ct_log_id
;
596 } elseif ( $row->ct_rc_id
) {
598 $fieldValue = $row->ct_rc_id
;
600 // don't know what's up; just skip it
604 // remove the tag from the relevant row of tag_summary
605 $tsResult = $dbw->selectField( 'tag_summary',
607 array( $field => $fieldValue ),
609 $tsValues = explode( ',', $tsResult );
610 $tsValues = array_values( array_diff( $tsValues, array( $tag ) ) );
612 // no tags left, so delete the row altogether
613 $dbw->delete( 'tag_summary',
614 array( $field => $fieldValue ),
617 $dbw->update( 'tag_summary',
618 array( 'ts_tags' => implode( ',', $tsValues ) ),
619 array( $field => $fieldValue ),
624 // delete from change_tag
625 $dbw->delete( 'change_tag', array( 'ct_tag' => $tag ), __METHOD__
);
627 $dbw->commit( __METHOD__
);
629 // give extensions a chance
630 $status = Status
::newGood();
631 Hooks
::run( 'ChangeTagAfterDelete', array( $tag, &$status ) );
632 // let's not allow error results, as the actual tag deletion succeeded
633 if ( !$status->isOK() ) {
634 wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
638 // clear the memcache of defined tags
639 self
::purgeTagCacheAll();
645 * Is it OK to allow the user to delete this tag?
647 * @param string $tag Tag that you are interested in deleting
648 * @param User|null $user User whose permission you wish to check, or null if
649 * you don't care (e.g. maintenance scripts)
653 public static function canDeleteTag( $tag, User
$user = null ) {
654 $tagUsage = self
::tagUsageStatistics();
656 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
657 return Status
::newFatal( 'tags-manage-no-permission' );
660 if ( !isset( $tagUsage[$tag] ) ) {
661 return Status
::newFatal( 'tags-delete-not-found', $tag );
664 if ( $tagUsage[$tag] > self
::MAX_DELETE_USES
) {
665 return Status
::newFatal( 'tags-delete-too-many-uses', $tag, self
::MAX_DELETE_USES
);
668 $extensionDefined = self
::listExtensionDefinedTags();
669 if ( in_array( $tag, $extensionDefined ) ) {
670 // extension-defined tags can't be deleted unless the extension
671 // specifically allows it
672 $status = Status
::newFatal( 'tags-delete-not-allowed' );
674 // user-defined tags are deletable unless otherwise specified
675 $status = Status
::newGood();
678 Hooks
::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) );
683 * Deletes a tag, checking whether it is allowed first, and adding a log entry
686 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
690 * @param string $reason
691 * @param User $user Who to give credit for the action
692 * @param bool $ignoreWarnings Can be used for API interaction, default false
693 * @return Status If successful, the Status contains the ID of the added log
697 public static function deleteTagWithChecks( $tag, $reason, User
$user,
698 $ignoreWarnings = false ) {
700 // are we allowed to do this?
701 $result = self
::canDeleteTag( $tag, $user );
702 if ( $ignoreWarnings ?
!$result->isOK() : !$result->isGood() ) {
703 $result->value
= null;
707 // store the tag usage statistics
708 $tagUsage = self
::tagUsageStatistics();
711 $deleteResult = self
::deleteTagEverywhere( $tag );
712 if ( !$deleteResult->isOK() ) {
713 return $deleteResult;
717 $logId = self
::logTagAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
718 $deleteResult->value
= $logId;
719 return $deleteResult;
723 * Lists those tags which extensions report as being "active".
728 public static function listExtensionActivatedTags() {
731 $key = wfMemcKey( 'active-tags' );
732 $tags = $wgMemc->get( $key );
737 // ask extensions which tags they consider active
738 $extensionActive = array();
739 Hooks
::run( 'ChangeTagsListActive', array( &$extensionActive ) );
741 // Short-term caching.
742 $wgMemc->set( $key, $extensionActive, 300 );
743 return $extensionActive;
747 * Basically lists defined tags which count even if they aren't applied to anything.
748 * It returns a union of the results of listExplicitlyDefinedTags() and
749 * listExtensionDefinedTags().
751 * @return string[] Array of strings: tags
753 public static function listDefinedTags() {
754 $tags1 = self
::listExplicitlyDefinedTags();
755 $tags2 = self
::listExtensionDefinedTags();
756 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
760 * Lists tags explicitly defined in the `valid_tag` table of the database.
761 * Tags in table 'change_tag' which are not in table 'valid_tag' are not
764 * Tries memcached first.
766 * @return string[] Array of strings: tags
769 public static function listExplicitlyDefinedTags() {
772 $key = wfMemcKey( 'valid-tags-db' );
773 $tags = $wgMemc->get( $key );
778 $emptyTags = array();
781 $dbr = wfGetDB( DB_SLAVE
);
782 $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__
);
783 foreach ( $res as $row ) {
784 $emptyTags[] = $row->vt_tag
;
787 $emptyTags = array_filter( array_unique( $emptyTags ) );
789 // Short-term caching.
790 $wgMemc->set( $key, $emptyTags, 300 );
795 * Lists tags defined by extensions using the ListDefinedTags hook.
796 * Extensions need only define those tags they deem to be in active use.
798 * Tries memcached first.
800 * @return string[] Array of strings: tags
803 public static function listExtensionDefinedTags() {
806 $key = wfMemcKey( 'valid-tags-hook' );
807 $tags = $wgMemc->get( $key );
812 $emptyTags = array();
813 Hooks
::run( 'ListDefinedTags', array( &$emptyTags ) );
814 $emptyTags = array_filter( array_unique( $emptyTags ) );
816 // Short-term caching.
817 $wgMemc->set( $key, $emptyTags, 300 );
822 * Invalidates the short-term cache of defined tags used by the
823 * list*DefinedTags functions, as well as the tag statistics cache.
826 public static function purgeTagCacheAll() {
828 $wgMemc->delete( wfMemcKey( 'active-tags' ) );
829 $wgMemc->delete( wfMemcKey( 'valid-tags-db' ) );
830 $wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) );
831 self
::purgeTagUsageCache();
835 * Invalidates the tag statistics cache only.
838 public static function purgeTagUsageCache() {
840 $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
844 * Returns a map of any tags used on the wiki to number of edits
845 * tagged with them, ordered descending by the hitcount.
847 * Keeps a short-term cache in memory, so calling this multiple times in the
848 * same request should be fine.
850 * @return array Array of string => int
852 public static function tagUsageStatistics() {
855 $key = wfMemcKey( 'change-tag-statistics' );
856 $stats = $wgMemc->get( $key );
863 $dbr = wfGetDB( DB_SLAVE
);
866 array( 'ct_tag', 'hitcount' => 'count(*)' ),
869 array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' )
872 foreach ( $res as $row ) {
873 $out[$row->ct_tag
] = $row->hitcount
;
875 foreach ( self
::listDefinedTags() as $tag ) {
876 if ( !isset( $out[$tag] ) ) {
881 // Cache for a very short time
882 $wgMemc->set( $key, $out, 300 );