SpecialWhatlinkshere: Mark redirects containing templates
[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 $tags = explode( ',', $tags );
45
46 // XXX(Ori Livneh, 2014-11-08): remove once bug 73181 is resolved.
47 $tags = array_diff( $tags, array( 'HHVM', '' ) );
48
49 if ( !$tags ) {
50 return array( '', array() );
51 }
52
53 $classes = array();
54
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 )
62 );
63 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
64 }
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 );
70
71 return array( $markers, $classes );
72 }
73
74 /**
75 * Get a short description for a tag
76 *
77 * @param string $tag Tag
78 *
79 * @return string Short description of the tag from "mediawiki:tag-$tag" if this message exists,
80 * html-escaped version of $tag otherwise
81 */
82 public static function tagDescription( $tag ) {
83 $msg = wfMessage( "tag-$tag" );
84 return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag );
85 }
86
87 /**
88 * Add tags to a change given its rc_id, rev_id and/or log_id
89 *
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'
95 *
96 * @throws MWException
97 * @return bool False if no changes are made, otherwise true
98 *
99 * @exception MWException When $rc_id, $rev_id and $log_id are all null
100 */
101 public static function addTags( $tags, $rc_id = null, $rev_id = null,
102 $log_id = null, $params = null
103 ) {
104 if ( !is_array( $tags ) ) {
105 $tags = array( $tags );
106 }
107
108 $tags = array_filter( $tags ); // Make sure we're submitting all tags...
109
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!' );
113 }
114
115 $dbw = wfGetDB( DB_MASTER );
116
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__
126 );
127 } elseif ( $rev_id ) {
128 $rc_id = $dbw->selectField(
129 'recentchanges',
130 'rc_id',
131 array( 'rc_this_oldid' => $rev_id ),
132 __METHOD__
133 );
134 }
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__
142 );
143 $rev_id = $dbw->selectField(
144 'recentchanges',
145 'rc_this_oldid',
146 array( 'rc_id' => $rc_id ),
147 __METHOD__
148 );
149 }
150
151 $tsConds = array_filter( array(
152 'ts_rc_id' => $rc_id,
153 'ts_rev_id' => $rev_id,
154 'ts_log_id' => $log_id )
155 );
156
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 );
166
167 if ( $prevTags == $newTags ) {
168 // No change.
169 return false;
170 }
171
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__
177 );
178
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
189 )
190 );
191 }
192
193 $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
194
195 self::purgeTagUsageCache();
196 return true;
197 }
198
199 /**
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.
203 *
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
210 *
211 * @throws MWException When unable to determine appropriate JOIN condition for tagging
212 */
213 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
214 &$join_conds, &$options, $filter_tag = false ) {
215 global $wgRequest, $wgUseTagFilter;
216
217 if ( $filter_tag === false ) {
218 $filter_tag = $wgRequest->getVal( 'tagfilter' );
219 }
220
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.' );
232 }
233
234 $fields['ts_tags'] = wfGetDB( DB_SLAVE )->buildGroupConcatField(
235 ',', 'change_tag', 'ct_tag', $join_cond
236 );
237
238 if ( $wgUseTagFilter && $filter_tag ) {
239 // Somebody wants to filter on a tag.
240 // Add an INNER JOIN on change_tag
241
242 $tables[] = 'change_tag';
243 $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond );
244 $conds['ct_tag'] = $filter_tag;
245 }
246 }
247
248 /**
249 * Build a text box to select a change tag
250 *
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
260 */
261 public static function buildTagFilterSelector( $selected = '',
262 $fullForm = false, Title $title = null
263 ) {
264 global $wgUseTagFilter;
265
266 if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
267 return $fullForm ? '' : array();
268 }
269
270 $data = array(
271 Html::rawElement(
272 'label',
273 array( 'for' => 'tagfilter' ),
274 wfMessage( 'tag-filter' )->parse()
275 ),
276 Xml::input(
277 'tagfilter',
278 20,
279 $selected,
280 array( 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' )
281 )
282 );
283
284 if ( !$fullForm ) {
285 return $data;
286 }
287
288 $html = implode( '&#160;', $data );
289 $html .= "\n" .
290 Xml::element(
291 'input',
292 array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() )
293 );
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
299 );
300
301 return $html;
302 }
303
304 /**
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.
309 *
310 * @param string $tag Tag to create
311 * @since 1.25
312 */
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__ );
319
320 // clear the memcache of defined tags
321 self::purgeTagCacheAll();
322 }
323
324 /**
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.
328 *
329 * @param string $tag Tag to remove
330 * @since 1.25
331 */
332 public static function undefineTag( $tag ) {
333 $dbw = wfGetDB( DB_MASTER );
334 $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ), __METHOD__ );
335
336 // clear the memcache of defined tags
337 self::purgeTagCacheAll();
338 }
339
340 /**
341 * Writes a tag action into the tag management log.
342 *
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
350 */
351 protected static function logTagAction( $action, $tag, $reason, User $user,
352 $tagCount = null ) {
353
354 $dbw = wfGetDB( DB_MASTER );
355
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 );
362
363 $params = array( '4::tag' => $tag );
364 if ( !is_null( $tagCount ) ) {
365 $params['5:number:count'] = $tagCount;
366 }
367 $logEntry->setParameters( $params );
368 $logEntry->setRelations( array( 'Tag' => $tag ) );
369
370 $logId = $logEntry->insert( $dbw );
371 $logEntry->publish( $logId );
372 return $logId;
373 }
374
375 /**
376 * Is it OK to allow the user to activate this tag?
377 *
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
383 */
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' );
387 }
388
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 );
393 }
394
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 );
401 }
402
403 return Status::newGood();
404 }
405
406 /**
407 * Activates a tag, checking whether it is allowed first, and adding a log
408 * entry afterwards.
409 *
410 * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need
411 * to do that.
412 *
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
420 */
421 public static function activateTagWithChecks( $tag, $reason, User $user,
422 $ignoreWarnings = false ) {
423
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;
429 }
430
431 // do it!
432 self::defineTag( $tag );
433
434 // log it
435 $logId = self::logTagAction( 'activate', $tag, $reason, $user );
436 return Status::newGood( $logId );
437 }
438
439 /**
440 * Is it OK to allow the user to deactivate this tag?
441 *
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
447 */
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' );
451 }
452
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 );
457 }
458 return Status::newGood();
459 }
460
461 /**
462 * Deactivates a tag, checking whether it is allowed first, and adding a log
463 * entry afterwards.
464 *
465 * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need
466 * to do that.
467 *
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
475 */
476 public static function deactivateTagWithChecks( $tag, $reason, User $user,
477 $ignoreWarnings = false ) {
478
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;
484 }
485
486 // do it!
487 self::undefineTag( $tag );
488
489 // log it
490 $logId = self::logTagAction( 'deactivate', $tag, $reason, $user );
491 return Status::newGood( $logId );
492 }
493
494 /**
495 * Is it OK to allow the user to create this tag?
496 *
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
502 */
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' );
506 }
507
508 // no empty tags
509 if ( $tag === '' ) {
510 return Status::newFatal( 'tags-create-no-name' );
511 }
512
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' );
517 }
518
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' );
523 }
524
525 // does the tag already exist?
526 $tagUsage = self::tagUsageStatistics();
527 if ( isset( $tagUsage[$tag] ) ) {
528 return Status::newFatal( 'tags-create-already-exists', $tag );
529 }
530
531 // check with hooks
532 $canCreateResult = Status::newGood();
533 Hooks::run( 'ChangeTagCanCreate', array( $tag, $user, &$canCreateResult ) );
534 return $canCreateResult;
535 }
536
537 /**
538 * Creates a tag by adding a row to the `valid_tag` table.
539 *
540 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
541 * do that.
542 *
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
550 */
551 public static function createTagWithChecks( $tag, $reason, User $user,
552 $ignoreWarnings = false ) {
553
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;
559 }
560
561 // do it!
562 self::defineTag( $tag );
563
564 // log it
565 $logId = self::logTagAction( 'create', $tag, $reason, $user );
566 return Status::newGood( $logId );
567 }
568
569 /**
570 * Permanently removes all traces of a tag from the DB. Good for removing
571 * misspelt or temporary tags.
572 *
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.
576 *
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
580 */
581 public static function deleteTagEverywhere( $tag ) {
582 $dbw = wfGetDB( DB_MASTER );
583 $dbw->begin( __METHOD__ );
584
585 // delete from valid_tag
586 self::undefineTag( $tag );
587
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;
606 }
607
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__ );
625 }
626 }
627
628 // delete from change_tag
629 $dbw->delete( 'change_tag', array( 'ct_tag' => $tag ), __METHOD__ );
630
631 $dbw->commit( __METHOD__ );
632
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;
640 }
641
642 // clear the memcache of defined tags
643 self::purgeTagCacheAll();
644
645 return $status;
646 }
647
648 /**
649 * Is it OK to allow the user to delete this tag?
650 *
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
656 */
657 public static function canDeleteTag( $tag, User $user = null ) {
658 $tagUsage = self::tagUsageStatistics();
659
660 if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) {
661 return Status::newFatal( 'tags-manage-no-permission' );
662 }
663
664 if ( !isset( $tagUsage[$tag] ) ) {
665 return Status::newFatal( 'tags-delete-not-found', $tag );
666 }
667
668 if ( $tagUsage[$tag] > self::MAX_DELETE_USES ) {
669 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
670 }
671
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();
680 }
681
682 Hooks::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) );
683 return $status;
684 }
685
686 /**
687 * Deletes a tag, checking whether it is allowed first, and adding a log entry
688 * afterwards.
689 *
690 * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
691 * do that.
692 *
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
700 */
701 public static function deleteTagWithChecks( $tag, $reason, User $user,
702 $ignoreWarnings = false ) {
703
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;
709 }
710
711 // store the tag usage statistics
712 $tagUsage = self::tagUsageStatistics();
713
714 // do it!
715 $deleteResult = self::deleteTagEverywhere( $tag );
716 if ( !$deleteResult->isOK() ) {
717 return $deleteResult;
718 }
719
720 // log it
721 $logId = self::logTagAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
722 $deleteResult->value = $logId;
723 return $deleteResult;
724 }
725
726 /**
727 * Lists those tags which extensions report as being "active".
728 *
729 * @return array
730 * @since 1.25
731 */
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;
739 }
740
741 // ask extensions which tags they consider active
742 $extensionActive = array();
743 Hooks::run( 'ChangeTagsListActive', array( &$extensionActive ) );
744
745 // Short-term caching.
746 $wgMemc->set( $key, $extensionActive, 300 );
747 return $extensionActive;
748 }
749
750 /**
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().
754 *
755 * @return string[] Array of strings: tags
756 */
757 public static function listDefinedTags() {
758 $tags1 = self::listExplicitlyDefinedTags();
759 $tags2 = self::listExtensionDefinedTags();
760 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
761 }
762
763 /**
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.
767 *
768 * Tries memcached first.
769 *
770 * @return string[] Array of strings: tags
771 * @since 1.25
772 */
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;
780 }
781
782 $emptyTags = array();
783
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;
789 }
790
791 $emptyTags = array_filter( array_unique( $emptyTags ) );
792
793 // Short-term caching.
794 $wgMemc->set( $key, $emptyTags, 300 );
795 return $emptyTags;
796 }
797
798 /**
799 * Lists tags defined by extensions using the ListDefinedTags hook.
800 * Extensions need only define those tags they deem to be in active use.
801 *
802 * Tries memcached first.
803 *
804 * @return string[] Array of strings: tags
805 * @since 1.25
806 */
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;
814 }
815
816 $emptyTags = array();
817 Hooks::run( 'ListDefinedTags', array( &$emptyTags ) );
818 $emptyTags = array_filter( array_unique( $emptyTags ) );
819
820 // Short-term caching.
821 $wgMemc->set( $key, $emptyTags, 300 );
822 return $emptyTags;
823 }
824
825 /**
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
829 */
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();
836 }
837
838 /**
839 * Invalidates the tag statistics cache only.
840 * @since 1.25
841 */
842 public static function purgeTagUsageCache() {
843 global $wgMemc;
844 $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
845 }
846
847 /**
848 * Returns a map of any tags used on the wiki to number of edits
849 * tagged with them, ordered descending by the hitcount.
850 *
851 * Keeps a short-term cache in memory, so calling this multiple times in the
852 * same request should be fine.
853 *
854 * @return array Array of string => int
855 */
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;
863 }
864
865 $out = array();
866
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' )
874 );
875
876 foreach ( $res as $row ) {
877 $out[$row->ct_tag] = $row->hitcount;
878 }
879 foreach ( self::listDefinedTags() as $tag ) {
880 if ( !isset( $out[$tag] ) ) {
881 $out[$tag] = 0;
882 }
883 }
884
885 // Cache for a very short time
886 $wgMemc->set( $key, $out, 300 );
887 return $out;
888 }
889 }