Make WordPress Core

Ticket #18276: wp-url-routes.php

File wp-url-routes.php, 23.3 KB (added by mikeschinkel, 15 years ago)

wp-url-routes.php

Line 
1<?php
2/*
3Filename: wp-url-routes.php
4Plugin Name: WP URL Routes
5Description: Robust and Flexible URL Routing replacement for WordPress' URL rewrite system of regular expression matching.
6Author: Mike Schinkel
7Author URL: http://mikeschinkel.com/
8Version: 0.0.1
9Globals: $wp_urls: Array with keys 'root', 'paths', 'query_vars', 'expansion_vars'
10*/
11
12/*
13 * Extends the WP class in WordPress and assigns an instance to the global $wp variable.
14 * Notes: This is needed because WordPress does not (yet?) have a hook for $wp->parse_request() as proposed in trac ticket #XXXXX
15*/
16class WP_Urls {
17 /*
18 * If self::$fallback
19 * ===true then WP_Urls_WP->parse_request() will fallback to call WP->parse_request()
20 * ===false then WP_Urls_WP->parse_request() will issue a 404 is wp_parse_request returns false.
21 */
22 static $fallback = true;
23 /*
24 * self::$path_segments - Holds the exploded path segments from $_SERVER['REQUEST_URI'] during and after inspection
25 */
26 static $path_segments = false;
27 /*
28 * self::$index - Hold the index of the path segment during inspection, starting with 0.
29 * Reset to false at end of WP_Urls::wp_parse_request().
30 */
31 static $index = false;
32
33 static $result = false;
34
35 static function on_load() {
36 add_filter( 'wp_parse_request', array( __CLASS__, 'wp_parse_request' ), 10, 2 );
37 add_action( 'init', array( __CLASS__, 'init' ), 0 ); // TODO: Verify if this should be 0. Why? Run before all other initializations remove_
38 }
39
40 static function init() {
41 global $wp_urls;
42 $wp_urls = array(
43 'root' => new WP_Url_Node(),
44 'paths' => array(),
45 'query_vars' => array(),
46 'expansion_vars' => array(),
47 );
48 }
49 static function wp_parse_request( $do_default, $extra_query_vars = '' ) {
50 global $wp;
51 if ( ! is_array( $wp->query_vars ) )
52 $wp->query_vars = array();
53
54 global $wp_urls;
55 $matched = false;
56
57 if ( self::$path_segments ) {
58 self::$index = self::$path_segments = false;
59 }
60
61 $path = esc_url( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
62 $path = str_replace( esc_url( $_SERVER['HTTP_HOST'] ), '', $path );
63 list( $path, $query ) = explode( '?', "{$path}?" );
64
65 if ( empty( $path ) || $path == '/' ) {
66
67 $wp->query_vars = array(); // Root is blank;
68 $matched = true;
69
70 } else {
71
72 self::$path_segments = explode( '/', trim( $path, '/' ) );
73
74 $node = $wp_urls['root'];
75 for( self::$index = 0; self::$index < count( self::$path_segments ); self::$index++ ) {
76 $this_path_segment = self::$path_segments[self::$index];
77 $node = self::_match_path_segment( $node );
78 if ( ! $node ) {
79 $matched = false;
80 break;
81 } else {
82 $matched = true;
83 if ( $node->inherit )
84 $wp->query_vars = array_merge( $wp->query_vars, $node->query_vars );
85 else
86 $wp->query_vars = $node->query_vars;
87 $wp->query_vars[preg_replace( '#%([^%]+)%#', '$1', $node->matched_pattern )] = $node->matched_segment;
88 }
89 }
90 }
91 if ( $matched ) {
92 if ( substr( $path, -1, 1 ) != '/' && $node->trailing_slash ) {
93 $new_path = "{$path}/" . ( strlen( $query ) == 0 ? '' : "?{$query}" );
94 if ( ! defined( 'SR_UNIT_TEST' ) ) {
95 wp_safe_redirect( site_url() . $new_path, 301 );
96 } else {
97 $wp->query_vars['_new_path'] = $new_path;
98 return $matched;
99 }
100 exit;
101 }
102 }
103 do_action('parse_request', $wp); // Mirror default WordPress
104
105 if ( $matched ) {
106 remove_action('template_redirect','redirect_canonical'); // TODO: Fix canonical redirects
107 }
108 return $matched;
109 }
110 function _match_path_segment( $parent_node ) {
111 $matched = false;
112 $path_segment = self::$path_segments[self::$index];
113
114 foreach( $parent_node->child_segments as $match_segment => $child_node ) {
115
116 if ( ! preg_match( '#%#', $match_segment ) ) { // Is it a literal? (vs a %var%)
117
118 if ( $path_segment == $match_segment ) {
119 $matched = true;
120 break;
121 }
122
123 } else //TODO: Make work for partial segments, i.e. /x-%foo%-y/
124 if ( preg_match( '#^%.+%$#', $match_segment, $the_match ) ) {
125
126 if ( $child_node->match_path_segment( $path_segment ) ) {
127 $matched = true;
128 break;
129 }
130
131 }
132 }
133
134 // To this point $matched is boolean. If $matched==true then we convert to an object of class WP_Url_Node to be the new $root.
135 if ( $matched ) {
136
137 $matched = &$child_node;
138
139 if ( ! $child_node->multi_segment ) {
140
141 $child_node->_expand_query_vars( $path_segment );
142
143 } else {
144
145 // This is for multi-segment matches; scan through to see if their is a match. Start with longest first.
146 $start_count = $segment_count = count( self::$path_segments );
147 while ( self::$index < $segment_count ) {
148 $path_segment = implode( '/', array_slice( self::$path_segments, self::$index, $segment_count ) );
149 if ( $child_node->match_path_segment( $path_segment ) ) {
150 $child_node->_expand_query_vars( $path_segment );
151 break;
152 }
153 $segment_count--;
154 }
155 if ( self::$index == $segment_count ) {
156 $matched = false;
157 } else if ( $start_count == $segment_count ) {
158 self::$index += $start_count; // All remaining segments were used.
159 } else {
160 self::$index += $segment_count - 1; // Decrement by one so when outer function's loop increments it will be in line.
161 }
162
163 }
164 $matched->matched_pattern = $match_segment;
165 $matched->parent_node = &$parent_node;
166 $matched->matched_segment = $path_segment;
167 }
168
169 return $matched;
170
171 }
172}
173WP_Urls::on_load();
174
175/*
176 * Represents one path node on the tree of URL path nodes. Each node has meta data associated this
177 * this plugin uses to determine how to route. Allow matches to be determined by a hierarchy of functionality
178 *
179 * TODO: Add all helpers needed to route WordPress' standard URLs.
180 *
181*/
182class WP_Url_Node {
183 var $matched_pattern = false; // The pattern used to match $this->matched_segment
184 var $matched_segment = ''; // The path that matched the $this->pattern
185 var $parent_node = false; // The parent node that was matched
186 var $child_segments = array(); // The child path segments that are potential at this level
187 var $multi_segment = false; // Is this a multi-segment (multi-backslash) node such as for pages with subpages?
188 var $inherit = false; // Inherit query vars from prior path segments?
189 var $regex = false; // regex that allows for matching
190
191 private $_trailing_slash = null; // Will this node have a trailing slash? Really only matters on last segment
192 private $_validate = false; // callable function that validates
193 private $_get_list = false; // callable function that returns a valid list
194 private $_this_list = false; // Temp storage for that returned by the evaluated $_get_list
195 private $_query_vars = array(); // The list of query vars
196
197 function __construct( $query_vars = array() ) {
198 $this->query_vars = $query_vars;
199 }
200 /*
201 * Attempt to match a path segment to this node
202 */
203 function match_path_segment( $path_segment ) {
204 $matched = false;
205 // We could add other ways to match URLs in the future
206 if ( $this->match_regex( $path_segment ) ||
207 $this->match_list( $path_segment ) ||
208 $this->match_validate( $path_segment ) ) {
209 $matched = true;
210 }
211 return $matched;
212 }
213
214 function match_regex( $path_segment ) {
215 $matched = false;
216 if ( $this->regex ) {
217 if ( preg_match( "#^{$this->regex}$#", $path_segment ) )
218 $matched = true;
219 }
220 return $matched;
221 }
222 function match_list( $path_segment ) {
223 $matched = false;
224 if ( $this->_get_list && ! $this->_this_list ) {
225 $this->_this_list = call_user_func( $this->get_list, $this->_get_args() );
226 if ( in_array( $path_segment, $this->_this_list ) ) {
227 $matched = true;
228 }
229 }
230 return $matched;
231 }
232 function match_validate( $path_segment ) {
233 $matched = false;
234 if ( $this->_validate ) {
235 if ( call_user_func( $this->validate, $this->_get_args( $path_segment ) ) ) {
236 $matched = true;
237 }
238 }
239 return $matched;
240 }
241
242 function _expand_query_vars( $path_segment = false, $args = false ) {
243 if ( ! $args )
244 $args = $this->_get_args( $path_segment );
245 foreach( $this->query_vars as $var_name => $var_value ) {
246 foreach( $args as $arg_name => $arg_value ) {
247 if ( strpos( $var_value, "%{$arg_name}%" ) !== false ) {
248 $this->_query_vars[$var_name] = str_replace( "%{$arg_name}%", $arg_value, $var_value );
249 }
250 }
251 }
252 return;
253 }
254
255 function _get_args( $path_segment = false ) {
256 $index = WP_Urls::$index;
257 $path_segments = WP_Urls::$path_segments;
258 if ( ! $path_segment )
259 $path_segment = $path_segments[$index];
260
261 $args = array( 'this' => $path_segment );
262
263 // Remove the path segment to test (which could be a multi-segment) and replace with a single dummy segment
264 $path_segments = implode( '/', $path_segments );
265 $path_segments = trim( str_replace( "/{$path_segment}/", '/~/', "/{$path_segments}/" ), '/' );
266 $path_segments = explode( '/', $path_segments );
267
268 // Now add the other args, as appropriate
269 if ( 0 < $index ) $args['parent'] = $path_segments[$index - 1];
270 if ( 1 < $index ) $args['grandparent'] = $path_segments[$index - 2];
271 if ( 2 < $index ) $args['greatgrandparent'] = $path_segments[$index - 3];
272 if ( $index + 1 < count( $path_segments ) ) $args['child'] = $path_segments[$index + 1];
273 if ( $index + 2 < count( $path_segments ) ) $args['grandchild'] = $path_segments[$index + 2];
274 if ( $index + 3 < count( $path_segments ) ) $args['greatgrandchild'] = $path_segments[$index + 3];
275
276 $args['ordered'] = $args['offset'] = array();
277 for( $segment = 0; $segment < count( $path_segments ); $segment++ ) {
278 $offset = $segment - $index;
279 $args['ordered'][$segment] = $args['offset'][$offset] = $segment == $index ? $path_segment : $path_segments[$segment];
280 }
281
282 return $args;
283 }
284 function __get( $name ) {
285 $value = null;
286 switch( $name ) {
287 case 'query_vars':
288 $value = &$this->_query_vars;
289 break;
290 case 'get_list':
291 case 'validate':
292 $property = "_{$name}";
293 $value = $this->$property;
294 $is_immediate_func = $value[0] == '%';
295 $is_delayed_func = $is_immediate_func ? false : ! empty( $value );
296 if ( $is_immediate_func || $is_delayed_func ) {
297 $callable = self::_locate_callable( strpos( $value, '::' ) > 0 ? explode( '::', $value ) : $value );
298 if ( $is_delayed_func )
299 $value = $callable;
300 else if ( $is_immediate_func ) {
301 $value = call_user_func( $callable, $this->_get_args() );
302 }
303 }
304 break;
305 case 'trailing_slash':
306 if ( is_null( $this->_trailing_slash ) ) {
307 $last_segment = WP_Urls::$path_segments[count(WP_Urls::$path_segments)-1];
308 $this->_trailing_slash = strpos( $last_segment, '.' ) === false;
309 }
310 $value = $this->_trailing_slash;
311 break;
312 }
313 return $value;
314 }
315 function __set( $name, $value ) {
316 switch ( $name ) {
317 case 'query_vars':
318 /*
319 * Convert query vars that are prefixed with '@' into properties of the Query Vars object
320 * This is a bit unorthodox, allowing other properties to be set by assigning one property
321 * specially formatted array, but this makes specification very easy and this class it
322 * very purpose built; it is not meant for reuse in other areas.
323 */
324 foreach( $value as $attribute_name => $property_value ) {
325 if ( $attribute_name[0] == '@' ) {
326 $property_name = substr( $attribute_name, 1 ); // Strip off the '@'
327 if ( property_exists( $this, $property_name ) ) {
328 $this->$property_name = $property_value;
329 } else if ( property_exists( $this, "_{$property_name}" )) {
330 $property_name = "_{$property_name}";
331 $this->$property_name = $property_value;
332 } else {
333 wp_die( "ERROR: Attempting to set a property named '{$property_name}' and " . __CLASS__ . ' does not contain that property.' );
334 }
335 // Remove it from the $query_vars array
336 unset( $value[$attribute_name] );
337 }
338 }
339 // Assign the remaining real query vars to the query_vars property.
340 $this->_query_vars = $value;
341 break;
342 }
343 }
344 static function _locate_callable( $callable ) {
345 // Check to see is this is a function name or an array representing a method call.
346 if ( ! function_exists( $callable ) ) {
347 foreach( array( 'WP_Url_Helpers' ) as $class ) {
348 // Check to see if it happens to be a method is the WP_Url_Helpers class.
349 if ( method_exists( $class, $callable ) ) {
350 $callable = array( $class, $callable );
351 break;
352 }
353 }
354 }
355 return $callable;
356 }
357}
358
359/*
360 * Class containing helper functions that can be used for lookups, etc.
361 * There's a lot of work that can be done here, what's here is just a starting point.
362 *
363 * TODO: Add all helpers needed to route WordPress' standard URLs.
364 *
365*/
366class WP_Url_Helpers {
367 static function is_category_slug( $args ) {
368 $term = get_term_by('slug', $args['this'], 'category' );
369 return $term !== false;
370 }
371 static function is_post_in_category( $args ) {
372 global $wpdb;
373 $query = new WP_Query( array( 'name' => $args['this'] ) );
374 $found = false;
375 if ( $query->post_count == 1 ) {
376 $category_slug = $args['parent'];
377 $term = get_term_by('slug', $category_slug, 'category' );
378 $sql = "SELECT ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tt.term_id = %d AND p.ID = %d";
379 $found = $query->post->ID == $wpdb->get_var( $wpdb->prepare( $sql, $term->term_id, $query->post->ID ) );
380 }
381 return $found;
382 }
383 static function get_post_type_by_slug( $args ) {
384 $post_type_archive_slugs = array_flip( self::get_post_type_archive_slug_list( $args ) );
385 if ( ! isset($post_type_archive_slugs[ $args['this'] ]) )
386 return false;
387 else
388 return $post_type_archive_slugs[ $args['this'] ];
389 }
390 static function get_page_list( $args ) { //TODO: Make this work with child pages and private posts or other ones that should match
391 global $wpdb;
392 static $parents = array();
393 $parent_path = $args['parent'];
394 if ( ! isset( $parents[$parent_path] ) ) {
395 $parent_id = 0;
396 if ( ! empty( $parent_path ) ) {
397 $parts = explode( '/', $parent_path );
398 foreach( $parts AS $index => $part ) {
399 $sql = "SELECT ID FROM {$wpdb->posts} WHERE post_name='%s' AND post_parent=%d AND post_type='page' AND post_status='publish'";
400 $parent_id = $wpdb->get_var( $wpdb->prepare( $sql, $part, $parent_id ) );
401 }
402 }
403 $parents[ $parent_path ] = $parent_id;
404 }
405 // TODO: We will cache these after the logic is robust
406 $sql = "SELECT post_name FROM {$wpdb->posts} WHERE post_parent={$parents[$parent_path]} AND post_type='page' AND post_status='publish'";
407 $page_names = $wpdb->get_col($sql);
408 return $page_names;
409 }
410 static function get_post_type_archive_slug_list( $args ) {
411 global $wp_post_types;
412 static $post_type_archive_slugs;
413 if ( ! isset( $post_type_archive_slugs ) ) {
414 $post_type_archive_slugs = array();
415 foreach( array_keys( $wp_post_types ) as $post_type) {
416 $slug = get_post_type_archive_link( $post_type ); // TODO: This is missing because of wp31
417 if ( $slug ) {
418 $slug = end( explode( '/', trim( $slug, '/' ) ) );
419 $post_type_archive_slugs[$post_type] = $slug;
420 }
421 }
422 }
423 return $post_type_archive_slugs;
424 }
425 static function is_valid_page( $args ) { //TODO: Make this work with child pages and private posts or other ones that should match
426 $page = get_page_by_path( $args['this'] );
427 return ( is_object( $page ) );
428 }
429 static function is_valid_post_type_slug( $args ) {
430 $post_type_archive_slugs = array_flip( self::get_post_type_archive_slug_list( $args ) );
431 return isset( $post_type_archive_slugs[ $args['this'] ] );
432 }
433 static function is_valid_post( $args ) {
434 global $wp_post_types;
435 $is_valid = false;
436 $post_type_slug = $args['parent'];
437 if ( $post_type_slug = self::get_post_type_by_slug($post_type_slug)) {
438 global $wpdb;
439 $post_type_object = $wp_post_types[$post_type_slug];
440 $sql = "SELECT COUNT(*) AS match_count FROM {$wpdb->posts} WHERE post_parent=0 AND post_type='{$post_type_slug}' AND post_name='%s' AND post_status='publish'";
441 $sql = $wpdb->prepare($sql,$post_name);
442 $match = $wpdb->get_var($sql);
443 $is_valid = ($match>0);
444 }
445 return $is_valid;
446 }
447}
448
449/*
450 * Extends the WP class in WordPress and assigns an instance to the global $wp variable.
451 * Notes: This is needed because WordPress does not (yet?) have a hook for $wp->parse_request() as proposed in trac ticket #XXXXX
452*/
453class WP_Urls_WP extends WP {
454 static function on_load() {
455 // 'setup_theme' is 1st hook run after WP is created.
456 add_action( 'setup_theme', array( __CLASS__, 'setup_theme' ) );
457 add_filter( 'template_include', array( __CLASS__, 'template_include' ) );
458 }
459 static function setup_theme() {
460 global $wp;
461 $wp = new WP_Urls_WP(); // Replace the global $wp
462 }
463 static function template_include( $template ) {
464 if ( WP_DEBUG ) {
465 //TODO: Come up with a better way to handle showing developers if routing matched or not.
466 $result = WP_Urls::$result;
467 echo "<div id=\"url-routing-results\">URL Routing Result: {$result}</div>";
468 }
469 return $template;
470 }
471 function parse_request( $extra_query_vars = '' ) {
472 if ( apply_filters( 'wp_parse_request', false, $extra_query_vars ) ) {
473 WP_Urls::$result = 'routed';
474 } else {
475 WP_Urls::$result = 'fallback';
476 if ( WP_Urls::$fallback ) {
477 parent::parse_request($extra_query_vars); // Delegate to WP class
478 } else {
479 wp_die( 'URL Routing failed.' );
480 }
481 }
482 return;
483 }
484}
485WP_Urls_WP::on_load();
486
487/*
488 * Register query variables so that we can associate meta data needed to route URLs.
489 *
490 * Attributes starting with '@' are keys that should end up on the resultant $wp->query_vars
491 *
492 * TODO: Define all the remaining query variables built into WordPress
493 */
494function register_query_var( $var, $args = array() ) {
495 global $wp_urls;
496
497 if ( strpos( $var, '%' ) !== false )
498 $var = str_replace( '%', '', $var );
499
500 switch ( $var ) {
501 case 'name':
502 $defaults = array(
503 '@inherit' => true, // WordPress does not (typically?) have any other query vars for posts
504 'name' => '%this%',
505 //'page' => '', // This is what WordPress has for a /year/mon/day/post URL
506
507 );
508 break;
509
510 case 'category_name':
511 $defaults = array(
512 '@validate' => 'is_category_slug',
513 'category_name' => '%this%',
514 );
515 break;
516
517 case 'pagename':
518 $defaults = array(
519 '@validate' => 'is_valid_page',
520 '@multi_segment' => true,
521 'page' => '', // This is match WordPress' behavior
522 'pagename' => '%this%',
523 );
524 break;
525
526 case 'post_type_slug':
527 $defaults = array(
528 '@validate' => 'is_valid_post_type_slug',
529 '@get_list' => 'get_post_type_archive_slug_list',
530 'post_type' => '%get_post_type_by_slug%',
531 );
532 break;
533
534 case 'year':
535 $defaults = array(
536 '@regex' => '([0-9]{4})',
537 'year' => '%this%',
538 'post_type' => 'post',
539 );
540 break;
541 }
542
543 if ( count( $args ) == 0 )
544 $args = $defaults;
545 else
546 $args = wp_parse_args( $args, $defaults );
547
548 $wp_urls['query_vars']["%{$var}%"] = &$args;
549
550 return $args;
551}
552/*
553 * Register a complete URL path using permalink structure format, i.e.
554 *
555 * '%category_name%/%name%'
556 *
557 * TODO: Define all the remaining url paths built into WordPress
558 *
559 */
560function register_url_path( $path, $query_vars = array() ) {
561 global $wp_urls;
562
563 // Register the query vars for this path.
564 __register_query_vars_from_path( $path );
565
566 // Split URL path on '/' into 'path segments'
567 $path_segments = explode( '/', trim( $path, '/' ) );
568
569 // Create a local reference to the anchoring 'root' URL
570 $root = &$wp_urls['root'];
571
572 // For each path segment
573 $parent_path = array();
574 foreach( $path_segments as $path_segment ) {
575
576 $parent_path[] = $path_segment; // Grab this for defining parent path of subnodes
577
578 // Grab the query predefined for this path segment, assuming it is full a query_var
579 $this_path_segment_query_vars = isset( $wp_urls['query_vars'][$path_segment] ) ? $wp_urls['query_vars'][$path_segment] : array();
580
581 // If this post segment for this URL route does not already has some query vars defined
582 if ( ! isset ( $root->child_segments[$path_segment] ) ) {
583 // Set them.
584 $root->child_segments[$path_segment] = new WP_Url_Node( $this_path_segment_query_vars );
585
586 }
587
588 // Set the child to be the root and continue down the URL path tree.
589 $root = &$root->child_segments[$path_segment];
590
591 }
592
593 // For the last path segment, merge the path segment's default query vars with the ones passed to this function taking the passed ones as prioprity.
594 $query_vars = array_merge( $this_path_segment_query_vars, $query_vars );
595
596 // Now look for the default query vars for this path, merge them with the ones passed to this function taking the passed ones as priority.
597 $root->query_vars = __register_url_path( $path, $query_vars );
598
599 // Finally set the query_vars for this path as well as for this segment.
600 $wp_urls['paths'][$path] = $root->query_vars;
601}
602/*
603 * Define the metadata for every pre-defined path so that these routes can be specified simply like this:
604 *
605 * register_url_path( '%category_name%/%name%' );
606 *
607 * This function will also provides a roadmap for hwo to define custom URL routes.
608 *
609 */
610function __register_url_path( $path, $query_vars = array() ) {
611
612 switch ( $path ) {
613
614 case '%category_name%/%name%':
615 $defaults = array(
616 '@validate' => 'is_post_in_category',
617 '@inherit' => true,
618 );
619 break;
620
621 case '%post_type_slug%/%name%':
622 $defaults = array(
623 '@validate' => 'is_valid_post',
624 '@get_list' => 'get_post_name_list',
625 );
626 break;
627
628 case '%year%':
629 $defaults = array(
630 '@regex' => '[0..9]{4}',
631 'year' => '%this%',
632 'post_type' => 'post',
633 );
634 break;
635
636 case '%year%/%monthnum%':
637 $defaults = array(
638 '@regex' => '(0[1..9]|[1][0..2])',
639 'monthnum' => '%this%',
640 );
641 break;
642
643 case '%year%/%monthnum%/%day%':
644 $defaults = array(
645 '@validate' => 'is_valid_day', //TODO: Can we make this work, using the query vars?
646 'day' => '%this%',
647 );
648 break;
649
650 case '%year%/%monthnum%/%day%/%name%': // TODO: Allow cumulative, but allow clearing/resetting of prior vars
651 $defaults = array(
652 '@validate' => 'is_valid_post_for_date', //TODO: Can we make this work, using the query vars?
653 'post_name' => '%this%',
654 );
655 break;
656
657 case '%pagename%':
658 $defaults = array(
659 '@multi_segment'=> true,
660 );
661 break;
662
663 case 'robots.txt':
664 $defaults = array(
665 'robots' => 1,
666 );
667 break;
668
669 }
670
671 if ( count( $query_vars ) == 0 )
672 $query_vars = $defaults;
673 else
674 $query_vars = array_merge( $defaults, $query_vars );
675
676 return $query_vars;
677}
678
679/*
680 * Take variables found in a path like '%category_name%/%name%'
681 * and register them as query variables if not already registered.
682 *
683 */
684function __register_query_vars_from_path( $path ) {
685 global $wp_urls;
686 preg_match_all( '#%([^%])+%#', $path, $matches, PREG_SET_ORDER );
687 foreach( $matches as $match ) {
688 if ( ! isset( $wp_urls[$match[0]] ) ) // Only register if not already registered. Why do twice?
689 register_query_var( $match[0] );
690 }
691}
692
693/*
694 * Expansion variables are double percent quoted that can expand,
695 * i.e. a '%%date_path%%' might be expanded to '%year%/%month%/%day%'
696 *
697 * TODO: Implement this
698 *
699 */
700function register_expansion_var( $var, $args = array() ) {
701 global $wp_urls;
702 $wp_urls['expansion_vars'][$var] = $args;
703}
704
705/*
706 * Define OMIT_URL_ROUTES_TEST_CONFIG if you want to omit this test configuration.
707 */
708if ( ! defined( 'OMIT_URL_ROUTES_TEST_CONFIG') ) {
709 add_action( 'init', '_wp_url_routes_test_config' );
710 function _wp_url_routes_test_config() {
711 register_url_path( '%category_name%/%name%' );
712 register_url_path( '%pagename%' );
713 }
714}