MediaWiki  REL1_20
PathRouter.php
Go to the documentation of this file.
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 }