MediaWiki  REL1_19
PathRouter.php
Go to the documentation of this file.
00001 <?php
00052 class PathRouter {
00053 
00060         protected function doAdd( $path, $params, $options, $key = null ) {
00061                 // Make sure all paths start with a /
00062                 if ( $path[0] !== '/' ) {
00063                         $path = '/' . $path;
00064                 }
00065 
00066                 if ( !isset( $options['strict'] ) || !$options['strict'] ) {
00067                         // Unless this is a strict path make sure that the path has a $1
00068                         if ( strpos( $path, '$1' ) === false ) {
00069                                 if ( substr( $path, -1 ) !== '/' ) {
00070                                         $path .= '/';
00071                                 }
00072                                 $path .= '$1';
00073                         }
00074                 }
00075 
00076                 // If 'title' is not specified and our path pattern contains a $1
00077                 // Add a default 'title' => '$1' rule to the parameters.
00078                 if ( !isset( $params['title'] ) && strpos( $path, '$1' ) !== false ) {
00079                         $params['title'] = '$1';
00080                 }
00081                 // If the user explicitly marked 'title' as false then omit it from the matches
00082                 if ( isset( $params['title'] ) && $params['title'] === false ) {
00083                         unset( $params['title'] );
00084                 }
00085 
00086                 // Loop over our parameters and convert basic key => string
00087                 // patterns into fully descriptive array form
00088                 foreach ( $params as $paramName => $paramData ) {
00089                         if ( is_string( $paramData ) ) {
00090                                 if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) {
00091                                         $paramArrKey = 'pattern';
00092                                 } else {
00093                                         // If there's no replacement use a value instead
00094                                         // of a pattern for a little more efficiency
00095                                         $paramArrKey = 'value';
00096                                 }
00097                                 $params[$paramName] = array(
00098                                         $paramArrKey => $paramData
00099                                 );
00100                         }
00101                 }
00102 
00103                 // Loop over our options and convert any single value $# restrictions
00104                 // into an array so we only have to do in_array tests.
00105                 foreach ( $options as $optionName => $optionData ) {
00106                         if ( preg_match( '/^\$\d+$/u', $optionName ) ) {
00107                                 if ( !is_array( $optionData ) ) {
00108                                         $options[$optionName] = array( $optionData );
00109                                 }
00110                         }
00111                 }
00112 
00113                 $pattern = (object)array(
00114                         'path'    => $path,
00115                         'params'  => $params,
00116                         'options' => $options,
00117                         'key'     => $key,
00118                 );
00119                 $pattern->weight = self::makeWeight( $pattern );
00120                 $this->patterns[] = $pattern;
00121         }
00122 
00130         public function add( $path, $params = array(), $options = array() ) {
00131                 if ( is_array( $path ) ) {
00132                         foreach ( $path as $key => $onePath ) {
00133                                 $this->doAdd( $onePath, $params, $options, $key );
00134                         }
00135                 } else {
00136                         $this->doAdd( $path, $params, $options );
00137                 }
00138         }
00139 
00144         public function addStrict( $path, $params = array(), $options = array() ) {
00145                 $options['strict'] = true;
00146                 $this->add( $path, $params, $options );
00147         }
00148 
00153         protected function sortByWeight() {
00154                 $weights = array();
00155                 foreach( $this->patterns as $key => $pattern ) {
00156                         $weights[$key] = $pattern->weight;
00157                 }
00158                 array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
00159         }
00160 
00161         protected static function makeWeight( $pattern ) {
00162                 # Start with a weight of 0
00163                 $weight = 0;
00164 
00165                 // Explode the path to work with
00166                 $path = explode( '/', $pattern->path );
00167 
00168                 # For each level of the path
00169                 foreach( $path as $piece ) {
00170                         if ( preg_match( '/^\$(\d+|key)$/u', $piece ) ) {
00171                                 # For a piece that is only a $1 variable add 1 points of weight
00172                                 $weight += 1;
00173                         } elseif ( preg_match( '/\$(\d+|key)/u', $piece ) ) {
00174                                 # For a piece that simply contains a $1 variable add 2 points of weight
00175                                 $weight += 2;
00176                         } else {
00177                                 # For a solid piece add a full 3 points of weight
00178                                 $weight += 3;
00179                         }
00180                 }
00181 
00182                 foreach ( $pattern->options as $key => $option ) {
00183                         if ( preg_match( '/^\$\d+$/u', $key ) ) {
00184                                 # Add 0.5 for restrictions to values
00185                                 # This way given two separate "/$2/$1" patterns the
00186                                 # one with a limited set of $2 values will dominate
00187                                 # the one that'll match more loosely
00188                                 $weight += 0.5;
00189                         }
00190                 }
00191 
00192                 return $weight;
00193         }
00194 
00201         public function parse( $path ) {
00202                 // Make sure our patterns are sorted by weight so the most specific
00203                 // matches are tested first
00204                 $this->sortByWeight();
00205                 
00206                 $matches = null;
00207 
00208                 foreach ( $this->patterns as $pattern ) {
00209                         $matches = self::extractTitle( $path, $pattern );
00210                         if ( !is_null( $matches ) ) {
00211                                 break;
00212                         }
00213                 }
00214 
00215                 // We know the difference between null (no matches) and
00216                 // array() (a match with no data) but our WebRequest caller
00217                 // expects array() even when we have no matches so return
00218                 // a array() when we have null
00219                 return is_null( $matches ) ? array() : $matches;
00220         }
00221 
00222         protected static function extractTitle( $path, $pattern ) {
00223                 // Convert the path pattern into a regexp we can match with
00224                 $regexp = preg_quote( $pattern->path, '#' );
00225                 // .* for the $1
00226                 $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp );
00227                 // .+ for the rest of the parameter numbers
00228                 $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp );
00229                 $regexp = "#^{$regexp}$#";
00230 
00231                 $matches = array();
00232                 $data = array();
00233 
00234                 // Try to match the path we were asked to parse with our regexp
00235                 if ( preg_match( $regexp, $path, $m ) ) {
00236                         // Ensure that any $# restriction we have set in our {$option}s
00237                         // matches properly here.
00238                         foreach ( $pattern->options as $key => $option ) {
00239                                 if ( preg_match( '/^\$\d+$/u', $key ) ) {
00240                                         $n = intval( substr( $key, 1 ) );
00241                                         $value = rawurldecode( $m["par{$n}"] );
00242                                         if ( !in_array( $value, $option ) ) {
00243                                                 // If any restriction does not match return null
00244                                                 // to signify that this rule did not match.
00245                                                 return null;
00246                                         }
00247                                 }
00248                         }
00249 
00250                         // Give our $data array a copy of every $# that was matched
00251                         foreach ( $m as $matchKey => $matchValue ) {
00252                                 if ( preg_match( '/^par\d+$/u', $matchKey ) ) {
00253                                         $n = intval( substr( $matchKey, 3 ) );
00254                                         $data['$'.$n] = rawurldecode( $matchValue );
00255                                 }
00256                         }
00257                         // If present give our $data array a $key as well
00258                         if ( isset( $pattern->key ) ) {
00259                                 $data['$key'] = $pattern->key;
00260                         }
00261 
00262                         // Go through our parameters for this match and add data to our matches and data arrays
00263                         foreach ( $pattern->params as $paramName => $paramData ) {
00264                                 $value = null;
00265                                 // Differentiate data: from normal parameters and keep the correct
00266                                 // array key around (ie: foo for data:foo)
00267                                 if ( preg_match( '/^data:/u', $paramName ) ) {
00268                                         $isData = true;
00269                                         $key = substr( $paramName, 5 );
00270                                 } else {
00271                                         $isData = false;
00272                                         $key = $paramName;
00273                                 }
00274 
00275                                 if ( isset( $paramData['value'] ) ) {
00276                                         // For basic values just set the raw data as the value
00277                                         $value = $paramData['value'];
00278                                 } elseif ( isset( $paramData['pattern'] ) ) {
00279                                         // For patterns we have to make value replacements on the string
00280                                         $value = $paramData['pattern'];
00281                                         $replacer = new PathRouterPatternReplacer;
00282                                         $replacer->params = $m;
00283                                         if ( isset( $pattern->key ) ) {
00284                                                 $replacer->key = $pattern->key;
00285                                         }
00286                                         $value = $replacer->replace( $value );
00287                                         if ( $value === false ) {
00288                                                 // Pattern required data that wasn't available, abort
00289                                                 return null;
00290                                         }
00291                                 }
00292 
00293                                 // Send things that start with data: to $data, the rest to $matches
00294                                 if ( $isData ) {
00295                                         $data[$key] = $value;
00296                                 } else {
00297                                         $matches[$key] = $value;
00298                                 }
00299                         }
00300 
00301                         // If this match includes a callback, execute it
00302                         if ( isset( $pattern->options['callback'] ) ) {
00303                                 call_user_func_array( $pattern->options['callback'], array( &$matches, $data ) );
00304                         }
00305                 } else {
00306                         // Our regexp didn't match, return null to signify no match.
00307                         return null;
00308                 }
00309                 // Fall through, everything went ok, return our matches array
00310                 return $matches;
00311         }
00312 
00313 }
00314 
00315 class PathRouterPatternReplacer {
00316 
00317         public $key, $params, $error;
00318 
00325         public function replace( $value ) {
00326                 $this->error = false;
00327                 $value = preg_replace_callback( '/\$(\d+|key)/u', array( $this, 'callback' ), $value );
00328                 if ( $this->error ) {
00329                         return false;
00330                 }
00331                 return $value;
00332         }
00333 
00334         protected function callback( $m ) {
00335                 if ( $m[1] == "key" ) {
00336                         if ( is_null( $this->key ) ) {
00337                                 $this->error = true;
00338                                 return '';
00339                         }
00340                         return $this->key;
00341                 } else {
00342                         $d = $m[1];
00343                         if ( !isset( $this->params["par$d"] ) ) {
00344                                 $this->error = true;
00345                                 return '';
00346                         }
00347                         return rawurldecode( $this->params["par$d"] );
00348                 }
00349         }
00350 
00351 }