MediaWiki  REL1_22
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 }