Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
19.01% |
23 / 121 |
|
13.64% |
3 / 22 |
CRAP | |
0.00% |
0 / 1 |
ChangesListFilter | |
19.01% |
23 / 121 |
|
13.64% |
3 / 22 |
1072.55 | |
0.00% |
0 / 1 |
__construct | |
71.43% |
15 / 21 |
|
0.00% |
0 / 1 |
6.84 | |||
conflictsWith | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
setUnidirectionalConflict | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
setAsSupersetOf | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGroup | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLabel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
displaysOnUnstructuredUi | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
displaysOnStructuredUi | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFeatureAvailableOnStructuredUi | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPriority | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCssClass | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
applyCssClassIfNeeded | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getJsData | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
6 | |||
isSelected | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getConflictingGroups | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConflictingFilters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
activelyInConflictWithGroup | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
hasConflictWithGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
activelyInConflictWithFilter | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
hasConflictWithFilter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSiblings | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
setDefaultHighlightColor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Represents a filter (used on ChangesListSpecialPage and descendants) |
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 | * @author Matthew Flaschen |
22 | */ |
23 | |
24 | use MediaWiki\Context\IContextSource; |
25 | use MediaWiki\Html\FormOptions; |
26 | |
27 | /** |
28 | * Represents a filter (used on ChangesListSpecialPage and descendants) |
29 | * |
30 | * @since 1.29 |
31 | */ |
32 | abstract class ChangesListFilter { |
33 | /** |
34 | * Filter name |
35 | * |
36 | * @var string |
37 | */ |
38 | protected $name; |
39 | |
40 | /** |
41 | * CSS class suffix used for attribution, e.g. 'bot'. |
42 | * |
43 | * In this example, if bot actions are included in the result set, this CSS class |
44 | * will then be included in all bot-flagged actions. |
45 | * |
46 | * @var string|null |
47 | */ |
48 | protected $cssClassSuffix; |
49 | |
50 | /** |
51 | * Callable that returns true if and only if a row is attributed to this filter |
52 | * |
53 | * @var callable |
54 | */ |
55 | protected $isRowApplicableCallable; |
56 | |
57 | /** |
58 | * Group. ChangesListFilterGroup this belongs to |
59 | * |
60 | * @var ChangesListFilterGroup |
61 | */ |
62 | protected $group; |
63 | |
64 | /** |
65 | * i18n key of label for structured UI |
66 | * |
67 | * @var string |
68 | */ |
69 | protected $label; |
70 | |
71 | /** |
72 | * i18n key of description for structured UI |
73 | * |
74 | * @var string |
75 | */ |
76 | protected $description; |
77 | |
78 | /** |
79 | * Array of associative arrays with conflict information. See |
80 | * setUnidirectionalConflict |
81 | * |
82 | * @var array |
83 | */ |
84 | protected $conflictingGroups = []; |
85 | |
86 | /** |
87 | * Array of associative arrays with conflict information. See |
88 | * setUnidirectionalConflict |
89 | * |
90 | * @var array |
91 | */ |
92 | protected $conflictingFilters = []; |
93 | |
94 | /** |
95 | * Array of associative arrays with subset information |
96 | * |
97 | * @var array |
98 | */ |
99 | protected $subsetFilters = []; |
100 | |
101 | /** |
102 | * Priority integer. Higher value means higher up in the group's filter list. |
103 | * |
104 | * @var int |
105 | */ |
106 | protected $priority; |
107 | |
108 | /** |
109 | * @var string |
110 | */ |
111 | protected $defaultHighlightColor; |
112 | |
113 | private const RESERVED_NAME_CHAR = '_'; |
114 | |
115 | /** |
116 | * Creates a new filter with the specified configuration, and registers it to the |
117 | * specified group. |
118 | * |
119 | * It infers which UI (it can be either or both) to display the filter on based on |
120 | * which messages are provided. |
121 | * |
122 | * If 'label' is provided, it will be displayed on the structured UI. Thus, |
123 | * 'label', 'description', and sub-class parameters are optional depending on which |
124 | * UI it's for. |
125 | * |
126 | * @param array $filterDefinition ChangesListFilter definition |
127 | * * $filterDefinition['name'] string Name of filter; use lowercase with no |
128 | * punctuation |
129 | * * $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark |
130 | * that a particular row belongs to this filter (when a row is included by the |
131 | * filter) (optional) |
132 | * * $filterDefinition['isRowApplicableCallable'] callable Callable taking two parameters, the |
133 | * IContextSource, and the RecentChange object for the row, and returning true if |
134 | * the row is attributed to this filter. The above CSS class will then be |
135 | * automatically added (optional, required if cssClassSuffix is used). |
136 | * * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this |
137 | * belongs to. |
138 | * * $filterDefinition['label'] string i18n key of label for structured UI. |
139 | * * $filterDefinition['description'] string i18n key of description for structured |
140 | * UI. |
141 | * * $filterDefinition['priority'] int Priority integer. Higher value means higher |
142 | * up in the group's filter list. |
143 | * @phpcs:ignore Generic.Files.LineLength |
144 | * @phan-param array{name:string,cssClassSuffix?:string,isRowApplicableCallable?:callable,group:ChangesListFilterGroup,label:string,description:string,priority:int} $filterDefinition |
145 | */ |
146 | public function __construct( array $filterDefinition ) { |
147 | if ( isset( $filterDefinition['group'] ) ) { |
148 | $this->group = $filterDefinition['group']; |
149 | } else { |
150 | throw new InvalidArgumentException( 'You must use \'group\' to specify the ' . |
151 | 'ChangesListFilterGroup this filter belongs to' ); |
152 | } |
153 | |
154 | if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) { |
155 | throw new InvalidArgumentException( 'Filter names may not contain \'' . |
156 | self::RESERVED_NAME_CHAR . |
157 | '\'. Use the naming convention: \'lowercase\'' |
158 | ); |
159 | } |
160 | |
161 | if ( $this->group->getFilter( $filterDefinition['name'] ) ) { |
162 | throw new InvalidArgumentException( 'Two filters in a group cannot have the ' . |
163 | "same name: '{$filterDefinition['name']}'" ); |
164 | } |
165 | |
166 | $this->name = $filterDefinition['name']; |
167 | |
168 | if ( isset( $filterDefinition['cssClassSuffix'] ) ) { |
169 | $this->cssClassSuffix = $filterDefinition['cssClassSuffix']; |
170 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Documented as required |
171 | $this->isRowApplicableCallable = $filterDefinition['isRowApplicableCallable']; |
172 | } |
173 | |
174 | if ( isset( $filterDefinition['label'] ) ) { |
175 | $this->label = $filterDefinition['label']; |
176 | $this->description = $filterDefinition['description']; |
177 | } |
178 | |
179 | $this->priority = $filterDefinition['priority']; |
180 | |
181 | $this->group->registerFilter( $this ); |
182 | } |
183 | |
184 | /** |
185 | * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object. |
186 | * |
187 | * WARNING: This means there is a conflict when both things are *shown* |
188 | * (not filtered out), even for the hide-based filters. So e.g. conflicting with |
189 | * 'hideanons' means there is a conflict if only anonymous users are *shown*. |
190 | * |
191 | * @param ChangesListFilterGroup|ChangesListFilter $other |
192 | * @param string $globalKey i18n key for top-level conflict message |
193 | * @param string $forwardKey i18n key for conflict message in this |
194 | * direction (when in UI context of $this object) |
195 | * @param string $backwardKey i18n key for conflict message in reverse |
196 | * direction (when in UI context of $other object) |
197 | */ |
198 | public function conflictsWith( $other, string $globalKey, string $forwardKey, string $backwardKey ) { |
199 | $this->setUnidirectionalConflict( |
200 | $other, |
201 | $globalKey, |
202 | $forwardKey |
203 | ); |
204 | |
205 | $other->setUnidirectionalConflict( |
206 | $this, |
207 | $globalKey, |
208 | $backwardKey |
209 | ); |
210 | } |
211 | |
212 | /** |
213 | * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with |
214 | * this object. |
215 | * |
216 | * Internal use ONLY. |
217 | * |
218 | * @param ChangesListFilterGroup|ChangesListFilter $other |
219 | * @param string $globalDescription i18n key for top-level conflict message |
220 | * @param string $contextDescription i18n key for conflict message in this |
221 | * direction (when in UI context of $this object) |
222 | */ |
223 | public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) { |
224 | if ( $other instanceof ChangesListFilterGroup ) { |
225 | $this->conflictingGroups[] = [ |
226 | 'group' => $other->getName(), |
227 | 'groupObject' => $other, |
228 | 'globalDescription' => $globalDescription, |
229 | 'contextDescription' => $contextDescription, |
230 | ]; |
231 | } elseif ( $other instanceof ChangesListFilter ) { |
232 | $this->conflictingFilters[] = [ |
233 | 'group' => $other->getGroup()->getName(), |
234 | 'filter' => $other->getName(), |
235 | 'filterObject' => $other, |
236 | 'globalDescription' => $globalDescription, |
237 | 'contextDescription' => $contextDescription, |
238 | ]; |
239 | } else { |
240 | throw new InvalidArgumentException( |
241 | 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' |
242 | ); |
243 | } |
244 | } |
245 | |
246 | /** |
247 | * Marks that the current instance is (also) a superset of the filter passed in. |
248 | * This can be called more than once. |
249 | * |
250 | * This means that anything in the results for the other filter is also in the |
251 | * results for this one. |
252 | * |
253 | * @param ChangesListFilter $other The filter the current instance is a superset of |
254 | */ |
255 | public function setAsSupersetOf( ChangesListFilter $other ) { |
256 | if ( $other->getGroup() !== $this->getGroup() ) { |
257 | throw new InvalidArgumentException( 'Supersets can only be defined for filters in the same group' ); |
258 | } |
259 | |
260 | $this->subsetFilters[] = [ |
261 | // It's always the same group, but this makes the representation |
262 | // more consistent with conflicts. |
263 | 'group' => $other->getGroup()->getName(), |
264 | 'filter' => $other->getName(), |
265 | ]; |
266 | } |
267 | |
268 | /** |
269 | * @return string Name, e.g. hideanons |
270 | */ |
271 | public function getName() { |
272 | return $this->name; |
273 | } |
274 | |
275 | /** |
276 | * @return ChangesListFilterGroup Group this belongs to |
277 | */ |
278 | public function getGroup() { |
279 | return $this->group; |
280 | } |
281 | |
282 | /** |
283 | * @return string i18n key of label for structured UI |
284 | */ |
285 | public function getLabel() { |
286 | return $this->label; |
287 | } |
288 | |
289 | /** |
290 | * @return string i18n key of description for structured UI |
291 | */ |
292 | public function getDescription() { |
293 | return $this->description; |
294 | } |
295 | |
296 | /** |
297 | * Checks whether the filter should display on the unstructured UI |
298 | * |
299 | * @return bool Whether to display |
300 | */ |
301 | abstract public function displaysOnUnstructuredUi(); |
302 | |
303 | /** |
304 | * Checks whether the filter should display on the structured UI |
305 | * This refers to the exact filter. See also isFeatureAvailableOnStructuredUi. |
306 | * |
307 | * @return bool Whether to display |
308 | */ |
309 | public function displaysOnStructuredUi() { |
310 | return $this->label !== null; |
311 | } |
312 | |
313 | /** |
314 | * Checks whether an equivalent feature for this filter is available on the |
315 | * structured UI. |
316 | * |
317 | * This can either be the exact filter, or a new filter that replaces it. |
318 | * @return bool |
319 | */ |
320 | public function isFeatureAvailableOnStructuredUi() { |
321 | return $this->displaysOnStructuredUi(); |
322 | } |
323 | |
324 | /** |
325 | * @return int Priority. Higher value means higher up in the group list |
326 | */ |
327 | public function getPriority() { |
328 | return $this->priority; |
329 | } |
330 | |
331 | /** |
332 | * Gets the CSS class |
333 | * |
334 | * @return string|null CSS class, or null if not defined |
335 | */ |
336 | protected function getCssClass() { |
337 | if ( $this->cssClassSuffix !== null ) { |
338 | return ChangesList::CSS_CLASS_PREFIX . $this->cssClassSuffix; |
339 | } else { |
340 | return null; |
341 | } |
342 | } |
343 | |
344 | /** |
345 | * Add CSS class if needed |
346 | * |
347 | * @param IContextSource $ctx Context source |
348 | * @param RecentChange $rc Recent changes object |
349 | * @param array &$classes Non-associative array of CSS class names; appended to if needed |
350 | */ |
351 | public function applyCssClassIfNeeded( IContextSource $ctx, RecentChange $rc, array &$classes ) { |
352 | if ( $this->isRowApplicableCallable === null ) { |
353 | return; |
354 | } |
355 | |
356 | if ( call_user_func( $this->isRowApplicableCallable, $ctx, $rc ) ) { |
357 | $classes[] = $this->getCssClass(); |
358 | } |
359 | } |
360 | |
361 | /** |
362 | * Gets the JS data required by the front-end of the structured UI |
363 | * |
364 | * @return array Associative array Data required by the front-end. messageKeys is |
365 | * a special top-level value, with the value being an array of the message keys to |
366 | * send to the client. |
367 | */ |
368 | public function getJsData() { |
369 | $output = [ |
370 | 'name' => $this->getName(), |
371 | 'label' => $this->getLabel(), |
372 | 'description' => $this->getDescription(), |
373 | 'cssClass' => $this->getCssClass(), |
374 | 'priority' => $this->priority, |
375 | 'subset' => $this->subsetFilters, |
376 | 'conflicts' => [], |
377 | 'defaultHighlightColor' => $this->defaultHighlightColor |
378 | ]; |
379 | |
380 | $output['messageKeys'] = [ |
381 | $this->getLabel(), |
382 | $this->getDescription(), |
383 | ]; |
384 | |
385 | $conflicts = array_merge( |
386 | $this->conflictingGroups, |
387 | $this->conflictingFilters |
388 | ); |
389 | |
390 | foreach ( $conflicts as $conflictInfo ) { |
391 | unset( $conflictInfo['filterObject'] ); |
392 | unset( $conflictInfo['groupObject'] ); |
393 | $output['conflicts'][] = $conflictInfo; |
394 | array_push( |
395 | $output['messageKeys'], |
396 | $conflictInfo['globalDescription'], |
397 | $conflictInfo['contextDescription'] |
398 | ); |
399 | } |
400 | |
401 | return $output; |
402 | } |
403 | |
404 | /** |
405 | * Checks whether this filter is selected in the provided options |
406 | * |
407 | * @param FormOptions $opts |
408 | * @return bool |
409 | */ |
410 | abstract public function isSelected( FormOptions $opts ); |
411 | |
412 | /** |
413 | * Get groups conflicting with this filter |
414 | * |
415 | * @return ChangesListFilterGroup[] |
416 | */ |
417 | public function getConflictingGroups() { |
418 | return array_column( $this->conflictingGroups, 'groupObject' ); |
419 | } |
420 | |
421 | /** |
422 | * Get filters conflicting with this filter |
423 | * |
424 | * @return ChangesListFilter[] |
425 | */ |
426 | public function getConflictingFilters() { |
427 | return array_column( $this->conflictingFilters, 'filterObject' ); |
428 | } |
429 | |
430 | /** |
431 | * Check if the conflict with a group is currently "active" |
432 | * |
433 | * @param ChangesListFilterGroup $group |
434 | * @param FormOptions $opts |
435 | * @return bool |
436 | */ |
437 | public function activelyInConflictWithGroup( ChangesListFilterGroup $group, FormOptions $opts ) { |
438 | if ( $group->anySelected( $opts ) && $this->isSelected( $opts ) ) { |
439 | /** @var ChangesListFilter $siblingFilter */ |
440 | foreach ( $this->getSiblings() as $siblingFilter ) { |
441 | if ( $siblingFilter->isSelected( $opts ) && !$siblingFilter->hasConflictWithGroup( $group ) ) { |
442 | return false; |
443 | } |
444 | } |
445 | return true; |
446 | } |
447 | return false; |
448 | } |
449 | |
450 | private function hasConflictWithGroup( ChangesListFilterGroup $group ) { |
451 | return in_array( $group, $this->getConflictingGroups() ); |
452 | } |
453 | |
454 | /** |
455 | * Check if the conflict with a filter is currently "active" |
456 | * |
457 | * @param ChangesListFilter $filter |
458 | * @param FormOptions $opts |
459 | * @return bool |
460 | */ |
461 | public function activelyInConflictWithFilter( ChangesListFilter $filter, FormOptions $opts ) { |
462 | if ( $this->isSelected( $opts ) && $filter->isSelected( $opts ) ) { |
463 | /** @var ChangesListFilter $siblingFilter */ |
464 | foreach ( $this->getSiblings() as $siblingFilter ) { |
465 | if ( |
466 | $siblingFilter->isSelected( $opts ) && |
467 | !$siblingFilter->hasConflictWithFilter( $filter ) |
468 | ) { |
469 | return false; |
470 | } |
471 | } |
472 | return true; |
473 | } |
474 | return false; |
475 | } |
476 | |
477 | private function hasConflictWithFilter( ChangesListFilter $filter ) { |
478 | return in_array( $filter, $this->getConflictingFilters() ); |
479 | } |
480 | |
481 | /** |
482 | * Get filters in the same group |
483 | * |
484 | * @return ChangesListFilter[] |
485 | */ |
486 | protected function getSiblings() { |
487 | return array_filter( |
488 | $this->getGroup()->getFilters(), |
489 | function ( $filter ) { |
490 | return $filter !== $this; |
491 | } |
492 | ); |
493 | } |
494 | |
495 | /** |
496 | * @param string $defaultHighlightColor |
497 | */ |
498 | public function setDefaultHighlightColor( $defaultHighlightColor ) { |
499 | $this->defaultHighlightColor = $defaultHighlightColor; |
500 | } |
501 | } |