Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/api/ApiOpenSearch.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

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
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. 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:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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