summaryrefslogtreecommitdiff
path: root/www/wiki/includes/widget
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/widget
first commit
Diffstat (limited to 'www/wiki/includes/widget')
-rw-r--r--www/wiki/includes/widget/AUTHORS.txt13
-rw-r--r--www/wiki/includes/widget/ComplexNamespaceInputWidget.php117
-rw-r--r--www/wiki/includes/widget/ComplexTitleInputWidget.php66
-rw-r--r--www/wiki/includes/widget/DateInputWidget.php160
-rw-r--r--www/wiki/includes/widget/DateTimeInputWidget.php73
-rw-r--r--www/wiki/includes/widget/LICENSE.txt25
-rw-r--r--www/wiki/includes/widget/NamespaceInputWidget.php64
-rw-r--r--www/wiki/includes/widget/SearchInputWidget.php72
-rw-r--r--www/wiki/includes/widget/SelectWithInputWidget.php63
-rw-r--r--www/wiki/includes/widget/SizeFilterWidget.php75
-rw-r--r--www/wiki/includes/widget/TitleInputWidget.php79
-rw-r--r--www/wiki/includes/widget/UserInputWidget.php31
-rw-r--r--www/wiki/includes/widget/UsersMultiselectWidget.php66
-rw-r--r--www/wiki/includes/widget/search/BasicSearchResultSetWidget.php134
-rw-r--r--www/wiki/includes/widget/search/DidYouMeanWidget.php105
-rw-r--r--www/wiki/includes/widget/search/FullSearchResultWidget.php285
-rw-r--r--www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php190
-rw-r--r--www/wiki/includes/widget/search/InterwikiSearchResultWidget.php66
-rw-r--r--www/wiki/includes/widget/search/SearchFormWidget.php314
-rw-r--r--www/wiki/includes/widget/search/SearchResultSetWidget.php18
-rw-r--r--www/wiki/includes/widget/search/SearchResultWidget.php18
-rw-r--r--www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php133
-rw-r--r--www/wiki/includes/widget/search/SimpleSearchResultWidget.php63
23 files changed, 2230 insertions, 0 deletions
diff --git a/www/wiki/includes/widget/AUTHORS.txt b/www/wiki/includes/widget/AUTHORS.txt
new file mode 100644
index 00000000..2490b9d8
--- /dev/null
+++ b/www/wiki/includes/widget/AUTHORS.txt
@@ -0,0 +1,13 @@
+Authors (alphabetically)
+
+Alex Monk <krenair@wikimedia.org>
+Bartosz DziewoƄski <bdziewonski@wikimedia.org>
+Brad Jorsch <bjorsch@wikimedia.org>
+Ed Sanders <esanders@wikimedia.org>
+Florian Schmidt <florian.schmidt.welzow@t-online.de>
+Geoffrey Mon <geofbot@gmail.com>
+James D. Forrester <jforrester@wikimedia.org>
+Roan Kattouw <roan@wikimedia.org>
+Sucheta Ghoshal <sghoshal@wikimedia.org>
+Timo Tijhof <krinklemail@gmail.com>
+Trevor Parscal <trevor@wikimedia.org>
diff --git a/www/wiki/includes/widget/ComplexNamespaceInputWidget.php b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php
new file mode 100644
index 00000000..5f5d1cd1
--- /dev/null
+++ b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Namespace input widget. Displays a dropdown box with the choice of available namespaces, plus two
+ * checkboxes to include associated namespace or to invert selection.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class ComplexNamespaceInputWidget extends \OOUI\Widget {
+
+ protected $config;
+ protected $namespace;
+ protected $associated = null;
+ protected $associatedLabel = null;
+ protected $invert = null;
+ protected $invertLabel = null;
+
+ /**
+ * @param array $config Configuration options
+ * - array $config['namespace'] Configuration for the NamespaceInputWidget
+ * dropdown with list of namespaces
+ * - string $config['namespace']['includeAllValue'] If specified,
+ * add an "all namespaces" option to the dropdown, and use this as the input value for it
+ * - array|null $config['invert'] Configuration for the "invert selection"
+ * CheckboxInputWidget. If null, the checkbox will not be generated.
+ * - array|null $config['associated'] Configuration for the "include associated namespace"
+ * CheckboxInputWidget. If null, the checkbox will not be generated.
+ * - array $config['invertLabel'] Configuration for the FieldLayout with label
+ * wrapping the "invert selection" checkbox
+ * - string $config['invertLabel']['label'] Label text for the label
+ * - array $config['associatedLabel'] Configuration for the FieldLayout with label
+ * wrapping the "include associated namespace" checkbox
+ * - string $config['associatedLabel']['label'] Label text for the label
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config = array_merge(
+ [
+ // Config options for nested widgets
+ 'namespace' => [],
+ 'invert' => [],
+ 'invertLabel' => [],
+ 'associated' => [],
+ 'associatedLabel' => [],
+ ],
+ $config
+ );
+
+ parent::__construct( $config );
+
+ // Properties
+ $this->config = $config;
+
+ $this->namespace = new NamespaceInputWidget( $config['namespace'] );
+ if ( $config['associated'] !== null ) {
+ $this->associated = new \OOUI\CheckboxInputWidget( array_merge(
+ [ 'value' => '1' ],
+ $config['associated']
+ ) );
+ // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet
+ $this->associatedLabel = new \OOUI\FieldLayout(
+ $this->associated,
+ array_merge(
+ [ 'align' => 'inline' ],
+ $config['associatedLabel']
+ )
+ );
+ }
+ if ( $config['invert'] !== null ) {
+ $this->invert = new \OOUI\CheckboxInputWidget( array_merge(
+ [ 'value' => '1' ],
+ $config['invert']
+ ) );
+ // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet
+ $this->invertLabel = new \OOUI\FieldLayout(
+ $this->invert,
+ array_merge(
+ [ 'align' => 'inline' ],
+ $config['invertLabel']
+ )
+ );
+ }
+
+ // Initialization
+ $this
+ ->addClasses( [ 'mw-widget-complexNamespaceInputWidget' ] )
+ ->appendContent( $this->namespace, $this->associatedLabel, $this->invertLabel );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.ComplexNamespaceInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config = array_merge(
+ $config,
+ array_intersect_key(
+ $this->config,
+ array_fill_keys(
+ [
+ 'namespace',
+ 'invert',
+ 'invertLabel',
+ 'associated',
+ 'associatedLabel'
+ ],
+ true
+ )
+ )
+ );
+ $config['namespace']['dropdown']['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/ComplexTitleInputWidget.php b/www/wiki/includes/widget/ComplexTitleInputWidget.php
new file mode 100644
index 00000000..ca6c8484
--- /dev/null
+++ b/www/wiki/includes/widget/ComplexTitleInputWidget.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Complex title input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class ComplexTitleInputWidget extends \OOUI\Widget {
+
+ protected $namespace = null;
+ protected $title = null;
+
+ /**
+ * Like TitleInputWidget, but the namespace has to be input through a separate dropdown field.
+ *
+ * @param array $config Configuration options
+ * - array $config['namespace'] Configuration for the NamespaceInputWidget dropdown
+ * with list of namespaces
+ * - array $config['title'] Configuration for the TitleInputWidget text field
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config = array_merge(
+ [
+ 'namespace' => [],
+ 'title' => [],
+ ],
+ $config
+ );
+
+ parent::__construct( $config );
+
+ // Properties
+ $this->config = $config;
+ $this->namespace = new NamespaceInputWidget( $config['namespace'] );
+ $this->title = new TitleInputWidget( array_merge(
+ $config['title'],
+ [
+ 'relative' => true,
+ 'namespace' => isset( $config['namespace']['value'] ) ?
+ $config['namespace']['value'] :
+ null,
+ ]
+ ) );
+
+ // Initialization
+ $this
+ ->addClasses( [ 'mw-widget-complexTitleInputWidget' ] )
+ ->appendContent( $this->namespace, $this->title );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.ComplexTitleInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['namespace'] = $this->config['namespace'];
+ $config['namespace']['dropdown']['$overlay'] = true;
+ $config['title'] = $this->config['title'];
+ $config['title']['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/DateInputWidget.php b/www/wiki/includes/widget/DateInputWidget.php
new file mode 100644
index 00000000..975f8e9c
--- /dev/null
+++ b/www/wiki/includes/widget/DateInputWidget.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use DateTime;
+
+/**
+ * Date input widget.
+ *
+ * @since 1.29
+ * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class DateInputWidget extends \OOUI\TextInputWidget {
+
+ protected $inputFormat = null;
+ protected $displayFormat = null;
+ protected $longDisplayFormat = null;
+ protected $placeholderLabel = null;
+ protected $placeholderDateFormat = null;
+ protected $precision = null;
+ protected $mustBeAfter = null;
+ protected $mustBeBefore = null;
+
+ /**
+ * @param array $config Configuration options
+ * - string $config['inputFormat'] Date format string to use for the textual input field.
+ * Displayed while the widget is active, and the user can type in a date in this format.
+ * Should be short and easy to type. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
+ * `precision`)
+ * - string $config['displayFormat'] Date format string to use for the clickable label.
+ * while the widget is inactive. Should be as unambiguous as possible (for example, prefer
+ * to spell out the month, rather than rely on the order), even if that makes it longer.
+ * Applicable only if the widget is infused. (default: language-specific)
+ * - string $config['longDisplayFormat'] If a custom displayFormat is not specified, use
+ * unabbreviated day of the week and month names in the default language-specific
+ * displayFormat. (default: false)
+ * - string $config['placeholderLabel'] Placeholder text shown when the widget is not
+ * selected. Applicable only if the widget is infused. (default: taken from message
+ * `mw-widgets-dateinput-no-date`)
+ * - string $config['placeholderDateFormat'] User-visible date format string displayed
+ * in the textual input field when it's empty. Should be the same as `inputFormat`, but
+ * translated to the user's language. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
+ * `precision`)
+ * - string $config['precision'] Date precision to use, 'day' or 'month' (default: 'day')
+ * - string $config['mustBeAfter'] Validates the date to be after this.
+ * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
+ * - string $config['mustBeBefore'] Validates the date to be before this.
+ * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
+ */
+ public function __construct( array $config = [] ) {
+ $config = array_merge( [
+ // Default config values
+ 'precision' => 'day',
+ 'longDisplayFormat' => false,
+ ], $config );
+
+ // Properties
+ if ( isset( $config['inputFormat'] ) ) {
+ $this->inputFormat = $config['inputFormat'];
+ }
+ if ( isset( $config['placeholderDateFormat'] ) ) {
+ $this->placeholderDateFormat = $config['placeholderDateFormat'];
+ }
+ $this->precision = $config['precision'];
+ if ( isset( $config['mustBeAfter'] ) ) {
+ $this->mustBeAfter = $config['mustBeAfter'];
+ }
+ if ( isset( $config['mustBeBefore'] ) ) {
+ $this->mustBeBefore = $config['mustBeBefore'];
+ }
+
+ // Properties stored for the infused JS widget
+ if ( isset( $config['displayFormat'] ) ) {
+ $this->displayFormat = $config['displayFormat'];
+ }
+ if ( isset( $config['longDisplayFormat'] ) ) {
+ $this->longDisplayFormat = $config['longDisplayFormat'];
+ }
+ if ( isset( $config['placeholderLabel'] ) ) {
+ $this->placeholderLabel = $config['placeholderLabel'];
+ }
+
+ // Set up placeholder text visible if the browser doesn't override it (logic taken from JS)
+ if ( $this->placeholderDateFormat !== null ) {
+ $placeholder = $this->placeholderDateFormat;
+ } elseif ( $this->inputFormat !== null ) {
+ // We have no way to display a translated placeholder for custom formats
+ $placeholder = '';
+ } else {
+ $placeholder = wfMessage( "mw-widgets-dateinput-placeholder-$this->precision" )->text();
+ }
+
+ $config = array_merge( [
+ // Processed config values
+ 'placeholder' => $placeholder,
+ ], $config );
+
+ parent::__construct( $config );
+
+ // Calculate min/max attributes (which are skipped by TextInputWidget) and add to <input>
+ // min/max attributes are inclusive, but mustBeAfter/Before are exclusive
+ if ( $this->mustBeAfter !== null ) {
+ $min = new DateTime( $this->mustBeAfter );
+ $min = $min->modify( '+1 day' );
+ $min = $min->format( 'Y-m-d' );
+ $this->input->setAttributes( [ 'min' => $min ] );
+ }
+ if ( $this->mustBeBefore !== null ) {
+ $max = new DateTime( $this->mustBeBefore );
+ $max = $max->modify( '-1 day' );
+ $max = $max->format( 'Y-m-d' );
+ $this->input->setAttributes( [ 'max' => $max ] );
+ }
+
+ // Initialization
+ $this->addClasses( [ 'mw-widget-dateInputWidget' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.DateInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ if ( $this->inputFormat !== null ) {
+ $config['inputFormat'] = $this->inputFormat;
+ }
+ if ( $this->displayFormat !== null ) {
+ $config['displayFormat'] = $this->displayFormat;
+ }
+ if ( $this->longDisplayFormat !== null ) {
+ $config['longDisplayFormat'] = $this->longDisplayFormat;
+ }
+ if ( $this->placeholderLabel !== null ) {
+ $config['placeholderLabel'] = $this->placeholderLabel;
+ }
+ if ( $this->placeholderDateFormat !== null ) {
+ $config['placeholderDateFormat'] = $this->placeholderDateFormat;
+ }
+ if ( $this->precision !== null ) {
+ $config['precision'] = $this->precision;
+ }
+ if ( $this->mustBeAfter !== null ) {
+ $config['mustBeAfter'] = $this->mustBeAfter;
+ }
+ if ( $this->mustBeBefore !== null ) {
+ $config['mustBeBefore'] = $this->mustBeBefore;
+ }
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+
+ public function getInputElement( $config ) {
+ // Inserts date/month type attribute
+ return parent::getInputElement( $config )
+ ->setAttributes( [
+ 'type' => ( $config['precision'] === 'month' ) ? 'month' : 'date'
+ ] );
+ }
+}
diff --git a/www/wiki/includes/widget/DateTimeInputWidget.php b/www/wiki/includes/widget/DateTimeInputWidget.php
new file mode 100644
index 00000000..21e3d793
--- /dev/null
+++ b/www/wiki/includes/widget/DateTimeInputWidget.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use OOUI\Tag;
+
+/**
+ * Date-time input widget.
+ *
+ * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class DateTimeInputWidget extends \OOUI\InputWidget {
+
+ protected $type = null;
+ protected $min = null;
+ protected $max = null;
+ protected $clearable = null;
+
+ /**
+ * @param array $config Configuration options
+ * - string $config['type'] 'date', 'time', or 'datetime'
+ * - string $config['min'] Minimum date, time, or datetime
+ * - string $config['max'] Maximum date, time, or datetime
+ * - bool $config['clearable'] Whether to provide for blanking the value.
+ */
+ public function __construct( array $config = [] ) {
+ // We need $this->type set before calling the parent constructor
+ if ( isset( $config['type'] ) ) {
+ $this->type = $config['type'];
+ } else {
+ throw new \InvalidArgumentException( '$config[\'type\'] must be specified' );
+ }
+
+ parent::__construct( $config );
+
+ // Properties, which are ignored in PHP and just shipped back to JS
+ if ( isset( $config['min'] ) ) {
+ $this->min = $config['min'];
+ }
+ if ( isset( $config['max'] ) ) {
+ $this->max = $config['max'];
+ }
+ if ( isset( $config['clearable'] ) ) {
+ $this->clearable = $config['clearable'];
+ }
+
+ // Initialization
+ $this->addClasses( [ 'mw-widgets-datetime-dateTimeInputWidget' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.datetime.DateTimeInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['type'] = $this->type;
+ if ( $this->min !== null ) {
+ $config['min'] = $this->min;
+ }
+ if ( $this->max !== null ) {
+ $config['max'] = $this->max;
+ }
+ if ( $this->clearable !== null ) {
+ $config['clearable'] = $this->clearable;
+ }
+ return parent::getConfig( $config );
+ }
+
+ protected function getInputElement( $config ) {
+ return ( new Tag( 'input' ) )->setAttributes( [ 'type' => $this->type ] );
+ }
+}
diff --git a/www/wiki/includes/widget/LICENSE.txt b/www/wiki/includes/widget/LICENSE.txt
new file mode 100644
index 00000000..b03ca801
--- /dev/null
+++ b/www/wiki/includes/widget/LICENSE.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2011-2015 MediaWiki Widgets Team and others under the
+terms of The MIT License (MIT), as follows:
+
+This software consists of voluntary contributions made by many
+individuals (AUTHORS.txt) For exact contribution history, see the
+revision history and logs, available at https://gerrit.wikimedia.org
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/www/wiki/includes/widget/NamespaceInputWidget.php b/www/wiki/includes/widget/NamespaceInputWidget.php
new file mode 100644
index 00000000..0840886a
--- /dev/null
+++ b/www/wiki/includes/widget/NamespaceInputWidget.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Namespace input widget. Displays a dropdown box with the choice of available namespaces.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class NamespaceInputWidget extends \OOUI\DropdownInputWidget {
+
+ protected $includeAllValue = null;
+
+ /**
+ * @param array $config Configuration options
+ * - string $config['includeAllValue'] If specified, add a "all namespaces" option to the
+ * namespace dropdown, and use this as the input value for it
+ * - int[] $config['exclude'] List of namespace numbers to exclude from the selector
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config['options'] = $this->getNamespaceDropdownOptions( $config );
+
+ parent::__construct( $config );
+
+ // Properties
+ $this->includeAllValue = isset( $config['includeAllValue'] ) ? $config['includeAllValue'] : null;
+ $this->exclude = isset( $config['exclude'] ) ? $config['exclude'] : [];
+
+ // Initialization
+ $this->addClasses( [ 'mw-widget-namespaceInputWidget' ] );
+ }
+
+ protected function getNamespaceDropdownOptions( array $config ) {
+ $namespaceOptionsParams = [
+ 'all' => isset( $config['includeAllValue'] ) ? $config['includeAllValue'] : null,
+ 'exclude' => isset( $config['exclude'] ) ? $config['exclude'] : null
+ ];
+ $namespaceOptions = \Html::namespaceSelectorOptions( $namespaceOptionsParams );
+
+ $options = [];
+ foreach ( $namespaceOptions as $id => $name ) {
+ $options[] = [
+ 'data' => (string)$id,
+ 'label' => $name,
+ ];
+ }
+
+ return $options;
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.NamespaceInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['includeAllValue'] = $this->includeAllValue;
+ $config['exclude'] = $this->exclude;
+ // Skip DropdownInputWidget's getConfig(), we don't need 'options' config
+ $config['dropdown']['$overlay'] = true;
+ return \OOUI\InputWidget::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/SearchInputWidget.php b/www/wiki/includes/widget/SearchInputWidget.php
new file mode 100644
index 00000000..6fed7942
--- /dev/null
+++ b/www/wiki/includes/widget/SearchInputWidget.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Search input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class SearchInputWidget extends TitleInputWidget {
+
+ protected $pushPending = false;
+ protected $performSearchOnClick = true;
+ protected $validateTitle = false;
+ protected $highlightFirst = false;
+ protected $dataLocation = 'header';
+
+ /**
+ * @param array $config Configuration options
+ * - int|null $config['pushPending'] Whether the input should be visually marked as
+ * "pending", while requesting suggestions (default: false)
+ * - bool|null $config['performSearchOnClick'] If true, the script will start a search
+ * whenever a user hits a suggestion. If false, the text of the suggestion is inserted into
+ * the text field only (default: true)
+ * - string $config['dataLocation'] Where the search input field will be
+ * used (header or content, default: header)
+ */
+ public function __construct( array $config = [] ) {
+ $config = array_merge( [
+ 'maxLength' => null,
+ 'icon' => 'search',
+ ], $config );
+
+ parent::__construct( $config );
+
+ // Properties, which are ignored in PHP and just shipped back to JS
+ if ( isset( $config['pushPending'] ) ) {
+ $this->pushPending = $config['pushPending'];
+ }
+
+ if ( isset( $config['performSearchOnClick'] ) ) {
+ $this->performSearchOnClick = $config['performSearchOnClick'];
+ }
+
+ if ( isset( $config['dataLocation'] ) ) {
+ // identifies the location of the search bar for tracking purposes
+ $this->dataLocation = $config['dataLocation'];
+ }
+
+ // Initialization
+ $this->addClasses( [ 'mw-widget-searchInputWidget' ] );
+ }
+
+ protected function getInputElement( $config ) {
+ return ( new \OOUI\Tag( 'input' ) )->setAttributes( [ 'type' => 'search' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.SearchInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['pushPending'] = $this->pushPending;
+ $config['performSearchOnClick'] = $this->performSearchOnClick;
+ if ( $this->dataLocation ) {
+ $config['dataLocation'] = $this->dataLocation;
+ }
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/SelectWithInputWidget.php b/www/wiki/includes/widget/SelectWithInputWidget.php
new file mode 100644
index 00000000..5ceed4c4
--- /dev/null
+++ b/www/wiki/includes/widget/SelectWithInputWidget.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use OOUI\DropdownInputWidget;
+use OOUI\TextInputWidget;
+
+/**
+ * Select and input widget.
+ *
+ * @copyright 2011-2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class SelectWithInputWidget extends \OOUI\Widget {
+
+ protected $textinput = null;
+ protected $dropdowninput = null;
+
+ /**
+ * A version of the SelectWithInputWidget, with `or` set to true.
+ *
+ * @param array $config Configuration options
+ * - array $config['textinput'] Configuration for the TextInputWidget
+ * - array $config['dropdowninput'] Configuration for the DropdownInputWidget
+ * - bool $config['or'] Configuration for whether the widget is dropdown AND input
+ * or dropdown OR input
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config = array_merge(
+ [
+ 'textinput' => [],
+ 'dropdowninput' => [],
+ 'or' => false
+ ],
+ $config
+ );
+
+ parent::__construct( $config );
+
+ // Properties
+ $this->config = $config;
+ $this->textinput = new TextInputWidget( $config['textinput'] );
+ $this->dropdowninput = new DropdownInputWidget( $config['dropdowninput'] );
+
+ // Initialization
+ $this
+ ->addClasses( [ 'mw-widget-selectWithInputWidget' ] )
+ ->appendContent( $this->dropdowninput, $this->textinput );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.SelectWithInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['textinput'] = $this->config['textinput'];
+ $config['dropdowninput'] = $this->config['dropdowninput'];
+ $config['dropdowninput']['dropdown']['$overlay'] = true;
+ $config['or'] = $this->config['or'];
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/SizeFilterWidget.php b/www/wiki/includes/widget/SizeFilterWidget.php
new file mode 100644
index 00000000..c4d1dfc8
--- /dev/null
+++ b/www/wiki/includes/widget/SizeFilterWidget.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use \OOUI\RadioSelectInputWidget;
+use \OOUI\TextInputWidget;
+use \OOUI\LabelWidget;
+
+/**
+ * Select and input widget.
+ *
+ * @copyright 2011-2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+class SizeFilterWidget extends \OOUI\Widget {
+
+ protected $radioselectinput = null;
+ protected $textinput = null;
+
+ /**
+ * RadioSelectInputWidget and a TextInputWidget to set minimum or maximum byte size
+ *
+ * @param array $config Configuration options
+ * - array $config['textinput'] Configuration for the TextInputWidget
+ * - array $config['radioselectinput'] Configuration for the RadioSelectWidget
+ * - bool $congif['selectMin'] Whether to select 'min', false would select 'max'
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config = array_merge( [
+ 'selectMin' => true,
+ 'textinput' => [],
+ 'radioselectinput' => []
+ ], $config );
+ $config['textinput'] = array_merge( [
+ 'type' => 'number'
+ ], $config['textinput'] );
+ $config['radioselectinput'] = array_merge( [ 'options' => [
+ [
+ 'data' => 'min',
+ 'label' => wfMessage( 'minimum-size' )->text()
+ ],
+ [
+ 'data' => 'max',
+ 'label' => wfMessage( 'maximum-size' )->text()
+ ]
+ ] ], $config['radioselectinput'] );
+
+ // Parent constructor
+ parent::__construct( $config );
+
+ // Properties
+ $this->config = $config;
+ $this->radioselectinput = new RadioSelectInputWidget( $config[ 'radioselectinput'] );
+ $this->textinput = new TextInputWidget( $config[ 'textinput' ] );
+ $this->label = new LabelWidget( [ 'label' => wfMessage( 'pagesize' )->text() ] );
+
+ // Initialization
+ $this->radioselectinput->setValue( $config[ 'selectMin' ] ? 'min' : 'max' );
+ $this
+ ->addClasses( [ 'mw-widget-sizeFilterWidget' ] )
+ ->appendContent( $this->radioselectinput, $this->textinput, $this->label );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.SizeFilterWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['textinput'] = $this->config['textinput'];
+ $config['radioselectinput'] = $this->config['radioselectinput'];
+ $config['selectMin'] = $this->config['selectMin'];
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/TitleInputWidget.php b/www/wiki/includes/widget/TitleInputWidget.php
new file mode 100644
index 00000000..db1ea0b2
--- /dev/null
+++ b/www/wiki/includes/widget/TitleInputWidget.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Title input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class TitleInputWidget extends \OOUI\TextInputWidget {
+
+ protected $namespace = null;
+ protected $relative = null;
+ protected $suggestions = null;
+ protected $highlightFirst = null;
+ protected $validateTitle = null;
+
+ /**
+ * @param array $config Configuration options
+ * - int|null $config['namespace'] Namespace to prepend to queries
+ * - bool|null $config['relative'] If a namespace is set,
+ * return a title relative to it (default: true)
+ * - bool|null $config['suggestions'] Display search suggestions (default: true)
+ * - bool|null $config['highlightFirst'] Automatically highlight
+ * the first result (default: true)
+ * - bool|null $config['validateTitle'] Whether the input must
+ * be a valid title (default: true)
+ */
+ public function __construct( array $config = [] ) {
+ parent::__construct(
+ array_merge( [ 'maxLength' => 255 ], $config )
+ );
+
+ // Properties, which are ignored in PHP and just shipped back to JS
+ if ( isset( $config['namespace'] ) ) {
+ $this->namespace = $config['namespace'];
+ }
+ if ( isset( $config['relative'] ) ) {
+ $this->relative = $config['relative'];
+ }
+ if ( isset( $config['suggestions'] ) ) {
+ $this->suggestions = $config['suggestions'];
+ }
+ if ( isset( $config['highlightFirst'] ) ) {
+ $this->highlightFirst = $config['highlightFirst'];
+ }
+ if ( isset( $config['validateTitle'] ) ) {
+ $this->validateTitle = $config['validateTitle'];
+ }
+
+ // Initialization
+ $this->addClasses( [ 'mw-widget-titleInputWidget' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.TitleInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ if ( $this->namespace !== null ) {
+ $config['namespace'] = $this->namespace;
+ }
+ if ( $this->relative !== null ) {
+ $config['relative'] = $this->relative;
+ }
+ if ( $this->suggestions !== null ) {
+ $config['suggestions'] = $this->suggestions;
+ }
+ if ( $this->highlightFirst !== null ) {
+ $config['highlightFirst'] = $this->highlightFirst;
+ }
+ if ( $this->validateTitle !== null ) {
+ $config['validateTitle'] = $this->validateTitle;
+ }
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/UserInputWidget.php b/www/wiki/includes/widget/UserInputWidget.php
new file mode 100644
index 00000000..36f63c15
--- /dev/null
+++ b/www/wiki/includes/widget/UserInputWidget.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * User input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class UserInputWidget extends \OOUI\TextInputWidget {
+
+ /**
+ * @param array $config Configuration options
+ */
+ public function __construct( array $config = [] ) {
+ parent::__construct( $config );
+
+ // Initialization
+ $this->addClasses( [ 'mw-widget-userInputWidget' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.UserInputWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/UsersMultiselectWidget.php b/www/wiki/includes/widget/UsersMultiselectWidget.php
new file mode 100644
index 00000000..68cdad66
--- /dev/null
+++ b/www/wiki/includes/widget/UsersMultiselectWidget.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use OOUI\MultilineTextInputWidget;
+
+/**
+ * Widget to select multiple users.
+ *
+ * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class UsersMultiselectWidget extends \OOUI\Widget {
+
+ protected $usersArray = [];
+ protected $inputName = null;
+ protected $inputPlaceholder = null;
+
+ /**
+ * @param array $config Configuration options
+ * - array $config['users'] Array of usernames to use as preset data
+ * - array $config['placeholder'] Placeholder message for input
+ * - array $config['name'] Name attribute (used in forms)
+ */
+ public function __construct( array $config = [] ) {
+ parent::__construct( $config );
+
+ // Properties
+ if ( isset( $config['default'] ) ) {
+ $this->usersArray = $config['default'];
+ }
+ if ( isset( $config['name'] ) ) {
+ $this->inputName = $config['name'];
+ }
+ if ( isset( $config['placeholder'] ) ) {
+ $this->inputPlaceholder = $config['placeholder'];
+ }
+
+ $textarea = new MultilineTextInputWidget( [
+ 'name' => $this->inputName,
+ 'value' => implode( "\n", $this->usersArray ),
+ 'rows' => 25,
+ ] );
+ $this->prependContent( $textarea );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.UsersMultiselectWidget';
+ }
+
+ public function getConfig( &$config ) {
+ if ( $this->usersArray !== null ) {
+ $config['selected'] = $this->usersArray;
+ }
+ if ( $this->inputName !== null ) {
+ $config['name'] = $this->inputName;
+ }
+ if ( $this->inputPlaceholder !== null ) {
+ $config['placeholder'] = $this->inputPlaceholder;
+ }
+
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+
+}
diff --git a/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php
new file mode 100644
index 00000000..e2366405
--- /dev/null
+++ b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use Message;
+use SearchResultSet;
+use SpecialSearch;
+use Status;
+
+/**
+ * Renders the search result area. Handles Title and Full-Text search results,
+ * along with inline and sidebar secondary (interwiki) results.
+ */
+class BasicSearchResultSetWidget {
+ /** @var SpecialSearch */
+ protected $specialPage;
+ /** @var SearchResultWidget */
+ protected $resultWidget;
+ /** @var InterwikiSearchResultSetWidget */
+ protected $sidebarWidget;
+
+ public function __construct(
+ SpecialSearch $specialPage,
+ SearchResultWidget $resultWidget,
+ SearchResultSetWidget $sidebarWidget
+ ) {
+ $this->specialPage = $specialPage;
+ $this->resultWidget = $resultWidget;
+ $this->sidebarWidget = $sidebarWidget;
+ }
+
+ /**
+ * @param string $term The search term to highlight
+ * @param int $offset The offset of the first result in the result set
+ * @param SearchResultSet|null $titleResultSet Results of searching only page titles
+ * @param SearchResultSet|null $textResultSet Results of general full text search.
+ * @return string HTML
+ */
+ public function render(
+ $term,
+ $offset,
+ SearchResultSet $titleResultSet = null,
+ SearchResultSet $textResultSet = null
+ ) {
+ global $wgContLang;
+
+ $hasTitle = $titleResultSet ? $titleResultSet->numRows() > 0 : false;
+ $hasText = $textResultSet ? $textResultSet->numRows() > 0 : false;
+ $hasSecondary = $textResultSet
+ ? $textResultSet->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS )
+ : false;
+ $hasSecondaryInline = $textResultSet
+ ? $textResultSet->hasInterwikiResults( SearchResultSet::INLINE_RESULTS )
+ : false;
+
+ if ( !$hasTitle && !$hasText && !$hasSecondary && !$hasSecondaryInline ) {
+ return '';
+ }
+
+ $out = '';
+ if ( $hasTitle ) {
+ $out .= $this->header( $this->specialPage->msg( 'titlematches' ) )
+ . $this->renderResultSet( $titleResultSet, $offset );
+ }
+
+ if ( $hasText ) {
+ if ( $hasTitle ) {
+ $out .= "<div class='mw-search-visualclear'></div>" .
+ $this->header( $this->specialPage->msg( 'textmatches' ) );
+ }
+ $out .= $this->renderResultSet( $textResultSet, $offset );
+ }
+
+ if ( $hasSecondaryInline ) {
+ $iwResults = $textResultSet->getInterwikiResults( SearchResultSet::INLINE_RESULTS );
+ foreach ( $iwResults as $interwiki => $results ) {
+ if ( $results instanceof Status || $results->numRows() === 0 ) {
+ // ignore bad interwikis for now
+ continue;
+ }
+ $out .=
+ "<h2 class='mw-search-interwiki-header mw-search-visualclear'>" .
+ $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() .
+ "</h2>";
+ $out .= $this->renderResultSet( $results, $offset );
+ }
+ }
+
+ if ( $hasSecondary ) {
+ $out .= $this->sidebarWidget->render(
+ $term,
+ $textResultSet->getInterwikiResults( SearchResultSet::SECONDARY_RESULTS )
+ );
+ }
+
+ // Convert the whole thing to desired language variant
+ // TODO: Move this up to Special:Search?
+ return $wgContLang->convert( $out );
+ }
+
+ /**
+ * Generate a headline for a section of the search results. In prior
+ * implementations this was rendering wikitext of '==$1==', but seems
+ * a waste to call the full parser to generate this tiny bit of html
+ *
+ * @param Message $msg i18n message to use as header
+ * @return string HTML
+ */
+ protected function header( Message $msg ) {
+ return "<h2>" .
+ "<span class='mw-headline'>" . $msg->escaped() . "</span>" .
+ "</h2>";
+ }
+
+ /**
+ * @param SearchResultSet $resultSet The search results to render
+ * @param int $offset Offset of the first result in $resultSet
+ * @return string HTML
+ */
+ protected function renderResultSet( SearchResultSet $resultSet, $offset ) {
+ global $wgContLang;
+
+ $terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() );
+
+ $hits = [];
+ $result = $resultSet->next();
+ while ( $result ) {
+ $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ );
+ $result = $resultSet->next();
+ }
+
+ return "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>";
+ }
+}
diff --git a/www/wiki/includes/widget/search/DidYouMeanWidget.php b/www/wiki/includes/widget/search/DidYouMeanWidget.php
new file mode 100644
index 00000000..4e5b76b6
--- /dev/null
+++ b/www/wiki/includes/widget/search/DidYouMeanWidget.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use HtmlArmor;
+use SearchResultSet;
+use SpecialSearch;
+
+/**
+ * Renders a suggested search for the user, or tells the user
+ * a suggested search was run instead of the one provided.
+ */
+class DidYouMeanWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+
+ public function __construct( SpecialSearch $specialSearch ) {
+ $this->specialSearch = $specialSearch;
+ }
+
+ /**
+ * @param string $term The user provided search term
+ * @param SearchResultSet $resultSet
+ * @return string HTML
+ */
+ public function render( $term, SearchResultSet $resultSet ) {
+ if ( $resultSet->hasRewrittenQuery() ) {
+ $html = $this->rewrittenHtml( $term, $resultSet );
+ } elseif ( $resultSet->hasSuggestion() ) {
+ $html = $this->suggestionHtml( $resultSet );
+ } else {
+ return '';
+ }
+
+ return "<div class='searchdidyoumean'>$html</div>";
+ }
+
+ /**
+ * Generates HTML shown to user when their query has been internally
+ * rewritten, and the results of the rewritten query are being returned.
+ *
+ * @param string $term The users search input
+ * @param SearchResultSet $resultSet The response to the search request
+ * @return string HTML Links the user to their original $term query, and the
+ * one suggested by $resultSet
+ */
+ protected function rewrittenHtml( $term, SearchResultSet $resultSet ) {
+ $params = [
+ 'search' => $resultSet->getQueryAfterRewrite(),
+ // Don't magic this link into a 'go' link, it should always
+ // show search results.
+ 'fultext' => 1,
+ ];
+ $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() );
+
+ $linkRenderer = $this->specialSearch->getLinkRenderer();
+ $snippet = $resultSet->getQueryAfterRewriteSnippet();
+ $rewritten = $linkRenderer->makeKnownLink(
+ $this->specialSearch->getPageTitle(),
+ $snippet ? new HtmlArmor( $snippet ) : null,
+ [ 'id' => 'mw-search-DYM-rewritten' ],
+ $stParams
+ );
+
+ $stParams['search'] = $term;
+ $stParams['runsuggestion'] = 0;
+ $original = $linkRenderer->makeKnownLink(
+ $this->specialSearch->getPageTitle(),
+ $term,
+ [ 'id' => 'mwsearch-DYM-original' ],
+ $stParams
+ );
+
+ return $this->specialSearch->msg( 'search-rewritten' )
+ ->rawParams( $rewritten, $original )
+ ->escaped();
+ }
+
+ /**
+ * Generates HTML shown to the user when we have a suggestion about
+ * a query that might give more/better results than their current
+ * query.
+ *
+ * @param SearchResultSet $resultSet
+ * @return string HTML
+ */
+ protected function suggestionHtml( SearchResultSet $resultSet ) {
+ $params = [
+ 'search' => $resultSet->getSuggestionQuery(),
+ 'fulltext' => 1,
+ ];
+ $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() );
+
+ $snippet = $resultSet->getSuggestionSnippet();
+ $suggest = $this->specialSearch->getLinkRenderer()->makeKnownLink(
+ $this->specialSearch->getPageTitle(),
+ $snippet ? new HtmlArmor( $snippet ) : null,
+ [ 'id' => 'mw-search-DYM-suggestion' ],
+ $stParams
+ );
+
+ return $this->specialSearch->msg( 'search-suggest' )
+ ->rawParams( $suggest )->parse();
+ }
+}
diff --git a/www/wiki/includes/widget/search/FullSearchResultWidget.php b/www/wiki/includes/widget/search/FullSearchResultWidget.php
new file mode 100644
index 00000000..af1e0275
--- /dev/null
+++ b/www/wiki/includes/widget/search/FullSearchResultWidget.php
@@ -0,0 +1,285 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use Category;
+use Hooks;
+use HtmlArmor;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResult;
+use SpecialSearch;
+use Title;
+
+/**
+ * Renders a 'full' multi-line search result with metadata.
+ *
+ * The Title
+ * some *highlighted* *text* about the search result
+ * 5KB (651 words) - 12:40, 6 Aug 2016
+ */
+class FullSearchResultWidget implements SearchResultWidget {
+ /** @var SpecialSearch */
+ protected $specialPage;
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+
+ public function __construct( SpecialSearch $specialPage, LinkRenderer $linkRenderer ) {
+ $this->specialPage = $specialPage;
+ $this->linkRenderer = $linkRenderer;
+ }
+
+ /**
+ * @param SearchResult $result The result to render
+ * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+ * @param int $position The result position, including offset
+ * @return string HTML
+ */
+ public function render( SearchResult $result, $terms, $position ) {
+ // If the page doesn't *exist*... our search index is out of date.
+ // The least confusing at this point is to drop the result.
+ // You may get less results, but... on well. :P
+ if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
+ return '';
+ }
+
+ $link = $this->generateMainLinkHtml( $result, $terms, $position );
+ // If page content is not readable, just return ths title.
+ // This is not quite safe, but better than showing excerpts from
+ // non-readable pages. Note that hiding the entry entirely would
+ // screw up paging (really?).
+ if ( !$result->getTitle()->userCan( 'read', $this->specialPage->getUser() ) ) {
+ return "<li>{$link}</li>";
+ }
+
+ $redirect = $this->generateRedirectHtml( $result );
+ $section = $this->generateSectionHtml( $result );
+ $category = $this->generateCategoryHtml( $result );
+ $date = $this->specialPage->getLanguage()->userTimeAndDate(
+ $result->getTimestamp(),
+ $this->specialPage->getUser()
+ );
+ list( $file, $desc, $thumb ) = $this->generateFileHtml( $result );
+ $snippet = $result->getTextSnippet( $terms );
+ if ( $snippet ) {
+ $extract = "<div class='searchresult'>$snippet</div>";
+ } else {
+ $extract = '';
+ }
+
+ if ( $thumb === null ) {
+ // If no thumb, then the description is about size
+ $desc = $this->generateSizeHtml( $result );
+
+ // Let hooks do their own final construction if desired.
+ // FIXME: Not sure why this is only for results without thumbnails,
+ // but keeping it as-is for now to prevent breaking hook consumers.
+ $html = null;
+ $score = '';
+ $related = '';
+ if ( !Hooks::run( 'ShowSearchHit', [
+ $this->specialPage, $result, $terms,
+ &$link, &$redirect, &$section, &$extract,
+ &$score, &$desc, &$date, &$related, &$html
+ ] ) ) {
+ return $html;
+ }
+ }
+
+ // All the pieces have been collected. Now generate the final HTML
+ $joined = "{$link} {$redirect} {$category} {$section} {$file}";
+ $meta = $this->buildMeta( $desc, $date );
+
+ if ( $thumb === null ) {
+ $html =
+ "<div class='mw-search-result-heading'>{$joined}</div>" .
+ "{$extract} {$meta}";
+ } else {
+ $html =
+ "<table class='searchResultImage'>" .
+ "<tr>" .
+ "<td style='width: 120px; text-align: center; vertical-align: top'>" .
+ $thumb .
+ "</td>" .
+ "<td style='vertical-align: top'>" .
+ "{$joined} {$extract} {$meta}" .
+ "</td>" .
+ "</tr>" .
+ "</table>";
+ }
+
+ return "<li>{$html}</li>";
+ }
+
+ /**
+ * Generates HTML for the primary call to action. It is
+ * typically the article title, but the search engine can
+ * return an exact snippet to use (typically the article
+ * title with highlighted words).
+ *
+ * @param SearchResult $result
+ * @param string $terms
+ * @param int $position
+ * @return string HTML
+ */
+ protected function generateMainLinkHtml( SearchResult $result, $terms, $position ) {
+ $snippet = $result->getTitleSnippet();
+ if ( $snippet === '' ) {
+ $snippet = null;
+ } else {
+ $snippet = new HtmlArmor( $snippet );
+ }
+
+ // clone to prevent hook from changing the title stored inside $result
+ $title = clone $result->getTitle();
+ $query = [];
+
+ $attributes = [ 'data-serp-pos' => $position ];
+ Hooks::run( 'ShowSearchHitTitle',
+ [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query, &$attributes ] );
+
+ $link = $this->linkRenderer->makeLink(
+ $title,
+ $snippet,
+ $attributes,
+ $query
+ );
+
+ return $link;
+ }
+
+ /**
+ * Generates an alternate title link, such as (redirect from <a>Foo</a>).
+ *
+ * @param string $msgKey i18n message used to wrap title
+ * @param Title|null $title The title to link to, or null to generate
+ * the message without a link. In that case $text must be non-null.
+ * @param string|null $text The text snippet to display, or null
+ * to use the title
+ * @return string HTML
+ */
+ protected function generateAltTitleHtml( $msgKey, Title $title = null, $text ) {
+ $inner = $title === null
+ ? $text
+ : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null );
+
+ return "<span class='searchalttitle'>" .
+ $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse()
+ . "</span>";
+ }
+
+ /**
+ * @param SearchResult $result
+ * @return string HTML
+ */
+ protected function generateRedirectHtml( SearchResult $result ) {
+ $title = $result->getRedirectTitle();
+ return $title === null
+ ? ''
+ : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() );
+ }
+
+ /**
+ * @param SearchResult $result
+ * @return string HTML
+ */
+ protected function generateSectionHtml( SearchResult $result ) {
+ $title = $result->getSectionTitle();
+ return $title === null
+ ? ''
+ : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() );
+ }
+
+ /**
+ * @param SearchResult $result
+ * @return string HTML
+ */
+ protected function generateCategoryHtml( SearchResult $result ) {
+ $snippet = $result->getCategorySnippet();
+ return $snippet
+ ? $this->generateAltTitleHtml( 'search-category', null, $snippet )
+ : '';
+ }
+
+ /**
+ * @param SearchResult $result
+ * @return string HTML
+ */
+ protected function generateSizeHtml( SearchResult $result ) {
+ $title = $result->getTitle();
+ if ( $title->getNamespace() === NS_CATEGORY ) {
+ $cat = Category::newFromTitle( $title );
+ return $this->specialPage->msg( 'search-result-category-size' )
+ ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
+ ->escaped();
+ // TODO: This is a bit odd...but requires changing the i18n message to fix
+ } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) {
+ $lang = $this->specialPage->getLanguage();
+ $bytes = $lang->formatSize( $result->getByteSize() );
+ $words = $result->getWordCount();
+
+ return $this->specialPage->msg( 'search-result-size', $bytes )
+ ->numParams( $words )
+ ->escaped();
+ }
+
+ return '';
+ }
+
+ /**
+ * @param SearchResult $result
+ * @return array Three element array containing the main file html,
+ * a text description of the file, and finally the thumbnail html.
+ * If no thumbnail is available the second and third will be null.
+ */
+ protected function generateFileHtml( SearchResult $result ) {
+ $title = $result->getTitle();
+ if ( $title->getNamespace() !== NS_FILE ) {
+ return [ '', null, null ];
+ }
+
+ if ( $result->isFileMatch() ) {
+ $html = "<span class='searchalttitle'>" .
+ $this->specialPage->msg( 'search-file-match' )->escaped() .
+ "</span>";
+ } else {
+ $html = '';
+ }
+
+ $descHtml = null;
+ $thumbHtml = null;
+
+ $img = $result->getFile() ?: wfFindFile( $title );
+ if ( $img ) {
+ $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
+ if ( $thumb ) {
+ $descHtml = $this->specialPage->msg( 'parentheses' )
+ ->rawParams( $img->getShortDesc() )
+ ->escaped();
+ $thumbHtml = $thumb->toHtml( [ 'desc-link' => true ] );
+ }
+ }
+
+ return [ $html, $descHtml, $thumbHtml ];
+ }
+
+ /**
+ * @param string $desc HTML description of result, ex: size in bytes, or empty string
+ * @param string $date HTML representation of last edit date, or empty string
+ * @return string HTML A div combining $desc and $date with a separator in a <div>.
+ * If either is missing only one will be represented. If both are missing an empty
+ * string will be returned.
+ */
+ protected function buildMeta( $desc, $date ) {
+ if ( $desc && $date ) {
+ $meta = "{$desc} - {$date}";
+ } elseif ( $desc ) {
+ $meta = $desc;
+ } elseif ( $date ) {
+ $meta = $date;
+ } else {
+ return '';
+ }
+
+ return "<div class='mw-search-result-data'>{$meta}</div>";
+ }
+}
diff --git a/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php
new file mode 100644
index 00000000..b4e34148
--- /dev/null
+++ b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResultSet;
+use SpecialSearch;
+use Title;
+use Html;
+use OOUI;
+
+/**
+ * Renders one or more SearchResultSets into a sidebar grouped by
+ * interwiki prefix. Includes a per-wiki header indicating where
+ * the results are from.
+ */
+class InterwikiSearchResultSetWidget implements SearchResultSetWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+ /** @var SearchResultWidget */
+ protected $resultWidget;
+ /** @var string[]|null */
+ protected $customCaptions;
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+ /** @var InterwikiLookup */
+ protected $iwLookup;
+ /** @var $output */
+ protected $output;
+ /** @var bool $showMultimedia */
+ protected $showMultimedia;
+
+ public function __construct(
+ SpecialSearch $specialSearch,
+ SearchResultWidget $resultWidget,
+ LinkRenderer $linkRenderer,
+ InterwikiLookup $iwLookup,
+ $showMultimedia = false
+ ) {
+ $this->specialSearch = $specialSearch;
+ $this->resultWidget = $resultWidget;
+ $this->linkRenderer = $linkRenderer;
+ $this->iwLookup = $iwLookup;
+ $this->output = $specialSearch->getOutput();
+ $this->showMultimedia = $showMultimedia;
+ }
+ /**
+ * @param string $term User provided search term
+ * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki
+ * results to render.
+ * @return string HTML
+ */
+ public function render( $term, $resultSets ) {
+ if ( !is_array( $resultSets ) ) {
+ $resultSets = [ $resultSets ];
+ }
+
+ $this->loadCustomCaptions();
+
+ if ( $this->showMultimedia ) {
+ $this->output->addModules( 'mediawiki.special.search.commonsInterwikiWidget' );
+ }
+ $this->output->addModuleStyles( 'mediawiki.special.search.interwikiwidget.styles' );
+
+ $iwResults = [];
+ foreach ( $resultSets as $resultSet ) {
+ $result = $resultSet->next();
+ while ( $result ) {
+ if ( !$result->isBrokenTitle() ) {
+ $iwResults[$result->getTitle()->getInterwiki()][] = $result;
+ }
+ $result = $resultSet->next();
+ }
+ }
+
+ $iwResultSetPos = 1;
+ $iwResultListOutput = '';
+
+ foreach ( $iwResults as $iwPrefix => $results ) {
+ // TODO: Assumes interwiki results are never paginated
+ $position = 0;
+ $iwResultItemOutput = '';
+
+ foreach ( $results as $result ) {
+ $iwResultItemOutput .= $this->resultWidget->render( $result, $term, $position++ );
+ }
+
+ $footerHtml = $this->footerHtml( $term, $iwPrefix );
+ $iwResultListOutput .= Html::rawElement( 'li',
+ [
+ 'class' => 'iw-resultset',
+ 'data-iw-resultset-pos' => $iwResultSetPos,
+ 'data-iw-resultset-source' => $iwPrefix
+ ],
+
+ $iwResultItemOutput .
+ $footerHtml
+ );
+
+ $iwResultSetPos++;
+ }
+
+ return Html::rawElement(
+ 'div',
+ [ 'id' => 'mw-interwiki-results' ],
+ Html::rawElement(
+ 'p',
+ [ 'class' => 'iw-headline' ],
+ $this->specialSearch->msg( 'search-interwiki-caption' )->parse()
+ ) .
+ Html::rawElement(
+ 'ul', [ 'class' => 'iw-results', ], $iwResultListOutput
+ )
+ );
+ }
+
+ /**
+ * Generates an HTML footer for the given interwiki prefix
+ *
+ * @param string $term User provided search term
+ * @param string $iwPrefix Interwiki prefix of wiki to show footer for
+ * @return string HTML
+ */
+ protected function footerHtml( $term, $iwPrefix ) {
+ $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL(
+ [ 'search' => $term, 'fulltext' => 1 ]
+ );
+
+ $interwiki = $this->iwLookup->fetch( $iwPrefix );
+ $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) );
+
+ if ( isset( $this->customCaptions[$iwPrefix] ) ) {
+ $caption = $this->customCaptions[$iwPrefix];
+ } else {
+ $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped();
+ }
+
+ $searchLink = Html::rawElement( 'em', null,
+ Html::rawElement( 'a', [ 'href' => $href, 'target' => '_blank' ], $caption )
+ );
+
+ return Html::rawElement( 'div',
+ [ 'class' => 'iw-result__footer' ],
+ $this->iwIcon( $iwPrefix ) . $searchLink );
+ }
+
+ protected function loadCustomCaptions() {
+ if ( $this->customCaptions !== null ) {
+ return;
+ }
+
+ $this->customCaptions = [];
+ $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() );
+ foreach ( $customLines as $line ) {
+ $parts = explode( ':', $line, 2 );
+ if ( count( $parts ) === 2 ) {
+ $this->customCaptions[$parts[0]] = $parts[1];
+ }
+ }
+ }
+
+ /**
+ * Generates a custom OOUI icon element with a favicon as the image.
+ * The favicon image URL is generated by parsing the interwiki URL
+ * and returning the default location of the favicon for that domain,
+ * which is assumed to be '/favicon.ico'.
+ *
+ * @param string $iwPrefix Interwiki prefix
+ * @return OOUI\IconWidget
+ */
+ protected function iwIcon( $iwPrefix ) {
+ $interwiki = $this->iwLookup->fetch( $iwPrefix );
+ $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) );
+
+ $iwIconUrl = $parsed['scheme'] .
+ $parsed['delimiter'] .
+ $parsed['host'] .
+ ( isset( $parsed['port'] ) ? ':' . $parsed['port'] : '' ) .
+ '/favicon.ico';
+
+ $iwIcon = new OOUI\IconWidget( [
+ 'icon' => 'favicon'
+ ] );
+
+ $iwIcon->setAttributes( [ 'style' => "background-image:url($iwIconUrl);" ] );
+
+ return $iwIcon;
+ }
+}
diff --git a/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php b/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php
new file mode 100644
index 00000000..4eead5e7
--- /dev/null
+++ b/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use HtmlArmor;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResult;
+use SpecialSearch;
+use Html;
+
+/**
+ * Renders an enhanced interwiki result
+ */
+class InterwikiSearchResultWidget implements SearchResultWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+
+ public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) {
+ $this->specialSearch = $specialSearch;
+ $this->linkRenderer = $linkRenderer;
+ }
+
+ /**
+ * @param SearchResult $result The result to render
+ * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+ * @param int $position The result position, including offset
+ * @return string HTML
+ */
+ public function render( SearchResult $result, $terms, $position ) {
+ $title = $result->getTitle();
+ $iwPrefix = $result->getTitle()->getInterwiki();
+ $titleSnippet = $result->getTitleSnippet();
+ $snippet = $result->getTextSnippet( $terms );
+
+ if ( $titleSnippet ) {
+ $titleSnippet = new HtmlArmor( $titleSnippet );
+ } else {
+ $titleSnippet = null;
+ }
+
+ $link = $this->linkRenderer->makeLink( $title, $titleSnippet );
+
+ $redirectTitle = $result->getRedirectTitle();
+ $redirect = '';
+ if ( $redirectTitle !== null ) {
+ $redirectText = $result->getRedirectSnippet();
+
+ if ( $redirectText ) {
+ $redirectText = new HtmlArmor( $redirectText );
+ } else {
+ $redirectText = null;
+ }
+
+ $redirect = Html::rawElement( 'span', [ 'class' => 'iw-result__redirect' ],
+ $this->specialSearch->msg( 'search-redirect' )->rawParams(
+ $this->linkRenderer->makeLink( $redirectTitle, $redirectText )
+ )->escaped()
+ );
+ }
+
+ return Html::rawElement( 'div', [ 'class' => 'iw-result__title' ], $link . ' ' . $redirect ) .
+ Html::rawElement( 'div', [ 'class' => 'iw-result__content' ], $snippet );
+ }
+}
diff --git a/www/wiki/includes/widget/search/SearchFormWidget.php b/www/wiki/includes/widget/search/SearchFormWidget.php
new file mode 100644
index 00000000..2c885630
--- /dev/null
+++ b/www/wiki/includes/widget/search/SearchFormWidget.php
@@ -0,0 +1,314 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use Hooks;
+use Html;
+use MediaWiki\Widget\SearchInputWidget;
+use MWNamespace;
+use SearchEngineConfig;
+use SpecialSearch;
+use Xml;
+
+class SearchFormWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+ /** @var SearchEngineConfig */
+ protected $searchConfig;
+ /** @var array */
+ protected $profiles;
+
+ /**
+ * @param SpecialSearch $specialSearch
+ * @param SearchEngineConfig $searchConfig
+ * @param array $profiles
+ */
+ public function __construct(
+ SpecialSearch $specialSearch,
+ SearchEngineConfig $searchConfig,
+ array $profiles
+ ) {
+ $this->specialSearch = $specialSearch;
+ $this->searchConfig = $searchConfig;
+ $this->profiles = $profiles;
+ }
+
+ /**
+ * @param string $profile The current search profile
+ * @param string $term The current search term
+ * @param int $numResults The number of results shown
+ * @param int $totalResults The total estimated results found
+ * @param int $offset Current offset in search results
+ * @param bool $isPowerSearch Is the 'advanced' section open?
+ * @return string HTML
+ */
+ public function render(
+ $profile,
+ $term,
+ $numResults,
+ $totalResults,
+ $offset,
+ $isPowerSearch
+ ) {
+ return Xml::openElement(
+ 'form',
+ [
+ 'id' => $isPowerSearch ? 'powersearch' : 'search',
+ 'method' => 'get',
+ 'action' => wfScript(),
+ ]
+ ) .
+ '<div id="mw-search-top-table">' .
+ $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) .
+ '</div>' .
+ "<div class='mw-search-visualclear'></div>" .
+ "<div class='mw-search-profile-tabs'>" .
+ $this->profileTabsHtml( $profile, $term ) .
+ "<div style='clear:both'></div>" .
+ "</div>" .
+ $this->optionsHtml( $term, $isPowerSearch, $profile ) .
+ '</form>';
+ }
+
+ /**
+ * @param string $profile The current search profile
+ * @param string $term The current search term
+ * @param int $numResults The number of results shown
+ * @param int $totalResults The total estimated results found
+ * @param int $offset Current offset in search results
+ * @return string HTML
+ */
+ protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) {
+ $html = '';
+
+ $searchWidget = new SearchInputWidget( [
+ 'id' => 'searchText',
+ 'name' => 'search',
+ 'autofocus' => trim( $term ) === '',
+ 'value' => $term,
+ 'dataLocation' => 'content',
+ 'infusable' => true,
+ ] );
+
+ $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [
+ 'type' => 'submit',
+ 'label' => $this->specialSearch->msg( 'searchbutton' )->text(),
+ 'flags' => [ 'progressive', 'primary' ],
+ ] ), [
+ 'align' => 'top',
+ ] );
+
+ $html .= $layout;
+
+ if ( $totalResults > 0 && $offset < $totalResults ) {
+ $html .= Xml::tags(
+ 'div',
+ [
+ 'class' => 'results-info',
+ 'data-mw-num-results-offset' => $offset,
+ 'data-mw-num-results-total' => $totalResults
+ ],
+ $this->specialSearch->msg( 'search-showingresults' )
+ ->numParams( $offset + 1, $offset + $numResults, $totalResults )
+ ->numParams( $numResults )
+ ->parse()
+ );
+ }
+
+ $html .=
+ Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
+ Html::hidden( 'profile', $profile ) .
+ Html::hidden( 'fulltext', '1' );
+
+ return $html;
+ }
+
+ /**
+ * Generates HTML for the list of available search profiles.
+ *
+ * @param string $profile The currently selected profile
+ * @param string $term The user provided search terms
+ * @return string HTML
+ */
+ protected function profileTabsHtml( $profile, $term ) {
+ $bareterm = $this->startsWithImage( $term )
+ ? substr( $term, strpos( $term, ':' ) + 1 )
+ : $term;
+ $lang = $this->specialSearch->getLanguage();
+ $items = [];
+ foreach ( $this->profiles as $id => $profileConfig ) {
+ $profileConfig['parameters']['profile'] = $id;
+ $tooltipParam = isset( $profileConfig['namespace-messages'] )
+ ? $lang->commaList( $profileConfig['namespace-messages'] )
+ : null;
+ $items[] = Xml::tags(
+ 'li',
+ [ 'class' => $profile === $id ? 'current' : 'normal' ],
+ $this->makeSearchLink(
+ $bareterm,
+ $this->specialSearch->msg( $profileConfig['message'] )->text(),
+ $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(),
+ $profileConfig['parameters']
+ )
+ );
+ }
+
+ return "<div class='search-types'>" .
+ "<ul>" . implode( '', $items ) . "</ul>" .
+ "</div>";
+ }
+
+ /**
+ * Check if query starts with image: prefix
+ *
+ * @param string $term The string to check
+ * @return bool
+ */
+ protected function startsWithImage( $term ) {
+ global $wgContLang;
+
+ $parts = explode( ':', $term );
+ return count( $parts ) > 1
+ ? $wgContLang->getNsIndex( $parts[0] ) === NS_FILE
+ : false;
+ }
+
+ /**
+ * Make a search link with some target namespaces
+ *
+ * @param string $term The term to search for
+ * @param string $label Link's text
+ * @param string $tooltip Link's tooltip
+ * @param array $params Query string parameters
+ * @return string HTML fragment
+ */
+ protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) {
+ $params += [
+ 'search' => $term,
+ 'fulltext' => 1,
+ ];
+
+ return Xml::element(
+ 'a',
+ [
+ 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ),
+ 'title' => $tooltip,
+ ],
+ $label
+ );
+ }
+
+ /**
+ * Generates HTML for advanced options available with the currently
+ * selected search profile.
+ *
+ * @param string $term User provided search term
+ * @param bool $isPowerSearch Is the advanced search profile enabled?
+ * @param string $profile The current search profile
+ * @return string HTML
+ */
+ protected function optionsHtml( $term, $isPowerSearch, $profile ) {
+ $html = '';
+
+ if ( $isPowerSearch ) {
+ $html .= $this->powerSearchBox( $term, [] );
+ } else {
+ $form = '';
+ Hooks::run( 'SpecialSearchProfileForm', [
+ $this->specialSearch, &$form, $profile, $term, []
+ ] );
+ $html .= $form;
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param string $term The current search term
+ * @param array $opts Additional key/value pairs that will be submitted
+ * with the generated form.
+ * @return string HTML
+ */
+ protected function powerSearchBox( $term, array $opts ) {
+ global $wgContLang;
+
+ $rows = [];
+ $activeNamespaces = $this->specialSearch->getNamespaces();
+ foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
+ $subject = MWNamespace::getSubject( $namespace );
+ if ( !isset( $rows[$subject] ) ) {
+ $rows[$subject] = "";
+ }
+
+ $name = $wgContLang->getConverter()->convertNamespace( $namespace );
+ if ( $name === '' ) {
+ $name = $this->specialSearch->msg( 'blanknamespace' )->text();
+ }
+
+ $rows[$subject] .=
+ '<td>' .
+ Xml::checkLabel(
+ $name,
+ "ns{$namespace}",
+ "mw-search-ns{$namespace}",
+ in_array( $namespace, $activeNamespaces )
+ ) .
+ '</td>';
+ }
+
+ // Lays out namespaces in multiple floating two-column tables so they'll
+ // be arranged nicely while still accomodating diferent screen widths
+ $tableRows = [];
+ foreach ( $rows as $row ) {
+ $tableRows[] = "<tr>{$row}</tr>";
+ }
+ $namespaceTables = [];
+ foreach ( array_chunk( $tableRows, 4 ) as $chunk ) {
+ $namespaceTables[] = implode( '', $chunk );
+ }
+
+ $showSections = [
+ 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>',
+ ];
+ Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] );
+
+ $hidden = '';
+ foreach ( $opts as $key => $value ) {
+ $hidden .= Html::hidden( $key, $value );
+ }
+
+ $divider = "<div class='divider'></div>";
+
+ // Stuff to feed SpecialSearch::saveNamespaces()
+ $user = $this->specialSearch->getUser();
+ $remember = '';
+ if ( $user->isLoggedIn() ) {
+ $remember = $divider . Xml::checkLabel(
+ $this->specialSearch->msg( 'powersearch-remember' )->text(),
+ 'nsRemember',
+ 'mw-search-powersearch-remember',
+ false,
+ // The token goes here rather than in a hidden field so it
+ // is only sent when necessary (not every form submission)
+ [ 'value' => $user->getEditToken(
+ 'searchnamespace',
+ $this->specialSearch->getRequest()
+ ) ]
+ );
+ }
+
+ return "<fieldset id='mw-searchoptions'>" .
+ "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
+ "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' .
+ // populated by js if available
+ "<div id='mw-search-togglebox'></div>" .
+ $divider .
+ implode(
+ $divider,
+ $showSections
+ ) .
+ $hidden .
+ $remember .
+ "</fieldset>";
+ }
+}
diff --git a/www/wiki/includes/widget/search/SearchResultSetWidget.php b/www/wiki/includes/widget/search/SearchResultSetWidget.php
new file mode 100644
index 00000000..6df6e65c
--- /dev/null
+++ b/www/wiki/includes/widget/search/SearchResultSetWidget.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use SearchResultSet;
+
+/**
+ * Renders a set of search results to HTML
+ */
+interface SearchResultSetWidget {
+ /**
+ * @param string $term User provided search term
+ * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki
+ * results to render.
+ * @return string HTML
+ */
+ public function render( $term, $resultSets );
+}
diff --git a/www/wiki/includes/widget/search/SearchResultWidget.php b/www/wiki/includes/widget/search/SearchResultWidget.php
new file mode 100644
index 00000000..3fbdbef2
--- /dev/null
+++ b/www/wiki/includes/widget/search/SearchResultWidget.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use SearchResult;
+
+/**
+ * Renders a single search result to HTML
+ */
+interface SearchResultWidget {
+ /**
+ * @param SearchResult $result The result to render
+ * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+ * @param int $position The zero indexed result position, including offset
+ * @return string HTML
+ */
+ public function render( SearchResult $result, $terms, $position );
+}
diff --git a/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php
new file mode 100644
index 00000000..d0c259fe
--- /dev/null
+++ b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResultSet;
+use SpecialSearch;
+use Title;
+use Html;
+
+/**
+ * Renders one or more SearchResultSets into a sidebar grouped by
+ * interwiki prefix. Includes a per-wiki header indicating where
+ * the results are from.
+ *
+ * @deprecated since 1.31. Use InterwikiSearchResultSetWidget
+ */
+class SimpleSearchResultSetWidget implements SearchResultSetWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+ /** @var SearchResultWidget */
+ protected $resultWidget;
+ /** @var string[]|null */
+ protected $customCaptions;
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+ /** @var InterwikiLookup */
+ protected $iwLookup;
+
+ public function __construct(
+ SpecialSearch $specialSearch,
+ SearchResultWidget $resultWidget,
+ LinkRenderer $linkRenderer,
+ InterwikiLookup $iwLookup
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $this->specialSearch = $specialSearch;
+ $this->resultWidget = $resultWidget;
+ $this->linkRenderer = $linkRenderer;
+ $this->iwLookup = $iwLookup;
+ }
+
+ /**
+ * @param string $term User provided search term
+ * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki
+ * results to render.
+ * @return string HTML
+ */
+ public function render( $term, $resultSets ) {
+ if ( !is_array( $resultSets ) ) {
+ $resultSets = [ $resultSets ];
+ }
+
+ $this->loadCustomCaptions();
+
+ $iwResults = [];
+ foreach ( $resultSets as $resultSet ) {
+ $result = $resultSet->next();
+ while ( $result ) {
+ if ( !$result->isBrokenTitle() ) {
+ $iwResults[$result->getTitle()->getInterwiki()][] = $result;
+ }
+ $result = $resultSet->next();
+ }
+ }
+
+ $out = '';
+ foreach ( $iwResults as $iwPrefix => $results ) {
+ $out .= $this->headerHtml( $iwPrefix, $term );
+ $out .= "<ul class='mw-search-iwresults'>";
+ // TODO: Assumes interwiki results are never paginated
+ $position = 0;
+ foreach ( $results as $result ) {
+ $out .= $this->resultWidget->render( $result, $term, $position++ );
+ }
+ $out .= "</ul>";
+ }
+
+ return "<div id='mw-search-interwiki'>" .
+ "<div id='mw-search-interwiki-caption'>" .
+ $this->specialSearch->msg( 'search-interwiki-caption' )->parse() .
+ '</div>' .
+ $out .
+ "</div>";
+ }
+
+ /**
+ * Generates an appropriate HTML header for the given interwiki prefix
+ *
+ * @param string $iwPrefix Interwiki prefix of wiki to show header for
+ * @param string $term User provided search term
+ * @return string HTML
+ */
+ protected function headerHtml( $iwPrefix, $term ) {
+ if ( isset( $this->customCaptions[$iwPrefix] ) ) {
+ $caption = $this->customCaptions[$iwPrefix];
+ } else {
+ $interwiki = $this->iwLookup->fetch( $iwPrefix );
+ $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) );
+ $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped();
+ }
+
+ $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL(
+ [ 'search' => $term, 'fulltext' => 1 ]
+ );
+ $searchLink = Html::rawElement(
+ 'a',
+ [ 'href' => $href ],
+ $this->specialSearch->msg( 'search-interwiki-more' )->escaped()
+ );
+
+ return "<div class='mw-search-interwiki-project'>" .
+ "<span class='mw-search-interwiki-more'>{$searchLink}</span>" .
+ $caption .
+ "</div>";
+ }
+
+ protected function loadCustomCaptions() {
+ if ( $this->customCaptions !== null ) {
+ return;
+ }
+
+ $this->customCaptions = [];
+ $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() );
+ foreach ( $customLines as $line ) {
+ $parts = explode( ':', $line, 2 );
+ if ( count( $parts ) === 2 ) {
+ $this->customCaptions[$parts[0]] = $parts[1];
+ }
+ }
+ }
+}
diff --git a/www/wiki/includes/widget/search/SimpleSearchResultWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php
new file mode 100644
index 00000000..552cbaf8
--- /dev/null
+++ b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use HtmlArmor;
+use MediaWiki\Linker\LinkRenderer;
+use SearchResult;
+use SpecialSearch;
+
+/**
+ * Renders a simple one-line result
+ *
+ * @deprecated since 1.31. Use other result widgets.
+ */
+class SimpleSearchResultWidget implements SearchResultWidget {
+ /** @var SpecialSearch */
+ protected $specialSearch;
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+
+ public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $this->specialSearch = $specialSearch;
+ $this->linkRenderer = $linkRenderer;
+ }
+
+ /**
+ * @param SearchResult $result The result to render
+ * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+ * @param int $position The result position, including offset
+ * @return string HTML
+ */
+ public function render( SearchResult $result, $terms, $position ) {
+ $title = $result->getTitle();
+ $titleSnippet = $result->getTitleSnippet();
+ if ( $titleSnippet ) {
+ $titleSnippet = new HtmlArmor( $titleSnippet );
+ } else {
+ $titleSnippet = null;
+ }
+
+ $link = $this->linkRenderer->makeLink( $title, $titleSnippet );
+
+ $redirectTitle = $result->getRedirectTitle();
+ $redirect = '';
+ if ( $redirectTitle !== null ) {
+ $redirectText = $result->getRedirectSnippet();
+ if ( $redirectText ) {
+ $redirectText = new HtmlArmor( $redirectText );
+ } else {
+ $redirectText = null;
+ }
+ $redirect =
+ "<span class='searchalttitle'>" .
+ $this->specialSearch->msg( 'search-redirect' )->rawParams(
+ $this->linkRenderer->makeLink( $redirectTitle, $redirectText )
+ )->parse() .
+ "</span>";
+ }
+
+ return "<li>{$link} {$redirect}</li>";
+ }
+}