diff options
Diffstat (limited to 'www/wiki/includes/changes/ChangesListFilterGroup.php')
-rw-r--r-- | www/wiki/includes/changes/ChangesListFilterGroup.php | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/www/wiki/includes/changes/ChangesListFilterGroup.php b/www/wiki/includes/changes/ChangesListFilterGroup.php new file mode 100644 index 00000000..3e2c464a --- /dev/null +++ b/www/wiki/includes/changes/ChangesListFilterGroup.php @@ -0,0 +1,473 @@ +<?php +/** + * Represents a filter group (used on ChangesListSpecialPage and descendants) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Matthew Flaschen + */ + +// TODO: Might want to make a super-class or trait to share behavior (especially re +// conflicts) between ChangesListFilter and ChangesListFilterGroup. +// What to call it. FilterStructure? That would also let me make +// setUnidirectionalConflict protected. + +use Wikimedia\Rdbms\IDatabase; + +/** + * Represents a filter group (used on ChangesListSpecialPage and descendants) + * + * @since 1.29 + */ +abstract class ChangesListFilterGroup { + /** + * Name (internal identifier) + * + * @var string $name + */ + protected $name; + + /** + * i18n key for title + * + * @var string $title + */ + protected $title; + + /** + * i18n key for header of What's This? + * + * @var string|null $whatsThisHeader + */ + protected $whatsThisHeader; + + /** + * i18n key for body of What's This? + * + * @var string|null $whatsThisBody + */ + protected $whatsThisBody; + + /** + * URL of What's This? link + * + * @var string|null $whatsThisUrl + */ + protected $whatsThisUrl; + + /** + * i18n key for What's This? link + * + * @var string|null $whatsThisLinkText + */ + protected $whatsThisLinkText; + + /** + * Type, from a TYPE constant of a subclass + * + * @var string $type + */ + protected $type; + + /** + * Priority integer. Higher values means higher up in the + * group list. + * + * @var string $priority + */ + protected $priority; + + /** + * Associative array of filters, as ChangesListFilter objects, with filter name as key + * + * @var array $filters + */ + protected $filters; + + /** + * Whether this group is full coverage. This means that checking every item in the + * group means no changes list (e.g. RecentChanges) entries are filtered out. + * + * @var bool $isFullCoverage + */ + protected $isFullCoverage; + + /** + * Array of associative arrays with conflict information. See + * setUnidirectionalConflict + * + * @var array $conflictingGroups + */ + protected $conflictingGroups = []; + + /** + * Array of associative arrays with conflict information. See + * setUnidirectionalConflict + * + * @var array $conflictingFilters + */ + protected $conflictingFilters = []; + + const DEFAULT_PRIORITY = -100; + + const RESERVED_NAME_CHAR = '_'; + + /** + * Create a new filter group with the specified configuration + * + * @param array $groupDefinition Configuration of group + * * $groupDefinition['name'] string Group name; use camelCase with no punctuation + * * $groupDefinition['title'] string i18n key for title (optional, can be omitted + * only if none of the filters in the group display in the structured UI) + * * $groupDefinition['type'] string A type constant from a subclass of this one + * * $groupDefinition['priority'] int Priority integer. Higher value means higher + * up in the group list (optional, defaults to -100). + * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which + * is an associative array to be passed to the filter constructor. However, + * 'priority' is optional for the filters. Any filter that has priority unset + * will be put to the bottom, in the order given. + * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage; + * if true, this means that checking every item in the group means no + * changes list entries are filtered out. + * * $groupDefinition['whatsThisHeader'] string i18n key for header of "What's + * This" popup (optional). + * * $groupDefinition['whatsThisBody'] string i18n key for body of "What's This" + * popup (optional). + * * $groupDefinition['whatsThisUrl'] string URL for main link of "What's This" + * popup (optional). + * * $groupDefinition['whatsThisLinkText'] string i18n key of text for main link of + * "What's This" popup (optional). + */ + public function __construct( array $groupDefinition ) { + if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) { + throw new MWException( 'Group names may not contain \'' . + self::RESERVED_NAME_CHAR . + '\'. Use the naming convention: \'camelCase\'' + ); + } + + $this->name = $groupDefinition['name']; + + if ( isset( $groupDefinition['title'] ) ) { + $this->title = $groupDefinition['title']; + } + + if ( isset( $groupDefinition['whatsThisHeader'] ) ) { + $this->whatsThisHeader = $groupDefinition['whatsThisHeader']; + $this->whatsThisBody = $groupDefinition['whatsThisBody']; + $this->whatsThisUrl = $groupDefinition['whatsThisUrl']; + $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText']; + } + + $this->type = $groupDefinition['type']; + if ( isset( $groupDefinition['priority'] ) ) { + $this->priority = $groupDefinition['priority']; + } else { + $this->priority = self::DEFAULT_PRIORITY; + } + + $this->isFullCoverage = $groupDefinition['isFullCoverage']; + + $this->filters = []; + $lowestSpecifiedPriority = -1; + foreach ( $groupDefinition['filters'] as $filterDefinition ) { + if ( isset( $filterDefinition['priority'] ) ) { + $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] ); + } + } + + // Convenience feature: If you specify a group (and its filters) all in + // one place, you don't have to specify priority. You can just put them + // in order. However, if you later add one (e.g. an extension adds a filter + // to a core-defined group), you need to specify it. + $autoFillPriority = $lowestSpecifiedPriority - 1; + foreach ( $groupDefinition['filters'] as $filterDefinition ) { + if ( !isset( $filterDefinition['priority'] ) ) { + $filterDefinition['priority'] = $autoFillPriority; + $autoFillPriority--; + } + $filterDefinition['group'] = $this; + + $filter = $this->createFilter( $filterDefinition ); + $this->registerFilter( $filter ); + } + } + + /** + * Creates a filter of the appropriate type for this group, from the definition + * + * @param array $filterDefinition Filter definition + * @return ChangesListFilter Filter + */ + abstract protected function createFilter( array $filterDefinition ); + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object. + * + * WARNING: This means there is a conflict when both things are *shown* + * (not filtered out), even for the hide-based filters. So e.g. conflicting with + * 'hideanons' means there is a conflict if only anonymous users are *shown*. + * + * @param ChangesListFilterGroup|ChangesListFilter $other + * @param string $globalKey i18n key for top-level conflict message + * @param string $forwardKey i18n key for conflict message in this + * direction (when in UI context of $this object) + * @param string $backwardKey i18n key for conflict message in reverse + * direction (when in UI context of $other object) + */ + public function conflictsWith( $other, $globalKey, $forwardKey, $backwardKey ) { + if ( $globalKey === null || $forwardKey === null || $backwardKey === null ) { + throw new MWException( 'All messages must be specified' ); + } + + $this->setUnidirectionalConflict( + $other, + $globalKey, + $forwardKey + ); + + $other->setUnidirectionalConflict( + $this, + $globalKey, + $backwardKey + ); + } + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with + * this object. + * + * Internal use ONLY. + * + * @param ChangesListFilterGroup|ChangesListFilter $other + * @param string $globalDescription i18n key for top-level conflict message + * @param string $contextDescription i18n key for conflict message in this + * direction (when in UI context of $this object) + */ + public function setUnidirectionalConflict( $other, $globalDescription, $contextDescription ) { + if ( $other instanceof ChangesListFilterGroup ) { + $this->conflictingGroups[] = [ + 'group' => $other->getName(), + 'groupObject' => $other, + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } elseif ( $other instanceof ChangesListFilter ) { + $this->conflictingFilters[] = [ + 'group' => $other->getGroup()->getName(), + 'filter' => $other->getName(), + 'filterObject' => $other, + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } else { + throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' ); + } + } + + /** + * @return string Internal name + */ + public function getName() { + return $this->name; + } + + /** + * @return string i18n key for title + */ + public function getTitle() { + return $this->title; + } + + /** + * @return string Type (TYPE constant from a subclass) + */ + public function getType() { + return $this->type; + } + + /** + * @return int Priority. Higher means higher in the group list + */ + public function getPriority() { + return $this->priority; + } + + /** + * @return ChangesListFilter[] Associative array of ChangesListFilter objects, with + * filter name as key + */ + public function getFilters() { + return $this->filters; + } + + /** + * Get filter by name + * + * @param string $name Filter name + * @return ChangesListFilter|null Specified filter, or null if it is not registered + */ + public function getFilter( $name ) { + return isset( $this->filters[$name] ) ? $this->filters[$name] : null; + } + + /** + * Gets the JS data in the format required by the front-end of the structured UI + * + * @return array|null Associative array, or null if there are no filters that + * display in the structured UI. messageKeys is a special top-level value, with + * the value being an array of the message keys to send to the client. + */ + public function getJsData() { + $output = [ + 'name' => $this->name, + 'type' => $this->type, + 'fullCoverage' => $this->isFullCoverage, + 'filters' => [], + 'priority' => $this->priority, + 'conflicts' => [], + 'messageKeys' => [ $this->title ] + ]; + + if ( isset( $this->whatsThisHeader ) ) { + $output['whatsThisHeader'] = $this->whatsThisHeader; + $output['whatsThisBody'] = $this->whatsThisBody; + $output['whatsThisUrl'] = $this->whatsThisUrl; + $output['whatsThisLinkText'] = $this->whatsThisLinkText; + + array_push( + $output['messageKeys'], + $output['whatsThisHeader'], + $output['whatsThisBody'], + $output['whatsThisLinkText'] + ); + } + + usort( $this->filters, function ( $a, $b ) { + return $b->getPriority() - $a->getPriority(); + } ); + + foreach ( $this->filters as $filterName => $filter ) { + if ( $filter->displaysOnStructuredUi() ) { + $filterData = $filter->getJsData(); + $output['messageKeys'] = array_merge( + $output['messageKeys'], + $filterData['messageKeys'] + ); + unset( $filterData['messageKeys'] ); + $output['filters'][] = $filterData; + } + } + + if ( count( $output['filters'] ) === 0 ) { + return null; + } + + $output['title'] = $this->title; + + $conflicts = array_merge( + $this->conflictingGroups, + $this->conflictingFilters + ); + + foreach ( $conflicts as $conflictInfo ) { + unset( $conflictInfo['filterObject'] ); + unset( $conflictInfo['groupObject'] ); + $output['conflicts'][] = $conflictInfo; + array_push( + $output['messageKeys'], + $conflictInfo['globalDescription'], + $conflictInfo['contextDescription'] + ); + } + + return $output; + } + + /** + * Get groups conflicting with this filter group + * + * @return ChangesListFilterGroup[] + */ + public function getConflictingGroups() { + return array_map( + function ( $conflictDesc ) { + return $conflictDesc[ 'groupObject' ]; + }, + $this->conflictingGroups + ); + } + + /** + * Get filters conflicting with this filter group + * + * @return ChangesListFilter[] + */ + public function getConflictingFilters() { + return array_map( + function ( $conflictDesc ) { + return $conflictDesc[ 'filterObject' ]; + }, + $this->conflictingFilters + ); + } + + /** + * Check if any filter in this group is selected + * + * @param FormOptions $opts + * @return bool + */ + public function anySelected( FormOptions $opts ) { + return !!count( array_filter( + $this->getFilters(), + function ( ChangesListFilter $filter ) use ( $opts ) { + return $filter->isSelected( $opts ); + } + ) ); + } + + /** + * Modifies the query to include the filter group. + * + * The modification is only done if the filter group is in effect. This means that + * one or more valid and allowed filters were selected. + * + * @param IDatabase $dbr Database, for addQuotes, makeList, and similar + * @param ChangesListSpecialPage $specialPage Current special page + * @param array &$tables Array of tables; see IDatabase::select $table + * @param array &$fields Array of fields; see IDatabase::select $vars + * @param array &$conds Array of conditions; see IDatabase::select $conds + * @param array &$query_options Array of query options; see IDatabase::select $options + * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds + * @param FormOptions $opts Wrapper for the current request options and their defaults + * @param bool $isStructuredFiltersEnabled True if the Structured UI is currently enabled + */ + abstract public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage, + &$tables, &$fields, &$conds, &$query_options, &$join_conds, + FormOptions $opts, $isStructuredFiltersEnabled ); + + /** + * All the options represented by this filter group to $opts + * + * @param FormOptions $opts + * @param bool $allowDefaults + * @param bool $isStructuredFiltersEnabled + */ + abstract public function addOptions( FormOptions $opts, $allowDefaults, + $isStructuredFiltersEnabled ); +} |