3 * Represents a filter (used on ChangesListSpecialPage and descendants)
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.
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.
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
22 * @author Matthew Flaschen
26 * Represents a filter (used on ChangesListSpecialPage and descendants)
30 abstract class ChangesListFilter
{
39 * CSS class suffix used for attribution, e.g. 'bot'.
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.
44 * @var string|null $cssClassSuffix
46 protected $cssClassSuffix;
49 * Callable that returns true if and only if a row is attributed to this filter
51 * @var callable $isRowApplicableCallable
53 protected $isRowApplicableCallable;
56 * Group. ChangesListFilterGroup this belongs to
58 * @var ChangesListFilterGroup $group
63 * i18n key of label for structured UI
70 * i18n key of description for structured UI
72 * @var string $description
74 protected $description;
77 * Array of associative arrays with conflict information. See
78 * setUnidirectionalConflict
80 * @var array $conflictingGroups
82 protected $conflictingGroups = [];
85 * Array of associative arrays with conflict information. See
86 * setUnidirectionalConflict
88 * @var array $conflictingFilters
90 protected $conflictingFilters = [];
93 * Array of associative arrays with subset information
95 * @var array $subsetFilters
97 protected $subsetFilters = [];
100 * Priority integer. Higher value means higher up in the group's filter list.
102 * @var string $priority
108 * @var string $defaultHighlightColor
110 protected $defaultHighlightColor;
112 const RESERVED_NAME_CHAR
= '_';
115 * Creates a new filter with the specified configuration, and registers it to the
118 * It infers which UI (it can be either or both) to display the filter on based on
119 * which messages are provided.
121 * If 'label' is provided, it will be displayed on the structured UI. Thus,
122 * 'label', 'description', and sub-class parameters are optional depending on which
125 * @param array $filterDefinition ChangesListFilter definition
126 * * $filterDefinition['name'] string Name of filter; use lowercase with no
128 * * $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark
129 * that a particular row belongs to this filter (when a row is included by the
131 * * $filterDefinition['isRowApplicableCallable'] Callable taking two parameters, the
132 * IContextSource, and the RecentChange object for the row, and returning true if
133 * the row is attributed to this filter. The above CSS class will then be
134 * automatically added (optional, required if cssClassSuffix is used).
135 * * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this
137 * * $filterDefinition['label'] string i18n key of label for structured UI.
138 * * $filterDefinition['description'] string i18n key of description for structured
140 * * $filterDefinition['priority'] int Priority integer. Higher value means higher
141 * up in the group's filter list.
143 public function __construct( array $filterDefinition ) {
144 if ( isset( $filterDefinition['group'] ) ) {
145 $this->group
= $filterDefinition['group'];
147 throw new MWException( 'You must use \'group\' to specify the ' .
148 'ChangesListFilterGroup this filter belongs to' );
151 if ( strpos( $filterDefinition['name'], self
::RESERVED_NAME_CHAR
) !== false ) {
152 throw new MWException( 'Filter names may not contain \'' .
153 self
::RESERVED_NAME_CHAR
.
154 '\'. Use the naming convention: \'lowercase\''
158 if ( $this->group
->getFilter( $filterDefinition['name'] ) ) {
159 throw new MWException( 'Two filters in a group cannot have the ' .
160 "same name: '{$filterDefinition['name']}'" );
163 $this->name
= $filterDefinition['name'];
165 if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
166 $this->cssClassSuffix
= $filterDefinition['cssClassSuffix'];
167 $this->isRowApplicableCallable
= $filterDefinition['isRowApplicableCallable'];
170 if ( isset( $filterDefinition['label'] ) ) {
171 $this->label
= $filterDefinition['label'];
172 $this->description
= $filterDefinition['description'];
175 $this->priority
= $filterDefinition['priority'];
177 $this->group
->registerFilter( $this );
181 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
183 * WARNING: This means there is a conflict when both things are *shown*
184 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
185 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
187 * @param ChangesListFilterGroup|ChangesListFilter $other
188 * @param string $globalKey i18n key for top-level conflict message
189 * @param string $forwardKey i18n key for conflict message in this
190 * direction (when in UI context of $this object)
191 * @param string $backwardKey i18n key for conflict message in reverse
192 * direction (when in UI context of $other object)
194 public function conflictsWith( $other, $globalKey, $forwardKey, $backwardKey ) {
195 if ( $globalKey === null ||
$forwardKey === null ||
$backwardKey === null ) {
196 throw new MWException( 'All messages must be specified' );
199 $this->setUnidirectionalConflict(
205 $other->setUnidirectionalConflict(
213 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
218 * @param ChangesListFilterGroup|ChangesListFilter $other
219 * @param string $globalDescription i18n key for top-level conflict message
220 * @param string $contextDescription i18n key for conflict message in this
221 * direction (when in UI context of $this object)
223 public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
224 if ( $other instanceof ChangesListFilterGroup
) {
225 $this->conflictingGroups
[] = [
226 'group' => $other->getName(),
227 'groupObject' => $other,
228 'globalDescription' => $globalDescription,
229 'contextDescription' => $contextDescription,
231 } elseif ( $other instanceof ChangesListFilter
) {
232 $this->conflictingFilters
[] = [
233 'group' => $other->getGroup()->getName(),
234 'filter' => $other->getName(),
235 'filterObject' => $other,
236 'globalDescription' => $globalDescription,
237 'contextDescription' => $contextDescription,
240 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
245 * Marks that the current instance is (also) a superset of the filter passed in.
246 * This can be called more than once.
248 * This means that anything in the results for the other filter is also in the
249 * results for this one.
251 * @param ChangesListFilter $other The filter the current instance is a superset of
253 public function setAsSupersetOf( ChangesListFilter
$other ) {
254 if ( $other->getGroup() !== $this->getGroup() ) {
255 throw new MWException( 'Supersets can only be defined for filters in the same group' );
258 $this->subsetFilters
[] = [
259 // It's always the same group, but this makes the representation
260 // more consistent with conflicts.
261 'group' => $other->getGroup()->getName(),
262 'filter' => $other->getName(),
267 * @return string Name, e.g. hideanons
269 public function getName() {
274 * @return ChangesListFilterGroup Group this belongs to
276 public function getGroup() {
281 * @return string i18n key of label for structured UI
283 public function getLabel() {
288 * @return string i18n key of description for structured UI
290 public function getDescription() {
291 return $this->description
;
295 * Checks whether the filter should display on the unstructured UI
297 * @return bool Whether to display
299 abstract public function displaysOnUnstructuredUi();
302 * Checks whether the filter should display on the structured UI
303 * This refers to the exact filter. See also isFeatureAvailableOnStructuredUi.
305 * @return bool Whether to display
307 public function displaysOnStructuredUi() {
308 return $this->label
!== null;
312 * Checks whether an equivalent feature for this filter is available on the
315 * This can either be the exact filter, or a new filter that replaces it.
318 public function isFeatureAvailableOnStructuredUi() {
319 return $this->displaysOnStructuredUi();
323 * @return int Priority. Higher value means higher up in the group list
325 public function getPriority() {
326 return $this->priority
;
332 * @return string|null CSS class, or null if not defined
334 protected function getCssClass() {
335 if ( $this->cssClassSuffix
!== null ) {
336 return ChangesList
::CSS_CLASS_PREFIX
. $this->cssClassSuffix
;
343 * Add CSS class if needed
345 * @param IContextSource $ctx Context source
346 * @param RecentChange $rc Recent changes object
347 * @param array &$classes Non-associative array of CSS class names; appended to if needed
349 public function applyCssClassIfNeeded( IContextSource
$ctx, RecentChange
$rc, array &$classes ) {
350 if ( $this->isRowApplicableCallable
=== null ) {
354 if ( call_user_func( $this->isRowApplicableCallable
, $ctx, $rc ) ) {
355 $classes[] = $this->getCssClass();
360 * Gets the JS data required by the front-end of the structured UI
362 * @return array Associative array Data required by the front-end. messageKeys is
363 * a special top-level value, with the value being an array of the message keys to
364 * send to the client.
366 public function getJsData() {
368 'name' => $this->getName(),
369 'label' => $this->getLabel(),
370 'description' => $this->getDescription(),
371 'cssClass' => $this->getCssClass(),
372 'priority' => $this->priority
,
373 'subset' => $this->subsetFilters
,
375 'defaultHighlightColor' => $this->defaultHighlightColor
378 $output['messageKeys'] = [
380 $this->getDescription(),
383 $conflicts = array_merge(
384 $this->conflictingGroups
,
385 $this->conflictingFilters
388 foreach ( $conflicts as $conflictInfo ) {
389 unset( $conflictInfo['filterObject'] );
390 unset( $conflictInfo['groupObject'] );
391 $output['conflicts'][] = $conflictInfo;
393 $output['messageKeys'],
394 $conflictInfo['globalDescription'],
395 $conflictInfo['contextDescription']
403 * Checks whether this filter is selected in the provided options
405 * @param FormOptions $opts
408 abstract public function isSelected( FormOptions
$opts );
411 * Get groups conflicting with this filter
413 * @return ChangesListFilterGroup[]
415 public function getConflictingGroups() {
417 function ( $conflictDesc ) {
418 return $conflictDesc[ 'groupObject' ];
420 $this->conflictingGroups
425 * Get filters conflicting with this filter
427 * @return ChangesListFilter[]
429 public function getConflictingFilters() {
431 function ( $conflictDesc ) {
432 return $conflictDesc[ 'filterObject' ];
434 $this->conflictingFilters
439 * Check if the conflict with a group is currently "active"
441 * @param ChangesListFilterGroup $group
442 * @param FormOptions $opts
445 public function activelyInConflictWithGroup( ChangesListFilterGroup
$group, FormOptions
$opts ) {
446 if ( $group->anySelected( $opts ) && $this->isSelected( $opts ) ) {
447 /** @var ChangesListFilter $siblingFilter */
448 foreach ( $this->getSiblings() as $siblingFilter ) {
449 if ( $siblingFilter->isSelected( $opts ) && !$siblingFilter->hasConflictWithGroup( $group ) ) {
458 private function hasConflictWithGroup( ChangesListFilterGroup
$group ) {
459 return in_array( $group, $this->getConflictingGroups() );
463 * Check if the conflict with a filter is currently "active"
465 * @param ChangesListFilter $filter
466 * @param FormOptions $opts
469 public function activelyInConflictWithFilter( ChangesListFilter
$filter, FormOptions
$opts ) {
470 if ( $this->isSelected( $opts ) && $filter->isSelected( $opts ) ) {
471 /** @var ChangesListFilter $siblingFilter */
472 foreach ( $this->getSiblings() as $siblingFilter ) {
474 $siblingFilter->isSelected( $opts ) &&
475 !$siblingFilter->hasConflictWithFilter( $filter )
485 private function hasConflictWithFilter( ChangesListFilter
$filter ) {
486 return in_array( $filter, $this->getConflictingFilters() );
490 * Get filters in the same group
492 * @return ChangesListFilter[]
494 protected function getSiblings() {
496 $this->getGroup()->getFilters(),
497 function ( $filter ) {
498 return $filter !== $this;
504 * @param string $defaultHighlightColor
506 public function setDefaultHighlightColor( $defaultHighlightColor ) {
507 $this->defaultHighlightColor
= $defaultHighlightColor;