0cdc24a41fb51b583990d96feee4f9fa287178b7
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
22 * @author Matthew Flaschen
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.
31 * Represents a filter group (used on ChangesListSpecialPage and descendants)
35 abstract class ChangesListFilterGroup
{
37 * Name (internal identifier)
51 * i18n key for header of What's This?
53 * @var string|null $whatsThisHeader
55 protected $whatsThisHeader;
58 * i18n key for body of What's This?
60 * @var string|null $whatsThisBody
62 protected $whatsThisBody;
65 * URL of What's This? link
67 * @var string|null $whatsThisUrl
69 protected $whatsThisUrl;
72 * i18n key for What's This? link
74 * @var string|null $whatsThisLinkText
76 protected $whatsThisLinkText;
79 * Type, from a TYPE constant of a subclass
86 * Priority integer. Higher values means higher up in the
89 * @var string $priority
94 * Associative array of filters, as ChangesListFilter objects, with filter name as key
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.
104 * @var bool $isFullCoverage
106 protected $isFullCoverage;
109 * List of conflicting groups
111 * @var array $conflictingGroups Array of associative arrays with conflict
112 * information. See setUnidirectionalConflict
114 protected $conflictingGroups = [];
117 * List of conflicting filters
119 * @var array $conflictingFilters Array of associative arrays with conflict
120 * information. See setUnidirectionalConflict
122 protected $conflictingFilters = [];
124 const DEFAULT_PRIORITY
= -100;
126 const RESERVED_NAME_CHAR
= '_';
129 * Create a new filter group with the specified configuration
131 * @param array $groupDefinition Configuration of group
132 * * $groupDefinition['name'] string Group name; use camelCase with no punctuation
133 * * $groupDefinition['title'] string i18n key for title (optional, can be omitted
134 * * only if none of the filters in the group display in the structured UI)
135 * * $groupDefinition['type'] string A type constant from a subclass of this one
136 * * $groupDefinition['priority'] int Priority integer. Higher value means higher
137 * * up in the group list (optional, defaults to -100).
138 * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which
139 * * is an associative array to be passed to the filter constructor. However,
140 * * 'priority' is optional for the filters. Any filter that has priority unset
141 * * will be put to the bottom, in the order given.
142 * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage;
143 * * if true, this means that checking every item in the group means no
144 * * changes list entries are filtered out.
146 public function __construct( array $groupDefinition ) {
147 if ( strpos( $groupDefinition['name'], self
::RESERVED_NAME_CHAR
) !== false ) {
148 throw new MWException( 'Group names may not contain \'' .
149 self
::RESERVED_NAME_CHAR
.
150 '\'. Use the naming convention: \'camelCase\''
154 $this->name
= $groupDefinition['name'];
156 if ( isset( $groupDefinition['title'] ) ) {
157 $this->title
= $groupDefinition['title'];
160 if ( isset ( $groupDefinition['whatsThisHeader'] ) ) {
161 $this->whatsThisHeader
= $groupDefinition['whatsThisHeader'];
162 $this->whatsThisBody
= $groupDefinition['whatsThisBody'];
163 $this->whatsThisUrl
= $groupDefinition['whatsThisUrl'];
164 $this->whatsThisLinkText
= $groupDefinition['whatsThisLinkText'];
167 $this->type
= $groupDefinition['type'];
168 if ( isset( $groupDefinition['priority'] ) ) {
169 $this->priority
= $groupDefinition['priority'];
171 $this->priority
= self
::DEFAULT_PRIORITY
;
174 $this->isFullCoverage
= $groupDefinition['isFullCoverage'];
177 $lowestSpecifiedPriority = -1;
178 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
179 if ( isset( $filterDefinition['priority'] ) ) {
180 $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] );
184 // Convenience feature: If you specify a group (and its filters) all in
185 // one place, you don't have to specify priority. You can just put them
186 // in order. However, if you later add one (e.g. an extension adds a filter
187 // to a core-defined group), you need to specify it.
188 $autoFillPriority = $lowestSpecifiedPriority - 1;
189 foreach ( $groupDefinition['filters'] as $filterDefinition ) {
190 if ( !isset( $filterDefinition['priority'] ) ) {
191 $filterDefinition['priority'] = $autoFillPriority;
194 $filterDefinition['group'] = $this;
196 $filter = $this->createFilter( $filterDefinition );
197 $this->registerFilter( $filter );
202 * Creates a filter of the appropriate type for this group, from the definition
204 * @param array $filterDefinition Filter definition
205 * @return ChangesListFilter Filter
207 abstract protected function createFilter( array $filterDefinition );
210 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object.
212 * WARNING: This means there is a conflict when both things are *shown*
213 * (not filtered out), even for the hide-based filters. So e.g. conflicting with
214 * 'hideanons' means there is a conflict if only anonymous users are *shown*.
216 * @param ChangesListFilterGroup|ChangesListFilter $other Other
217 * ChangesListFilterGroup or ChangesListFilter
218 * @param string $globalKey i18n key for top-level conflict message
219 * @param string $forwardKey i18n key for conflict message in this
220 * direction (when in UI context of $this object)
221 * @param string $backwardKey i18n key for conflict message in reverse
222 * direction (when in UI context of $other object)
224 public function conflictsWith( $other, $globalKey, $forwardKey,
227 if ( $globalKey === null ||
$forwardKey === null ||
228 $backwardKey === null ) {
230 throw new MWException( 'All messages must be specified' );
233 $this->setUnidirectionalConflict(
239 $other->setUnidirectionalConflict(
247 * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with
252 * @param ChangesListFilterGroup|ChangesListFilter $other Other
253 * ChangesListFilterGroup or ChangesListFilter
254 * @param string $globalDescription i18n key for top-level conflict message
255 * @param string $contextDescription i18n key for conflict message in this
256 * direction (when in UI context of $this object)
258 public function setUnidirectionalConflict( $other, $globalDescription,
259 $contextDescription ) {
261 if ( $other instanceof ChangesListFilterGroup
) {
262 $this->conflictingGroups
[] = [
263 'group' => $other->getName(),
264 'groupObject' => $other,
265 'globalDescription' => $globalDescription,
266 'contextDescription' => $contextDescription,
268 } elseif ( $other instanceof ChangesListFilter
) {
269 $this->conflictingFilters
[] = [
270 'group' => $other->getGroup()->getName(),
271 'filter' => $other->getName(),
272 'filterObject' => $other,
273 'globalDescription' => $globalDescription,
274 'contextDescription' => $contextDescription,
277 throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' );
282 * @return string Internal name
284 public function getName() {
289 * @return string i18n key for title
291 public function getTitle() {
296 * @return string Type (TYPE constant from a subclass)
298 public function getType() {
303 * @return int Priority. Higher means higher in the group list
305 public function getPriority() {
306 return $this->priority
;
310 * @return array Associative array of ChangesListFilter objects, with filter name as key
312 public function getFilters() {
313 return $this->filters
;
319 * @param string $name Filter name
320 * @return ChangesListFilter|null Specified filter, or null if it is not registered
322 public function getFilter( $name ) {
323 return isset( $this->filters
[$name] ) ?
$this->filters
[$name] : null;
327 * Check whether the URL parameter is for the group, or for individual filters.
328 * Defaults can also be defined on the group if and only if this is true.
330 * @return bool True if and only if the URL parameter is per-group
332 abstract public function isPerGroupRequestParameter();
335 * Gets the JS data in the format required by the front-end of the structured UI
337 * @return array|null Associative array, or null if there are no filters that
338 * display in the structured UI. messageKeys is a special top-level value, with
339 * the value being an array of the message keys to send to the client.
341 public function getJsData() {
343 'name' => $this->name
,
344 'type' => $this->type
,
345 'fullCoverage' => $this->isFullCoverage
,
347 'priority' => $this->priority
,
349 'messageKeys' => [ $this->title
]
352 if ( isset ( $this->whatsThisHeader
) ) {
353 $output['whatsThisHeader'] = $this->whatsThisHeader
;
354 $output['whatsThisBody'] = $this->whatsThisBody
;
355 $output['whatsThisUrl'] = $this->whatsThisUrl
;
356 $output['whatsThisLinkText'] = $this->whatsThisLinkText
;
359 $output['messageKeys'],
360 $output['whatsThisHeader'],
361 $output['whatsThisBody'],
362 $output['whatsThisLinkText']
366 usort( $this->filters
, function ( $a, $b ) {
367 return $b->getPriority() - $a->getPriority();
370 foreach ( $this->filters
as $filterName => $filter ) {
371 if ( $filter->displaysOnStructuredUi() ) {
372 $filterData = $filter->getJsData();
373 $output['messageKeys'] = array_merge(
374 $output['messageKeys'],
375 $filterData['messageKeys']
377 unset( $filterData['messageKeys'] );
378 $output['filters'][] = $filterData;
382 if ( count( $output['filters'] ) === 0 ) {
386 $output['title'] = $this->title
;
388 $conflicts = array_merge(
389 $this->conflictingGroups
,
390 $this->conflictingFilters
393 foreach ( $conflicts as $conflictInfo ) {
394 $output['conflicts'][] = $conflictInfo;
395 unset( $conflictInfo['filterObject'] );
396 unset( $conflictInfo['groupObject'] );
398 $output['messageKeys'],
399 $conflictInfo['globalDescription'],
400 $conflictInfo['contextDescription']
408 * Get groups conflicting with this filter group
410 * @return ChangesListFilterGroup[]
412 public function getConflictingGroups() {
414 function ( $conflictDesc ) {
415 return $conflictDesc[ 'groupObject' ];
417 $this->conflictingGroups
422 * Get filters conflicting with this filter group
424 * @return ChangesListFilter[]
426 public function getConflictingFilters() {
428 function ( $conflictDesc ) {
429 return $conflictDesc[ 'filterObject' ];
431 $this->conflictingFilters
436 * Check if any filter in this group is selected
438 * @param FormOptions $opts
441 public function anySelected( FormOptions
$opts ) {
442 return !!count( array_filter(
444 function ( ChangesListFilter
$filter ) use ( $opts ) {
445 return $filter->isSelected( $opts );