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