summaryrefslogtreecommitdiff
path: root/www/wiki/includes/changes/ChangesListFilter.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/includes/changes/ChangesListFilter.php')
-rw-r--r--www/wiki/includes/changes/ChangesListFilter.php508
1 files changed, 508 insertions, 0 deletions
diff --git a/www/wiki/includes/changes/ChangesListFilter.php b/www/wiki/includes/changes/ChangesListFilter.php
new file mode 100644
index 00000000..d1914536
--- /dev/null
+++ b/www/wiki/includes/changes/ChangesListFilter.php
@@ -0,0 +1,508 @@
+<?php
+/**
+ * Represents a filter (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
+ */
+
+/**
+ * Represents a filter (used on ChangesListSpecialPage and descendants)
+ *
+ * @since 1.29
+ */
+abstract class ChangesListFilter {
+ /**
+ * Filter name
+ *
+ * @var string $name
+ */
+ protected $name;
+
+ /**
+ * CSS class suffix used for attribution, e.g. 'bot'.
+ *
+ * In this example, if bot actions are included in the result set, this CSS class
+ * will then be included in all bot-flagged actions.
+ *
+ * @var string|null $cssClassSuffix
+ */
+ protected $cssClassSuffix;
+
+ /**
+ * Callable that returns true if and only if a row is attributed to this filter
+ *
+ * @var callable $isRowApplicableCallable
+ */
+ protected $isRowApplicableCallable;
+
+ /**
+ * Group. ChangesListFilterGroup this belongs to
+ *
+ * @var ChangesListFilterGroup $group
+ */
+ protected $group;
+
+ /**
+ * i18n key of label for structured UI
+ *
+ * @var string $label
+ */
+ protected $label;
+
+ /**
+ * i18n key of description for structured UI
+ *
+ * @var string $description
+ */
+ protected $description;
+
+ /**
+ * 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 = [];
+
+ /**
+ * Array of associative arrays with subset information
+ *
+ * @var array $subsetFilters
+ */
+ protected $subsetFilters = [];
+
+ /**
+ * Priority integer. Higher value means higher up in the group's filter list.
+ *
+ * @var string $priority
+ */
+ protected $priority;
+
+ /**
+ *
+ * @var string $defaultHighlightColor
+ */
+ protected $defaultHighlightColor;
+
+ const RESERVED_NAME_CHAR = '_';
+
+ /**
+ * Creates a new filter with the specified configuration, and registers it to the
+ * specified group.
+ *
+ * It infers which UI (it can be either or both) to display the filter on based on
+ * which messages are provided.
+ *
+ * If 'label' is provided, it will be displayed on the structured UI. Thus,
+ * 'label', 'description', and sub-class parameters are optional depending on which
+ * UI it's for.
+ *
+ * @param array $filterDefinition ChangesListFilter definition
+ * * $filterDefinition['name'] string Name of filter; use lowercase with no
+ * punctuation
+ * * $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark
+ * that a particular row belongs to this filter (when a row is included by the
+ * filter) (optional)
+ * * $filterDefinition['isRowApplicableCallable'] Callable taking two parameters, the
+ * IContextSource, and the RecentChange object for the row, and returning true if
+ * the row is attributed to this filter. The above CSS class will then be
+ * automatically added (optional, required if cssClassSuffix is used).
+ * * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this
+ * belongs to.
+ * * $filterDefinition['label'] string i18n key of label for structured UI.
+ * * $filterDefinition['description'] string i18n key of description for structured
+ * UI.
+ * * $filterDefinition['priority'] int Priority integer. Higher value means higher
+ * up in the group's filter list.
+ */
+ public function __construct( array $filterDefinition ) {
+ if ( isset( $filterDefinition['group'] ) ) {
+ $this->group = $filterDefinition['group'];
+ } else {
+ throw new MWException( 'You must use \'group\' to specify the ' .
+ 'ChangesListFilterGroup this filter belongs to' );
+ }
+
+ if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
+ throw new MWException( 'Filter names may not contain \'' .
+ self::RESERVED_NAME_CHAR .
+ '\'. Use the naming convention: \'lowercase\''
+ );
+ }
+
+ if ( $this->group->getFilter( $filterDefinition['name'] ) ) {
+ throw new MWException( 'Two filters in a group cannot have the ' .
+ "same name: '{$filterDefinition['name']}'" );
+ }
+
+ $this->name = $filterDefinition['name'];
+
+ if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
+ $this->cssClassSuffix = $filterDefinition['cssClassSuffix'];
+ $this->isRowApplicableCallable = $filterDefinition['isRowApplicableCallable'];
+ }
+
+ if ( isset( $filterDefinition['label'] ) ) {
+ $this->label = $filterDefinition['label'];
+ $this->description = $filterDefinition['description'];
+ }
+
+ $this->priority = $filterDefinition['priority'];
+
+ $this->group->registerFilter( $this );
+ }
+
+ /**
+ * 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' );
+ }
+ }
+
+ /**
+ * Marks that the current instance is (also) a superset of the filter passed in.
+ * This can be called more than once.
+ *
+ * This means that anything in the results for the other filter is also in the
+ * results for this one.
+ *
+ * @param ChangesListFilter $other The filter the current instance is a superset of
+ */
+ public function setAsSupersetOf( ChangesListFilter $other ) {
+ if ( $other->getGroup() !== $this->getGroup() ) {
+ throw new MWException( 'Supersets can only be defined for filters in the same group' );
+ }
+
+ $this->subsetFilters[] = [
+ // It's always the same group, but this makes the representation
+ // more consistent with conflicts.
+ 'group' => $other->getGroup()->getName(),
+ 'filter' => $other->getName(),
+ ];
+ }
+
+ /**
+ * @return string Name, e.g. hideanons
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @return ChangesListFilterGroup Group this belongs to
+ */
+ public function getGroup() {
+ return $this->group;
+ }
+
+ /**
+ * @return string i18n key of label for structured UI
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * @return string i18n key of description for structured UI
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * Checks whether the filter should display on the unstructured UI
+ *
+ * @return bool Whether to display
+ */
+ abstract public function displaysOnUnstructuredUi();
+
+ /**
+ * Checks whether the filter should display on the structured UI
+ * This refers to the exact filter. See also isFeatureAvailableOnStructuredUi.
+ *
+ * @return bool Whether to display
+ */
+ public function displaysOnStructuredUi() {
+ return $this->label !== null;
+ }
+
+ /**
+ * Checks whether an equivalent feature for this filter is available on the
+ * structured UI.
+ *
+ * This can either be the exact filter, or a new filter that replaces it.
+ * @return bool
+ */
+ public function isFeatureAvailableOnStructuredUi() {
+ return $this->displaysOnStructuredUi();
+ }
+
+ /**
+ * @return int Priority. Higher value means higher up in the group list
+ */
+ public function getPriority() {
+ return $this->priority;
+ }
+
+ /**
+ * Gets the CSS class
+ *
+ * @return string|null CSS class, or null if not defined
+ */
+ protected function getCssClass() {
+ if ( $this->cssClassSuffix !== null ) {
+ return ChangesList::CSS_CLASS_PREFIX . $this->cssClassSuffix;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Add CSS class if needed
+ *
+ * @param IContextSource $ctx Context source
+ * @param RecentChange $rc Recent changes object
+ * @param array &$classes Non-associative array of CSS class names; appended to if needed
+ */
+ public function applyCssClassIfNeeded( IContextSource $ctx, RecentChange $rc, array &$classes ) {
+ if ( $this->isRowApplicableCallable === null ) {
+ return;
+ }
+
+ if ( call_user_func( $this->isRowApplicableCallable, $ctx, $rc ) ) {
+ $classes[] = $this->getCssClass();
+ }
+ }
+
+ /**
+ * Gets the JS data required by the front-end of the structured UI
+ *
+ * @return array Associative array Data required by the front-end. 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->getName(),
+ 'label' => $this->getLabel(),
+ 'description' => $this->getDescription(),
+ 'cssClass' => $this->getCssClass(),
+ 'priority' => $this->priority,
+ 'subset' => $this->subsetFilters,
+ 'conflicts' => [],
+ 'defaultHighlightColor' => $this->defaultHighlightColor
+ ];
+
+ $output['messageKeys'] = [
+ $this->getLabel(),
+ $this->getDescription(),
+ ];
+
+ $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;
+ }
+
+ /**
+ * Checks whether this filter is selected in the provided options
+ *
+ * @param FormOptions $opts
+ * @return bool
+ */
+ abstract public function isSelected( FormOptions $opts );
+
+ /**
+ * Get groups conflicting with this filter
+ *
+ * @return ChangesListFilterGroup[]
+ */
+ public function getConflictingGroups() {
+ return array_map(
+ function ( $conflictDesc ) {
+ return $conflictDesc[ 'groupObject' ];
+ },
+ $this->conflictingGroups
+ );
+ }
+
+ /**
+ * Get filters conflicting with this filter
+ *
+ * @return ChangesListFilter[]
+ */
+ public function getConflictingFilters() {
+ return array_map(
+ function ( $conflictDesc ) {
+ return $conflictDesc[ 'filterObject' ];
+ },
+ $this->conflictingFilters
+ );
+ }
+
+ /**
+ * Check if the conflict with a group is currently "active"
+ *
+ * @param ChangesListFilterGroup $group
+ * @param FormOptions $opts
+ * @return bool
+ */
+ public function activelyInConflictWithGroup( ChangesListFilterGroup $group, FormOptions $opts ) {
+ if ( $group->anySelected( $opts ) && $this->isSelected( $opts ) ) {
+ /** @var ChangesListFilter $siblingFilter */
+ foreach ( $this->getSiblings() as $siblingFilter ) {
+ if ( $siblingFilter->isSelected( $opts ) && !$siblingFilter->hasConflictWithGroup( $group ) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private function hasConflictWithGroup( ChangesListFilterGroup $group ) {
+ return in_array( $group, $this->getConflictingGroups() );
+ }
+
+ /**
+ * Check if the conflict with a filter is currently "active"
+ *
+ * @param ChangesListFilter $filter
+ * @param FormOptions $opts
+ * @return bool
+ */
+ public function activelyInConflictWithFilter( ChangesListFilter $filter, FormOptions $opts ) {
+ if ( $this->isSelected( $opts ) && $filter->isSelected( $opts ) ) {
+ /** @var ChangesListFilter $siblingFilter */
+ foreach ( $this->getSiblings() as $siblingFilter ) {
+ if (
+ $siblingFilter->isSelected( $opts ) &&
+ !$siblingFilter->hasConflictWithFilter( $filter )
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private function hasConflictWithFilter( ChangesListFilter $filter ) {
+ return in_array( $filter, $this->getConflictingFilters() );
+ }
+
+ /**
+ * Get filters in the same group
+ *
+ * @return ChangesListFilter[]
+ */
+ protected function getSiblings() {
+ return array_filter(
+ $this->getGroup()->getFilters(),
+ function ( $filter ) {
+ return $filter !== $this;
+ }
+ );
+ }
+
+ /**
+ * @param string $defaultHighlightColor
+ */
+ public function setDefaultHighlightColor( $defaultHighlightColor ) {
+ $this->defaultHighlightColor = $defaultHighlightColor;
+ }
+}