]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/changes/ChangesListFilterGroup.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.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                         unset( $conflictInfo['filterObject'] );
398                         unset( $conflictInfo['groupObject'] );
399                         $output['conflicts'][] = $conflictInfo;
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 }