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/PathRouter.php (1 issue)

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
 * Parser to extract query parameters out of REQUEST_URI paths.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * PathRouter class.
25
 * This class can take patterns such as /wiki/$1 and use them to
26
 * parse query parameters out of REQUEST_URI paths.
27
 *
28
 * $router->add( "/wiki/$1" );
29
 *   - Matches /wiki/Foo style urls and extracts the title
30
 * $router->add( [ 'edit' => "/edit/$key" ], [ 'action' => '$key' ] );
31
 *   - Matches /edit/Foo style urls and sets action=edit
32
 * $router->add( '/$2/$1',
33
 *   [ 'variant' => '$2' ],
34
 *   [ '$2' => [ 'zh-hant', 'zh-hans' ] ]
35
 * );
36
 *   - Matches /zh-hant/Foo or /zh-hans/Foo
37
 * $router->addStrict( "/foo/Bar", [ 'title' => 'Baz' ] );
38
 *   - Matches /foo/Bar explicitly and uses "Baz" as the title
39
 * $router->add( '/help/$1', [ 'title' => 'Help:$1' ] );
40
 *   - Matches /help/Foo with "Help:Foo" as the title
41
 * $router->add( '/$1', [ 'foo' => [ 'value' => 'bar$2' ] ] );
42
 *   - Matches /Foo and sets 'foo' to 'bar$2' without $2 being replaced
43
 * $router->add( '/$1', [ 'data:foo' => 'bar' ], [ 'callback' => 'functionname' ] );
44
 *   - Matches /Foo, adds the key 'foo' with the value 'bar' to the data array
45
 *     and calls functionname( &$matches, $data );
46
 *
47
 * Path patterns:
48
 *   - Paths may contain $# patterns such as $1, $2, etc...
49
 *   - $1 will match 0 or more while the rest will match 1 or more
50
 *   - Unless you use addStrict "/wiki" and "/wiki/" will be expanded to "/wiki/$1"
51
 *
52
 * Params:
53
 *   - In a pattern $1, $2, etc... will be replaced with the relevant contents
54
 *   - If you used a keyed array as a path pattern, $key will be replaced with
55
 *     the relevant contents
56
 *   - The default behavior is equivalent to `array( 'title' => '$1' )`,
57
 *     if you don't want the title parameter you can explicitly use `array( 'title' => false )`
58
 *   - You can specify a value that won't have replacements in it
59
 *     using `'foo' => [ 'value' => 'bar' ];`
60
 *
61
 * Options:
62
 *   - The option keys $1, $2, etc... can be specified to restrict the possible values
63
 *     of that variable. A string can be used for a single value, or an array for multiple.
64
 *   - When the option key 'strict' is set (Using addStrict is simpler than doing this directly)
65
 *     the path won't have $1 implicitly added to it.
66
 *   - The option key 'callback' can specify a callback that will be run when a path is matched.
67
 *     The callback will have the arguments ( &$matches, $data ) and the matches array can
68
 *     be modified.
69
 *
70
 * @since 1.19
71
 * @author Daniel Friesen
72
 */
73
class PathRouter {
74
75
	/**
76
	 * @var array
77
	 */
78
	private $patterns = [];
79
80
	/**
81
	 * Protected helper to do the actual bulk work of adding a single pattern.
82
	 * This is in a separate method so that add() can handle the difference between
83
	 * a single string $path and an array() $path that contains multiple path
84
	 * patterns each with an associated $key to pass on.
85
	 * @param string $path
86
	 * @param array $params
87
	 * @param array $options
88
	 * @param null|string $key
89
	 */
90
	protected function doAdd( $path, $params, $options, $key = null ) {
91
		// Make sure all paths start with a /
92
		if ( $path[0] !== '/' ) {
93
			$path = '/' . $path;
94
		}
95
96
		if ( !isset( $options['strict'] ) || !$options['strict'] ) {
97
			// Unless this is a strict path make sure that the path has a $1
98
			if ( strpos( $path, '$1' ) === false ) {
99
				if ( substr( $path, -1 ) !== '/' ) {
100
					$path .= '/';
101
				}
102
				$path .= '$1';
103
			}
104
		}
105
106
		// If 'title' is not specified and our path pattern contains a $1
107
		// Add a default 'title' => '$1' rule to the parameters.
108 View Code Duplication
		if ( !isset( $params['title'] ) && strpos( $path, '$1' ) !== false ) {
109
			$params['title'] = '$1';
110
		}
111
		// If the user explicitly marked 'title' as false then omit it from the matches
112 View Code Duplication
		if ( isset( $params['title'] ) && $params['title'] === false ) {
113
			unset( $params['title'] );
114
		}
115
116
		// Loop over our parameters and convert basic key => string
117
		// patterns into fully descriptive array form
118
		foreach ( $params as $paramName => $paramData ) {
119
			if ( is_string( $paramData ) ) {
120
				if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) {
121
					$paramArrKey = 'pattern';
122
				} else {
123
					// If there's no replacement use a value instead
124
					// of a pattern for a little more efficiency
125
					$paramArrKey = 'value';
126
				}
127
				$params[$paramName] = [
128
					$paramArrKey => $paramData
129
				];
130
			}
131
		}
132
133
		// Loop over our options and convert any single value $# restrictions
134
		// into an array so we only have to do in_array tests.
135
		foreach ( $options as $optionName => $optionData ) {
136
			if ( preg_match( '/^\$\d+$/u', $optionName ) ) {
137
				if ( !is_array( $optionData ) ) {
138
					$options[$optionName] = [ $optionData ];
139
				}
140
			}
141
		}
142
143
		$pattern = (object)[
144
			'path' => $path,
145
			'params' => $params,
146
			'options' => $options,
147
			'key' => $key,
148
		];
149
		$pattern->weight = self::makeWeight( $pattern );
150
		$this->patterns[] = $pattern;
151
	}
152
153
	/**
154
	 * Add a new path pattern to the path router
155
	 *
156
	 * @param string|array $path The path pattern to add
157
	 * @param array $params The params for this path pattern
158
	 * @param array $options The options for this path pattern
159
	 */
160
	public function add( $path, $params = [], $options = [] ) {
161
		if ( is_array( $path ) ) {
162
			foreach ( $path as $key => $onePath ) {
163
				$this->doAdd( $onePath, $params, $options, $key );
164
			}
165
		} else {
166
			$this->doAdd( $path, $params, $options );
167
		}
168
	}
169
170
	/**
171
	 * Add a new path pattern to the path router with the strict option on
172
	 * @see self::add
173
	 * @param string|array $path
174
	 * @param array $params
175
	 * @param array $options
176
	 */
177
	public function addStrict( $path, $params = [], $options = [] ) {
178
		$options['strict'] = true;
179
		$this->add( $path, $params, $options );
180
	}
181
182
	/**
183
	 * Protected helper to re-sort our patterns so that the most specific
184
	 * (most heavily weighted) patterns are at the start of the array.
185
	 */
186
	protected function sortByWeight() {
187
		$weights = [];
188
		foreach ( $this->patterns as $key => $pattern ) {
189
			$weights[$key] = $pattern->weight;
190
		}
191
		array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
192
	}
193
194
	/**
195
	 * @param object $pattern
196
	 * @return float|int
197
	 */
198
	protected static function makeWeight( $pattern ) {
199
		# Start with a weight of 0
200
		$weight = 0;
201
202
		// Explode the path to work with
203
		$path = explode( '/', $pattern->path );
204
205
		# For each level of the path
206
		foreach ( $path as $piece ) {
207
			if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
208
				# For a piece that is only a $1 variable add 1 points of weight
209
				$weight += 1;
210
			} elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
211
				# For a piece that simply contains a $1 variable add 2 points of weight
212
				$weight += 2;
213
			} else {
214
				# For a solid piece add a full 3 points of weight
215
				$weight += 3;
216
			}
217
		}
218
219
		foreach ( $pattern->options as $key => $option ) {
220
			if ( preg_match( '/^\$\d+$/u', $key ) ) {
221
				# Add 0.5 for restrictions to values
222
				# This way given two separate "/$2/$1" patterns the
223
				# one with a limited set of $2 values will dominate
224
				# the one that'll match more loosely
225
				$weight += 0.5;
226
			}
227
		}
228
229
		return $weight;
230
	}
231
232
	/**
233
	 * Parse a path and return the query matches for the path
234
	 *
235
	 * @param string $path The path to parse
236
	 * @return array The array of matches for the path
237
	 */
238
	public function parse( $path ) {
239
		// Make sure our patterns are sorted by weight so the most specific
240
		// matches are tested first
241
		$this->sortByWeight();
242
243
		$matches = null;
244
245
		foreach ( $this->patterns as $pattern ) {
246
			$matches = self::extractTitle( $path, $pattern );
247
			if ( !is_null( $matches ) ) {
248
				break;
249
			}
250
		}
251
252
		// We know the difference between null (no matches) and
253
		// array() (a match with no data) but our WebRequest caller
254
		// expects array() even when we have no matches so return
255
		// a array() when we have null
256
		return is_null( $matches ) ? [] : $matches;
257
	}
258
259
	/**
260
	 * @param string $path
261
	 * @param string $pattern
262
	 * @return array|null
263
	 */
264
	protected static function extractTitle( $path, $pattern ) {
265
		// Convert the path pattern into a regexp we can match with
266
		$regexp = preg_quote( $pattern->path, '#' );
267
		// .* for the $1
268
		$regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
269
		// .+ for the rest of the parameter numbers
270
		$regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
271
		$regexp = "#^{$regexp}$#";
272
273
		$matches = [];
274
		$data = [];
275
276
		// Try to match the path we were asked to parse with our regexp
277
		if ( preg_match( $regexp, $path, $m ) ) {
278
			// Ensure that any $# restriction we have set in our {$option}s
279
			// matches properly here.
280
			foreach ( $pattern->options as $key => $option ) {
281
				if ( preg_match( '/^\$\d+$/u', $key ) ) {
282
					$n = intval( substr( $key, 1 ) );
283
					$value = rawurldecode( $m["par{$n}"] );
284
					if ( !in_array( $value, $option ) ) {
285
						// If any restriction does not match return null
286
						// to signify that this rule did not match.
287
						return null;
288
					}
289
				}
290
			}
291
292
			// Give our $data array a copy of every $# that was matched
293
			foreach ( $m as $matchKey => $matchValue ) {
294
				if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
295
					$n = intval( substr( $matchKey, 3 ) );
296
					$data['$' . $n] = rawurldecode( $matchValue );
297
				}
298
			}
299
			// If present give our $data array a $key as well
300
			if ( isset( $pattern->key ) ) {
301
				$data['$key'] = $pattern->key;
302
			}
303
304
			// Go through our parameters for this match and add data to our matches and data arrays
305
			foreach ( $pattern->params as $paramName => $paramData ) {
306
				$value = null;
307
				// Differentiate data: from normal parameters and keep the correct
308
				// array key around (ie: foo for data:foo)
309
				if ( preg_match( '/^data:/u', $paramName ) ) {
310
					$isData = true;
311
					$key = substr( $paramName, 5 );
312
				} else {
313
					$isData = false;
314
					$key = $paramName;
315
				}
316
317
				if ( isset( $paramData['value'] ) ) {
318
					// For basic values just set the raw data as the value
319
					$value = $paramData['value'];
320
				} elseif ( isset( $paramData['pattern'] ) ) {
321
					// For patterns we have to make value replacements on the string
322
					$value = $paramData['pattern'];
323
					$replacer = new PathRouterPatternReplacer;
324
					$replacer->params = $m;
325
					if ( isset( $pattern->key ) ) {
326
						$replacer->key = $pattern->key;
327
					}
328
					$value = $replacer->replace( $value );
329
					if ( $value === false ) {
330
						// Pattern required data that wasn't available, abort
331
						return null;
332
					}
333
				}
334
335
				// Send things that start with data: to $data, the rest to $matches
336
				if ( $isData ) {
337
					$data[$key] = $value;
338
				} else {
339
					$matches[$key] = $value;
340
				}
341
			}
342
343
			// If this match includes a callback, execute it
344
			if ( isset( $pattern->options['callback'] ) ) {
345
				call_user_func_array( $pattern->options['callback'], [ &$matches, $data ] );
346
			}
347
		} else {
348
			// Our regexp didn't match, return null to signify no match.
349
			return null;
350
		}
351
		// Fall through, everything went ok, return our matches array
352
		return $matches;
353
	}
354
355
}
356
357
class PathRouterPatternReplacer {
358
359
	public $key, $params, $error;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
360
361
	/**
362
	 * Replace keys inside path router patterns with text.
363
	 * We do this inside of a replacement callback because after replacement we can't tell the
364
	 * difference between a $1 that was not replaced and a $1 that was part of
365
	 * the content a $1 was replaced with.
366
	 * @param string $value
367
	 * @return string
368
	 */
369
	public function replace( $value ) {
370
		$this->error = false;
371
		$value = preg_replace_callback( '/\$(\d+|key)/u', [ $this, 'callback' ], $value );
372
		if ( $this->error ) {
373
			return false;
374
		}
375
		return $value;
376
	}
377
378
	/**
379
	 * @param array $m
380
	 * @return string
381
	 */
382
	protected function callback( $m ) {
383
		if ( $m[1] == "key" ) {
384
			if ( is_null( $this->key ) ) {
385
				$this->error = true;
386
				return '';
387
			}
388
			return $this->key;
389
		} else {
390
			$d = $m[1];
391
			if ( !isset( $this->params["par$d"] ) ) {
392
				$this->error = true;
393
				return '';
394
			}
395
			return rawurldecode( $this->params["par$d"] );
396
		}
397
	}
398
399
}
400