wikimedia /
mediawiki
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.