MediaWiki master
PathRouter.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Request;
10
13use stdClass;
14
66
70 private $patterns = [];
71
82 protected function doAdd( $path, $params, $options, $key = null ) {
83 // Make sure all paths start with a /
84 if ( $path[0] !== '/' ) {
85 $path = '/' . $path;
86 }
87
88 if ( !isset( $options['strict'] ) || !$options['strict'] ) {
89 // Unless this is a strict path make sure that the path has a $1
90 if ( !str_contains( $path, '$1' ) ) {
91 if ( $path[-1] !== '/' ) {
92 $path .= '/';
93 }
94 $path .= '$1';
95 }
96 }
97
98 // If 'title' is not specified and our path pattern contains a $1
99 // Add a default 'title' => '$1' rule to the parameters.
100 if ( !isset( $params['title'] ) && str_contains( $path, '$1' ) ) {
101 $params['title'] = '$1';
102 }
103 // If the user explicitly marked 'title' as false then omit it from the matches
104 if ( isset( $params['title'] ) && $params['title'] === false ) {
105 unset( $params['title'] );
106 }
107
108 // Loop over our parameters and convert basic key => string
109 // patterns into fully descriptive array form
110 foreach ( $params as $paramName => $paramData ) {
111 if ( is_string( $paramData ) ) {
112 if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) {
113 $paramArrKey = 'pattern';
114 } else {
115 // If there's no replacement use a value instead
116 // of a pattern for a little more efficiency
117 $paramArrKey = 'value';
118 }
119 $params[$paramName] = [
120 $paramArrKey => $paramData
121 ];
122 }
123 }
124
125 // Loop over our options and convert any single value $# restrictions
126 // into an array so we only have to do in_array tests.
127 foreach ( $options as $optionName => $optionData ) {
128 if ( preg_match( '/^\$\d+$/u', $optionName ) && !is_array( $optionData ) ) {
129 $options[$optionName] = [ $optionData ];
130 }
131 }
132
133 $pattern = (object)[
134 'path' => $path,
135 'params' => $params,
136 'options' => $options,
137 'key' => $key,
138 ];
139 $pattern->weight = self::makeWeight( $pattern );
140 $this->patterns[] = $pattern;
141 }
142
150 public function add( $path, $params = [], $options = [] ) {
151 if ( is_array( $path ) ) {
152 foreach ( $path as $key => $onePath ) {
153 $this->doAdd( $onePath, $params, $options, $key );
154 }
155 } else {
156 $this->doAdd( $path, $params, $options );
157 }
158 }
159
167 public function validateRoute( $path, $varName ) {
168 if ( $path && !preg_match( '/^(https?:\/\/|\/)/', $path ) ) {
169 // T48998: Bail out early if path is non-absolute
170 throw new FatalError(
171 "If you use a relative URL for \$$varName, it must start " .
172 'with a slash (<code>/</code>).<br><br>See ' .
173 "<a href=\"https://www.mediawiki.org/wiki/Manual:\$$varName\">" .
174 "https://www.mediawiki.org/wiki/Manual:\$$varName</a>."
175 );
176 }
177 }
178
186 public function addStrict( $path, $params = [], $options = [] ) {
187 $options['strict'] = true;
188 $this->add( $path, $params, $options );
189 }
190
195 protected function sortByWeight() {
196 $weights = [];
197 foreach ( $this->patterns as $key => $pattern ) {
198 $weights[$key] = $pattern->weight;
199 }
200 array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
201 }
202
207 protected static function makeWeight( $pattern ) {
208 # Start with a weight of 0
209 $weight = 0;
210
211 // Explode the path to work with
212 $path = explode( '/', $pattern->path );
213
214 # For each level of the path
215 foreach ( $path as $piece ) {
216 if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
217 # For a piece that is only a $1 variable add 1 points of weight
218 $weight++;
219 } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
220 # For a piece that simply contains a $1 variable add 2 points of weight
221 $weight += 2;
222 } else {
223 # For a solid piece add a full 3 points of weight
224 $weight += 3;
225 }
226 }
227
228 foreach ( $pattern->options as $key => $option ) {
229 if ( preg_match( '/^\$\d+$/u', $key ) ) {
230 # Add 0.5 for restrictions to values
231 # This way given two separate "/$2/$1" patterns the
232 # one with a limited set of $2 values will dominate
233 # the one that'll match more loosely
234 $weight += 0.5;
235 }
236 }
237
238 return $weight;
239 }
240
247 public function parse( $path ) {
248 // Make sure our patterns are sorted by weight so the most specific
249 // matches are tested first
250 $this->sortByWeight();
251
252 $matches = $this->internalParse( $path );
253 if ( $matches === null ) {
254 // Try with the normalized path (T100782)
255 $path = UrlUtils::removeDotSegments( $path );
256 $path = preg_replace( '#/+#', '/', $path );
257 $matches = $this->internalParse( $path );
258 }
259
260 // We know the difference between null (no matches) and
261 // [] (a match with no data) but our WebRequest caller
262 // expects [] even when we have no matches so return
263 // a [] when we have null
264 return $matches ?? [];
265 }
266
273 protected function internalParse( $path ) {
274 $matches = null;
275
276 foreach ( $this->patterns as $pattern ) {
277 $matches = self::extractTitle( $path, $pattern );
278 if ( $matches !== null ) {
279 break;
280 }
281 }
282 return $matches;
283 }
284
290 protected static function extractTitle( $path, $pattern ) {
291 // Convert the path pattern into a regexp we can match with
292 $regexp = preg_quote( $pattern->path, '#' );
293 // .* for the $1
294 $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
295 // .+ for the rest of the parameter numbers
296 $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
297 $regexp = "#^{$regexp}$#";
298
299 $matches = [];
300 $data = [];
301
302 // Try to match the path we were asked to parse with our regexp
303 if ( preg_match( $regexp, $path, $m ) ) {
304 // Ensure that any $# restriction we have set in our {$option}s
305 // matches properly here.
306 foreach ( $pattern->options as $key => $option ) {
307 if ( preg_match( '/^\$\d+$/u', $key ) ) {
308 $n = intval( substr( $key, 1 ) );
309 $value = rawurldecode( $m["par{$n}"] );
310 if ( !in_array( $value, $option ) ) {
311 // If any restriction does not match return null
312 // to signify that this rule did not match.
313 return null;
314 }
315 }
316 }
317
318 // Give our $data array a copy of every $# that was matched
319 foreach ( $m as $matchKey => $matchValue ) {
320 if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
321 $n = intval( substr( $matchKey, 3 ) );
322 $data['$' . $n] = rawurldecode( $matchValue );
323 }
324 }
325 // If present give our $data array a $key as well
326 if ( isset( $pattern->key ) ) {
327 $data['$key'] = $pattern->key;
328 }
329
330 // Go through our parameters for this match and add data to our matches and data arrays
331 foreach ( $pattern->params as $paramName => $paramData ) {
332 $value = null;
333 // Differentiate data: from normal parameters and keep the correct
334 // array key around (ie: foo for data:foo)
335 if ( preg_match( '/^data:/u', $paramName ) ) {
336 $isData = true;
337 $key = substr( $paramName, 5 );
338 } else {
339 $isData = false;
340 $key = $paramName;
341 }
342
343 if ( isset( $paramData['value'] ) ) {
344 // For basic values just set the raw data as the value
345 $value = $paramData['value'];
346 } elseif ( isset( $paramData['pattern'] ) ) {
347 // For patterns we have to make value replacements on the string
348 $value = self::expandParamValue( $m, $pattern->key ?? null,
349 $paramData['pattern'] );
350 if ( $value === false ) {
351 // Pattern required data that wasn't available, abort
352 return null;
353 }
354 }
355
356 // Send things that start with data: to $data, the rest to $matches
357 if ( $isData ) {
358 $data[$key] = $value;
359 } else {
360 $matches[$key] = $value;
361 }
362 }
363
364 // If this match includes a callback, execute it
365 if ( isset( $pattern->options['callback'] ) ) {
366 $pattern->options['callback']( $matches, $data );
367 }
368 } else {
369 // Our regexp didn't match, return null to signify no match.
370 return null;
371 }
372 // Fall through, everything went ok, return our matches array
373 return $matches;
374 }
375
384 protected static function expandParamValue( $pathMatches, $key, $value ) {
385 $error = false;
386
387 $replacer = static function ( $m ) use ( $pathMatches, $key, &$error ) {
388 if ( $m[1] == "key" ) {
389 if ( $key === null ) {
390 $error = true;
391
392 return '';
393 }
394
395 return $key;
396 } else {
397 $d = $m[1];
398 if ( !isset( $pathMatches["par$d"] ) ) {
399 $error = true;
400
401 return '';
402 }
403
404 return rawurldecode( $pathMatches["par$d"] );
405 }
406 };
407
408 $value = preg_replace_callback( '/\$(\d+|key)/u', $replacer, $value );
409 if ( $error ) {
410 return false;
411 }
412
413 return $value;
414 }
415
422 public static function getActionPaths( array $actionPaths, $articlePath ) {
423 if ( !$actionPaths ) {
424 return false;
425 }
426 // Processing of urls for this feature requires that 'view' is set.
427 // By default, set it to the pretty article path.
428 if ( !isset( $actionPaths['view'] ) ) {
429 $actionPaths['view'] = $articlePath;
430 }
431 return $actionPaths;
432 }
433}
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