MediaWiki master
PathRouter.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Request;
24
25use FatalError;
26use stdClass;
27
79
83 private $patterns = [];
84
95 protected function doAdd( $path, $params, $options, $key = null ) {
96 // Make sure all paths start with a /
97 if ( $path[0] !== '/' ) {
98 $path = '/' . $path;
99 }
100
101 if ( !isset( $options['strict'] ) || !$options['strict'] ) {
102 // Unless this is a strict path make sure that the path has a $1
103 if ( strpos( $path, '$1' ) === false ) {
104 if ( $path[-1] !== '/' ) {
105 $path .= '/';
106 }
107 $path .= '$1';
108 }
109 }
110
111 // If 'title' is not specified and our path pattern contains a $1
112 // Add a default 'title' => '$1' rule to the parameters.
113 if ( !isset( $params['title'] ) && strpos( $path, '$1' ) !== false ) {
114 $params['title'] = '$1';
115 }
116 // If the user explicitly marked 'title' as false then omit it from the matches
117 if ( isset( $params['title'] ) && $params['title'] === false ) {
118 unset( $params['title'] );
119 }
120
121 // Loop over our parameters and convert basic key => string
122 // patterns into fully descriptive array form
123 foreach ( $params as $paramName => $paramData ) {
124 if ( is_string( $paramData ) ) {
125 if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) {
126 $paramArrKey = 'pattern';
127 } else {
128 // If there's no replacement use a value instead
129 // of a pattern for a little more efficiency
130 $paramArrKey = 'value';
131 }
132 $params[$paramName] = [
133 $paramArrKey => $paramData
134 ];
135 }
136 }
137
138 // Loop over our options and convert any single value $# restrictions
139 // into an array so we only have to do in_array tests.
140 foreach ( $options as $optionName => $optionData ) {
141 if ( preg_match( '/^\$\d+$/u', $optionName ) && !is_array( $optionData ) ) {
142 $options[$optionName] = [ $optionData ];
143 }
144 }
145
146 $pattern = (object)[
147 'path' => $path,
148 'params' => $params,
149 'options' => $options,
150 'key' => $key,
151 ];
152 $pattern->weight = self::makeWeight( $pattern );
153 $this->patterns[] = $pattern;
154 }
155
163 public function add( $path, $params = [], $options = [] ) {
164 if ( is_array( $path ) ) {
165 foreach ( $path as $key => $onePath ) {
166 $this->doAdd( $onePath, $params, $options, $key );
167 }
168 } else {
169 $this->doAdd( $path, $params, $options );
170 }
171 }
172
180 public function validateRoute( $path, $varName ) {
181 if ( $path && !preg_match( '/^(https?:\/\/|\/)/', $path ) ) {
182 // T48998: Bail out early if path is non-absolute
183 throw new FatalError(
184 "If you use a relative URL for \$$varName, it must start " .
185 'with a slash (<code>/</code>).<br><br>See ' .
186 "<a href=\"https://www.mediawiki.org/wiki/Manual:\$$varName\">" .
187 "https://www.mediawiki.org/wiki/Manual:\$$varName</a>."
188 );
189 }
190 }
191
199 public function addStrict( $path, $params = [], $options = [] ) {
200 $options['strict'] = true;
201 $this->add( $path, $params, $options );
202 }
203
208 protected function sortByWeight() {
209 $weights = [];
210 foreach ( $this->patterns as $key => $pattern ) {
211 $weights[$key] = $pattern->weight;
212 }
213 array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
214 }
215
220 protected static function makeWeight( $pattern ) {
221 # Start with a weight of 0
222 $weight = 0;
223
224 // Explode the path to work with
225 $path = explode( '/', $pattern->path );
226
227 # For each level of the path
228 foreach ( $path as $piece ) {
229 if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
230 # For a piece that is only a $1 variable add 1 points of weight
231 $weight += 1;
232 } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
233 # For a piece that simply contains a $1 variable add 2 points of weight
234 $weight += 2;
235 } else {
236 # For a solid piece add a full 3 points of weight
237 $weight += 3;
238 }
239 }
240
241 foreach ( $pattern->options as $key => $option ) {
242 if ( preg_match( '/^\$\d+$/u', $key ) ) {
243 # Add 0.5 for restrictions to values
244 # This way given two separate "/$2/$1" patterns the
245 # one with a limited set of $2 values will dominate
246 # the one that'll match more loosely
247 $weight += 0.5;
248 }
249 }
250
251 return $weight;
252 }
253
260 public function parse( $path ) {
261 // Make sure our patterns are sorted by weight so the most specific
262 // matches are tested first
263 $this->sortByWeight();
264
265 $matches = $this->internalParse( $path );
266 if ( $matches === null ) {
267 // Try with the normalized path (T100782)
269 $path = preg_replace( '#/+#', '/', $path );
270 $matches = $this->internalParse( $path );
271 }
272
273 // We know the difference between null (no matches) and
274 // [] (a match with no data) but our WebRequest caller
275 // expects [] even when we have no matches so return
276 // a [] when we have null
277 return $matches ?? [];
278 }
279
286 protected function internalParse( $path ) {
287 $matches = null;
288
289 foreach ( $this->patterns as $pattern ) {
290 $matches = self::extractTitle( $path, $pattern );
291 if ( $matches !== null ) {
292 break;
293 }
294 }
295 return $matches;
296 }
297
303 protected static function extractTitle( $path, $pattern ) {
304 // Convert the path pattern into a regexp we can match with
305 $regexp = preg_quote( $pattern->path, '#' );
306 // .* for the $1
307 $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
308 // .+ for the rest of the parameter numbers
309 $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
310 $regexp = "#^{$regexp}$#";
311
312 $matches = [];
313 $data = [];
314
315 // Try to match the path we were asked to parse with our regexp
316 if ( preg_match( $regexp, $path, $m ) ) {
317 // Ensure that any $# restriction we have set in our {$option}s
318 // matches properly here.
319 foreach ( $pattern->options as $key => $option ) {
320 if ( preg_match( '/^\$\d+$/u', $key ) ) {
321 $n = intval( substr( $key, 1 ) );
322 $value = rawurldecode( $m["par{$n}"] );
323 if ( !in_array( $value, $option ) ) {
324 // If any restriction does not match return null
325 // to signify that this rule did not match.
326 return null;
327 }
328 }
329 }
330
331 // Give our $data array a copy of every $# that was matched
332 foreach ( $m as $matchKey => $matchValue ) {
333 if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
334 $n = intval( substr( $matchKey, 3 ) );
335 $data['$' . $n] = rawurldecode( $matchValue );
336 }
337 }
338 // If present give our $data array a $key as well
339 if ( isset( $pattern->key ) ) {
340 $data['$key'] = $pattern->key;
341 }
342
343 // Go through our parameters for this match and add data to our matches and data arrays
344 foreach ( $pattern->params as $paramName => $paramData ) {
345 $value = null;
346 // Differentiate data: from normal parameters and keep the correct
347 // array key around (ie: foo for data:foo)
348 if ( preg_match( '/^data:/u', $paramName ) ) {
349 $isData = true;
350 $key = substr( $paramName, 5 );
351 } else {
352 $isData = false;
353 $key = $paramName;
354 }
355
356 if ( isset( $paramData['value'] ) ) {
357 // For basic values just set the raw data as the value
358 $value = $paramData['value'];
359 } elseif ( isset( $paramData['pattern'] ) ) {
360 // For patterns we have to make value replacements on the string
361 $value = self::expandParamValue( $m, $pattern->key ?? null,
362 $paramData['pattern'] );
363 if ( $value === false ) {
364 // Pattern required data that wasn't available, abort
365 return null;
366 }
367 }
368
369 // Send things that start with data: to $data, the rest to $matches
370 if ( $isData ) {
371 $data[$key] = $value;
372 } else {
373 $matches[$key] = $value;
374 }
375 }
376
377 // If this match includes a callback, execute it
378 if ( isset( $pattern->options['callback'] ) ) {
379 call_user_func_array( $pattern->options['callback'], [ &$matches, $data ] );
380 }
381 } else {
382 // Our regexp didn't match, return null to signify no match.
383 return null;
384 }
385 // Fall through, everything went ok, return our matches array
386 return $matches;
387 }
388
397 protected static function expandParamValue( $pathMatches, $key, $value ) {
398 $error = false;
399
400 $replacer = static function ( $m ) use ( $pathMatches, $key, &$error ) {
401 if ( $m[1] == "key" ) {
402 if ( $key === null ) {
403 $error = true;
404
405 return '';
406 }
407
408 return $key;
409 } else {
410 $d = $m[1];
411 if ( !isset( $pathMatches["par$d"] ) ) {
412 $error = true;
413
414 return '';
415 }
416
417 return rawurldecode( $pathMatches["par$d"] );
418 }
419 };
420
421 $value = preg_replace_callback( '/\$(\d+|key)/u', $replacer, $value );
422 if ( $error ) {
423 return false;
424 }
425
426 return $value;
427 }
428
435 public static function getActionPaths( array $actionPaths, $articlePath ) {
436 if ( !$actionPaths ) {
437 return false;
438 }
439 // Processing of urls for this feature requires that 'view' is set.
440 // By default, set it to the pretty article path.
441 if ( !isset( $actionPaths['view'] ) ) {
442 $actionPaths['view'] = $articlePath;
443 }
444 return $actionPaths;
445 }
446}
447
451class_alias( PathRouter::class, 'PathRouter' );
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
Abort the web request with a custom HTML string that will represent the entire response.
MediaWiki\Request\PathRouter class.
static getActionPaths(array $actionPaths, $articlePath)
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
static expandParamValue( $pathMatches, $key, $value)
Replace $key etc.
static extractTitle( $path, $pattern)
validateRoute( $path, $varName)
sortByWeight()
Protected helper to re-sort our patterns so that the most specific (most heavily weighted) patterns a...
doAdd( $path, $params, $options, $key=null)
Protected helper to do the actual bulk work of adding a single pattern.
static makeWeight( $pattern)
internalParse( $path)
Match a path against each defined pattern.
addStrict( $path, $params=[], $options=[])
Add a new path pattern to the path router with the strict option on.
parse( $path)
Parse a path and return the query matches for the path.