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