These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 | * @ingroup Change tagging |
||
22 | */ |
||
23 | |||
24 | class ChangeTags { |
||
25 | /** |
||
26 | * Can't delete tags with more than this many uses. Similar in intent to |
||
27 | * the bigdelete user right |
||
28 | * @todo Use the job queue for tag deletion to avoid this restriction |
||
29 | */ |
||
30 | const MAX_DELETE_USES = 5000; |
||
31 | |||
32 | /** |
||
33 | * @var string[] |
||
34 | */ |
||
35 | private static $coreTags = [ 'mw-contentmodelchange' ]; |
||
36 | |||
37 | /** |
||
38 | * Creates HTML for the given tags |
||
39 | * |
||
40 | * @param string $tags Comma-separated list of tags |
||
41 | * @param string $page A label for the type of action which is being displayed, |
||
42 | * for example: 'history', 'contributions' or 'newpages' |
||
43 | * @param IContextSource|null $context |
||
44 | * @note Even though it takes null as a valid argument, an IContextSource is preferred |
||
45 | * in a new code, as the null value is subject to change in the future |
||
46 | * @return array Array with two items: (html, classes) |
||
47 | * - html: String: HTML for displaying the tags (empty string when param $tags is empty) |
||
48 | * - classes: Array of strings: CSS classes used in the generated html, one class for each tag |
||
49 | */ |
||
50 | public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) { |
||
51 | if ( !$tags ) { |
||
52 | return [ '', [] ]; |
||
53 | } |
||
54 | if ( !$context ) { |
||
55 | $context = RequestContext::getMain(); |
||
56 | } |
||
57 | |||
58 | $classes = []; |
||
59 | |||
60 | $tags = explode( ',', $tags ); |
||
61 | $displayTags = []; |
||
62 | foreach ( $tags as $tag ) { |
||
63 | if ( !$tag ) { |
||
64 | continue; |
||
65 | } |
||
66 | $description = self::tagDescription( $tag, $context ); |
||
67 | if ( $description === false ) { |
||
68 | continue; |
||
69 | } |
||
70 | $displayTags[] = Xml::tags( |
||
71 | 'span', |
||
72 | [ 'class' => 'mw-tag-marker ' . |
||
73 | Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ], |
||
74 | $description |
||
75 | ); |
||
76 | $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" ); |
||
77 | } |
||
78 | |||
79 | if ( !$displayTags ) { |
||
80 | return [ '', [] ]; |
||
81 | } |
||
82 | |||
83 | $markers = $context->msg( 'tag-list-wrapper' ) |
||
84 | ->numParams( count( $displayTags ) ) |
||
85 | ->rawParams( $context->getLanguage()->commaList( $displayTags ) ) |
||
86 | ->parse(); |
||
87 | $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers ); |
||
88 | |||
89 | return [ $markers, $classes ]; |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Get a short description for a tag. |
||
94 | * |
||
95 | * Checks if message key "mediawiki:tag-$tag" exists. If it does not, |
||
96 | * returns the HTML-escaped tag name. Uses the message if the message |
||
97 | * exists, provided it is not disabled. If the message is disabled, |
||
98 | * we consider the tag hidden, and return false. |
||
99 | * |
||
100 | * @param string $tag Tag |
||
101 | * @param IContextSource $context |
||
102 | * @return string|bool Tag description or false if tag is to be hidden. |
||
103 | * @since 1.25 Returns false if tag is to be hidden. |
||
104 | */ |
||
105 | public static function tagDescription( $tag, IContextSource $context ) { |
||
106 | $msg = $context->msg( "tag-$tag" ); |
||
107 | if ( !$msg->exists() ) { |
||
108 | // No such message, so return the HTML-escaped tag name. |
||
109 | return htmlspecialchars( $tag ); |
||
110 | } |
||
111 | if ( $msg->isDisabled() ) { |
||
112 | // The message exists but is disabled, hide the tag. |
||
113 | return false; |
||
114 | } |
||
115 | |||
116 | // Message exists and isn't disabled, use it. |
||
117 | return $msg->parse(); |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * Add tags to a change given its rc_id, rev_id and/or log_id |
||
122 | * |
||
123 | * @param string|string[] $tags Tags to add to the change |
||
124 | * @param int|null $rc_id The rc_id of the change to add the tags to |
||
125 | * @param int|null $rev_id The rev_id of the change to add the tags to |
||
126 | * @param int|null $log_id The log_id of the change to add the tags to |
||
127 | * @param string $params Params to put in the ct_params field of table 'change_tag' |
||
128 | * @param RecentChange|null $rc Recent change, in case the tagging accompanies the action |
||
129 | * (this should normally be the case) |
||
130 | * |
||
131 | * @throws MWException |
||
132 | * @return bool False if no changes are made, otherwise true |
||
133 | */ |
||
134 | public static function addTags( $tags, $rc_id = null, $rev_id = null, |
||
135 | $log_id = null, $params = null, RecentChange $rc = null |
||
136 | ) { |
||
137 | $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc ); |
||
138 | return (bool)$result[0]; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, |
||
143 | * without verifying that the tags exist or are valid. If a tag is present in |
||
144 | * both $tagsToAdd and $tagsToRemove, it will be removed. |
||
145 | * |
||
146 | * This function should only be used by extensions to manipulate tags they |
||
147 | * have registered using the ListDefinedTags hook. When dealing with user |
||
148 | * input, call updateTagsWithChecks() instead. |
||
149 | * |
||
150 | * @param string|array|null $tagsToAdd Tags to add to the change |
||
151 | * @param string|array|null $tagsToRemove Tags to remove from the change |
||
152 | * @param int|null &$rc_id The rc_id of the change to add the tags to. |
||
153 | * Pass a variable whose value is null if the rc_id is not relevant or unknown. |
||
154 | * @param int|null &$rev_id The rev_id of the change to add the tags to. |
||
155 | * Pass a variable whose value is null if the rev_id is not relevant or unknown. |
||
156 | * @param int|null &$log_id The log_id of the change to add the tags to. |
||
157 | * Pass a variable whose value is null if the log_id is not relevant or unknown. |
||
158 | * @param string $params Params to put in the ct_params field of table |
||
159 | * 'change_tag' when adding tags |
||
160 | * @param RecentChange|null $rc Recent change being tagged, in case the tagging accompanies |
||
161 | * the action |
||
162 | * @param User|null $user Tagging user, in case the tagging is subsequent to the tagged action |
||
163 | * |
||
164 | * @throws MWException When $rc_id, $rev_id and $log_id are all null |
||
165 | * @return array Index 0 is an array of tags actually added, index 1 is an |
||
166 | * array of tags actually removed, index 2 is an array of tags present on the |
||
167 | * revision or log entry before any changes were made |
||
168 | * |
||
169 | * @since 1.25 |
||
170 | */ |
||
171 | public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null, |
||
172 | &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null, |
||
173 | User $user = null |
||
174 | ) { |
||
175 | |||
176 | $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags... |
||
177 | $tagsToRemove = array_filter( (array)$tagsToRemove ); |
||
178 | |||
179 | if ( !$rc_id && !$rev_id && !$log_id ) { |
||
180 | throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' . |
||
181 | 'specified when adding or removing a tag from a change!' ); |
||
182 | } |
||
183 | |||
184 | $dbw = wfGetDB( DB_MASTER ); |
||
185 | |||
186 | // Might as well look for rcids and so on. |
||
187 | if ( !$rc_id ) { |
||
188 | // Info might be out of date, somewhat fractionally, on replica DB. |
||
189 | // LogEntry/LogPage and WikiPage match rev/log/rc timestamps, |
||
190 | // so use that relation to avoid full table scans. |
||
191 | if ( $log_id ) { |
||
192 | $rc_id = $dbw->selectField( |
||
193 | [ 'logging', 'recentchanges' ], |
||
194 | 'rc_id', |
||
195 | [ |
||
196 | 'log_id' => $log_id, |
||
197 | 'rc_timestamp = log_timestamp', |
||
198 | 'rc_logid = log_id' |
||
199 | ], |
||
200 | __METHOD__ |
||
201 | ); |
||
202 | View Code Duplication | } elseif ( $rev_id ) { |
|
203 | $rc_id = $dbw->selectField( |
||
204 | [ 'revision', 'recentchanges' ], |
||
205 | 'rc_id', |
||
206 | [ |
||
207 | 'rev_id' => $rev_id, |
||
208 | 'rc_timestamp = rev_timestamp', |
||
209 | 'rc_this_oldid = rev_id' |
||
210 | ], |
||
211 | __METHOD__ |
||
212 | ); |
||
213 | } |
||
214 | } elseif ( !$log_id && !$rev_id ) { |
||
215 | // Info might be out of date, somewhat fractionally, on replica DB. |
||
216 | $log_id = $dbw->selectField( |
||
217 | 'recentchanges', |
||
218 | 'rc_logid', |
||
219 | [ 'rc_id' => $rc_id ], |
||
220 | __METHOD__ |
||
221 | ); |
||
222 | $rev_id = $dbw->selectField( |
||
223 | 'recentchanges', |
||
224 | 'rc_this_oldid', |
||
225 | [ 'rc_id' => $rc_id ], |
||
226 | __METHOD__ |
||
227 | ); |
||
228 | } |
||
229 | |||
230 | if ( $log_id && !$rev_id ) { |
||
231 | $rev_id = $dbw->selectField( |
||
232 | 'log_search', |
||
233 | 'ls_value', |
||
234 | [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ], |
||
235 | __METHOD__ |
||
236 | ); |
||
237 | } elseif ( !$log_id && $rev_id ) { |
||
238 | $log_id = $dbw->selectField( |
||
239 | 'log_search', |
||
240 | 'ls_log_id', |
||
241 | [ 'ls_field' => 'associated_rev_id', 'ls_value' => $rev_id ], |
||
242 | __METHOD__ |
||
243 | ); |
||
244 | } |
||
245 | |||
246 | // update the tag_summary row |
||
247 | $prevTags = []; |
||
248 | if ( !self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, |
||
249 | $log_id, $prevTags ) ) { |
||
250 | |||
251 | // nothing to do |
||
252 | return [ [], [], $prevTags ]; |
||
253 | } |
||
254 | |||
255 | // insert a row into change_tag for each new tag |
||
256 | if ( count( $tagsToAdd ) ) { |
||
257 | $tagsRows = []; |
||
258 | View Code Duplication | foreach ( $tagsToAdd as $tag ) { |
|
259 | // Filter so we don't insert NULLs as zero accidentally. |
||
260 | // Keep in mind that $rc_id === null means "I don't care/know about the |
||
261 | // rc_id, just delete $tag on this revision/log entry". It doesn't |
||
262 | // mean "only delete tags on this revision/log WHERE rc_id IS NULL". |
||
263 | $tagsRows[] = array_filter( |
||
264 | [ |
||
265 | 'ct_tag' => $tag, |
||
266 | 'ct_rc_id' => $rc_id, |
||
267 | 'ct_log_id' => $log_id, |
||
268 | 'ct_rev_id' => $rev_id, |
||
269 | 'ct_params' => $params |
||
270 | ] |
||
271 | ); |
||
272 | } |
||
273 | |||
274 | $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] ); |
||
275 | } |
||
276 | |||
277 | // delete from change_tag |
||
278 | if ( count( $tagsToRemove ) ) { |
||
279 | View Code Duplication | foreach ( $tagsToRemove as $tag ) { |
|
280 | $conds = array_filter( |
||
281 | [ |
||
282 | 'ct_tag' => $tag, |
||
283 | 'ct_rc_id' => $rc_id, |
||
284 | 'ct_log_id' => $log_id, |
||
285 | 'ct_rev_id' => $rev_id |
||
286 | ] |
||
287 | ); |
||
288 | $dbw->delete( 'change_tag', $conds, __METHOD__ ); |
||
289 | } |
||
290 | } |
||
291 | |||
292 | self::purgeTagUsageCache(); |
||
293 | |||
294 | Hooks::run( 'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags, |
||
295 | $rc_id, $rev_id, $log_id, $params, $rc, $user ] ); |
||
296 | |||
297 | return [ $tagsToAdd, $tagsToRemove, $prevTags ]; |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * Adds or removes a given set of tags to/from the relevant row of the |
||
302 | * tag_summary table. Modifies the tagsToAdd and tagsToRemove arrays to |
||
303 | * reflect the tags that were actually added and/or removed. |
||
304 | * |
||
305 | * @param array &$tagsToAdd |
||
306 | * @param array &$tagsToRemove If a tag is present in both $tagsToAdd and |
||
307 | * $tagsToRemove, it will be removed |
||
308 | * @param int|null $rc_id Null if not known or not applicable |
||
309 | * @param int|null $rev_id Null if not known or not applicable |
||
310 | * @param int|null $log_id Null if not known or not applicable |
||
311 | * @param array &$prevTags Optionally outputs a list of the tags that were |
||
312 | * in the tag_summary row to begin with |
||
313 | * @return bool True if any modifications were made, otherwise false |
||
314 | * @since 1.25 |
||
315 | */ |
||
316 | protected static function updateTagSummaryRow( &$tagsToAdd, &$tagsToRemove, |
||
317 | $rc_id, $rev_id, $log_id, &$prevTags = [] ) { |
||
318 | |||
319 | $dbw = wfGetDB( DB_MASTER ); |
||
320 | |||
321 | $tsConds = array_filter( [ |
||
322 | 'ts_rc_id' => $rc_id, |
||
323 | 'ts_rev_id' => $rev_id, |
||
324 | 'ts_log_id' => $log_id |
||
325 | ] ); |
||
326 | |||
327 | // Can't both add and remove a tag at the same time... |
||
328 | $tagsToAdd = array_diff( $tagsToAdd, $tagsToRemove ); |
||
329 | |||
330 | // Update the summary row. |
||
331 | // $prevTags can be out of date on replica DBs, especially when addTags is called consecutively, |
||
332 | // causing loss of tags added recently in tag_summary table. |
||
333 | $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ ); |
||
334 | $prevTags = $prevTags ? $prevTags : ''; |
||
335 | $prevTags = array_filter( explode( ',', $prevTags ) ); |
||
336 | |||
337 | // add tags |
||
338 | $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) ); |
||
339 | $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) ); |
||
340 | |||
341 | // remove tags |
||
342 | $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) ); |
||
343 | $newTags = array_values( array_diff( $newTags, $tagsToRemove ) ); |
||
344 | |||
345 | sort( $prevTags ); |
||
346 | sort( $newTags ); |
||
347 | if ( $prevTags == $newTags ) { |
||
348 | // No change. |
||
349 | return false; |
||
350 | } |
||
351 | |||
352 | if ( !$newTags ) { |
||
353 | // no tags left, so delete the row altogether |
||
354 | $dbw->delete( 'tag_summary', $tsConds, __METHOD__ ); |
||
355 | } else { |
||
356 | $dbw->replace( 'tag_summary', |
||
357 | [ 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ], |
||
358 | array_filter( array_merge( $tsConds, [ 'ts_tags' => implode( ',', $newTags ) ] ) ), |
||
359 | __METHOD__ |
||
360 | ); |
||
361 | } |
||
362 | |||
363 | return true; |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * Helper function to generate a fatal status with a 'not-allowed' type error. |
||
368 | * |
||
369 | * @param string $msgOne Message key to use in the case of one tag |
||
370 | * @param string $msgMulti Message key to use in the case of more than one tag |
||
371 | * @param array $tags Restricted tags (passed as $1 into the message, count of |
||
372 | * $tags passed as $2) |
||
373 | * @return Status |
||
374 | * @since 1.25 |
||
375 | */ |
||
376 | protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) { |
||
377 | $lang = RequestContext::getMain()->getLanguage(); |
||
378 | $count = count( $tags ); |
||
379 | return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne, |
||
380 | $lang->commaList( $tags ), $count ); |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Is it OK to allow the user to apply all the specified tags at the same time |
||
385 | * as they edit/make the change? |
||
386 | * |
||
387 | * @param array $tags Tags that you are interested in applying |
||
388 | * @param User|null $user User whose permission you wish to check, or null if |
||
389 | * you don't care (e.g. maintenance scripts) |
||
390 | * @return Status |
||
391 | * @since 1.25 |
||
392 | */ |
||
393 | View Code Duplication | public static function canAddTagsAccompanyingChange( array $tags, |
|
394 | User $user = null ) { |
||
395 | |||
396 | if ( !is_null( $user ) ) { |
||
397 | if ( !$user->isAllowed( 'applychangetags' ) ) { |
||
398 | return Status::newFatal( 'tags-apply-no-permission' ); |
||
399 | } elseif ( $user->isBlocked() ) { |
||
400 | return Status::newFatal( 'tags-apply-blocked' ); |
||
401 | } |
||
402 | } |
||
403 | |||
404 | // to be applied, a tag has to be explicitly defined |
||
405 | // @todo Allow extensions to define tags that can be applied by users... |
||
406 | $allowedTags = self::listExplicitlyDefinedTags(); |
||
407 | $disallowedTags = array_diff( $tags, $allowedTags ); |
||
408 | if ( $disallowedTags ) { |
||
409 | return self::restrictedTagError( 'tags-apply-not-allowed-one', |
||
410 | 'tags-apply-not-allowed-multi', $disallowedTags ); |
||
411 | } |
||
412 | |||
413 | return Status::newGood(); |
||
414 | } |
||
415 | |||
416 | /** |
||
417 | * Adds tags to a given change, checking whether it is allowed first, but |
||
418 | * without adding a log entry. Useful for cases where the tag is being added |
||
419 | * along with the action that generated the change (e.g. tagging an edit as |
||
420 | * it is being made). |
||
421 | * |
||
422 | * Extensions should not use this function, unless directly handling a user |
||
423 | * request to add a particular tag. Normally, extensions should call |
||
424 | * ChangeTags::updateTags() instead. |
||
425 | * |
||
426 | * @param array $tags Tags to apply |
||
427 | * @param int|null $rc_id The rc_id of the change to add the tags to |
||
428 | * @param int|null $rev_id The rev_id of the change to add the tags to |
||
429 | * @param int|null $log_id The log_id of the change to add the tags to |
||
430 | * @param string $params Params to put in the ct_params field of table |
||
431 | * 'change_tag' when adding tags |
||
432 | * @param User $user Who to give credit for the action |
||
433 | * @return Status |
||
434 | * @since 1.25 |
||
435 | */ |
||
436 | public static function addTagsAccompanyingChangeWithChecks( |
||
437 | array $tags, $rc_id, $rev_id, $log_id, $params, User $user |
||
438 | ) { |
||
439 | |||
440 | // are we allowed to do this? |
||
441 | $result = self::canAddTagsAccompanyingChange( $tags, $user ); |
||
442 | if ( !$result->isOK() ) { |
||
443 | $result->value = null; |
||
444 | return $result; |
||
445 | } |
||
446 | |||
447 | // do it! |
||
448 | self::addTags( $tags, $rc_id, $rev_id, $log_id, $params ); |
||
449 | |||
450 | return Status::newGood( true ); |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * Is it OK to allow the user to adds and remove the given tags tags to/from a |
||
455 | * change? |
||
456 | * |
||
457 | * @param array $tagsToAdd Tags that you are interested in adding |
||
458 | * @param array $tagsToRemove Tags that you are interested in removing |
||
459 | * @param User|null $user User whose permission you wish to check, or null if |
||
460 | * you don't care (e.g. maintenance scripts) |
||
461 | * @return Status |
||
462 | * @since 1.25 |
||
463 | */ |
||
464 | public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove, |
||
465 | User $user = null ) { |
||
466 | |||
467 | if ( !is_null( $user ) ) { |
||
468 | if ( !$user->isAllowed( 'changetags' ) ) { |
||
469 | return Status::newFatal( 'tags-update-no-permission' ); |
||
470 | } elseif ( $user->isBlocked() ) { |
||
471 | return Status::newFatal( 'tags-update-blocked' ); |
||
472 | } |
||
473 | } |
||
474 | |||
475 | if ( $tagsToAdd ) { |
||
476 | // to be added, a tag has to be explicitly defined |
||
477 | // @todo Allow extensions to define tags that can be applied by users... |
||
478 | $explicitlyDefinedTags = self::listExplicitlyDefinedTags(); |
||
479 | $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags ); |
||
480 | if ( $diff ) { |
||
481 | return self::restrictedTagError( 'tags-update-add-not-allowed-one', |
||
482 | 'tags-update-add-not-allowed-multi', $diff ); |
||
483 | } |
||
484 | } |
||
485 | |||
486 | if ( $tagsToRemove ) { |
||
487 | // to be removed, a tag must not be defined by an extension, or equivalently it |
||
488 | // has to be either explicitly defined or not defined at all |
||
489 | // (assuming no edge case of a tag both explicitly-defined and extension-defined) |
||
490 | $softwareDefinedTags = self::listSoftwareDefinedTags(); |
||
491 | $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags ); |
||
492 | if ( $intersect ) { |
||
493 | return self::restrictedTagError( 'tags-update-remove-not-allowed-one', |
||
494 | 'tags-update-remove-not-allowed-multi', $intersect ); |
||
495 | } |
||
496 | } |
||
497 | |||
498 | return Status::newGood(); |
||
499 | } |
||
500 | |||
501 | /** |
||
502 | * Adds and/or removes tags to/from a given change, checking whether it is |
||
503 | * allowed first, and adding a log entry afterwards. |
||
504 | * |
||
505 | * Includes a call to ChangeTag::canUpdateTags(), so your code doesn't need |
||
506 | * to do that. However, it doesn't check whether the *_id parameters are a |
||
507 | * valid combination. That is up to you to enforce. See ApiTag::execute() for |
||
508 | * an example. |
||
509 | * |
||
510 | * @param array|null $tagsToAdd If none, pass array() or null |
||
511 | * @param array|null $tagsToRemove If none, pass array() or null |
||
512 | * @param int|null $rc_id The rc_id of the change to add the tags to |
||
513 | * @param int|null $rev_id The rev_id of the change to add the tags to |
||
514 | * @param int|null $log_id The log_id of the change to add the tags to |
||
515 | * @param string $params Params to put in the ct_params field of table |
||
516 | * 'change_tag' when adding tags |
||
517 | * @param string $reason Comment for the log |
||
518 | * @param User $user Who to give credit for the action |
||
519 | * @return Status If successful, the value of this Status object will be an |
||
520 | * object (stdClass) with the following fields: |
||
521 | * - logId: the ID of the added log entry, or null if no log entry was added |
||
522 | * (i.e. no operation was performed) |
||
523 | * - addedTags: an array containing the tags that were actually added |
||
524 | * - removedTags: an array containing the tags that were actually removed |
||
525 | * @since 1.25 |
||
526 | */ |
||
527 | public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove, |
||
528 | $rc_id, $rev_id, $log_id, $params, $reason, User $user ) { |
||
529 | |||
530 | if ( is_null( $tagsToAdd ) ) { |
||
531 | $tagsToAdd = []; |
||
532 | } |
||
533 | if ( is_null( $tagsToRemove ) ) { |
||
534 | $tagsToRemove = []; |
||
535 | } |
||
536 | View Code Duplication | if ( !$tagsToAdd && !$tagsToRemove ) { |
|
537 | // no-op, don't bother |
||
538 | return Status::newGood( (object)[ |
||
539 | 'logId' => null, |
||
540 | 'addedTags' => [], |
||
541 | 'removedTags' => [], |
||
542 | ] ); |
||
543 | } |
||
544 | |||
545 | // are we allowed to do this? |
||
546 | $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user ); |
||
547 | if ( !$result->isOK() ) { |
||
548 | $result->value = null; |
||
549 | return $result; |
||
550 | } |
||
551 | |||
552 | // basic rate limiting |
||
553 | if ( $user->pingLimiter( 'changetag' ) ) { |
||
554 | return Status::newFatal( 'actionthrottledtext' ); |
||
555 | } |
||
556 | |||
557 | // do it! |
||
558 | list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd, |
||
559 | $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user ); |
||
560 | View Code Duplication | if ( !$tagsAdded && !$tagsRemoved ) { |
|
561 | // no-op, don't log it |
||
562 | return Status::newGood( (object)[ |
||
563 | 'logId' => null, |
||
564 | 'addedTags' => [], |
||
565 | 'removedTags' => [], |
||
566 | ] ); |
||
567 | } |
||
568 | |||
569 | // log it |
||
570 | $logEntry = new ManualLogEntry( 'tag', 'update' ); |
||
571 | $logEntry->setPerformer( $user ); |
||
572 | $logEntry->setComment( $reason ); |
||
573 | |||
574 | // find the appropriate target page |
||
575 | if ( $rev_id ) { |
||
576 | $rev = Revision::newFromId( $rev_id ); |
||
577 | if ( $rev ) { |
||
578 | $logEntry->setTarget( $rev->getTitle() ); |
||
0 ignored issues
–
show
|
|||
579 | } |
||
580 | } elseif ( $log_id ) { |
||
581 | // This function is from revision deletion logic and has nothing to do with |
||
582 | // change tags, but it appears to be the only other place in core where we |
||
583 | // perform logged actions on log items. |
||
584 | $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) ); |
||
585 | } |
||
586 | |||
587 | if ( !$logEntry->getTarget() ) { |
||
588 | // target is required, so we have to set something |
||
589 | $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) ); |
||
590 | } |
||
591 | |||
592 | $logParams = [ |
||
593 | '4::revid' => $rev_id, |
||
594 | '5::logid' => $log_id, |
||
595 | '6:list:tagsAdded' => $tagsAdded, |
||
596 | '7:number:tagsAddedCount' => count( $tagsAdded ), |
||
597 | '8:list:tagsRemoved' => $tagsRemoved, |
||
598 | '9:number:tagsRemovedCount' => count( $tagsRemoved ), |
||
599 | 'initialTags' => $initialTags, |
||
600 | ]; |
||
601 | $logEntry->setParameters( $logParams ); |
||
602 | $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] ); |
||
603 | |||
604 | $dbw = wfGetDB( DB_MASTER ); |
||
605 | $logId = $logEntry->insert( $dbw ); |
||
606 | // Only send this to UDP, not RC, similar to patrol events |
||
607 | $logEntry->publish( $logId, 'udp' ); |
||
608 | |||
609 | return Status::newGood( (object)[ |
||
610 | 'logId' => $logId, |
||
611 | 'addedTags' => $tagsAdded, |
||
612 | 'removedTags' => $tagsRemoved, |
||
613 | ] ); |
||
614 | } |
||
615 | |||
616 | /** |
||
617 | * Applies all tags-related changes to a query. |
||
618 | * Handles selecting tags, and filtering. |
||
619 | * Needs $tables to be set up properly, so we can figure out which join conditions to use. |
||
620 | * |
||
621 | * @param string|array $tables Table names, see Database::select |
||
622 | * @param string|array $fields Fields used in query, see Database::select |
||
623 | * @param string|array $conds Conditions used in query, see Database::select |
||
624 | * @param array $join_conds Join conditions, see Database::select |
||
625 | * @param array $options Options, see Database::select |
||
626 | * @param bool|string $filter_tag Tag to select on |
||
627 | * |
||
628 | * @throws MWException When unable to determine appropriate JOIN condition for tagging |
||
629 | */ |
||
630 | public static function modifyDisplayQuery( &$tables, &$fields, &$conds, |
||
631 | &$join_conds, &$options, $filter_tag = false ) { |
||
632 | global $wgRequest, $wgUseTagFilter; |
||
633 | |||
634 | if ( $filter_tag === false ) { |
||
635 | $filter_tag = $wgRequest->getVal( 'tagfilter' ); |
||
636 | } |
||
637 | |||
638 | // Figure out which conditions can be done. |
||
639 | if ( in_array( 'recentchanges', $tables ) ) { |
||
640 | $join_cond = 'ct_rc_id=rc_id'; |
||
641 | } elseif ( in_array( 'logging', $tables ) ) { |
||
642 | $join_cond = 'ct_log_id=log_id'; |
||
643 | } elseif ( in_array( 'revision', $tables ) ) { |
||
644 | $join_cond = 'ct_rev_id=rev_id'; |
||
645 | } elseif ( in_array( 'archive', $tables ) ) { |
||
646 | $join_cond = 'ct_rev_id=ar_rev_id'; |
||
647 | } else { |
||
648 | throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' ); |
||
649 | } |
||
650 | |||
651 | $fields['ts_tags'] = wfGetDB( DB_REPLICA )->buildGroupConcatField( |
||
652 | ',', 'change_tag', 'ct_tag', $join_cond |
||
653 | ); |
||
654 | |||
655 | if ( $wgUseTagFilter && $filter_tag ) { |
||
656 | // Somebody wants to filter on a tag. |
||
657 | // Add an INNER JOIN on change_tag |
||
658 | |||
659 | $tables[] = 'change_tag'; |
||
660 | $join_conds['change_tag'] = [ 'INNER JOIN', $join_cond ]; |
||
661 | $conds['ct_tag'] = $filter_tag; |
||
662 | } |
||
663 | } |
||
664 | |||
665 | /** |
||
666 | * Build a text box to select a change tag |
||
667 | * |
||
668 | * @param string $selected Tag to select by default |
||
669 | * @param bool $ooui Use an OOUI TextInputWidget as selector instead of a non-OOUI input field |
||
670 | * You need to call OutputPage::enableOOUI() yourself. |
||
671 | * @return array an array of (label, selector) |
||
672 | */ |
||
673 | public static function buildTagFilterSelector( $selected = '', $ooui = false ) { |
||
674 | global $wgUseTagFilter; |
||
675 | |||
676 | if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) { |
||
677 | return []; |
||
678 | } |
||
679 | |||
680 | $data = [ |
||
681 | Html::rawElement( |
||
682 | 'label', |
||
683 | [ 'for' => 'tagfilter' ], |
||
684 | wfMessage( 'tag-filter' )->parse() |
||
685 | ) |
||
686 | ]; |
||
687 | |||
688 | if ( $ooui ) { |
||
689 | $data[] = new OOUI\TextInputWidget( [ |
||
690 | 'id' => 'tagfilter', |
||
691 | 'name' => 'tagfilter', |
||
692 | 'value' => $selected, |
||
693 | 'classes' => 'mw-tagfilter-input', |
||
694 | ] ); |
||
695 | } else { |
||
696 | $data[] = Xml::input( |
||
697 | 'tagfilter', |
||
698 | 20, |
||
699 | $selected, |
||
700 | [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ] |
||
701 | ); |
||
702 | } |
||
703 | |||
704 | return $data; |
||
705 | } |
||
706 | |||
707 | /** |
||
708 | * Defines a tag in the valid_tag table, without checking that the tag name |
||
709 | * is valid. |
||
710 | * Extensions should NOT use this function; they can use the ListDefinedTags |
||
711 | * hook instead. |
||
712 | * |
||
713 | * @param string $tag Tag to create |
||
714 | * @since 1.25 |
||
715 | */ |
||
716 | View Code Duplication | public static function defineTag( $tag ) { |
|
717 | $dbw = wfGetDB( DB_MASTER ); |
||
718 | $dbw->replace( 'valid_tag', |
||
719 | [ 'vt_tag' ], |
||
720 | [ 'vt_tag' => $tag ], |
||
721 | __METHOD__ ); |
||
722 | |||
723 | // clear the memcache of defined tags |
||
724 | self::purgeTagCacheAll(); |
||
725 | } |
||
726 | |||
727 | /** |
||
728 | * Removes a tag from the valid_tag table. The tag may remain in use by |
||
729 | * extensions, and may still show up as 'defined' if an extension is setting |
||
730 | * it from the ListDefinedTags hook. |
||
731 | * |
||
732 | * @param string $tag Tag to remove |
||
733 | * @since 1.25 |
||
734 | */ |
||
735 | View Code Duplication | public static function undefineTag( $tag ) { |
|
736 | $dbw = wfGetDB( DB_MASTER ); |
||
737 | $dbw->delete( 'valid_tag', [ 'vt_tag' => $tag ], __METHOD__ ); |
||
738 | |||
739 | // clear the memcache of defined tags |
||
740 | self::purgeTagCacheAll(); |
||
741 | } |
||
742 | |||
743 | /** |
||
744 | * Writes a tag action into the tag management log. |
||
745 | * |
||
746 | * @param string $action |
||
747 | * @param string $tag |
||
748 | * @param string $reason |
||
749 | * @param User $user Who to attribute the action to |
||
750 | * @param int $tagCount For deletion only, how many usages the tag had before |
||
751 | * it was deleted. |
||
752 | * @return int ID of the inserted log entry |
||
753 | * @since 1.25 |
||
754 | */ |
||
755 | protected static function logTagManagementAction( $action, $tag, $reason, |
||
756 | User $user, $tagCount = null ) { |
||
757 | |||
758 | $dbw = wfGetDB( DB_MASTER ); |
||
759 | |||
760 | $logEntry = new ManualLogEntry( 'managetags', $action ); |
||
761 | $logEntry->setPerformer( $user ); |
||
762 | // target page is not relevant, but it has to be set, so we just put in |
||
763 | // the title of Special:Tags |
||
764 | $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) ); |
||
0 ignored issues
–
show
It seems like
\Title::newFromText('Special:Tags') can be null ; however, setTarget() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
Loading history...
|
|||
765 | $logEntry->setComment( $reason ); |
||
766 | |||
767 | $params = [ '4::tag' => $tag ]; |
||
768 | if ( !is_null( $tagCount ) ) { |
||
769 | $params['5:number:count'] = $tagCount; |
||
770 | } |
||
771 | $logEntry->setParameters( $params ); |
||
772 | $logEntry->setRelations( [ 'Tag' => $tag ] ); |
||
773 | |||
774 | $logId = $logEntry->insert( $dbw ); |
||
775 | $logEntry->publish( $logId ); |
||
776 | return $logId; |
||
777 | } |
||
778 | |||
779 | /** |
||
780 | * Is it OK to allow the user to activate this tag? |
||
781 | * |
||
782 | * @param string $tag Tag that you are interested in activating |
||
783 | * @param User|null $user User whose permission you wish to check, or null if |
||
784 | * you don't care (e.g. maintenance scripts) |
||
785 | * @return Status |
||
786 | * @since 1.25 |
||
787 | */ |
||
788 | public static function canActivateTag( $tag, User $user = null ) { |
||
789 | if ( !is_null( $user ) ) { |
||
790 | if ( !$user->isAllowed( 'managechangetags' ) ) { |
||
791 | return Status::newFatal( 'tags-manage-no-permission' ); |
||
792 | } elseif ( $user->isBlocked() ) { |
||
793 | return Status::newFatal( 'tags-manage-blocked' ); |
||
794 | } |
||
795 | } |
||
796 | |||
797 | // defined tags cannot be activated (a defined tag is either extension- |
||
798 | // defined, in which case the extension chooses whether or not to active it; |
||
799 | // or user-defined, in which case it is considered active) |
||
800 | $definedTags = self::listDefinedTags(); |
||
801 | if ( in_array( $tag, $definedTags ) ) { |
||
802 | return Status::newFatal( 'tags-activate-not-allowed', $tag ); |
||
803 | } |
||
804 | |||
805 | // non-existing tags cannot be activated |
||
806 | $tagUsage = self::tagUsageStatistics(); |
||
807 | if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined |
||
808 | return Status::newFatal( 'tags-activate-not-found', $tag ); |
||
809 | } |
||
810 | |||
811 | return Status::newGood(); |
||
812 | } |
||
813 | |||
814 | /** |
||
815 | * Activates a tag, checking whether it is allowed first, and adding a log |
||
816 | * entry afterwards. |
||
817 | * |
||
818 | * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need |
||
819 | * to do that. |
||
820 | * |
||
821 | * @param string $tag |
||
822 | * @param string $reason |
||
823 | * @param User $user Who to give credit for the action |
||
824 | * @param bool $ignoreWarnings Can be used for API interaction, default false |
||
825 | * @return Status If successful, the Status contains the ID of the added log |
||
826 | * entry as its value |
||
827 | * @since 1.25 |
||
828 | */ |
||
829 | View Code Duplication | public static function activateTagWithChecks( $tag, $reason, User $user, |
|
830 | $ignoreWarnings = false ) { |
||
831 | |||
832 | // are we allowed to do this? |
||
833 | $result = self::canActivateTag( $tag, $user ); |
||
834 | if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { |
||
835 | $result->value = null; |
||
836 | return $result; |
||
837 | } |
||
838 | |||
839 | // do it! |
||
840 | self::defineTag( $tag ); |
||
841 | |||
842 | // log it |
||
843 | $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user ); |
||
844 | return Status::newGood( $logId ); |
||
845 | } |
||
846 | |||
847 | /** |
||
848 | * Is it OK to allow the user to deactivate this tag? |
||
849 | * |
||
850 | * @param string $tag Tag that you are interested in deactivating |
||
851 | * @param User|null $user User whose permission you wish to check, or null if |
||
852 | * you don't care (e.g. maintenance scripts) |
||
853 | * @return Status |
||
854 | * @since 1.25 |
||
855 | */ |
||
856 | View Code Duplication | public static function canDeactivateTag( $tag, User $user = null ) { |
|
857 | if ( !is_null( $user ) ) { |
||
858 | if ( !$user->isAllowed( 'managechangetags' ) ) { |
||
859 | return Status::newFatal( 'tags-manage-no-permission' ); |
||
860 | } elseif ( $user->isBlocked() ) { |
||
861 | return Status::newFatal( 'tags-manage-blocked' ); |
||
862 | } |
||
863 | } |
||
864 | |||
865 | // only explicitly-defined tags can be deactivated |
||
866 | $explicitlyDefinedTags = self::listExplicitlyDefinedTags(); |
||
867 | if ( !in_array( $tag, $explicitlyDefinedTags ) ) { |
||
868 | return Status::newFatal( 'tags-deactivate-not-allowed', $tag ); |
||
869 | } |
||
870 | return Status::newGood(); |
||
871 | } |
||
872 | |||
873 | /** |
||
874 | * Deactivates a tag, checking whether it is allowed first, and adding a log |
||
875 | * entry afterwards. |
||
876 | * |
||
877 | * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need |
||
878 | * to do that. |
||
879 | * |
||
880 | * @param string $tag |
||
881 | * @param string $reason |
||
882 | * @param User $user Who to give credit for the action |
||
883 | * @param bool $ignoreWarnings Can be used for API interaction, default false |
||
884 | * @return Status If successful, the Status contains the ID of the added log |
||
885 | * entry as its value |
||
886 | * @since 1.25 |
||
887 | */ |
||
888 | View Code Duplication | public static function deactivateTagWithChecks( $tag, $reason, User $user, |
|
889 | $ignoreWarnings = false ) { |
||
890 | |||
891 | // are we allowed to do this? |
||
892 | $result = self::canDeactivateTag( $tag, $user ); |
||
893 | if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { |
||
894 | $result->value = null; |
||
895 | return $result; |
||
896 | } |
||
897 | |||
898 | // do it! |
||
899 | self::undefineTag( $tag ); |
||
900 | |||
901 | // log it |
||
902 | $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user ); |
||
903 | return Status::newGood( $logId ); |
||
904 | } |
||
905 | |||
906 | /** |
||
907 | * Is it OK to allow the user to create this tag? |
||
908 | * |
||
909 | * @param string $tag Tag that you are interested in creating |
||
910 | * @param User|null $user User whose permission you wish to check, or null if |
||
911 | * you don't care (e.g. maintenance scripts) |
||
912 | * @return Status |
||
913 | * @since 1.25 |
||
914 | */ |
||
915 | public static function canCreateTag( $tag, User $user = null ) { |
||
916 | if ( !is_null( $user ) ) { |
||
917 | if ( !$user->isAllowed( 'managechangetags' ) ) { |
||
918 | return Status::newFatal( 'tags-manage-no-permission' ); |
||
919 | } elseif ( $user->isBlocked() ) { |
||
920 | return Status::newFatal( 'tags-manage-blocked' ); |
||
921 | } |
||
922 | } |
||
923 | |||
924 | // no empty tags |
||
925 | if ( $tag === '' ) { |
||
926 | return Status::newFatal( 'tags-create-no-name' ); |
||
927 | } |
||
928 | |||
929 | // tags cannot contain commas (used as a delimiter in tag_summary table) or |
||
930 | // slashes (would break tag description messages in MediaWiki namespace) |
||
931 | if ( strpos( $tag, ',' ) !== false || strpos( $tag, '/' ) !== false ) { |
||
932 | return Status::newFatal( 'tags-create-invalid-chars' ); |
||
933 | } |
||
934 | |||
935 | // could the MediaWiki namespace description messages be created? |
||
936 | $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" ); |
||
937 | if ( is_null( $title ) ) { |
||
938 | return Status::newFatal( 'tags-create-invalid-title-chars' ); |
||
939 | } |
||
940 | |||
941 | // does the tag already exist? |
||
942 | $tagUsage = self::tagUsageStatistics(); |
||
943 | View Code Duplication | if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) { |
|
944 | return Status::newFatal( 'tags-create-already-exists', $tag ); |
||
945 | } |
||
946 | |||
947 | // check with hooks |
||
948 | $canCreateResult = Status::newGood(); |
||
949 | Hooks::run( 'ChangeTagCanCreate', [ $tag, $user, &$canCreateResult ] ); |
||
950 | return $canCreateResult; |
||
951 | } |
||
952 | |||
953 | /** |
||
954 | * Creates a tag by adding a row to the `valid_tag` table. |
||
955 | * |
||
956 | * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to |
||
957 | * do that. |
||
958 | * |
||
959 | * @param string $tag |
||
960 | * @param string $reason |
||
961 | * @param User $user Who to give credit for the action |
||
962 | * @param bool $ignoreWarnings Can be used for API interaction, default false |
||
963 | * @return Status If successful, the Status contains the ID of the added log |
||
964 | * entry as its value |
||
965 | * @since 1.25 |
||
966 | */ |
||
967 | View Code Duplication | public static function createTagWithChecks( $tag, $reason, User $user, |
|
968 | $ignoreWarnings = false ) { |
||
969 | |||
970 | // are we allowed to do this? |
||
971 | $result = self::canCreateTag( $tag, $user ); |
||
972 | if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { |
||
973 | $result->value = null; |
||
974 | return $result; |
||
975 | } |
||
976 | |||
977 | // do it! |
||
978 | self::defineTag( $tag ); |
||
979 | |||
980 | // log it |
||
981 | $logId = self::logTagManagementAction( 'create', $tag, $reason, $user ); |
||
982 | return Status::newGood( $logId ); |
||
983 | } |
||
984 | |||
985 | /** |
||
986 | * Permanently removes all traces of a tag from the DB. Good for removing |
||
987 | * misspelt or temporary tags. |
||
988 | * |
||
989 | * This function should be directly called by maintenance scripts only, never |
||
990 | * by user-facing code. See deleteTagWithChecks() for functionality that can |
||
991 | * safely be exposed to users. |
||
992 | * |
||
993 | * @param string $tag Tag to remove |
||
994 | * @return Status The returned status will be good unless a hook changed it |
||
995 | * @since 1.25 |
||
996 | */ |
||
997 | public static function deleteTagEverywhere( $tag ) { |
||
998 | $dbw = wfGetDB( DB_MASTER ); |
||
999 | $dbw->startAtomic( __METHOD__ ); |
||
1000 | |||
1001 | // delete from valid_tag |
||
1002 | self::undefineTag( $tag ); |
||
1003 | |||
1004 | // find out which revisions use this tag, so we can delete from tag_summary |
||
1005 | $result = $dbw->select( 'change_tag', |
||
1006 | [ 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ], |
||
1007 | [ 'ct_tag' => $tag ], |
||
1008 | __METHOD__ ); |
||
1009 | foreach ( $result as $row ) { |
||
1010 | // remove the tag from the relevant row of tag_summary |
||
1011 | $tagsToAdd = []; |
||
1012 | $tagsToRemove = [ $tag ]; |
||
1013 | self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $row->ct_rc_id, |
||
1014 | $row->ct_rev_id, $row->ct_log_id ); |
||
1015 | } |
||
1016 | |||
1017 | // delete from change_tag |
||
1018 | $dbw->delete( 'change_tag', [ 'ct_tag' => $tag ], __METHOD__ ); |
||
1019 | |||
1020 | $dbw->endAtomic( __METHOD__ ); |
||
1021 | |||
1022 | // give extensions a chance |
||
1023 | $status = Status::newGood(); |
||
1024 | Hooks::run( 'ChangeTagAfterDelete', [ $tag, &$status ] ); |
||
1025 | // let's not allow error results, as the actual tag deletion succeeded |
||
1026 | if ( !$status->isOK() ) { |
||
1027 | wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' ); |
||
1028 | $status->setOK( true ); |
||
1029 | } |
||
1030 | |||
1031 | // clear the memcache of defined tags |
||
1032 | self::purgeTagCacheAll(); |
||
1033 | |||
1034 | return $status; |
||
1035 | } |
||
1036 | |||
1037 | /** |
||
1038 | * Is it OK to allow the user to delete this tag? |
||
1039 | * |
||
1040 | * @param string $tag Tag that you are interested in deleting |
||
1041 | * @param User|null $user User whose permission you wish to check, or null if |
||
1042 | * you don't care (e.g. maintenance scripts) |
||
1043 | * @return Status |
||
1044 | * @since 1.25 |
||
1045 | */ |
||
1046 | public static function canDeleteTag( $tag, User $user = null ) { |
||
1047 | $tagUsage = self::tagUsageStatistics(); |
||
1048 | |||
1049 | if ( !is_null( $user ) ) { |
||
1050 | if ( !$user->isAllowed( 'deletechangetags' ) ) { |
||
1051 | return Status::newFatal( 'tags-delete-no-permission' ); |
||
1052 | } elseif ( $user->isBlocked() ) { |
||
1053 | return Status::newFatal( 'tags-manage-blocked' ); |
||
1054 | } |
||
1055 | } |
||
1056 | |||
1057 | View Code Duplication | if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) { |
|
1058 | return Status::newFatal( 'tags-delete-not-found', $tag ); |
||
1059 | } |
||
1060 | |||
1061 | if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > self::MAX_DELETE_USES ) { |
||
1062 | return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES ); |
||
1063 | } |
||
1064 | |||
1065 | $softwareDefined = self::listSoftwareDefinedTags(); |
||
1066 | if ( in_array( $tag, $softwareDefined ) ) { |
||
1067 | // extension-defined tags can't be deleted unless the extension |
||
1068 | // specifically allows it |
||
1069 | $status = Status::newFatal( 'tags-delete-not-allowed' ); |
||
1070 | } else { |
||
1071 | // user-defined tags are deletable unless otherwise specified |
||
1072 | $status = Status::newGood(); |
||
1073 | } |
||
1074 | |||
1075 | Hooks::run( 'ChangeTagCanDelete', [ $tag, $user, &$status ] ); |
||
1076 | return $status; |
||
1077 | } |
||
1078 | |||
1079 | /** |
||
1080 | * Deletes a tag, checking whether it is allowed first, and adding a log entry |
||
1081 | * afterwards. |
||
1082 | * |
||
1083 | * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to |
||
1084 | * do that. |
||
1085 | * |
||
1086 | * @param string $tag |
||
1087 | * @param string $reason |
||
1088 | * @param User $user Who to give credit for the action |
||
1089 | * @param bool $ignoreWarnings Can be used for API interaction, default false |
||
1090 | * @return Status If successful, the Status contains the ID of the added log |
||
1091 | * entry as its value |
||
1092 | * @since 1.25 |
||
1093 | */ |
||
1094 | public static function deleteTagWithChecks( $tag, $reason, User $user, |
||
1095 | $ignoreWarnings = false ) { |
||
1096 | |||
1097 | // are we allowed to do this? |
||
1098 | $result = self::canDeleteTag( $tag, $user ); |
||
1099 | if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { |
||
1100 | $result->value = null; |
||
1101 | return $result; |
||
1102 | } |
||
1103 | |||
1104 | // store the tag usage statistics |
||
1105 | $tagUsage = self::tagUsageStatistics(); |
||
1106 | $hitcount = isset( $tagUsage[$tag] ) ? $tagUsage[$tag] : 0; |
||
1107 | |||
1108 | // do it! |
||
1109 | $deleteResult = self::deleteTagEverywhere( $tag ); |
||
1110 | if ( !$deleteResult->isOK() ) { |
||
1111 | return $deleteResult; |
||
1112 | } |
||
1113 | |||
1114 | // log it |
||
1115 | $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user, $hitcount ); |
||
1116 | $deleteResult->value = $logId; |
||
1117 | return $deleteResult; |
||
1118 | } |
||
1119 | |||
1120 | /** |
||
1121 | * Lists those tags which core or extensions report as being "active". |
||
1122 | * |
||
1123 | * @return array |
||
1124 | * @since 1.25 |
||
1125 | */ |
||
1126 | View Code Duplication | public static function listSoftwareActivatedTags() { |
|
1127 | // core active tags |
||
1128 | $tags = self::$coreTags; |
||
1129 | if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) { |
||
1130 | return $tags; |
||
1131 | } |
||
1132 | return ObjectCache::getMainWANInstance()->getWithSetCallback( |
||
1133 | wfMemcKey( 'active-tags' ), |
||
1134 | WANObjectCache::TTL_MINUTE * 5, |
||
1135 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) { |
||
1136 | $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); |
||
1137 | |||
1138 | // Ask extensions which tags they consider active |
||
1139 | Hooks::run( 'ChangeTagsListActive', [ &$tags ] ); |
||
1140 | return $tags; |
||
1141 | }, |
||
1142 | [ |
||
1143 | 'checkKeys' => [ wfMemcKey( 'active-tags' ) ], |
||
1144 | 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, |
||
1145 | 'pcTTL' => WANObjectCache::TTL_PROC_LONG |
||
1146 | ] |
||
1147 | ); |
||
1148 | } |
||
1149 | |||
1150 | /** |
||
1151 | * @see listSoftwareActivatedTags |
||
1152 | * @deprecated since 1.28 call listSoftwareActivatedTags directly |
||
1153 | * @return array |
||
1154 | */ |
||
1155 | public static function listExtensionActivatedTags() { |
||
1156 | wfDeprecated( __METHOD__, '1.28' ); |
||
1157 | return self::listSoftwareActivatedTags(); |
||
1158 | } |
||
1159 | |||
1160 | /** |
||
1161 | * Basically lists defined tags which count even if they aren't applied to anything. |
||
1162 | * It returns a union of the results of listExplicitlyDefinedTags() and |
||
1163 | * listExtensionDefinedTags(). |
||
1164 | * |
||
1165 | * @return string[] Array of strings: tags |
||
1166 | */ |
||
1167 | public static function listDefinedTags() { |
||
1168 | $tags1 = self::listExplicitlyDefinedTags(); |
||
1169 | $tags2 = self::listSoftwareDefinedTags(); |
||
1170 | return array_values( array_unique( array_merge( $tags1, $tags2 ) ) ); |
||
1171 | } |
||
1172 | |||
1173 | /** |
||
1174 | * Lists tags explicitly defined in the `valid_tag` table of the database. |
||
1175 | * Tags in table 'change_tag' which are not in table 'valid_tag' are not |
||
1176 | * included. |
||
1177 | * |
||
1178 | * Tries memcached first. |
||
1179 | * |
||
1180 | * @return string[] Array of strings: tags |
||
1181 | * @since 1.25 |
||
1182 | */ |
||
1183 | public static function listExplicitlyDefinedTags() { |
||
1184 | $fname = __METHOD__; |
||
1185 | |||
1186 | return ObjectCache::getMainWANInstance()->getWithSetCallback( |
||
1187 | wfMemcKey( 'valid-tags-db' ), |
||
1188 | WANObjectCache::TTL_MINUTE * 5, |
||
1189 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) { |
||
1190 | $dbr = wfGetDB( DB_REPLICA ); |
||
1191 | |||
1192 | $setOpts += Database::getCacheSetOptions( $dbr ); |
||
1193 | |||
1194 | $tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', [], $fname ); |
||
1195 | |||
1196 | return array_filter( array_unique( $tags ) ); |
||
1197 | }, |
||
1198 | [ |
||
1199 | 'checkKeys' => [ wfMemcKey( 'valid-tags-db' ) ], |
||
1200 | 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, |
||
1201 | 'pcTTL' => WANObjectCache::TTL_PROC_LONG |
||
1202 | ] |
||
1203 | ); |
||
1204 | } |
||
1205 | |||
1206 | /** |
||
1207 | * Lists tags defined by core or extensions using the ListDefinedTags hook. |
||
1208 | * Extensions need only define those tags they deem to be in active use. |
||
1209 | * |
||
1210 | * Tries memcached first. |
||
1211 | * |
||
1212 | * @return string[] Array of strings: tags |
||
1213 | * @since 1.25 |
||
1214 | */ |
||
1215 | View Code Duplication | public static function listSoftwareDefinedTags() { |
|
1216 | // core defined tags |
||
1217 | $tags = self::$coreTags; |
||
1218 | if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) { |
||
1219 | return $tags; |
||
1220 | } |
||
1221 | return ObjectCache::getMainWANInstance()->getWithSetCallback( |
||
1222 | wfMemcKey( 'valid-tags-hook' ), |
||
1223 | WANObjectCache::TTL_MINUTE * 5, |
||
1224 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) { |
||
1225 | $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); |
||
1226 | |||
1227 | Hooks::run( 'ListDefinedTags', [ &$tags ] ); |
||
1228 | return array_filter( array_unique( $tags ) ); |
||
1229 | }, |
||
1230 | [ |
||
1231 | 'checkKeys' => [ wfMemcKey( 'valid-tags-hook' ) ], |
||
1232 | 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, |
||
1233 | 'pcTTL' => WANObjectCache::TTL_PROC_LONG |
||
1234 | ] |
||
1235 | ); |
||
1236 | } |
||
1237 | |||
1238 | /** |
||
1239 | * Call listSoftwareDefinedTags directly |
||
1240 | * |
||
1241 | * @see listSoftwareDefinedTags |
||
1242 | * @deprecated since 1.28 |
||
1243 | */ |
||
1244 | public static function listExtensionDefinedTags() { |
||
1245 | wfDeprecated( __METHOD__, '1.28' ); |
||
1246 | return self::listSoftwareDefinedTags(); |
||
1247 | } |
||
1248 | |||
1249 | /** |
||
1250 | * Invalidates the short-term cache of defined tags used by the |
||
1251 | * list*DefinedTags functions, as well as the tag statistics cache. |
||
1252 | * @since 1.25 |
||
1253 | */ |
||
1254 | public static function purgeTagCacheAll() { |
||
1255 | $cache = ObjectCache::getMainWANInstance(); |
||
1256 | |||
1257 | $cache->touchCheckKey( wfMemcKey( 'active-tags' ) ); |
||
1258 | $cache->touchCheckKey( wfMemcKey( 'valid-tags-db' ) ); |
||
1259 | $cache->touchCheckKey( wfMemcKey( 'valid-tags-hook' ) ); |
||
1260 | |||
1261 | self::purgeTagUsageCache(); |
||
1262 | } |
||
1263 | |||
1264 | /** |
||
1265 | * Invalidates the tag statistics cache only. |
||
1266 | * @since 1.25 |
||
1267 | */ |
||
1268 | public static function purgeTagUsageCache() { |
||
1269 | $cache = ObjectCache::getMainWANInstance(); |
||
1270 | |||
1271 | $cache->touchCheckKey( wfMemcKey( 'change-tag-statistics' ) ); |
||
1272 | } |
||
1273 | |||
1274 | /** |
||
1275 | * Returns a map of any tags used on the wiki to number of edits |
||
1276 | * tagged with them, ordered descending by the hitcount. |
||
1277 | * This does not include tags defined somewhere that have never been applied. |
||
1278 | * |
||
1279 | * Keeps a short-term cache in memory, so calling this multiple times in the |
||
1280 | * same request should be fine. |
||
1281 | * |
||
1282 | * @return array Array of string => int |
||
1283 | */ |
||
1284 | public static function tagUsageStatistics() { |
||
1285 | $fname = __METHOD__; |
||
1286 | return ObjectCache::getMainWANInstance()->getWithSetCallback( |
||
1287 | wfMemcKey( 'change-tag-statistics' ), |
||
1288 | WANObjectCache::TTL_MINUTE * 5, |
||
1289 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) { |
||
1290 | $dbr = wfGetDB( DB_REPLICA, 'vslow' ); |
||
1291 | |||
1292 | $setOpts += Database::getCacheSetOptions( $dbr ); |
||
1293 | |||
1294 | $res = $dbr->select( |
||
1295 | 'change_tag', |
||
1296 | [ 'ct_tag', 'hitcount' => 'count(*)' ], |
||
1297 | [], |
||
1298 | $fname, |
||
1299 | [ 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ] |
||
1300 | ); |
||
1301 | |||
1302 | $out = []; |
||
1303 | foreach ( $res as $row ) { |
||
1304 | $out[$row->ct_tag] = $row->hitcount; |
||
1305 | } |
||
1306 | |||
1307 | return $out; |
||
1308 | }, |
||
1309 | [ |
||
1310 | 'checkKeys' => [ wfMemcKey( 'change-tag-statistics' ) ], |
||
1311 | 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, |
||
1312 | 'pcTTL' => WANObjectCache::TTL_PROC_LONG |
||
1313 | ] |
||
1314 | ); |
||
1315 | } |
||
1316 | |||
1317 | /** |
||
1318 | * Indicate whether change tag editing UI is relevant |
||
1319 | * |
||
1320 | * Returns true if the user has the necessary right and there are any |
||
1321 | * editable tags defined. |
||
1322 | * |
||
1323 | * This intentionally doesn't check "any addable || any deletable", because |
||
1324 | * it seems like it would be more confusing than useful if the checkboxes |
||
1325 | * suddenly showed up because some abuse filter stopped defining a tag and |
||
1326 | * then suddenly disappeared when someone deleted all uses of that tag. |
||
1327 | * |
||
1328 | * @param User $user |
||
1329 | * @return bool |
||
1330 | */ |
||
1331 | public static function showTagEditingUI( User $user ) { |
||
1332 | return $user->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags(); |
||
1333 | } |
||
1334 | } |
||
1335 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: