MediaWiki
REL1_22
|
00001 <?php 00073 class PathRouter { 00074 00078 private $patterns = array(); 00079 00090 protected function doAdd( $path, $params, $options, $key = null ) { 00091 // Make sure all paths start with a / 00092 if ( $path[0] !== '/' ) { 00093 $path = '/' . $path; 00094 } 00095 00096 if ( !isset( $options['strict'] ) || !$options['strict'] ) { 00097 // Unless this is a strict path make sure that the path has a $1 00098 if ( strpos( $path, '$1' ) === false ) { 00099 if ( substr( $path, -1 ) !== '/' ) { 00100 $path .= '/'; 00101 } 00102 $path .= '$1'; 00103 } 00104 } 00105 00106 // If 'title' is not specified and our path pattern contains a $1 00107 // Add a default 'title' => '$1' rule to the parameters. 00108 if ( !isset( $params['title'] ) && strpos( $path, '$1' ) !== false ) { 00109 $params['title'] = '$1'; 00110 } 00111 // If the user explicitly marked 'title' as false then omit it from the matches 00112 if ( isset( $params['title'] ) && $params['title'] === false ) { 00113 unset( $params['title'] ); 00114 } 00115 00116 // Loop over our parameters and convert basic key => string 00117 // patterns into fully descriptive array form 00118 foreach ( $params as $paramName => $paramData ) { 00119 if ( is_string( $paramData ) ) { 00120 if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) { 00121 $paramArrKey = 'pattern'; 00122 } else { 00123 // If there's no replacement use a value instead 00124 // of a pattern for a little more efficiency 00125 $paramArrKey = 'value'; 00126 } 00127 $params[$paramName] = array( 00128 $paramArrKey => $paramData 00129 ); 00130 } 00131 } 00132 00133 // Loop over our options and convert any single value $# restrictions 00134 // into an array so we only have to do in_array tests. 00135 foreach ( $options as $optionName => $optionData ) { 00136 if ( preg_match( '/^\$\d+$/u', $optionName ) ) { 00137 if ( !is_array( $optionData ) ) { 00138 $options[$optionName] = array( $optionData ); 00139 } 00140 } 00141 } 00142 00143 $pattern = (object)array( 00144 'path' => $path, 00145 'params' => $params, 00146 'options' => $options, 00147 'key' => $key, 00148 ); 00149 $pattern->weight = self::makeWeight( $pattern ); 00150 $this->patterns[] = $pattern; 00151 } 00152 00160 public function add( $path, $params = array(), $options = array() ) { 00161 if ( is_array( $path ) ) { 00162 foreach ( $path as $key => $onePath ) { 00163 $this->doAdd( $onePath, $params, $options, $key ); 00164 } 00165 } else { 00166 $this->doAdd( $path, $params, $options ); 00167 } 00168 } 00169 00177 public function addStrict( $path, $params = array(), $options = array() ) { 00178 $options['strict'] = true; 00179 $this->add( $path, $params, $options ); 00180 } 00181 00186 protected function sortByWeight() { 00187 $weights = array(); 00188 foreach ( $this->patterns as $key => $pattern ) { 00189 $weights[$key] = $pattern->weight; 00190 } 00191 array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns ); 00192 } 00193 00198 protected static function makeWeight( $pattern ) { 00199 # Start with a weight of 0 00200 $weight = 0; 00201 00202 // Explode the path to work with 00203 $path = explode( '/', $pattern->path ); 00204 00205 # For each level of the path 00206 foreach ( $path as $piece ) { 00207 if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) { 00208 # For a piece that is only a $1 variable add 1 points of weight 00209 $weight += 1; 00210 } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) { 00211 # For a piece that simply contains a $1 variable add 2 points of weight 00212 $weight += 2; 00213 } else { 00214 # For a solid piece add a full 3 points of weight 00215 $weight += 3; 00216 } 00217 } 00218 00219 foreach ( $pattern->options as $key => $option ) { 00220 if ( preg_match( '/^\$\d+$/u', $key ) ) { 00221 # Add 0.5 for restrictions to values 00222 # This way given two separate "/$2/$1" patterns the 00223 # one with a limited set of $2 values will dominate 00224 # the one that'll match more loosely 00225 $weight += 0.5; 00226 } 00227 } 00228 00229 return $weight; 00230 } 00231 00238 public function parse( $path ) { 00239 // Make sure our patterns are sorted by weight so the most specific 00240 // matches are tested first 00241 $this->sortByWeight(); 00242 00243 $matches = null; 00244 00245 foreach ( $this->patterns as $pattern ) { 00246 $matches = self::extractTitle( $path, $pattern ); 00247 if ( !is_null( $matches ) ) { 00248 break; 00249 } 00250 } 00251 00252 // We know the difference between null (no matches) and 00253 // array() (a match with no data) but our WebRequest caller 00254 // expects array() even when we have no matches so return 00255 // a array() when we have null 00256 return is_null( $matches ) ? array() : $matches; 00257 } 00258 00264 protected static function extractTitle( $path, $pattern ) { 00265 // Convert the path pattern into a regexp we can match with 00266 $regexp = preg_quote( $pattern->path, '#' ); 00267 // .* for the $1 00268 $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp ); 00269 // .+ for the rest of the parameter numbers 00270 $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp ); 00271 $regexp = "#^{$regexp}$#"; 00272 00273 $matches = array(); 00274 $data = array(); 00275 00276 // Try to match the path we were asked to parse with our regexp 00277 if ( preg_match( $regexp, $path, $m ) ) { 00278 // Ensure that any $# restriction we have set in our {$option}s 00279 // matches properly here. 00280 foreach ( $pattern->options as $key => $option ) { 00281 if ( preg_match( '/^\$\d+$/u', $key ) ) { 00282 $n = intval( substr( $key, 1 ) ); 00283 $value = rawurldecode( $m["par{$n}"] ); 00284 if ( !in_array( $value, $option ) ) { 00285 // If any restriction does not match return null 00286 // to signify that this rule did not match. 00287 return null; 00288 } 00289 } 00290 } 00291 00292 // Give our $data array a copy of every $# that was matched 00293 foreach ( $m as $matchKey => $matchValue ) { 00294 if ( preg_match( '/^par\d+$/u', $matchKey ) ) { 00295 $n = intval( substr( $matchKey, 3 ) ); 00296 $data['$' . $n] = rawurldecode( $matchValue ); 00297 } 00298 } 00299 // If present give our $data array a $key as well 00300 if ( isset( $pattern->key ) ) { 00301 $data['$key'] = $pattern->key; 00302 } 00303 00304 // Go through our parameters for this match and add data to our matches and data arrays 00305 foreach ( $pattern->params as $paramName => $paramData ) { 00306 $value = null; 00307 // Differentiate data: from normal parameters and keep the correct 00308 // array key around (ie: foo for data:foo) 00309 if ( preg_match( '/^data:/u', $paramName ) ) { 00310 $isData = true; 00311 $key = substr( $paramName, 5 ); 00312 } else { 00313 $isData = false; 00314 $key = $paramName; 00315 } 00316 00317 if ( isset( $paramData['value'] ) ) { 00318 // For basic values just set the raw data as the value 00319 $value = $paramData['value']; 00320 } elseif ( isset( $paramData['pattern'] ) ) { 00321 // For patterns we have to make value replacements on the string 00322 $value = $paramData['pattern']; 00323 $replacer = new PathRouterPatternReplacer; 00324 $replacer->params = $m; 00325 if ( isset( $pattern->key ) ) { 00326 $replacer->key = $pattern->key; 00327 } 00328 $value = $replacer->replace( $value ); 00329 if ( $value === false ) { 00330 // Pattern required data that wasn't available, abort 00331 return null; 00332 } 00333 } 00334 00335 // Send things that start with data: to $data, the rest to $matches 00336 if ( $isData ) { 00337 $data[$key] = $value; 00338 } else { 00339 $matches[$key] = $value; 00340 } 00341 } 00342 00343 // If this match includes a callback, execute it 00344 if ( isset( $pattern->options['callback'] ) ) { 00345 call_user_func_array( $pattern->options['callback'], array( &$matches, $data ) ); 00346 } 00347 } else { 00348 // Our regexp didn't match, return null to signify no match. 00349 return null; 00350 } 00351 // Fall through, everything went ok, return our matches array 00352 return $matches; 00353 } 00354 00355 } 00356 00357 class PathRouterPatternReplacer { 00358 00359 public $key, $params, $error; 00360 00369 public function replace( $value ) { 00370 $this->error = false; 00371 $value = preg_replace_callback( '/\$(\d+|key)/u', array( $this, 'callback' ), $value ); 00372 if ( $this->error ) { 00373 return false; 00374 } 00375 return $value; 00376 } 00377 00382 protected function callback( $m ) { 00383 if ( $m[1] == "key" ) { 00384 if ( is_null( $this->key ) ) { 00385 $this->error = true; 00386 return ''; 00387 } 00388 return $this->key; 00389 } else { 00390 $d = $m[1]; 00391 if ( !isset( $this->params["par$d"] ) ) { 00392 $this->error = true; 00393 return ''; 00394 } 00395 return rawurldecode( $this->params["par$d"] ); 00396 } 00397 } 00398 00399 }