Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.95% covered (warning)
80.95%
51 / 63
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryFunctions
80.95% covered (warning)
80.95%
51 / 63
0.00% covered (danger)
0.00%
0 / 1
22.76
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 run
80.95% covered (warning)
80.95%
51 / 63
0.00% covered (danger)
0.00%
0 / 1
17.77
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
2
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * WikiLambda API to search functions
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda\ActionAPI;
12
13use MediaWiki\Api\ApiBase;
14use MediaWiki\Api\ApiQuery;
15use MediaWiki\Extension\WikiLambda\HttpStatus;
16use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
17use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry;
18use MediaWiki\Extension\WikiLambda\ZErrorFactory;
19use MediaWiki\Extension\WikiLambda\ZObjectStore;
20use MediaWiki\Extension\WikiLambda\ZObjectUtils;
21use MediaWiki\Language\LanguageFallback;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Title\Title;
24use Wikimedia\ParamValidator\ParamValidator;
25use Wikimedia\ParamValidator\TypeDef\IntegerDef;
26
27class ApiQueryFunctions extends WikiLambdaApiQueryGeneratorBase {
28
29    protected LanguageFallback $languageFallback;
30    protected ZLangRegistry $langRegistry;
31
32    /**
33     * @codeCoverageIgnore
34     */
35    public function __construct(
36        ApiQuery $query,
37        string $moduleName,
38        LanguageFallback $languageFallback,
39        protected readonly ZObjectStore $zObjectStore
40    ) {
41        parent::__construct( $query, $moduleName, 'wikilambdasearch_functions_' );
42
43        $this->languageFallback = $languageFallback;
44        $this->langRegistry = ZLangRegistry::singleton();
45    }
46
47    /**
48     * @inheritDoc
49     */
50    protected function run( $resultPageSet = null ) {
51        // Exit if we're running in non-repo mode (e.g. on a client wiki)
52        if ( !$this->getConfig()->get( 'WikiLambdaEnableRepoMode' ) ) {
53            WikiLambdaApiBase::dieWithZError(
54                ZErrorFactory::createZErrorInstance(
55                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN,
56                    []
57                ),
58                HttpStatus::BAD_REQUEST
59            );
60        }
61
62        [
63            'search' => $searchTerm,
64            'language' => $language,
65            'renderable' => $renderable,
66            'input_types' => $inputTypes,
67            'output_type' => $outputType,
68            'limit' => $limit,
69            'continue' => $continue,
70        ] = $this->extractRequestParams();
71
72        $languageZids = $this->langRegistry->getListOfFallbackLanguageZids( $this->languageFallback, $language );
73        $outputType = $outputType ?: null;
74        $inputArray = $inputTypes ? array_count_values( $inputTypes ) : [];
75
76        $res = $this->zObjectStore->searchFunctions(
77            $searchTerm,
78            $languageZids,
79            $renderable,
80            $inputArray,
81            $outputType
82        );
83
84        // 1. Set match_rate for every entry and eliminate duplicates with lower match rates
85        $matches = [];
86        $hasSearchTerm = ( $searchTerm !== '' );
87        $matchField = ZObjectUtils::isValidZObjectReference( $searchTerm ) ? 'wlzl_zobject_zid' : 'wlzl_label';
88
89        foreach ( $res as $row ) {
90            $matchRate = $hasSearchTerm ? self::getMatchRate( $searchTerm, $row->{ $matchField } ) : 0;
91
92            // If the current row is new or a better match, keep. Else, ignore.
93            if ( !array_key_exists( $row->wlzl_zobject_zid, $matches ) ||
94                ( $matches[ $row->wlzl_zobject_zid ][ 'match_rate' ] < $matchRate ) ) {
95                $matches[ $row->wlzl_zobject_zid ] = [
96                    'page_id' => $row->page_id,
97                    // TODO (T258915): When we support redirects, implement.
98                    'page_is_redirect' => false,
99                    'page_namespace' => NS_MAIN,
100                    'page_content_model' => CONTENT_MODEL_ZOBJECT,
101                    'page_title' => $row->wlzl_zobject_zid,
102                    'match_label' => $hasSearchTerm ? $row->{ $matchField } : null,
103                    'match_lang' => $hasSearchTerm ? $row->wlzl_language : null,
104                    'match_rate' => $matchRate,
105                    'label' => $row->preferred_label,
106                    'language' => $row->preferred_language
107                ];
108            }
109        }
110
111        // 2. Sort all results by match_rate to get best hits
112        usort( $matches, static function ( $a, $b ) {
113            return $b[ 'match_rate' ] <=> $a[ 'match_rate' ];
114        } );
115
116        // 3. Prune the result set to the limit, slice to requested page, and set continue
117        $continue = $continue === null ? 0 : intval( $continue );
118        $hits = array_slice( $matches, $continue * $limit, $limit );
119        $pageSize = count( $matches ) - ( $continue * $limit );
120        if ( $pageSize > $limit ) {
121            $this->setContinueEnumParameter( 'continue', strval( $continue + 1 ) );
122        }
123
124        if ( $resultPageSet ) {
125            foreach ( $hits as $index => $entry ) {
126                $resultPageSet->setGeneratorData(
127                    Title::makeTitle( $entry['page_namespace'], $entry['page_title'] ),
128                    [ 'index' => $index + $continue + 1 ]
129                );
130            }
131        } else {
132            $result = $this->getResult();
133            foreach ( $hits as $entry ) {
134                $result->addValue( [ 'query', $this->getModuleName() ], null, $entry );
135            }
136        }
137    }
138
139    /**
140     * @inheritDoc
141     * @codeCoverageIgnore
142     */
143    protected function getAllowedParams(): array {
144        // Don't try to read the supported languages from the DB on client wikis, we can't.
145        $supportedLanguageCodes =
146            ( MediaWikiServices::getInstance()->getMainConfig()->get( 'WikiLambdaEnableRepoMode' ) ) ?
147            $this->zObjectStore->fetchAllZLanguageCodes() :
148            [];
149
150        return [
151            'search' => [
152                ParamValidator::PARAM_TYPE => 'string',
153                ParamValidator::PARAM_DEFAULT => '',
154            ],
155            'language' => [
156                ParamValidator::PARAM_TYPE => $supportedLanguageCodes,
157                ParamValidator::PARAM_REQUIRED => true,
158            ],
159            'renderable' => [
160                ParamValidator::PARAM_TYPE => 'boolean',
161                ParamValidator::PARAM_DEFAULT => false,
162            ],
163            'input_types' => [
164                ParamValidator::PARAM_TYPE => 'string',
165                ParamValidator::PARAM_ISMULTI => true,
166                ParamValidator::PARAM_ALLOW_DUPLICATES => true,
167                ParamValidator::PARAM_REQUIRED => false,
168            ],
169            'output_type' => [
170                ParamValidator::PARAM_TYPE => 'string',
171                ParamValidator::PARAM_REQUIRED => false,
172            ],
173            'limit' => [
174                ParamValidator::PARAM_TYPE => 'limit',
175                ParamValidator::PARAM_DEFAULT => 10,
176                IntegerDef::PARAM_MIN => 1,
177                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
178                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
179            ],
180            'continue' => [
181                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
182            ],
183        ];
184    }
185
186    /**
187     * @see ApiBase::getExamplesMessages()
188     * @return array
189     * @codeCoverageIgnore
190     */
191    protected function getExamplesMessages() {
192        return [
193            // Functions with label that matches 'Engl' and have renderable IOs
194            'action=query&list=wikilambdasearch_functions'
195            . '&wikilambdasearch_functions_search=Engl'
196            . '&wikilambdasearch_functions_language=en'
197            . '&wikilambdasearch_functions_renderable=1'
198            => 'apihelp-query+wikilambdasearch_functions-example-renderable',
199            // Functions that have at least two String inputs and output String
200            'action=query&list=wikilambdasearch_functions'
201            . '&wikilambdasearch_functions_language=en'
202            . '&wikilambdasearch_functions_input_types=Z6%7CZ6'
203            . '&wikilambdasearch_functions_output_type=Z6'
204            => 'apihelp-query+wikilambdasearch_functions-example-io-types',
205        ];
206    }
207}