Merge "Revert "Hide HHVM tag on Special:{Contributions,RecentChanges,...}""
[lhc/web/wiklou.git] / includes / ChangeTags.php
1 <?php
2 /**
3 * Recent changes tagging.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 */
22
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
28 */
29 const MAX_DELETE_USES = 5000;
30
31 /**
32 * Creates HTML for the given tags
33 *
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
40 */
41 public static function formatSummaryRow( $tags, $page ) {
42 global $wgLang;
43
44 if ( !$tags ) {
45 return array( '', array() );
46 }
47
48 $classes = array();
49
50 $tags = explode( ',', $tags );
51 $displayTags = array();
52 foreach ( $tags as $tag ) {
53 $displayTags[] = Xml::tags(
54 'span',
55 array( 'class' => 'mw-tag-marker ' .
56 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ),
57 self::tagDescription( $tag )
58 );
59 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
60 }
61 $markers = wfMessage( 'tag-list-wrapper' )
62 ->numParams( count( $displayTags ) )
63 ->rawParams( $wgLang->commaList( $displayTags ) )
64 ->parse();
65 $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
66
67 return array( $markers, $classes );
68 }
69
70 /**
71 * Get a short description for a tag
72 *
73 * @param string $tag Tag
74 *
75 * @return string Short description of the tag from "mediawiki:tag-$tag" if this message exists,
76 * html-escaped version of $tag otherwise
77 */
78 public static function tagDescription( $tag ) {
79 $msg = wfMessage( "tag-$tag" );
80 return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag );
81 }
82
83 /**
84 * Add tags to a change given its rc_id, rev_id and/or log_id
85 *
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'
91 *
92 * @throws MWException
93 * @return bool False if no changes are made, otherwise true
94 *
95 * @exception MWException When $rc_id, $rev_id and $log_id are all null
96 */
97 public static function addTags( $tags, $rc_id = null, $rev_id = null,
98 $log_id = null, $params = null
99 ) {
100 if ( !is_array( $tags ) ) {
101 $tags = array( $tags );
102 }
103
104 $tags = array_filter( $tags ); // Make sure we're submitting all tags...
105
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!' );
109 }
110
111 $dbw = wfGetDB( DB_MASTER );
112
113 // Might as well look for rcids and so on.
114 if ( !$rc_id ) {
115 // Info might be out of date, somewhat fractionally, on slave.
116 if ( $log_id ) {
117 $rc_id = $dbw->selectField(
118 'recentchanges',
119 'rc_id',
120 array( 'rc_logid' => $log_id ),
121 __METHOD__
122 );
123 } elseif ( $rev_id ) {
124 $rc_id = $dbw->selectField(
125 'recentchanges',
126 'rc_id',
127 array( 'rc_this_oldid' => $rev_id ),
128 __METHOD__
129 );
130 }
131 } elseif ( !$log_id && !$rev_id ) {
132 // Info might be out of date, somewhat fractionally, on slave.
133 $log_id = $dbw->selectField(
134 'recentchanges',
135 'rc_logid',
136 array( 'rc_id' => $rc_id ),
137 __METHOD__
138 );
139 $rev_id = $dbw->selectField(
140 'recentchanges',
141 'rc_this_oldid',
142 array( 'rc_id' => $rc_id ),
143 __METHOD__
144 );
145 }
146
147 $tsConds = array_filter( array(
148 'ts_rc_id' => $rc_id,
149 'ts_rev_id' => $rev_id,
150 'ts_log_id' => $log_id )
151 );
152
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 ) );
160 sort( $prevTags );
161 sort( $newTags );
162
163 if ( $prevTags == $newTags ) {
164 // No change.
165 return false;
166 }
167
168 $dbw->replace(
169 'tag_summary',
170 array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
171 array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
172 __METHOD__
173 );
174
175 // Insert the tags rows.
176 $tagsRows = array();
177 foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
178 $tagsRows[] = array_filter(
179 array(
180 'ct_tag' => $tag,
181 'ct_rc_id' => $rc_id,
182 'ct_log_id' => $log_id,
183 'ct_rev_id' => $rev_id,
184 'ct_params' => $params
185 )
186 );
187 }
188
189 $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
190
191 self::purgeTagUsageCache();
192 return true;
193 }
194
195 /**
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.
199 *
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
206 *
207 * @throws MWException When unable to determine appropriate JOIN condition for tagging
208 */
209 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
210 &$join_conds, &$options, $filter_tag = false ) {
211 global $wgRequest, $wgUseTagFilter;
212
213 if ( $filter_tag === false ) {
214 $filter_tag = $wgRequest->getVal( 'tagfilter' );
215 }
216
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';
226 } else {
227 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
228 }
229
230 $fields['ts_tags'] = wfGetDB( DB_SLAVE )->buildGroupConcatField(
231 ',', 'change_tag', 'ct_tag', $join_cond
232 );
233
234 if ( $wgUseTagFilter && $filter_tag ) {
235 // Somebody wants to filter on a tag.
236 // Add an INNER JOIN on change_tag
237
238 $tables[] = 'change_tag';
239 $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond );
240 $conds['ct_tag'] = $filter_tag;
241 }
242 }
243
244 /**
245 * Build a text box to select a change tag
246 *
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
256 */
257 public static function buildTagFilterSelector( $selected = '',
258 $fullForm = false, Title $title = null
259 ) {
260 global $wgUseTagFilter;
261
262 if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
263 return $fullForm ? '' : array();
264 }
265
266 $data = array(
267 Html::rawElement(
268 'label',
269 array( 'for' => 'tagfilter' ),
270 wfMessage( 'tag-filter' )->parse()
271 ),
272 Xml::input(
273 'tagfilter',
274 20,
275 $selected,
276 array( 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' )
277 )
278 );
279
280 if ( !$fullForm ) {
281 return $data;
282 }
283
284 $html = implode( '&#160;', $data );
285 $html .= "\n" .
286 Xml::element(
287 'input',
288 array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() )
289 );
290 $html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() );
291 $html = Xml::tags(
292 'form',
293 array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ),
294 $html
295 );
296
297 return $html;
298 }
299
300 /**
301 * Defines a tag in the valid_tag table, without checking that the tag name
302 * is valid.
303 * Extensions should NOT use this function; they can use the ListDefinedTags
304 * hook instead.
305 *
306 * @param string $tag Tag to create
307 * @since 1.25
308 */
309 public static function defineTag( $tag ) {
310 $dbw = wfGetDB( DB_MASTER );
311 $dbw->replace( 'valid_tag',
312 array( 'vt_tag' ),
313 array( 'vt_tag' => $tag ),
314 __METHOD__ );
315
316 // clear the memcache of defined tags
317 self::purgeTagCacheAll();
318 }
319
320 /**
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.
324 *
325 * @param string $tag Tag to remove
326 * @since 1.25
327 */
328 public static function undefineTag( $tag ) {
329 $dbw = wfGetDB( DB_MASTER );
330 $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ), __METHOD__ );
331
332 // clear the memcache of defined tags
333 self::purgeTagCacheAll();
334 }
335
336 /**
337 * Writes a tag action into the tag management log.
338 *
339 * @param string $action
340 * @param string $tag
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
344 * it was deleted.
345 * @since 1.25
346 */
347 protected static function logTagAction( $action, $tag, $reason, User $user,
348 $tagCount = null ) {
349
350 $dbw = wfGetDB( DB_MASTER );
351
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 );
358
359 $params = array( '4::tag' => $tag );
360 if ( !is_null( $tagCount ) ) {
361 $params['5:number:count'] = $tagCount;
362 }
363 $logEntry->setParameters( $params );
364 $logEntry->setRelations( array( 'Tag' => $tag ) );
365
366 $logId = $logEntry->insert( $dbw );
367 $logEntry->publish( $logId );
368 return $logId;
369 }
370
371 /**
372 * Is it OK to allow the user to activate this tag?
373 *
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)
377 * @return Status
378 * @since 1.25
379 */
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' );
383 }
384
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 );
389 }
390
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 );
397 }
398
399 return Status::newGood();
400 }
401
402 /**
403 * Activates a tag, checking whether it is allowed first, and adding a log
404 * entry afterwards.
405 *
406 * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need
407 * to do that.
408 *
409 * @param string $tag
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
414 * entry as its value
415 * @since 1.25
416 */
417 public static function activateTagWithChecks( $tag, $reason, User $user,
418 $ignoreWarnings = false ) {
419
420 // are we allowed to do this?
421 $result = self::canActivateTag( $tag, $user );
422 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
423 $result->value = null;
424 return $result;
425 }
426
427 // do it!
428 self::defineTag( $tag );
429
430 // log it
431 $logId = self::logTagAction( 'activate', $tag, $reason, $user );
432 return Status::newGood( $logId );
433 }
434
435 /**
436 * Is it OK to allow the user to deactivate this tag?
437 *
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)
441 * @return Status
442 * @since 1.25
443 */
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' );
447 }
448
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 );
453 }
454 return Status::newGood();
455 }
456
457 /**
458 * Deactivates a tag, checking whether it is allowed first, and adding a log
459 * entry afterwards.
460 *
461 * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need
462 * to do that.
463 *
464 * @param string $tag
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
469 * entry as its value
470 * @since 1.25
471 */
472 public static function deactivateTagWithChecks( $tag, $reason, User $user,
473 $ignoreWarnings = false ) {
474
475 // are we allowed to do this?
476 $result = self::canDeactivateTag( $tag, $user );
477 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
478 $result->value = null;
479 return $result;
480 }
481
482 // do it!
483 self::undefineTag( $tag );
484
485 // log it
486 $logId = self::logTagAction( 'deactivate', $tag, $reason, $user );
487 return Status::newGood( $logId );
488 }
489
490 /**
491 * Is it OK to allow the user to create this tag?
492 *
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)
496 * @return Status
497 * @since 1.25
498 */
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' );
502 }
503
504 // no empty tags
505 if ( $tag === '' ) {
506 return Status::newFatal( 'tags-create-no-name' );
507 }
508
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' );
513 }
514
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' );
519 }
520
521 // does the tag already exist?
522 $tagUsage = self::tagUsageStatistics();
523 if ( isset( $tagUsage[$tag] ) ) {
524 return Status::newFatal( 'tags-create-already-exists', $tag );
525 }
526
527 // check with hooks
528 $canCreateResult = Status::newGood();
529 Hooks::run( 'ChangeTagCanCreate', array( $tag, $user, &$canCreateResult ) );
530 return $canCreateResult;
531 }
532
533 /**
534 * Creates a tag by adding a row to the `valid_tag` table.
535 *
536 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
537 * do that.
538 *
539 * @param string $tag
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
544 * entry as its value
545 * @since 1.25
546 */
547 public static function createTagWithChecks( $tag, $reason, User $user,
548 $ignoreWarnings = false ) {
549
550 // are we allowed to do this?
551 $result = self::canCreateTag( $tag, $user );
552 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
553 $result->value = null;
554 return $result;
555 }
556
557 // do it!
558 self::defineTag( $tag );
559
560 // log it
561 $logId = self::logTagAction( 'create', $tag, $reason, $user );
562 return Status::newGood( $logId );
563 }
564
565 /**
566 * Permanently removes all traces of a tag from the DB. Good for removing
567 * misspelt or temporary tags.
568 *
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.
572 *
573 * @param string $tag Tag to remove
574 * @return Status The returned status will be good unless a hook changed it
575 * @since 1.25
576 */
577 public static function deleteTagEverywhere( $tag ) {
578 $dbw = wfGetDB( DB_MASTER );
579 $dbw->begin( __METHOD__ );
580
581 // delete from valid_tag
582 self::undefineTag( $tag );
583
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 ),
588 __METHOD__ );
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 ) {
597 $field = 'ts_rc_id';
598 $fieldValue = $row->ct_rc_id;
599 } else {
600 // don't know what's up; just skip it
601 continue;
602 }
603
604 // remove the tag from the relevant row of tag_summary
605 $tsResult = $dbw->selectField( 'tag_summary',
606 'ts_tags',
607 array( $field => $fieldValue ),
608 __METHOD__ );
609 $tsValues = explode( ',', $tsResult );
610 $tsValues = array_values( array_diff( $tsValues, array( $tag ) ) );
611 if ( !$tsValues ) {
612 // no tags left, so delete the row altogether
613 $dbw->delete( 'tag_summary',
614 array( $field => $fieldValue ),
615 __METHOD__ );
616 } else {
617 $dbw->update( 'tag_summary',
618 array( 'ts_tags' => implode( ',', $tsValues ) ),
619 array( $field => $fieldValue ),
620 __METHOD__ );
621 }
622 }
623
624 // delete from change_tag
625 $dbw->delete( 'change_tag', array( 'ct_tag' => $tag ), __METHOD__ );
626
627 $dbw->commit( __METHOD__ );
628
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' );
635 $status->ok = true;
636 }
637
638 // clear the memcache of defined tags
639 self::purgeTagCacheAll();
640
641 return $status;
642 }
643
644 /**
645 * Is it OK to allow the user to delete this tag?
646 *
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)
650 * @return Status
651 * @since 1.25
652 */
653 public static function canDeleteTag( $tag, User $user = null ) {
654 $tagUsage = self::tagUsageStatistics();
655
656 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
657 return Status::newFatal( 'tags-manage-no-permission' );
658 }
659
660 if ( !isset( $tagUsage[$tag] ) ) {
661 return Status::newFatal( 'tags-delete-not-found', $tag );
662 }
663
664 if ( $tagUsage[$tag] > self::MAX_DELETE_USES ) {
665 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
666 }
667
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' );
673 } else {
674 // user-defined tags are deletable unless otherwise specified
675 $status = Status::newGood();
676 }
677
678 Hooks::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) );
679 return $status;
680 }
681
682 /**
683 * Deletes a tag, checking whether it is allowed first, and adding a log entry
684 * afterwards.
685 *
686 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
687 * do that.
688 *
689 * @param string $tag
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
694 * entry as its value
695 * @since 1.25
696 */
697 public static function deleteTagWithChecks( $tag, $reason, User $user,
698 $ignoreWarnings = false ) {
699
700 // are we allowed to do this?
701 $result = self::canDeleteTag( $tag, $user );
702 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
703 $result->value = null;
704 return $result;
705 }
706
707 // store the tag usage statistics
708 $tagUsage = self::tagUsageStatistics();
709
710 // do it!
711 $deleteResult = self::deleteTagEverywhere( $tag );
712 if ( !$deleteResult->isOK() ) {
713 return $deleteResult;
714 }
715
716 // log it
717 $logId = self::logTagAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
718 $deleteResult->value = $logId;
719 return $deleteResult;
720 }
721
722 /**
723 * Lists those tags which extensions report as being "active".
724 *
725 * @return array
726 * @since 1.25
727 */
728 public static function listExtensionActivatedTags() {
729 // Caching...
730 global $wgMemc;
731 $key = wfMemcKey( 'active-tags' );
732 $tags = $wgMemc->get( $key );
733 if ( $tags ) {
734 return $tags;
735 }
736
737 // ask extensions which tags they consider active
738 $extensionActive = array();
739 Hooks::run( 'ChangeTagsListActive', array( &$extensionActive ) );
740
741 // Short-term caching.
742 $wgMemc->set( $key, $extensionActive, 300 );
743 return $extensionActive;
744 }
745
746 /**
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().
750 *
751 * @return string[] Array of strings: tags
752 */
753 public static function listDefinedTags() {
754 $tags1 = self::listExplicitlyDefinedTags();
755 $tags2 = self::listExtensionDefinedTags();
756 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
757 }
758
759 /**
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
762 * included.
763 *
764 * Tries memcached first.
765 *
766 * @return string[] Array of strings: tags
767 * @since 1.25
768 */
769 public static function listExplicitlyDefinedTags() {
770 // Caching...
771 global $wgMemc;
772 $key = wfMemcKey( 'valid-tags-db' );
773 $tags = $wgMemc->get( $key );
774 if ( $tags ) {
775 return $tags;
776 }
777
778 $emptyTags = array();
779
780 // Some DB stuff
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;
785 }
786
787 $emptyTags = array_filter( array_unique( $emptyTags ) );
788
789 // Short-term caching.
790 $wgMemc->set( $key, $emptyTags, 300 );
791 return $emptyTags;
792 }
793
794 /**
795 * Lists tags defined by extensions using the ListDefinedTags hook.
796 * Extensions need only define those tags they deem to be in active use.
797 *
798 * Tries memcached first.
799 *
800 * @return string[] Array of strings: tags
801 * @since 1.25
802 */
803 public static function listExtensionDefinedTags() {
804 // Caching...
805 global $wgMemc;
806 $key = wfMemcKey( 'valid-tags-hook' );
807 $tags = $wgMemc->get( $key );
808 if ( $tags ) {
809 return $tags;
810 }
811
812 $emptyTags = array();
813 Hooks::run( 'ListDefinedTags', array( &$emptyTags ) );
814 $emptyTags = array_filter( array_unique( $emptyTags ) );
815
816 // Short-term caching.
817 $wgMemc->set( $key, $emptyTags, 300 );
818 return $emptyTags;
819 }
820
821 /**
822 * Invalidates the short-term cache of defined tags used by the
823 * list*DefinedTags functions, as well as the tag statistics cache.
824 * @since 1.25
825 */
826 public static function purgeTagCacheAll() {
827 global $wgMemc;
828 $wgMemc->delete( wfMemcKey( 'active-tags' ) );
829 $wgMemc->delete( wfMemcKey( 'valid-tags-db' ) );
830 $wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) );
831 self::purgeTagUsageCache();
832 }
833
834 /**
835 * Invalidates the tag statistics cache only.
836 * @since 1.25
837 */
838 public static function purgeTagUsageCache() {
839 global $wgMemc;
840 $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
841 }
842
843 /**
844 * Returns a map of any tags used on the wiki to number of edits
845 * tagged with them, ordered descending by the hitcount.
846 *
847 * Keeps a short-term cache in memory, so calling this multiple times in the
848 * same request should be fine.
849 *
850 * @return array Array of string => int
851 */
852 public static function tagUsageStatistics() {
853 // Caching...
854 global $wgMemc;
855 $key = wfMemcKey( 'change-tag-statistics' );
856 $stats = $wgMemc->get( $key );
857 if ( $stats ) {
858 return $stats;
859 }
860
861 $out = array();
862
863 $dbr = wfGetDB( DB_SLAVE );
864 $res = $dbr->select(
865 'change_tag',
866 array( 'ct_tag', 'hitcount' => 'count(*)' ),
867 array(),
868 __METHOD__,
869 array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' )
870 );
871
872 foreach ( $res as $row ) {
873 $out[$row->ct_tag] = $row->hitcount;
874 }
875 foreach ( self::listDefinedTags() as $tag ) {
876 if ( !isset( $out[$tag] ) ) {
877 $out[$tag] = 0;
878 }
879 }
880
881 // Cache for a very short time
882 $wgMemc->set( $key, $out, 300 );
883 return $out;
884 }
885 }