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 ) {
44 $tags = explode( ',', $tags );
46 // XXX(Ori Livneh, 2014-11-08): remove once bug 73181 is resolved.
47 $tags = array_diff( $tags, array( 'HHVM', '' ) );
50 return array( '', array() );
55 $displayTags = array();
56 foreach ( $tags as $tag ) {
57 $displayTags[] = Xml
::tags(
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 ) )
69 $markers = Xml
::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
71 return array( $markers, $classes );
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 );
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'
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.
119 // Info might be out of date, somewhat fractionally, on slave.
121 $rc_id = $dbw->selectField(
124 array( 'rc_logid' => $log_id ),
127 } elseif ( $rev_id ) {
128 $rc_id = $dbw->selectField(
131 array( 'rc_this_oldid' => $rev_id ),
135 } elseif ( !$log_id && !$rev_id ) {
136 // Info might be out of date, somewhat fractionally, on slave.
137 $log_id = $dbw->selectField(
140 array( 'rc_id' => $rc_id ),
143 $rev_id = $dbw->selectField(
146 array( 'rc_id' => $rc_id ),
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 ) );
167 if ( $prevTags == $newTags ) {
174 array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
175 array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
179 // Insert the tags rows.
181 foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
182 $tagsRows[] = array_filter(
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();
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';
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();
273 array( 'for' => 'tagfilter' ),
274 wfMessage( 'tag-filter' )->parse()
280 array( 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' )
288 $html = implode( ' ', $data );
292 array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() )
294 $html .= "\n" . Html
::hidden( 'title', $title->getPrefixedText() );
297 array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ),
305 * Defines a tag in the valid_tag table, without checking that the tag name
307 * Extensions should NOT use this function; they can use the ListDefinedTags
310 * @param string $tag Tag to create
313 public static function defineTag( $tag ) {
314 $dbw = wfGetDB( DB_MASTER
);
315 $dbw->replace( 'valid_tag',
317 array( 'vt_tag' => $tag ),
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
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
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
351 protected static function logTagAction( $action, $tag, $reason, User
$user,
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 );
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)
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
410 * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need
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
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;
432 self
::defineTag( $tag );
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)
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
465 * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need
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
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;
487 self
::undefineTag( $tag );
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)
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' );
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 );
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
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
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;
562 self
::defineTag( $tag );
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
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 ),
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
) {
602 $fieldValue = $row->ct_rc_id
;
604 // don't know what's up; just skip it
608 // remove the tag from the relevant row of tag_summary
609 $tsResult = $dbw->selectField( 'tag_summary',
611 array( $field => $fieldValue ),
613 $tsValues = explode( ',', $tsResult );
614 $tsValues = array_values( array_diff( $tsValues, array( $tag ) ) );
616 // no tags left, so delete the row altogether
617 $dbw->delete( 'tag_summary',
618 array( $field => $fieldValue ),
621 $dbw->update( 'tag_summary',
622 array( 'ts_tags' => implode( ',', $tsValues ) ),
623 array( $field => $fieldValue ),
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' );
642 // clear the memcache of defined tags
643 self
::purgeTagCacheAll();
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)
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' );
678 // user-defined tags are deletable unless otherwise specified
679 $status = Status
::newGood();
682 Hooks
::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) );
687 * Deletes a tag, checking whether it is allowed first, and adding a log entry
690 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
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
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;
711 // store the tag usage statistics
712 $tagUsage = self
::tagUsageStatistics();
715 $deleteResult = self
::deleteTagEverywhere( $tag );
716 if ( !$deleteResult->isOK() ) {
717 return $deleteResult;
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".
732 public static function listExtensionActivatedTags() {
735 $key = wfMemcKey( 'active-tags' );
736 $tags = $wgMemc->get( $key );
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
768 * Tries memcached first.
770 * @return string[] Array of strings: tags
773 public static function listExplicitlyDefinedTags() {
776 $key = wfMemcKey( 'valid-tags-db' );
777 $tags = $wgMemc->get( $key );
782 $emptyTags = array();
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 );
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
807 public static function listExtensionDefinedTags() {
810 $key = wfMemcKey( 'valid-tags-hook' );
811 $tags = $wgMemc->get( $key );
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 );
826 * Invalidates the short-term cache of defined tags used by the
827 * list*DefinedTags functions, as well as the tag statistics cache.
830 public static function purgeTagCacheAll() {
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.
842 public static function purgeTagUsageCache() {
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() {
859 $key = wfMemcKey( 'change-tag-statistics' );
860 $stats = $wgMemc->get( $key );
867 $dbr = wfGetDB( DB_SLAVE
);
870 array( 'ct_tag', 'hitcount' => 'count(*)' ),
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] ) ) {
885 // Cache for a very short time
886 $wgMemc->set( $key, $out, 300 );