Merge "Add support for PHP7 random_bytes in favor of mcrypt_create_iv"
[lhc/web/wiklou.git] / includes / changes / ChangesListFilter.php
1 <?php
2 /**
3 * Represents a filter (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 /**
26 * Represents a filter (used on ChangesListSpecialPage and descendants)
27 *
28 * @since 1.29
29 */
30 abstract class ChangesListFilter {
31 /**
32 * Filter name
33 *
34 * @var string $name
35 */
36 protected $name;
37
38 /**
39 * CSS class suffix used for attribution, e.g. 'bot'.
40 *
41 * In this example, if bot actions are included in the result set, this CSS class
42 * will then be included in all bot-flagged actions.
43 *
44 * @var string|null $cssClassSuffix
45 */
46 protected $cssClassSuffix;
47
48 /**
49 * Callable that returns true if and only if a row is attributed to this filter
50 *
51 * @var callable $isRowApplicableCallable
52 */
53 protected $isRowApplicableCallable;
54
55 /**
56 * Group. ChangesListFilterGroup this belongs to
57 *
58 * @var ChangesListFilterGroup $group
59 */
60 protected $group;
61
62 /**
63 * i18n key of label for structured UI
64 *
65 * @var string $label
66 */
67 protected $label;
68
69 /**
70 * i18n key of description for structured UI
71 *
72 * @var string $description
73 */
74 protected $description;
75
76 /**
77 * Callable used to check whether this filter is allowed to take effect
78 *
79 * @var callable $isAllowedCallable
80 */
81 protected $isAllowedCallable;
82
83 /**
84 * List of conflicting groups
85 *
86 * @var array $conflictingGroups Array of associative arrays with conflict
87 * information. See setUnidirectionalConflict
88 */
89 protected $conflictingGroups = [];
90
91 /**
92 * List of conflicting filters
93 *
94 * @var array $conflictingFilters Array of associative arrays with conflict
95 * information. See setUnidirectionalConflict
96 */
97 protected $conflictingFilters = [];
98
99 /**
100 * List of filters that are a subset of the current filter
101 *
102 * @var array $subsetFilters Array of associative arrays with subset information
103 */
104 protected $subsetFilters = [];
105
106 /**
107 * Priority integer. Higher value means higher up in the group's filter list.
108 *
109 * @var string $priority
110 */
111 protected $priority;
112
113 const RESERVED_NAME_CHAR = '_';
114
115 /**
116 * Creates a new filter with the specified configuration, and registers it to the
117 * specified group.
118 *
119 * It infers which UI (it can be either or both) to display the filter on based on
120 * which messages are provided.
121 *
122 * If 'label' is provided, it will be displayed on the structured UI. Thus,
123 * 'label', 'description', and sub-class parameters are optional depending on which
124 * UI it's for.
125 *
126 * @param array $filterDefinition ChangesListFilter definition
127 *
128 * $filterDefinition['name'] string Name of filter; use lowercase with no
129 * punctuation
130 * $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark
131 * that a particular row belongs to this filter (when a row is included by the
132 * filter) (optional)
133 * $filterDefinition['isRowApplicableCallable'] Callable taking two parameters, the
134 * IContextSource, and the RecentChange object for the row, and returning true if
135 * the row is attributed to this filter. The above CSS class will then be
136 * automatically added (optional, required if cssClassSuffix is used).
137 * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this
138 * belongs to.
139 * $filterDefinition['label'] string i18n key of label for structured UI.
140 * $filterDefinition['description'] string i18n key of description for structured
141 * UI.
142 * $filterDefinition['isAllowedCallable'] callable Callable taking two parameters,
143 * the class name of the special page and an IContextSource, and returning true
144 * if and only if the current user is permitted to use this filter on the current
145 * wiki. If it returns false, it will both hide the UI (in all UIs) and prevent
146 * the DB query modification from taking effect. (optional, defaults to allowed)
147 * $filterDefinition['priority'] int Priority integer. Higher value means higher
148 * up in the group's filter list.
149 */
150 public function __construct( array $filterDefinition ) {
151 if ( isset( $filterDefinition['group'] ) ) {
152 $this->group = $filterDefinition['group'];
153 } else {
154 throw new MWException( 'You must use \'group\' to specify the ' .
155 'ChangesListFilterGroup this filter belongs to' );
156 }
157
158 if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
159 throw new MWException( 'Filter names may not contain \'' .
160 self::RESERVED_NAME_CHAR .
161 '\'. Use the naming convention: \'lowercase\''
162 );
163 }
164
165 if ( $this->group->getFilter( $filterDefinition['name'] ) ) {
166 throw new MWException( 'Two filters in a group cannot have the ' .
167 "same name: '{$filterDefinition['name']}'" );
168 }
169
170 $this->name = $filterDefinition['name'];
171
172 if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
173 $this->cssClassSuffix = $filterDefinition['cssClassSuffix'];
174 $this->isRowApplicableCallable = $filterDefinition['isRowApplicableCallable'];
175 }
176
177 if ( isset( $filterDefinition['label'] ) ) {
178 $this->label = $filterDefinition['label'];
179 $this->description = $filterDefinition['description'];
180 }
181
182 if ( isset( $filterDefinition['isAllowedCallable'] ) ) {
183 $this->isAllowedCallable = $filterDefinition['isAllowedCallable'];
184 }
185
186 $this->priority = $filterDefinition['priority'];
187
188 $this->group->registerFilter( $this );
189 }
190
191 /**
192 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
193 *
194 * WARNING: This means there is a conflict when both things are *shown*
195 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
196 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
197 *
198 * @param ChangesListFilterGroup|ChangesListFilter $other Other
199 * ChangesListFilterGroup or ChangesListFilter
200 * @param string $globalKey i18n key for top-level conflict message
201 * @param string $forwardKey i18n key for conflict message in this
202 * direction (when in UI context of $this object)
203 * @param string $backwardKey i18n key for conflict message in reverse
204 * direction (when in UI context of $other object)
205 */
206 public function conflictsWith( $other, $globalKey, $forwardKey,
207 $backwardKey ) {
208
209 if ( $globalKey === null || $forwardKey === null ||
210 $backwardKey === null ) {
211
212 throw new MWException( 'All messages must be specified' );
213 }
214
215 $this->setUnidirectionalConflict(
216 $other,
217 $globalKey,
218 $forwardKey
219 );
220
221 $other->setUnidirectionalConflict(
222 $this,
223 $globalKey,
224 $backwardKey
225 );
226 }
227
228 /**
229 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
230 * this object.
231 *
232 * Internal use ONLY.
233 *
234 * @param ChangesListFilterGroup|ChangesListFilter $other Other
235 * ChangesListFilterGroup or ChangesListFilter
236 * @param string $globalDescription i18n key for top-level conflict message
237 * @param string $contextDescription i18n key for conflict message in this
238 * direction (when in UI context of $this object)
239 */
240 public function setUnidirectionalConflict( $other, $globalDescription,
241 $contextDescription ) {
242
243 if ( $other instanceof ChangesListFilterGroup ) {
244 $this->conflictingGroups[] = [
245 'group' => $other->getName(),
246 'globalDescription' => $globalDescription,
247 'contextDescription' => $contextDescription,
248 ];
249 } elseif ( $other instanceof ChangesListFilter ) {
250 $this->conflictingFilters[] = [
251 'group' => $other->getGroup()->getName(),
252 'filter' => $other->getName(),
253 'globalDescription' => $globalDescription,
254 'contextDescription' => $contextDescription,
255 ];
256 } else {
257 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
258 }
259 }
260
261 /**
262 * Marks that the current instance is (also) a superset of the filter passed in.
263 * This can be called more than once.
264 *
265 * This means that anything in the results for the other filter is also in the
266 * results for this one.
267 *
268 * @param ChangesListFilter The filter the current instance is a superset of
269 */
270 public function setAsSupersetOf( ChangesListFilter $other ) {
271 if ( $other->getGroup() !== $this->getGroup() ) {
272 throw new MWException( 'Supersets can only be defined for filters in the same group' );
273 }
274
275 $this->subsetFilters[] = [
276 // It's always the same group, but this makes the representation
277 // more consistent with conflicts.
278 'group' => $other->getGroup()->getName(),
279 'filter' => $other->getName(),
280 ];
281 }
282
283 /**
284 * @return string Name, e.g. hideanons
285 */
286 public function getName() {
287 return $this->name;
288 }
289
290 /**
291 * @return ChangesListFilterGroup Group this belongs to
292 */
293 public function getGroup() {
294 return $this->group;
295 }
296
297 /**
298 * @return string i18n key of label for structured UI
299 */
300 public function getLabel() {
301 return $this->label;
302 }
303
304 /**
305 * @return string i18n key of description for structured UI
306 */
307 public function getDescription() {
308 return $this->description;
309 }
310
311 /**
312 * Checks whether the filter should display on the unstructured UI
313 *
314 * @param ChangesListSpecialPage $specialPage Current special page
315 * @return bool Whether to display
316 */
317 abstract public function displaysOnUnstructuredUi( ChangesListSpecialPage $specialPage );
318
319 /**
320 * Checks whether the filter should display on the structured UI
321 * This refers to the exact filter. See also isFeatureAvailableOnStructuredUi.
322 *
323 * @param ChangesListSpecialPage $specialPage Current special page
324 * @return bool Whether to display
325 */
326 public function displaysOnStructuredUi( ChangesListSpecialPage $specialPage ) {
327 return $this->label !== null && $this->isAllowed( $specialPage );
328 }
329
330 /**
331 * Checks whether an equivalent feature for this filter is available on the
332 * structured UI.
333 *
334 * This can either be the exact filter, or a new filter that replaces it.
335 */
336 public function isFeatureAvailableOnStructuredUi( ChangesListSpecialPage $specialPage ) {
337 return $this->displaysOnStructuredUi( $specialPage );
338 }
339
340 /**
341 * @return int Priority. Higher value means higher up in the group list
342 */
343 public function getPriority() {
344 return $this->priority;
345 }
346
347 /**
348 * Checks whether the filter is allowed for the current context
349 *
350 * @param ChangesListSpecialPage $specialPage Current special page
351 * @return bool Whether it is allowed
352 */
353 public function isAllowed( ChangesListSpecialPage $specialPage ) {
354 if ( $this->isAllowedCallable === null ) {
355 return true;
356 } else {
357 return call_user_func(
358 $this->isAllowedCallable,
359 get_class( $specialPage ),
360 $specialPage->getContext()
361 );
362 }
363 }
364
365 /**
366 * Gets the CSS class
367 *
368 * @return string|null CSS class, or null if not defined
369 */
370 protected function getCssClass() {
371 if ( $this->cssClassSuffix !== null ) {
372 return ChangesList::CSS_CLASS_PREFIX . $this->cssClassSuffix;
373 } else {
374 return null;
375 }
376 }
377
378 /**
379 * Add CSS class if needed
380 *
381 * @param IContextSource $ctx Context source
382 * @param RecentChange $rc Recent changes object
383 * @param Non-associative array of CSS class names; appended to if needed
384 */
385 public function applyCssClassIfNeeded( IContextSource $ctx, RecentChange $rc, array &$classes ) {
386 if ( $this->isRowApplicableCallable === null ) {
387 return;
388 }
389
390 if ( call_user_func( $this->isRowApplicableCallable, $ctx, $rc ) ) {
391 $classes[] = $this->getCssClass();
392 }
393 }
394
395 /**
396 * Gets the JS data required by the front-end of the structured UI
397 *
398 * @return array Associative array Data required by the front-end. messageKeys is
399 * a special top-level value, with the value being an array of the message keys to
400 * send to the client.
401 */
402 public function getJsData() {
403 $output = [
404 'name' => $this->getName(),
405 'label' => $this->getLabel(),
406 'description' => $this->getDescription(),
407 'cssClass' => $this->getCssClass(),
408 'priority' => $this->priority,
409 'subset' => $this->subsetFilters,
410 'conflicts' => [],
411 ];
412
413 $output['messageKeys'] = [
414 $this->getLabel(),
415 $this->getDescription(),
416 ];
417
418 $conflicts = array_merge(
419 $this->conflictingGroups,
420 $this->conflictingFilters
421 );
422
423 foreach ( $conflicts as $conflictInfo ) {
424 $output['conflicts'][] = $conflictInfo;
425 array_push(
426 $output['messageKeys'],
427 $conflictInfo['globalDescription'],
428 $conflictInfo['contextDescription']
429 );
430 }
431
432 return $output;
433 }
434 }