MediaWiki  master
PathRouter.php
Go to the documentation of this file.
1 <?php
73 class PathRouter {
74 
78  private $patterns = [];
79 
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  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  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 ) && !is_array( $optionData ) ) {
137  $options[$optionName] = [ $optionData ];
138  }
139  }
140 
141  $pattern = (object)[
142  'path' => $path,
143  'params' => $params,
144  'options' => $options,
145  'key' => $key,
146  ];
147  $pattern->weight = self::makeWeight( $pattern );
148  $this->patterns[] = $pattern;
149  }
150 
158  public function add( $path, $params = [], $options = [] ) {
159  if ( is_array( $path ) ) {
160  foreach ( $path as $key => $onePath ) {
161  $this->doAdd( $onePath, $params, $options, $key );
162  }
163  } else {
164  $this->doAdd( $path, $params, $options );
165  }
166  }
167 
175  public function validateRoute( $path, $varName ) {
176  if ( $path && !preg_match( '/^(https?:\/\/|\/)/', $path ) ) {
177  // T48998: Bail out early if path is non-absolute
178  throw new FatalError(
179  "If you use a relative URL for \$$varName, it must start " .
180  'with a slash (<code>/</code>).<br><br>See ' .
181  "<a href=\"https://www.mediawiki.org/wiki/Manual:\$$varName\">" .
182  "https://www.mediawiki.org/wiki/Manual:\$$varName</a>."
183  );
184  }
185  }
186 
194  public function addStrict( $path, $params = [], $options = [] ) {
195  $options['strict'] = true;
196  $this->add( $path, $params, $options );
197  }
198 
203  protected function sortByWeight() {
204  $weights = [];
205  foreach ( $this->patterns as $key => $pattern ) {
206  $weights[$key] = $pattern->weight;
207  }
208  array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
209  }
210 
215  protected static function makeWeight( $pattern ) {
216  # Start with a weight of 0
217  $weight = 0;
218 
219  // Explode the path to work with
220  $path = explode( '/', $pattern->path );
221 
222  # For each level of the path
223  foreach ( $path as $piece ) {
224  if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
225  # For a piece that is only a $1 variable add 1 points of weight
226  $weight += 1;
227  } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
228  # For a piece that simply contains a $1 variable add 2 points of weight
229  $weight += 2;
230  } else {
231  # For a solid piece add a full 3 points of weight
232  $weight += 3;
233  }
234  }
235 
236  foreach ( $pattern->options as $key => $option ) {
237  if ( preg_match( '/^\$\d+$/u', $key ) ) {
238  # Add 0.5 for restrictions to values
239  # This way given two separate "/$2/$1" patterns the
240  # one with a limited set of $2 values will dominate
241  # the one that'll match more loosely
242  $weight += 0.5;
243  }
244  }
245 
246  return $weight;
247  }
248 
255  public function parse( $path ) {
256  // Make sure our patterns are sorted by weight so the most specific
257  // matches are tested first
258  $this->sortByWeight();
259 
260  $matches = $this->internalParse( $path );
261  if ( is_null( $matches ) ) {
262  // Try with the normalized path (T100782)
264  $path = preg_replace( '#/+#', '/', $path );
265  $matches = $this->internalParse( $path );
266  }
267 
268  // We know the difference between null (no matches) and
269  // [] (a match with no data) but our WebRequest caller
270  // expects [] even when we have no matches so return
271  // a [] when we have null
272  return $matches ?? [];
273  }
274 
281  protected function internalParse( $path ) {
282  $matches = null;
283 
284  foreach ( $this->patterns as $pattern ) {
285  $matches = self::extractTitle( $path, $pattern );
286  if ( !is_null( $matches ) ) {
287  break;
288  }
289  }
290  return $matches;
291  }
292 
298  protected static function extractTitle( $path, $pattern ) {
299  // Convert the path pattern into a regexp we can match with
300  $regexp = preg_quote( $pattern->path, '#' );
301  // .* for the $1
302  $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
303  // .+ for the rest of the parameter numbers
304  $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
305  $regexp = "#^{$regexp}$#";
306 
307  $matches = [];
308  $data = [];
309 
310  // Try to match the path we were asked to parse with our regexp
311  if ( preg_match( $regexp, $path, $m ) ) {
312  // Ensure that any $# restriction we have set in our {$option}s
313  // matches properly here.
314  foreach ( $pattern->options as $key => $option ) {
315  if ( preg_match( '/^\$\d+$/u', $key ) ) {
316  $n = intval( substr( $key, 1 ) );
317  $value = rawurldecode( $m["par{$n}"] );
318  if ( !in_array( $value, $option ) ) {
319  // If any restriction does not match return null
320  // to signify that this rule did not match.
321  return null;
322  }
323  }
324  }
325 
326  // Give our $data array a copy of every $# that was matched
327  foreach ( $m as $matchKey => $matchValue ) {
328  if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
329  $n = intval( substr( $matchKey, 3 ) );
330  $data['$' . $n] = rawurldecode( $matchValue );
331  }
332  }
333  // If present give our $data array a $key as well
334  if ( isset( $pattern->key ) ) {
335  $data['$key'] = $pattern->key;
336  }
337 
338  // Go through our parameters for this match and add data to our matches and data arrays
339  foreach ( $pattern->params as $paramName => $paramData ) {
340  $value = null;
341  // Differentiate data: from normal parameters and keep the correct
342  // array key around (ie: foo for data:foo)
343  if ( preg_match( '/^data:/u', $paramName ) ) {
344  $isData = true;
345  $key = substr( $paramName, 5 );
346  } else {
347  $isData = false;
348  $key = $paramName;
349  }
350 
351  if ( isset( $paramData['value'] ) ) {
352  // For basic values just set the raw data as the value
353  $value = $paramData['value'];
354  } elseif ( isset( $paramData['pattern'] ) ) {
355  // For patterns we have to make value replacements on the string
356  $value = self::expandParamValue( $m, $pattern->key ?? null,
357  $paramData['pattern'] );
358  if ( $value === false ) {
359  // Pattern required data that wasn't available, abort
360  return null;
361  }
362  }
363 
364  // Send things that start with data: to $data, the rest to $matches
365  if ( $isData ) {
366  $data[$key] = $value;
367  } else {
368  $matches[$key] = $value;
369  }
370  }
371 
372  // If this match includes a callback, execute it
373  if ( isset( $pattern->options['callback'] ) ) {
374  call_user_func_array( $pattern->options['callback'], [ &$matches, $data ] );
375  }
376  } else {
377  // Our regexp didn't match, return null to signify no match.
378  return null;
379  }
380  // Fall through, everything went ok, return our matches array
381  return $matches;
382  }
383 
392  protected static function expandParamValue( $pathMatches, $key, $value ) {
393  $error = false;
394 
395  $replacer = function ( $m ) use ( $pathMatches, $key, &$error ) {
396  if ( $m[1] == "key" ) {
397  if ( is_null( $key ) ) {
398  $error = true;
399 
400  return '';
401  }
402 
403  return $key;
404  } else {
405  $d = $m[1];
406  if ( !isset( $pathMatches["par$d"] ) ) {
407  $error = true;
408 
409  return '';
410  }
411 
412  return rawurldecode( $pathMatches["par$d"] );
413  }
414  };
415 
416  $value = preg_replace_callback( '/\$(\d+|key)/u', $replacer, $value );
417  if ( $error ) {
418  return false;
419  }
420 
421  return $value;
422  }
423 
430  public static function getActionPaths( array $actionPaths, $articlePath ) {
431  if ( !$actionPaths ) {
432  return false;
433  }
434  // Processing of urls for this feature requires that 'view' is set.
435  // By default, set it to the pretty article path.
436  if ( !isset( $actionPaths['view'] ) ) {
437  $actionPaths['view'] = $articlePath;
438  }
439  return $actionPaths;
440  }
441 }
parse( $path)
Parse a path and return the query matches for the path.
Definition: PathRouter.php:255
addStrict( $path, $params=[], $options=[])
Add a new path pattern to the path router with the strict option on.
Definition: PathRouter.php:194
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
static makeWeight( $pattern)
Definition: PathRouter.php:215
PathRouter class.
Definition: PathRouter.php:73
validateRoute( $path, $varName)
Definition: PathRouter.php:175
static getActionPaths(array $actionPaths, $articlePath)
Definition: PathRouter.php:430
doAdd( $path, $params, $options, $key=null)
Protected helper to do the actual bulk work of adding a single pattern.
Definition: PathRouter.php:90
Abort the web request with a custom HTML string that will represent the entire response.
Definition: FatalError.php:35
sortByWeight()
Protected helper to re-sort our patterns so that the most specific (most heavily weighted) patterns a...
Definition: PathRouter.php:203
static extractTitle( $path, $pattern)
Definition: PathRouter.php:298
array $patterns
Definition: PathRouter.php:78
static expandParamValue( $pathMatches, $key, $value)
Replace $key etc.
Definition: PathRouter.php:392
internalParse( $path)
Match a path against each defined pattern.
Definition: PathRouter.php:281
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
Definition: PathRouter.php:158
$matches