MediaWiki  1.34.0
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 addStrict( $path, $params = [], $options = [] ) {
176  $options['strict'] = true;
177  $this->add( $path, $params, $options );
178  }
179 
184  protected function sortByWeight() {
185  $weights = [];
186  foreach ( $this->patterns as $key => $pattern ) {
187  $weights[$key] = $pattern->weight;
188  }
189  array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
190  }
191 
196  protected static function makeWeight( $pattern ) {
197  # Start with a weight of 0
198  $weight = 0;
199 
200  // Explode the path to work with
201  $path = explode( '/', $pattern->path );
202 
203  # For each level of the path
204  foreach ( $path as $piece ) {
205  if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
206  # For a piece that is only a $1 variable add 1 points of weight
207  $weight += 1;
208  } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
209  # For a piece that simply contains a $1 variable add 2 points of weight
210  $weight += 2;
211  } else {
212  # For a solid piece add a full 3 points of weight
213  $weight += 3;
214  }
215  }
216 
217  foreach ( $pattern->options as $key => $option ) {
218  if ( preg_match( '/^\$\d+$/u', $key ) ) {
219  # Add 0.5 for restrictions to values
220  # This way given two separate "/$2/$1" patterns the
221  # one with a limited set of $2 values will dominate
222  # the one that'll match more loosely
223  $weight += 0.5;
224  }
225  }
226 
227  return $weight;
228  }
229 
236  public function parse( $path ) {
237  // Make sure our patterns are sorted by weight so the most specific
238  // matches are tested first
239  $this->sortByWeight();
240 
241  $matches = $this->internalParse( $path );
242  if ( is_null( $matches ) ) {
243  // Try with the normalized path (T100782)
245  $path = preg_replace( '#/+#', '/', $path );
246  $matches = $this->internalParse( $path );
247  }
248 
249  // We know the difference between null (no matches) and
250  // [] (a match with no data) but our WebRequest caller
251  // expects [] even when we have no matches so return
252  // a [] when we have null
253  return $matches ?? [];
254  }
255 
262  protected function internalParse( $path ) {
263  $matches = null;
264 
265  foreach ( $this->patterns as $pattern ) {
266  $matches = self::extractTitle( $path, $pattern );
267  if ( !is_null( $matches ) ) {
268  break;
269  }
270  }
271  return $matches;
272  }
273 
279  protected static function extractTitle( $path, $pattern ) {
280  // Convert the path pattern into a regexp we can match with
281  $regexp = preg_quote( $pattern->path, '#' );
282  // .* for the $1
283  $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
284  // .+ for the rest of the parameter numbers
285  $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
286  $regexp = "#^{$regexp}$#";
287 
288  $matches = [];
289  $data = [];
290 
291  // Try to match the path we were asked to parse with our regexp
292  if ( preg_match( $regexp, $path, $m ) ) {
293  // Ensure that any $# restriction we have set in our {$option}s
294  // matches properly here.
295  foreach ( $pattern->options as $key => $option ) {
296  if ( preg_match( '/^\$\d+$/u', $key ) ) {
297  $n = intval( substr( $key, 1 ) );
298  $value = rawurldecode( $m["par{$n}"] );
299  if ( !in_array( $value, $option ) ) {
300  // If any restriction does not match return null
301  // to signify that this rule did not match.
302  return null;
303  }
304  }
305  }
306 
307  // Give our $data array a copy of every $# that was matched
308  foreach ( $m as $matchKey => $matchValue ) {
309  if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
310  $n = intval( substr( $matchKey, 3 ) );
311  $data['$' . $n] = rawurldecode( $matchValue );
312  }
313  }
314  // If present give our $data array a $key as well
315  if ( isset( $pattern->key ) ) {
316  $data['$key'] = $pattern->key;
317  }
318 
319  // Go through our parameters for this match and add data to our matches and data arrays
320  foreach ( $pattern->params as $paramName => $paramData ) {
321  $value = null;
322  // Differentiate data: from normal parameters and keep the correct
323  // array key around (ie: foo for data:foo)
324  if ( preg_match( '/^data:/u', $paramName ) ) {
325  $isData = true;
326  $key = substr( $paramName, 5 );
327  } else {
328  $isData = false;
329  $key = $paramName;
330  }
331 
332  if ( isset( $paramData['value'] ) ) {
333  // For basic values just set the raw data as the value
334  $value = $paramData['value'];
335  } elseif ( isset( $paramData['pattern'] ) ) {
336  // For patterns we have to make value replacements on the string
337  $value = self::expandParamValue( $m, $pattern->key ?? null,
338  $paramData['pattern'] );
339  if ( $value === false ) {
340  // Pattern required data that wasn't available, abort
341  return null;
342  }
343  }
344 
345  // Send things that start with data: to $data, the rest to $matches
346  if ( $isData ) {
347  $data[$key] = $value;
348  } else {
349  $matches[$key] = $value;
350  }
351  }
352 
353  // If this match includes a callback, execute it
354  if ( isset( $pattern->options['callback'] ) ) {
355  call_user_func_array( $pattern->options['callback'], [ &$matches, $data ] );
356  }
357  } else {
358  // Our regexp didn't match, return null to signify no match.
359  return null;
360  }
361  // Fall through, everything went ok, return our matches array
362  return $matches;
363  }
364 
373  protected static function expandParamValue( $pathMatches, $key, $value ) {
374  $error = false;
375 
376  $replacer = function ( $m ) use ( $pathMatches, $key, &$error ) {
377  if ( $m[1] == "key" ) {
378  if ( is_null( $key ) ) {
379  $error = true;
380 
381  return '';
382  }
383 
384  return $key;
385  } else {
386  $d = $m[1];
387  if ( !isset( $pathMatches["par$d"] ) ) {
388  $error = true;
389 
390  return '';
391  }
392 
393  return rawurldecode( $pathMatches["par$d"] );
394  }
395  };
396 
397  $value = preg_replace_callback( '/\$(\d+|key)/u', $replacer, $value );
398  if ( $error ) {
399  return false;
400  }
401 
402  return $value;
403  }
404 
411  public static function getActionPaths( array $actionPaths, $articlePath ) {
412  if ( !$actionPaths ) {
413  return false;
414  }
415  // Processing of urls for this feature requires that 'view' is set.
416  // By default, set it to the pretty article path.
417  if ( !isset( $actionPaths['view'] ) ) {
418  $actionPaths['view'] = $articlePath;
419  }
420  return $actionPaths;
421  }
422 }
PathRouter\add
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
Definition: PathRouter.php:158
PathRouter\internalParse
internalParse( $path)
Match a path against each defined pattern.
Definition: PathRouter.php:262
PathRouter\doAdd
doAdd( $path, $params, $options, $key=null)
Protected helper to do the actual bulk work of adding a single pattern.
Definition: PathRouter.php:90
PathRouter\addStrict
addStrict( $path, $params=[], $options=[])
Add a new path pattern to the path router with the strict option on.
Definition: PathRouter.php:175
wfRemoveDotSegments
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
Definition: GlobalFunctions.php:641
PathRouter\parse
parse( $path)
Parse a path and return the query matches for the path.
Definition: PathRouter.php:236
PathRouter\makeWeight
static makeWeight( $pattern)
Definition: PathRouter.php:196
PathRouter\getActionPaths
static getActionPaths(array $actionPaths, $articlePath)
Definition: PathRouter.php:411
$matches
$matches
Definition: NoLocalSettings.php:24
PathRouter\$patterns
array $patterns
Definition: PathRouter.php:78
$path
$path
Definition: NoLocalSettings.php:25
PathRouter\sortByWeight
sortByWeight()
Protected helper to re-sort our patterns so that the most specific (most heavily weighted) patterns a...
Definition: PathRouter.php:184
PathRouter
PathRouter class.
Definition: PathRouter.php:73
PathRouter\expandParamValue
static expandParamValue( $pathMatches, $key, $value)
Replace $key etc.
Definition: PathRouter.php:373
PathRouter\extractTitle
static extractTitle( $path, $pattern)
Definition: PathRouter.php:279