Merge "Drop index oi_name_archive_name on table oldimage"
[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 * List of conflicting groups
110 *
111 * @var array $conflictingGroups Array of associative arrays with conflict
112 * information. See setUnidirectionalConflict
113 */
114 protected $conflictingGroups = [];
115
116 /**
117 * List of conflicting filters
118 *
119 * @var array $conflictingFilters Array of associative arrays with conflict
120 * information. See setUnidirectionalConflict
121 */
122 protected $conflictingFilters = [];
123
124 const DEFAULT_PRIORITY = -100;
125
126 /**
127 * Create a new filter group with the specified configuration
128 *
129 * @param array $groupDefinition Configuration of group
130 * * $groupDefinition['name'] string Group name
131 * * $groupDefinition['title'] string i18n key for title (optional, can be omitted
132 * * only if none of the filters in the group display in the structured UI)
133 * * $groupDefinition['type'] string A type constant from a subclass of this one
134 * * $groupDefinition['priority'] int Priority integer. Higher value means higher
135 * * up in the group list (optional, defaults to -100).
136 * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which
137 * * is an associative array to be passed to the filter constructor. However,
138 * * 'priority' is optional for the filters. Any filter that has priority unset
139 * * will be put to the bottom, in the order given.
140 * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage;
141 * * if true, this means that checking every item in the group means no
142 * * changes list entries are filtered out.
143 */
144 public function __construct( array $groupDefinition ) {
145 $this->name = $groupDefinition['name'];
146
147 if ( isset( $groupDefinition['title'] ) ) {
148 $this->title = $groupDefinition['title'];
149 }
150
151 if ( isset ( $groupDefinition['whatsThisHeader'] ) ) {
152 $this->whatsThisHeader = $groupDefinition['whatsThisHeader'];
153 $this->whatsThisBody = $groupDefinition['whatsThisBody'];
154 $this->whatsThisUrl = $groupDefinition['whatsThisUrl'];
155 $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText'];
156 }
157
158 $this->type = $groupDefinition['type'];
159 if ( isset( $groupDefinition['priority'] ) ) {
160 $this->priority = $groupDefinition['priority'];
161 } else {
162 $this->priority = self::DEFAULT_PRIORITY;
163 }
164
165 $this->isFullCoverage = $groupDefinition['isFullCoverage'];
166
167 $this->filters = [];
168 $lowestSpecifiedPriority = -1;
169 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
170 if ( isset( $filterDefinition['priority'] ) ) {
171 $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
172 }
173 }
174
175 // Convenience feature: If you specify a group (and its filters) all in
176 // one place, you don't have to specify priority. You can just put them
177 // in order. However, if you later add one (e.g. an extension adds a filter
178 // to a core-defined group), you need to specify it.
179 $autoFillPriority = $lowestSpecifiedPriority - 1;
180 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
181 if ( !isset( $filterDefinition['priority'] ) ) {
182 $filterDefinition['priority'] = $autoFillPriority;
183 $autoFillPriority--;
184 }
185 $filterDefinition['group'] = $this;
186
187 $filter = $this->createFilter( $filterDefinition );
188 $this->registerFilter( $filter );
189 }
190 }
191
192 /**
193 * Creates a filter of the appropriate type for this group, from the definition
194 *
195 * @param array $filterDefinition Filter definition
196 * @return ChangesListFilter Filter
197 */
198 abstract protected function createFilter( array $filterDefinition );
199
200 /**
201 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
202 *
203 * WARNING: This means there is a conflict when both things are *shown*
204 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
205 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
206 *
207 * @param ChangesListFilterGroup|ChangesListFilter $other Other
208 * ChangesListFilterGroup or ChangesListFilter
209 * @param string $globalKey i18n key for top-level conflict message
210 * @param string $forwardKey i18n key for conflict message in this
211 * direction (when in UI context of $this object)
212 * @param string $backwardKey i18n key for conflict message in reverse
213 * direction (when in UI context of $other object)
214 */
215 public function conflictsWith( $other, $globalKey, $forwardKey,
216 $backwardKey ) {
217
218 if ( $globalKey === null || $forwardKey === null ||
219 $backwardKey === null ) {
220
221 throw new MWException( 'All messages must be specified' );
222 }
223
224 $this->setUnidirectionalConflict(
225 $other,
226 $globalKey,
227 $forwardKey
228 );
229
230 $other->setUnidirectionalConflict(
231 $this,
232 $globalKey,
233 $backwardKey
234 );
235 }
236
237 /**
238 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
239 * this object.
240 *
241 * Internal use ONLY.
242 *
243 * @param ChangesListFilterGroup|ChangesListFilter $other Other
244 * ChangesListFilterGroup or ChangesListFilter
245 * @param string $globalDescription i18n key for top-level conflict message
246 * @param string $contextDescription i18n key for conflict message in this
247 * direction (when in UI context of $this object)
248 */
249 public function setUnidirectionalConflict( $other, $globalDescription,
250 $contextDescription ) {
251
252 if ( $other instanceof ChangesListFilterGroup ) {
253 $this->conflictingGroups[] = [
254 'group' => $other->getName(),
255 'globalDescription' => $globalDescription,
256 'contextDescription' => $contextDescription,
257 ];
258 } elseif ( $other instanceof ChangesListFilter ) {
259 $this->conflictingFilters[] = [
260 'group' => $other->getGroup()->getName(),
261 'filter' => $other->getName(),
262 'globalDescription' => $globalDescription,
263 'contextDescription' => $contextDescription,
264 ];
265 } else {
266 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
267 }
268 }
269
270 /**
271 * @return string Internal name
272 */
273 public function getName() {
274 return $this->name;
275 }
276
277 /**
278 * @return string i18n key for title
279 */
280 public function getTitle() {
281 return $this->title;
282 }
283
284 /**
285 * @return string Type (TYPE constant from a subclass)
286 */
287 public function getType() {
288 return $this->type;
289 }
290
291 /**
292 * @return int Priority. Higher means higher in the group list
293 */
294 public function getPriority() {
295 return $this->priority;
296 }
297
298 /**
299 * @return array Associative array of ChangesListFilter objects, with filter name as key
300 */
301 public function getFilters() {
302 return $this->filters;
303 }
304
305 /**
306 * Get filter by name
307 *
308 * @param string $name Filter name
309 * @return ChangesListFilter Specified filter
310 */
311 public function getFilter( $name ) {
312 return $this->filters[$name];
313 }
314
315 /**
316 * Check whether the URL parameter is for the group, or for individual filters.
317 * Defaults can also be defined on the group if and only if this is true.
318 *
319 * @return bool True if and only if the URL parameter is per-group
320 */
321 abstract public function isPerGroupRequestParameter();
322
323 /**
324 * Gets the JS data in the format required by the front-end of the structured UI
325 *
326 * @param ChangesListSpecialPage $specialPage
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( ChangesListSpecialPage $specialPage ) {
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( $specialPage ) ) {
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 $output['conflicts'][] = $conflictInfo;
385 array_push(
386 $output['messageKeys'],
387 $conflictInfo['globalDescription'],
388 $conflictInfo['contextDescription']
389 );
390 }
391
392 return $output;
393 }
394 }