Mglaser has submitted this change and it was merged.

Change subject: ExtendenSearch: Facet settings
......................................................................


ExtendenSearch: Facet settings

* Introducing facet settings
* Some refactoring and code clean up
** explicit declaration of public fields in SearchRequest
** type hinting and doc block fixes
** splitting of SearchOptions::readInSearchRequest
* Minor user interface improvements (facet labels are now clickable)
* Implemented CR
** Introducing new boost-query-config-variable
* Fixed "facet selection not working issue" in MW 1.24+

Change-Id: If05ee37071ce9516253e514d336b11db13e6bc78
---
M ExtendedSearch/ExtendedSearch.class.php
M ExtendedSearch/ExtendedSearch.setup.php
M ExtendedSearch/doc/Hooks.txt
M ExtendedSearch/i18n/de.json
M ExtendedSearch/i18n/en.json
M ExtendedSearch/i18n/qqq.json
M ExtendedSearch/includes/SearchIndex/SearchOptions.class.php
M ExtendedSearch/includes/SearchIndex/SearchRequest.class.php
M ExtendedSearch/includes/SearchIndex/SearchResult.class.php
A ExtendedSearch/resources/BS.ExtendedSearch/tip/FacetSettings.js
M ExtendedSearch/resources/bluespice.extendedSearch.specialpage.css
M ExtendedSearch/resources/bluespice.extendedSearch.specialpage.js
A ExtendedSearch/resources/bluespice.facetsettings.js
M ExtendedSearch/views/view.ExtendedSearchFacetBox.php
M ExtendedSearch/views/view.SearchResult.php
15 files changed, 774 insertions(+), 401 deletions(-)

Approvals:
  Mglaser: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/ExtendedSearch/ExtendedSearch.class.php 
b/ExtendedSearch/ExtendedSearch.class.php
index f35c838..e8fc798 100644
--- a/ExtendedSearch/ExtendedSearch.class.php
+++ b/ExtendedSearch/ExtendedSearch.class.php
@@ -81,7 +81,7 @@
 
                // max 32 chars with userlevel! 123 456789012345678 
90123456789012 '::' counts as one char :-)
                BsConfig::registerVar( 'MW::ExtendedSearch::DefFuzziness', 
'0.5', BsConfig::TYPE_STRING, 'bs-extendedsearch-pref-defduzziness' );
-               BsConfig::registerVar( 'MW::ExtendedSearch::LimitResults', 15, 
BsConfig::TYPE_INT|BsConfig::LEVEL_USER,  
'bs-extendedsearch-pref-limitresultdef', 'int' );
+               BsConfig::registerVar( 'MW::ExtendedSearch::LimitResults', 25, 
BsConfig::TYPE_INT|BsConfig::LEVEL_USER,  
'bs-extendedsearch-pref-limitresultdef', 'int' );
                BsConfig::registerVar( 'MW::ExtendedSearch::SearchFiles', true, 
BsConfig::TYPE_BOOL|BsConfig::LEVEL_USER|BsConfig::RENDER_AS_JAVASCRIPT, 
'bs-extendedsearch-pref-searchfiles', 'toggle' );
                BsConfig::registerVar( 'MW::ExtendedSearch::JumpToTitle', 
false, BsConfig::TYPE_BOOL|BsConfig::LEVEL_USER, 
'bs-extendedsearch-pref-jumptotitle', 'toggle' );
                BsConfig::registerVar( 'MW::ExtendedSearch::ShowCreateSugg', 
true, BsConfig::TYPE_BOOL|BsConfig::LEVEL_USER, 
'bs-extendedsearch-pref-showcreatesugg', 'toggle' );
@@ -126,6 +126,8 @@
                $this->setHook( 'SkinTemplateOutputPageBeforeExec' );
 
                $this->mCore->registerPermission( 'searchfiles', array( 'user' 
), array( 'type' => 'global' ) );
+
+               $this->resolveNamespaceBoostQueryConfig();
 
                wfProfileOut( 'BS::'.__METHOD__ );
        }
@@ -496,4 +498,32 @@
                return true;
        }
 
+       /**
+        * Uses the wildcard config entry in
+        * $bsgExtendedSearchBoostQuerySettings['namespace'] to calculate 
concrete
+        * settings in dependency of current system settings
+        * Needs to be done after "setup time"
+        * @global array $wgContentNamespaces
+        * @global array $bsgExtendedSearchBoostQuerySettings
+        * @return void
+        */
+       protected function resolveNamespaceBoostQueryConfig() {
+               global $wgContentNamespaces, 
$bsgExtendedSearchBoostQuerySettings;
+
+               if( !isset( 
$bsgExtendedSearchBoostQuerySettings['namespace']['*'] ) ) {
+                       return;
+               }
+
+               $iBoostFactor = 
$bsgExtendedSearchBoostQuerySettings['namespace']['*'];
+               //Resolving '*' namespace config at runtime
+               foreach ( $wgContentNamespaces as $iNs ) {
+                       //If there already is an explicit boost factor we leave 
it unchanged
+                       if( isset( 
$bsgExtendedSearchBoostQuerySettings['namespace'][$iNs] ) ) {
+                               continue;
+                       }
+                       $bsgExtendedSearchBoostQuerySettings['namespace'][$iNs] 
= $iBoostFactor;
+               }
+               unset( $bsgExtendedSearchBoostQuerySettings['namespace']['*'] );
+       }
+
 }
diff --git a/ExtendedSearch/ExtendedSearch.setup.php 
b/ExtendedSearch/ExtendedSearch.setup.php
index 6cad9c7..374a3db 100644
--- a/ExtendedSearch/ExtendedSearch.setup.php
+++ b/ExtendedSearch/ExtendedSearch.setup.php
@@ -42,10 +42,15 @@
 ) + $aResourceModuleTemplate;
 
 $wgResourceModules['ext.bluespice.extendedsearch.specialpage'] = array(
-       'scripts' => 'bluespice.extendedSearch.specialpage.js',
+       'scripts' => array(
+               'bluespice.extendedSearch.specialpage.js',
+               'bluespice.facetsettings.js'
+       ),
        'messages' => array(
                'bs-extendedsearch-more',
-               'bs-extendedsearch-fewer'
+               'bs-extendedsearch-fewer',
+               'bs-extendedsearch-facetsetting-op-and',
+               'bs-extendedsearch-facetsetting-op-or'
        )
 ) + $aResourceModuleTemplate;
 
@@ -102,4 +107,14 @@
 $wgSpecialPages['SpecialExtendedSearch'] = 'SpecialExtendedSearch';
 
 $wgHooks['LoadExtensionSchemaUpdates'][] = 'ExtendedSearch::getSchemaUpdates';
-$GLOBALS['wgHooks']['OpenSearchUrls'][] = 'ExtendedSearch::onOpenSearchUrls';
\ No newline at end of file
+$GLOBALS['wgHooks']['OpenSearchUrls'][] = 'ExtendedSearch::onOpenSearchUrls';
+
+//Allows for changes in the 'bq' parameter that gets send to solr.
+$bsgExtendedSearchBoostQuerySettings = array(
+       'namespace' => array(
+               //This is for every MediaWiki content namespace;
+               //Concrete values will be calculated at runtime
+               '*' => 2,
+               999 => 2 // Pseudo namespace for files
+       )
+);
diff --git a/ExtendedSearch/doc/Hooks.txt b/ExtendedSearch/doc/Hooks.txt
index b377342..28839f6 100644
--- a/ExtendedSearch/doc/Hooks.txt
+++ b/ExtendedSearch/doc/Hooks.txt
@@ -28,9 +28,6 @@
 &$sSearchString
 
 ==BuildIndexMainControl.class==
-'BS:ExtendedSearch:AddDocumentLog':
-$oSolrDocument:
-
 'BSExtendedSearchBuildIndex':
 $this:
 
@@ -67,28 +64,21 @@
 'BSExtendedSearchGetCustomerId':
 &$sCustomerId:
 
-'BSExtendedSearchEmptyNamespacesElse':
-&$this:
-$oSearchRequest:
-
+'BSExtendedSearchSearchOptionsAppendFilterQueryAndFacetFields':
+$oSearchOptions
+&$aTerms
+$sFieldName
+$sTagName
 
 ==SearchRequest.class==
 
 'BSExtendedSearchRequestProcessInputs':
 &$this:
 
-
-==view.ExtendedSearchFormPage.php==
-
-'FormDefaults':
- $this:
-&$aSearchBoxKeyValues:
-
-
 ==view.SearchSuggest.php==
 
 'BSExtendedSearchAdditionalActions':
-&$sCreatesuggest:
+ &$sCreatesuggest:
  &$sSearchUrlencoded:
  &$sSearchHtmlEntities:
  &$oTitle
\ No newline at end of file
diff --git a/ExtendedSearch/i18n/de.json b/ExtendedSearch/i18n/de.json
index 820a7ed..ae0c9c7 100644
--- a/ExtendedSearch/i18n/de.json
+++ b/ExtendedSearch/i18n/de.json
@@ -101,5 +101,7 @@
        "bs-extendedsearch-redirect": "Weiterleitung von: $1",
        "bs-extendedsearch-totalnoofarticles": "Zu indexierende Artikel: $1",
        "bs-extendedsearch-more": "Mehr",
-       "bs-extendedsearch-fewer": "Weniger"
+       "bs-extendedsearch-fewer": "Weniger",
+       "bs-extendedsearch-facetsetting-op-and": "UND",
+       "bs-extendedsearch-facetsetting-op-or": "ODER"
 }
diff --git a/ExtendedSearch/i18n/en.json b/ExtendedSearch/i18n/en.json
index 68367d5..7f105c9 100644
--- a/ExtendedSearch/i18n/en.json
+++ b/ExtendedSearch/i18n/en.json
@@ -98,5 +98,7 @@
        "bs-extendedsearch-redirect": "Redirect from: $1",
        "bs-extendedsearch-totalnoofarticles": "Number of pages to index: $1",
        "bs-extendedsearch-more": "More",
-       "bs-extendedsearch-fewer": "Less"
+       "bs-extendedsearch-fewer": "Less",
+       "bs-extendedsearch-facetsetting-op-and": "AND",
+       "bs-extendedsearch-facetsetting-op-or": "OR"
 }
diff --git a/ExtendedSearch/i18n/qqq.json b/ExtendedSearch/i18n/qqq.json
index 950e226..cb4faba 100644
--- a/ExtendedSearch/i18n/qqq.json
+++ b/ExtendedSearch/i18n/qqq.json
@@ -105,5 +105,7 @@
        "bs-extendedsearch-redirect": "Text for redirect from: $1 \n\n* $1 is a 
comma separated list of redirects",
        "bs-extendedsearch-totalnoofarticles": "Text for number of pages to 
index: $1 \n\n* $1 is the total number of pages",
        "bs-extendedsearch-more": "Anchor text for more\n{{Identical|More}}",
-       "bs-extendedsearch-fewer": "Anchor text for less\n{{Identical|Less}}"
+       "bs-extendedsearch-fewer": "Anchor text for less\n{{Identical|Less}}",
+       "bs-extendedsearch-facetsetting-op-and": "Label for setting of logical 
AND operator. Should be upper case",
+       "bs-extendedsearch-facetsetting-op-or": "Label for setting of logical 
OR operator. Should be upper case"
 }
diff --git a/ExtendedSearch/includes/SearchIndex/SearchOptions.class.php 
b/ExtendedSearch/includes/SearchIndex/SearchOptions.class.php
index 9c2b3db..0ccba32 100644
--- a/ExtendedSearch/includes/SearchIndex/SearchOptions.class.php
+++ b/ExtendedSearch/includes/SearchIndex/SearchOptions.class.php
@@ -39,7 +39,7 @@
         * Data being passed as $params to instance of BsSearchService, member 
function searchs($query, $offset = 0, $limit = 10, $params = array())
         * @var array List of options.
         */
-       protected $aSearchOptions = array(); // data being passed as $params to 
instance of BsSearchService, member function searchs($query, $offset = 0, 
$limit = 10, $params = array())
+       protected $aSearchOptions = array();
        /**
         * List of Solr query options
         * @var array List of options.
@@ -61,6 +61,25 @@
         * @var object of search service
         */
        protected static $oInstance = null;
+
+       /**
+        *
+        * @var SearchRequest
+        */
+       protected $oSearchRequest = null;
+
+       /**
+        * Buffer used to assemble the 'facet.field' search option
+        * @var array
+        */
+       protected $aFacetFields = array();
+
+       /**
+        * This is just in case somebody
+        * set 0 to "MW::ExtendedSearch::LimitResults"
+        * @var int
+        */
+       protected $iSearchLimitFailSettingDefault = 25;
 
        /**
         * Constructor  for SearchOptions class
@@ -144,7 +163,9 @@
         * @return array List of url parameters.
         */
        public function getSolrQuery() {
-               if ( $this->aSolrQuery !== null ) return $this->aSolrQuery;
+               if ( $this->aSolrQuery !== null ) {
+                       return $this->aSolrQuery;
+               }
 
                $this->aSolrQuery = array(
                        'searchString' => $this->aOptions['searchStringFinal'],
@@ -274,236 +295,29 @@
                );
        }
 
+
+       /**
+        * The buffer for filter query assembly
+        * @var array
+        */
+       protected $aFq = array();
        /**
         * Processes incoming search request
         */
        public function readInSearchRequest() {
-               global $wgCanonicalNamespaceNames, $wgExtraNamespaces, 
$wgContentNamespaces;
-               $this->aOptions['searchStringRaw'] = 
$this->oSearchRequest->sInput;
-               $this->aOptions['searchStringOrig'] = 
ExtendedSearchBase::preprocessSearchInput( $this->oSearchRequest->sInput );
+               $this->aFq[] = 'redirect:0';
+               $this->aFq[] = 'special:0';
+               $this->aFq[] = 'wiki:(' . $this->getCustomerId() . ')';
 
-               self::$searchStringRaw = $this->aOptions['searchStringRaw'];
+               $this->readInBasicOptions();
+               $this->readInSearchTerm();
 
-               $sCustomerId = $this->getCustomerId();
-               $sLogOp = ' OR ';
-               $aFq = array();
-               $aBq = array();
+               $this->readInNamespaces();
+               $this->readInCategories();
+               $this->readInTypes();
+               $this->readInEditors();
 
-               $oRequest = RequestContext::getMain()->getRequest();
-               $scope = $oRequest->getVal( 'search_scope', BsConfig::get( 
'MW::ExtendedSearch::DefScopeUser' ) ) == 'title'
-                       ? 'title'
-                       : 'text';
-               $this->aOptions['scope'] = $scope;
-
-               $vNamespace = $this->checkSearchstringForNamespace(
-                       $this->aOptions['searchStringRaw'],
-                       $this->aOptions['searchStringOrig'],
-                       $aFq,
-                       BsConfig::get( 'MW::ExtendedSearch::ShowFacets' )
-               );
-
-               $this->aOptions['searchStringWildcarded'] = 
SearchService::wildcardSearchstring( $this->aOptions['searchStringOrig'] );
-               $this->aOptions['searchStringForStatistics'] = 
$this->aOptions['searchStringWildcarded'];
-
-               $aSearchTitle = array(
-                       'title:(' . $this->aOptions['searchStringOrig'] . ')^5',
-                       'titleWord:(' . $this->aOptions['searchStringOrig'] . 
')^2',
-                       'titleReverse:(' . 
$this->aOptions['searchStringWildcarded'] . ')',
-                       'redirects:(' . $this->aOptions['searchStringOrig'] . 
')'
-               );
-               $aSearchText = array(
-                       'textWord:(' . $this->aOptions['searchStringOrig'] 
.')^2',
-                       'textReverse:(' . 
$this->aOptions['searchStringWildcarded'] . ')',
-                       'sections:(' . $this->aOptions['searchStringOrig'] . ')'
-               );
-
-               $sSearchStringTitle = implode( $sLogOp, $aSearchTitle );
-               $sSearchStringText = implode( $sLogOp, $aSearchText );
-
-               $this->aOptions['searchStringFinal'] = ( 
$this->aOptions['scope'] === 'title' )
-                       ? $sSearchStringTitle
-                       : $sSearchStringTitle . $sLogOp . $sSearchStringText;
-
-               // filter query
-               $aFq[] = 'redirect:0';
-               $aFq[] = 'special:0';
-               $aFq[] = 'wiki:('.$sCustomerId.')';
-
-               // $this->aOptions['namespaces'] HAS TO BE an array with 
numeric indices of type string!
-               $this->aOptions['namespaces'] = 
$this->oSearchRequest->aNamespaces;
-
-               $aNamespaces = array_slice( $wgCanonicalNamespaceNames, 2 );
-               $aNamespaces = $aNamespaces + $wgExtraNamespaces;
-
-               $bTagNamespace = false;
-               if ( $vNamespace === false ) {
-                       $this->aOptions['files'] = ( 
$this->oSearchRequest->bSearchFiles === true )
-                               ? true
-                               : false;
-
-                       $oUser = RequestContext::getMain()->getUser();
-                       if ( !$oUser->getOption( 'searcheverything' ) ) {
-                               if ( empty( $this->aOptions['namespaces'] ) && 
$this->oSearchRequest->bNoSelect === false ) {
-                                       $this->aOptions['namespaces'] = array();
-
-                                       $aOptions = $oUser->getOptions();
-                                       foreach ( $aOptions as $sOpt => $sValue 
) {
-                                               if ( strpos( $sOpt, 'searchNs' 
) !== false && $sValue == true ) {
-                                                       
$this->aOptions['namespaces'][] = '' . str_replace( 'searchNs', '', $sOpt );
-                                               }
-                                       }
-
-                                       $aAllowedTypes = explode( ',' , 
BsConfig::get( 'MW::ExtendedSearch::IndexFileTypes' ) );
-                                       $aAllowedTypes = array_map( 'trim', 
$aAllowedTypes );
-                                       $aSearchFilesFacet = array_intersect( 
$this->oSearchRequest->aType, $aAllowedTypes );
-
-                                       if ( ( $this->aOptions['files'] === 
true || !empty( $aSearchFilesFacet ) )
-                                               && $oUser->isAllowed( 
'searchfiles' ) ) {
-                                               $this->aOptions['namespaces'][] 
= '999';
-                                               $this->aOptions['namespaces'][] 
= '998';
-                                       }
-                               } else {
-                                       $bTagNamespace = true;
-                                       $aTmp = array();
-                                       foreach ( $this->aOptions['namespaces'] 
as $iNs ) {
-                                               if ( 
BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) === true ) {
-                                                       $aTmp[] = $iNs;
-                                               }
-                                       }
-                                       $this->aOptions['namespaces'] = $aTmp;
-                               }
-                       } else {
-                               if ( empty( $this->aOptions['namespaces'] ) ) {
-                                       $aTmp = array();
-                                       foreach ( $aNamespaces as $iNs ) {
-                                               if ( 
BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) === true ) {
-                                                       
$this->aOptions['namespaces'][] = $iNs;
-                                               }
-                                       }
-                               } else {
-                                       $bTagNamespace = true;
-                                       $aTmp = array();
-                                       foreach ( $this->aOptions['namespaces'] 
as $iNs ) {
-                                               if ( 
!BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) ) {
-                                                       $aTmp[] = $iNs;
-                                               }
-                                       }
-
-                                       if ( !empty( $aTmp ) ) {
-                                               $this->aOptions['namespaces'] = 
array_diff( $this->aOptions['namespaces'], $aTmp );
-                                       }
-                               }
-                       }
-               } else {
-                       $bTagNamespace = true;
-                       $this->aOptions['namespaces'][] = '' . $vNamespace;
-               }
-
-               $this->aOptions['namespaces'] = array_unique( 
$this->aOptions['namespaces'] );
-
-               if ( !empty( $this->aOptions['namespaces'] ) ) {
-                       $aFqNamespaces = array();
-                       foreach ( $this->aOptions['namespaces'] as $sNamespace 
) {
-                               $aFqNamespaces[] = $sNamespace;
-                               if ( $sNamespace == '999' ) {
-                                       $filesAlreadyAddedInLoopBefore = true;
-                               }
-                       }
-
-                       if ( !isset( $filesAlreadyAddedInLoopBefore ) && 
$this->aOptions['files'] === true
-                               && $oUser->isAllowed( 'searchfiles' ) ) {
-                               $aFqNamespaces[] = '999';
-                       }
-
-                       $bTagNamespace = true;
-                       $aFq[] = ( BsConfig::get( 
'MW::ExtendedSearch::ShowFacets' ) )
-                               ? '{!tag=na}namespace:("' . implode( '" "', 
$aFqNamespaces ) . '")'
-                               : 'namespace:("' . implode( '" "', 
$aFqNamespaces ) . '")';
-               }
-
-               // $this->aOptions['cats'] = $this->oSearchRequest->sCat; // 
string, defaults to '' if 'search_cat' not set in REQUEST
-               $this->aOptions['cats'] = $this->oSearchRequest->sCategories; 
// array of strings or empty array
-               if ( !empty( $this->aOptions['cats'] ) ) {
-                       if ( isset( $this->oSearchRequest->sOperator ) ) {
-                               switch ( $this->oSearchRequest->sOperator ) {
-                                       case 'AND':
-                                               $sLogOp = ' AND ';
-                                               break;
-                                       default:
-                               }
-                       }
-                       $sFqCategories = ( BsConfig::get( 
'MW::ExtendedSearch::ShowFacets' ) ) ? '{!tag=ca}' : '';
-                       $sFqCategories .= 'cat:("' . implode( '"' . $sLogOp . 
'"', $this->aOptions['cats'] ) . '")';
-                       $aFq[] = $sFqCategories;
-               }
-
-               $this->aOptions['type'] = $this->oSearchRequest->aType;
-               if ( !empty( $this->aOptions['type'] ) ) {
-                       $sFqType = ( BsConfig::get( 
'MW::ExtendedSearch::ShowFacets' ) ) ? '{!tag=ty}' : '';
-                       $sFqType .= 'type:("' . implode( '"' . $sLogOp . '"', 
$this->aOptions['type'] ) . '")';
-                       $aFq[] = $sFqType;
-               }
-
-               $this->aOptions['editor'] = $this->oSearchRequest->sEditor;
-               if ( !empty( $this->aOptions['editor'] ) ) {
-                       // there may be spaces in name of editor. solr analyses 
those to two
-                       // terms (editor's names) thus we wrap the name into 
quotation marks
-                       // todo: better: in schema.xml define field editor not 
to be tokenized
-                       //       at whitespace
-                       // but: +editor:("Robert V" "Mathias S") is already 
split correctly!
-                       $sFqEditor = ( BsConfig::get( 
'MW::ExtendedSearch::ShowFacets' ) ) ? '{!tag=ed}' : '';
-                       $sFqEditor .= 'editor:("' . implode( '"' . $sLogOp . 
'"', $this->aOptions['editor'] ) . '")';
-                       $aFq[] = $sFqEditor;
-               }
-
-               // Boost query
-               foreach ( $wgContentNamespaces as $iNs ) {
-                       $aBq[] = "namespace:{$iNs}^2";
-               }
-               // We want that files are also seen as a content namespace
-               $aBq[] = "namespace:999^2";
-
-               $searchLimit = BsConfig::get( 
'MW::ExtendedSearch::LimitResults' );
-
-               $this->aOptions['offset'] = $this->oSearchRequest->iOffset;
-               $this->aOptions['order'] = $this->oSearchRequest->sOrder;
-               $this->aOptions['asc'] = $this->oSearchRequest->sAsc;
-               $this->aOptions['searchLimit'] = ( $searchLimit == 0 ) ? 15 : 
$searchLimit;
-               $this->aOptions['titleExists'] = 
ExtendedSearchBase::titleExists( $this->oSearchRequest->sInput, $this->aOptions 
);
-               $this->aOptions['bExtendedForm'] = 
$this->oSearchRequest->bExtendedForm;
-
-               $this->aSearchOptions['defType'] = 'edismax';
-               $this->aSearchOptions['fl'] = 
'uid,type,title,path,namespace,cat,ts,redirects,overall_type';
-               $this->aSearchOptions['fq'] = $aFq;
-               $this->aSearchOptions['sort'] = $this->aOptions['order'] . ' ' 
. $this->aOptions['asc'];
-               $this->aSearchOptions['hl'] = 'on';
-               $this->aSearchOptions['hl.fl'] = 'titleWord, titleReverse, 
sections, textWord, textReverse';
-               $this->aSearchOptions['hl.snippets'] = BsConfig::get( 
'MW::ExtendedSearch::HighlightSnippets' );
-               $this->aSearchOptions['bq'] = implode( ' ', $aBq );
-
-               if ( BsConfig::get( 'MW::ExtendedSearch::ShowFacets' ) ) {
-                       $this->aSearchOptions['facet'] = 'on';
-                       $this->aSearchOptions['facet.sort'] = 'false';
-                       $this->aSearchOptions['facet.mincount'] = '1';
-                       $this->aSearchOptions['facet.missing'] = 'true';
-                       $this->aSearchOptions['facet.field'] = array();
-
-                       $this->aSearchOptions['facet.field'][] = ( 
$bTagNamespace === true )
-                               ? '{!ex=na}namespace'
-                               : 'namespace';
-
-                       $this->aSearchOptions['facet.field'][] = ( isset( 
$sFqCategories ) )
-                               ? '{!ex=ca}cat'
-                               : 'cat';
-
-                       $this->aSearchOptions['facet.field'][] = ( isset( 
$sFqType ) )
-                               ? '{!ex=ty}type'
-                               : 'type';
-
-                       $this->aSearchOptions['facet.field'][] = ( isset( 
$sFqEditor ) )
-                               ? '{!ex=ed}editor'
-                               : 'editor';
-               }
+               $this->assembleSearchOptions();
        }
 
        /**
@@ -511,10 +325,10 @@
         * @param string $sSearchString given searchstring
         * @param string $sSolrSearchString the solr searchstring
         * @param array $aQueryFq solr filter query
-        * @param boolean $bWtihTag flag for tagging
+        * @param boolean $bWithTag flag for tagging
         * @return int|boolean id of namespace or false
         */
-       public function checkSearchstringForNamespace( $sSearchString, 
&$sSolrSearchString, &$aQueryFq, $bWtihTag = false ) {
+       public function checkSearchstringForNamespace( $sSearchString, 
&$sSolrSearchString, &$aQueryFq, $bWithTag = false ) {
                if ( empty( $sSearchString ) ) {
                        return false;
                }
@@ -544,11 +358,257 @@
 
                $sSolrSearchString = $aParts[1];
 
-               $aQueryFq[] = ( $bWtihTag )
+               $aQueryFq[] = ( $bWithTag )
                        ? '{!tag=na}namespace:(' . $iNamespace . ')'
                        : 'namespace:(' . $iNamespace . ')';
 
                return $iNamespace;
        }
 
-}
\ No newline at end of file
+       protected function readInNamespaces() {
+               global $wgCanonicalNamespaceNames, $wgExtraNamespaces;
+
+               $vNamespace = $this->checkSearchstringForNamespace(
+                       $this->aOptions['searchStringRaw'],
+                       $this->aOptions['searchStringOrig'],
+                       $this->aFq,
+                       BsConfig::get( 'MW::ExtendedSearch::ShowFacets' )
+               );
+
+               $this->aOptions['namespaces'] = 
$this->oSearchRequest->aNamespaces; //HAS TO BE an array with numeric indices 
of type string!
+
+               $aNamespaces = array_slice( $wgCanonicalNamespaceNames, 2 );
+               $aNamespaces = $aNamespaces + $wgExtraNamespaces;
+
+               if ( $vNamespace === false ) {
+                       $this->aOptions['files'] = ( 
$this->oSearchRequest->bSearchFiles === true )
+                               ? true
+                               : false;
+
+                       $oUser = RequestContext::getMain()->getUser();
+                       if ( !$oUser->getOption( 'searcheverything' ) ) {
+                               if ( empty( $this->aOptions['namespaces'] ) && 
$this->oSearchRequest->bNoSelect === false ) {
+                                       $this->aOptions['namespaces'] = array();
+
+                                       $aOptions = $oUser->getOptions();
+                                       foreach ( $aOptions as $sOpt => $sValue 
) {
+                                               if ( strpos( $sOpt, 'searchNs' 
) !== false && $sValue == true ) {
+                                                       
$this->aOptions['namespaces'][] = '' . str_replace( 'searchNs', '', $sOpt );
+                                               }
+                                       }
+
+                                       $aAllowedTypes = explode( ',' , 
BsConfig::get( 'MW::ExtendedSearch::IndexFileTypes' ) );
+                                       $aAllowedTypes = array_map( 'trim', 
$aAllowedTypes );
+                                       $aSearchFilesFacet = array_intersect( 
$this->oSearchRequest->aType, $aAllowedTypes );
+
+                                       if ( ( $this->aOptions['files'] === 
true || !empty( $aSearchFilesFacet ) )
+                                               && $oUser->isAllowed( 
'searchfiles' ) ) {
+                                               $this->aOptions['namespaces'][] 
= '999';
+                                               $this->aOptions['namespaces'][] 
= '998';
+                                       }
+                               } else {
+                                       $aTmp = array();
+                                       foreach ( $this->aOptions['namespaces'] 
as $iNs ) {
+                                               if ( 
BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) === true ) {
+                                                       $aTmp[] = $iNs;
+                                               }
+                                       }
+                                       $this->aOptions['namespaces'] = $aTmp;
+                               }
+                       } else {
+                               if ( empty( $this->aOptions['namespaces'] ) ) {
+                                       $aTmp = array();
+                                       foreach ( $aNamespaces as $iNs ) {
+                                               if ( 
BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) === true ) {
+                                                       
$this->aOptions['namespaces'][] = $iNs;
+                                               }
+                                       }
+                               } else {
+                                       $aTmp = array();
+                                       foreach ( $this->aOptions['namespaces'] 
as $iNs ) {
+                                               if ( 
!BsNamespaceHelper::checkNamespacePermission( $iNs, 'read' ) ) {
+                                                       $aTmp[] = $iNs;
+                                               }
+                                       }
+
+                                       if ( !empty( $aTmp ) ) {
+                                               $this->aOptions['namespaces'] = 
array_diff( $this->aOptions['namespaces'], $aTmp );
+                                       }
+                               }
+                       }
+               } else {
+                       $this->aOptions['namespaces'][] = '' . $vNamespace;
+               }
+
+               $this->aOptions['namespaces'] = array_unique( 
$this->aOptions['namespaces'] );
+
+               if ( !empty( $this->aOptions['namespaces'] ) ) {
+                       $aFqNamespaces = array();
+                       foreach ( $this->aOptions['namespaces'] as $sNamespace 
) {
+                               $aFqNamespaces[] = $sNamespace;
+                               if ( $sNamespace == '999' ) {
+                                       $filesAlreadyAddedInLoopBefore = true;
+                               }
+                       }
+
+                       if ( !isset( $filesAlreadyAddedInLoopBefore ) && 
$this->aOptions['files'] === true
+                               && $oUser->isAllowed( 'searchfiles' ) ) {
+                               $aFqNamespaces[] = '999';
+                       }
+
+                       $this->appendFilterQueryAndFacetFields( $aFqNamespaces, 
'namespace', 'na' );
+               }
+               else {
+                       /*
+                        * This is to make sure that the 'namespace' facet is 
displayed
+                        * even if no namespace is selected.
+                        * TODO: '$this->aFacetFields' should not be appended 
outside of
+                        * 'appendFilterQueryAndFacetFields'. Unfortunately 
this is a
+                        * special case in the logic that can not be handled 
any better
+                        * without doing a major refactoring of this and 
several other
+                        * classes.
+                        */
+                       $this->aFacetFields[] = 'namespace';
+               }
+       }
+
+       protected function readInCategories() {
+               $this->aOptions['cats'] = $this->oSearchRequest->sCategories; 
// array of strings or empty array
+               $this->appendFilterQueryAndFacetFields( 
$this->aOptions['cats'], 'cat', 'ca' );
+       }
+
+       protected function readInTypes() {
+               $this->aOptions['type'] = $this->oSearchRequest->aType;
+               $this->appendFilterQueryAndFacetFields( 
$this->aOptions['type'], 'type', 'ty' );
+       }
+
+       protected function readInEditors() {
+               $this->aOptions['editor'] = $this->oSearchRequest->sEditor;
+               $this->appendFilterQueryAndFacetFields( 
$this->aOptions['editor'], 'editor', 'ed' );
+       }
+
+       protected function makeBoostQuery() {
+               global $bsgExtendedSearchBoostQuerySettings;
+
+               $aBq = array();
+               foreach( $bsgExtendedSearchBoostQuerySettings as $sFieldName => 
$aValueConfs ) {
+                       foreach( $aValueConfs as $mValue => $iBoostFactor ) {
+                               $aBq[] = $sFieldName . ':' . $mValue . '^' . 
$iBoostFactor;
+                       }
+               }
+
+               return $aBq;
+       }
+
+       protected function assembleSearchOptions() {
+               $this->aSearchOptions['defType'] = 'edismax';
+               $this->aSearchOptions['fl'] = 
'uid,type,title,path,namespace,cat,ts,redirects,overall_type';
+               $this->aSearchOptions['fq'] = $this->aFq;
+               $this->aSearchOptions['sort'] = $this->aOptions['order'] . ' ' 
. $this->aOptions['asc'];
+               $this->aSearchOptions['hl'] = 'on';
+               $this->aSearchOptions['hl.fl'] = 'titleWord, titleReverse, 
sections, textWord, textReverse';
+               $this->aSearchOptions['hl.snippets'] = BsConfig::get( 
'MW::ExtendedSearch::HighlightSnippets' );
+               $this->aSearchOptions['bq'] = implode( ' ', 
$this->makeBoostQuery() );
+
+               if ( BsConfig::get( 'MW::ExtendedSearch::ShowFacets' ) ) {
+                       $this->aSearchOptions['facet'] = 'on';
+                       $this->aSearchOptions['facet.sort'] = 'false';
+                       $this->aSearchOptions['facet.mincount'] = '1';
+                       $this->aSearchOptions['facet.missing'] = 'true';
+                       $this->aSearchOptions['facet.field'] = array();
+
+                       foreach( $this->aFacetFields as $sFacetField ) {
+                               $this->aSearchOptions['facet.field'][] = 
$sFacetField;
+                       }
+               }
+       }
+
+       protected function readInBasicOptions() {
+               $this->aOptions['searchStringRaw'] = self::$searchStringRaw = 
$this->oSearchRequest->sInput;
+               $this->aOptions['searchStringOrig'] = 
ExtendedSearchBase::preprocessSearchInput( $this->oSearchRequest->sInput );
+               $this->aOptions['fset'] = $this->oSearchRequest->aFacetSettings;
+
+               $searchLimit = BsConfig::get( 
'MW::ExtendedSearch::LimitResults' );
+               $this->aOptions['offset'] = $this->oSearchRequest->iOffset;
+               $this->aOptions['order'] = $this->oSearchRequest->sOrder;
+               $this->aOptions['asc'] = $this->oSearchRequest->sAsc;
+               $this->aOptions['searchLimit'] = ( $searchLimit == 0 ) ? 
$this->iSearchLimitFailSettingDefault : $searchLimit;
+               $this->aOptions['titleExists'] = 
ExtendedSearchBase::titleExists( $this->oSearchRequest->sInput, $this->aOptions 
);
+               $this->aOptions['bExtendedForm'] = 
$this->oSearchRequest->bExtendedForm;
+
+               $this->aOptions['searchStringWildcarded'] = 
SearchService::wildcardSearchstring( $this->aOptions['searchStringOrig'] );
+               $this->aOptions['searchStringForStatistics'] = 
$this->aOptions['searchStringWildcarded'];
+
+               //This overrides setting from SearchRequest::processInput
+               //TODO: Check if still needed; find better place for this
+               $oRequest = RequestContext::getMain()->getRequest();
+               $scope = $oRequest->getVal( 'search_scope', BsConfig::get( 
'MW::ExtendedSearch::DefScopeUser' ) ) == 'title'
+                       ? 'title'
+                       : 'text';
+               $this->aOptions['scope'] = $scope;
+       }
+
+       protected function readInSearchTerm() {
+               $aSearchTitle = array(
+                       'title:(' . $this->aOptions['searchStringOrig'] . ')^5',
+                       'titleWord:(' . $this->aOptions['searchStringOrig'] . 
')^2',
+                       'titleReverse:(' . 
$this->aOptions['searchStringWildcarded'] . ')',
+                       'redirects:(' . $this->aOptions['searchStringOrig'] . 
')'
+               );
+               $aSearchText = array(
+                       'textWord:(' . $this->aOptions['searchStringOrig'] 
.')^2',
+                       'textReverse:(' . 
$this->aOptions['searchStringWildcarded'] . ')',
+                       'sections:(' . $this->aOptions['searchStringOrig'] . ')'
+               );
+
+               $this->aSearchOptions['qf'] = '';
+
+               $sLogOp = ' OR ';
+               $sSearchStringTitle = implode( $sLogOp, $aSearchTitle );
+               $sSearchStringText = implode( $sLogOp, $aSearchText );
+
+               $this->aOptions['searchStringFinal'] = ( 
$this->aOptions['scope'] === 'title' )
+                       ? $sSearchStringTitle
+                       : $sSearchStringTitle . $sLogOp . $sSearchStringText;
+       }
+
+       protected function getFacetOperator( $sTagName ) {
+               /*
+                * This is not a good solution as it is decoupled from the facet
+                * definition in "SearchResult::createFacets".
+                * But without a major refactoring there is not nice way to do 
this.
+                */
+               $sLogOp = ' OR ';
+               if( isset( $this->aOptions['fset'][$sTagName]['op'] ) ) {
+                       if( strtoupper( 
$this->aOptions['fset'][$sTagName]['op'] ) === 'AND' ) {
+                               $sLogOp = ' AND ';
+                       }
+               }
+               return $sLogOp;
+       }
+
+       protected function appendFilterQueryAndFacetFields( $aTerms, 
$sFieldName, $sTagName ) {
+               //Allow extensions that register own document types to the
+               //index to modify the query
+               Hooks::run(
+                       
'BSExtendedSearchSearchOptionsAppendFilterQueryAndFacetFields',
+                       array( $this, &$aTerms, $sFieldName, $sTagName )
+               );
+
+               $sFq = '';
+               $sFacetField = $sFieldName;
+               $sLogOp = $this->getFacetOperator( $sTagName );
+               if ( !empty( $aTerms ) ) {
+                       $sFacetField = "{!ex=$sTagName}$sFieldName";
+                       if( BsConfig::get( 'MW::ExtendedSearch::ShowFacets' ) ) 
{
+                               $sFq = "{!tag=$sTagName}";
+                       }
+
+                       $sFq .= $sFieldName.':("' . implode( '"' . $sLogOp . 
'"', $aTerms ) . '")';
+                       $this->aFq[] = $sFq;
+               }
+               //We can add it in every case, because it get's only used by
+               //'assembleSearchOptions' if 'MW::ExtendedSearch::ShowFacets' 
is true
+               $this->aFacetFields[] = $sFacetField;
+       }
+}
diff --git a/ExtendedSearch/includes/SearchIndex/SearchRequest.class.php 
b/ExtendedSearch/includes/SearchIndex/SearchRequest.class.php
index 27fe9b8..7b1575d 100644
--- a/ExtendedSearch/includes/SearchIndex/SearchRequest.class.php
+++ b/ExtendedSearch/includes/SearchIndex/SearchRequest.class.php
@@ -87,24 +87,145 @@
        }
 
        /**
+        *
+        * @var string
+        */
+       public $sScope = null;
+
+       /**
+        * Not used anymore
+        * @var string
+        * @deprecated since version 1.23
+        */
+       public $sOperator = null;
+
+       /**
+        *
+        * @var string
+        */
+       public $sAsc = null;
+
+       /**
+        *
+        * @var int
+        */
+       public $iOffset = null;
+
+       /**
+        *
+        * @var string
+        */
+       public $sOrder = null;
+
+       /**
+        *
+        * @var string
+        */
+       public $sId = null;
+
+       /**
+        *
+        * @var string
+        */
+       public $sInput = null;
+
+       /**
+        *
+        * @var string
+        */
+       public $sHidden = null;
+
+       /**
+        *
+        * @var boolean
+        */
+       public $bExtendedForm = null;
+
+       /**
+        * Flag that indicates whether or not to automatically redirect to an
+        * existing page if search term matches page title. Used in
+        * 'SearchIndex::search'
+        * @var boolean
+        */
+       public $bSft = null;
+
+       /**
+        * This is a legacy typo. Should be 'aEditors'.
+        * @var array
+        * @deprecated since version 1.23
+        */
+       public $sEditor = array();
+
+       /**
+        *
+        * @var array
+        */
+       public $aEditors = array();
+
+       /**
+        * This is a legacy typo. Should be 'aCategories'.
+        * @var array
+        * @deprecated since version 1.23
+        */
+       public $sCategories = array();
+
+       /**
+        *
+        * @var array
+        */
+       public $aCategories = array();
+
+       /**
+        *
+        * @var array
+        */
+       public $aNamespaces = null;
+
+       /**
+        * This is a legacy typo. Should be 'aTypes'.
+        * @var array
+        * @deprecated since version 1.23
+        */
+       public $aType = array();
+
+       /**
+        *
+        * @var array
+        */
+       public $aTypes = null;
+
+       /**
+        *
+        * @var boolean
+        */
+       public $bNoSelect = null;
+
+       /**
+        *
+        * @var array
+        */
+       public $aFacetSettings = array();
+
+       /**
         * Get values from url parameters
         */
        protected function processInputs() {
                $this->sScope = $this->oRequest->getVal( 'search_scope' );
                $this->sOperator = $this->oRequest->getVal( 'op' );
-               $this->sAsc = $this->oRequest->getVal( 'search_asc', 
$this->sAsc );
+               $this->sAsc = $this->oRequest->getVal( 'search_asc', 
$this->sAsc ); //ASC|DESC
                $this->iOffset = $this->oRequest->getVal( 'search_offset', 
$this->iOffset ); // todo: type is int??
-               $this->sOrder = $this->oRequest->getVal( 'search_order', 
$this->sOrder );
+               $this->sOrder = $this->oRequest->getVal( 'search_order', 
$this->sOrder ); //score|titleSort|type|ts
                $this->sId = $this->oRequest->getVal( 'search_id', false );
                $this->sInput = $this->oRequest->getVal( 'q', false );
                $this->sHidden = $this->oRequest->getVal( 'search_hidden' );
                $this->bExtendedForm = $this->oRequest->getFuzzyBool( 
'search_extended', false );
                $this->bSft = $this->oRequest->getFuzzyBool( 'sft', false );
-               $this->sEditor = $this->oRequest->getArray( 'ed', array() );
-               $this->sCategories = $this->oRequest->getArray( 'ca', array() );
+               $this->sEditor = $this->aEditors = $this->oRequest->getArray( 
'ed', array() );
+               $this->sCategories = $this->aCategories = 
$this->oRequest->getArray( 'ca', array() );
                $this->aNamespaces = $this->oRequest->getArray( 'na', array() );
-               $this->aType = $this->oRequest->getArray( 'ty', array() );
+               $this->aType =$this->aTypes = $this->oRequest->getArray( 'ty', 
array() );
                $this->bNoSelect = $this->oRequest->getBool( 'nosel', false );
+               $this->aFacetSettings = FormatJson::decode( 
$this->oRequest->getVal( 'fset', '{}' ), true );
 
                $this->bSearchFiles = ( $this->oRequest->getInt( 
'search_files', 0 ) === 1 )
                        ? true
diff --git a/ExtendedSearch/includes/SearchIndex/SearchResult.class.php 
b/ExtendedSearch/includes/SearchIndex/SearchResult.class.php
index e4bd129..1e7c916 100644
--- a/ExtendedSearch/includes/SearchIndex/SearchResult.class.php
+++ b/ExtendedSearch/includes/SearchIndex/SearchResult.class.php
@@ -19,7 +19,7 @@
 
        /**
         * Instance of apache solr response
-        * @var object of apache solr response
+        * @var Apache_Solr_Response
         */
        protected $oResponse = null;
        /**
@@ -29,36 +29,36 @@
        protected $aData = array();
        /**
         * RequestContext
-        * @var RequestContext object of RequestContext
+        * @var RequestContext
         */
        protected $oContext;
        /**
         * View that renders search results
-        * @var ViewSearchResult ViewSearchResult object
+        * @var ViewSearchResult
         */
        protected $vSearchResult = null;
        /**
         * Instance of SearchOptions
-        * @var SearchOptions object of SearchOptions
+        * @var SearchOptions
         */
        protected $oSearchOptions = null;
        /**
         * Instance of SearchUriBuilder
-        * @var object SearchUriBuilder object
+        * @var SearchUriBuilder
         */
        protected $oSearchUriBuilder = null;
        /**
         * Maximum character length of facets
-        * @var int Numberof characters
+        * @var int
         */
        protected $iMaxFacetLength = 20;
 
        /**
         * Constructor for SearchResult class
-        * @param object $oContext RequestContext
-        * @param object $oSearchOptions SearchOptions
-        * @param object $oSearchUriBuilder SearchUriBuilder
-        * @param object $oResponse solr result set
+        * @param RequestContext $oContext RequestContext
+        * @param SearchOptions $oSearchOptions SearchOptions
+        * @param SearchUriBuilder $oSearchUriBuilder SearchUriBuilder
+        * @param Apache_Solr_Response $oResponse solr result set
         */
        public function __construct( $oContext, $oSearchOptions, 
$oSearchUriBuilder, $oResponse ) {
                $this->oContext = $oContext;
@@ -166,6 +166,9 @@
                                'url' => $this->oSearchUriBuilder->buildUri(
                                        SearchUriBuilder::ALL,
                                        SearchUriBuilder::CATS
+                               ),
+                               'settings' => array(
+                                       'op' => 'OR'
                                )
                        ),
                        'editor' => array(
@@ -175,6 +178,9 @@
                                'url' => $this->oSearchUriBuilder->buildUri(
                                        SearchUriBuilder::ALL,
                                        
SearchUriBuilder::NAMESPACES|SearchUriBuilder::EDITOR
+                               ),
+                               'settings' => array(
+                                       'op' => 'OR'
                                )
                        ),
                        'type' => array(
@@ -190,138 +196,141 @@
 
                wfRunHooks( 'BSExtendedSearchBeforeCreateFacets', array( 
&$aBaseFacets ) );
 
+               $aFacetSettings = $this->oSearchOptions->getOption( 'fset' );
                foreach ( $aBaseFacets as $sFacet => $aConfig ) {
-                       $oFacet = new ViewSearchFacet();
+                       $oFacet = new ViewSearchFacet( $aConfig );
+                       if( isset( $aFacetSettings[$aConfig['param']] ) ) {
+                               $oFacet->setOption( 'fset', 
$aFacetSettings[$aConfig['param']] );
+                       }
 
-                       if ( !is_null( 
$this->oResponse->facet_counts->facet_fields->{$sFacet} ) ) {
-                               $oFacet->setOption( 'title', $aConfig['i18n'] );
+                       if( !isset( 
$this->oResponse->facet_counts->facet_fields->{$sFacet} ) ){
+                               continue;
+                       }
+                       /* alters to:
+                        * array(
+                        *     0 => array( 'checked' => true ),
+                        *     1 => array( 'count' => 15 ),
+                        *     999 => array( 'checked' => true, 'count' => 2 )
+                        * )*/
+                       $aFacets = array();
+                       $aData = $this->oSearchOptions->getOption( 
$aConfig['option'] );
 
-                               /* alters to:
-                                * array(
-                                *     0 => array( 'checked' => true ),
-                                *     1 => array( 'count' => 15 ),
-                                *     999 => array( 'checked' => true, 'count' 
=> 2 )
-                                * )*/
-                               $aFacets = array();
-                               $aData = $this->oSearchOptions->getOption( 
$aConfig['option'] );
-
-                               if ( !empty( $aData ) ) {
-                                       foreach ( $aData as $key => $value ) {
-                                               $aFacets[$value]['checked'] = 
true;
-                                       }
-                                       unset( $aData );
+                       if ( !empty( $aData ) ) {
+                               foreach ( $aData as $key => $value ) {
+                                       $aFacets[$value]['checked'] = true;
                                }
+                               unset( $aData );
+                       }
 
-                               // Get all available facets
-                               $aFacetsInRespsonse = 
$this->oResponse->facet_counts->facet_fields->{$sFacet};
-                               foreach ( $aFacetsInRespsonse as $key => $count 
) {
-                                       if ( $key == '_empty_' ) continue;
-                                       if ( $sFacet === 'namespace' ) {
-                                               if ( 
BsNamespaceHelper::checkNamespacePermission( $key, 'read' ) === false || $count 
== '0' ) {
-                                                       unset( $aFacets[$key] );
-                                                       continue;
-                                               }
-                                       } elseif ( $sFacet === 'type' ) {
-                                               if ( $key != 'wiki'
-                                                       && 
!$this->oContext->getUser()->isAllowed( 'searchfiles' ) ) {
-                                                       continue;
-                                               }
-                                       }
-                                       $aFacets[$key]['count'] = $count;
-                               }
-
-                               // Prepare available facets. Add some 
information for each facet
-                               foreach ( $aFacets as $key => $attributes ) {
-                                       if ( !isset( $aFacets[$key]['count'] ) 
) {
+                       // Get all available facets
+                       $aFacetsInRespsonse = 
$this->oResponse->facet_counts->facet_fields->{$sFacet};
+                       foreach ( $aFacetsInRespsonse as $key => $count ) {
+                               if ( $key == '_empty_' ) continue;
+                               if ( $sFacet === 'namespace' ) {
+                                       if ( 
BsNamespaceHelper::checkNamespacePermission( $key, 'read' ) === false || $count 
== '0' ) {
                                                unset( $aFacets[$key] );
                                                continue;
                                        }
-
-                                       if ( $sFacet === 'namespace' ) {
-                                               if ( $key == '999' ) {
-                                                       $sTitle = wfMessage( 
'bs-extendedsearch-facet-namespace-files' )->plain();
-                                               } elseif ( $key == '998' ) {
-                                                       $sTitle = wfMessage( 
'bs-extendedsearch-facet-namespace-extfiles' )->plain();
-                                               } elseif ( $key == '0' ) {
-                                                       $sTitle = wfMessage( 
'bs-ns_main' )->plain();
-                                               } else {
-                                                       $sTitle = 
BsNamespaceHelper::getNamespaceName( $key, false );
-
-                                                       if ( empty( $sTitle ) ) 
{
-                                                               unset( 
$aFacets[$key] );
-                                                               continue;
-                                                       }
-                                               }
-                                       } elseif ( $sFacet === 'cat' ) {
-                                               $sTitle = ( $key == 
'notcategorized' )
-                                                       ? wfMessage( 
'bs-extendedsearch-facet-uncategorized' )->plain()
-                                                       : $key;
-                                       } elseif ( $sFacet === 'editor' ) {
-                                               $sTitle = ( $key === 'unknown' )
-                                                       ? wfMessage( 
'bs-extendedsearch-unknown' )->plain()
-                                                       : $key;
-                                       } elseif (  $sFacet === 'type' ) {
-                                               $sTitle = $key;
+                               } elseif ( $sFacet === 'type' ) {
+                                       if ( $key != 'wiki'
+                                               && 
!$this->oContext->getUser()->isAllowed( 'searchfiles' ) ) {
+                                               continue;
                                        }
-
-                                       $aFacets[$key]['title'] = 
$this->getFacetTitle( $sTitle );
-                                       $aFacets[$key]['name'] = 
$this->reduceMaxFacetLength( $sTitle );
                                }
+                               $aFacets[$key]['count'] = $count;
+                       }
 
-                               uasort( $aFacets, array( $this, 
'compareEntries' ) );
-
-                               $aFacetAll = array();
-                               foreach ( $aFacets as $key => $attributes ) {
-                                       $aDataSet = array();
-                                       if ( isset( $attributes['checked'] ) ) 
$aDataSet['checked'] = true;
-
-                                       if ( $sFacet === 'namespace' ) {
-                                               if ( $key == '999' ) {
-                                                       $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::NAMESPACES | SearchUriBuilder::FILES );
-                                                       $uri .= 
'&search_files=' . ( isset( $attributes['checked'] ) ) ? '0' : '1';
-                                               } else {
-                                                       $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::NAMESPACES );
-                                               }
-                                       } elseif ( $sFacet === 'cat' ) {
-                                               $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::CATS );
-                                       } elseif ( $sFacet === 'editor' ) {
-                                               $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::EDITOR );
-                                       } elseif ( $sFacet === 'type' ) {
-                                               $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::TYPE );
-                                       }
-
-                                       foreach ( $aFacets as $namespaceUrl => 
$attributesUrl ) {
-                                               $bOwnUrlAndNotAlreadyChecked = 
( ( $key == $namespaceUrl ) && !isset( $attributesUrl['checked'] ) );
-                                               $bOtherUrlAndAlreadyChecked = ( 
( $key != $namespaceUrl ) && isset( $attributesUrl['checked'] ) );
-
-                                               if ( 
$bOwnUrlAndNotAlreadyChecked || $bOtherUrlAndAlreadyChecked ) {
-                                                       $uri .= 
"&{$aConfig['param']}[]=".$namespaceUrl;
-                                               }
-                                       }
-
-                                       $aDataSet['uri'] = $uri;
-                                       $aDataSet['diff'] = 
"{$aConfig['param']}[]=".$key;
-                                       $aDataSet['name'] = $attributes['name'];
-                                       $aDataSet['title'] = 
$attributes['title'];
-                                       $aDataSet['count'] = 
(int)$attributes['count'];
-
-                                       $oFacet->setData( $aDataSet );
-                                       $aFacetAll[] = 
"{$aConfig['param']}[]=".$key;
+                       // Prepare available facets. Add some information for 
each facet
+                       foreach ( $aFacets as $key => $attributes ) {
+                               if ( !isset( $aFacets[$key]['count'] ) ) {
+                                       unset( $aFacets[$key] );
+                                       continue;
                                }
 
                                if ( $sFacet === 'namespace' ) {
-                                       $aReqNs = 
$this->oSearchOptions->getOption( 'namespaces' );
-                                       foreach ( $aReqNs as $ikey => $value ) {
-                                               if ( !array_key_exists( $value, 
$aFacets ) ){
-                                                       $aFacetAll[] = 
"{$aConfig['param']}[]=".$value;
-                                                       $sSiteUri = 
str_replace( "&{$aConfig['param']}[]=$value", '', $sSiteUri );
+                                       if ( $key == '999' ) {
+                                               $sTitle = wfMessage( 
'bs-extendedsearch-facet-namespace-files' )->plain();
+                                       } elseif ( $key == '998' ) {
+                                               $sTitle = wfMessage( 
'bs-extendedsearch-facet-namespace-extfiles' )->plain();
+                                       } elseif ( $key == '0' ) {
+                                               $sTitle = wfMessage( 
'bs-ns_main' )->plain();
+                                       } else {
+                                               $sTitle = 
BsNamespaceHelper::getNamespaceName( $key, false );
+
+                                               if ( empty( $sTitle ) ) {
+                                                       unset( $aFacets[$key] );
+                                                       continue;
                                                }
+                                       }
+                               } elseif ( $sFacet === 'cat' ) {
+                                       $sTitle = ( $key == 'notcategorized' )
+                                               ? wfMessage( 
'bs-extendedsearch-facet-uncategorized' )->plain()
+                                               : $key;
+                               } elseif ( $sFacet === 'editor' ) {
+                                       $sTitle = ( $key === 'unknown' )
+                                               ? wfMessage( 
'bs-extendedsearch-unknown' )->plain()
+                                               : $key;
+                               } elseif (  $sFacet === 'type' ) {
+                                       $sTitle = $key;
+                               }
+
+                               $aFacets[$key]['title'] = $this->getFacetTitle( 
$sTitle );
+                               $aFacets[$key]['name'] = 
$this->reduceMaxFacetLength( $sTitle );
+                       }
+
+                       uasort( $aFacets, array( $this, 'compareEntries' ) );
+
+                       $aFacetAll = array();
+                       foreach ( $aFacets as $key => $attributes ) {
+                               $aDataSet = array();
+                               if ( isset( $attributes['checked'] ) ) 
$aDataSet['checked'] = true;
+
+                               if ( $sFacet === 'namespace' ) {
+                                       if ( $key == '999' ) {
+                                               $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::NAMESPACES | SearchUriBuilder::FILES );
+                                               $uri .= '&search_files=' . ( 
isset( $attributes['checked'] ) ) ? '0' : '1';
+                                       } else {
+                                               $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::NAMESPACES );
+                                       }
+                               } elseif ( $sFacet === 'cat' ) {
+                                       $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::CATS );
+                               } elseif ( $sFacet === 'editor' ) {
+                                       $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::EDITOR );
+                               } elseif ( $sFacet === 'type' ) {
+                                       $uri = 
$this->oSearchUriBuilder->buildUri( SearchUriBuilder::ALL, 
SearchUriBuilder::TYPE );
+                               }
+
+                               foreach ( $aFacets as $namespaceUrl => 
$attributesUrl ) {
+                                       $bOwnUrlAndNotAlreadyChecked = ( ( $key 
== $namespaceUrl ) && !isset( $attributesUrl['checked'] ) );
+                                       $bOtherUrlAndAlreadyChecked = ( ( $key 
!= $namespaceUrl ) && isset( $attributesUrl['checked'] ) );
+
+                                       if ( $bOwnUrlAndNotAlreadyChecked || 
$bOtherUrlAndAlreadyChecked ) {
+                                               $uri .= 
"&{$aConfig['param']}[]=".$namespaceUrl;
                                        }
                                }
 
-                               $sFacetAll = implode( '&', $aFacetAll );
-                               $oFacet->setOption( 'uri-facet-all-diff', 
$sFacetAll );
+                               $aDataSet['uri'] = $uri;
+                               $aDataSet['diff'] = 
"{$aConfig['param']}[]=".$key;
+                               $aDataSet['name'] = $attributes['name'];
+                               $aDataSet['title'] = $attributes['title'];
+                               $aDataSet['count'] = (int)$attributes['count'];
+
+                               $oFacet->setData( $aDataSet );
+                               $aFacetAll[] = "{$aConfig['param']}[]=".$key;
                        }
+
+                       if ( $sFacet === 'namespace' ) {
+                               $aReqNs = $this->oSearchOptions->getOption( 
'namespaces' );
+                               foreach ( $aReqNs as $ikey => $value ) {
+                                       if ( !array_key_exists( $value, 
$aFacets ) ){
+                                               $aFacetAll[] = 
"{$aConfig['param']}[]=".$value;
+                                               $sSiteUri = str_replace( 
"&{$aConfig['param']}[]=$value", '', $sSiteUri );
+                                       }
+                               }
+                       }
+
+                       $sFacetAll = implode( '&', $aFacetAll );
+                       $oFacet->setOption( 'uri-facet-all-diff', $sFacetAll );
 
                        $this->vSearchResult->setFacet( $oFacet );
                }
@@ -336,15 +345,22 @@
                $oParser = new Parser();
 
                foreach ( $this->oResponse->response->docs as $oDocument ) {
+                       //If pseudo namespace '999' we need tu use NS_FILE to 
have a
+                       //valid title object
                        $iNamespace = ( $oDocument->namespace == '999' )
                                ? NS_FILE
                                : $oDocument->namespace;
 
                        $oTitle = Title::makeTitle( $iNamespace, 
$oDocument->title );
 
-                       // external files will never exist for mediawiki
-                       if ( $oDocument->namespace != '998' ) {
-                               if ( !$oTitle->exists() ) continue;
+                       /* Only bail out if it should be a wiki page but it is 
not.
+                        * In case of other overall_types like 'external', 
'linked'
+                        * there can never be a valid title.
+                        * If an extension adds a new overall_type it also is 
responsible
+                        * to render the link using 
'BSExtendedSearchFormatLink' below.
+                        */
+                       if ( $oDocument->overall_type === 'wiki' && 
!$oTitle->exists() ) {
+                               continue;
                        }
 
                        $oSkin = $this->oContext->getSkin();
@@ -352,12 +368,11 @@
                        $sIconPath = '';
                        wfRunHooks( 'BSExtendedSearchFormatLink', array( 
&$sSearchLink, $oDocument, $oSkin, &$sIconPath ) );
 
+                       $sIcon = 'default';
                        if ( empty( $sSearchLink ) ) {
                                if ( $oDocument->type == 'wiki' ) {
                                        if ( !$oTitle->userCan( 'read' ) ) 
continue;
                                        $sSearchLink = $this->getWikiLink( 
$oDocument, $oTitle, $oParser );
-                                       $sIcon = 'default';
-
                                } elseif ( 
$this->oContext->getUser()->isAllowed( 'searchfiles' ) ) {
                                        $sSearchLink = $this->getFileLink( 
$oDocument, $oTitle );
                                        $sIcon = $oDocument->type;
diff --git a/ExtendedSearch/resources/BS.ExtendedSearch/tip/FacetSettings.js 
b/ExtendedSearch/resources/BS.ExtendedSearch/tip/FacetSettings.js
new file mode 100644
index 0000000..027e818
--- /dev/null
+++ b/ExtendedSearch/resources/BS.ExtendedSearch/tip/FacetSettings.js
@@ -0,0 +1,47 @@
+Ext.define( 'BS.ExtendedSearch.tip.FacetSettings', {
+       extend: 'Ext.tip.ToolTip',
+       anchor: 'left',
+       autoHide : false,
+       autoShow: false,
+       initComponent: function() {
+               this.rdgOperator = new Ext.form.RadioGroup({
+                       width: 130,
+                       items: [
+                               {
+                                       boxLabel: mw.message( 
'bs-extendedsearch-facetsetting-op-or' ).plain(),
+                                       name: 'op',
+                                       inputValue: 'OR',
+                                       checked: true
+                               },
+                               {
+                                       boxLabel: mw.message( 
'bs-extendedsearch-facetsetting-op-and' ).plain(),
+                                       name: 'op',
+                                       inputValue: 'AND'
+                               }
+                       ]
+               });
+               this.rdgOperator.on( 'change', this.settingChanged, this );
+
+               this.items = [
+                       this.rdgOperator
+               ];
+
+               this.addEvents( 'settingschange' );
+
+               this.callParent( arguments );
+       },
+
+       settingChanged: function( sender, newValue, oldValue, eOpts ) {
+               //TODO: When more settings are available, we need to walk 
through all
+               //fields and collect the whole "settings"
+               $( this.target.dom ).data( 'fset', newValue );
+               this.fireEvent( 'settingschange', newValue );
+               this.hide();
+       },
+
+       setData: function( data ) {
+               this.rdgOperator.suspendEvents( false ); //Do not reload the 
view
+               this.rdgOperator.setValue( data );
+               this.rdgOperator.resumeEvents();
+       }
+});
\ No newline at end of file
diff --git a/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.css 
b/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.css
index da84775..26fcc9d 100644
--- a/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.css
+++ b/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.css
@@ -362,4 +362,12 @@
 
 .bs-extendedsearch-sorting-sortby {
        float: right;
+}
+
+#bs-extendedsearch-filters .bs-es-facetsettings {
+       margin-left: 5px;
+}
+
+#bs-extendedsearch-filters .bs-extendedsearch-facetbox {
+       clear: both;
 }
\ No newline at end of file
diff --git a/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.js 
b/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.js
index 17db448..6c9609d 100644
--- a/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.js
+++ b/ExtendedSearch/resources/bluespice.extendedSearch.specialpage.js
@@ -204,7 +204,7 @@
 
                // facets armed with attribute urldiff...
                $( '[urldiff]' ).click( function() {
-                       ExtendedSearchAjaxManager.changeRequestFacets( $( this 
).attr( 'urldiff' ), $( this ).attr( 'checked' ) );
+                       ExtendedSearchAjaxManager.changeRequestFacets( $( this 
).attr( 'urldiff' ), $( this ).is( ':checked' ) );
                });
 
                $('#bs-extendedsearch-filters-results-paging A').click( 
function( event ) {
diff --git a/ExtendedSearch/resources/bluespice.facetsettings.js 
b/ExtendedSearch/resources/bluespice.facetsettings.js
new file mode 100644
index 0000000..15e616d
--- /dev/null
+++ b/ExtendedSearch/resources/bluespice.facetsettings.js
@@ -0,0 +1,37 @@
+$(document).on( 'click', '.bs-es-facetsettings', function( e ) {
+       e.preventDefault();
+       var me = this;
+
+       mw.loader.using( 'ext.bluespice.extjs', function() {
+               me.settingsFlyout = me.settingsFlyout || 
Ext.create('BS.ExtendedSearch.tip.FacetSettings', {
+                       target: me,
+                       listeners: {
+                               'hide' : function() {
+                                       //Disable ExtJS tooltip functionality 
to show up on mouse over
+                                       //We want it only to show on click
+                                       me.settingsFlyout.destroy();
+                                       me.settingsFlyout = null;
+                               },
+                               'settingschange': function() {
+                                       var fsets = {};
+                                       $( '.bs-es-facetsettings' ).each( 
function(){
+                                               var fset = $(me).data( 'fset' );
+                                               var fsetparam = $(me).data( 
'fset-param' );
+                                               fsets[fsetparam] = fset;
+                                       });
+
+                                       /* Unfortunately neither 
'ExtendedSearchAjaxManager' in
+                                        * general nor 'changeRequestFacets' 
provide a way to
+                                        * _replace_ a url fragment part. 
Therefore we just clean
+                                        * it up manually */
+                                       document.location.hash = 
document.location.hash.replace(/(&?)fset=.*?(&|$)/g, '$2');
+                                       
ExtendedSearchAjaxManager.changeRequestFacets( 'fset=' + JSON.stringify( fsets 
), true );
+                               }
+                       }
+               });
+               me.settingsFlyout.setData( $(me).data( 'fset' ) );
+               me.settingsFlyout.show();
+       });
+
+       return false;
+});
diff --git a/ExtendedSearch/views/view.ExtendedSearchFacetBox.php 
b/ExtendedSearch/views/view.ExtendedSearchFacetBox.php
index 340e85b..4c8340d 100644
--- a/ExtendedSearch/views/view.ExtendedSearchFacetBox.php
+++ b/ExtendedSearch/views/view.ExtendedSearchFacetBox.php
@@ -22,6 +22,14 @@
 class ViewSearchFacet extends ViewBaseElement {
 
        /**
+        * Basic configuration
+        * @var array
+        */
+       protected $aConfig = array(
+
+       );
+
+       /**
         * Number of checked facets. Used to determine whether overall checkbox 
should be checked as well
         * @var int Number of checked facets
         */
@@ -37,6 +45,17 @@
         */
        protected $aEntriesUnChecked = array();
 
+       public function __construct( $aConfig ) {
+               $this->aConfig = $aConfig;
+               $this->setOption( 'title', $aConfig['i18n'] );
+               $this->setOption( 'fset', array() );
+               if( isset( $aConfig['settings'] ) ) {
+                       $this->setOption( 'fset', $aConfig['settings'] );
+               }
+
+               parent::__construct();
+       }
+
        /**
         * Add facet either to checked or unchecked set.
         * @param array $dataSet Set of facets.
@@ -47,6 +66,9 @@
                }
                if ( isset( $dataSet['uri'] ) ) {
                        $dataSet['uri'] = htmlspecialchars( $dataSet['uri'], 
ENT_QUOTES, 'UTF-8' );
+               }
+               if( !isset( $dataSet['id'] ) ) {
+                       $dataSet['id'] = Sanitizer::escapeId( $dataSet['diff'] 
);
                }
 
                $dataSet['title'] = "{$dataSet['title']}";
@@ -69,8 +91,8 @@
         */
        public function setTemplate( $template ) {
                $out = '<div class="facetBarEntry" title="{title}">';
-               $out .= '<input type="checkbox"{checked} {diff} 
class="searchcheckbox" />';
-               $out .= '<label>{name-and-count}</label>';
+               $out .= '<input id="{id}" type="checkbox"{checked} {diff} 
class="searchcheckbox" />';
+               $out .= '<label for="{id}">{name-and-count}</label>';
                $out .= '</div>';
                parent::setTemplate( $out );
        }
@@ -82,15 +104,6 @@
        public function execute( $params = false ) {
                $this->setTemplate( '' );
 
-               $bChecked = ( $this->iEntriesChecked > 0 )
-                       ? true
-                       : false;
-
-               $sFacetHead = '<div class="bs-facet-title 
bs-extendedsearch-default-textspacing">';
-               $sFacetHead .= Xml::check( '', $bChecked, array( 'urldiff' => 
$this->getOption( 'uri-facet-all-diff' ) ) );
-               $sFacetHead .= '<label>' . wfMessage( $this->getOption( 'title' 
) )->plain(). '</label>';
-               $sFacetHead .= '</div>';
-
                $this->addCompleteDataset( $this->aEntriesChecked );
                $body = parent::execute();
 
@@ -101,10 +114,12 @@
                $this->addCompleteDataset( $this->aEntriesUnChecked );
                $body .= parent::execute();
 
-               if ( empty( $body ) ) return '';
+               if ( empty( $body ) ) {
+                       return '';
+               }
 
+               $sFacetHead = $this->makeFacetHead( $this->iEntriesChecked > 0 
);
                $body .= '<div class="bs-extendedsearch-facetend"></div><div 
class="bs-extendedsearch-facetbox-more"></div>';
-
                $body = Xml::openElement( 'div', array( 'class' => 
'bs-extendedsearch-facetbox-container' ) ) .
                                $body .
                                Xml::closeElement( 'div' );
@@ -115,4 +130,25 @@
                return $sFacetHead.$body;
        }
 
-}
\ No newline at end of file
+       public function makeFacetHead( $bChecked ) {
+               $sId = Sanitizer::escapeId( $this->getOption( 
'uri-facet-all-diff' ) );
+               $sFacetHead = '<div class="bs-facet-title 
bs-extendedsearch-default-textspacing">';
+               $sFacetHead .= Xml::check( '', $bChecked, array( 'id' => $sId, 
'urldiff' => $this->getOption( 'uri-facet-all-diff' ) ) );
+               $sFacetHead .= '<label for="'.$sId.'">' . wfMessage( 
$this->getOption( 'title' ) )->plain(). '</label>';
+               if( isset( $this->aConfig['settings'] ) ) {
+                       $sFacetHead .= Html::element(
+                               'a',
+                               array(
+                                       'href' => '#',
+                                       'class' => 'bs-es-facetsettings 
icon-wrench',
+                                       'data-fset-param' => 
$this->aConfig['param'],
+                                       'data-fset' => FormatJson::encode( 
$this->getOption( 'fset' ) )
+                               ),
+                               ''
+                       );
+               }
+               $sFacetHead .= '</div>';
+               return $sFacetHead;
+       }
+
+}
diff --git a/ExtendedSearch/views/view.SearchResult.php 
b/ExtendedSearch/views/view.SearchResult.php
index 65668ac..12a95dc 100644
--- a/ExtendedSearch/views/view.SearchResult.php
+++ b/ExtendedSearch/views/view.SearchResult.php
@@ -35,7 +35,7 @@
        protected $aResultEntryView = array();
        /**
         * List of facet boxes.
-        * @var array List of ViewExtendedSearchFacetBox.
+        * @var ViewSearchFacet[] List of ViewSearchFacet.
         */
        protected $aFacetBoxes = array();
 
@@ -305,10 +305,18 @@
                return implode( "\n" , $aOut );
        }
 
+       /**
+        *
+        * @param ViewSearchFacet $oFacet
+        */
        public function setFacet( $oFacet ) {
                $this->aFacetBoxes[] = $oFacet;
        }
 
+       /**
+        *
+        * @return ViewSearchFacet[]
+        */
        public function getFacets() {
                return $this->aFacetBoxes;
        }

-- 
To view, visit https://gerrit.wikimedia.org/r/282107
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: If05ee37071ce9516253e514d336b11db13e6bc78
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/extensions/BlueSpiceExtensions
Gerrit-Branch: REL1_23
Gerrit-Owner: Mglaser <[email protected]>
Gerrit-Reviewer: Dvogel hallowelt <[email protected]>
Gerrit-Reviewer: Ljonka <[email protected]>
Gerrit-Reviewer: Mglaser <[email protected]>
Gerrit-Reviewer: Pwirth <[email protected]>
Gerrit-Reviewer: Robert Vogel <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to