MediaWiki REL1_30
ChangeTags.php
Go to the documentation of this file.
1<?php
26
33 const MAX_DELETE_USES = 5000;
34
38 private static $coreTags = [ 'mw-contentmodelchange' ];
39
53 public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
54 if ( !$tags ) {
55 return [ '', [] ];
56 }
57 if ( !$context ) {
59 }
60
61 $classes = [];
62
63 $tags = explode( ',', $tags );
64 $displayTags = [];
65 foreach ( $tags as $tag ) {
66 if ( !$tag ) {
67 continue;
68 }
69 $description = self::tagDescription( $tag, $context );
70 if ( $description === false ) {
71 continue;
72 }
73 $displayTags[] = Xml::tags(
74 'span',
75 [ 'class' => 'mw-tag-marker ' .
76 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
77 $description
78 );
79 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
80 }
81
82 if ( !$displayTags ) {
83 return [ '', [] ];
84 }
85
86 $markers = $context->msg( 'tag-list-wrapper' )
87 ->numParams( count( $displayTags ) )
88 ->rawParams( $context->getLanguage()->commaList( $displayTags ) )
89 ->parse();
90 $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
91
92 return [ $markers, $classes ];
93 }
94
108 public static function tagDescription( $tag, IContextSource $context ) {
109 $msg = $context->msg( "tag-$tag" );
110 if ( !$msg->exists() ) {
111 // No such message, so return the HTML-escaped tag name.
112 return htmlspecialchars( $tag );
113 }
114 if ( $msg->isDisabled() ) {
115 // The message exists but is disabled, hide the tag.
116 return false;
117 }
118
119 // Message exists and isn't disabled, use it.
120 return $msg->parse();
121 }
122
135 public static function tagLongDescriptionMessage( $tag, IContextSource $context ) {
136 $msg = $context->msg( "tag-$tag-description" );
137 if ( !$msg->exists() ) {
138 return false;
139 }
140 if ( $msg->isDisabled() ) {
141 // The message exists but is disabled, hide the description.
142 return false;
143 }
144
145 // Message exists and isn't disabled, use it.
146 return $msg;
147 }
148
163 public static function addTags( $tags, $rc_id = null, $rev_id = null,
164 $log_id = null, $params = null, RecentChange $rc = null
165 ) {
166 $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
167 return (bool)$result[0];
168 }
169
200 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
201 &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
202 User $user = null
203 ) {
204 $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
205 $tagsToRemove = array_filter( (array)$tagsToRemove );
206
207 if ( !$rc_id && !$rev_id && !$log_id ) {
208 throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
209 'specified when adding or removing a tag from a change!' );
210 }
211
212 $dbw = wfGetDB( DB_MASTER );
213
214 // Might as well look for rcids and so on.
215 if ( !$rc_id ) {
216 // Info might be out of date, somewhat fractionally, on replica DB.
217 // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
218 // so use that relation to avoid full table scans.
219 if ( $log_id ) {
220 $rc_id = $dbw->selectField(
221 [ 'logging', 'recentchanges' ],
222 'rc_id',
223 [
224 'log_id' => $log_id,
225 'rc_timestamp = log_timestamp',
226 'rc_logid = log_id'
227 ],
228 __METHOD__
229 );
230 } elseif ( $rev_id ) {
231 $rc_id = $dbw->selectField(
232 [ 'revision', 'recentchanges' ],
233 'rc_id',
234 [
235 'rev_id' => $rev_id,
236 'rc_timestamp = rev_timestamp',
237 'rc_this_oldid = rev_id'
238 ],
239 __METHOD__
240 );
241 }
242 } elseif ( !$log_id && !$rev_id ) {
243 // Info might be out of date, somewhat fractionally, on replica DB.
244 $log_id = $dbw->selectField(
245 'recentchanges',
246 'rc_logid',
247 [ 'rc_id' => $rc_id ],
248 __METHOD__
249 );
250 $rev_id = $dbw->selectField(
251 'recentchanges',
252 'rc_this_oldid',
253 [ 'rc_id' => $rc_id ],
254 __METHOD__
255 );
256 }
257
258 if ( $log_id && !$rev_id ) {
259 $rev_id = $dbw->selectField(
260 'log_search',
261 'ls_value',
262 [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
263 __METHOD__
264 );
265 } elseif ( !$log_id && $rev_id ) {
266 $log_id = $dbw->selectField(
267 'log_search',
268 'ls_log_id',
269 [ 'ls_field' => 'associated_rev_id', 'ls_value' => $rev_id ],
270 __METHOD__
271 );
272 }
273
274 // update the tag_summary row
275 $prevTags = [];
276 if ( !self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id,
277 $log_id, $prevTags )
278 ) {
279 // nothing to do
280 return [ [], [], $prevTags ];
281 }
282
283 // insert a row into change_tag for each new tag
284 if ( count( $tagsToAdd ) ) {
285 $tagsRows = [];
286 foreach ( $tagsToAdd as $tag ) {
287 // Filter so we don't insert NULLs as zero accidentally.
288 // Keep in mind that $rc_id === null means "I don't care/know about the
289 // rc_id, just delete $tag on this revision/log entry". It doesn't
290 // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
291 $tagsRows[] = array_filter(
292 [
293 'ct_tag' => $tag,
294 'ct_rc_id' => $rc_id,
295 'ct_log_id' => $log_id,
296 'ct_rev_id' => $rev_id,
297 'ct_params' => $params
298 ]
299 );
300 }
301
302 $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
303 }
304
305 // delete from change_tag
306 if ( count( $tagsToRemove ) ) {
307 foreach ( $tagsToRemove as $tag ) {
308 $conds = array_filter(
309 [
310 'ct_tag' => $tag,
311 'ct_rc_id' => $rc_id,
312 'ct_log_id' => $log_id,
313 'ct_rev_id' => $rev_id
314 ]
315 );
316 $dbw->delete( 'change_tag', $conds, __METHOD__ );
317 }
318 }
319
321
322 Hooks::run( 'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags,
323 $rc_id, $rev_id, $log_id, $params, $rc, $user ] );
324
325 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
326 }
327
344 protected static function updateTagSummaryRow( &$tagsToAdd, &$tagsToRemove,
345 $rc_id, $rev_id, $log_id, &$prevTags = []
346 ) {
347 $dbw = wfGetDB( DB_MASTER );
348
349 $tsConds = array_filter( [
350 'ts_rc_id' => $rc_id,
351 'ts_rev_id' => $rev_id,
352 'ts_log_id' => $log_id
353 ] );
354
355 // Can't both add and remove a tag at the same time...
356 $tagsToAdd = array_diff( $tagsToAdd, $tagsToRemove );
357
358 // Update the summary row.
359 // $prevTags can be out of date on replica DBs, especially when addTags is called consecutively,
360 // causing loss of tags added recently in tag_summary table.
361 $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
362 $prevTags = $prevTags ? $prevTags : '';
363 $prevTags = array_filter( explode( ',', $prevTags ) );
364
365 // add tags
366 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
367 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
368
369 // remove tags
370 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
371 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
372
373 sort( $prevTags );
374 sort( $newTags );
375 if ( $prevTags == $newTags ) {
376 // No change.
377 return false;
378 }
379
380 if ( !$newTags ) {
381 // no tags left, so delete the row altogether
382 $dbw->delete( 'tag_summary', $tsConds, __METHOD__ );
383 } else {
384 $dbw->replace( 'tag_summary',
385 [ 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ],
386 array_filter( array_merge( $tsConds, [ 'ts_tags' => implode( ',', $newTags ) ] ) ),
387 __METHOD__
388 );
389 }
390
391 return true;
392 }
393
404 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
405 $lang = RequestContext::getMain()->getLanguage();
406 $count = count( $tags );
407 return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
408 $lang->commaList( $tags ), $count );
409 }
410
421 public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
422 if ( !is_null( $user ) ) {
423 if ( !$user->isAllowed( 'applychangetags' ) ) {
424 return Status::newFatal( 'tags-apply-no-permission' );
425 } elseif ( $user->isBlocked() ) {
426 return Status::newFatal( 'tags-apply-blocked', $user->getName() );
427 }
428 }
429
430 // to be applied, a tag has to be explicitly defined
431 $allowedTags = self::listExplicitlyDefinedTags();
432 Hooks::run( 'ChangeTagsAllowedAdd', [ &$allowedTags, $tags, $user ] );
433 $disallowedTags = array_diff( $tags, $allowedTags );
434 if ( $disallowedTags ) {
435 return self::restrictedTagError( 'tags-apply-not-allowed-one',
436 'tags-apply-not-allowed-multi', $disallowedTags );
437 }
438
439 return Status::newGood();
440 }
441
463 array $tags, $rc_id, $rev_id, $log_id, $params, User $user
464 ) {
465 // are we allowed to do this?
466 $result = self::canAddTagsAccompanyingChange( $tags, $user );
467 if ( !$result->isOK() ) {
468 $result->value = null;
469 return $result;
470 }
471
472 // do it!
473 self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
474
475 return Status::newGood( true );
476 }
477
489 public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
490 User $user = null
491 ) {
492 if ( !is_null( $user ) ) {
493 if ( !$user->isAllowed( 'changetags' ) ) {
494 return Status::newFatal( 'tags-update-no-permission' );
495 } elseif ( $user->isBlocked() ) {
496 return Status::newFatal( 'tags-update-blocked', $user->getName() );
497 }
498 }
499
500 if ( $tagsToAdd ) {
501 // to be added, a tag has to be explicitly defined
502 // @todo Allow extensions to define tags that can be applied by users...
503 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
504 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
505 if ( $diff ) {
506 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
507 'tags-update-add-not-allowed-multi', $diff );
508 }
509 }
510
511 if ( $tagsToRemove ) {
512 // to be removed, a tag must not be defined by an extension, or equivalently it
513 // has to be either explicitly defined or not defined at all
514 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
515 $softwareDefinedTags = self::listSoftwareDefinedTags();
516 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
517 if ( $intersect ) {
518 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
519 'tags-update-remove-not-allowed-multi', $intersect );
520 }
521 }
522
523 return Status::newGood();
524 }
525
552 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
553 $rc_id, $rev_id, $log_id, $params, $reason, User $user
554 ) {
555 if ( is_null( $tagsToAdd ) ) {
556 $tagsToAdd = [];
557 }
558 if ( is_null( $tagsToRemove ) ) {
559 $tagsToRemove = [];
560 }
561 if ( !$tagsToAdd && !$tagsToRemove ) {
562 // no-op, don't bother
563 return Status::newGood( (object)[
564 'logId' => null,
565 'addedTags' => [],
566 'removedTags' => [],
567 ] );
568 }
569
570 // are we allowed to do this?
571 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
572 if ( !$result->isOK() ) {
573 $result->value = null;
574 return $result;
575 }
576
577 // basic rate limiting
578 if ( $user->pingLimiter( 'changetag' ) ) {
579 return Status::newFatal( 'actionthrottledtext' );
580 }
581
582 // do it!
583 list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
584 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
585 if ( !$tagsAdded && !$tagsRemoved ) {
586 // no-op, don't log it
587 return Status::newGood( (object)[
588 'logId' => null,
589 'addedTags' => [],
590 'removedTags' => [],
591 ] );
592 }
593
594 // log it
595 $logEntry = new ManualLogEntry( 'tag', 'update' );
596 $logEntry->setPerformer( $user );
597 $logEntry->setComment( $reason );
598
599 // find the appropriate target page
600 if ( $rev_id ) {
601 $rev = Revision::newFromId( $rev_id );
602 if ( $rev ) {
603 $logEntry->setTarget( $rev->getTitle() );
604 }
605 } elseif ( $log_id ) {
606 // This function is from revision deletion logic and has nothing to do with
607 // change tags, but it appears to be the only other place in core where we
608 // perform logged actions on log items.
609 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
610 }
611
612 if ( !$logEntry->getTarget() ) {
613 // target is required, so we have to set something
614 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
615 }
616
617 $logParams = [
618 '4::revid' => $rev_id,
619 '5::logid' => $log_id,
620 '6:list:tagsAdded' => $tagsAdded,
621 '7:number:tagsAddedCount' => count( $tagsAdded ),
622 '8:list:tagsRemoved' => $tagsRemoved,
623 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
624 'initialTags' => $initialTags,
625 ];
626 $logEntry->setParameters( $logParams );
627 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
628
629 $dbw = wfGetDB( DB_MASTER );
630 $logId = $logEntry->insert( $dbw );
631 // Only send this to UDP, not RC, similar to patrol events
632 $logEntry->publish( $logId, 'udp' );
633
634 return Status::newGood( (object)[
635 'logId' => $logId,
636 'addedTags' => $tagsAdded,
637 'removedTags' => $tagsRemoved,
638 ] );
639 }
640
661 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
662 &$join_conds, &$options, $filter_tag = '' ) {
663 global $wgUseTagFilter;
664
665 // Normalize to arrays
667 $fields = (array)$fields;
668 $conds = (array)$conds;
670
671 // Figure out which ID field to use
672 if ( in_array( 'recentchanges', $tables ) ) {
673 $join_cond = 'ct_rc_id=rc_id';
674 } elseif ( in_array( 'logging', $tables ) ) {
675 $join_cond = 'ct_log_id=log_id';
676 } elseif ( in_array( 'revision', $tables ) ) {
677 $join_cond = 'ct_rev_id=rev_id';
678 } elseif ( in_array( 'archive', $tables ) ) {
679 $join_cond = 'ct_rev_id=ar_rev_id';
680 } else {
681 throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
682 }
683
684 $fields['ts_tags'] = wfGetDB( DB_REPLICA )->buildGroupConcatField(
685 ',', 'change_tag', 'ct_tag', $join_cond
686 );
687
688 if ( $wgUseTagFilter && $filter_tag ) {
689 // Somebody wants to filter on a tag.
690 // Add an INNER JOIN on change_tag
691
692 $tables[] = 'change_tag';
693 $join_conds['change_tag'] = [ 'INNER JOIN', $join_cond ];
694 $conds['ct_tag'] = $filter_tag;
695 if (
696 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
697 !in_array( 'DISTINCT', $options )
698 ) {
699 $options[] = 'DISTINCT';
700 }
701 }
702 }
703
715 public static function buildTagFilterSelector(
716 $selected = '', $ooui = false, IContextSource $context = null
717 ) {
718 if ( !$context ) {
720 }
721
722 $config = $context->getConfig();
723 if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
724 return [];
725 }
726
727 $data = [
728 Html::rawElement(
729 'label',
730 [ 'for' => 'tagfilter' ],
731 $context->msg( 'tag-filter' )->parse()
732 )
733 ];
734
735 if ( $ooui ) {
736 $data[] = new OOUI\TextInputWidget( [
737 'id' => 'tagfilter',
738 'name' => 'tagfilter',
739 'value' => $selected,
740 'classes' => 'mw-tagfilter-input',
741 ] );
742 } else {
743 $data[] = Xml::input(
744 'tagfilter',
745 20,
746 $selected,
747 [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
748 );
749 }
750
751 return $data;
752 }
753
763 public static function defineTag( $tag ) {
764 $dbw = wfGetDB( DB_MASTER );
765 $dbw->replace( 'valid_tag',
766 [ 'vt_tag' ],
767 [ 'vt_tag' => $tag ],
768 __METHOD__ );
769
770 // clear the memcache of defined tags
772 }
773
782 public static function undefineTag( $tag ) {
783 $dbw = wfGetDB( DB_MASTER );
784 $dbw->delete( 'valid_tag', [ 'vt_tag' => $tag ], __METHOD__ );
785
786 // clear the memcache of defined tags
788 }
789
804 protected static function logTagManagementAction( $action, $tag, $reason,
805 User $user, $tagCount = null, array $logEntryTags = []
806 ) {
807 $dbw = wfGetDB( DB_MASTER );
808
809 $logEntry = new ManualLogEntry( 'managetags', $action );
810 $logEntry->setPerformer( $user );
811 // target page is not relevant, but it has to be set, so we just put in
812 // the title of Special:Tags
813 $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
814 $logEntry->setComment( $reason );
815
816 $params = [ '4::tag' => $tag ];
817 if ( !is_null( $tagCount ) ) {
818 $params['5:number:count'] = $tagCount;
819 }
820 $logEntry->setParameters( $params );
821 $logEntry->setRelations( [ 'Tag' => $tag ] );
822 $logEntry->setTags( $logEntryTags );
823
824 $logId = $logEntry->insert( $dbw );
825 $logEntry->publish( $logId );
826 return $logId;
827 }
828
838 public static function canActivateTag( $tag, User $user = null ) {
839 if ( !is_null( $user ) ) {
840 if ( !$user->isAllowed( 'managechangetags' ) ) {
841 return Status::newFatal( 'tags-manage-no-permission' );
842 } elseif ( $user->isBlocked() ) {
843 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
844 }
845 }
846
847 // defined tags cannot be activated (a defined tag is either extension-
848 // defined, in which case the extension chooses whether or not to active it;
849 // or user-defined, in which case it is considered active)
850 $definedTags = self::listDefinedTags();
851 if ( in_array( $tag, $definedTags ) ) {
852 return Status::newFatal( 'tags-activate-not-allowed', $tag );
853 }
854
855 // non-existing tags cannot be activated
856 $tagUsage = self::tagUsageStatistics();
857 if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
858 return Status::newFatal( 'tags-activate-not-found', $tag );
859 }
860
861 return Status::newGood();
862 }
863
881 public static function activateTagWithChecks( $tag, $reason, User $user,
882 $ignoreWarnings = false, array $logEntryTags = []
883 ) {
884 // are we allowed to do this?
885 $result = self::canActivateTag( $tag, $user );
886 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
887 $result->value = null;
888 return $result;
889 }
890
891 // do it!
892 self::defineTag( $tag );
893
894 // log it
895 $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
896 null, $logEntryTags );
897
898 return Status::newGood( $logId );
899 }
900
910 public static function canDeactivateTag( $tag, User $user = null ) {
911 if ( !is_null( $user ) ) {
912 if ( !$user->isAllowed( 'managechangetags' ) ) {
913 return Status::newFatal( 'tags-manage-no-permission' );
914 } elseif ( $user->isBlocked() ) {
915 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
916 }
917 }
918
919 // only explicitly-defined tags can be deactivated
920 $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
921 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
922 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
923 }
924 return Status::newGood();
925 }
926
944 public static function deactivateTagWithChecks( $tag, $reason, User $user,
945 $ignoreWarnings = false, array $logEntryTags = []
946 ) {
947 // are we allowed to do this?
948 $result = self::canDeactivateTag( $tag, $user );
949 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
950 $result->value = null;
951 return $result;
952 }
953
954 // do it!
955 self::undefineTag( $tag );
956
957 // log it
958 $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
959 null, $logEntryTags );
960
961 return Status::newGood( $logId );
962 }
963
971 public static function isTagNameValid( $tag ) {
972 // no empty tags
973 if ( $tag === '' ) {
974 return Status::newFatal( 'tags-create-no-name' );
975 }
976
977 // tags cannot contain commas (used as a delimiter in tag_summary table),
978 // pipe (used as a delimiter between multiple tags in
979 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
980 // MediaWiki namespace)
981 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
982 || strpos( $tag, '/' ) !== false ) {
983 return Status::newFatal( 'tags-create-invalid-chars' );
984 }
985
986 // could the MediaWiki namespace description messages be created?
987 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
988 if ( is_null( $title ) ) {
989 return Status::newFatal( 'tags-create-invalid-title-chars' );
990 }
991
992 return Status::newGood();
993 }
994
1004 public static function canCreateTag( $tag, User $user = null ) {
1005 if ( !is_null( $user ) ) {
1006 if ( !$user->isAllowed( 'managechangetags' ) ) {
1007 return Status::newFatal( 'tags-manage-no-permission' );
1008 } elseif ( $user->isBlocked() ) {
1009 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1010 }
1011 }
1012
1013 $status = self::isTagNameValid( $tag );
1014 if ( !$status->isGood() ) {
1015 return $status;
1016 }
1017
1018 // does the tag already exist?
1019 $tagUsage = self::tagUsageStatistics();
1020 if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1021 return Status::newFatal( 'tags-create-already-exists', $tag );
1022 }
1023
1024 // check with hooks
1025 $canCreateResult = Status::newGood();
1026 Hooks::run( 'ChangeTagCanCreate', [ $tag, $user, &$canCreateResult ] );
1027 return $canCreateResult;
1028 }
1029
1046 public static function createTagWithChecks( $tag, $reason, User $user,
1047 $ignoreWarnings = false, array $logEntryTags = []
1048 ) {
1049 // are we allowed to do this?
1050 $result = self::canCreateTag( $tag, $user );
1051 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1052 $result->value = null;
1053 return $result;
1054 }
1055
1056 // do it!
1057 self::defineTag( $tag );
1058
1059 // log it
1060 $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1061 null, $logEntryTags );
1062
1063 return Status::newGood( $logId );
1064 }
1065
1078 public static function deleteTagEverywhere( $tag ) {
1079 $dbw = wfGetDB( DB_MASTER );
1080 $dbw->startAtomic( __METHOD__ );
1081
1082 // delete from valid_tag
1083 self::undefineTag( $tag );
1084
1085 // find out which revisions use this tag, so we can delete from tag_summary
1086 $result = $dbw->select( 'change_tag',
1087 [ 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ],
1088 [ 'ct_tag' => $tag ],
1089 __METHOD__ );
1090 foreach ( $result as $row ) {
1091 // remove the tag from the relevant row of tag_summary
1092 $tagsToAdd = [];
1093 $tagsToRemove = [ $tag ];
1094 self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $row->ct_rc_id,
1095 $row->ct_rev_id, $row->ct_log_id );
1096 }
1097
1098 // delete from change_tag
1099 $dbw->delete( 'change_tag', [ 'ct_tag' => $tag ], __METHOD__ );
1100
1101 $dbw->endAtomic( __METHOD__ );
1102
1103 // give extensions a chance
1104 $status = Status::newGood();
1105 Hooks::run( 'ChangeTagAfterDelete', [ $tag, &$status ] );
1106 // let's not allow error results, as the actual tag deletion succeeded
1107 if ( !$status->isOK() ) {
1108 wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1109 $status->setOK( true );
1110 }
1111
1112 // clear the memcache of defined tags
1114
1115 return $status;
1116 }
1117
1127 public static function canDeleteTag( $tag, User $user = null ) {
1128 $tagUsage = self::tagUsageStatistics();
1129
1130 if ( !is_null( $user ) ) {
1131 if ( !$user->isAllowed( 'deletechangetags' ) ) {
1132 return Status::newFatal( 'tags-delete-no-permission' );
1133 } elseif ( $user->isBlocked() ) {
1134 return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1135 }
1136 }
1137
1138 if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1139 return Status::newFatal( 'tags-delete-not-found', $tag );
1140 }
1141
1142 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > self::MAX_DELETE_USES ) {
1143 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1144 }
1145
1146 $softwareDefined = self::listSoftwareDefinedTags();
1147 if ( in_array( $tag, $softwareDefined ) ) {
1148 // extension-defined tags can't be deleted unless the extension
1149 // specifically allows it
1150 $status = Status::newFatal( 'tags-delete-not-allowed' );
1151 } else {
1152 // user-defined tags are deletable unless otherwise specified
1153 $status = Status::newGood();
1154 }
1155
1156 Hooks::run( 'ChangeTagCanDelete', [ $tag, $user, &$status ] );
1157 return $status;
1158 }
1159
1177 public static function deleteTagWithChecks( $tag, $reason, User $user,
1178 $ignoreWarnings = false, array $logEntryTags = []
1179 ) {
1180 // are we allowed to do this?
1181 $result = self::canDeleteTag( $tag, $user );
1182 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1183 $result->value = null;
1184 return $result;
1185 }
1186
1187 // store the tag usage statistics
1188 $tagUsage = self::tagUsageStatistics();
1189 $hitcount = isset( $tagUsage[$tag] ) ? $tagUsage[$tag] : 0;
1190
1191 // do it!
1192 $deleteResult = self::deleteTagEverywhere( $tag );
1193 if ( !$deleteResult->isOK() ) {
1194 return $deleteResult;
1195 }
1196
1197 // log it
1198 $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1199 $hitcount, $logEntryTags );
1200
1201 $deleteResult->value = $logId;
1202 return $deleteResult;
1203 }
1204
1211 public static function listSoftwareActivatedTags() {
1212 // core active tags
1213 $tags = self::$coreTags;
1214 if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) {
1215 return $tags;
1216 }
1217 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1218 return $cache->getWithSetCallback(
1219 $cache->makeKey( 'active-tags' ),
1220 WANObjectCache::TTL_MINUTE * 5,
1221 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1222 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1223
1224 // Ask extensions which tags they consider active
1225 Hooks::run( 'ChangeTagsListActive', [ &$tags ] );
1226 return $tags;
1227 },
1228 [
1229 'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1230 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1231 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1232 ]
1233 );
1234 }
1235
1241 public static function listExtensionActivatedTags() {
1242 wfDeprecated( __METHOD__, '1.28' );
1244 }
1245
1253 public static function listDefinedTags() {
1256 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1257 }
1258
1269 public static function listExplicitlyDefinedTags() {
1270 $fname = __METHOD__;
1271
1272 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1273 return $cache->getWithSetCallback(
1274 $cache->makeKey( 'valid-tags-db' ),
1275 WANObjectCache::TTL_MINUTE * 5,
1276 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1277 $dbr = wfGetDB( DB_REPLICA );
1278
1279 $setOpts += Database::getCacheSetOptions( $dbr );
1280
1281 $tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', [], $fname );
1282
1283 return array_filter( array_unique( $tags ) );
1284 },
1285 [
1286 'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1287 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1288 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1289 ]
1290 );
1291 }
1292
1302 public static function listSoftwareDefinedTags() {
1303 // core defined tags
1304 $tags = self::$coreTags;
1305 if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) {
1306 return $tags;
1307 }
1308 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1309 return $cache->getWithSetCallback(
1310 $cache->makeKey( 'valid-tags-hook' ),
1311 WANObjectCache::TTL_MINUTE * 5,
1312 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1313 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1314
1315 Hooks::run( 'ListDefinedTags', [ &$tags ] );
1316 return array_filter( array_unique( $tags ) );
1317 },
1318 [
1319 'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1320 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1321 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1322 ]
1323 );
1324 }
1325
1333 public static function listExtensionDefinedTags() {
1334 wfDeprecated( __METHOD__, '1.28' );
1336 }
1337
1343 public static function purgeTagCacheAll() {
1344 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1345
1346 $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1347 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1348 $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1349
1351 }
1352
1357 public static function purgeTagUsageCache() {
1358 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1359
1360 $cache->touchCheckKey( $cache->makeKey( 'change-tag-statistics' ) );
1361 }
1362
1373 public static function tagUsageStatistics() {
1374 $fname = __METHOD__;
1375 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1376 return $cache->getWithSetCallback(
1377 $cache->makeKey( 'change-tag-statistics' ),
1378 WANObjectCache::TTL_MINUTE * 5,
1379 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1380 $dbr = wfGetDB( DB_REPLICA, 'vslow' );
1381
1382 $setOpts += Database::getCacheSetOptions( $dbr );
1383
1384 $res = $dbr->select(
1385 'change_tag',
1386 [ 'ct_tag', 'hitcount' => 'count(*)' ],
1387 [],
1388 $fname,
1389 [ 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ]
1390 );
1391
1392 $out = [];
1393 foreach ( $res as $row ) {
1394 $out[$row->ct_tag] = $row->hitcount;
1395 }
1396
1397 return $out;
1398 },
1399 [
1400 'checkKeys' => [ $cache->makeKey( 'change-tag-statistics' ) ],
1401 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1402 'pcTTL' => WANObjectCache::TTL_PROC_LONG
1403 ]
1404 );
1405 }
1406
1421 public static function showTagEditingUI( User $user ) {
1422 return $user->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1423 }
1424}
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined.
Definition Setup.php:36
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding a row to the valid_tag table.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
static purgeTagUsageCache()
Invalidates the tag statistics cache only.
static deactivateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static logTagManagementAction( $action, $tag, $reason, User $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, User $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
static updateTagSummaryRow(&$tagsToAdd, &$tagsToRemove, $rc_id, $rev_id, $log_id, &$prevTags=[])
Adds or removes a given set of tags to/from the relevant row of the tag_summary table.
static tagLongDescriptionMessage( $tag, IContextSource $context)
Get the message object for the tag's long description.
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
static activateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static tagDescription( $tag, IContextSource $context)
Get a short description for a tag.
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static undefineTag( $tag)
Removes a tag from the valid_tag table.
static canCreateTag( $tag, User $user=null)
Is it OK to allow the user to create this tag?
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static addTagsAccompanyingChangeWithChecks(array $tags, $rc_id, $rev_id, $log_id, $params, User $user)
Adds tags to a given change, checking whether it is allowed first, but without adding a log entry.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
static listExtensionActivatedTags()
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $reason, User $user)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
static defineTag( $tag)
Defines a tag in the valid_tag table, without checking that the tag name is valid.
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
static canDeleteTag( $tag, User $user=null)
Is it OK to allow the user to delete this tag?
static deleteTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static isTagNameValid( $tag)
Is the tag name valid?
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
static listExtensionDefinedTags()
Call listSoftwareDefinedTags directly.
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, User $user=null)
Is it OK to allow the user to adds and remove the given tags tags to/from a change?
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the valid_tag table of the database.
static string[] $coreTags
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
MediaWiki exception.
Class for creating log entries manually, to inject them into the database.
Definition LogEntry.php:400
MediaWikiServices is the service locator for the application scope of MediaWiki.
Utility class for creating new RC entries.
static getMain()
Static methods.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:116
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
Relational database abstraction object.
Definition Database.php:45
if(! $regexes) $dbr
Definition cleanup.php:94
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
when a variable name is used in a function
Definition design.txt:94
the array() calling protocol came about after MediaWiki 1.4rc1.
namespace being checked & $result
Definition hooks.txt:2293
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1013
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1971
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2780
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:862
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1049
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1760
Interface for objects which can provide a MediaWiki context on request.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
$params
if(!isset( $args[0])) $lang