This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Created on Oct 13, 2006 |
||
4 | * |
||
5 | * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
||
6 | * Copyright © 2008 Brion Vibber <[email protected]> |
||
7 | * Copyright © 2014 Brad Jorsch <[email protected]> |
||
8 | * |
||
9 | * This program is free software; you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation; either version 2 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * This program is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License along |
||
20 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
21 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
22 | * http://www.gnu.org/copyleft/gpl.html |
||
23 | * |
||
24 | * @file |
||
25 | */ |
||
26 | |||
27 | use MediaWiki\MediaWikiServices; |
||
28 | |||
29 | /** |
||
30 | * @ingroup API |
||
31 | */ |
||
32 | class ApiOpenSearch extends ApiBase { |
||
33 | use SearchApi; |
||
34 | |||
35 | private $format = null; |
||
36 | private $fm = null; |
||
37 | |||
38 | /** @var array list of api allowed params */ |
||
39 | private $allowedParams = null; |
||
40 | |||
41 | /** |
||
42 | * Get the output format |
||
43 | * |
||
44 | * @return string |
||
45 | */ |
||
46 | protected function getFormat() { |
||
47 | if ( $this->format === null ) { |
||
48 | $params = $this->extractRequestParams(); |
||
49 | $format = $params['format']; |
||
50 | |||
51 | $allowedParams = $this->getAllowedParams(); |
||
52 | if ( !in_array( $format, $allowedParams['format'][ApiBase::PARAM_TYPE] ) ) { |
||
53 | $format = $allowedParams['format'][ApiBase::PARAM_DFLT]; |
||
54 | } |
||
55 | |||
56 | if ( substr( $format, -2 ) === 'fm' ) { |
||
57 | $this->format = substr( $format, 0, -2 ); |
||
58 | $this->fm = 'fm'; |
||
59 | } else { |
||
60 | $this->format = $format; |
||
61 | $this->fm = ''; |
||
62 | } |
||
63 | } |
||
64 | return $this->format; |
||
65 | } |
||
66 | |||
67 | public function getCustomPrinter() { |
||
68 | switch ( $this->getFormat() ) { |
||
69 | case 'json': |
||
70 | return new ApiOpenSearchFormatJson( |
||
71 | $this->getMain(), $this->fm, $this->getParameter( 'warningsaserror' ) |
||
72 | ); |
||
73 | |||
74 | case 'xml': |
||
75 | $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm ); |
||
76 | $printer->setRootElement( 'SearchSuggestion' ); |
||
77 | return $printer; |
||
78 | |||
79 | default: |
||
80 | ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" ); |
||
81 | } |
||
82 | } |
||
83 | |||
84 | public function execute() { |
||
85 | $params = $this->extractRequestParams(); |
||
86 | $search = $params['search']; |
||
87 | $suggest = $params['suggest']; |
||
88 | $results = []; |
||
89 | if ( !$suggest || $this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) { |
||
90 | // Open search results may be stored for a very long time |
||
91 | $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) ); |
||
92 | $this->getMain()->setCacheMode( 'public' ); |
||
93 | $results = $this->search( $search, $params ); |
||
94 | |||
95 | // Allow hooks to populate extracts and images |
||
96 | Hooks::run( 'ApiOpenSearchSuggest', [ &$results ] ); |
||
97 | |||
98 | // Trim extracts, if necessary |
||
99 | $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' ); |
||
100 | foreach ( $results as &$r ) { |
||
101 | if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) { |
||
102 | $r['extract'] = self::trimExtract( $r['extract'], $length ); |
||
103 | } |
||
104 | } |
||
105 | } |
||
106 | |||
107 | // Populate result object |
||
108 | $this->populateResult( $search, $results ); |
||
109 | } |
||
110 | |||
111 | /** |
||
112 | * Perform the search |
||
113 | * @param string $search the search query |
||
114 | * @param array $params api request params |
||
115 | * @return array search results. Keys are integers. |
||
116 | */ |
||
117 | private function search( $search, array $params ) { |
||
118 | $searchEngine = $this->buildSearchEngine( $params ); |
||
119 | $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); |
||
120 | $results = []; |
||
121 | |||
122 | if ( !$titles ) { |
||
123 | return $results; |
||
124 | } |
||
125 | |||
126 | // Special pages need unique integer ids in the return list, so we just |
||
127 | // assign them negative numbers because those won't clash with the |
||
128 | // always positive articleIds that non-special pages get. |
||
129 | $nextSpecialPageId = -1; |
||
130 | |||
131 | if ( $params['redirects'] === null ) { |
||
132 | // Backwards compatibility, don't resolve for JSON. |
||
133 | $resolveRedir = $this->getFormat() !== 'json'; |
||
134 | } else { |
||
135 | $resolveRedir = $params['redirects'] === 'resolve'; |
||
136 | } |
||
137 | |||
138 | if ( $resolveRedir ) { |
||
139 | // Query for redirects |
||
140 | $redirects = []; |
||
141 | $lb = new LinkBatch( $titles ); |
||
142 | if ( !$lb->isEmpty() ) { |
||
143 | $db = $this->getDB(); |
||
144 | $res = $db->select( |
||
145 | [ 'page', 'redirect' ], |
||
146 | [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ], |
||
147 | [ |
||
148 | 'rd_from = page_id', |
||
149 | 'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ), |
||
150 | $lb->constructSet( 'page', $db ), |
||
151 | ], |
||
152 | __METHOD__ |
||
153 | ); |
||
154 | foreach ( $res as $row ) { |
||
0 ignored issues
–
show
|
|||
155 | $redirects[$row->page_namespace][$row->page_title] = |
||
156 | [ $row->rd_namespace, $row->rd_title ]; |
||
157 | } |
||
158 | } |
||
159 | |||
160 | // Bypass any redirects |
||
161 | $seen = []; |
||
162 | foreach ( $titles as $title ) { |
||
163 | $ns = $title->getNamespace(); |
||
164 | $dbkey = $title->getDBkey(); |
||
165 | $from = null; |
||
166 | if ( isset( $redirects[$ns][$dbkey] ) ) { |
||
167 | list( $ns, $dbkey ) = $redirects[$ns][$dbkey]; |
||
168 | $from = $title; |
||
169 | $title = Title::makeTitle( $ns, $dbkey ); |
||
170 | } |
||
171 | if ( !isset( $seen[$ns][$dbkey] ) ) { |
||
172 | $seen[$ns][$dbkey] = true; |
||
173 | $resultId = $title->getArticleID(); |
||
174 | if ( $resultId === 0 ) { |
||
175 | $resultId = $nextSpecialPageId; |
||
176 | $nextSpecialPageId -= 1; |
||
177 | } |
||
178 | $results[$resultId] = [ |
||
179 | 'title' => $title, |
||
180 | 'redirect from' => $from, |
||
181 | 'extract' => false, |
||
182 | 'extract trimmed' => false, |
||
183 | 'image' => false, |
||
184 | 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ), |
||
185 | ]; |
||
186 | } |
||
187 | } |
||
188 | } else { |
||
189 | foreach ( $titles as $title ) { |
||
190 | $resultId = $title->getArticleID(); |
||
191 | if ( $resultId === 0 ) { |
||
192 | $resultId = $nextSpecialPageId; |
||
193 | $nextSpecialPageId -= 1; |
||
194 | } |
||
195 | $results[$resultId] = [ |
||
196 | 'title' => $title, |
||
197 | 'redirect from' => null, |
||
198 | 'extract' => false, |
||
199 | 'extract trimmed' => false, |
||
200 | 'image' => false, |
||
201 | 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ), |
||
202 | ]; |
||
203 | } |
||
204 | } |
||
205 | |||
206 | return $results; |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * @param string $search |
||
211 | * @param array &$results |
||
212 | */ |
||
213 | protected function populateResult( $search, &$results ) { |
||
214 | $result = $this->getResult(); |
||
215 | |||
216 | switch ( $this->getFormat() ) { |
||
217 | case 'json': |
||
218 | // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1 |
||
219 | $result->addArrayType( null, 'array' ); |
||
220 | $result->addValue( null, 0, strval( $search ) ); |
||
221 | $terms = []; |
||
222 | $descriptions = []; |
||
223 | $urls = []; |
||
224 | foreach ( $results as $r ) { |
||
225 | $terms[] = $r['title']->getPrefixedText(); |
||
226 | $descriptions[] = strval( $r['extract'] ); |
||
227 | $urls[] = $r['url']; |
||
228 | } |
||
229 | $result->addValue( null, 1, $terms ); |
||
230 | $result->addValue( null, 2, $descriptions ); |
||
231 | $result->addValue( null, 3, $urls ); |
||
232 | break; |
||
233 | |||
234 | case 'xml': |
||
235 | // http://msdn.microsoft.com/en-us/library/cc891508%28v=vs.85%29.aspx |
||
236 | $imageKeys = [ |
||
237 | 'source' => true, |
||
238 | 'alt' => true, |
||
239 | 'width' => true, |
||
240 | 'height' => true, |
||
241 | 'align' => true, |
||
242 | ]; |
||
243 | $items = []; |
||
244 | foreach ( $results as $r ) { |
||
245 | $item = [ |
||
246 | 'Text' => $r['title']->getPrefixedText(), |
||
247 | 'Url' => $r['url'], |
||
248 | ]; |
||
249 | if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) { |
||
250 | $item['Description'] = $r['extract']; |
||
251 | } |
||
252 | if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) { |
||
253 | $item['Image'] = array_intersect_key( $r['image'], $imageKeys ); |
||
254 | } |
||
255 | ApiResult::setSubelementsList( $item, array_keys( $item ) ); |
||
256 | $items[] = $item; |
||
257 | } |
||
258 | ApiResult::setIndexedTagName( $items, 'Item' ); |
||
259 | $result->addValue( null, 'version', '2.0' ); |
||
260 | $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' ); |
||
261 | $result->addValue( null, 'Query', strval( $search ) ); |
||
262 | $result->addSubelementsList( null, 'Query' ); |
||
263 | $result->addValue( null, 'Section', $items ); |
||
264 | break; |
||
265 | |||
266 | default: |
||
267 | ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" ); |
||
268 | } |
||
269 | } |
||
270 | |||
271 | public function getAllowedParams() { |
||
272 | if ( $this->allowedParams !== null ) { |
||
273 | return $this->allowedParams; |
||
274 | } |
||
275 | $this->allowedParams = $this->buildCommonApiParams( false ) + [ |
||
276 | 'suggest' => false, |
||
277 | 'redirects' => [ |
||
278 | ApiBase::PARAM_TYPE => [ 'return', 'resolve' ], |
||
279 | ], |
||
280 | 'format' => [ |
||
281 | ApiBase::PARAM_DFLT => 'json', |
||
282 | ApiBase::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ], |
||
283 | ], |
||
284 | 'warningsaserror' => false, |
||
285 | ]; |
||
286 | |||
287 | // Use open search specific default limit |
||
288 | $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get( |
||
289 | 'OpenSearchDefaultLimit' |
||
290 | ); |
||
291 | |||
292 | return $this->allowedParams; |
||
293 | } |
||
294 | |||
295 | public function getSearchProfileParams() { |
||
296 | return [ |
||
297 | 'profile' => [ |
||
298 | 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE, |
||
299 | 'help-message' => 'apihelp-query+prefixsearch-param-profile' |
||
300 | ], |
||
301 | ]; |
||
302 | } |
||
303 | |||
304 | protected function getExamplesMessages() { |
||
305 | return [ |
||
306 | 'action=opensearch&search=Te' |
||
307 | => 'apihelp-opensearch-example-te', |
||
308 | ]; |
||
309 | } |
||
310 | |||
311 | public function getHelpUrls() { |
||
312 | return 'https://www.mediawiki.org/wiki/API:Opensearch'; |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Trim an extract to a sensible length. |
||
317 | * |
||
318 | * Adapted from Extension:OpenSearchXml, which adapted it from |
||
319 | * Extension:ActiveAbstract. |
||
320 | * |
||
321 | * @param string $text |
||
322 | * @param int $length Target length; actual result will continue to the end of a sentence. |
||
323 | * @return string |
||
324 | */ |
||
325 | public static function trimExtract( $text, $length ) { |
||
326 | static $regex = null; |
||
327 | |||
328 | if ( $regex === null ) { |
||
329 | $endchars = [ |
||
330 | '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII |
||
331 | '。', // full-width ideographic full-stop |
||
332 | '.', '!', '?', // double-width roman forms |
||
333 | '。', // half-width ideographic full stop |
||
334 | ]; |
||
335 | $endgroup = implode( '|', $endchars ); |
||
336 | $end = "(?:$endgroup)"; |
||
337 | $sentence = ".{{$length},}?$end+"; |
||
338 | $regex = "/^($sentence)/u"; |
||
339 | } |
||
340 | |||
341 | $matches = []; |
||
342 | if ( preg_match( $regex, $text, $matches ) ) { |
||
343 | return trim( $matches[1] ); |
||
344 | } else { |
||
345 | // Just return the first line |
||
346 | return trim( explode( "\n", $text )[0] ); |
||
347 | } |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Fetch the template for a type. |
||
352 | * |
||
353 | * @param string $type MIME type |
||
354 | * @return string |
||
355 | * @throws MWException |
||
356 | */ |
||
357 | public static function getOpenSearchTemplate( $type ) { |
||
358 | $config = MediaWikiServices::getInstance()->getSearchEngineConfig(); |
||
359 | $template = $config->getConfig()->get( 'OpenSearchTemplate' ); |
||
360 | |||
361 | if ( $template && $type === 'application/x-suggestions+json' ) { |
||
362 | return $template; |
||
363 | } |
||
364 | |||
365 | $ns = implode( '|', $config->defaultNamespaces() ); |
||
366 | if ( !$ns ) { |
||
367 | $ns = '0'; |
||
368 | } |
||
369 | |||
370 | switch ( $type ) { |
||
371 | case 'application/x-suggestions+json': |
||
372 | return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' ) |
||
373 | . '?action=opensearch&search={searchTerms}&namespace=' . $ns; |
||
374 | |||
375 | case 'application/x-suggestions+xml': |
||
376 | return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' ) |
||
377 | . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns; |
||
378 | |||
379 | default: |
||
380 | throw new MWException( __METHOD__ . ": Unknown type '$type'" ); |
||
381 | } |
||
382 | } |
||
383 | } |
||
384 | |||
385 | class ApiOpenSearchFormatJson extends ApiFormatJson { |
||
386 | private $warningsAsError = false; |
||
387 | |||
388 | public function __construct( ApiMain $main, $fm, $warningsAsError ) { |
||
389 | parent::__construct( $main, "json$fm" ); |
||
390 | $this->warningsAsError = $warningsAsError; |
||
391 | } |
||
392 | |||
393 | public function execute() { |
||
394 | if ( !$this->getResult()->getResultData( 'error' ) ) { |
||
395 | $result = $this->getResult(); |
||
396 | |||
397 | // Ignore warnings or treat as errors, as requested |
||
398 | $warnings = $result->removeValue( 'warnings', null ); |
||
399 | if ( $this->warningsAsError && $warnings ) { |
||
400 | $this->dieUsage( |
||
401 | 'Warnings cannot be represented in OpenSearch JSON format', 'warnings', 0, |
||
402 | [ 'warnings' => $warnings ] |
||
403 | ); |
||
404 | } |
||
405 | |||
406 | // Ignore any other unexpected keys (e.g. from $wgDebugToolbar) |
||
407 | $remove = array_keys( array_diff_key( |
||
408 | $result->getResultData(), |
||
409 | [ 0 => 'search', 1 => 'terms', 2 => 'descriptions', 3 => 'urls' ] |
||
410 | ) ); |
||
411 | foreach ( $remove as $key ) { |
||
412 | $result->removeValue( $key, null ); |
||
413 | } |
||
414 | } |
||
415 | |||
416 | parent::execute(); |
||
417 | } |
||
418 | } |
||
419 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.