MediaWiki  master
PathRouter.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Request;
24 
25 use FatalError;
26 use stdClass;
27 
78 class PathRouter {
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 ( substr( $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 
448 class_alias( PathRouter::class, 'PathRouter' );
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
$matches
Abort the web request with a custom HTML string that will represent the entire response.
Definition: FatalError.php:37
MediaWiki\Request\PathRouter class.
Definition: PathRouter.php:78
static extractTitle( $path, $pattern)
Definition: PathRouter.php:303
static expandParamValue( $pathMatches, $key, $value)
Replace $key etc.
Definition: PathRouter.php:397
internalParse( $path)
Match a path against each defined pattern.
Definition: PathRouter.php:286
parse( $path)
Parse a path and return the query matches for the path.
Definition: PathRouter.php:260
addStrict( $path, $params=[], $options=[])
Add a new path pattern to the path router with the strict option on.
Definition: PathRouter.php:199
doAdd( $path, $params, $options, $key=null)
Protected helper to do the actual bulk work of adding a single pattern.
Definition: PathRouter.php:95
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
Definition: PathRouter.php:163
static getActionPaths(array $actionPaths, $articlePath)
Definition: PathRouter.php:435
sortByWeight()
Protected helper to re-sort our patterns so that the most specific (most heavily weighted) patterns a...
Definition: PathRouter.php:208