Remove perf tracking code that was moved to WikimediaEvents in Ib300af5c
[lhc/web/wiklou.git] / includes / changes / ChangesListFilterGroup.php
1 <?php
2 /**
3 * Represents a filter group (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 * @license GPL 2+
22 * @author Matthew Flaschen
23 */
24
25 // TODO: Might want to make a super-class or trait to share behavior (especially re
26 // conflicts) between ChangesListFilter and ChangesListFilterGroup.
27 // What to call it. FilterStructure? That would also let me make
28 // setUnidirectionalConflict protected.
29
30 /**
31 * Represents a filter group (used on ChangesListSpecialPage and descendants)
32 *
33 * @since 1.29
34 */
35 abstract class ChangesListFilterGroup {
36 /**
37 * Name (internal identifier)
38 *
39 * @var string $name
40 */
41 protected $name;
42
43 /**
44 * i18n key for title
45 *
46 * @var string $title
47 */
48 protected $title;
49
50 /**
51 * i18n key for header of What's This?
52 *
53 * @var string|null $whatsThisHeader
54 */
55 protected $whatsThisHeader;
56
57 /**
58 * i18n key for body of What's This?
59 *
60 * @var string|null $whatsThisBody
61 */
62 protected $whatsThisBody;
63
64 /**
65 * URL of What's This? link
66 *
67 * @var string|null $whatsThisUrl
68 */
69 protected $whatsThisUrl;
70
71 /**
72 * i18n key for What's This? link
73 *
74 * @var string|null $whatsThisLinkText
75 */
76 protected $whatsThisLinkText;
77
78 /**
79 * Type, from a TYPE constant of a subclass
80 *
81 * @var string $type
82 */
83 protected $type;
84
85 /**
86 * Priority integer. Higher values means higher up in the
87 * group list.
88 *
89 * @var string $priority
90 */
91 protected $priority;
92
93 /**
94 * Associative array of filters, as ChangesListFilter objects, with filter name as key
95 *
96 * @var array $filters
97 */
98 protected $filters;
99
100 /**
101 * Whether this group is full coverage. This means that checking every item in the
102 * group means no changes list (e.g. RecentChanges) entries are filtered out.
103 *
104 * @var bool $isFullCoverage
105 */
106 protected $isFullCoverage;
107
108 /**
109 * Array of associative arrays with conflict information. See
110 * setUnidirectionalConflict
111 *
112 * @var array $conflictingGroups
113 */
114 protected $conflictingGroups = [];
115
116 /**
117 * Array of associative arrays with conflict information. See
118 * setUnidirectionalConflict
119 *
120 * @var array $conflictingFilters
121 */
122 protected $conflictingFilters = [];
123
124 const DEFAULT_PRIORITY = -100;
125
126 const RESERVED_NAME_CHAR = '_';
127
128 /**
129 * Create a new filter group with the specified configuration
130 *
131 * @param array $groupDefinition Configuration of group
132 * * $groupDefinition['name'] string Group name; use camelCase with no punctuation
133 * * $groupDefinition['title'] string i18n key for title (optional, can be omitted
134 * only if none of the filters in the group display in the structured UI)
135 * * $groupDefinition['type'] string A type constant from a subclass of this one
136 * * $groupDefinition['priority'] int Priority integer. Higher value means higher
137 * up in the group list (optional, defaults to -100).
138 * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which
139 * is an associative array to be passed to the filter constructor. However,
140 * 'priority' is optional for the filters. Any filter that has priority unset
141 * will be put to the bottom, in the order given.
142 * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage;
143 * if true, this means that checking every item in the group means no
144 * changes list entries are filtered out.
145 * * $groupDefinition['whatsThisHeader'] string i18n key for header of "What's
146 * This" popup (optional).
147 * * $groupDefinition['whatsThisBody'] string i18n key for body of "What's This"
148 * popup (optional).
149 * * $groupDefinition['whatsThisUrl'] string URL for main link of "What's This"
150 * popup (optional).
151 * * $groupDefinition['whatsThisLinkText'] string i18n key of text for main link of
152 * "What's This" popup (optional).
153 */
154 public function __construct( array $groupDefinition ) {
155 if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
156 throw new MWException( 'Group names may not contain \'' .
157 self::RESERVED_NAME_CHAR .
158 '\'. Use the naming convention: \'camelCase\''
159 );
160 }
161
162 $this->name = $groupDefinition['name'];
163
164 if ( isset( $groupDefinition['title'] ) ) {
165 $this->title = $groupDefinition['title'];
166 }
167
168 if ( isset( $groupDefinition['whatsThisHeader'] ) ) {
169 $this->whatsThisHeader = $groupDefinition['whatsThisHeader'];
170 $this->whatsThisBody = $groupDefinition['whatsThisBody'];
171 $this->whatsThisUrl = $groupDefinition['whatsThisUrl'];
172 $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText'];
173 }
174
175 $this->type = $groupDefinition['type'];
176 if ( isset( $groupDefinition['priority'] ) ) {
177 $this->priority = $groupDefinition['priority'];
178 } else {
179 $this->priority = self::DEFAULT_PRIORITY;
180 }
181
182 $this->isFullCoverage = $groupDefinition['isFullCoverage'];
183
184 $this->filters = [];
185 $lowestSpecifiedPriority = -1;
186 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
187 if ( isset( $filterDefinition['priority'] ) ) {
188 $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
189 }
190 }
191
192 // Convenience feature: If you specify a group (and its filters) all in
193 // one place, you don't have to specify priority. You can just put them
194 // in order. However, if you later add one (e.g. an extension adds a filter
195 // to a core-defined group), you need to specify it.
196 $autoFillPriority = $lowestSpecifiedPriority - 1;
197 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
198 if ( !isset( $filterDefinition['priority'] ) ) {
199 $filterDefinition['priority'] = $autoFillPriority;
200 $autoFillPriority--;
201 }
202 $filterDefinition['group'] = $this;
203
204 $filter = $this->createFilter( $filterDefinition );
205 $this->registerFilter( $filter );
206 }
207 }
208
209 /**
210 * Creates a filter of the appropriate type for this group, from the definition
211 *
212 * @param array $filterDefinition Filter definition
213 * @return ChangesListFilter Filter
214 */
215 abstract protected function createFilter( array $filterDefinition );
216
217 /**
218 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
219 *
220 * WARNING: This means there is a conflict when both things are *shown*
221 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
222 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
223 *
224 * @param ChangesListFilterGroup|ChangesListFilter $other Other
225 * ChangesListFilterGroup or ChangesListFilter
226 * @param string $globalKey i18n key for top-level conflict message
227 * @param string $forwardKey i18n key for conflict message in this
228 * direction (when in UI context of $this object)
229 * @param string $backwardKey i18n key for conflict message in reverse
230 * direction (when in UI context of $other object)
231 */
232 public function conflictsWith( $other, $globalKey, $forwardKey, $backwardKey ) {
233 if ( $globalKey === null || $forwardKey === null || $backwardKey === null ) {
234 throw new MWException( 'All messages must be specified' );
235 }
236
237 $this->setUnidirectionalConflict(
238 $other,
239 $globalKey,
240 $forwardKey
241 );
242
243 $other->setUnidirectionalConflict(
244 $this,
245 $globalKey,
246 $backwardKey
247 );
248 }
249
250 /**
251 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
252 * this object.
253 *
254 * Internal use ONLY.
255 *
256 * @param ChangesListFilterGroup|ChangesListFilter $other Other
257 * ChangesListFilterGroup or ChangesListFilter
258 * @param string $globalDescription i18n key for top-level conflict message
259 * @param string $contextDescription i18n key for conflict message in this
260 * direction (when in UI context of $this object)
261 */
262 public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
263 if ( $other instanceof ChangesListFilterGroup ) {
264 $this->conflictingGroups[] = [
265 'group' => $other->getName(),
266 'groupObject' => $other,
267 'globalDescription' => $globalDescription,
268 'contextDescription' => $contextDescription,
269 ];
270 } elseif ( $other instanceof ChangesListFilter ) {
271 $this->conflictingFilters[] = [
272 'group' => $other->getGroup()->getName(),
273 'filter' => $other->getName(),
274 'filterObject' => $other,
275 'globalDescription' => $globalDescription,
276 'contextDescription' => $contextDescription,
277 ];
278 } else {
279 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
280 }
281 }
282
283 /**
284 * @return string Internal name
285 */
286 public function getName() {
287 return $this->name;
288 }
289
290 /**
291 * @return string i18n key for title
292 */
293 public function getTitle() {
294 return $this->title;
295 }
296
297 /**
298 * @return string Type (TYPE constant from a subclass)
299 */
300 public function getType() {
301 return $this->type;
302 }
303
304 /**
305 * @return int Priority. Higher means higher in the group list
306 */
307 public function getPriority() {
308 return $this->priority;
309 }
310
311 /**
312 * @return ChangesListFilter[] Associative array of ChangesListFilter objects, with
313 * filter name as key
314 */
315 public function getFilters() {
316 return $this->filters;
317 }
318
319 /**
320 * Get filter by name
321 *
322 * @param string $name Filter name
323 * @return ChangesListFilter|null Specified filter, or null if it is not registered
324 */
325 public function getFilter( $name ) {
326 return isset( $this->filters[$name] ) ? $this->filters[$name] : null;
327 }
328
329 /**
330 * Check whether the URL parameter is for the group, or for individual filters.
331 * Defaults can also be defined on the group if and only if this is true.
332 *
333 * @return bool True if and only if the URL parameter is per-group
334 */
335 abstract public function isPerGroupRequestParameter();
336
337 /**
338 * Gets the JS data in the format required by the front-end of the structured UI
339 *
340 * @return array|null Associative array, or null if there are no filters that
341 * display in the structured UI. messageKeys is a special top-level value, with
342 * the value being an array of the message keys to send to the client.
343 */
344 public function getJsData() {
345 $output = [
346 'name' => $this->name,
347 'type' => $this->type,
348 'fullCoverage' => $this->isFullCoverage,
349 'filters' => [],
350 'priority' => $this->priority,
351 'conflicts' => [],
352 'messageKeys' => [ $this->title ]
353 ];
354
355 if ( isset( $this->whatsThisHeader ) ) {
356 $output['whatsThisHeader'] = $this->whatsThisHeader;
357 $output['whatsThisBody'] = $this->whatsThisBody;
358 $output['whatsThisUrl'] = $this->whatsThisUrl;
359 $output['whatsThisLinkText'] = $this->whatsThisLinkText;
360
361 array_push(
362 $output['messageKeys'],
363 $output['whatsThisHeader'],
364 $output['whatsThisBody'],
365 $output['whatsThisLinkText']
366 );
367 }
368
369 usort( $this->filters, function ( $a, $b ) {
370 return $b->getPriority() - $a->getPriority();
371 } );
372
373 foreach ( $this->filters as $filterName => $filter ) {
374 if ( $filter->displaysOnStructuredUi() ) {
375 $filterData = $filter->getJsData();
376 $output['messageKeys'] = array_merge(
377 $output['messageKeys'],
378 $filterData['messageKeys']
379 );
380 unset( $filterData['messageKeys'] );
381 $output['filters'][] = $filterData;
382 }
383 }
384
385 if ( count( $output['filters'] ) === 0 ) {
386 return null;
387 }
388
389 $output['title'] = $this->title;
390
391 $conflicts = array_merge(
392 $this->conflictingGroups,
393 $this->conflictingFilters
394 );
395
396 foreach ( $conflicts as $conflictInfo ) {
397 $output['conflicts'][] = $conflictInfo;
398 unset( $conflictInfo['filterObject'] );
399 unset( $conflictInfo['groupObject'] );
400 array_push(
401 $output['messageKeys'],
402 $conflictInfo['globalDescription'],
403 $conflictInfo['contextDescription']
404 );
405 }
406
407 return $output;
408 }
409
410 /**
411 * Get groups conflicting with this filter group
412 *
413 * @return ChangesListFilterGroup[]
414 */
415 public function getConflictingGroups() {
416 return array_map(
417 function ( $conflictDesc ) {
418 return $conflictDesc[ 'groupObject' ];
419 },
420 $this->conflictingGroups
421 );
422 }
423
424 /**
425 * Get filters conflicting with this filter group
426 *
427 * @return ChangesListFilter[]
428 */
429 public function getConflictingFilters() {
430 return array_map(
431 function ( $conflictDesc ) {
432 return $conflictDesc[ 'filterObject' ];
433 },
434 $this->conflictingFilters
435 );
436 }
437
438 /**
439 * Check if any filter in this group is selected
440 *
441 * @param FormOptions $opts
442 * @return bool
443 */
444 public function anySelected( FormOptions $opts ) {
445 return !!count( array_filter(
446 $this->getFilters(),
447 function ( ChangesListFilter $filter ) use ( $opts ) {
448 return $filter->isSelected( $opts );
449 }
450 ) );
451 }
452 }