WordPress.org

Make WordPress Core

Ticket #18276: wp-url-routes.php

File wp-url-routes.php, 23.3 KB (added by mikeschinkel, 3 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}