[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Parser to extract query parameters out of REQUEST_URI paths. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23 /** 24 * PathRouter class. 25 * This class can take patterns such as /wiki/$1 and use them to 26 * parse query parameters out of REQUEST_URI paths. 27 * 28 * $router->add( "/wiki/$1" ); 29 * - Matches /wiki/Foo style urls and extracts the title 30 * $router->add( array( 'edit' => "/edit/$key" ), array( 'action' => '$key' ) ); 31 * - Matches /edit/Foo style urls and sets action=edit 32 * $router->add( '/$2/$1', 33 * array( 'variant' => '$2' ), 34 * array( '$2' => array( 'zh-hant', 'zh-hans' ) 35 * ); 36 * - Matches /zh-hant/Foo or /zh-hans/Foo 37 * $router->addStrict( "/foo/Bar", array( 'title' => 'Baz' ) ); 38 * - Matches /foo/Bar explicitly and uses "Baz" as the title 39 * $router->add( '/help/$1', array( 'title' => 'Help:$1' ) ); 40 * - Matches /help/Foo with "Help:Foo" as the title 41 * $router->add( '/$1', array( 'foo' => array( 'value' => 'bar$2' ) ); 42 * - Matches /Foo and sets 'foo' to 'bar$2' without $2 being replaced 43 * $router->add( '/$1', array( 'data:foo' => 'bar' ), array( 'callback' => 'functionname' ) ); 44 * - Matches /Foo, adds the key 'foo' with the value 'bar' to the data array 45 * and calls functionname( &$matches, $data ); 46 * 47 * Path patterns: 48 * - Paths may contain $# patterns such as $1, $2, etc... 49 * - $1 will match 0 or more while the rest will match 1 or more 50 * - Unless you use addStrict "/wiki" and "/wiki/" will be expanded to "/wiki/$1" 51 * 52 * Params: 53 * - In a pattern $1, $2, etc... will be replaced with the relevant contents 54 * - If you used a keyed array as a path pattern, $key will be replaced with 55 * the relevant contents 56 * - The default behavior is equivalent to `array( 'title' => '$1' )`, 57 * if you don't want the title parameter you can explicitly use `array( 'title' => false )` 58 * - You can specify a value that won't have replacements in it 59 * using `'foo' => array( 'value' => 'bar' );` 60 * 61 * Options: 62 * - The option keys $1, $2, etc... can be specified to restrict the possible values 63 * of that variable. A string can be used for a single value, or an array for multiple. 64 * - When the option key 'strict' is set (Using addStrict is simpler than doing this directly) 65 * the path won't have $1 implicitly added to it. 66 * - The option key 'callback' can specify a callback that will be run when a path is matched. 67 * The callback will have the arguments ( &$matches, $data ) and the matches array can 68 * be modified. 69 * 70 * @since 1.19 71 * @author Daniel Friesen 72 */ 73 class PathRouter { 74 75 /** 76 * @var array 77 */ 78 private $patterns = array(); 79 80 /** 81 * Protected helper to do the actual bulk work of adding a single pattern. 82 * This is in a separate method so that add() can handle the difference between 83 * a single string $path and an array() $path that contains multiple path 84 * patterns each with an associated $key to pass on. 85 * @param string $path 86 * @param array $params 87 * @param array $options 88 * @param null|string $key 89 */ 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] = array( 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 ) ) { 137 if ( !is_array( $optionData ) ) { 138 $options[$optionName] = array( $optionData ); 139 } 140 } 141 } 142 143 $pattern = (object)array( 144 'path' => $path, 145 'params' => $params, 146 'options' => $options, 147 'key' => $key, 148 ); 149 $pattern->weight = self::makeWeight( $pattern ); 150 $this->patterns[] = $pattern; 151 } 152 153 /** 154 * Add a new path pattern to the path router 155 * 156 * @param string|array $path The path pattern to add 157 * @param array $params The params for this path pattern 158 * @param array $options The options for this path pattern 159 */ 160 public function add( $path, $params = array(), $options = array() ) { 161 if ( is_array( $path ) ) { 162 foreach ( $path as $key => $onePath ) { 163 $this->doAdd( $onePath, $params, $options, $key ); 164 } 165 } else { 166 $this->doAdd( $path, $params, $options ); 167 } 168 } 169 170 /** 171 * Add a new path pattern to the path router with the strict option on 172 * @see self::add 173 * @param string|array $path 174 * @param array $params 175 * @param array $options 176 */ 177 public function addStrict( $path, $params = array(), $options = array() ) { 178 $options['strict'] = true; 179 $this->add( $path, $params, $options ); 180 } 181 182 /** 183 * Protected helper to re-sort our patterns so that the most specific 184 * (most heavily weighted) patterns are at the start of the array. 185 */ 186 protected function sortByWeight() { 187 $weights = array(); 188 foreach ( $this->patterns as $key => $pattern ) { 189 $weights[$key] = $pattern->weight; 190 } 191 array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns ); 192 } 193 194 /** 195 * @param object $pattern 196 * @return float|int 197 */ 198 protected static function makeWeight( $pattern ) { 199 # Start with a weight of 0 200 $weight = 0; 201 202 // Explode the path to work with 203 $path = explode( '/', $pattern->path ); 204 205 # For each level of the path 206 foreach ( $path as $piece ) { 207 if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) { 208 # For a piece that is only a $1 variable add 1 points of weight 209 $weight += 1; 210 } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) { 211 # For a piece that simply contains a $1 variable add 2 points of weight 212 $weight += 2; 213 } else { 214 # For a solid piece add a full 3 points of weight 215 $weight += 3; 216 } 217 } 218 219 foreach ( $pattern->options as $key => $option ) { 220 if ( preg_match( '/^\$\d+$/u', $key ) ) { 221 # Add 0.5 for restrictions to values 222 # This way given two separate "/$2/$1" patterns the 223 # one with a limited set of $2 values will dominate 224 # the one that'll match more loosely 225 $weight += 0.5; 226 } 227 } 228 229 return $weight; 230 } 231 232 /** 233 * Parse a path and return the query matches for the path 234 * 235 * @param string $path The path to parse 236 * @return array The array of matches for the path 237 */ 238 public function parse( $path ) { 239 // Make sure our patterns are sorted by weight so the most specific 240 // matches are tested first 241 $this->sortByWeight(); 242 243 $matches = null; 244 245 foreach ( $this->patterns as $pattern ) { 246 $matches = self::extractTitle( $path, $pattern ); 247 if ( !is_null( $matches ) ) { 248 break; 249 } 250 } 251 252 // We know the difference between null (no matches) and 253 // array() (a match with no data) but our WebRequest caller 254 // expects array() even when we have no matches so return 255 // a array() when we have null 256 return is_null( $matches ) ? array() : $matches; 257 } 258 259 /** 260 * @param string $path 261 * @param string $pattern 262 * @return array|null 263 */ 264 protected static function extractTitle( $path, $pattern ) { 265 // Convert the path pattern into a regexp we can match with 266 $regexp = preg_quote( $pattern->path, '#' ); 267 // .* for the $1 268 $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp ); 269 // .+ for the rest of the parameter numbers 270 $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp ); 271 $regexp = "#^{$regexp}$#"; 272 273 $matches = array(); 274 $data = array(); 275 276 // Try to match the path we were asked to parse with our regexp 277 if ( preg_match( $regexp, $path, $m ) ) { 278 // Ensure that any $# restriction we have set in our {$option}s 279 // matches properly here. 280 foreach ( $pattern->options as $key => $option ) { 281 if ( preg_match( '/^\$\d+$/u', $key ) ) { 282 $n = intval( substr( $key, 1 ) ); 283 $value = rawurldecode( $m["par{$n}"] ); 284 if ( !in_array( $value, $option ) ) { 285 // If any restriction does not match return null 286 // to signify that this rule did not match. 287 return null; 288 } 289 } 290 } 291 292 // Give our $data array a copy of every $# that was matched 293 foreach ( $m as $matchKey => $matchValue ) { 294 if ( preg_match( '/^par\d+$/u', $matchKey ) ) { 295 $n = intval( substr( $matchKey, 3 ) ); 296 $data['$' . $n] = rawurldecode( $matchValue ); 297 } 298 } 299 // If present give our $data array a $key as well 300 if ( isset( $pattern->key ) ) { 301 $data['$key'] = $pattern->key; 302 } 303 304 // Go through our parameters for this match and add data to our matches and data arrays 305 foreach ( $pattern->params as $paramName => $paramData ) { 306 $value = null; 307 // Differentiate data: from normal parameters and keep the correct 308 // array key around (ie: foo for data:foo) 309 if ( preg_match( '/^data:/u', $paramName ) ) { 310 $isData = true; 311 $key = substr( $paramName, 5 ); 312 } else { 313 $isData = false; 314 $key = $paramName; 315 } 316 317 if ( isset( $paramData['value'] ) ) { 318 // For basic values just set the raw data as the value 319 $value = $paramData['value']; 320 } elseif ( isset( $paramData['pattern'] ) ) { 321 // For patterns we have to make value replacements on the string 322 $value = $paramData['pattern']; 323 $replacer = new PathRouterPatternReplacer; 324 $replacer->params = $m; 325 if ( isset( $pattern->key ) ) { 326 $replacer->key = $pattern->key; 327 } 328 $value = $replacer->replace( $value ); 329 if ( $value === false ) { 330 // Pattern required data that wasn't available, abort 331 return null; 332 } 333 } 334 335 // Send things that start with data: to $data, the rest to $matches 336 if ( $isData ) { 337 $data[$key] = $value; 338 } else { 339 $matches[$key] = $value; 340 } 341 } 342 343 // If this match includes a callback, execute it 344 if ( isset( $pattern->options['callback'] ) ) { 345 call_user_func_array( $pattern->options['callback'], array( &$matches, $data ) ); 346 } 347 } else { 348 // Our regexp didn't match, return null to signify no match. 349 return null; 350 } 351 // Fall through, everything went ok, return our matches array 352 return $matches; 353 } 354 355 } 356 357 class PathRouterPatternReplacer { 358 359 public $key, $params, $error; 360 361 /** 362 * Replace keys inside path router patterns with text. 363 * We do this inside of a replacement callback because after replacement we can't tell the 364 * difference between a $1 that was not replaced and a $1 that was part of 365 * the content a $1 was replaced with. 366 * @param string $value 367 * @return string 368 */ 369 public function replace( $value ) { 370 $this->error = false; 371 $value = preg_replace_callback( '/\$(\d+|key)/u', array( $this, 'callback' ), $value ); 372 if ( $this->error ) { 373 return false; 374 } 375 return $value; 376 } 377 378 /** 379 * @param array $m 380 * @return string 381 */ 382 protected function callback( $m ) { 383 if ( $m[1] == "key" ) { 384 if ( is_null( $this->key ) ) { 385 $this->error = true; 386 return ''; 387 } 388 return $this->key; 389 } else { 390 $d = $m[1]; 391 if ( !isset( $this->params["par$d"] ) ) { 392 $this->error = true; 393 return ''; 394 } 395 return rawurldecode( $this->params["par$d"] ); 396 } 397 } 398 399 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |