MediaWiki REL1_39
PathRouter.php
Go to the documentation of this file.
1<?php
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 ( $matches === null ) {
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 ( $matches !== null ) {
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 = static function ( $m ) use ( $pathMatches, $key, &$error ) {
396 if ( $m[1] == "key" ) {
397 if ( $key === null ) {
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}
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
Abort the web request with a custom HTML string that will represent the entire response.
PathRouter class.
static getActionPaths(array $actionPaths, $articlePath)
sortByWeight()
Protected helper to re-sort our patterns so that the most specific (most heavily weighted) patterns a...
static extractTitle( $path, $pattern)
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
static expandParamValue( $pathMatches, $key, $value)
Replace $key etc.
addStrict( $path, $params=[], $options=[])
Add a new path pattern to the path router with the strict option on.
static makeWeight( $pattern)
parse( $path)
Parse a path and return the query matches for the path.
doAdd( $path, $params, $options, $key=null)
Protected helper to do the actual bulk work of adding a single pattern.
internalParse( $path)
Match a path against each defined pattern.
validateRoute( $path, $varName)