summaryrefslogtreecommitdiff
path: root/www/wiki/includes/changes/ChangesListFilterGroup.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/includes/changes/ChangesListFilterGroup.php')
-rw-r--r--www/wiki/includes/changes/ChangesListFilterGroup.php473
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 );
+}