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