Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.31% covered (warning)
84.31%
43 / 51
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangesListStringOptionsFilterGroup
86.00% covered (warning)
86.00%
43 / 50
50.00% covered (danger)
50.00%
4 / 8
17.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 setDefault
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 getDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 modifyQuery
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
6.01
 getJsData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\RecentChanges;
8
9use InvalidArgumentException;
10use MediaWiki\Html\FormOptions;
11use MediaWiki\SpecialPage\ChangesListSpecialPage;
12use Wikimedia\Rdbms\IReadableDatabase;
13
14/**
15 * Represents a filter group with multiple string options. They are passed to the server as
16 * a single form parameter separated by a delimiter.  The parameter name is the
17 * group name.  E.g. groupname=opt1;opt2 .
18 *
19 * If all options are selected they are replaced by the term "all".
20 *
21 * There is also a single DB query modification for the whole group.
22 *
23 * @since 1.29
24 * @ingroup RecentChanges
25 * @author Matthew Flaschen
26 */
27class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
28    /**
29     * Type marker, used by JavaScript
30     */
31    public const TYPE = 'string_options';
32
33    /**
34     * Delimiter
35     */
36    public const SEPARATOR = ';';
37
38    /**
39     * Signifies that all options in the group are selected.
40     */
41    public const ALL = 'all';
42
43    /**
44     * Signifies that no options in the group are selected, meaning the group has no effect.
45     *
46     * For full-coverage groups, this is the same as ALL if all filters are allowed.
47     * For others, it is not.
48     */
49    public const NONE = '';
50
51    /**
52     * Default parameter value
53     *
54     * @var string
55     */
56    protected $defaultValue;
57
58    /**
59     * Callable used to do the actual query modification; see constructor
60     *
61     * @var callable|null
62     */
63    protected $queryCallable;
64
65    /**
66     * Create a new filter group with the specified configuration
67     *
68     * @param array $groupDefinition Configuration of group
69     * * $groupDefinition['name'] string Group name
70     * * $groupDefinition['title'] string i18n key for title (optional, can be omitted
71     *     only if none of the filters in the group display in the structured UI)
72     * * $groupDefinition['priority'] int Priority integer.  Higher means higher in the
73     *     group list.
74     * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which
75     *     is an associative array to be passed to the filter constructor.  However,
76     *     'priority' is optional for the filters.  Any filter that has priority unset
77     *     will be put to the bottom, in the order given.
78     * * $groupDefinition['default'] string Default for group.
79     * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage;
80     *     if true, this means that checking every item in the group means no
81     *     changes list entries are filtered out.
82     * * $groupDefinition['queryCallable'] callable Callable accepting parameters:
83     *     * string $specialPageClassName Class name of current special page
84     *     * IContextSource $context Context, for e.g. user
85     *     * IDatabase $dbr Database, for addQuotes, makeList, and similar
86     *     * array &$tables Array of tables; see IDatabase::select $table
87     *     * array &$fields Array of fields; see IDatabase::select $vars
88     *     * array &$conds Array of conditions; see IDatabase::select $conds
89     *     * array &$query_options Array of query options; see IDatabase::select $options
90     *     * array &$join_conds Array of join conditions; see IDatabase::select $join_conds
91     *     * array $selectedValues The allowed and requested values, lower-cased and sorted
92     * * $groupDefinition['whatsThisHeader'] string i18n key for header of "What's
93     *     This" popup (optional).
94     * * $groupDefinition['whatsThisBody'] string i18n key for body of "What's This"
95     *     popup (optional).
96     * * $groupDefinition['whatsThisUrl'] string URL for main link of "What's This"
97     *     popup (optional).
98     * * $groupDefinition['whatsThisLinkText'] string i18n key of text for main link of
99     *     "What's This" popup (optional).
100     */
101    public function __construct( array $groupDefinition ) {
102        if ( !isset( $groupDefinition['isFullCoverage'] ) ) {
103            throw new InvalidArgumentException( 'You must specify isFullCoverage' );
104        }
105
106        $groupDefinition['type'] = self::TYPE;
107
108        parent::__construct( $groupDefinition );
109
110        $this->queryCallable = $groupDefinition['queryCallable'] ?? null;
111
112        if ( isset( $groupDefinition['default'] ) ) {
113            $this->setDefault( $groupDefinition['default'] );
114        } else {
115            throw new InvalidArgumentException( 'You must specify a default' );
116        }
117    }
118
119    /**
120     * Sets default of filter group.
121     *
122     * @param string $defaultValue
123     */
124    public function setDefault( $defaultValue ) {
125        if ( !is_string( $defaultValue ) ) {
126            throw new InvalidArgumentException(
127                "Can't set the default of filter options group \"{$this->getName()}\"" .
128                ' to a value of type "' . gettype( $defaultValue ) . ': string expected' );
129        }
130        $this->defaultValue = $defaultValue;
131    }
132
133    /**
134     * Gets default of filter group
135     *
136     * @return string
137     */
138    public function getDefault() {
139        return $this->defaultValue;
140    }
141
142    /**
143     * @inheritDoc
144     */
145    protected function createFilter( array $filterDefinition ) {
146        return new ChangesListStringOptionsFilter( $filterDefinition );
147    }
148
149    /**
150     * Registers a filter in this group
151     *
152     * @param ChangesListStringOptionsFilter $filter
153     * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch
154     */
155    public function registerFilter( ChangesListStringOptionsFilter $filter ) {
156        $this->filters[$filter->getName()] = $filter;
157    }
158
159    /**
160     * @inheritDoc
161     */
162    public function modifyQuery( IReadableDatabase $dbr, ChangesListSpecialPage $specialPage,
163        &$tables, &$fields, &$conds, &$query_options, &$join_conds,
164        FormOptions $opts, $isStructuredFiltersEnabled
165    ) {
166        // STRING_OPTIONS filter groups are exclusively active on Structured UI
167        if ( !$isStructuredFiltersEnabled ) {
168            return;
169        }
170        if ( !$this->queryCallable ) {
171            return;
172        }
173
174        $value = $opts[ $this->getName() ];
175        $allowedFilterNames = [];
176        foreach ( $this->filters as $filter ) {
177            $allowedFilterNames[] = $filter->getName();
178        }
179
180        if ( $value === self::ALL ) {
181            $selectedValues = $allowedFilterNames;
182        } else {
183            $selectedValues = explode( self::SEPARATOR, strtolower( $value ) );
184
185            // remove values that are not recognized or not currently allowed
186            $selectedValues = array_intersect(
187                $selectedValues,
188                $allowedFilterNames
189            );
190        }
191
192        // If there are now no values, because all are disallowed or invalid (also,
193        // the user may not have selected any), this is a no-op.
194
195        // If everything is unchecked, the group always has no effect, regardless
196        // of full-coverage.
197        if ( count( $selectedValues ) === 0 ) {
198            return;
199        }
200
201        sort( $selectedValues );
202
203        ( $this->queryCallable )(
204            get_class( $specialPage ),
205            $specialPage->getContext(),
206            $dbr,
207            $tables,
208            $fields,
209            $conds,
210            $query_options,
211            $join_conds,
212            $selectedValues
213        );
214    }
215
216    /**
217     * @inheritDoc
218     */
219    public function getJsData() {
220        $output = parent::getJsData();
221
222        $output['separator'] = self::SEPARATOR;
223        $output['default'] = $this->getDefault();
224
225        return $output;
226    }
227
228    /**
229     * @inheritDoc
230     */
231    public function addOptions( FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled ) {
232        $opts->add( $this->getName(), $allowDefaults ? $this->getDefault() : '' );
233    }
234}
235
236/** @deprecated class alias since 1.44 */
237class_alias( ChangesListStringOptionsFilterGroup::class, 'ChangesListStringOptionsFilterGroup' );