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