3 * Represents a filter group (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
21 * @author Matthew Flaschen
24 // TODO: Might want to make a super-class or trait to share behavior (especially re
25 // conflicts) between ChangesListFilter and ChangesListFilterGroup.
26 // What to call it. FilterStructure? That would also let me make
27 // setUnidirectionalConflict protected.
29 use Wikimedia\Rdbms\IDatabase
;
32 * Represents a filter group (used on ChangesListSpecialPage and descendants)
36 abstract class ChangesListFilterGroup
{
38 * Name (internal identifier)
52 * i18n key for header of What's This?
54 * @var string|null $whatsThisHeader
56 protected $whatsThisHeader;
59 * i18n key for body of What's This?
61 * @var string|null $whatsThisBody
63 protected $whatsThisBody;
66 * URL of What's This? link
68 * @var string|null $whatsThisUrl
70 protected $whatsThisUrl;
73 * i18n key for What's This? link
75 * @var string|null $whatsThisLinkText
77 protected $whatsThisLinkText;
80 * Type, from a TYPE constant of a subclass
87 * Priority integer. Higher values means higher up in the
90 * @var string $priority
95 * Associative array of filters, as ChangesListFilter objects, with filter name as key
102 * Whether this group is full coverage. This means that checking every item in the
103 * group means no changes list (e.g. RecentChanges) entries are filtered out.
105 * @var bool $isFullCoverage
107 protected $isFullCoverage;
110 * Array of associative arrays with conflict information. See
111 * setUnidirectionalConflict
113 * @var array $conflictingGroups
115 protected $conflictingGroups = [];
118 * Array of associative arrays with conflict information. See
119 * setUnidirectionalConflict
121 * @var array $conflictingFilters
123 protected $conflictingFilters = [];
125 const DEFAULT_PRIORITY
= -100;
127 const RESERVED_NAME_CHAR
= '_';
130 * Create a new filter group with the specified configuration
132 * @param array $groupDefinition Configuration of group
133 * * $groupDefinition['name'] string Group name; use camelCase with no punctuation
134 * * $groupDefinition['title'] string i18n key for title (optional, can be omitted
135 * only if none of the filters in the group display in the structured UI)
136 * * $groupDefinition['type'] string A type constant from a subclass of this one
137 * * $groupDefinition['priority'] int Priority integer. Higher value means higher
138 * up in the group list (optional, defaults to -100).
139 * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which
140 * is an associative array to be passed to the filter constructor. However,
141 * 'priority' is optional for the filters. Any filter that has priority unset
142 * will be put to the bottom, in the order given.
143 * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage;
144 * if true, this means that checking every item in the group means no
145 * changes list entries are filtered out.
146 * * $groupDefinition['whatsThisHeader'] string i18n key for header of "What's
147 * This" popup (optional).
148 * * $groupDefinition['whatsThisBody'] string i18n key for body of "What's This"
150 * * $groupDefinition['whatsThisUrl'] string URL for main link of "What's This"
152 * * $groupDefinition['whatsThisLinkText'] string i18n key of text for main link of
153 * "What's This" popup (optional).
155 public function __construct( array $groupDefinition ) {
156 if ( strpos( $groupDefinition['name'], self
::RESERVED_NAME_CHAR
) !== false ) {
157 throw new MWException( 'Group names may not contain \'' .
158 self
::RESERVED_NAME_CHAR
.
159 '\'. Use the naming convention: \'camelCase\''
163 $this->name
= $groupDefinition['name'];
165 if ( isset( $groupDefinition['title'] ) ) {
166 $this->title
= $groupDefinition['title'];
169 if ( isset( $groupDefinition['whatsThisHeader'] ) ) {
170 $this->whatsThisHeader
= $groupDefinition['whatsThisHeader'];
171 $this->whatsThisBody
= $groupDefinition['whatsThisBody'];
172 $this->whatsThisUrl
= $groupDefinition['whatsThisUrl'];
173 $this->whatsThisLinkText
= $groupDefinition['whatsThisLinkText'];
176 $this->type
= $groupDefinition['type'];
177 if ( isset( $groupDefinition['priority'] ) ) {
178 $this->priority
= $groupDefinition['priority'];
180 $this->priority
= self
::DEFAULT_PRIORITY
;
183 $this->isFullCoverage
= $groupDefinition['isFullCoverage'];
186 $lowestSpecifiedPriority = -1;
187 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
188 if ( isset( $filterDefinition['priority'] ) ) {
189 $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
193 // Convenience feature: If you specify a group (and its filters) all in
194 // one place, you don't have to specify priority. You can just put them
195 // in order. However, if you later add one (e.g. an extension adds a filter
196 // to a core-defined group), you need to specify it.
197 $autoFillPriority = $lowestSpecifiedPriority - 1;
198 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
199 if ( !isset( $filterDefinition['priority'] ) ) {
200 $filterDefinition['priority'] = $autoFillPriority;
203 $filterDefinition['group'] = $this;
205 $filter = $this->createFilter( $filterDefinition );
206 $this->registerFilter( $filter );
211 * Creates a filter of the appropriate type for this group, from the definition
213 * @param array $filterDefinition Filter definition
214 * @return ChangesListFilter Filter
216 abstract protected function createFilter( array $filterDefinition );
219 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
221 * WARNING: This means there is a conflict when both things are *shown*
222 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
223 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
225 * @param ChangesListFilterGroup|ChangesListFilter $other
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)
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' );
237 $this->setUnidirectionalConflict(
243 $other->setUnidirectionalConflict(
251 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
256 * @param ChangesListFilterGroup|ChangesListFilter $other
257 * @param string $globalDescription i18n key for top-level conflict message
258 * @param string $contextDescription i18n key for conflict message in this
259 * direction (when in UI context of $this object)
261 public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) {
262 if ( $other instanceof ChangesListFilterGroup
) {
263 $this->conflictingGroups
[] = [
264 'group' => $other->getName(),
265 'groupObject' => $other,
266 'globalDescription' => $globalDescription,
267 'contextDescription' => $contextDescription,
269 } elseif ( $other instanceof ChangesListFilter
) {
270 $this->conflictingFilters
[] = [
271 'group' => $other->getGroup()->getName(),
272 'filter' => $other->getName(),
273 'filterObject' => $other,
274 'globalDescription' => $globalDescription,
275 'contextDescription' => $contextDescription,
278 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
283 * @return string Internal name
285 public function getName() {
290 * @return string i18n key for title
292 public function getTitle() {
297 * @return string Type (TYPE constant from a subclass)
299 public function getType() {
304 * @return int Priority. Higher means higher in the group list
306 public function getPriority() {
307 return $this->priority
;
311 * @return ChangesListFilter[] Associative array of ChangesListFilter objects, with
314 public function getFilters() {
315 return $this->filters
;
321 * @param string $name Filter name
322 * @return ChangesListFilter|null Specified filter, or null if it is not registered
324 public function getFilter( $name ) {
325 return $this->filters
[$name] ??
null;
329 * Gets the JS data in the format required by the front-end of the structured UI
331 * @return array|null Associative array, or null if there are no filters that
332 * display in the structured UI. messageKeys is a special top-level value, with
333 * the value being an array of the message keys to send to the client.
335 public function getJsData() {
337 'name' => $this->name
,
338 'type' => $this->type
,
339 'fullCoverage' => $this->isFullCoverage
,
341 'priority' => $this->priority
,
343 'messageKeys' => [ $this->title
]
346 if ( isset( $this->whatsThisHeader
) ) {
347 $output['whatsThisHeader'] = $this->whatsThisHeader
;
348 $output['whatsThisBody'] = $this->whatsThisBody
;
349 $output['whatsThisUrl'] = $this->whatsThisUrl
;
350 $output['whatsThisLinkText'] = $this->whatsThisLinkText
;
353 $output['messageKeys'],
354 $output['whatsThisHeader'],
355 $output['whatsThisBody'],
356 $output['whatsThisLinkText']
360 usort( $this->filters
, function ( $a, $b ) {
361 return $b->getPriority() <=> $a->getPriority();
364 foreach ( $this->filters
as $filterName => $filter ) {
365 if ( $filter->displaysOnStructuredUi() ) {
366 $filterData = $filter->getJsData();
367 $output['messageKeys'] = array_merge(
368 $output['messageKeys'],
369 $filterData['messageKeys']
371 unset( $filterData['messageKeys'] );
372 $output['filters'][] = $filterData;
376 if ( count( $output['filters'] ) === 0 ) {
380 $output['title'] = $this->title
;
382 $conflicts = array_merge(
383 $this->conflictingGroups
,
384 $this->conflictingFilters
387 foreach ( $conflicts as $conflictInfo ) {
388 unset( $conflictInfo['filterObject'] );
389 unset( $conflictInfo['groupObject'] );
390 $output['conflicts'][] = $conflictInfo;
392 $output['messageKeys'],
393 $conflictInfo['globalDescription'],
394 $conflictInfo['contextDescription']
402 * Get groups conflicting with this filter group
404 * @return ChangesListFilterGroup[]
406 public function getConflictingGroups() {
408 function ( $conflictDesc ) {
409 return $conflictDesc[ 'groupObject' ];
411 $this->conflictingGroups
416 * Get filters conflicting with this filter group
418 * @return ChangesListFilter[]
420 public function getConflictingFilters() {
422 function ( $conflictDesc ) {
423 return $conflictDesc[ 'filterObject' ];
425 $this->conflictingFilters
430 * Check if any filter in this group is selected
432 * @param FormOptions $opts
435 public function anySelected( FormOptions
$opts ) {
436 return !!count( array_filter(
438 function ( ChangesListFilter
$filter ) use ( $opts ) {
439 return $filter->isSelected( $opts );
445 * Modifies the query to include the filter group.
447 * The modification is only done if the filter group is in effect. This means that
448 * one or more valid and allowed filters were selected.
450 * @param IDatabase $dbr Database, for addQuotes, makeList, and similar
451 * @param ChangesListSpecialPage $specialPage Current special page
452 * @param array &$tables Array of tables; see IDatabase::select $table
453 * @param array &$fields Array of fields; see IDatabase::select $vars
454 * @param array &$conds Array of conditions; see IDatabase::select $conds
455 * @param array &$query_options Array of query options; see IDatabase::select $options
456 * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds
457 * @param FormOptions $opts Wrapper for the current request options and their defaults
458 * @param bool $isStructuredFiltersEnabled True if the Structured UI is currently enabled
460 abstract public function modifyQuery( IDatabase
$dbr, ChangesListSpecialPage
$specialPage,
461 &$tables, &$fields, &$conds, &$query_options, &$join_conds,
462 FormOptions
$opts, $isStructuredFiltersEnabled );
465 * All the options represented by this filter group to $opts
467 * @param FormOptions $opts
468 * @param bool $allowDefaults
469 * @param bool $isStructuredFiltersEnabled
471 abstract public function addOptions( FormOptions
$opts, $allowDefaults,
472 $isStructuredFiltersEnabled );