Index: trunk/phase3/tests/phpunit/includes/PathRouterTest.php |
— | — | @@ -202,4 +202,25 @@ |
203 | 203 | $this->assertEquals( $matches, array( 'title' => "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." ) ); |
204 | 204 | } |
205 | 205 | |
| 206 | + |
| 207 | + /** |
| 208 | + * Ensure that the php passed site of parameter values are not urldecoded |
| 209 | + */ |
| 210 | + public function testPatternUrlencoding() { |
| 211 | + $router = new PathRouter; |
| 212 | + $router->add( "/wiki/$1", array( 'title' => '%20:$1' ) ); |
| 213 | + $matches = $router->parse( "/wiki/Foo" ); |
| 214 | + $this->assertEquals( $matches, array( 'title' => '%20:Foo' ) ); |
| 215 | + } |
| 216 | + |
| 217 | + /** |
| 218 | + * Ensure that raw parameter values do not have any variable replacements or urldecoding |
| 219 | + */ |
| 220 | + public function testRawParamValue() { |
| 221 | + $router = new PathRouter; |
| 222 | + $router->add( "/wiki/$1", array( 'title' => array( 'value' => 'bar%20$1' ) ) ); |
| 223 | + $matches = $router->parse( "/wiki/Foo" ); |
| 224 | + $this->assertEquals( $matches, array( 'title' => 'bar%20$1' ) ); |
| 225 | + } |
| 226 | + |
206 | 227 | } |
Index: trunk/phase3/includes/WebRequest.php |
— | — | @@ -62,15 +62,19 @@ |
63 | 63 | } |
64 | 64 | |
65 | 65 | /** |
66 | | - * Extract the PATH_INFO variable even when it isn't a reasonable |
67 | | - * value. On some large webhosts, PATH_INFO includes the script |
68 | | - * path as well as everything after it. |
| 66 | + * Extract relevant query arguments from the http request uri's path |
| 67 | + * to be merged with the normal php provided query arguments. |
| 68 | + * Tries to use the REQUEST_URI data if available and parses it |
| 69 | + * according to the wiki's configuration looking for any known pattern. |
69 | 70 | * |
| 71 | + * If the REQUEST_URI is not provided we'll fall back on the PATH_INFO |
| 72 | + * provided by the server if any and use that to set a 'title' parameter. |
| 73 | + * |
70 | 74 | * @param $want string: If this is not 'all', then the function |
71 | 75 | * will return an empty array if it determines that the URL is |
72 | 76 | * inside a rewrite path. |
73 | 77 | * |
74 | | - * @return Array: 'title' key is the title of the article. |
| 78 | + * @return Array: Any query arguments found in path matches. |
75 | 79 | */ |
76 | 80 | static public function getPathInfo( $want = 'all' ) { |
77 | 81 | // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892 |
— | — | @@ -120,13 +124,6 @@ |
121 | 125 | |
122 | 126 | global $wgVariantArticlePath, $wgContLang; |
123 | 127 | if( $wgVariantArticlePath ) { |
124 | | - /*$variantPaths = array(); |
125 | | - foreach( $wgContLang->getVariants() as $variant ) { |
126 | | - $variantPaths[$variant] = |
127 | | - str_replace( '$2', $variant, $wgVariantArticlePath ); |
128 | | - } |
129 | | - $router->add( $variantPaths, array( 'parameter' => 'variant' ) );*/ |
130 | | - // Maybe actually this? |
131 | 128 | $router->add( $wgVariantArticlePath, |
132 | 129 | array( 'variant' => '$2'), |
133 | 130 | array( '$2' => $wgContLang->getVariants() ) |
Index: trunk/phase3/includes/PathRouter.php |
— | — | @@ -50,7 +50,15 @@ |
51 | 51 | * @author Daniel Friesen |
52 | 52 | */ |
53 | 53 | class PathRouter { |
| 54 | + |
| 55 | + /** |
| 56 | + * Protected helper to do the actual bulk work of adding a single pattern. |
| 57 | + * This is in a separate method so that add() can handle the difference between |
| 58 | + * a single string $path and an array() $path that contains multiple path |
| 59 | + * patterns each with an associated $key to pass on. |
| 60 | + */ |
54 | 61 | protected function doAdd( $path, $params, $options, $key = null ) { |
| 62 | + // Make sure all paths start with a / |
55 | 63 | if ( $path[0] !== '/' ) { |
56 | 64 | $path = '/' . $path; |
57 | 65 | } |
— | — | @@ -65,13 +73,18 @@ |
66 | 74 | } |
67 | 75 | } |
68 | 76 | |
| 77 | + // If 'title' is not specified and our path pattern contains a $1 |
| 78 | + // Add a default 'title' => '$1' rule to the parameters. |
69 | 79 | if ( !isset( $params['title'] ) && strpos( $path, '$1' ) !== false ) { |
70 | 80 | $params['title'] = '$1'; |
71 | 81 | } |
| 82 | + // If the user explicitly marked 'title' as false then omit it from the matches |
72 | 83 | if ( isset( $params['title'] ) && $params['title'] === false ) { |
73 | 84 | unset( $params['title'] ); |
74 | 85 | } |
75 | 86 | |
| 87 | + // Loop over our parameters and convert basic key => string |
| 88 | + // patterns into fully descriptive array form |
76 | 89 | foreach ( $params as $paramName => $paramData ) { |
77 | 90 | if ( is_string( $paramData ) ) { |
78 | 91 | if ( preg_match( '/\$(\d+|key)/u', $paramData ) ) { |
— | — | @@ -87,6 +100,8 @@ |
88 | 101 | } |
89 | 102 | } |
90 | 103 | |
| 104 | + // Loop over our options and convert any single value $# restrictions |
| 105 | + // into an array so we only have to do in_array tests. |
91 | 106 | foreach ( $options as $optionName => $optionData ) { |
92 | 107 | if ( preg_match( '/^\$\d+$/u', $optionName ) ) { |
93 | 108 | if ( !is_array( $optionData ) ) { |
— | — | @@ -131,6 +146,10 @@ |
132 | 147 | $this->add( $path, $params, $options ); |
133 | 148 | } |
134 | 149 | |
| 150 | + /** |
| 151 | + * Protected helper to re-sort our patterns so that the most specific |
| 152 | + * (most heavily weighted) patterns are at the start of the array. |
| 153 | + */ |
135 | 154 | protected function sortByWeight() { |
136 | 155 | $weights = array(); |
137 | 156 | foreach( $this->patterns as $key => $pattern ) { |
— | — | @@ -139,7 +158,7 @@ |
140 | 159 | array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns ); |
141 | 160 | } |
142 | 161 | |
143 | | - public static function makeWeight( $pattern ) { |
| 162 | + protected static function makeWeight( $pattern ) { |
144 | 163 | # Start with a weight of 0 |
145 | 164 | $weight = 0; |
146 | 165 | |
— | — | @@ -180,6 +199,8 @@ |
181 | 200 | * @return Array The array of matches for the path |
182 | 201 | */ |
183 | 202 | public function parse( $path ) { |
| 203 | + // Make sure our patterns are sorted by weight so the most specific |
| 204 | + // matches are tested first |
184 | 205 | $this->sortByWeight(); |
185 | 206 | |
186 | 207 | $matches = null; |
— | — | @@ -191,41 +212,58 @@ |
192 | 213 | } |
193 | 214 | } |
194 | 215 | |
| 216 | + // We know the difference between null (no matches) and |
| 217 | + // array() (a match with no data) but our WebRequest caller |
| 218 | + // expects array() even when we have no matches so return |
| 219 | + // a array() when we have null |
195 | 220 | return is_null( $matches ) ? array() : $matches; |
196 | 221 | } |
197 | 222 | |
198 | 223 | protected static function extractTitle( $path, $pattern ) { |
| 224 | + // Convert the path pattern into a regexp we can match with |
199 | 225 | $regexp = preg_quote( $pattern->path, '#' ); |
| 226 | + // .* for the $1 |
200 | 227 | $regexp = preg_replace( '#\\\\\$1#u', '(?P<par1>.*)', $regexp ); |
| 228 | + // .+ for the rest of the parameter numbers |
201 | 229 | $regexp = preg_replace( '#\\\\\$(\d+)#u', '(?P<par$1>.+?)', $regexp ); |
202 | 230 | $regexp = "#^{$regexp}$#"; |
203 | 231 | |
204 | 232 | $matches = array(); |
205 | 233 | $data = array(); |
206 | 234 | |
| 235 | + // Try to match the path we were asked to parse with our regexp |
207 | 236 | if ( preg_match( $regexp, $path, $m ) ) { |
| 237 | + // Ensure that any $# restriction we have set in our {$option}s |
| 238 | + // matches properly here. |
208 | 239 | foreach ( $pattern->options as $key => $option ) { |
209 | 240 | if ( preg_match( '/^\$\d+$/u', $key ) ) { |
210 | 241 | $n = intval( substr( $key, 1 ) ); |
211 | 242 | $value = rawurldecode( $m["par{$n}"] ); |
212 | 243 | if ( !in_array( $value, $option ) ) { |
| 244 | + // If any restriction does not match return null |
| 245 | + // to signify that this rule did not match. |
213 | 246 | return null; |
214 | 247 | } |
215 | 248 | } |
216 | 249 | } |
217 | 250 | |
| 251 | + // Give our $data array a copy of every $# that was matched |
218 | 252 | foreach ( $m as $matchKey => $matchValue ) { |
219 | 253 | if ( preg_match( '/^par\d+$/u', $matchKey ) ) { |
220 | 254 | $n = intval( substr( $matchKey, 3 ) ); |
221 | 255 | $data['$'.$n] = rawurldecode( $matchValue ); |
222 | 256 | } |
223 | 257 | } |
| 258 | + // If present give our $data array a $key as well |
224 | 259 | if ( isset( $pattern->key ) ) { |
225 | 260 | $data['$key'] = $pattern->key; |
226 | 261 | } |
227 | 262 | |
| 263 | + // Go through our parameters for this match and add data to our matches and data arrays |
228 | 264 | foreach ( $pattern->params as $paramName => $paramData ) { |
229 | 265 | $value = null; |
| 266 | + // Differentiate data: from normal parameters and keep the correct |
| 267 | + // array key around (ie: foo for data:foo) |
230 | 268 | if ( preg_match( '/^data:/u', $paramName ) ) { |
231 | 269 | $isData = true; |
232 | 270 | $key = substr( $paramName, 5 ); |
— | — | @@ -235,15 +273,19 @@ |
236 | 274 | } |
237 | 275 | |
238 | 276 | if ( isset( $paramData['value'] ) ) { |
| 277 | + // For basic values just set the raw data as the value |
239 | 278 | $value = $paramData['value']; |
240 | 279 | } elseif ( isset( $paramData['pattern'] ) ) { |
| 280 | + // For patterns we have to make value replacements on the string |
241 | 281 | $value = $paramData['pattern']; |
| 282 | + // For each $# match replace any $# within the value |
242 | 283 | foreach ( $m as $matchKey => $matchValue ) { |
243 | 284 | if ( preg_match( '/^par\d+$/u', $matchKey ) ) { |
244 | 285 | $n = intval( substr( $matchKey, 3 ) ); |
245 | 286 | $value = str_replace( '$' . $n, rawurldecode( $matchValue ), $value ); |
246 | 287 | } |
247 | 288 | } |
| 289 | + // If a key was set replace any $key within the value |
248 | 290 | if ( isset( $pattern->key ) ) { |
249 | 291 | $value = str_replace( '$key', $pattern->key, $value ); |
250 | 292 | } |
— | — | @@ -254,6 +296,7 @@ |
255 | 297 | } |
256 | 298 | } |
257 | 299 | |
| 300 | + // Send things that start with data: to $data, the rest to $matches |
258 | 301 | if ( $isData ) { |
259 | 302 | $data[$key] = $value; |
260 | 303 | } else { |
— | — | @@ -261,12 +304,15 @@ |
262 | 305 | } |
263 | 306 | } |
264 | 307 | |
| 308 | + // If this match includes a callback, execute it |
265 | 309 | if ( isset( $pattern->options['callback'] ) ) { |
266 | 310 | call_user_func_array( $pattern->options['callback'], array( &$matches, $data ) ); |
267 | 311 | } |
268 | 312 | } else { |
| 313 | + // Our regexp didn't match, return null to signify no match. |
269 | 314 | return null; |
270 | 315 | } |
| 316 | + // Fall through, everything went ok, return our matches array |
271 | 317 | return $matches; |
272 | 318 | } |
273 | 319 | |