if ( $this->allowedParams !== null ) {
return $this->allowedParams;
}
- $this->allowedParams = [
- 'search' => null,
- 'limit' => [
- ApiBase::PARAM_DFLT => $this->getConfig()->get( 'OpenSearchDefaultLimit' ),
- ApiBase::PARAM_TYPE => 'limit',
- ApiBase::PARAM_MIN => 1,
- ApiBase::PARAM_MAX => 100,
- ApiBase::PARAM_MAX2 => 100
- ],
- 'namespace' => [
- ApiBase::PARAM_DFLT => NS_MAIN,
- ApiBase::PARAM_TYPE => 'namespace',
- ApiBase::PARAM_ISMULTI => true
- ],
+ $this->allowedParams = $this->buildCommonApiParams( false ) + [
'suggest' => false,
'redirects' => [
ApiBase::PARAM_TYPE => [ 'return', 'resolve' ],
'warningsaserror' => false,
];
- $profileParam = $this->buildProfileApiParam( SearchEngine::COMPLETION_PROFILE_TYPE,
- 'apihelp-query+prefixsearch-param-profile' );
- if ( $profileParam ) {
- $this->allowedParams['profile'] = $profileParam;
- }
+ // Use open search specific default limit
+ $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get(
+ 'OpenSearchDefaultLimit'
+ );
+
return $this->allowedParams;
}
public function getSearchProfileParams() {
- if ( isset( $this->getAllowedParams()['profile'] ) ) {
- return [ SearchEngine::COMPLETION_PROFILE_TYPE => 'profile' ];
- }
- return [];
+ return [
+ 'qiprofile' => [
+ 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
+ 'help-message' => 'apihelp-query+prefixsearch-param-profile'
+ ],
+ ];
}
protected function getExamplesMessages() {
if ( $this->allowedParams !== null ) {
return $this->allowedParams;
}
- $this->allowedParams = [
- 'search' => [
- ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_REQUIRED => true,
- ],
- 'namespace' => [
- ApiBase::PARAM_DFLT => NS_MAIN,
- ApiBase::PARAM_TYPE => 'namespace',
- ApiBase::PARAM_ISMULTI => true,
- ],
- 'limit' => [
- ApiBase::PARAM_DFLT => 10,
- ApiBase::PARAM_TYPE => 'limit',
- ApiBase::PARAM_MIN => 1,
- // Non-standard value for compatibility with action=opensearch
- ApiBase::PARAM_MAX => 100,
- ApiBase::PARAM_MAX2 => 200,
- ],
- 'offset' => [
- ApiBase::PARAM_DFLT => 0,
- ApiBase::PARAM_TYPE => 'integer',
- ],
- ];
- $profileParam = $this->buildProfileApiParam( SearchEngine::COMPLETION_PROFILE_TYPE,
- 'apihelp-query+prefixsearch-param-profile' );
- if ( $profileParam ) {
- $this->allowedParams['profile'] = $profileParam;
- }
+ $this->allowedParams = $this->buildCommonApiParams();
+
return $this->allowedParams;
}
public function getSearchProfileParams() {
- if ( isset( $this->getAllowedParams()['profile'] ) ) {
- return [ SearchEngine::COMPLETION_PROFILE_TYPE => 'profile' ];
- }
- return [];
+ return [
+ 'profile' => [
+ 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
+ 'help-message' => 'apihelp-query+prefixsearch-param-profile',
+ ],
+ ];
}
protected function getExamplesMessages() {
/** @var array list of api allowed params */
private $allowedParams;
- /**
- * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't
- * a valid option for an array for PARAM_TYPE, so we'll use a fake name
- * that can't possibly be a class name and describes what the null behavior
- * does
- */
- const BACKEND_NULL_PARAM = 'database-backed';
-
public function __construct( ApiQuery $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'sr' );
}
global $wgContLang;
$params = $this->extractRequestParams();
- if ( isset( $params['backend'] ) && $params['backend'] == self::BACKEND_NULL_PARAM ) {
- unset( $params['backend'] );
- }
-
// Extract parameters
$query = $params['search'];
$what = $params['what'];
return $this->allowedParams;
}
- $this->allowedParams = [
- 'search' => [
- ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_REQUIRED => true
- ],
- 'namespace' => [
- ApiBase::PARAM_DFLT => NS_MAIN,
- ApiBase::PARAM_TYPE => 'namespace',
- ApiBase::PARAM_ISMULTI => true,
- ],
+ $this->allowedParams = $this->buildCommonApiParams() + [
'what' => [
ApiBase::PARAM_TYPE => [
'title',
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
],
- 'offset' => [
- ApiBase::PARAM_DFLT => 0,
- ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
- ],
- 'limit' => [
- ApiBase::PARAM_DFLT => 10,
- ApiBase::PARAM_TYPE => 'limit',
- ApiBase::PARAM_MIN => 1,
- ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
- ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
- ],
'interwiki' => false,
'enablerewrites' => false,
];
- $searchConfig = MediaWikiServices::getInstance()->getSearchEngineConfig();
- $alternatives = $searchConfig->getSearchTypes();
- if ( count( $alternatives ) > 1 ) {
- if ( $alternatives[0] === null ) {
- $alternatives[0] = self::BACKEND_NULL_PARAM;
- }
- $this->allowedParams['backend'] = [
- ApiBase::PARAM_DFLT => $searchConfig->getSearchType(),
- ApiBase::PARAM_TYPE => $alternatives,
- ];
- // @todo: support profile selection when multiple
- // backends are available. The solution could be to
- // merge all possible profiles and let ApiBase
- // subclasses do the check. Making ApiHelp and ApiSandbox
- // comprehensive might be more difficult.
- } else {
- $profileParam = $this->buildProfileApiParam( SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE,
- 'apihelp-query+search-param-qiprofile' );
- if ( $profileParam ) {
- $this->allowedParams['qiprofile'] = $profileParam;
- }
- }
-
return $this->allowedParams;
}
public function getSearchProfileParams() {
- if ( isset( $this->getAllowedParams()['qiprofile'] ) ) {
- return [ SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE => 'qiprofile' ];
- }
- return [];
+ return [
+ 'qiprofile' => [
+ 'profile-type' => SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE,
+ 'help-message' => 'apihelp-query+search-param-qiprofile',
+ ],
+ ];
}
protected function getExamplesMessages() {
* @ingroup API
*/
trait SearchApi {
+
+ /**
+ * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't
+ * a valid option for an array for PARAM_TYPE, so we'll use a fake name
+ * that can't possibly be a class name and describes what the null behavior
+ * does
+ */
+ private static $BACKEND_NULL_PARAM = 'database-backed';
+
/**
- * Build the profile api param definitions.
+ * The set of api parameters that are shared between api calls that
+ * call the SearchEngine. Primarily this defines parameters that
+ * are utilized by self::buildSearchEngine().
*
- * @param string $profileType type of profile to customize
- * @param string $helpMsg i18n message
- * @param string|null $backendType SearchEngine backend type or null for default engine
- * @return array|null the api param definition or null if profiles are
- * not supported by the searchEngine implementation.
+ * @param bool $isScrollable True if the api offers scrolling
+ * @return array
*/
- public function buildProfileApiParam( $profileType, $helpMsg, $backendType = null ) {
- $searchEngine = null;
- if ( $backendType !== null ) {
- $searchEngine = MediaWikiServices::getInstance()
- ->getSearchEngineFactory()->create( $backendType );
+ public function buildCommonApiParams( $isScrollable = true ) {
+ $params = [
+ 'search' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'namespace' => [
+ ApiBase::PARAM_DFLT => NS_MAIN,
+ ApiBase::PARAM_TYPE => 'namespace',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'limit' => [
+ ApiBase::PARAM_DFLT => 10,
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
+ ],
+ ];
+ if ( $isScrollable ) {
+ $params['offset'] = [
+ ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+ ];
+ }
+
+ $searchConfig = MediaWikiServices::getInstance()->getSearchEngineConfig();
+ $alternatives = $searchConfig->getSearchTypes();
+ if ( count( $alternatives ) > 1 ) {
+ if ( $alternatives[0] === null ) {
+ $alternatives[0] = self::$BACKEND_NULL_PARAM;
+ }
+ $this->allowedParams['backend'] = [
+ ApiBase::PARAM_DFLT => $searchConfig->getSearchType(),
+ ApiBase::PARAM_TYPE => $alternatives,
+ ];
+ // @todo: support profile selection when multiple
+ // backends are available. The solution could be to
+ // merge all possible profiles and let ApiBase
+ // subclasses do the check. Making ApiHelp and ApiSandbox
+ // comprehensive might be more difficult.
} else {
- $searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
+ $params += $this->buildProfileApiParam();
}
- $profiles = $searchEngine->getProfiles( $profileType );
- if ( $profiles ) {
+ return $params;
+ }
+
+ /**
+ * Build the profile api param definitions. Makes bold assumption only one search
+ * engine is available, ensure that is true before calling.
+ *
+ * @return array array containing available additional api param definitions.
+ * Empty if profiles are not supported by the searchEngine implementation.
+ */
+ private function buildProfileApiParam() {
+ $configs = $this->getSearchProfileParams();
+ $searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
+ $params = [];
+ foreach ( $configs as $paramName => $paramConfig ) {
+ $profiles = $searchEngine->getProfiles( $paramConfig['profile-type'] );
+ if ( !$profiles ) {
+ continue;
+ }
+
$types = [];
$helpMessages = [];
$defaultProfile = null;
$defaultProfile = $profile['name'];
}
}
- return [
+
+ $params[$paramName] = [
ApiBase::PARAM_TYPE => $types,
- ApiBase::PARAM_HELP_MSG => $helpMsg,
+ ApiBase::PARAM_HELP_MSG => $paramConfig['help-message'],
ApiBase::PARAM_HELP_MSG_PER_VALUE => $helpMessages,
ApiBase::PARAM_DFLT => $defaultProfile,
];
}
- return null;
+
+ return $params;
}
/**
* Build the search engine to use.
* If $params is provided then the following searchEngine options
* will be set:
+ * - backend: which search backend to use
* - limit: mandatory
* - offset: optional, if set limit will be incremented by
* one ( to support the continue parameter )
public function buildSearchEngine( array $params = null ) {
if ( $params != null ) {
$type = isset( $params['backend'] ) ? $params['backend'] : null;
+ if ( $type === self::$BACKEND_NULL_PARAM ) {
+ $type = null;
+ }
$searchEngine = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type );
$limit = $params['limit'];
$searchEngine->setNamespaces( $params['namespace'] );
$limit += 1;
}
$searchEngine->setLimitOffset( $limit, $offset );
- foreach ( $this->getSearchProfileParams() as $type => $param ) {
- if ( isset( $params[$param] ) ) {
- $searchEngine->setFeatureData( $type, $params[$param] );
+
+ // Initialize requested search profiles.
+ $configs = $this->getSearchProfileParams();
+ foreach ( $configs as $paramName => $paramConfig ) {
+ if ( isset( $params[$paramName] ) ) {
+ $searchEngine->setFeatureData(
+ $paramConfig['profile-type'],
+ $params[$paramName]
+ );
}
}
} else {
}
/**
- * @return string[] the list of supported search profile types. Key is
- * the profile type and its associated value is the request param.
+ * @return array[] array of arrays mapping from parameter name to a two value map
+ * containing 'help-message' and 'profile-type' keys.
*/
abstract public function getSearchProfileParams();
}
return new IdentityCollation;
case 'uca-default':
return new IcuCollation( 'root' );
+ case 'uca-default-u-kn':
+ return new IcuCollation( 'root-u-kn' );
case 'xx-uca-ckb':
return new CollationCkb;
case 'xx-uca-et':
/** @var Language */
protected $digitTransformLanguage;
+ /** @var boolean */
+ private $useNumericCollation = false;
+
/** @var array */
private $firstLetterData;
$this->primaryCollator = Collator::create( $locale );
$this->primaryCollator->setStrength( Collator::PRIMARY );
+
+ // If the special suffix for numeric collation is present, turn on numeric collation.
+ if ( substr( $locale, -5, 5 ) === '-u-kn' ) {
+ $this->useNumericCollation = true;
+ // Strip off the special suffix so it doesn't trip up fetchFirstLetterData().
+ $this->locale = substr( $this->locale, 0, -5 );
+ $this->mainCollator->setAttribute( Collator::NUMERIC_COLLATION, Collator::ON );
+ $this->primaryCollator->setAttribute( Collator::NUMERIC_COLLATION, Collator::ON );
+ }
}
public function getSortKey( $string ) {
return '';
}
- // Check for CJK
$firstChar = mb_substr( $string, 0, 1, 'UTF-8' );
+
+ // If the first character is a CJK character, just return that character.
if ( ord( $firstChar ) > 0x7f && self::isCjk( UtfNormal\Utils::utf8ToCodepoint( $firstChar ) ) ) {
return $firstChar;
}
// Before the first letter
return '';
}
- return $this->getLetterByIndex( $min );
+
+ $sortLetter = $this->getLetterByIndex( $min );
+
+ if ( $this->useNumericCollation ) {
+ // If the sort letter is a number, return '0–9' (or localized equivalent).
+ // ASCII value of 0 is 48. ASCII value of 9 is 57.
+ // Note that this also applies to non-Arabic numerals since they are
+ // mapped to Arabic numeral sort letters. For example, ২ sorts as 2.
+ if ( ord( $sortLetter ) >= 48 && ord( $sortLetter ) <= 57 ) {
+ $sortLetter = wfMessage( 'category-header-numerals' )->numParams( 0, 9 )->text();
+ }
+ }
+ return $sortLetter;
}
/**
}
/**
+ * Test if a code point is a CJK (Chinese, Japanese, Korean) character
* @since 1.16.3
*/
public static function isCjk( $codepoint ) {
"noindex-category": "Noindexed pages",
"broken-file-category": "Pages with broken file links",
"categoryviewer-pagedlinks": "($1) ($2)",
+ "category-header-numerals": "$1–$2",
"about": "About",
"article": "Content page",
"newwindow": "(opens in new window)",
"noindex-category": "Name of the [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]] where pages with the <nowiki>__NOINDEX__</nowiki> behavior switch are listed.\n\nFor description of this behavior switch see [[mw:Special:MyLanguage/Help:Magic_words#Behavior_switches|MediaWiki]].\n\nSee also:\n* {{msg-mw|Noindex-category-desc}}",
"broken-file-category": "Name of [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]] where pages that embed files that do not exist (\"broken images\") are listed.\n\nSee also:\n* {{msg-mw|Broken-file-category-desc}}",
"categoryviewer-pagedlinks": "{{Optional}}\nThe pagination links in category viewer. Parameters:\n* $1 - the previous link, uses {{msg-mw|Prevn}}\n* $2 - the next link, uses {{msg-mw|Nextn}}",
+ "category-header-numerals": "{{Optional}}\nA header for all pages whose titles start with a number. This is used on category pages. This should only be translated if your language uses a different method to indicate a range of numbers (other than a dash).\n* $1 - 0 (or localized equivalent)\n* $2 – 9 (or localized equivalent)",
"about": "{{Identical|About}}",
"article": "A 'content page' is a page that forms part of the purpose of the wiki. It includes the main page and pages in the main namespace and any other namespaces that are included when the wiki is customised. For example on Wikimedia Commons 'content pages' include pages in the file and category namespaces. On Wikinews 'content pages' include pages in the Portal namespace. For technical definition of 'content namespaces' see [[mw:Manual:Using_custom_namespaces#Content_namespaces|MediaWiki]].\n\nPossible alternatives to the word 'content' are 'subject matter' or 'wiki subject' or 'wiki purpose'.\n\n{{Identical|Content page}}",
"newwindow": "Below the edit form, next to \"{{msg-mw|Edithelp}}\".",
--- /dev/null
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+class ApiOpenSearchTest extends MediaWikiTestCase {
+ public function testGetAllowedParams() {
+ $config = $this->replaceSearchEngineConfig();
+ $config->expects( $this->any() )
+ ->method( 'getSearchTypes' )
+ ->will( $this->returnValue( [ 'the one ring' ] ) );
+
+ $engine = $this->replaceSearchEngine();
+ $engine->expects( $this->any() )
+ ->method( 'getProfiles' )
+ ->will( $this->returnValueMap( [
+ [ SearchEngine::COMPLETION_PROFILE_TYPE, [
+ [
+ 'name' => 'normal',
+ 'desc-message' => 'normal-message',
+ 'default' => true,
+ ],
+ [
+ 'name' => 'strict',
+ 'desc-message' => 'strict-message',
+ ],
+ ] ],
+ ] ) );
+
+ $api = $this->createApi();
+ $params = $api->getAllowedParams();
+
+ $this->assertArrayNotHasKey( 'offset', $params );
+ $this->assertArrayHasKey( 'qiprofile', $params, print_r( $params, true ) );
+ $this->assertEquals( 'normal', $params['qiprofile'][ApiBase::PARAM_DFLT] );
+ }
+
+ private function replaceSearchEngineConfig() {
+ $config = $this->getMockBuilder( 'SearchEngineConfig' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->setService( 'SearchEngineConfig', $config );
+
+ return $config;
+ }
+
+ private function replaceSearchEngine() {
+ $engine = $this->getMockBuilder( 'SearchEngine' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $engineFactory = $this->getMockBuilder( 'SearchEngineFactory' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $engineFactory->expects( $this->any() )
+ ->method( 'create' )
+ ->will( $this->returnValue( $engine ) );
+ $this->setService( 'SearchEngineFactory', $engineFactory );
+
+ return $engine;
+ }
+
+ private function createApi() {
+ $ctx = new RequestContext();
+ $apiMain = new ApiMain( $ctx );
+ return new ApiOpenSearch( $apiMain, 'opensearch', '' );
+ }
+}