--- /dev/null
+Horde_Routes Integration Guide
+
+This document is based on the manual for the Python version available at:
+http://routes.groovie.org/integration.html
+
+
+Integrating Horde_Routes
+
+Horde_Routes is built to function with any PHP 5 web framework that utilizes a
+MVC style approach to design. However, it can be used completely outside of a
+MVC context as well.
+
+For full integration with a web framework, several steps should be done during
+different phases of the request cycle. The web framework developer should also
+have a way to add routes to the mapper, and accessing utility functions.
+
+Terminology:
+
+Framework Integrator
+ Typically the person who is working to integrate routes into a web framework
+Web/Framework Developer
+ The web application developer using the web framework
+
+
+Route Setup
+
+Horde_Routes requires PHP 5.1 or above. It does not require() its own files, so
+an autoloader must be registered that will load the Horde_Routes files from its
+PEAR directory. Alternatively, the files can be loaded explicitly:
+
+ require_once 'Horde/Routes/Mapper.php';
+ require_once 'Horde/Routes/Exception.php';
+ require_once 'Horde/Routes/Route.php';
+ require_once 'Horde/Routes/Utils.php';
+
+The web framework developer will need a place to add routes that will be used
+during the URL resolution phase. Commonly, the mapper object will be directly
+exposed in a configuration file:
+
+$m = new Horde_Routes_Mapper();
+$m->connect(':controller/:action/:id');
+
+The developer can then add additional routes as they see fit. The Framework
+Integrator should make sure that their code takes care of using the Mapper
+instance and preserving it somewhere for use by the framework.
+
+
+Pre-Loading the Controllers
+
+Due to the way Horde_Routes makes use of regular expressions for URL
+recognition, Horde_Routes_Mapper requires a valid list of Controllers before
+it can match URLs. Controller names can contain any character including /.
+Once the controller list is created, the Mapper method createRegs() should be
+called:
+
+$m->createRegs(array('content', 'admin/comments', 'blog'))
+
+After this step, the Mapper object is now ready to match URLs.
+
+Instead of calling Horde_Routes_Mapper->createRegs() directly, a PHP callback
+can also be passed to the Horde_Routes_Mapper constructor. See the inline
+source documentation.
+
+
+URL Resolution
+
+When the URL is looked up, it should be matched against the Mapper. When
+matching an incoming URL, it is assumed that the URL path is the only string
+being matched. All query args should be stripped before matching:
+
+ $m->connect('articles/:year/:month',
+ array('controller'=>'blog', 'action'=>'view', 'year'=>null));
+
+ $m->match('/articles/2003/10');
+ // Returns:
+ // array('controller'=>'blog', 'action'=>'view',
+ 'year'=>'2003', 'month'=>'10')
+
+Matching a URL will return an associative array of the match results. If
+you'd like to differentiate between where the argument came from you can use
+routematch() which will return the Route object that has all these details:
+
+ $m->connect('articles/:year/:month',
+ array('controller'=>'blog', 'action'=>'view', 'year'=>null));
+
+ $result = $m->routematch('/articles/2003/10');
+ // result is array(matchdata, Horde_Routes_Route)
+
+ // result[0] - array('controller'=>'blog', 'action'=>'view',
+ // 'year'=>'2003', 'month'=>'10')
+ //
+ // result[1] - Horde_Routes_Route instance
+ // result[1]->defaults - array('controller'=>'blog', 'action'=>'view',
+ // 'year'=>null);
+ // result[1]->hardcoded - array('controller', 'action')
+
+Your integration code is then expected to dispatch to a controller and action
+in the associative array. How it does this is entirely up to the framework
+integrator. Your integration should also typically provide the web developer a
+mechanism to access the additional associative array values.
+
+
+Providing Utilities
+
+In addition to allowing the web developer to define routes on the Mapper instance,
+access to a Horde_Routes_Utils instance should be provided:
+
+ $utils = $m->utils;
+
+The $utils instance provides the web developer with the utility functions urlFor() and
+redirectTo(), which will be used to generate URLs from the route mapping.
+
+ $url = $utils->urlFor(array('controller' => 'articles', 'action' => 'view',
+ 'id' => $article->id));
--- /dev/null
+Horde_Routes Manual
+
+This document is based on the manual for the Python version available at:
+http://routes.groovie.org/manual.html
+
+
+1 Introduction
+
+Horde_Routes tackles an interesting problem that comes up frequently in web
+development, how do you map a URL to your code? While there are many solutions
+to this problem, that range from using the URL paths as an object publishing
+hierarchy, to regular expression matching; Horde_Routes goes a slightly different
+way.
+
+Using Horde_Routes, you specify parts of the URL path and how to match them to your
+Controllers and Actions. The specific web framework you're using may actually
+call them by slightly different names, but for the sake of consistency we will
+use these names.
+
+Horde_Routes lets you have multiple ways to get to the same Controller and Action,
+and uses an intelligent lookup mechanism to try and guarantee you the URL with
+the least cruft when generating the URL.
+
+URL Cruft
+ Shorthand reference to what will occur if a Route can't handle all the
+ arguments we want to send it. Those arguments become HTTP query args
+ (/something ?query=arg&another=arg ), which we try to avoid when
+ generating a URL.
+
+
+2 Setting Up Routes
+
+To setup Horde_Routes, it is assumed that you are using a web framework that has the
+Horde_Routes mechanism integrated for you. The web framework should have somewhere
+setup for you to add a Route to your Mapper.
+
+Route (Horde_Routes_Route)
+ A Route is a mapping of a URL to a controller, action, and/or additional
+ variables. Matching a Route will always result in a controller and action.
+ Route objects are typically created and managed by the Mapper.
+Mapper (Horde_Routes_Mapper)
+ The Mapper is the main class used to hold, organize, and match Routes.
+ While you can create a Route object independently of the Mapper, its not
+ nearly as useful. The Mapper is what you will use to add Routes and what
+ the web framework uses to match incoming URLs.
+
+We will also assume for this introduction that your Mapper instance is exposed
+to you as $m, for example:
+
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:id');
+
+The above example covers one of the most common routes that is typically
+considered the default route. This very flexible route allows virtually all of
+your controllers and actions to be called. Adding more routes is done in a
+similar manner, by calling $m->connect(...) and giving the Mapper instance a set
+of arguments.
+
+The following are all valid examples of adding routes:
+
+ $m->connect('archives/:year/:month/:day',
+ array('controller'=>'archives', 'action'=>'view', 'year'=>2004,
+ 'requirements'=> array('year'=>'\d{2,4}', 'month'=>'\d{1,2}')));
+
+ $m->connect('feeds/:category/atom.xml',
+ array('controller'=>'feeds', 'action'=>'atom'));
+
+ $m->connect('history', 'archives/by_eon/:century',
+ array('controller'=>'archives', 'action'=>'aggregate', 'century'=>1800));
+
+ $m->connect('article', 'article/:section/:slug/:page.html',
+ array('controller'=>'article', 'action'=>'view'));
+
+ $m->connect(':controller/:action/:id');
+
+ $m->connect('home', '',
+ array('controller'=>'blog', 'action'=>'index'));
+
+In the following sections, we'll highlight the section of the Route we're
+referring to in the first example.
+
+
+2.1 Route Name
+
+Optional
+
+ $m->connect('history', 'archives/by_eon/:century',
+ array('controller'=>'archives...
+
+A Route can have a name, this is also referred to as Named Routes and lets you
+quickly reference the Defaults that the route was configured with. This is the
+first non-keyword argument, and if not present the first non-keyword argument
+is assumed to be the route path.
+
+Route Names are mainly used when generating routes, and have no other effect
+on matching a URL.
+
+
+2.1.1 Static Named Routes
+
+Horde_Routes also supports static named routes. These are routes that do not
+involve actual URL generation, but instead allow you to quickly alias common
+URLs. For example:
+
+ $m->connect('google_search', 'http://www.google.com/search',
+ array('_static'=>True));
+
+Static Named Routes are ignored entirely when matching a URL.
+
+
+2.1.2 Filter Functions
+
+Named routes can have functions associated with them that will operate on the
+arguments used during generation. If you have a route that requires multiple
+arguments to generate, like:
+
+ $m->connect('archives/:year/:month/:day', array('controller'=>'archives',
+ 'action'=>'view', 'year'=>2004,
+ 'requirements'=>array('year'=>'\d{2,4}', 'month'=>'\d{1,2}')));
+
+To generate a URL for this will require a month and day argument, and a year
+argument if you don't want to use 2004. When using Routes with a database or
+other objects that might have all this information, it's useful to let Routes
+expand that information so you don't have to.
+
+Consider the case where you have a story object which has a year, month, and
+day attribute. You could generate the URL with:
+
+$utils = $m->utils;
+$utils->urlFor(array('year' => $story->year,
+ 'month' => $story->month,
+ 'day' => $story->day));
+
+This isn't terribly convenient, and can be brittle if for some reason you need
+to change the story object's interface. Here's an example of setting up a
+filter function:
+
+function story_expand($kargs) {
+ // only alter $kargs if a story keyword arg is present
+ if (! in_array('story', $kargs)) {
+ return $kargs;
+ }
+
+ $story = $kargs['story'];
+ unset ($kargs['story']);
+
+ $kargs['year'] = $story->year;
+ $kargs['month'] = $story->month;
+ $kargs['day'] = $story->day;
+
+ return $kargs;
+}
+
+$m->connect('archives', 'archives/:year/:month/:day',
+ array('controller' => 'archives', 'action' => 'view', 'year' => 2004,
+ 'requirements' => array('year'=>'\d{2,4}', 'month'=>'\d{1,2}'),
+ 'filter'=> 'story_expand'));
+
+This filter function will be used when using the named route archives. If a
+story keyword argument is present, it will use that and alter the keyword
+arguments used to generate the actual route.
+
+If you have a story object with those attributes, making the route would now
+be done with the following arguments:
+
+$utils = $m->utils;
+$utils->urlFor('archives', array('story' => $myStory));
+
+If the story interface changes, you can change how the arguments are pulled
+out in a single location. This also makes it substantially easier to generate
+the URL.
+
+*Warning* Using the filter function requires the route to be a named route.
+ This is due to how the filter function can affect the route that
+ actually gets chosen. The only way to reliably ensure the proper
+ filter function gets used is by naming the route, and using its
+ route name with Horde_Routes_Utils->urlFor().
+
+
+2.2 Route Path
+
+Required
+
+ $m->connect('feeds/:category/atom.xml',
+ array('controller'=>'feeds', 'action'=>'atom'));
+
+The Route Path determines the URL mapping for the Route. In the above example
+a URL like /feeds/electronics/atom.xml will match this route.
+
+A Route Path is separated into parts that you define, the naming used when
+referencing the different types of route parts are:
+
+Static Part
+
+ $m->connect('feeds/:category/atom.xml',
+ array('controller'=>'feeds', 'action'=>'atom'));
+
+ A plain-text part of the URL, this doesn't result in any Route variables.
+
+Dynamic Part
+
+ $m->connect('feeds/:category/atom.xml',
+ array('controller'=>'feeds', 'action'=>'atom'))
+
+ A dynamic part matches text in that part of the URL, and assigns what it
+ finds to the name after the : mark.
+
+Wildcard Part
+
+ $m->connect('file/*url',
+ array('controller'=>'file', 'action'=>'serve'));
+
+ A wildcard part will match everything except the other parts around it.
+
+Groupings
+
+ $m->connect('article', 'article/:section/:slug/:(page).html', ...
+
+ $m->connect('file/*(url).html',
+ array('controller'=>'file', 'action'=>'serve'));
+
+ Groupings let you define boundaries for the match with the () characters.
+ This allows you to match wildcards and dynamics next to other static and
+ dynamic parts. Care should be taken when using Groupings next to each
+ other.
+
+
+2.3 Defaults
+
+Optional
+
+ $m->connect('history', 'archives/by_eon/:century',
+ array('controller'=>'archives', 'action'=>'aggregate', 'century'=>1800));
+
+The keyword options in a route (not including the requirements keyword arg)
+that can determine the default for a route. If a default is specified for a
+variable that is not a dynamic part, then its not only a default but is also a
+hardcoded variable. The controller and action are hardcoded variables in the
+example above because despite the URL, they will always be 'archives' and
+'aggregate' respectively.
+
+Hardcoded Variable
+ Default keyword that does not exist in the route path. This keyword
+ variable cannot be changed by the URL coming in.
+
+
+2.4 Requirements
+
+Optional
+
+ $m->connect('archives/:year/:month/:day',
+ array('controller'=>'archives', 'action'=>'view', 'year'=>2004,
+ 'requirements' => array('year'=>'\d{2,4}', 'month'=>'\d{1,2}')));
+
+Requirements is a special keyword used by Routes to enforce a regular
+expression restriction on the dynamic part or wildcard part of a route path.
+
+Note in the example above that the regular expressions do not have boundaries
+such as they would with a PHP function like preg_match(). The expression is
+simply "\d{2,4}" and not "/\d{2,4}/".
+
+
+2.5 Conditions
+
+Optional
+
+ $m->connect('user/new;preview',
+ array('controller' => 'user', 'action' => 'preview',
+ 'conditions' => array('method' => array('POST'))));
+
+Conditions specifies a set of special conditions that must be met for the
+route to be accepted as a valid match for the URL. The conditions argument
+must always be a dictionary and can accept 3 different keys.
+
+method
+ Request must be one of the HTTP methods defined here. This argument must
+ be a list of HTTP methods, and should be upper-case.
+subDomain
+ Can either be True or an array of sub-domains, one of which must be
+ present.
+function
+ A function that will be used to evaluate if the Route is a match. Must
+ return True or False, and will be called with the environ and match_dict.
+ The match_dict is a dict with all the Route variables for the request.
+ Modifications to match_dict will appear identical to Route variables from
+ the original match.
+
+Examples:
+
+ // The method to be either GET or HEAD
+ m->connect('user/list',
+ array('controller' => 'user', 'action' => 'list',
+ 'conditions' => array('method' => array('GET', 'HEAD'))));
+
+
+ // A sub-domain should be present
+ $m->connect('',
+ array('controller' => 'user', 'action' => 'home',
+ 'conditions' => array('subDomain' => true)));
+
+ // Sub-domain should be either 'fred' or 'george'
+ $m->connect('',
+ array('controller' => 'user', 'action' => 'home',
+ 'conditions' => array('subDomain' => array('fred', 'george')));
+
+
+ /**
+ * Put the referrer into the resulting match dictionary,
+ * this won't stop the match since it always returns True
+ */
+ function referals($environ, $result) {
+ $referer = isset($environ['HTTP_REFERER']) ? $environ['HTTP_REFERER'] : null;
+ $result['referer'] = $referer;
+ return true;
+ }
+
+ $m->connect(':controller/:action/:id',
+ array('conditions' => array('function'=>'referals')));
+
+
+3 The Nitty Gritty of Route Setup
+
+3.1 Minimum URLs
+
+Routes will use your defaults to try and minimize the required length of your
+URL whenever possible. For example:
+
+ $m->connect(':controller/:action/:id',
+ array('action'=>'view', 'id'=>4));
+
+ # Will match all of the following
+ # /content/view/4
+ # /content/view
+ # /content
+
+Trailing dynamic parts of a route path that have defaults setup are not
+required to exist in the URL being matched. This means that each of the URL
+examples shown above will result in the same set of keyword arguments being
+sent to the same controller and action.
+
+If a dynamic part with a default is followed by either static parts or dynamic
+parts without defaults, that dynamic part will be required despite having a
+default:
+
+ // Remember that :action has an implicit default
+ $m->connect('archives/:action/:article',
+ array('controller'=>'blog'));
+
+ # Matches:
+ # /archives/view/introduction
+ # /archives/edit/recipes
+
+ # Does Not Match:
+ # /archives/introduction
+ # /archives/recipes
+
+This way, the URL coming in maps up to the route path you created, part for part.
+
+When using Groupings, parts will still be left off, but only if the remainder
+of the URL has no static after it. This can lead to some odd looking URLs
+being generated if you aren't careful about your requirements and defaults.
+For example:
+
+ # Groupings without requirements
+ $m->connect(':controller/:(action)-:(id)')
+
+ # Matches:
+ # /archives/view-3
+ # /archives/view-
+
+ # Generation:
+ $utils = $m->utils;
+ $utils->urlFor(array('controller'=>'archives', 'action'=>'view');
+ # /archives/view-
+
+It's unlikely you want such a URL, and would prefer to ensure that there's
+always an id supplied. To enforce this behavior we will use Requirements:
+
+ # Groupings without requirements
+ $m->connect(':controller/:(action)-:(id)',
+ array('requirements'=> array('id'=>'\d+')));
+
+ # Matches:
+ # /archives/view-3
+ # /archives/view-2
+
+ # Does Not Match:
+ # /archives/view-
+
+ # Generation:
+ $utils = $m->utils;
+ $utils->urlFor(array('controller'=>'archives', 'action'=>'view', 'id'=>2));
+ # /archives/view-2
+
+If you end up with URLs missing parts you'd like left on when using Groupings,
+add a requirement to that part.
+
+
+3.2 Implicit Defaults
+
+The above rule regarding minimum URLs has two built-in implicit defaults. If
+you use either action or id in your route path and don't specify defaults for
+them, Routes will automatically assign the following defaults to them for you:
+
+ array('action' => 'index', 'id' => null)
+
+This is why using the following setup doesn't require an action or id in the URL:
+
+ $m->connect(':controller/:action/:id');
+
+ # '/blog' -> controller='blog', action='index', id=None
+
+
+3.3 Search Order
+
+When setting up your routes, remember that when using routes the order in
+which you set them up can affect the URL that's generated. Routes will try and
+use all the keyword args during route generation and if multiple routes can be
+generated given the set of keyword args, the first and shortest route that was
+connected to the mapper will be used. Hardcoded variables are also used first
+if available as they typically result in shorter URLs.
+
+For example:
+ # Route Setup
+ $m->connect('archives/:year',
+ array('controller'=>'blog', 'action'=>'view', 'year'=null));
+ $m->connect(':controller/:action/:id');
+
+ # Route Usage
+ $utils = $m->utils;
+ $utils->urlFor(array('controller'=>'blog', 'action'=>'view'));
+ # -> '/archives'
+
+You will typically want your specific and detailed routes at the top of your
+Route setup and the more generic routes at the bottom.
+
+
+3.4 Wildcard Limitations and Gotchas
+
+Due to the nature of wildcard parts, using wildcards in your route path can
+result in URL matches that you didn't expect. Wildcard parts are extremely
+powerful and when combined with dynamic parts that have defaults can confuse
+the new Routes user.
+
+When you have dynamic parts with defaults, you should never place them
+directly next to a wildcard part. This can result in the wildcard part eating
+the part in the URL that was intended as the dynamic part.
+
+For example:
+
+ $m->connect('*url/:username',
+ array('controller'=>'blog', 'action'=>'view', 'username'=>'george'));
+
+ # When matching url variable username variable
+ # /some/long/url/george /some/long/url/george george
+ # /some/other/stuff/fred /some/other/stuff/fred george
+
+This occurs because Routes sees the default as being optional, and the
+wildcard part attempts to gobble as much of the URL as possible before a
+required section of the route path is found. By having a trailing dynamic part
+with a default, that section gets dropped.
+
+Notice how removing the dynamic part default results in the variables we expect:
+
+ $m->connect('*url/:username',
+ array('controller'=>'blog', 'action'=>'view'));
+
+ # When matching url variable username variable
+ # /some/long/url/george /some/long/url george
+ # /some/other/stuff/fred /some/other/stuff fred
+
+Let's try one more time, but put in a static part between the dynamic part
+with a default and the wildcard:
+
+ $m->connect('*url/user/:username',
+ array('controller'=>'blog', 'action'=>'view', 'username'=>'george'));
+
+ # When matching url variable username variable
+ # /some/long/url/user/george /some/long/url george
+ # /some/other/stuff/user/fred /some/other/stuff fred
+
+
+3.5 Unicode
+
+Not currently supported in the PHP version.
+
+
+4 Using Routes
+
+Once you have setup the Routes to map URLs to your controllers and actions,
+you will likely want to generate URLs from within your web application.
+
+
+Horde_Routes_Utils includes two functions for use in your web application that
+are commonly desired.
+
+ * redirectTo()
+ * urlFor()
+
+Both of these functions take a similar set of arguments. The most important
+being an associative array of keyword arguments that describes the controller,
+action, and additional variables you'd like present for the URL that's created.
+
+To save you from repeating things, Routes has two mechanisms to reduce the
+amount of information you need to supply the urlFor() or redirectTo() function.
+
+
+4.1 Named Routes
+
+We saw earlier how the route name ties a set of defaults to a name. We can use
+this name with our Route functions and its as if we used that set of keyword
+args:
+
+ $m->connect('category_home', 'category/:section',
+ array('controller'=>'blog', 'action'=>'view', 'section'=>'home'));
+
+ $utils = $m->utils;
+ $utils->urlFor('category_home');
+
+ // is equivalent to
+ $utils->urlFor(array('controller'=>'blog', 'action'=>'view', 'section'=>'home'));
+
+You can also specify keyword arguments and it will override defaults
+associated with the route name:
+
+ $utils->urlFor('category_home', array('action'=>'index'));
+
+ // is equivalent to
+ $utils->urlFor(array('controller'=>'blog', 'action'=>'index', 'section'=>'home'));
+
+As you can see, the amount of typing you save yourself by using the route name
+feature is quite handy.
+
+Using the recently introduced static named routes feature allows you to
+quickly use common URLs and easily add query arguments:
+
+ $m->connect('google_search', 'http://www.google.com/search',
+ array('_static' => true));
+
+ $utils = $m->utils;
+ $utils->urlFor('google_search', array('q'=>'routes'));
+ // will result in
+ // http://www.google.com/search?q=routes
+
+
+4.1.1 Non-Existent Route Names
+
+If you supply a route name that does not exist, urlFor() will assume that you
+intend to use the name as the actual URL. It will also prepend it with the
+proper SCRIPT_NAME if applicable:
+
+ $utils->urlFor('/css/source.css');
+ # if running underneath a 'mount' point of /myapp will become
+ # /myapp/css/source.css
+
+For portable web applications, it's highly encouraged that you use urlFor() for
+all your URLs, even those that are static resources and images. This will
+ensure that the URLs are properly handled in various deployment cases.
+
+
+4.2 Route Memory
+
+When your controller and action is matched up from the URL, the variables it
+set to get there are preserved. This lets you update small bits of the
+keywords that got you there without specifying the entire thing:
+
+ $m->connect('archives/:year/:month/:day',
+ array('controller'=>'archives', 'action'=>'view', 'year'=>2004,
+ 'requirements'=>array('year'=>'\d{2,4}', 'month'=>'\d{1,2}')));
+
+ # URL used: /archives/2005/10/4
+
+ # Route dict: {'controller': 'archives', 'action': 'view', 'year': '2005',
+ # 'month': '10', 'day': '4'}
+
+ $utils->urlFor(array('day'=>6)) # => /archives/2005/10/6
+ $utils->urlFor(array('month'=>4)) # => /archives/2005/4/4
+ $utils->urlFor() # => /archives/2005/10/4
+ $utils->urlFor(array('controller'=>'/archives')) # => /archives
+
+The route memory is always used for values with the following conditions:
+
+ * If the controller name begins with a /, no values from the Route dict are used
+ * If the controller name changes and no action is specified, action will be set to 'index'
+ * If you use named routes, no values from the Route dict are used
+
+
+4.3 Overriding Route Memory
+
+Sometimes one doesn't want to have Route Memory present, as well as removing
+the Implicit Defaults. Routes can disable route memory and implicit defaults
+either globally, or on a per-route basis. Setting explicit routes:
+
+ $m = new Horde_Routes_Mapper(array('explicit'=>true));
+
+When toggling explicit behavior for individual routes, only the implicit route
+defaults will be de-activated. urlFor() behavior can only be set globally with
+the mapper explicit keyword. Setting explicit behavior for a route:
+
+ $m = new Horde_Routes_Mapper();
+
+ # Note no 'id' value will be assumed for a default
+ $m->connect('archives/:year',
+ array('controller'=>'archives', 'action'=>'view', '_explicit'=>true));
+
+ # This will now require an action and id present
+ $m->connect(':controller/:action/:id',
+ array('_explicit'=>true));
+
+
+5 Sub-domain Support
+
+Routes comes with sub-domain support to make it easy to handle sub-domains in
+an integrated fashion. When sub-domain support is turned on, Routes will
+always have a subDomain argument present with the sub-domain if present, or
+None.
+
+To avoid matching common aliases to your main domain like www, the sub-domain
+support can be set to ignore some sub-domains.
+
+Example:
+
+ $m = new Horde_Routes_Mapper();
+
+ // Turn on sub-domain support
+ $m->subDomains = true;
+
+ // Ignore the www sub-domain
+ $m->subDomainsIgnore = array('www');
+
+
+5.1 Generating URLs with sub-domains
+
+When sub-domain support is on, the urlFor() function will accept a subDomain
+keyword argument. Routes will then ensure that the generated URL has the
+sub-domain indicated. This feature works with Route memory to ensure that the
+sub-domain is only added when necessary.
+
+Some examples:
+
+ // Assuming that the current URL from the request is http://george.example.com/users/edit
+ // Also assuming that you're using the map options above with the default routing of
+ // ':controller/:action/:id'
+
+ $utils->urlFor(array('action'=>'update', 'subDomain'=>'fred'));
+ # -> http://fred.example.com/users/update
+
+ $utils->urlFor(array('controller'=>'/content', 'action'=>'view', 'subDomain'=>'www'));
+ # will become -> http://example.com/content/view
+
+ $utils->urlFor(array('action'=>'new', 'subDomain'=>null));
+ # -> http://example.com/users/new
+
+
+6 RESTful Services
+
+To make it easier to setup RESTful web services with Routes, there's a
+shortcut Mapper method that will setup a batch of routes for you along with
+conditions that will restrict them to specific HTTP methods. This is directly
+styled on the Rails version of $map->resource(), which was based heavily on the
+Atom Publishing Protocol.
+
+The Horde_Routes_Mapper->resource() command creates a set of Routes for common
+operations on a collection of resources, individually referred to as
+'members'. Consider the common case where you have a system that deals with
+users. In that case operations dealing with the entire group of users (or
+perhaps a subset) would be considered collection methods. Operations (or
+actions) that act on an individual member of that collection are considered
+member methods. These terms are important to remember as the options to
+$map->resource() rely on a clear understanding of collection actions vs.
+member actions.
+
+The default mapping that $map->resource() sets up looks like this:
+
+ $map->resource('message', 'messages')
+
+ // Will setup all the routes as if you had typed the following map commands:
+ $map->connect('messages',
+ array('controller'=>'messages', 'action'=>'create',
+ 'conditions'=>array('method'=>array('POST'))));
+ $map->connect('messages', 'messages',
+ array('controller'=>'messages', 'action'=>'index',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('formatted_messages', 'messages.:(format)',
+ array('controller'=>'messages', action=>'index',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('new_message', 'messages/new',
+ array('controller'=>'messages', 'action'=>'new',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('formatted_new_message', 'messages/new.:(format)',
+ array('controller'=>'messages', 'action'=>'new',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('messages/:id',
+ array('controller'=>'messages', 'action'=>'update',
+ 'conditions'=>array('method'=>array('PUT'))));
+ $map->connect('messages/:id',
+ array('controller'=>'messages', 'action'=>'delete',
+ 'conditions'=>array('method'=>array('DELETE'))));
+ $map->connect('edit_message', 'messages/:(id);edit',
+ array('controller'=>'messages', 'action'=>'edit',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('formatted_edit_message', 'messages/:(id).:(format);edit',
+ array('controller'=>'messages', 'action'=>'edit',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('message', 'messages/:id',
+ array('controller'=>'messages', 'action'=>'show',
+ 'conditions'=>array('method'=>array('GET'))));
+ $map->connect('formatted_message', 'messages/:(id).:(format)',
+ array('controller'=>'messages', 'action'=>'show',
+ 'conditions'=>array('method'=>array('GET'))));
+
+The most important aspects of this is the following mapping that is established:
+
+ GET /messages -> messages.index() -> $utils->urlFor('messages')
+ POST /messages -> messages.create() -> $utils->urlFor('messages')
+ GET /messages/new -> messages.new() -> $utils->urlFor('new_message')
+ PUT /messages/1 -> messages.update(id) -> $utils->urlFor('message', array('id'=>1))
+ DELETE /messages/1 -> messages.delete(id) -> $utils->urlFor('message', array('id'=>1))
+ GET /messages/1 -> messages.show(id) -> $utils->urlFor('message', array('id'=>1))
+ GET /messages/1;edit -> messages.edit(id) -> $utils->urlFor('edit_message', array('id'=>1))
+
+*Note* Several of these methods map to functions intended to display forms. The new
+ message method should be used to return a form allowing someone to create a
+ new message, while it should POST to /messages. The edit message function
+ should work similarly returning a form to edit a message, which then performs a
+ PUT to the /messages/1 resource.
+
+Additional methods that respond to either a new member, or different ways of
+viewing collections can be added via keyword arguments to $map->resource() as
+shown in the complete list with examples of the $map->resource() options.
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * Exception class for the Horde_Routes package. All exceptions thrown
+ * from the package will be of this type.
+ *
+ * @package Horde_Routes
+ */
+class Horde_Routes_Exception extends Exception
+{}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * The mapper class handles URL generation and recognition for web applications
+ *
+ * The mapper class is built by handling associated arrays of information and passing
+ * associated arrays back to the application for it to handle and dispatch the
+ * appropriate scripts.
+ *
+ * @package Horde_Routes
+ */
+class Horde_Routes_Mapper
+{
+ /**
+ * Filtered request environment with keys like SCRIPT_NAME
+ * @var array
+ */
+ public $environ = array();
+
+ /**
+ * Callback function used to get array of controller names
+ * @var callback
+ */
+ public $controllerScan;
+
+ /**
+ * Path to controller directory passed to controllerScan function
+ * @var string
+ */
+ public $directory;
+
+ /**
+ * Call controllerScan callback before every route match?
+ * @var boolean
+ */
+ public $alwaysScan;
+
+ /**
+ * Disable route memory and implicit defaults?
+ * @var boolean
+ */
+ public $explicit;
+
+ /**
+ * Collect debug information during route match?
+ * @var boolean
+ */
+ public $debug = false;
+
+ /**
+ * Use sub-domain support?
+ * @var boolean
+ */
+ public $subDomains = false;
+
+ /**
+ * Array of sub-domains to ignore if using sub-domain support
+ * @var array
+ */
+ public $subDomainsIgnore = array();
+
+ /**
+ * Append trailing slash ('/') to generated routes?
+ * @var boolean
+ */
+ public $appendSlash = false;
+
+ /**
+ * Prefix to strip during matching and to append during generation
+ * @var null|string
+ */
+ public $prefix = null;
+
+ /**
+ * Array of connected routes
+ * @var array
+ */
+ public $matchList = array();
+
+ /**
+ * Array of connected named routes, indexed by name
+ * @var array
+ */
+ public $routeNames = array();
+
+ /**
+ * Cache of URLs used in generate()
+ * @var array
+ */
+ public $urlCache = array();
+
+ /**
+ * Encoding of routes URLs (not yet supported)
+ * @var string
+ */
+ public $encoding = 'utf-8';
+
+ /**
+ * What to do on decoding errors? 'ignore' or 'replace'
+ * @var string
+ */
+ public $decodeErrors = 'ignore';
+
+ /**
+ * Partial regexp used to match domain part of the end of URLs to match
+ * @var string
+ */
+ public $domainMatch = '[^\.\/]+?\.[^\.\/]+';
+
+ /**
+ * Array of all connected routes, indexed by the serialized array of all
+ * keys that each route could utilize.
+ * @var array
+ */
+ public $maxKeys = array();
+
+ /**
+ * Array of all connected routes, indexed by the serialized array of the
+ * minimum keys that each route needs.
+ * @var array
+ */
+ public $minKeys = array();
+
+ /**
+ * Utility functions like urlFor() and redirectTo() for this Mapper
+ * @var Horde_Routes_Utils
+ */
+ public $utils;
+
+ /**
+ * Have regular expressions been created for all connected routes?
+ * @var boolean
+ */
+ protected $_createdRegs = false;
+
+ /**
+ * Have generation hashes been created for all connected routes?
+ * @var boolean
+ */
+ protected $_createdGens = false;
+
+ /**
+ * Generation hashes created for all connected routes
+ * @var array
+ */
+ protected $_gendict;
+
+ /**
+ * Temporary variable used to pass array of keys into _keysort() callback
+ * @var array
+ */
+ protected $_keysortTmp;
+
+ /**
+ * Regular expression generated to match after the prefix
+ * @var string
+ */
+ protected $_regPrefix = null;
+
+
+ /**
+ * Constructor.
+ *
+ * Keyword arguments ($kargs):
+ * ``controllerScan`` (callback)
+ * Function to return an array of valid controllers
+ *
+ * ``redirect`` (callback)
+ * Function to perform a redirect for Horde_Routes_Utils->redirectTo()
+ *
+ * ``directory`` (string)
+ * Path to the directory that will be passed to the
+ * controllerScan callback
+ *
+ * ``alwaysScan`` (boolean)
+ * Should the controllerScan callback be called
+ * before every URL match?
+ *
+ * ``explicit`` (boolean)
+ * Should routes be connected with the implicit defaults of
+ * array('controller'=>'content', 'action'=>'index', 'id'=>null)?
+ * When set to True, these will not be added to route connections.
+ */
+ public function __construct($kargs = array())
+ {
+ $callback = array('Horde_Routes_Utils', 'controllerScan');
+
+ $defaultKargs = array('controllerScan' => $callback,
+ 'directory' => null,
+ 'alwaysScan' => false,
+ 'explicit' => false);
+ $kargs = array_merge($defaultKargs, $kargs);
+
+ // Most default assignments that were in the construct in the Python
+ // version have been moved to outside the constructor unless they were variable
+
+ $this->directory = $kargs['directory'];
+ $this->alwaysScan = $kargs['alwaysScan'];
+ $this->controllerScan = $kargs['controllerScan'];
+ $this->explicit = $kargs['explicit'];
+
+ $this->utils = new Horde_Routes_Utils($this);
+ }
+
+ /**
+ * Create and connect a new Route to the Mapper.
+ *
+ * Usage:
+ * $m = new Horde_Routes_Mapper();
+ * $m->connect(':controller/:action/:id');
+ * $m->connect('date/:year/:month/:day', array('controller' => "blog", 'action' => 'view');
+ * $m->connect('archives/:page', array('controller' => 'blog', 'action' => 'by_page',
+ * ' requirements' => array('page' => '\d{1,2}')));
+ * $m->connect('category_list',
+ * 'archives/category/:section', array('controller' => 'blog', 'action' => 'category',
+ * 'section' => 'home', 'type' => 'list'));
+ * $m->connect('home',
+ * '',
+ * array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ *
+ * @param mixed $first First argument in vargs, see usage above.
+ * @param mixed $second Second argument in varags
+ * @param mixed $third Third argument in varargs
+ * @return void
+ */
+ public function connect($first, $second = null, $third = null)
+ {
+ if ($third !== null) {
+ // 3 args given
+ // connect('route_name', ':/controller/:action/:id', array('kargs'=>'here'))
+ $routeName = $first;
+ $routePath = $second;
+ $kargs = $third;
+ } else if ($second !== null) {
+ // 2 args given
+ if (is_array($second)) {
+ // connect(':/controller/:action/:id', array('kargs'=>'here'))
+ $routeName = null;
+ $routePath = $first;
+ $kargs = $second;
+ } else {
+ // connect('route_name', ':/controller/:action/:id')
+ $routeName = $first;
+ $routePath = $second;
+ $kargs = array();
+ }
+ } else {
+ // 1 arg given
+ // connect('/:controller/:action/:id')
+ $routeName = null;
+ $routePath = $first;
+ $kargs = array();
+ }
+
+ if (!in_array('_explicit', $kargs)) {
+ $kargs['_explicit'] = $this->explicit;
+ }
+
+ $route = new Horde_Routes_Route($routePath, $kargs);
+
+ if ($this->encoding != 'utf-8' || $this->decodeErrors != 'ignore') {
+ $route->encoding = $this->encoding;
+ $route->decodeErrors = $this->decodeErrors;
+ }
+
+ $this->matchList[] = $route;
+
+ if (isset($routeName)) {
+ $this->routeNames[$routeName] = $route;
+ }
+
+ if ($route->static) {
+ return;
+ }
+
+ $exists = false;
+ foreach ($this->maxKeys as $key => $value) {
+ if (unserialize($key) == $route->maxKeys) {
+ $this->maxKeys[$key][] = $route;
+ $exists = true;
+ break;
+ }
+ }
+
+ if (!$exists) {
+ $this->maxKeys[serialize($route->maxKeys)] = array($route);
+ }
+
+ $this->_createdGens = false;
+ }
+
+ /**
+ * Create the generation hashes (arrays) for route lookups
+ *
+ * @return void
+ */
+ protected function _createGens()
+ {
+ // Use keys temporarily to assemble the list to avoid excessive
+ // list iteration testing with foreach. We include the '*' in the
+ // case that a generate contains a controller/action that has no
+ // hardcodes.
+ $actionList = $controllerList = array('*' => true);
+
+ // Assemble all the hardcoded/defaulted actions/controllers used
+ foreach ($this->matchList as $route) {
+ if ($route->static) {
+ continue;
+ }
+ if (isset($route->defaults['controller'])) {
+ $controllerList[$route->defaults['controller']] = true;
+ }
+ if (isset($route->defaults['action'])) {
+ $actionList[$route->defaults['action']] = true;
+ }
+ }
+
+ $actionList = array_keys($actionList);
+ $controllerList = array_keys($controllerList);
+
+ // Go through our list again, assemble the controllers/actions we'll
+ // add each route to. If its hardcoded, we only add it to that dict key.
+ // Otherwise we add it to every hardcode since it can be changed.
+ $gendict = array(); // Our generated two-deep hash
+ foreach ($this->matchList as $route) {
+ if ($route->static) {
+ continue;
+ }
+ $clist = $controllerList;
+ $alist = $actionList;
+ if (in_array('controller', $route->hardCoded)) {
+ $clist = array($route->defaults['controller']);
+ }
+ if (in_array('action', $route->hardCoded)) {
+ $alist = array($route->defaults['action']);
+ }
+ foreach ($clist as $controller) {
+ foreach ($alist as $action) {
+ if (in_array($controller, array_keys($gendict))) {
+ $actiondict = &$gendict[$controller];
+ } else {
+ $gendict[$controller] = array();
+ $actiondict = &$gendict[$controller];
+ }
+ if (in_array($action, array_keys($actiondict))) {
+ $tmp = $actiondict[$action];
+ } else {
+ $tmp = array(array(), array());
+ }
+ $tmp[0][] = $route;
+ $actiondict[$action] = $tmp;
+ }
+ }
+ }
+ if (!isset($gendict['*'])) {
+ $gendict['*'] = array();
+ }
+ $this->_gendict = $gendict;
+ $this->_createdGens = true;
+ }
+
+ /**
+ * Creates the regexes for all connected routes
+ *
+ * @param array $clist controller list, controller_scan will be used otherwise
+ * @return void
+ */
+ public function createRegs($clist = null)
+ {
+ if ($clist === null) {
+ if ($this->directory === null) {
+ $clist = call_user_func($this->controllerScan);
+ } else {
+ $clist = call_user_func($this->controllerScan, $this->directory);
+ }
+ }
+
+ foreach ($this->maxKeys as $key => $val) {
+ foreach ($val as $route) {
+ $route->makeRegexp($clist);
+ }
+ }
+
+ // Create our regexp to strip the prefix
+ if (!empty($this->prefix)) {
+ $this->_regPrefix = $this->prefix . '(.*)';
+ }
+ $this->_createdRegs = true;
+ }
+
+ /**
+ * Internal Route matcher
+ *
+ * Matches a URL against a route, and returns a tuple (array) of the
+ * match dict (array) and the route object if a match is successful,
+ * otherwise it returns null.
+ *
+ * @param string $url URL to match
+ * @return null|array Match data if matched, otherwise null
+ */
+ protected function _match($url)
+ {
+ if (!$this->_createdRegs && !empty($this->controllerScan)) {
+ $this->createRegs();
+ } else if (!$this->_createdRegs) {
+ $msg = 'You must generate the regular expressions before matching.';
+ throw new Horde_Routes_Exception($msg);
+ }
+
+ if ($this->alwaysScan) {
+ $this->createRegs();
+ }
+
+ $matchLog = array();
+ if (!empty($this->prefix)) {
+ if (preg_match('@' . $this->_regPrefix . '@', $url)) {
+ $url = preg_replace('@' . $this->_regPrefix . '@', '$1', $url);
+ if (empty($url)) {
+ $url = '/';
+ }
+ }
+ else {
+ return array(null, null, $matchLog);
+ }
+ }
+
+ foreach ($this->matchList as $route) {
+ if ($route->static) {
+ if ($this->debug) {
+ $matchLog[] = array('route' => $route, 'static' => true);
+ }
+ continue;
+ }
+
+ $match = $route->match($url, array('environ' => $this->environ,
+ 'subDomains' => $this->subDomains,
+ 'subDomainsIgnore' => $this->subDomainsIgnore,
+ 'domainMatch' => $this->domainMatch));
+ if ($this->debug) {
+ $matchLog[] = array('route' => $route, 'regexp' => (bool)$match);
+ }
+ if ($match) {
+ return array($match, $route, $matchLog);
+ }
+ }
+
+ return array(null, null, $matchLog);
+ }
+
+ /**
+ * Match a URL against one of the routes contained.
+ * It will return null if no valid match is found.
+ *
+ * Usage:
+ * $resultdict = $m->match('/joe/sixpack');
+ *
+ * @param string $url URL to match
+ * @param array|null Array if matched, otherwise null
+ */
+ public function match($url)
+ {
+ if (!strlen($url)) {
+ $msg = 'No URL provided, the minimum URL necessary to match is "/"';
+ throw new Horde_Routes_Exception($msg);
+ }
+
+ $result = $this->_match($url);
+
+ if ($this->debug) {
+ return array($result[0], $result[1], $result[2]);
+ }
+
+ return ($result[0]) ? $result[0] : null;
+ }
+
+ /**
+ * Match a URL against one of the routes contained.
+ * It will return null if no valid match is found, otherwise
+ * a result dict (array) and a route object is returned.
+ *
+ * Usage:
+ * list($resultdict, $resultobj) = $m->match('/joe/sixpack');
+ *
+ * @param string $url URL to match
+ * @param array|null Array if matched, otherwise null
+ */
+ public function routematch($url)
+ {
+ $result = $this->_match($url);
+
+ if ($this->debug) {
+ return array($result[0], $result[1], $result[2]);
+ }
+
+ return ($result[0]) ? array($result[0], $result[1]) : null;
+ }
+
+ /**
+ * Generates the URL from a given set of keywords
+ * Returns the URL text, or null if no URL could be generated.
+ *
+ * Usage:
+ * $m->generate(array('controller' => 'content', 'action' => 'view', 'id' => 10));
+ *
+ * @param array $kargs Keyword arguments (key/value pairs)
+ * @return null|string URL text or null
+ */
+ public function generate($kargs = array())
+ {
+ // Generate ourself if we haven't already
+ if (!$this->_createdGens) {
+ $this->_createGens();
+ }
+
+ if ($this->appendSlash) {
+ $kargs['_appendSlash'] = true;
+ }
+
+ if (!$this->explicit) {
+ if (!in_array('controller', array_keys($kargs))) {
+ $kargs['controller'] = 'content';
+ }
+ if (!in_array('action', array_keys($kargs))) {
+ $kargs['action'] = 'index';
+ }
+ }
+
+ $environ = $this->environ;
+ $controller = isset($kargs['controller']) ? $kargs['controller'] : null;
+ $action = isset($kargs['action']) ? $kargs['action'] : null;
+
+ // If the URL didn't depend on the SCRIPT_NAME, we'll cache it
+ // keyed by just the $kargs; otherwise we need to cache it with
+ // both SCRIPT_NAME and $kargs:
+ $cacheKey = $kargs;
+ if (!empty($environ['SCRIPT_NAME'])) {
+ $cacheKeyScriptName = sprintf('%s:%s', $environ['SCRIPT_NAME'], $cacheKey);
+ } else {
+ $cacheKeyScriptName = $cacheKey;
+ }
+
+ // Check the URL cache to see if it exists, use it if it does.
+ foreach (array($cacheKey, $cacheKeyScriptName) as $key) {
+ if (in_array($key, array_keys($this->urlCache))) {
+ return $this->urlCache[$key];
+ }
+ }
+
+ $actionList = isset($this->_gendict[$controller]) ? $this->_gendict[$controller] : $this->_gendict['*'];
+
+ list($keyList, $sortCache) =
+ (isset($actionList[$action])) ? $actionList[$action] : ((isset($actionList['*'])) ? $actionList['*'] : array(null, null));
+
+ if ($keyList === null) {
+ return null;
+ }
+
+ $keys = array_keys($kargs);
+
+ // necessary to pass $keys to _keysort() callback used by PHP's usort()
+ $this->_keysortTmp = $keys;
+
+ $newList = array();
+ foreach ($keyList as $route) {
+ $tmp = Horde_Routes_Utils::arraySubtract($route->minKeys, $keys);
+ if (count($tmp) == 0) {
+ $newList[] = $route;
+ }
+ }
+ $keyList = $newList;
+
+ // inline python function keysort() moved below as _keycmp()
+
+ $this->_keysort($keyList);
+
+ foreach ($keyList as $route) {
+ $fail = false;
+ foreach ($route->hardCoded as $key) {
+ $kval = isset($kargs[$key]) ? $kargs[$key] : null;
+ if ($kval == null) {
+ continue;
+ }
+
+ if ($kval != $route->defaults[$key]) {
+ $fail = true;
+ break;
+ }
+ }
+ if ($fail) {
+ continue;
+ }
+
+ $path = $route->generate($kargs);
+
+ if ($path) {
+ if ($this->prefix) {
+ $path = $this->prefix . $path;
+ }
+ if (!empty($environ['SCRIPT_NAME']) && !$route->absolute) {
+ $path = $environ['SCRIPT_NAME'] . $path;
+ $key = $cacheKeyScriptName;
+ } else {
+ $key = $cacheKey;
+ }
+ if ($this->urlCache != null) {
+ $this->urlCache[$key] = $path;
+ }
+ return $path;
+ } else {
+ continue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generate routes for a controller resource
+ *
+ * The $memberName name should be the appropriate singular version of the
+ * resource given your locale and used with members of the collection.
+ *
+ * The $collectionName name will be used to refer to the resource
+ * collection methods and should be a plural version of the $memberName
+ * argument. By default, the $memberName name will also be assumed to map
+ * to a controller you create.
+ *
+ * The concept of a web resource maps somewhat directly to 'CRUD'
+ * operations. The overlying things to keep in mind is that mapping a
+ * resource is about handling creating, viewing, and editing that
+ * resource.
+ *
+ * All keyword arguments ($kargs) are optional.
+ *
+ * ``controller``
+ * If specified in the keyword args, the controller will be the actual
+ * controller used, but the rest of the naming conventions used for
+ * the route names and URL paths are unchanged.
+ *
+ * ``collection``
+ * Additional action mappings used to manipulate/view the entire set of
+ * resources provided by the controller.
+ *
+ * Example::
+ *
+ * $map->resource('message', 'messages',
+ * array('collection' => array('rss' => 'GET)));
+ * # GET /message;rss (maps to the rss action)
+ * # also adds named route "rss_message"
+ *
+ * ``member``
+ * Additional action mappings used to access an individual 'member'
+ * of this controllers resources.
+ *
+ * Example::
+ *
+ * $map->resource('message', 'messages',
+ * array('member' => array('mark' => 'POST')));
+ * # POST /message/1;mark (maps to the mark action)
+ * # also adds named route "mark_message"
+ *
+ * ``new``
+ * Action mappings that involve dealing with a new member in the
+ * controller resources.
+ *
+ * Example::
+ *
+ * $map->resource('message', 'messages',
+ * array('new' => array('preview' => 'POST')));
+ * # POST /message/new;preview (maps to the preview action)
+ * # also adds a url named "preview_new_message"
+ *
+ * ``pathPrefix``
+ * Prepends the URL path for the Route with the pathPrefix given.
+ * This is most useful for cases where you want to mix resources
+ * or relations between resources.
+ *
+ * ``namePrefix``
+ * Perpends the route names that are generated with the namePrefix
+ * given. Combined with the pathPrefix option, it's easy to
+ * generate route names and paths that represent resources that are
+ * in relations.
+ *
+ * Example::
+ *
+ * map.resource('message', 'messages',
+ * array('controller' => 'categories',
+ * 'pathPrefix' => '/category/:category_id',
+ * 'namePrefix' => 'category_')));
+ * # GET /category/7/message/1
+ * # has named route "category_message"
+ *
+ * ``parentResource``
+ * An assoc. array containing information about the parent resource,
+ * for creating a nested resource. It should contain the ``$memberName``
+ * and ``collectionName`` of the parent resource. This assoc. array will
+ * be available via the associated ``Route`` object which can be
+ * accessed during a request via ``request.environ['routes.route']``
+ *
+ * If ``parentResource`` is supplied and ``pathPrefix`` isn't,
+ * ``pathPrefix`` will be generated from ``parentResource`` as
+ * "<parent collection name>/:<parent member name>_id".
+ *
+ * If ``parentResource`` is supplied and ``namePrefix`` isn't,
+ * ``namePrefix`` will be generated from ``parentResource`` as
+ * "<parent member name>_".
+ *
+ * Example::
+ *
+ * $m = new Horde_Routes_Mapper();
+ * $utils = $m->utils;
+ *
+ * $m->resource('location', 'locations',
+ * array('parentResource' =>
+ * array('memberName' => 'region',
+ * 'collectionName' => 'regions'))));
+ * # pathPrefix is "regions/:region_id"
+ * # namePrefix is "region_"
+ *
+ * $utils->urlFor('region_locations', array('region_id'=>13));
+ * # '/regions/13/locations'
+ *
+ * $utils->urlFor('region_new_location', array('region_id'=>13));
+ * # '/regions/13/locations/new'
+ *
+ * $utils->urlFor('region_location',
+ * array('region_id'=>13, 'id'=>60));
+ * # '/regions/13/locations/60'
+ *
+ * $utils->urlFor('region_edit_location',
+ * array('region_id'=>13, 'id'=>60));
+ * # '/regions/13/locations/60/edit'
+ *
+ * Overriding generated ``pathPrefix``::
+ *
+ * $m = new Horde_Routes_Mapper();
+ * $utils = new Horde_Routes_Utils();
+ *
+ * $m->resource('location', 'locations',
+ * array('parentResource' =>
+ * array('memberName' => 'region',
+ * 'collectionName' => 'regions'),
+ * 'pathPrefix' => 'areas/:area_id')));
+ * # name prefix is "region_"
+ *
+ * $utils->urlFor('region_locations', array('area_id'=>51));
+ * # '/areas/51/locations'
+ *
+ * Overriding generated ``namePrefix``::
+ *
+ * $m = new Horde_Routes_Mapper
+ * $m->resource('location', 'locations',
+ * array('parentResource' =>
+ * array('memberName' => 'region',
+ * 'collectionName' => 'regions'),
+ * 'namePrefix' => '')));
+ * # pathPrefix is "regions/:region_id"
+ *
+ * $utils->urlFor('locations', array('region_id'=>51));
+ * # '/regions/51/locations'
+ *
+ * Note: Since Horde Routes 0.2.0 and Python Routes 1.8, this method is
+ * not compatible with earlier versions inasmuch as the semicolon is no
+ * longer used to delimit custom actions. This was a change in Rails
+ * itself (http://dev.rubyonrails.org/changeset/6485) and adopting it
+ * here allows us to keep parity with Rails and ActiveResource.
+ *
+ * @param string $memberName Singular version of the resource name
+ * @param string $collectionName Collection name (plural of $memberName)
+ * @param array $kargs Keyword arguments (see above)
+ * @return void
+ */
+ public function resource($memberName, $collectionName, $kargs = array())
+ {
+ $defaultKargs = array('collection' => array(),
+ 'member' => array(),
+ 'new' => array(),
+ 'pathPrefix' => null,
+ 'namePrefix' => null,
+ 'parentResource' => null);
+ $kargs = array_merge($defaultKargs, $kargs);
+
+ // Generate ``pathPrefix`` if ``pathPrefix`` wasn't specified and
+ // ``parentResource`` was. Likewise for ``namePrefix``. Make sure
+ // that ``pathPrefix`` and ``namePrefix`` *always* take precedence if
+ // they are specified--in particular, we need to be careful when they
+ // are explicitly set to "".
+ if ($kargs['parentResource'] !== null) {
+ if ($kargs['pathPrefix'] === null) {
+ $kargs['pathPrefix'] = $kargs['parentResource']['collectionName'] . '/:'
+ . $kargs['parentResource']['memberName'] . '_id';
+ }
+ if ($kargs['namePrefix'] === null) {
+ $kargs['namePrefix'] = $kargs['parentResource']['memberName'] . '_';
+ }
+ } else {
+ if ($kargs['pathPrefix'] === null) {
+ $kargs['pathPrefix'] = '';
+ }
+ if ($kargs['namePrefix'] === null) {
+ $kargs['namePrefix'] = '';
+ }
+ }
+
+ // Ensure the edit and new actions are in and GET
+ $kargs['member']['edit'] = 'GET';
+ $kargs['new']['new'] = 'GET';
+
+ // inline python method swap() moved below as _swap()
+
+ $collectionMethods = $this->_swap($kargs['collection'], array());
+ $memberMethods = $this->_swap($kargs['member'], array());
+ $newMethods = $this->_swap($kargs['new'], array());
+
+ // Insert create, update, and destroy methods
+ if (!isset($collectionMethods['POST'])) {
+ $collectionMethods['POST'] = array();
+ }
+ array_unshift($collectionMethods['POST'], 'create');
+
+ if (!isset($memberMethods['PUT'])) {
+ $memberMethods['PUT'] = array();
+ }
+ array_unshift($memberMethods['PUT'], 'update');
+
+ if (!isset($memberMethods['DELETE'])) {
+ $memberMethods['DELETE'] = array();
+ }
+ array_unshift($memberMethods['DELETE'], 'delete');
+
+ // If there's a path prefix option, use it with the controller
+ $controller = $this->_stripSlashes($collectionName);
+ $kargs['pathPrefix'] = $this->_stripSlashes($kargs['pathPrefix']);
+ if ($kargs['pathPrefix']) {
+ $path = $kargs['pathPrefix'] . '/' . $controller;
+ } else {
+ $path = $controller;
+ }
+ $collectionPath = $path;
+ $newPath = $path . '/new';
+ $memberPath = $path . '/:(id)';
+
+ $options = array(
+ 'controller' => (isset($kargs['controller']) ? $kargs['controller'] : $controller),
+ '_memberName' => $memberName,
+ '_collectionName' => $collectionName,
+ '_parentResource' => $kargs['parentResource']
+ );
+
+ // inline python method requirements_for() moved below as _requirementsFor()
+
+ // Add the routes for handling collection methods
+ foreach ($collectionMethods as $method => $lst) {
+ $primary = ($method != 'GET' && isset($lst[0])) ? array_shift($lst) : null;
+ $routeOptions = $this->_requirementsFor($method, $options);
+
+ foreach ($lst as $action) {
+ $routeOptions['action'] = $action;
+ $routeName = sprintf('%s%s_%s', $kargs['namePrefix'], $action, $collectionName);
+
+ $this->connect($routeName,
+ sprintf("%s/%s", $collectionPath, $action),
+ $routeOptions);
+ $this->connect('formatted_' . $routeName,
+ sprintf("%s/%s.:(format)", $collectionPath, $action),
+ $routeOptions);
+ }
+ if ($primary) {
+ $routeOptions['action'] = $primary;
+ $this->connect($collectionPath, $routeOptions);
+ $this->connect($collectionPath . '.:(format)', $routeOptions);
+ }
+ }
+
+ // Specifically add in the built-in 'index' collection method and its
+ // formatted version
+ $connectkargs = array('action' => 'index',
+ 'conditions' => array('method' => array('GET')));
+ $this->connect($kargs['namePrefix'] . $collectionName,
+ $collectionPath,
+ array_merge($connectkargs, $options));
+ $this->connect('formatted_' . $kargs['namePrefix'] . $collectionName,
+ $collectionPath . '.:(format)',
+ array_merge($connectkargs, $options));
+
+ // Add the routes that deal with new resource methods
+ foreach ($newMethods as $method => $lst) {
+ $routeOptions = $this->_requirementsFor($method, $options);
+ foreach ($lst as $action) {
+ if ($action == 'new' && $newPath) {
+ $path = $newPath;
+ } else {
+ $path = sprintf('%s/%s', $newPath, $action);
+ }
+
+ $name = 'new_' . $memberName;
+ if ($action != 'new') {
+ $name = $action . '_' . $name;
+ }
+ $routeOptions['action'] = $action;
+ $this->connect($kargs['namePrefix'] . $name, $path, $routeOptions);
+
+ if ($action == 'new' && $newPath) {
+ $path = $newPath . '.:(format)';
+ } else {
+ $path = sprintf('%s/%s.:(format)', $newPath, $action);
+ }
+
+ $this->connect('formatted_' . $kargs['namePrefix'] . $name,
+ $path, $routeOptions);
+ }
+ }
+
+ $requirementsRegexp = '[\w\-_]+';
+
+ // Add the routes that deal with member methods of a resource
+ foreach ($memberMethods as $method => $lst) {
+ $routeOptions = $this->_requirementsFor($method, $options);
+ $routeOptions['requirements'] = array('id' => $requirementsRegexp);
+
+ if (!in_array($method, array('POST', 'GET', 'any'))) {
+ $primary = array_shift($lst);
+ } else {
+ $primary = null;
+ }
+
+ foreach ($lst as $action) {
+ $routeOptions['action'] = $action;
+ $this->connect(sprintf('%s%s_%s', $kargs['namePrefix'], $action, $memberName),
+ sprintf('%s/%s', $memberPath, $action),
+ $routeOptions);
+ $this->connect(sprintf('formatted_%s%s_%s', $kargs['namePrefix'], $action, $memberName),
+ sprintf('%s/%s.:(format)', $memberPath, $action),
+ $routeOptions);
+ }
+
+ if ($primary) {
+ $routeOptions['action'] = $primary;
+ $this->connect($memberPath, $routeOptions);
+ $this->connect($memberPath . '.:(format)', $routeOptions);
+ }
+ }
+
+ // Specifically add the member 'show' method
+ $routeOptions = $this->_requirementsFor('GET', $options);
+ $routeOptions['action'] = 'show';
+ $routeOptions['requirements'] = array('id' => $requirementsRegexp);
+ $this->connect($kargs['namePrefix'] . $memberName, $memberPath, $routeOptions);
+ $this->connect('formatted_' . $kargs['namePrefix'] . $memberName,
+ $memberPath . '.:(format)', $routeOptions);
+ }
+
+ /**
+ * Returns a new dict to be used for all route creation as
+ * the route options.
+ * @see resource()
+ *
+ * @param string $method Request method ('get', 'post', etc.) or 'any'
+ * @param array $options Assoc. array to populate with 'conditions' key
+ * @return $options populated
+ */
+ protected function _requirementsFor($meth, $options)
+ {
+ if ($meth != 'any') {
+ $options['conditions'] = array('method' => array(strtoupper($meth)));
+ }
+ return $options;
+ }
+
+ /**
+ * Swap the keys and values in the dict, and uppercase the values
+ * from the dict during the swap.
+ * @see resource()
+ *
+ * @param array $dct Input dict (assoc. array)
+ * @param array $newdct Output dict to populate
+ * @return array $newdct populated
+ */
+ protected function _swap($dct, $newdct)
+ {
+ foreach ($dct as $key => $val) {
+ $newkey = strtoupper($val);
+ if (!isset($newdct[$newkey])) {
+ $newdct[$newkey] = array();
+ }
+ $newdct[$newkey][] = $key;
+ }
+ return $newdct;
+ }
+
+ /**
+ * Sort an array of Horde_Routes_Routes to using _keycmp() for the comparision
+ * to order them ideally for matching.
+ *
+ * An unfortunate property of PHP's usort() is that if two members compare
+ * equal, their order in the sorted array is undefined (see PHP manual).
+ * This is unsuitable for us because the order that the routes were
+ * connected to the mapper is significant.
+ *
+ * Uses this method uses merge sort algorithm based on the
+ * comments in http://www.php.net/usort
+ *
+ * @param array $array Array Horde_Routes_Route objects to sort (by reference)
+ * @return void
+ */
+ protected function _keysort(&$array)
+ {
+ // arrays of size < 2 require no action.
+ if (count($array) < 2) { return; }
+
+ // split the array in half
+ $halfway = count($array) / 2;
+ $array1 = array_slice($array, 0, $halfway);
+ $array2 = array_slice($array, $halfway);
+
+ // recurse to sort the two halves
+ $this->_keysort($array1);
+ $this->_keysort($array2);
+
+ // if all of $array1 is <= all of $array2, just append them.
+ if ($this->_keycmp(end($array1), $array2[0]) < 1) {
+ $array = array_merge($array1, $array2);
+ return;
+ }
+
+ // merge the two sorted arrays into a single sorted array
+ $array = array();
+ $ptr1 = 0;
+ $ptr2 = 0;
+ while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
+ if ($this->_keycmp($array1[$ptr1], $array2[$ptr2]) < 1) {
+ $array[] = $array1[$ptr1++];
+ }
+ else {
+ $array[] = $array2[$ptr2++];
+ }
+ }
+
+ // merge the remainder
+ while ($ptr1 < count($array1)) { $array[] = $array1[$ptr1++]; }
+ while ($ptr2 < count($array2)) { $array[] = $array2[$ptr2++]; }
+ return;
+ }
+
+ /**
+ * Compare two Horde_Route_Routes objects by their keys against
+ * the instance variable $keysortTmp. Used by _keysort().
+ *
+ * @param array $a First dict (assoc. array)
+ * @param array $b Second dict
+ * @return integer
+ */
+ protected function _keycmp($a, $b)
+ {
+ $keys = $this->_keysortTmp;
+ $am = $a->minKeys;
+ $a = $a->maxKeys;
+ $b = $b->maxKeys;
+
+ $lendiffa = count(array_diff($keys, $a));
+ $lendiffb = count(array_diff($keys, $b));
+
+ // If they both match, don't switch them
+ if ($lendiffa == 0 && $lendiffb == 0) {
+ return 0;
+ }
+
+ // First, if $a matches exactly, use it
+ if ($lendiffa == 0) {
+ return -1;
+ }
+
+ // Or $b matches exactly, use it
+ if ($lendiffb == 0) {
+ return 1;
+ }
+
+ // Neither matches exactly, return the one with the most in common
+ if ($this->_cmp($lendiffa, $lendiffb) != 0) {
+ return $this->_cmp($lendiffa, $lendiffb);
+ }
+
+ // Neither matches exactly, but if they both have just as much in common
+ if (count($this->_arrayUnion($keys, $b)) == count($this->_arrayUnion($keys, $a))) {
+ return $this->_cmp(count($a), count($b));
+
+ // Otherwise, we return the one that has the most in common
+ } else {
+ return $this->_cmp(count($this->_arrayUnion($keys, $b)), count($this->_arrayUnion($keys, $a)));
+ }
+ }
+
+ /**
+ * Create a union of two arrays.
+ *
+ * @param array $a First array
+ * @param array $b Second array
+ * @return array Union of $a and $b
+ */
+ protected function _arrayUnion($a, $b)
+ {
+ return array_merge(array_diff($a, $b), array_diff($b, $a), array_intersect($a, $b));
+ }
+
+ /**
+ * Equivalent of Python's cmp() function.
+ *
+ * @param integer|float $a First item to compare
+ * @param integer|flot $b Second item to compare
+ * @param integer Result of comparison
+ */
+ protected function _cmp($a, $b)
+ {
+ if ($a < $b) {
+ return -1;
+ }
+ if ($a == $b) {
+ return 0;
+ }
+ return 1;
+ }
+
+ /**
+ * Trims slashes from the beginning or end of a part/URL.
+ *
+ * @param string $name Part or URL with slash at begin/end
+ * @return string Part or URL with begin/end slashes removed
+ */
+ protected function _stripSlashes($name)
+ {
+ if (substr($name, 0, 1) == '/') {
+ $name = substr($name, 1);
+ }
+ if (substr($name, -1, 1) == '/') {
+ $name = substr($name, 0, -1);
+ }
+ return $name;
+ }
+
+}
+
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * The Route object holds a route recognition and generation routine.
+ * See __construct() docs for usage.
+ *
+ * @package Horde_Routes
+ */
+class Horde_Routes_Route
+{
+ /**
+ * The path for this route, such as ':controller/:action/:id'
+ * @var string
+ */
+ public $routePath;
+
+ /**
+ * Encoding of this route (not yet supported)
+ * @var string
+ */
+ public $encoding = 'utf-8';
+
+ /**
+ * What to do on decoding errors? 'ignore' or 'replace'
+ * @var string
+ */
+ public $decodeErrors = 'replace';
+
+ /**
+ * Is this a static route?
+ * @var string
+ */
+ public $static;
+
+ /**
+ * Filter function to operate on arguments before generation
+ * @var callback
+ */
+ public $filter;
+
+ /**
+ * Is this an absolute path? (Mapper will not prepend SCRIPT_NAME)
+ * @var boolean
+ */
+ public $absolute;
+
+ /**
+ * Does this route use explicit mode (no implicit defaults)?
+ * @var boolean
+ */
+ public $explicit;
+
+ /**
+ * Default keyword arguments for this route
+ * @var array
+ */
+ public $defaults = array();
+
+ /**
+ * Array of keyword args for special conditions (method, subDomain, function)
+ * @var array
+ */
+ public $conditions;
+
+ /**
+ * Maximum keys that this route could utilize.
+ * @var array
+ */
+ public $maxKeys;
+
+ /**
+ * Minimum keys required to generate this route
+ * @var array
+ */
+ public $minKeys;
+
+ /**
+ * Default keywords that don't exist in the path; can't be changed by an incomng URL.
+ * @var array
+ */
+ public $hardCoded;
+
+ /**
+ * Requirements for this route
+ * @var array
+ */
+ public $reqs;
+
+ /**
+ * Regular expression for matching this route
+ * @var string
+ */
+ public $regexp;
+
+ /**
+ * Route path split by '/'
+ * @var array
+ */
+ protected $_routeList;
+
+ /**
+ * Reverse of $routeList
+ * @var array
+ */
+ protected $_routeBackwards;
+
+ /**
+ * Characters that split the parts of a URL
+ * @var array
+ */
+ protected $_splitChars;
+
+ /**
+ * Last path part used by buildNextReg()
+ * @var string
+ */
+ protected $_prior;
+
+ /**
+ * Requirements formatted as regexps suitable for preg_match()
+ * @var array
+ */
+ protected $_reqRegs;
+
+ /**
+ * Member name if this is a RESTful route
+ * @see resource()
+ * @var null|string
+ */
+ protected $_memberName;
+
+ /**
+ * Collection name if this is a RESTful route
+ * @see resource()
+ * @var null|string
+ */
+ protected $_collectionName;
+
+ /**
+ * Name of the parent resource, if this is a RESTful route & has a parent
+ * @see resource
+ * @var string
+ */
+ protected $_parentResource;
+
+
+ /**
+ * Initialize a route, with a given routepath for matching/generation
+ *
+ * The set of keyword args will be used as defaults.
+ *
+ * Usage:
+ * $route = new Horde_Routes_Route(':controller/:action/:id');
+ *
+ * $route = new Horde_Routes_Route('date/:year/:month/:day',
+ * array('controller'=>'blog', 'action'=>'view'));
+ *
+ * $route = new Horde_Routes_Route('archives/:page',
+ * array('controller'=>'blog', 'action'=>'by_page',
+ * 'requirements' => array('page'=>'\d{1,2}'));
+ *
+ * Note:
+ * Route is generally not called directly, a Mapper instance connect()
+ * method should be used to add routes.
+ */
+ public function __construct($routePath, $kargs = array())
+ {
+ $this->routePath = $routePath;
+
+ // Don't bother forming stuff we don't need if its a static route
+ $this->static = isset($kargs['_static']) ? $kargs['_static'] : false;
+
+ $this->filter = isset($kargs['_filter']) ? $kargs['_filter'] : null;
+ unset($kargs['_filter']);
+
+ $this->absolute = isset($kargs['_absolute']) ? $kargs['_absolute'] : false;
+ unset($kargs['absolute']);
+
+ // Pull out the member/collection name if present, this applies only to
+ // map.resource
+ $this->_memberName = isset($kargs['_memberName']) ? $kargs['_memberName'] : null;
+ unset($kargs['_memberName']);
+
+ $this->_collectionName = isset($kargs['_collectionName']) ? $kargs['_collectionName'] : null;
+ unset($kargs['_collectionName']);
+
+ $this->_parentResource = isset($kargs['_parentResource']) ? $kargs['_parentResource'] : null;
+ unset($kargs['_parentResource']);
+
+ // Pull out route conditions
+ $this->conditions = isset($kargs['conditions']) ? $kargs['conditions'] : null;
+ unset($kargs['conditions']);
+
+ // Determine if explicit behavior should be used
+ $this->explicit = isset($kargs['_explicit']) ? $kargs['_explicit'] : false;
+ unset($kargs['_explicit']);
+
+ // Reserved keys that don't count
+ $reservedKeys = array('requirements');
+
+ // Name has been changed from the Python version
+ // This is a list of characters natural splitters in a URL
+ $this->_splitChars = array('/', ',', ';', '.', '#');
+
+ // trip preceding '/' if present
+ if (substr($this->routePath, 0, 1) == '/') {
+ $routePath = substr($this->routePath, 1);
+ }
+
+ // Build our routelist, and the keys used in the route
+ $this->_routeList = $this->_pathKeys($routePath);
+ $routeKeys = array();
+ foreach ($this->_routeList as $key) {
+ if (is_array($key)) { $routeKeys[] = $key['name']; }
+ }
+
+ // Build a req list with all the regexp requirements for our args
+ $this->reqs = isset($kargs['requirements']) ? $kargs['requirements'] : array();
+ $this->_reqRegs = array();
+ foreach ($this->reqs as $key => $value) {
+ $this->_reqRegs[$key] = '@^' . str_replace('@', '\@', $value) . '$@';
+ }
+
+ // Update our defaults and set new default keys if needed. defaults
+ // needs to be saved
+ list($this->defaults, $defaultKeys) = $this->_defaults($routeKeys, $reservedKeys, $kargs);
+
+ // Save the maximum keys we could utilize
+ $this->maxKeys = array_keys(array_flip(array_merge($defaultKeys, $routeKeys)));
+ list($this->minKeys, $this->_routeBackwards) = $this->_minKeys($this->_routeList);
+
+ // Populate our hardcoded keys, these are ones that are set and don't
+ // exist in the route
+ $this->hardCoded = array();
+ foreach ($this->maxKeys as $key) {
+ if (!in_array($key, $routeKeys) && $this->defaults[$key] != null) {
+ $this->hardCoded[] = $key;
+ }
+ }
+ }
+
+ /**
+ * Utility method to walk the route, and pull out the valid
+ * dynamic/wildcard keys
+ *
+ * @param string $routePath Route path
+ * @return array Route list
+ */
+ protected function _pathKeys($routePath)
+ {
+ $collecting = false;
+ $current = '';
+ $doneOn = array();
+ $varType = '';
+ $justStarted = false;
+ $routeList = array();
+
+ foreach (preg_split('//', $routePath, -1, PREG_SPLIT_NO_EMPTY) as $char) {
+ if (!$collecting && in_array($char, array(':', '*'))) {
+ $justStarted = true;
+ $collecting = true;
+ $varType = $char;
+ if (strlen($current) > 0) {
+ $routeList[] = $current;
+ $current = '';
+ }
+ } else if ($collecting && $justStarted) {
+ $justStarted = false;
+ if ($char == '(') {
+ $doneOn = array(')');
+ } else {
+ $current = $char;
+ // Basically appends '-' to _splitChars
+ // Helps it fall in line with the Python idioms.
+ $doneOn = $this->_splitChars + array('-');
+ }
+ } else if ($collecting && !in_array($char, $doneOn)) {
+ $current .= $char;
+ } else if ($collecting) {
+ $collecting = false;
+ $routeList[] = array('type' => $varType, 'name' => $current);
+ if (in_array($char, $this->_splitChars)) {
+ $routeList[] = $char;
+ }
+ $doneOn = $varType = $current = '';
+ } else {
+ $current .= $char;
+ }
+ }
+ if ($collecting) {
+ $routeList[] = array('type' => $varType, 'name' => $current);
+ } else if (!empty($current)) {
+ $routeList[] = $current;
+ }
+ return $routeList;
+ }
+
+ /**
+ * Utility function to walk the route backwards
+ *
+ * Will determine the minimum keys we must have to generate a
+ * working route.
+ *
+ * @param array $routeList Route path split by '/'
+ * @return array [minimum keys for route, route list reversed]
+ */
+ protected function _minKeys($routeList)
+ {
+ $minKeys = array();
+ $backCheck = array_reverse($routeList);
+ $gaps = false;
+ foreach ($backCheck as $part) {
+ if (!is_array($part) && !in_array($part, $this->_splitChars)) {
+ $gaps = true;
+ continue;
+ } else if (!is_array($part)) {
+ continue;
+ }
+ $key = $part['name'];
+ if (array_key_exists($key, $this->defaults) && !$gaps)
+ continue;
+ $minKeys[] = $key;
+ $gaps = true;
+ }
+ return array($minKeys, $backCheck);
+ }
+
+ /**
+ * Creates a default array of strings
+ *
+ * Puts together the array of defaults, turns non-null values to strings,
+ * and add in our action/id default if they use and do not specify it
+ *
+ * Precondition: $this->_defaultKeys is an array of the currently assumed default keys
+ *
+ * @param array $routekeys All the keys found in the route path
+ * @param array $reservedKeys Array of keys not in the route path
+ * @param array $kargs Keyword args passed to the Route constructor
+ * @return array [defaults, new default keys]
+ */
+ protected function _defaults($routeKeys, $reservedKeys, $kargs)
+ {
+ $defaults = array();
+
+ // Add in a controller/action default if they don't exist
+ if ((!in_array('controller', $routeKeys)) &&
+ (!in_array('controller', array_keys($kargs))) &&
+ (!$this->explicit)) {
+ $kargs['controller'] = 'content';
+ }
+
+ if (!in_array('action', $routeKeys) &&
+ (!in_array('action', array_keys($kargs))) &&
+ (!$this->explicit)) {
+ $kargs['action'] = 'index';
+ }
+
+ $defaultKeys = array();
+ foreach (array_keys($kargs) as $key) {
+ if (!in_array($key, $reservedKeys)) {
+ $defaultKeys[] = $key;
+ }
+ }
+
+ foreach ($defaultKeys as $key) {
+ if ($kargs[$key] !== null) {
+ $defaults[$key] = (string)$kargs[$key];
+ } else {
+ $defaults[$key] = null;
+ }
+ }
+
+ if (in_array('action', $routeKeys) &&
+ (!array_key_exists('action', $defaults)) &&
+ (!$this->explicit)) {
+ $defaults['action'] = 'index';
+ }
+
+ if (in_array('id', $routeKeys) &&
+ (!array_key_exists('id', $defaults)) &&
+ (!$this->explicit)) {
+ $defaults['id'] = null;
+ }
+
+ $newDefaultKeys = array();
+ foreach (array_keys($defaults) as $key) {
+ if (!in_array($key, $reservedKeys)) {
+ $newDefaultKeys[] = $key;
+ }
+ }
+ return array($defaults, $newDefaultKeys);
+ }
+
+ /**
+ * Create the regular expression for matching.
+ *
+ * Note: This MUST be called before match can function properly.
+ *
+ * clist should be a list of valid controller strings that can be
+ * matched, for this reason makeregexp should be called by the web
+ * framework after it knows all available controllers that can be
+ * utilized.
+ *
+ * @param array $clist List of all possible controllers
+ * @return void
+ */
+ public function makeRegexp($clist)
+ {
+ list($reg, $noreqs, $allblank) = $this->buildNextReg($this->_routeList, $clist);
+
+ if (empty($reg)) {
+ $reg = '/';
+ }
+ $reg = $reg . '(/)?$';
+ if (substr($reg, 0, 1) != '/') {
+ $reg = '/' . $reg;
+ }
+ $reg = '^' . $reg;
+
+ $this->regexp = $reg;
+ }
+
+ /**
+ * Recursively build a regexp given a path, and a controller list.
+ *
+ * Returns the regular expression string, and two booleans that can be
+ * ignored as they're only used internally by buildnextreg.
+ *
+ * @param array $path The RouteList for the path
+ * @param array $clist List of all possible controllers
+ * @return array [array, boolean, boolean]
+ */
+ public function buildNextReg($path, $clist)
+ {
+ if (!empty($path)) {
+ $part = $path[0];
+ } else {
+ $part = '';
+ }
+
+ // noreqs will remember whether the remainder has either a string
+ // match, or a non-defaulted regexp match on a key, allblank remembers
+ // if the rest could possible be completely empty
+ list($rest, $noreqs, $allblank) = array('', true, true);
+
+ if (count($path) > 1) {
+ $this->_prior = $part;
+ list($rest, $noreqs, $allblank) = $this->buildNextReg(array_slice($path, 1), $clist);
+ }
+
+ if (is_array($part) && $part['type'] == ':') {
+ $var = $part['name'];
+ $partreg = '';
+
+ // First we plug in the proper part matcher
+ if (array_key_exists($var, $this->reqs)) {
+ $partreg = '(?P<' . $var . '>' . $this->reqs[$var] . ')';
+ } else if ($var == 'controller') {
+ $partreg = '(?P<' . $var . '>' . implode('|', array_map('preg_quote', $clist)) . ')';
+ } else if (in_array($this->_prior, array('/', '#'))) {
+ $partreg = '(?P<' . $var . '>[^' . $this->_prior . ']+?)';
+ } else {
+ if (empty($rest)) {
+ $partreg = '(?P<' . $var . '>[^/]+?)';
+ } else {
+ $partreg = '(?P<' . $var . '>[^' . implode('', $this->_splitChars) . ']+?)';
+ }
+ }
+
+ if (array_key_exists($var, $this->reqs)) {
+ $noreqs = false;
+ }
+ if (!array_key_exists($var, $this->defaults)) {
+ $allblank = false;
+ $noreqs = false;
+ }
+
+ // Now we determine if its optional, or required. This changes
+ // depending on what is in the rest of the match. If noreqs is
+ // true, then its possible the entire thing is optional as there's
+ // no reqs or string matches.
+ if ($noreqs) {
+ // The rest is optional, but now we have an optional with a
+ // regexp. Wrap to ensure that if we match anything, we match
+ // our regexp first. It's still possible we could be completely
+ // blank as we have a default
+ if (array_key_exists($var, $this->reqs) && array_key_exists($var, $this->defaults)) {
+ $reg = '(' . $partreg . $rest . ')?';
+
+ // Or we have a regexp match with no default, so now being
+ // completely blank form here on out isn't possible
+ } else if (array_key_exists($var, $this->reqs)) {
+ $allblank = false;
+ $reg = $partreg . $rest;
+
+ // If the character before this is a special char, it has to be
+ // followed by this
+ } else if (array_key_exists($var, $this->defaults) && in_array($this->_prior, array(',', ';', '.'))) {
+ $reg = $partreg . $rest;
+
+ // Or we have a default with no regexp, don't touch the allblank
+ } else if (array_key_exists($var, $this->defaults)) {
+ $reg = $partreg . '?' . $rest;
+
+ // Or we have a key with no default, and no reqs. Not possible
+ // to be all blank from here
+ } else {
+ $allblank = false;
+ $reg = $partreg . $rest;
+ }
+
+ // In this case, we have something dangling that might need to be
+ // matched
+ } else {
+ // If they can all be blank, and we have a default here, we know
+ // its safe to make everything from here optional. Since
+ // something else in the chain does have req's though, we have
+ // to make the partreg here required to continue matching
+ if ($allblank && array_key_exists($var, $this->defaults)) {
+ $reg = '(' . $partreg . $rest . ')?';
+
+ // Same as before, but they can't all be blank, so we have to
+ // require it all to ensure our matches line up right
+ } else {
+ $reg = $partreg . $rest;
+ }
+ }
+ } else if (is_array($part) && $part['type'] == '*') {
+ $var = $part['name'];
+ if ($noreqs) {
+ $reg = '(?P<' . $var . '>.*)' . $rest;
+ if (!array_key_exists($var, $this->defaults)) {
+ $allblank = false;
+ $noreqs = false;
+ }
+ } else {
+ if ($allblank && array_key_exists($var, $this->defaults)) {
+ $reg = '(?P<' . $var . '>.*)' . $rest;
+ } else if (array_key_exists($var, $this->defaults)) {
+ $reg = '(?P<' . $var . '>.*)' . $rest;
+ } else {
+ $allblank = false;
+ $noreqs = false;
+ $reg = '(?P<' . $var . '>.*)' . $rest;
+ }
+ }
+ } else if ($part && in_array(substr($part, -1), $this->_splitChars)) {
+ if ($allblank) {
+ $reg = preg_quote(substr($part, 0, -1)) . '(' . preg_quote(substr($part, -1)) . $rest . ')?';
+ } else {
+ $allblank = false;
+ $reg = preg_quote($part) . $rest;
+ }
+
+ // We have a normal string here, this is a req, and it prevents us from
+ // being all blank
+ } else {
+ $noreqs = false;
+ $allblank = false;
+ $reg = preg_quote($part) . $rest;
+ }
+
+ return array($reg, $noreqs, $allblank);
+ }
+
+ /**
+ * Match a url to our regexp.
+ *
+ * While the regexp might match, this operation isn't
+ * guaranteed as there's other factors that can cause a match to fail
+ * even though the regexp succeeds (Default that was relied on wasn't
+ * given, requirement regexp doesn't pass, etc.).
+ *
+ * Therefore the calling function shouldn't assume this will return a
+ * valid dict, the other possible return is False if a match doesn't work
+ * out.
+ *
+ * @param string $url URL to match
+ * @param array Keyword arguments
+ * @return null|array Array of match data if matched, Null otherwise
+ */
+ public function match($url, $kargs = array())
+ {
+ $defaultKargs = array('environ' => array(),
+ 'subDomains' => false,
+ 'subDomainsIgnore' => array(),
+ 'domainMatch' => '');
+ $kargs = array_merge($defaultKargs, $kargs);
+
+ // Static routes don't match, they generate only
+ if ($this->static) {
+ return false;
+ }
+
+ if (substr($url, -1) == '/' && strlen($url) > 1) {
+ $url = substr($url, 0, -1);
+ }
+
+ // Match the regexps we generated
+ $match = preg_match('@' . str_replace('@', '\@', $this->regexp) . '@', $url, $matches);
+ if ($match == 0) {
+ return false;
+ }
+
+ $host = isset($kargs['environ']['HTTP_HOST']) ? $kargs['environ']['HTTP_HOST'] : null;
+ if ($host !== null && !empty($kargs['subDomains'])) {
+ $host = substr($host, 0, strpos(':', $host));
+ $subMatch = '@^(.+?)\.' . $kargs['domainMatch'] . '$';
+ $subdomain = preg_replace($subMatch, '$1', $host);
+ if (!in_array($subdomain, $kargs['subDomainsIgnore'] && $host != $subdomain)) {
+ $subDomain = $subdomain;
+ }
+ }
+
+ if (!empty($this->conditions)) {
+ if (isset($this->conditions['method'])) {
+ if (empty($kargs['environ']['REQUEST_METHOD'])) { return false; }
+
+ if (!in_array($kargs['environ']['REQUEST_METHOD'], $this->conditions['method'])) {
+ return false;
+ }
+ }
+
+ // Check sub-domains?
+ $use_sd = isset($this->conditions['subDomain']) ? $this->conditions['subDomain'] : null;
+ if (!empty($use_sd) && empty($subDomain)) {
+ return false;
+ }
+ if (is_array($use_sd) && !in_array($subDomain, $use_sd)) {
+ return false;
+ }
+ }
+ $matchDict = $matches;
+
+ // Clear out int keys as PHP gives us both the named subgroups and numbered subgroups
+ foreach ($matchDict as $key => $val) {
+ if (is_int($key)) {
+ unset($matchDict[$key]);
+ }
+ }
+ $result = array();
+ $extras = Horde_Routes_Utils::arraySubtract(array_keys($this->defaults), array_keys($matchDict));
+
+ foreach ($matchDict as $key => $val) {
+ // TODO: character set decoding
+ if ($key != 'path_info' && $this->encoding) {
+ $val = urldecode($val);
+ }
+
+ if (empty($val) && array_key_exists($key, $this->defaults) && !empty($this->defaults[$key])) {
+ $result[$key] = $this->defaults[$key];
+ } else {
+ $result[$key] = $val;
+ }
+ }
+
+ foreach ($extras as $key) {
+ $result[$key] = $this->defaults[$key];
+ }
+
+ // Add the sub-domain if there is one
+ if (!empty($kargs['subDomains'])) {
+ $result['subDomain'] = $subDomain;
+ }
+
+ // If there's a function, call it with environ and expire if it
+ // returns False
+ if (!empty($this->conditions) && array_key_exists('function', $this->conditions) &&
+ !call_user_func_array($this->conditions['function'], array($kargs['environ'], $result))) {
+ return false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generate a URL from ourself given a set of keyword arguments
+ *
+ * @param array $kargs Keyword arguments
+ * @param boolean|string False if generation failed, URL otherwise
+ */
+ public function generate($kargs)
+ {
+ $defaultKargs = array('_ignoreReqList' => false,
+ '_appendSlash' => false);
+ $kargs = array_merge($defaultKargs, $kargs);
+
+ $_appendSlash = $kargs['_appendSlash'];
+ unset($kargs['_appendSlash']);
+
+ $_ignoreReqList = $kargs['_ignoreReqList'];
+ unset($kargs['_ignoreReqList']);
+
+ // Verify that our args pass any regexp requirements
+ if (!$_ignoreReqList) {
+ foreach ($this->reqs as $key => $v) {
+ $value = (isset($kargs[$key])) ? $kargs[$key] : null;
+
+ if (!empty($value) && !preg_match($this->_reqRegs[$key], $value)) {
+ return false;
+ }
+ }
+ }
+
+ // Verify that if we have a method arg, it's in the method accept list.
+ // Also, method will be changed to _method for route generation.
+ $meth = (isset($kargs['method'])) ? $kargs['method'] : null;
+
+ if ($meth) {
+ if ($this->conditions && isset($this->conditions['method']) &&
+ (!in_array(strtoupper($meth), $this->conditions['method']))) {
+
+ return false;
+ }
+ unset($kargs['method']);
+ }
+
+ $routeList = $this->_routeBackwards;
+ $urlList = array();
+ $gaps = false;
+ foreach ($routeList as $part) {
+ if (is_array($part) && $part['type'] == ':') {
+ $arg = $part['name'];
+
+ // For efficiency, check these just once
+ $hasArg = array_key_exists($arg, $kargs);
+ $hasDefault = array_key_exists($arg, $this->defaults);
+
+ // Determine if we can leave this part off
+ // First check if the default exists and wasn't provided in the
+ // call (also no gaps)
+ if ($hasDefault && !$hasArg && !$gaps) {
+ continue;
+ }
+
+ // Now check to see if there's a default and it matches the
+ // incoming call arg
+ if (($hasDefault && $hasArg) && $kargs[$arg] == $this->defaults[$arg] && !$gaps) {
+ continue;
+ }
+
+ // We need to pull the value to append, if the arg is NULL and
+ // we have a default, use that
+ if ($hasArg && $kargs[$arg] === null && $hasDefault && !$gaps) {
+ continue;
+
+ // Otherwise if we do have an arg, use that
+ } else if ($hasArg) {
+ $val = ($kargs[$arg] === null) ? 'null' : $kargs[$arg];
+ } else if ($hasDefault && $this->defaults[$arg] != null) {
+ $val = $this->defaults[$arg];
+
+ // No arg at all? This won't work
+ } else {
+ return false;
+ }
+
+ $urlList[] = Horde_Routes_Utils::urlQuote($val, $this->encoding);
+ if ($hasArg) {
+ unset($kargs[$arg]);
+ }
+ $gaps = true;
+ } else if (is_array($part) && $part['type'] == '*') {
+ $arg = $part['name'];
+ $kar = (isset($kargs[$arg])) ? $kargs[$arg] : null;
+ if ($kar != null) {
+ $urlList[] = Horde_Routes_Utils::urlQuote($kar, $this->encoding);
+ $gaps = true;
+ }
+ } else if (!empty($part) && in_array(substr($part, -1), $this->_splitChars)) {
+ if (!$gaps && in_array($part, $this->_splitChars)) {
+ continue;
+ } else if (!$gaps) {
+ $gaps = true;
+ $urlList[] = substr($part, 0, -1);
+ } else {
+ $gaps = true;
+ $urlList[] = $part;
+ }
+ } else {
+ $gaps = true;
+ $urlList[] = $part;
+ }
+ }
+
+ $urlList = array_reverse($urlList);
+ $url = implode('', $urlList);
+ if (substr($url, 0, 1) != '/') {
+ $url = '/' . $url;
+ }
+
+ $extras = $kargs;
+ foreach ($this->maxKeys as $key) {
+ unset($extras[$key]);
+ }
+ $extras = array_keys($extras);
+
+ if (!empty($extras)) {
+ if ($_appendSlash && substr($url, -1) != '/') {
+ $url .= '/';
+ }
+ $url .= '?';
+ $newExtras = array();
+ foreach ($kargs as $key => $value) {
+ if (in_array($key, $extras) && ($key != 'action' || $key != 'controller')) {
+ $newExtras[$key] = $value;
+ }
+ }
+ $url .= http_build_query($newExtras);
+ } else if ($_appendSlash && substr($url, -1) != '/') {
+ $url .= '/';
+ }
+ return $url;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * Utility functions for use in templates and controllers
+ *
+ * @package Horde_Routes
+ */
+class Horde_Routes_Utils
+{
+ /**
+ * @var Horde_Routes_Mapper
+ */
+ public $mapper;
+
+ /**
+ * Match data from last match; implements for urlFor() route memory
+ * @var array
+ */
+ public $mapperDict = array();
+
+ /**
+ * Callback function used for redirectTo()
+ * @var callback
+ */
+ public $redirect;
+
+
+ /**
+ * Constructor
+ *
+ * @param Horde_Routes_Mapper $mapper Mapper for these utilities
+ * @param callback $redirect Redirect callback for redirectTo()
+ */
+ public function __construct($mapper, $redirect = null)
+ {
+ $this->mapper = $mapper;
+ $this->redirect = $redirect;
+ }
+
+ /**
+ * Generates a URL.
+ *
+ * All keys given to urlFor are sent to the Routes Mapper instance for
+ * generation except for::
+ *
+ * anchor specified the anchor name to be appened to the path
+ * host overrides the default (current) host if provided
+ * protocol overrides the default (current) protocol if provided
+ * qualified creates the URL with the host/port information as
+ * needed
+ *
+ * The URL is generated based on the rest of the keys. When generating a new
+ * URL, values will be used from the current request's parameters (if
+ * present). The following rules are used to determine when and how to keep
+ * the current requests parameters:
+ *
+ * * If the controller is present and begins with '/', no defaults are used
+ * * If the controller is changed, action is set to 'index' unless otherwise
+ * specified
+ *
+ * For example, if the current request yielded a dict (associative array) of
+ * array('controller'=>'blog', 'action'=>'view', 'id'=>2), with the standard
+ * ':controller/:action/:id' route, you'd get the following results::
+ *
+ * urlFor(array('id'=>4)) => '/blog/view/4',
+ * urlFor(array('controller'=>'/admin')) => '/admin',
+ * urlFor(array('controller'=>'admin')) => '/admin/view/2'
+ * urlFor(array('action'=>'edit')) => '/blog/edit/2',
+ * urlFor(array('action'=>'list', id=NULL)) => '/blog/list'
+ *
+ * **Static and Named Routes**
+ *
+ * If there is a string present as the first argument, a lookup is done
+ * against the named routes table to see if there's any matching routes. The
+ * keyword defaults used with static routes will be sent in as GET query
+ * arg's if a route matches.
+ *
+ * If no route by that name is found, the string is assumed to be a raw URL.
+ * Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will
+ * be added if present, otherwise the string will be used as the url with
+ * keyword args becoming GET query args.
+ */
+ public function urlFor($first = array(), $second = array())
+ {
+ if (is_array($first)) {
+ // urlFor(array('controller' => 'foo', ...))
+ $routeName = null;
+ $kargs = $first;
+ } else {
+ // urlFor('named_route')
+ // urlFor('named_route', array('id' => 3, ...))
+ // urlFor('static_path')
+ $routeName = $first;
+ $kargs = $second;
+ }
+
+ $anchor = isset($kargs['anchor']) ? $kargs['anchor'] : null;
+ $host = isset($kargs['host']) ? $kargs['host'] : null;
+ $protocol = isset($kargs['protocol']) ? $kargs['protocol'] : null;
+ $qualified = isset($kargs['qualified']) ? $kargs['qualified'] : null;
+ unset($kargs['qualified']);
+
+ // Remove special words from kargs, convert placeholders
+ foreach (array('anchor', 'host', 'protocol') as $key) {
+ if (array_key_exists($key, $kargs)) {
+ unset($kargs[$key]);
+ }
+ if (array_key_exists($key . '_', $kargs)) {
+ $kargs[$key] = $kargs[$key . '_'];
+ unset($kargs[$key . '_']);
+ }
+ }
+
+ $route = null;
+ $static = false;
+ $encoding = $this->mapper->encoding;
+ $environ = $this->mapper->environ;
+ $url = '';
+
+ if (isset($routeName)) {
+
+ if (isset($this->mapper->routeNames[$routeName])) {
+ $route = $this->mapper->routeNames[$routeName];
+ }
+
+ if ($route && array_key_exists('_static', $route->defaults)) {
+ $static = true;
+ $url = $route->routePath;
+ }
+
+ // No named route found, assume the argument is a relative path
+ if ($route === null) {
+ $static = true;
+ $url = $routeName;
+ }
+
+ if ((substr($url, 0, 1) == '/') &&
+ isset($environ['SCRIPT_NAME'])) {
+ $url = $environ['SCRIPT_NAME'] . $url;
+ }
+
+ if ($static) {
+ if (!empty($kargs)) {
+ $url .= '?';
+ $query_args = array();
+ foreach ($kargs as $key => $val) {
+ $query_args[] = urlencode(utf8_decode($key)) . '=' .
+ urlencode(utf8_decode($val));
+ }
+ $url .= implode('&', $query_args);
+ }
+ }
+ }
+
+ if (! $static) {
+ if ($route) {
+ $newargs = $route->defaults;
+ foreach ($kargs as $key => $value) {
+ $newargs[$key] = $value;
+ }
+
+ // If this route has a filter, apply it
+ if (!empty($route->filter)) {
+ $newargs = call_user_func($route->filter, $newargs);
+ }
+
+ $newargs = $this->_subdomainCheck($newargs);
+ } else {
+ $newargs = $this->_screenArgs($kargs);
+ }
+
+ $anchor = (isset($newargs['_anchor'])) ? $newargs['_anchor'] : $anchor;
+ unset($newargs['_anchor']);
+
+ $host = (isset($newargs['_host'])) ? $newargs['_host'] : $host;
+ unset($newargs['_host']);
+
+ $protocol = (isset($newargs['_protocol'])) ? $newargs['_protocol'] : $protocol;
+ unset($newargs['_protocol']);
+
+ $url = $this->mapper->generate($newargs);
+ }
+
+ if (!empty($anchor)) {
+ $url .= '#' . self::urlQuote($anchor, $encoding);
+ }
+
+ if (!empty($host) || !empty($qualified) || !empty($protocol)) {
+ $http_host = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
+ $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
+ $fullhost = !is_null($http_host) ? $http_host : $server_name;
+
+ if (empty($host) && empty($qualified)) {
+ $host = explode(':', $fullhost);
+ $host = $host[0];
+ } else if (empty($host)) {
+ $host = $fullhost;
+ }
+ if (empty($protocol)) {
+ if (!empty($environ['HTTPS']) && $environ['HTTPS'] != 'off') {
+ $protocol = 'https';
+ } else {
+ $protocol = 'http';
+ }
+ }
+ if ($url !== null) {
+ $url = $protocol . '://' . $host . $url;
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * Issues a redirect based on the arguments.
+ *
+ * Redirects *should* occur as a "302 Moved" header, however the web
+ * framework may utilize a different method.
+ *
+ * All arguments are passed to urlFor() to retrieve the appropriate URL, then
+ * the resulting URL it sent to the redirect function as the URL.
+ *
+ * @param mixed $first First argument in varargs, same as urlFor()
+ * @param mixed $second Second argument in varargs
+ * @return mixed Result of redirect callback
+ */
+ public function redirectTo($first = array(), $second = array())
+ {
+ $target = $this->urlFor($first, $second);
+ return call_user_func($this->redirect, $target);
+ }
+
+ /**
+ * Scan a directory for PHP files and use them as controllers. Used
+ * as the default scanner callback for Horde_Routes_Mapper. See the
+ * constructor of that class for more information.
+ *
+ * Given a directory with:
+ * foo.php, bar.php, baz.php
+ * Returns an array:
+ * foo, bar, baz
+ *
+ * @param string $dirname Directory to scan for controller files
+ * @param string $prefix Prefix controller names (optional)
+ * @return array Array of controller names
+ */
+ public static function controllerScan($dirname = null, $prefix = '')
+ {
+ $controllers = array();
+
+ if ($dirname === null) {
+ return $controllers;
+ }
+
+ $baseregexp = preg_quote($dirname . DIRECTORY_SEPARATOR, '/');
+
+ foreach (new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($dirname)) as $entry) {
+
+ if ($entry->isFile()) {
+ // match .php files that don't start with an underscore
+ if (preg_match('/^[^_]{1,1}.*\.php$/', basename($entry->getFilename())) != 0) {
+ // strip off base path: dirname/admin/users.php -> admin/users.php
+ $controller = preg_replace("/^$baseregexp(.*)\.php/", '\\1', $entry->getPathname());
+
+ // add to controller list
+ $controllers[] = $prefix . $controller;
+ }
+ }
+ }
+
+ $callback = array('Horde_Routes_Utils', 'longestFirst');
+ usort($controllers, $callback);
+
+ return $controllers;
+ }
+
+ /**
+ * Private function that takes a dict, and screens it against the current
+ * request dict to determine what the dict should look like that is used.
+ * This is responsible for the requests "memory" of the current.
+ */
+ private function _screenArgs($kargs)
+ {
+ if ($this->mapper->explicit && $this->mapper->subDomains) {
+ return $this->_subdomainCheck($kargs);
+ } else if ($this->mapper->explicit) {
+ return $kargs;
+ }
+
+ $controllerName = (isset($kargs['controller'])) ? $kargs['controller'] : null;
+
+ if (!empty($controllerName) && substr($controllerName, 0, 1) == '/') {
+ // If the controller name starts with '/', ignore route memory
+ $kargs['controller'] = substr($kargs['controller'], 1);
+ return $kargs;
+ } else if (!empty($controllerName) && !array_key_exists('action', $kargs)) {
+ // Fill in an action if we don't have one, but have a controller
+ $kargs['action'] = 'index';
+ }
+
+ $memoryKargs = $this->mapperDict;
+
+ // Remove keys from memory and kargs if kargs has them as null
+ foreach ($kargs as $key => $value) {
+ if ($value === null) {
+ unset($kargs[$key]);
+ if (array_key_exists($key, $memoryKargs)) {
+ unset($memoryKargs[$key]);
+ }
+ }
+ }
+
+ // Merge the new args on top of the memory args
+ foreach ($kargs as $key => $value) {
+ $memoryKargs[$key] = $value;
+ }
+
+ // Setup a sub-domain if applicable
+ if (!empty($this->mapper->subDomains)) {
+ $memoryKargs = $this->_subdomainCheck($memoryKargs);
+ }
+
+ return $memoryKargs;
+ }
+
+ /**
+ * Screen the kargs for a subdomain and alter it appropriately depending
+ * on the current subdomain or lack therof.
+ */
+ private function _subdomainCheck($kargs)
+ {
+ if ($this->mapper->subDomains) {
+ $subdomain = (isset($kargs['subDomain'])) ? $kargs['subDomain'] : null;
+ unset($kargs['subDomain']);
+
+ $environ = $this->mapper->environ;
+ $http_host = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
+ $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
+ $fullhost = !is_null($http_host) ? $http_host : $server_name;
+
+ $hostmatch = explode(':', $fullhost);
+ $host = $hostmatch[0];
+ $port = '';
+ if (count($hostmatch) > 1) {
+ $port .= ':' . $hostmatch[1];
+ }
+
+ $subMatch = '^.+?\.(' . $this->mapper->domainMatch . ')$';
+ $domain = preg_replace("@$subMatch@", '$1', $host);
+
+ if ($subdomain && (substr($host, 0, strlen($subdomain)) != $subdomain)
+ && (! in_array($subdomain, $this->mapper->subDomainsIgnore))) {
+ $kargs['_host'] = $subdomain . '.' . $domain . $port;
+ } else if (($subdomain === null || in_array($subdomain, $this->mapper->subDomainsIgnore))
+ && $domain != $host) {
+ $kargs['_host'] = $domain . $port;
+ }
+ return $kargs;
+ } else {
+ return $kargs;
+ }
+ }
+
+ /**
+ * Quote a string containing a URL in a given encoding.
+ *
+ * @todo This is a placeholder. Multiple encodings aren't yet supported.
+ *
+ * @param string $url URL to encode
+ * @param string $encoding Encoding to use
+ */
+ public static function urlQuote($url, $encoding = null)
+ {
+ if ($encoding === null) {
+ return str_replace('%2F', '/', urlencode($url));
+ } else {
+ return str_replace('%2F', '/', urlencode(utf8_decode($url)));
+ }
+ }
+
+ /**
+ * Callback used by usort() in controllerScan() to sort controller
+ * names by the longest first.
+ *
+ * @param string $fst First string to compare
+ * @param string $lst Last string to compare
+ * @return integer Difference of string length (first - last)
+ */
+ public static function longestFirst($fst, $lst)
+ {
+ return strlen($lst) - strlen($fst);
+ }
+
+ /**
+ */
+ public static function arraySubtract($a1, $a2)
+ {
+ foreach ($a2 as $key) {
+ if (in_array($key, $a1)) {
+ unset($a1[array_search($key, $a1)]);
+ }
+ }
+ return $a1;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Routes</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Routes URL mapping system</summary>
+ <description>This package provides classes for mapping URLs into
+ the controllers and actions of an MVC system. It is a port of a
+ Python library, Routes, by Ben Bangert (http://routes.groovie.org).
+ </description>
+ <lead>
+ <name>Mike Naberezny</name>
+ <user>mnaberez</user>
+ <email>mike@maintainable.com</email>
+ <active>yes</active>
+ </lead>
+ <lead>
+ <name>Chuck Hagenbuch</name>
+ <user>chuck</user>
+ <email>chuck@horde.org</email>
+ <active>yes</active>
+ </lead>
+ <date>2008-09-19</date>
+ <version>
+ <release>0.3.1</release>
+ <api>0.3.0</api>
+ </version>
+ <stability>
+ <release>beta</release>
+ <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>* Fixed callbacks for PHP versions prior to PHP 5.2.</notes>
+ <contents>
+ <dir name="/">
+ <dir name="lib">
+ <dir name="Horde">
+ <dir name="Routes">
+ <file name="Exception.php" role="php" />
+ <file name="Mapper.php" role="php" />
+ <file name="Route.php" role="php" />
+ <file name="Utils.php" role="php" />
+ </dir> <!-- /lib/Horde/Routes -->
+ </dir> <!-- /lib/Horde -->
+ </dir> <!-- /lib -->
+ </dir> <!-- / -->
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.1.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.4.0b1</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease>
+ <filelist>
+ <install name="lib/Horde/Routes/Exception.php" as="Horde/Routes/Exception.php" />
+ <install name="lib/Horde/Routes/Mapper.php" as="Horde/Routes/Mapper.php" />
+ <install name="lib/Horde/Routes/Route.php" as="Horde/Routes/Route.php" />
+ <install name="lib/Horde/Routes/Utils.php" as="Horde/Routes/Utils.php" />
+ </filelist>
+ </phprelease>
+</package>
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+ define('PHPUnit_MAIN_METHOD', 'Horde_Routes_AllTests::main');
+}
+
+error_reporting(E_ALL | E_STRICT);
+
+$lib = dirname(dirname(dirname(dirname(__FILE__)))) . '/lib/Horde/Routes/';
+require_once "$lib/Utils.php";
+require_once "$lib/Mapper.php";
+require_once "$lib/Route.php";
+require_once "$lib/Exception.php";
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * @package Horde_Routes
+ */
+class Horde_Routes_AllTests
+{
+ public static function main()
+ {
+ PHPUnit_TextUI_TestRunner::run(self::suite());
+ }
+
+ public static function suite()
+ {
+ $suite = new PHPUnit_Framework_TestSuite('Routes');
+
+ $basedir = dirname(__FILE__);
+ $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+ foreach(new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($basedir)) as $file) {
+ if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+ $pathname = $file->getPathname();
+ require $pathname;
+
+ $class = str_replace(DIRECTORY_SEPARATOR, '_',
+ preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+ $suite->addTestSuite($class);
+ }
+ }
+
+ return $suite;
+ }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Routes_AllTests::main') {
+ Horde_Routes_AllTests::main();
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * @package Horde_Routes
+ */
+class GenerationTest extends PHPUnit_Framework_TestCase
+{
+
+ public function testAllStaticNoReqs()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('hello/world');
+
+ $this->assertEquals('/hello/world', $m->generate());
+ }
+
+ public function testBasicDynamic()
+ {
+ foreach (array('hi/:fred', 'hi/:(fred)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/hi/index', $m->generate(array('fred' => 'index')));
+ $this->assertEquals('/hi/show', $m->generate(array('fred' => 'show')));
+ $this->assertEquals('/hi/list+people', $m->generate(array('fred' => 'list people')));
+ $this->assertNull($m->generate());
+ }
+ }
+
+ public function testDynamicWithDefault()
+ {
+ foreach (array('hi/:action', 'hi/:(action)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/hi', $m->generate(array('action' => 'index')));
+ $this->assertEquals('/hi/show', $m->generate(array('action' => 'show')));
+ $this->assertEquals('/hi/list+people', $m->generate(array('action' => 'list people')));
+ $this->assertEquals('/hi', $m->generate());
+ }
+ }
+
+ /**
+ * Some of these assertions are invalidated in PHP, it passes in Python because
+ * unicode(None) == unicode('None'). In PHP, we don't have a function similar
+ * to unicode(). These have the comment "unicode false equiv"
+ */
+ public function testDynamicWithFalseEquivs()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('article/:page', array('page' => false));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/blog/view/0',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => '0')));
+
+ // unicode false equiv
+ // $this->assertEquals('/blog/view/0',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 0)));
+ //$this->assertEquals('/blog/view/False',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => false)));
+
+ $this->assertEquals('/blog/view/False',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 'False')));
+ $this->assertEquals('/blog/view',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => null)));
+
+ // unicode false equiv
+ // $this->assertEquals('/blog/view',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 'null')));
+
+ $this->assertEquals('/article',
+ $m->generate(array('page' => null)));
+ }
+
+ public function testDynamicWithUnderscoreParts()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('article/:small_page', array('small_page' => false));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/blog/view/0',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => '0')));
+
+ // unicode false equiv
+ // $this->assertEquals('/blog/view/False',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => false)));
+
+ // unicode False Equiv
+ //$this->assertEquals('/blog/view',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 'null'))); */
+
+ $this->assertEquals('/article',
+ $m->generate(array('small_page' => null)));
+ $this->assertEquals('/article/hobbes',
+ $m->generate(array('small_page' => 'hobbes')));
+ }
+
+ public function testDynamicWithFalseEquivsAndSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('article/:(page)', array('page' => false));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/blog/view/0',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => '0')));
+
+ // unicode false equiv
+ // $this->assertEquals('/blog/view/0',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 0)));
+ // $this->assertEquals('/blog/view/False',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => false));
+
+ $this->assertEquals('/blog/view/False',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 'False')));
+ $this->assertEquals('/blog/view',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => null)));
+
+ // unicode false equiv
+ //$this->assertEquals('/blog/view',
+ // $m->generate(array('controller' => 'blog', 'action' => 'view', 'id' => 'null')));
+
+ $this->assertEquals('/article',
+ $m->generate(array('page' => null)));
+
+ $m = new Horde_Routes_Mapper();
+ $m->connect('view/:(home)/:(area)', array('home' => 'austere', 'area' => null));
+
+ $this->assertEquals('/view/sumatra',
+ $m->generate(array('home' => 'sumatra')));
+ $this->assertEquals('/view/austere/chicago',
+ $m->generate(array('area' => 'chicago')));
+
+ $m = new Horde_Routes_Mapper();
+ $m->connect('view/:(home)/:(area)', array('home' => null, 'area' => null));
+
+ $this->assertEquals('/view/null/chicago',
+ $m->generate(array('home' => null, 'area' => 'chicago')));
+ }
+
+ public function testDynamicWithRegExpCondition()
+ {
+ foreach (array('hi/:name', 'hi/:(name)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('requirements' => array('name' => '[a-z]+')));
+
+ $this->assertEquals('/hi/index', $m->generate(array('name' => 'index')));
+ $this->assertNull($m->generate(array('name' => 'fox5')));
+ $this->assertNull($m->generate(array('name' => 'something_is_up')));
+ $this->assertEquals('/hi/abunchofcharacter',
+ $m->generate(array('name' => 'abunchofcharacter')));
+ $this->assertNull($m->generate());
+ }
+ }
+
+ public function testDynamicWithDefaultAndRegexpCondition()
+ {
+ foreach (array('hi/:action', 'hi/:(action)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('requirements' => array('action' => '[a-z]+')));
+
+ $this->assertEquals('/hi', $m->generate(array('action' => 'index')));
+ $this->assertNull($m->generate(array('action' => 'fox5')));
+ $this->assertNull($m->generate(array('action' => 'something_is_up')));
+ $this->assertNull($m->generate(array('action' => 'list people')));
+ $this->assertEquals('/hi/abunchofcharacter',
+ $m->generate(array('action' => 'abunchofcharacter')));
+ $this->assertEquals('/hi', $m->generate());
+ }
+ }
+
+ public function testPath()
+ {
+ foreach (array('hi/*file', 'hi/*(file)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/hi',
+ $m->generate(array('file' => null)));
+ $this->assertEquals('/hi/books/learning_python.pdf',
+ $m->generate(array('file' => 'books/learning_python.pdf')));
+ $this->assertEquals('/hi/books/development%26whatever/learning_python.pdf',
+ $m->generate(array('file' => 'books/development&whatever/learning_python.pdf')));
+ }
+ }
+
+ public function testPathBackwards()
+ {
+ foreach (array('*file/hi', '*(file)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/hi',
+ $m->generate(array('file' => null)));
+ $this->assertEquals('/books/learning_python.pdf/hi',
+ $m->generate(array('file' => 'books/learning_python.pdf')));
+ $this->assertEquals('/books/development%26whatever/learning_python.pdf/hi',
+ $m->generate(array('file' => 'books/development&whatever/learning_python.pdf')));
+ }
+ }
+
+ public function testController()
+ {
+ foreach (array('hi/:controller', 'hi/:(controller)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/hi/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/hi/admin/user',
+ $m->generate(array('controller' => 'admin/user')));
+ }
+ }
+
+ public function testControllerWithStatic()
+ {
+ foreach (array('hi/:controller', 'hi/:(controller)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $utils = $m->utils;
+ $m->connect($path);
+ $m->connect('google', 'http://www.google.com', array('_static' => true));
+
+ $this->assertEquals('/hi/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/hi/admin/user',
+ $m->generate(array('controller' => 'admin/user')));
+ $this->assertEquals('http://www.google.com', $utils->urlFor('google'));
+ }
+ }
+
+ public function testStandardRoute()
+ {
+ foreach (array(':controller/:action/:id', ':(controller)/:(action)/:(id)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/content',
+ $m->generate(array('controller' => 'content', 'action' => 'index')));
+ $this->assertEquals('/content/list',
+ $m->generate(array('controller' => 'content', 'action' => 'list')));
+ $this->assertEquals('/content/show/10',
+ $m->generate(array('controller' => 'content', 'action' => 'show', 'id' => '10')));
+
+ $this->assertEquals('/admin/user',
+ $m->generate(array('controller' => 'admin/user', 'action' => 'index')));
+ $this->assertEquals('/admin/user/list',
+ $m->generate(array('controller' => 'admin/user', 'action' => 'list')));
+ $this->assertEquals('/admin/user/show/10',
+ $m->generate(array('controller' => 'admin/user', 'action' => 'show', 'id' => '10')));
+ }
+ }
+
+ public function testMultiroute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:year/:month/:day', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:id', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/blog/view?year=2004&month=blah',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => 'blah')));
+ $this->assertEquals('/archive/2004/11',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => 11)));
+ $this->assertEquals('/archive/2004/11',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => '11')));
+ $this->assertEquals('/archive/2004/11',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'year' => 2004, 'month' => 11)));
+ $this->assertEquals('/archive/2004', $m->generate(array('controller' => 'blog', 'action' => 'view', 'year' => 2004)));
+ $this->assertEquals('/viewpost/3',
+ $m->generate(array('controller' => 'post', 'action' => 'view', 'id' => 3)));
+ }
+
+ public function testMultirouteWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:(year)/:(month)/:(day)', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:(id)', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/blog/view?year=2004&month=blah',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => 'blah')));
+ $this->assertEquals('/archive/2004/11',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => 11)));
+ $this->assertEquals('/archive/2004/11',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004, 'month' => '11')));
+ $this->assertEquals('/archive/2004',
+ $m->generate(array('controller' => 'blog', 'action' => 'view', 'year' => 2004)));
+ $this->assertEquals('/viewpost/3',
+ $m->generate(array('controller' => 'post', 'action' => 'view', 'id' => 3)));
+ }
+
+ public function testBigMultiroute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('', array('controller' => 'articles', 'action' => 'index'));
+ $m->connect('admin', array('controller' => 'admin/general', 'action' => 'index'));
+
+ $m->connect('admin/comments/article/:article_id/:action/:id',
+ array('controller' => 'admin/comments', 'action' => null, 'id' => null));
+ $m->connect('admin/trackback/article/:article_id/:action/:id',
+ array('controller' => 'admin/trackback', 'action' => null, 'id' => null));
+ $m->connect('admin/content/:action/:id', array('controller' => 'admin/content'));
+
+ $m->connect('xml/:action/feed.xml', array('controller' => 'xml'));
+ $m->connect('xml/articlerss/:id/feed.xml', array('controller' => 'xml', 'action' => 'articlerss'));
+ $m->connect('index.rdf', array('controller' => 'xml', 'action' => 'rss'));
+
+ $m->connect('articles', array('controller' => 'articles', 'action' => 'index'));
+ $m->connect('articles/page/:page',
+ array('controller' => 'articles', 'action' => 'index',
+ 'requirements' => array('page' => '\d+')));
+ $m->connect('articles/:year/:month/:day/page/:page',
+ array('controller' => 'articles', 'action' => 'find_by_date',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('year' => '\d{4}', 'month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('articles/category/:id', array('controller' => 'articles', 'action' => 'category'));
+ $m->connect('pages/*name', array('controller' => 'articles', 'action' => 'view_page'));
+
+ $this->assertEquals('/pages/the/idiot/has/spoken',
+ $m->generate(array('controller' => 'articles', 'action' => 'view_page',
+ 'name' => 'the/idiot/has/spoken')));
+ $this->assertEquals('/',
+ $m->generate(array('controller' => 'articles', 'action' => 'index')));
+ $this->assertEquals('/xml/articlerss/4/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'articlerss', 'id' => 4)));
+ $this->assertEquals('/xml/rss/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'rss')));
+ $this->assertEquals('/admin/comments/article/4/view/2',
+ $m->generate(array('controller' => 'admin/comments', 'action' => 'view',
+ 'article_id' => 4, 'id' => 2)));
+ $this->assertEquals('/admin',
+ $m->generate(array('controller' => 'admin/general')));
+ $this->assertEquals('/admin/comments/article/4/index',
+ $m->generate(array('controller' => 'admin/comments', 'article_id' => 4)));
+ $this->assertEquals('/admin/comments/article/4',
+ $m->generate(array('controller' => 'admin/comments', 'action' => null,
+ 'article_id' => 4)));
+ $this->assertEquals('/articles/2004/2/20/page/1',
+ $m->generate(array('controller' => 'articles', 'action' => 'find_by_date',
+ 'year' => 2004, 'month' => 2, 'day' => 20, 'page' => 1)));
+ $this->assertEquals('/articles/category',
+ $m->generate(array('controller' => 'articles', 'action' => 'category')));
+ $this->assertEquals('/xml/index/feed.xml',
+ $m->generate(array('controller' => 'xml')));
+ $this->assertEquals('/xml/articlerss/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'articlerss')));
+ $this->assertNull($m->generate(array('controller' => 'admin/comments', 'id' => 2)));
+ $this->assertNull($m->generate(array('controller' => 'articles', 'action' => 'find_by_date',
+ 'year' => 2004)));
+ }
+
+ public function testBigMultirouteWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('', array('controller' => 'articles', 'action' => 'index'));
+ $m->connect('admin', array('controller' => 'admin/general', 'action' => 'index'));
+
+ $m->connect('admin/comments/article/:(article_id)/:(action)/:(id)',
+ array('controller' => 'admin/comments', 'action' => null, 'id' => null));
+ $m->connect('admin/trackback/article/:(article_id)/:(action)/:(id)',
+ array('controller' => 'admin/trackback', 'action' => null, 'id' => null));
+ $m->connect('admin/content/:(action)/:(id)', array('controller' => 'admin/content'));
+
+ $m->connect('xml/:(action)/feed.xml', array('controller' => 'xml'));
+ $m->connect('xml/articlerss/:(id)/feed.xml', array('controller' => 'xml', 'action' => 'articlerss'));
+ $m->connect('index.rdf', array('controller' => 'xml', 'action' => 'rss'));
+
+ $m->connect('articles', array('controller' => 'articles', 'action' => 'index'));
+ $m->connect('articles/page/:(page)',
+ array('controller' => 'articles', 'action' => 'index',
+ 'requirements' => array('page' => '\d+')));
+ $m->connect('articles/:(year)/:(month)/:(day)/page/:(page)',
+ array('controller' => 'articles', 'action' => 'find_by_date',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('year' => '\d{4}', 'month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('articles/category/:(id)', array('controller' => 'articles', 'action' => 'category'));
+ $m->connect('pages/*name', array('controller' => 'articles', 'action' => 'view_page'));
+
+ $this->assertEquals('/pages/the/idiot/has/spoken',
+ $m->generate(array('controller' => 'articles', 'action' => 'view_page',
+ 'name' => 'the/idiot/has/spoken')));
+ $this->assertEquals('/',
+ $m->generate(array('controller' => 'articles', 'action' => 'index')));
+ $this->assertEquals('/xml/articlerss/4/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'articlerss', 'id' => 4)));
+ $this->assertEquals('/xml/rss/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'rss')));
+ $this->assertEquals('/admin/comments/article/4/view/2',
+ $m->generate(array('controller' => 'admin/comments', 'action' => 'view',
+ 'article_id' => 4, 'id' => 2)));
+ $this->assertEquals('/admin',
+ $m->generate(array('controller' => 'admin/general')));
+ $this->assertEquals('/admin/comments/article/4/index',
+ $m->generate(array('controller' => 'admin/comments', 'article_id' => 4)));
+ $this->assertEquals('/admin/comments/article/4',
+ $m->generate(array('controller' => 'admin/comments', 'action' => null,
+ 'article_id' => 4)));
+ $this->assertEquals('/articles/2004/2/20/page/1',
+ $m->generate(array('controller' => 'articles', 'action' => 'find_by_date',
+ 'year' => 2004, 'month' => 2, 'day' => 20, 'page' => 1)));
+ $this->assertEquals('/articles/category',
+ $m->generate(array('controller' => 'articles', 'action' => 'category')));
+ $this->assertEquals('/xml/index/feed.xml',
+ $m->generate(array('controller' => 'xml')));
+ $this->assertEquals('/xml/articlerss/feed.xml',
+ $m->generate(array('controller' => 'xml', 'action' => 'articlerss')));
+ $this->assertNull($m->generate(array('controller' => 'admin/comments', 'id' => 2)));
+ $this->assertNull($m->generate(array('controller' => 'articles', 'action' => 'find_by_date',
+ 'year' => 2004)));
+ }
+
+ public function testNoExtras()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:id');
+ $m->connect('archive/:year/:month/:day', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null));
+
+ $this->assertEquals('/archive/2004',
+ $m->generate(array('controller' => 'blog', 'action' => 'view',
+ 'year' => 2004)));
+ }
+
+ public function testNoExtrasWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':(controller)/:(action)/:(id)');
+ $m->connect('archive/:(year)/:(month)/:(day)', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null));
+ }
+
+ public function testTheSmallestRoute()
+ {
+ foreach (array('pages/:title', 'pages/:(title)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('', array('controller' => 'page', 'action' => 'view', 'title' => 'HomePage'));
+ $m->connect($path, array('controller' => 'page', 'action' => 'view'));
+
+ $this->assertEquals('/pages/joe', $m->generate(array('controller' => 'page', 'action' => 'view', 'title' => 'joe')));
+ $this->assertEquals('/',
+ $m->generate(array('controller' => 'page', 'action' => 'view',
+ 'title' => 'HomePage')));
+ }
+ }
+
+ public function testExtras()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('viewpost/:id', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/viewpost/2?extra=x%2Fy',
+ $m->generate(array('controller' => 'post', 'action' => 'view',
+ 'id' => 2, 'extra' => 'x/y')));
+ $this->assertEquals('/blog?extra=3',
+ $m->generate(array('controller' => 'blog', 'action' => 'index', 'extra' => 3)));
+ $this->assertEquals('/viewpost/2?extra=3',
+ $m->generate(array('controller' => 'post', 'action' => 'view',
+ 'id' => 2, 'extra' => 3)));
+ }
+
+ public function testExtrasWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('viewpost/:(id)', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/viewpost/2?extra=x%2Fy',
+ $m->generate(array('controller' => 'post', 'action' => 'view',
+ 'id' => 2, 'extra' => 'x/y')));
+ $this->assertEquals('/blog?extra=3',
+ $m->generate(array('controller' => 'blog', 'action' => 'index', 'extra' => 3)));
+ $this->assertEquals('/viewpost/2?extra=3',
+ $m->generate(array('controller' => 'post', 'action' => 'view',
+ 'id' => 2, 'extra' => 3)));
+ }
+
+ public function testStatic()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('hello/world', array('controller' => 'content', 'action' => 'index',
+ 'known' => 'known_value'));
+
+ $this->assertEquals('/hello/world',
+ $m->generate(array('controller' => 'content', 'action' => 'index',
+ 'known' => 'known_value')));
+ $this->assertEquals('/hello/world?extra=hi',
+ $m->generate(array('controller' => 'content', 'action' => 'index',
+ 'known' => 'known_value', 'extra' => 'hi')));
+ $this->assertNull($m->generate(array('known' => 'foo')));
+ }
+
+ public function testTypical()
+ {
+ foreach (array(':controller/:action/:id', ':(controller)/:(action)/:(id)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('action' => 'index', 'id' => null));
+
+ $this->assertEquals('/content',
+ $m->generate(array('controller' => 'content', 'action' => 'index')));
+ $this->assertEquals('/content/list',
+ $m->generate(array('controller' => 'content', 'action' => 'list')));
+ $this->assertEquals('/content/show/10',
+ $m->generate(array('controller' => 'content', 'action' => 'show', 'id' => 10)));
+
+ $this->assertEquals('/admin/user',
+ $m->generate(array('controller' => 'admin/user', 'action' => 'index')));
+ $this->assertEquals('/admin/user',
+ $m->generate(array('controller' => 'admin/user')));
+ $this->assertEquals('/admin/user/show/10',
+ $m->generate(array('controller' => 'admin/user', 'action' => 'show', 'id' => 10)));
+
+ $this->assertEquals('/content', $m->generate(array('controller' => 'content')));
+ }
+ }
+
+ public function testRouteWithFixnumDefault()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('page/:id', array('controller' => 'content', 'action' => 'show_page', 'id' => 1));
+
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page')));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 1)));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => '1')));
+ $this->assertEquals('/page/10',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 10)));
+ $this->assertEquals('/blog/show/4',
+ $m->generate(array('controller' => 'blog', 'action' => 'show', 'id' => 4)));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page')));
+ $this->assertEquals('/page/4',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 4)));
+ $this->assertEquals('/content/show',
+ $m->generate(array('controller' => 'content', 'action' => 'show')));
+ }
+
+ public function testRouteWithFixnumDefaultWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('page/:(id)', array('controller' => 'content', 'action' => 'show_page', 'id' => 1));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page')));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 1)));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => '1')));
+ $this->assertEquals('/page/10',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 10)));
+ $this->assertEquals('/blog/show/4',
+ $m->generate(array('controller' => 'blog', 'action' => 'show', 'id' => 4)));
+ $this->assertEquals('/page',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page')));
+ $this->assertEquals('/page/4',
+ $m->generate(array('controller' => 'content', 'action' => 'show_page', 'id' => 4)));
+ $this->assertEquals('/content/show',
+ $m->generate(array('controller' => 'content', 'action' => 'show')));
+ }
+
+ public function testUppercaseRecognition()
+ {
+ foreach (array(':controller/:action/:id', ':(controller)/:(action)/:(id)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+
+ $this->assertEquals('/Content',
+ $m->generate(array('controller' => 'Content', 'action' => 'index')));
+ $this->assertEquals('/Content/list',
+ $m->generate(array('controller' => 'Content', 'action' => 'list')));
+ $this->assertEquals('/Content/show/10',
+ $m->generate(array('controller' => 'Content', 'action' => 'show', 'id' => '10')));
+
+ $this->assertEquals('/Admin/NewsFeed',
+ $m->generate(array('controller' => 'Admin/NewsFeed', 'action' => 'index')));
+ }
+ }
+
+ public function testBackwards()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('page/:id/:action', array('controller' => 'pages', 'action' => 'show'));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/page/20',
+ $m->generate(array('controller' => 'pages', 'action' => 'show', 'id' => 20)));
+ $this->assertEquals('/pages/boo',
+ $m->generate(array('controller' => 'pages', 'action' => 'boo')));
+ }
+
+ public function testBackwardsWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('page/:(id)/:(action)', array('controller' => 'pages', 'action' => 'show'));
+ $m->connect(':(controller)/:(action)/:(id)');
+
+ $this->assertEquals('/page/20',
+ $m->generate(array('controller' => 'pages', 'action' => 'show', 'id' => 20)));
+ $this->assertEquals('/pages/boo',
+ $m->generate(array('controller' => 'pages', 'action' => 'boo')));
+ }
+
+ public function testBothRequirementAndOptional()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('test/:year', array('controller' => 'post', 'action' => 'show',
+ 'year' => null, 'requirements' => array('year' => '\d{4}')));
+
+ $this->assertEquals('/test',
+ $m->generate(array('controller' => 'post', 'action' => 'show')));
+ $this->assertEquals('/test',
+ $m->generate(array('controller' => 'post', 'action' => 'show', 'year' => null)));
+ }
+
+ public function testSetToNilForgets()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('pages/:year/:month/:day',
+ array('controller' => 'content', 'action' => 'list_pages',
+ 'month' => null, 'day' => null));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/pages/2005',
+ $m->generate(array('controller' => 'content', 'action' => 'list_pages',
+ 'year' => 2005)));
+ $this->assertEquals('/pages/2005/6',
+ $m->generate(array('controller' => 'content', 'action' => 'list_pages',
+ 'year' => 2005, 'month' => 6)));
+ $this->assertEquals('/pages/2005/6/12',
+ $m->generate(array('controller' => 'content', 'action' => 'list_pages',
+ 'year' => 2005, 'month' => 6, 'day' => 12)));
+ }
+
+ public function testUrlWithNoActionSpecified()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('', array('controller' => 'content'));
+ $m->connect(':controller/:action/:id');
+
+ $this->assertEquals('/',
+ $m->generate(array('controller' => 'content', 'action' => 'index')));
+ $this->assertEquals('/',
+ $m->generate(array('controller' => 'content')));
+ }
+
+ public function testUrlWithPrefix()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->prefix = '/blog';
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/blog/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/blog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/blog/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+ }
+
+ public function testUrlWithPrefixDeeper()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->prefix = '/blog/phil';
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/blog/phil/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/blog/phil/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/blog/phil/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+ }
+
+ public function testUrlWithEnvironEmpty()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '');
+
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+ }
+
+ public function testUrlWithEnviron()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '/blog');
+
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/blog/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/blog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/blog/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+
+ $m->environ['SCRIPT_NAME'] = '/notblog';
+
+ $this->assertEquals('/notblog/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/notblog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/notblog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/notblog/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+ }
+
+ public function testUrlWithEnvironAndAbsolute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '/blog');
+
+ $utils = $m->utils;
+
+ $m->connect('image', 'image/:name', array('_absolute' => true));
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/blog/content/view',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/blog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/blog/content',
+ $m->generate(array('controller' => 'content')));
+ $this->assertEquals('/blog/admin/comments',
+ $m->generate(array('controller' => 'admin/comments')));
+ $this->assertEquals('/image/topnav.jpg',
+ $utils->urlFor('image', array('name' => 'topnav.jpg')));
+ }
+
+ public function testRouteWithOddLeftovers()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array();
+
+ $m->connect(':controller/:(action)-:(id)');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/content/view-',
+ $m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index-',
+ $m->generate(array('controller' => 'content')));
+ }
+
+ public function testRouteWithEndExtension()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array();
+
+ $m->connect(':controller/:(action)-:(id).html');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertNull($m->generate(array('controller' => 'content', 'action' => 'view')));
+ $this->assertNull($m->generate(array('controller' => 'content')));
+
+ $this->assertEquals('/content/view-3.html',
+ $m->generate(array('controller' => 'content', 'action' => 'view', 'id' => 3)));
+ $this->assertEquals('/content/index-2.html',
+ $m->generate(array('controller' => 'content', 'id' => 2)));
+ }
+
+ public function testResources()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array();
+
+ $utils = $m->utils;
+
+ $m->resource('message', 'messages');
+ $m->createRegs(array('messages'));
+ $options = array('controller' => 'messages');
+
+ $this->assertEquals('/messages',
+ $utils->urlFor('messages'));
+ $this->assertEquals('/messages.xml',
+ $utils->urlFor('messages', array('format' => 'xml')));
+ $this->assertEquals('/messages/1',
+ $utils->urlFor('message', array('id' => 1)));
+ $this->assertEquals('/messages/1.xml',
+ $utils->urlFor('message', array('id' => 1, 'format' => 'xml')));
+ $this->assertEquals('/messages/new',
+ $utils->urlFor('new_message'));
+ $this->assertEquals('/messages/1.xml',
+ $utils->urlFor('message', array('id' => 1, 'format' => 'xml')));
+ $this->assertEquals('/messages/1/edit',
+ $utils->urlFor('edit_message', array('id' => 1)));
+ $this->assertEquals('/messages/1/edit.xml',
+ $utils->urlFor('edit_message', array('id' => 1, 'format' => 'xml')));
+
+ $this->assertRestfulRoutes($m, $options);
+ }
+
+ public function testResourcesWithPathPrefix()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages', array('pathPrefix' => '/thread/:threadid'));
+ $m->createRegs(array('messages'));
+ $options = array('controller' => 'messages', 'threadid' => '5');
+ $this->assertRestfulRoutes($m, $options, 'thread/5/');
+ }
+
+ public function testResourcesWithCollectionAction()
+ {
+ $m = new Horde_Routes_Mapper();
+ $utils = $m->utils;
+ $m->resource('message', 'messages', array('collection' => array('rss' => 'GET')));
+ $m->createRegs(array('messages'));
+ $options = array('controller' => 'messages');
+ $this->assertRestfulRoutes($m, $options);
+
+ $this->assertEquals('/messages/rss',
+ $m->generate(array('controller' => 'messages', 'action' => 'rss')));
+ $this->assertEquals('/messages/rss',
+ $utils->urlFor('rss_messages'));
+ $this->assertEquals('/messages/rss.xml',
+ $m->generate(array('controller' => 'messages', 'action' => 'rss',
+ 'format' => 'xml')));
+ $this->assertEquals('/messages/rss.xml',
+ $utils->urlFor('formatted_rss_messages', array('format' => 'xml')));
+ }
+
+ public function testResourcesWithMemberAction()
+ {
+ foreach (array('put', 'post') as $method) {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages', array('member' => array('mark' => $method)));
+ $m->createRegs(array('messages'));
+
+ $options = array('controller' => 'messages');
+ $this->assertRestfulRoutes($m, $options);
+
+ $connectkargs = array('method' => $method, 'action' => 'mark', 'id' => '1');
+ $this->assertEquals('/messages/1/mark',
+ $m->generate(array_merge($connectkargs, $options)));
+
+ $connectkargs = array('method' => $method, 'action' => 'mark',
+ 'id' => '1', 'format' => 'xml');
+ $this->assertEquals('/messages/1/mark.xml',
+ $m->generate(array_merge($connectkargs, $options)));
+ }
+ }
+
+ public function testResourcesWithNewAction()
+ {
+ $m = new Horde_Routes_Mapper();
+ $utils = $m->utils;
+ $m->resource('message', 'messages/', array('new' => array('preview' => 'POST')));
+ $m->createRegs(array('messages'));
+ $this->assertRestfulRoutes($m, array('controller' => 'messages'));
+
+ $this->assertEquals('/messages/new/preview',
+ $m->generate(array('controller' => 'messages', 'action' => 'preview',
+ 'method' => 'post')));
+ $this->assertEquals('/messages/new/preview',
+ $utils->urlFor('preview_new_message'));
+ $this->assertEquals('/messages/new/preview.xml',
+ $m->generate(array('controller' => 'messages', 'action' => 'preview',
+ 'method' => 'post', 'format' => 'xml')));
+ $this->assertEquals('/messages/new/preview.xml',
+ $utils->urlFor('preview_new_message', array('format' => 'xml')));
+ }
+
+ public function testResourcesWithNamePrefix()
+ {
+ $m = new Horde_Routes_Mapper();
+ $utils = $m->utils;
+ $m->resource('message', 'messages', array('namePrefix' => 'category_',
+ 'new' => array('preview' => 'POST')));
+ $m->createRegs(array('messages'));
+ $options = array('controller' => 'messages');
+ $this->assertRestfulRoutes($m, $options);
+
+ $this->assertEquals('/messages/new/preview',
+ $utils->urlFor('category_preview_new_message'));
+
+ $this->assertNull($utils->urlFor('category_preview_new_message', array('method' => 'get')));
+ }
+
+ public function testUnicode()
+ {
+ // php version does not handing decoding
+ }
+
+ public function testUnicodeStatic()
+ {
+ // php version does not handing decoding
+ }
+
+ public function testOtherSpecialChars()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('/:year/:(slug).:(format),:(locale)', array('locale' => 'en', 'format' => 'html'));
+ $m->createRegs(array('content'));
+
+ $this->assertEquals('/2007/test',
+ $m->generate(array('year' => 2007, 'slug' => 'test')));
+ $this->assertEquals('/2007/test.xml',
+ $m->generate(array('year' => 2007, 'slug' => 'test', 'format' => 'xml')));
+ $this->assertEquals('/2007/test.xml,ja',
+ $m->generate(array('year' => 2007, 'slug' => 'test', 'format' => 'xml',
+ 'locale' => 'ja')));
+ $this->assertNull($m->generate(array('year' => 2007, 'format' => 'html')));
+ }
+
+ // Test Helpers
+
+ public function assertRestfulRoutes($m, $options, $pathPrefix = '')
+ {
+ $baseroute = '/' . $pathPrefix . $options['controller'];
+
+ $this->assertEquals($baseroute,
+ $m->generate(array_merge($options, array('action' => 'index'))));
+
+ $this->assertEquals($baseroute . '.xml',
+ $m->generate(array_merge($options, array('action' => 'index',
+ 'format' => 'xml'))));
+ $this->assertEquals($baseroute . '/new',
+ $m->generate(array_merge($options, array('action' => 'new'))));
+ $this->assertEquals($baseroute . '/1',
+ $m->generate(array_merge($options, array('action' => 'show',
+ 'id' => '1'))));
+ $this->assertEquals($baseroute . '/1/edit',
+ $m->generate(array_merge($options, array('action' => 'edit',
+ 'id' => '1'))));
+ $this->assertEquals($baseroute . '/1.xml',
+ $m->generate(array_merge($options, array('action' => 'show',
+ 'id' => '1',
+ 'format' => 'xml'))));
+ $this->assertEquals($baseroute,
+ $m->generate(array_merge($options, array('action' => 'create',
+ 'method' => 'post'))));
+
+ $this->assertEquals($baseroute . '/1',
+ $m->generate(array_merge($options, array('action' => 'update',
+ 'method' => 'put',
+ 'id' => '1'))));
+ $this->assertEquals($baseroute . '/1',
+ $m->generate(array_merge($options, array('action' => 'delete',
+ 'method' => 'delete',
+ 'id' => '1'))));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * @package Horde_Routes
+ */
+class RecognitionTest extends PHPUnit_Framework_TestCase
+{
+ public function testRegexpCharEscaping()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:(action).:(id)');
+ $m->createRegs(array('content'));
+
+ $this->assertNull($m->match('/content/view#2'));
+
+ $matchdata = array('action' => 'view', 'controller' => 'content', 'id' => '2');
+ $this->assertEquals($matchdata, $m->match('/content/view.2'));
+
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'find.all'));
+
+ $matchdata = array('action' => 'view#2', 'controller' => 'content', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/content/view#2'));
+
+ $matchdata = array('action' => 'view', 'controller' => 'find.all', 'id'=> null);
+ $this->assertEquals($matchdata, $m->match('/find.all/view'));
+
+ $this->assertNull($m->match('/findzall/view'));
+ }
+
+ public function testAllStatic()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('hello/world/how/are/you', array('controller' => 'content', 'action' => 'index'));
+ $m->createRegs(array());
+
+ $this->assertNull($m->match('/x'));
+ $this->assertNull($m->match('/hello/world/how'));
+ $this->assertNull($m->match('/hello/world/how/are'));
+ $this->assertNull($m->match('/hello/world/how/are/you/today'));
+
+ $matchdata = array('controller' => 'content', 'action' =>'index');
+ $this->assertEquals($matchdata, $m->match('/hello/world/how/are/you'));
+ }
+
+ public function testUnicode()
+ {
+ // php version does not handing decoding
+ }
+
+ public function testDisablingUnicode()
+ {
+ // php version does not handing decoding
+ }
+
+ public function testBasicDynamic()
+ {
+ foreach(array('hi/:name', 'hi/:(name)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+ $this->assertNull($m->match('/hi/dude/what'));
+
+ $matchdata = array('controller' => 'content', 'name' => 'dude', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/dude'));
+ $this->assertEquals($matchdata, $m->match('/hi/dude/'));
+ }
+ }
+
+ public function testBasicDynamicBackwards()
+ {
+ foreach (array(':name/hi', ':(name)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+ $m->createRegs();
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/hi'));
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/shop/walmart/hi'));
+
+ $matchdata = array('name' => 'fred', 'action' => 'index', 'controller' => 'content');
+ $this->assertEquals($matchdata, $m->match('/fred/hi'));
+
+ $matchdata = array('name' => 'index', 'action' => 'index', 'controller' => 'content');
+ $this->assertEquals($matchdata, $m->match('/index/hi'));
+ }
+ }
+
+ public function testDynamicWithUnderscores()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('article/:small_page', array('small_page' => false));
+ $m->connect(':(controller)/:(action)/:(id)');
+ $m->createRegs(array('article', 'blog'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => '0');
+ $this->assertEquals($matchdata, $m->match('/blog/view/0'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/view'));
+ }
+
+ public function testDynamicWithDefault()
+ {
+ foreach (array('hi/:action', 'hi/:(action)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi/dude/what'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/index'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/hi/dude'));
+ }
+ }
+
+ public function testDynamicWithDefaultBackwards()
+ {
+ foreach (array(':action/hi', ':(action)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/index/hi'));
+ $this->assertEquals($matchdata, $m->match('/index/hi/'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/hi'));
+ }
+ }
+
+ public function testDynamicWithStringCondition()
+ {
+ foreach (array(':name/hi', ':(name)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content',
+ 'requirements' => array('name' => 'index')));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+ $this->assertNull($m->match('/dude/what/hi'));
+
+ $matchdata = array('controller' => 'content', 'name' => 'index', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/index/hi'));
+ $this->assertNull($m->match('/dude/hi'));
+ }
+ }
+
+ public function testDynamicWithStringConditionBackwards()
+ {
+ foreach (array('hi/:name', 'hi/:(name)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content',
+ 'requirements' => array('name' => 'index')));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+ $this->assertNull($m->match('/hi/dude/what'));
+
+ $matchdata = array('controller' => 'content', 'name' => 'index', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/index'));
+
+ $this->assertEquals($matchdata, $m->match('/hi/index'));
+
+ $this->assertNull($m->match('/dude/hi'));
+ }
+ }
+
+ public function testDynamicWithRegexpCondition()
+ {
+ foreach (array('hi/:name', 'hi/:(name)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content',
+ 'requirements' => array('name' => '[a-z]+')));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+ $this->assertNull($m->match('/hi/FOXY'));
+ $this->assertNull($m->match('/hi/138708jkhdf'));
+ $this->assertNull($m->match('/hi/dkjfl8792343dfsf'));
+ $this->assertNull($m->match('/hi/dude/what'));
+ $this->assertNull($m->match('/hi/dude/what/'));
+
+ $matchdata = array('controller' => 'content', 'name' => 'index', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/index'));
+
+ $matchdata = array('controller' => 'content', 'name' => 'dude', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/dude'));
+ }
+ }
+
+ public function testDynamicWithRegexpAndDefault()
+ {
+ foreach (array('hi/:action', 'hi/:(action)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content',
+ 'requirements' => array('action' => '[a-z]+')));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi/FOXY'));
+ $this->assertNull($m->match('/hi/138708jkhdf'));
+ $this->assertNull($m->match('/hi/dkjfl8792343dfsf'));
+ $this->assertNull($m->match('/hi/dude/what/'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi'));
+ $this->assertEquals($matchdata, $m->match('/hi/index'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/hi/dude'));
+ }
+ }
+
+ public function testDynamicWithDefaultAndStringConditionBackwards()
+ {
+ foreach (array(':action/hi', ':(action)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+ $m->createRegs();
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+
+ $matchdata = array('action' => 'index', 'controller' => 'content');
+ $this->assertEquals($matchdata, $m->match('/index/hi'));
+ }
+ }
+
+ public function testDynamicAndControllerWithStringAndDefaultBackwards()
+ {
+ foreach (array(':controller/:action/hi', ':(controller)/:(action)/hi') as $path) {
+ $m = new Horde_Routes_Mapper();
+
+ $m->connect($path, array('controller' => 'content'));
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/fred'));
+ }
+ }
+
+ public function testMultiroute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:year/:month/:day', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:id', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('post','blog','admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/archive'));
+ $this->assertNull($m->match('/archive/2004/ab'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/view'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null, 'year' => '2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => '4', 'day' => null, 'year' =>'2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004/4'));
+ }
+
+ public function testMultirouteWithSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:(year)/:(month)/:(day)', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}',
+ 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:(id)', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':(controller)/:(action)/:(id)');
+ $m->createRegs(array('post','blog','admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/archive'));
+ $this->assertNull($m->match('/archive/2004/ab'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/view'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null, 'year' => '2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => '4', 'day' => null, 'year' => '2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004/4'));
+ }
+
+ public function testDynamicWithRegexpDefaultsAndGaps()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:year/:month/:day', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}')));
+ $m->connect('view/:id/:controller', array('controller' => 'blog', 'action' => 'view',
+ 'id' => 2, 'requirements' => array('id' => '\d{1,2}')));
+ $m->createRegs(array('post','blog','admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/archive'));
+ $this->assertNull($m->match('/archive/2004/haha'));
+ $this->assertNull($m->match('/view/blog'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => '2');
+ $this->assertEquals($matchdata, $m->match('/view'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null, 'year' => '2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004'));
+ }
+
+ public function testDynamicWithRegexpDefaultsAndGapsAndSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archive/:(year)/:(month)/:(day)', array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}')));
+ $m->connect('view/:(id)/:(controller)', array('controller' => 'blog', 'action' => 'view',
+ 'id' => 2, 'requirements' => array('id' => '\d{1,2}')));
+ $m->createRegs(array('post','blog','admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/archive'));
+ $this->assertNull($m->match('/archive/2004/haha'));
+ $this->assertNull($m->match('/view/blog'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => '2');
+ $this->assertEquals($matchdata, $m->match('/view'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view',
+ 'month' => null, 'day' => null, 'year' => '2004');
+ $this->assertEquals($matchdata, $m->match('/archive/2004'));
+ }
+
+ public function testDynamicWithRegexpGapsControllers()
+ {
+ foreach(array('view/:id/:controller', 'view/:(id)/:(controller)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('id' => 2, 'action' => 'view', 'requirements' => array('id' => '\d{1,2}')));
+ $m->createRegs(array('post', 'blog', 'admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/view'));
+ $this->assertNull($m->match('/view/blog'));
+ $this->assertNull($m->match('/view/3'));
+ $this->assertNull($m->match('/view/4/honker'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => '2');
+ $this->assertEquals($matchdata, $m->match('/view/2/blog'));
+ }
+ }
+
+ public function testDynamicWithTrailingStrings()
+ {
+ foreach (array('view/:id/:controller/super', 'view/:(id)/:(controller)/super') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'blog', 'action' => 'view',
+ 'id' => 2, 'requirements' => array('id' => '\d{1,2}')));
+ $m->createRegs(array('post', 'blog', 'admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/view'));
+ $this->assertNull($m->match('/view/blah/blog/super'));
+ $this->assertNull($m->match('/view/ha/super'));
+ $this->assertNull($m->match('/view/super'));
+ $this->assertNull($m->match('/view/4/super'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'id' => '2');
+ $this->assertEquals($matchdata, $m->match('/view/2/blog/super'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'id' => '4');
+ $this->assertEquals($matchdata, $m->match('/view/4/admin/user/super'));
+ }
+ }
+
+ public function testDynamicWithTrailingNonKeywordStrings()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('somewhere/:over/rainbow', array('controller' => 'blog'));
+ $m->connect('somewhere/:over', array('controller' => 'post'));
+ $m->createRegs(array('post', 'blog', 'admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/somewhere'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'index', 'over' => 'near');
+ $this->assertEquals($matchdata, $m->match('/somewhere/near/rainbow'));
+
+ $matchdata = array('controller' => 'post', 'action' => 'index', 'over' => 'tomorrow');
+ $this->assertEquals($matchdata, $m->match('/somewhere/tomorrow'));
+ }
+
+ public function testDynamicWithTrailingDynamicDefaults()
+ {
+ foreach (array('archives/:action/:article', 'archives/:(action)/:(article)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'blog'));
+ $m->createRegs(array('blog'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/archives'));
+ $this->assertNull($m->match('/archives/introduction'));
+ $this->assertNull($m->match('/archives/sample'));
+ $this->assertNull($m->match('/view/super'));
+ $this->assertNull($m->match('/view/4/super'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'view', 'article' => 'introduction');
+ $this->assertEquals($matchdata, $m->match('/archives/view/introduction'));
+
+ $matchdata = array('controller' => 'blog', 'action' => 'edit', 'article' => 'recipes');
+ $this->assertEquals($matchdata, $m->match('/archives/edit/recipes'));
+ }
+ }
+
+ public function testPath()
+ {
+ foreach (array('hi/*file', 'hi/*(file)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content', 'action' => 'download'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download',
+ 'file' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/hi/books/learning_python.pdf'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download',
+ 'file' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/hi/dude'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download',
+ 'file' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/hi/dude/what'));
+ }
+ }
+
+ public function testDynamicWithPath()
+ {
+ foreach (array(':controller/:action/*url', ':(controller)/:(action)/*(url)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path);
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/'));
+ $this->assertNull($m->match('/blog'));
+ $this->assertNull($m->match('/content'));
+ $this->assertNull($m->match('/content/view'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view', 'url' => 'blob');
+ $this->assertEquals($matchdata, $m->match('/content/view/blob'));
+
+ $this->assertNull($m->match('/admin/user'));
+ $this->assertNull($m->match('/admin/user/view'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view',
+ 'url' => 'blob/check');
+ $this->assertEquals($matchdata, $m->match('/admin/user/view/blob/check'));
+ }
+ }
+
+ public function testPathWithDynamicAndDefault()
+ {
+ foreach (array(':controller/:action/*url', ':(controller)/:(action)/*(url)') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content', 'action' => 'view', 'url' => null));
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/goober/view/here'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view', 'url' => null);
+ $this->assertEquals($matchdata, $m->match('/content'));
+ $this->assertEquals($matchdata, $m->match('/content/'));
+ $this->assertEquals($matchdata, $m->match('/content/view'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view', 'url' => 'fred');
+ $this->assertEquals($matchdata, $m->match('/content/view/fred'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'url' => null);
+ $this->assertEquals($matchdata, $m->match('/admin/user'));
+ $this->assertEquals($matchdata, $m->match('/admin/user/view'));
+ }
+ }
+
+ public function testPathWithDynamicAndDefaultBackwards()
+ {
+ foreach (array('*file/login', '*(file)/login') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content', 'action' => 'download', 'file' => null));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download', 'file' => '');
+ $this->assertEquals($matchdata, $m->match('//login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download',
+ 'file' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download', 'file' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download', 'file' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/login'));
+ }
+ }
+
+ public function testPathBackwards()
+ {
+ foreach (array('*file/login', '*(file)/login') as $path) {
+ $m = new Horde_Routes_Mapper();
+ $m->connect($path, array('controller' => 'content', 'action' => 'download'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download',
+ 'file' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download', 'file' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'download', 'file' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/login'));
+ }
+ }
+
+ public function testPathBackwardsWithController()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('*url/login', array('controller' => 'content', 'action' => 'check_access'));
+ $m->connect('*url/:controller', array('action' => 'view'));
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access',
+ 'url' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access', 'url' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access', 'url' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/login'));
+
+ $this->assertNull($m->match('/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view',
+ 'url' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'url' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'url' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/admin/user'));
+ }
+
+ public function testPathBackwardsWithControllerAndSplits()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('*(url)/login', array('controller' => 'content', 'action' => 'check_access'));
+ $m->connect('*(url)/:(controller)', array('action' => 'view'));
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access',
+ 'url' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access', 'url' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/login'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'check_access', 'url' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/login'));
+
+ $this->assertNull($m->match('/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view',
+ 'url' => 'books/learning_python.pdf');
+ $this->assertEquals($matchdata, $m->match('/books/learning_python.pdf/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'url' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/dude/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view', 'url' => 'dude/what');
+ $this->assertEquals($matchdata, $m->match('/dude/what/admin/user'));
+ }
+
+ public function testController()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('hi/:controller', array('action' => 'hi'));
+ $m->createRegs(array('content', 'admin/user'));
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi/13870948'));
+ $this->assertNull($m->match('/hi/content/dog'));
+ $this->assertNull($m->match('/hi/admin/user/foo'));
+ $this->assertNull($m->match('/hi/admin/user/foo/'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'hi');
+ $this->assertEquals($matchdata, $m->match('/hi/content'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'hi');
+ $this->assertEquals($matchdata, $m->match('/hi/admin/user'));
+ }
+
+ public function testStandardRoute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'admin/user'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/content'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'list', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/content/list'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'show', 'id' => '10');
+ $this->assertEquals($matchdata, $m->match('/content/show/10'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/admin/user'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'list', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/admin/user/list'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'show', 'id' => 'bbangert');
+ $this->assertEquals($matchdata, $m->match('/admin/user/show/bbangert'));
+
+ $this->assertNull($m->match('/content/show/10/20'));
+ $this->assertNull($m->match('/food'));
+ }
+
+ public function testStandardRouteWithGaps()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:(id).py');
+ $m->createRegs(array('content', 'admin/user'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index', 'id' => 'None');
+ $this->assertEquals($matchdata, $m->match('/content/index/None.py'));
+
+ $matchdata = array('controller' =>'content', 'action' => 'list', 'id' => 'None');
+ $this->assertEquals($matchdata, $m->match('/content/list/None.py'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'show', 'id' => '10');
+ $this->assertEquals($matchdata, $m->match('/content/show/10.py'));
+ }
+
+ public function testStandardRouteWithGapsAndDomains()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('manage/:domain.:ext', array('controller' => 'admin/user', 'action' => 'view',
+ 'ext' => 'html'));
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'admin/user'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index', 'id' => 'None.py');
+ $this->assertEquals($matchdata, $m->match('/content/index/None.py'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'list', 'id' => 'None.py');
+ $this->assertEquals($matchdata, $m->match('/content/list/None.py'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'show', 'id' => '10.py');
+ $this->assertEquals($matchdata, $m->match('/content/show/10.py'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'show.all', 'id' => '10.py');
+ $this->assertEquals($matchdata, $m->match('/content/show.all/10.py'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'show', 'id' => 'www.groovie.org');
+ $this->assertEquals($matchdata, $m->match('/content/show/www.groovie.org'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view',
+ 'ext' => 'html', 'domain' => 'groovie');
+ $this->assertEquals($matchdata, $m->match('/manage/groovie'));
+
+ $matchdata = array('controller' => 'admin/user', 'action' => 'view',
+ 'ext' => 'xml', 'domain' => 'groovie');
+ $this->assertEquals($matchdata, $m->match('/manage/groovie.xml'));
+ }
+
+ public function testStandardWithDomains()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('manage/:domain', array('controller' => 'domains', 'action' => 'view'));
+ $m->createRegs(array('domains'));
+
+ $matchdata = array('controller' => 'domains', 'action' => 'view', 'domain' => 'www.groovie.org');
+ $this->assertEquals($matchdata, $m->match('/manage/www.groovie.org'));
+ }
+
+ public function testDefaultRoute()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('', array('controller' => 'content', 'action' => 'index'));
+ $m->createRegs(array('content'));
+
+ $this->assertNull($m->match('/x'));
+ $this->assertNull($m->match('/hello/world'));
+ $this->assertNull($m->match('/hello/world/how/are'));
+ $this->assertNull($m->match('/hello/world/how/are/you/today'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/'));
+ }
+
+ public function testDynamicWithPrefix()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->prefix = '/blog';
+ $m->connect(':controller/:action/:id');
+ $m->connect('', array('controller' => 'content', 'action' => 'index'));
+ $m->createRegs(array('content', 'archive', 'admin/comments'));
+
+ $this->assertNull($m->match('/x'));
+ $this->assertNull($m->match('/admin/comments'));
+ $this->assertNull($m->match('/content/view'));
+ $this->assertNull($m->match('/archive/view/4'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/blog'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/content'));
+
+ $matchdata = array('controller' => 'admin/comments', 'action' => 'view', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/admin/comments/view'));
+
+ $matchdata = array('controller' => 'archive', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/archive'));
+
+ $matchdata = array('controller' => 'archive', 'action' => 'view', 'id' => '4');
+ $this->assertEquals($matchdata, $m->match('/blog/archive/view/4'));
+ }
+
+ public function testDynamicWithMultipleAndPrefix()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->prefix = '/blog';
+ $m->connect(':controller/:action/:id');
+ $m->connect('home/:action', array('controller' => 'archive'));
+ $m->connect('', array('controller' => 'content'));
+ $m->createRegs(array('content', 'archive', 'admin/comments'));
+
+ $this->assertNull($m->match('/x'));
+ $this->assertNull($m->match('/admin/comments'));
+ $this->assertNull($m->match('/content/view'));
+ $this->assertNull($m->match('/archive/view/4'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/blog/'));
+
+ $matchdata = array('controller' => 'archive', 'action' => 'view');
+ $this->assertEquals($matchdata, $m->match('/blog/home/view'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/content'));
+
+ $matchdata = array('controller' => 'admin/comments', 'action' => 'view', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/admin/comments/view'));
+
+ $matchdata = array('controller' => 'archive', 'action' => 'index', 'id' => null);
+ $this->assertEquals($matchdata, $m->match('/blog/archive'));
+
+ $matchdata = array('controller' => 'archive', 'action' => 'view', 'id' => '4');
+ $this->assertEquals($matchdata, $m->match('/blog/archive/view/4'));
+ }
+
+ public function testSplitsWithExtension()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('hi/:(action).html', array('controller' => 'content'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/boo/blah'));
+ $this->assertNull($m->match('/hi/dude/what'));
+ $this->assertNull($m->match('/hi'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'index');
+ $this->assertEquals($matchdata, $m->match('/hi/index.html'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'dude');
+ $this->assertEquals($matchdata, $m->match('/hi/dude.html'));
+ }
+
+ public function testSplitsWithDashes()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archives/:(year)-:(month)-:(day).html',
+ array('controller' => 'archives', 'action' => 'view'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/archives'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '2004', 'month' => '12', 'day' => '4');
+ $this->assertEquals($matchdata, $m->match('/archives/2004-12-4.html'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '04', 'month' => '10', 'day' => '4');
+ $this->assertEquals($matchdata, $m->match('/archives/04-10-4.html'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '04', 'month' => '1', 'day' => '1');
+ $this->assertEquals($matchdata, $m->match('/archives/04-1-1.html'));
+ }
+
+ public function testSplitsPackedWithRegexps()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('archives/:(year):(month):(day).html',
+ array('controller' => 'archives', 'action' => 'view',
+ 'requirements' => array('year' => '\d{4}', 'month' => '\d{2}',
+ 'day' => '\d{2}')));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/boo'));
+ $this->assertNull($m->match('/archives'));
+ $this->assertNull($m->match('/archives/2004020.html'));
+ $this->assertNull($m->match('/archives/200502.html'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '2004', 'month' => '12', 'day' => '04');
+ $this->assertEquals($matchdata, $m->match('/archives/20041204.html'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '2005', 'month' => '10', 'day' => '04');
+ $this->assertEquals($matchdata, $m->match('/archives/20051004.html'));
+
+ $matchdata = array('controller' => 'archives', 'action' => 'view',
+ 'year' => '2006', 'month' => '01', 'day' => '01');
+ $this->assertEquals($matchdata, $m->match('/archives/20060101.html'));
+ }
+
+ public function testSplitsWithSlashes()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':name/:(action)-:(day)', array('controller' => 'content'));
+
+ $this->assertNull($m->match('/something'));
+ $this->assertNull($m->match('/something/is-'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view',
+ 'day' => '3', 'name' => 'group');
+ $this->assertEquals($matchdata, $m->match('/group/view-3'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view',
+ 'day' => '5', 'name' => 'group');
+ $this->assertEquals($matchdata, $m->match('/group/view-5'));
+ }
+
+ public function testSplitsWithSlashesAndDefault()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':name/:(action)-:(id)', array('controller' => 'content'));
+ $m->createRegs();
+
+ $this->assertNull($m->match('/something'));
+ $this->assertNull($m->match('/something/is'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view',
+ 'id' => '3', 'name' => 'group');
+ $this->assertEquals($matchdata, $m->match('/group/view-3'));
+
+ $matchdata = array('controller' => 'content', 'action' => 'view',
+ 'id' => null, 'name' => 'group');
+ $this->assertEquals($matchdata, $m->match('/group/view-'));
+ }
+
+ public function testNoRegMake()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':name/:(action)-:(id)', array('controller' => 'content'));
+ $m->controllerScan = false;
+ try {
+ $m->match('/group/view-3');
+ $this->fail();
+ } catch (Horde_Routes_Exception $e) {
+ $this->assertRegExp('/must generate the regular expressions/i', $e->getMessage());
+ }
+ }
+
+ public function testRoutematch()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content'));
+ $route = $m->matchList[0];
+
+ list($resultdict, $resultObj) = $m->routematch('/content');
+
+ $this->assertEquals(array('controller' => 'content', 'action' => 'index', 'id' => null),
+ $resultdict);
+ $this->assertSame($route, $resultObj);
+ $this->assertNull($m->routematch('/nowhere'));
+ }
+
+ public function testRoutematchDebug()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect(':controller/:action/:id');
+ $m->debug = true;
+ $m->createRegs(array('content'));
+ $route = $m->matchList[0];
+
+ list($resultdict, $resultObj, $debug) = $m->routematch('/content');
+
+ $this->assertEquals(array('controller' => 'content', 'action' => 'index', 'id' => null),
+ $resultdict);
+ $this->assertSame($route, $resultObj);
+
+ list($resultdict, $resultObj, $debug) = $m->routematch('/nowhere');
+ $this->assertNull($resultdict);
+ $this->assertNull($resultObj);
+ $this->assertEquals(1, count($debug));
+ }
+
+ public function testMatchDebug()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->connect('nowhere', 'http://nowhere.com/', array('_static' => true));
+ $m->connect(':controller/:action/:id');
+ $m->debug = true;
+ $m->createRegs(array('content'));
+ $route = $m->matchList[1];
+
+ list($resultdict, $resultObj, $debug) = $m->match('/content');
+ $this->assertEquals(array('controller' => 'content', 'action' => 'index', 'id' => null),
+ $resultdict);
+
+ $this->assertSame($route, $resultObj);
+
+ list($resultdict, $resultObj, $debug) = $m->match('/nowhere');
+ $this->assertNull($resultdict);
+ $this->assertNull($resultObj);
+ $this->assertEquals(2, count($debug));
+ }
+
+ public function testResourceCollection()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages');
+ $m->createRegs(array('messages'));
+
+ $path = '/messages';
+
+ $m->environ = array('REQUEST_METHOD' => 'GET');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'index'),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'POST');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'create'),
+ $m->match($path));
+ }
+
+ public function testFormattedResourceCollection()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages');
+ $m->createRegs(array('messages'));
+
+ $path = '/messages.xml';
+
+ $m->environ = array('REQUEST_METHOD' => 'GET');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'index',
+ 'format' => 'xml'),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'POST');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'create',
+ 'format' => 'xml'),
+ $m->match($path));
+ }
+
+ public function testResourceMember()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages');
+ $m->createRegs(array('messages'));
+
+ $path = '/messages/42';
+
+ $m->environ = array('REQUEST_METHOD' => 'GET');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'show',
+ 'id' => 42),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'POST');
+ $this->assertNull($m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'PUT');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'update',
+ 'id' => 42),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'DELETE');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'delete',
+ 'id' => 42),
+ $m->match($path));
+ }
+
+ public function testFormattedResourceMember()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->resource('message', 'messages');
+ $m->createRegs(array('messages'));
+
+ $path = '/messages/42.xml';
+
+ $m->environ = array('REQUEST_METHOD' => 'GET');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'show',
+ 'id' => 42, 'format' => 'xml'),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'POST');
+ $this->assertNull($m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'PUT');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'update',
+ 'id' => 42, 'format' => 'xml'),
+ $m->match($path));
+
+ $m->environ = array('REQUEST_METHOD' => 'DELETE');
+ $this->assertEquals(array('controller' => 'messages', 'action' => 'delete',
+ 'id' => 42, 'format' => 'xml'),
+ $m->match($path));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+/**
+ * @package Horde_Routes
+ */
+class Horde_Routes_TestHelper
+{
+ /**
+ * Update a Mapper instance with a new $environ. If PATH_INFO
+ * is present, try to match it and update mapperDict.
+ *
+ * @param Horde_Routes_Mapper $mapper Mapper instance to update
+ * @param array $environ Environ to set in Mapper
+ * @return void
+ */
+ public static function updateMapper($mapper, $environ)
+ {
+ $mapper->environ = $environ;
+ $mapper->utils->mapperdict = null;
+
+ if (isset($environ['PATH_INFO'])) {
+ $result = $mapper->routeMatch($environ['PATH_INFO']);
+ $mapper->utils->mapperDict = isset($result) ? $result[0] : null;
+ }
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+require_once dirname(__FILE__) . '/TestHelper.php';
+
+/**
+ * @package Horde_Routes
+ */
+class UtilTest extends PHPUnit_Framework_TestCase
+{
+
+ public function setUp()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('HTTP_HOST' => 'www.test.com');
+
+ $m->connect('archive/:year/:month/:day',
+ array('controller' => 'blog',
+ 'action' => 'view',
+ 'month' => null,
+ 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}', 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:id', array('controller' => 'post', 'action' => 'view'));
+ $m->connect(':controller/:action/:id');
+
+ $this->mapper = $m;
+ $this->utils = $m->utils;
+ }
+
+ public function testUrlForSelf()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array();
+
+ $this->assertEquals('/blog', $utils->urlFor(array('controller' => 'blog')));
+ $this->assertEquals('/content', $utils->urlFor());
+ $this->assertEquals('https://www.test.com/viewpost', $utils->urlFor(array('controller' => 'post', 'action' => 'view', 'protocol' => 'https')));
+ $this->assertEquals('http://www.test.org/content/view/2', $utils->urlFor(array('host' => 'www.test.org', 'controller' => 'content', 'action' => 'view', 'id' => 2)));
+ }
+
+ public function testUrlForWithDefaults()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'id' => 4);
+
+ $this->assertEquals('/blog/view/4', $utils->urlFor());
+ $this->assertEquals('/post/index/4', $utils->urlFor(array('controller' => 'post')));
+ $this->assertEquals('/blog/view/2', $utils->urlFor(array('id' => 2)));
+ $this->assertEquals('/viewpost/4', $utils->urlFor(array('controller' => 'post', 'action' => 'view', 'id' => 4)));
+
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'year' => 2004);
+ $this->assertEquals('/archive/2004/10', $utils->urlFor(array('month' => 10)));
+ $this->assertEquals('/archive/2004/9/2', $utils->urlFor(array('month' => 9, 'day' => 2)));
+ $this->assertEquals('/blog', $utils->urlFor(array('controller' => 'blog', 'year' => null)));
+ }
+
+ public function testUrlForWithMoreDefaults()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'id' => 4);
+ $this->assertEquals('/blog/view/4', $utils->urlFor());
+ $this->assertEquals('/post/index/4', $utils->urlFor(array('controller' => 'post')));
+ $this->assertEquals('/viewpost/4', $utils->urlfor(array('controller' => 'post', 'action' => 'view', 'id' => 4)));
+
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'year' => 2004);
+ $this->assertEquals('/archive/2004/10', $utils->urlFor(array('month' => 10)));
+ $this->assertEquals('/archive/2004/9/2', $utils->urlFor(array('month' => 9, 'day' => 2)));
+ $this->assertEquals('/blog', $utils->urlFor(array('controller' => 'blog', 'year' => null)));
+ $this->assertEquals('/archive/2004', $utils->urlFor());
+ }
+
+ public function testUrlForWithDefaultsAndQualified()
+ {
+ $m = $this->mapper;
+ $utils = $m->utils;
+
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section', array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'www.example.com',
+ 'PATH_INFO' => '/blog/view/4');
+ Horde_Routes_TestHelper::updateMapper($m, $environ);
+
+ $this->assertEquals('/blog/view/4', $utils->urlFor());
+ $this->assertEquals('/post/index/4', $utils->urlFor(array('controller' => 'post')));
+ $this->assertEquals('http://www.example.com/blog/view/4', $utils->urlFor(array('qualified' => true)));
+ $this->assertEquals('/blog/view/2', $utils->urlFor(array('id' => 2)));
+ $this->assertEquals('/viewpost/4', $utils->urlFor(array('controller' => 'post', 'action' => 'view', 'id' => 4)));
+
+ $environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'www.example.com:8080', 'PATH_INFO' => '/blog/view/4');
+ Horde_Routes_TestHelper::updateMapper($m, $environ);
+
+ $this->assertEquals('/post/index/4',
+ $utils->urlFor(array('controller' => 'post')));
+ $this->assertEquals('http://www.example.com:8080/blog/view/4',
+ $utils->urlFor(array('qualified' => true)));
+ }
+
+ public function testWithRouteNames()
+ {
+ $m = $this->mapper;
+
+ $utils = $this->utils;
+ $utils->mapperDict = array();
+
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content',
+ $utils->urlFor(array('controller' => 'content')));
+ $this->assertEquals('/admin/comments',
+ $utils->urlFor(array('controller' => 'admin/comments')));
+ $this->assertEquals('/category',
+ $utils->urlFor('category_home'));
+ $this->assertEquals('/category/food',
+ $utils->urlFor('category_home', array('section' => 'food')));
+ $this->assertEquals('/category',
+ $utils->urlFor('home', array('action' => 'view', 'section' => 'home')));
+ $this->assertEquals('/content/splash',
+ $utils->urlFor('home', array('controller' => 'content')));
+ $this->assertEquals('/',
+ $utils->urlFor('/'));
+ }
+
+ public function testWithRouteNamesAndDefaults()
+ {
+ $this->markTestSkipped();
+
+ $m = $this->mapper;
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section', array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect('building', 'building/:campus/:building/alljacks', array('controller' => 'building', 'action' => 'showjacks'));
+ $m->createRegs(array('content', 'blog', 'admin/comments', 'building'));
+
+ $utils->mapperDict = array('controller' => 'building', 'action' => 'showjacks', 'campus' => 'wilma', 'building' => 'port');
+ $this->assertEquals('/building/wilma/port/alljacks', $utils->urlFor());
+ $this->assertEquals('/', $utils->urlFor('home'));
+ }
+
+ // callback used by testRedirectTo
+ // Python version is inlined in test_redirect_to
+ public function printer($echo)
+ {
+ $this->redirectToResult = $echo;
+ }
+
+ public function testRedirectTo()
+ {
+ $m = $this->mapper;
+ $m->environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'www.example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $callback = array($this, 'printer');
+ $utils->redirect = $callback;
+
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->redirectToResult = null;
+ $utils->redirectTo(array('controller' => 'content', 'action' => 'view'));
+ $this->assertEquals('/content/view', $this->redirectToResult);
+
+ $this->redirectToResult = null;
+ $utils->redirectTo(array('controller' => 'content', 'action' => 'lookup', 'id' => 4));
+ $this->assertEquals('/content/lookup/4', $this->redirectToResult);
+
+ $this->redirectToResult = null;
+ $utils->redirectTo(array('controller' => 'admin/comments', 'action' => 'splash'));
+ $this->assertEquals('/admin/comments/splash', $this->redirectToResult);
+
+ $this->redirectToResult = null;
+ $utils->redirectTo('http://www.example.com/');
+ $this->assertEquals('http://www.example.com/', $this->redirectToResult);
+
+ $this->redirectToResult = null;
+ $utils->redirectTo('/somewhere.html', array('var' => 'keyword'));
+ $this->assertEquals('/somewhere.html?var=keyword', $this->redirectToResult);
+ }
+
+ public function testStaticRoute()
+ {
+ $m = $this->mapper;
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'example.com');
+ Horde_Routes_TestHelper::updateMapper($m, $environ);
+
+ $m->connect(':controller/:action/:id');
+ $m->connect('home', 'http://www.groovie.org/', array('_static' => true));
+ $m->connect('space', '/nasa/images', array('_static' => true));
+ $m->createRegs(array('content', 'blog'));
+
+ $this->assertEquals('http://www.groovie.org/',
+ $utils->urlFor('home'));
+ $this->assertEquals('http://www.groovie.org/?s=stars',
+ $utils->urlFor('home', array('s' => 'stars')));
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/nasa/images?search=all',
+ $utils->urlFor('space', array('search' => 'all')));
+ }
+
+ public function testStaticRouteWithScript()
+ {
+ $m = $this->mapper;
+ $m->environ = array('SCRIPT_NAME' => '/webapp', 'HTTP_HOST' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+
+ $m->connect(':controller/:action/:id');
+ $m->connect('home', 'http://www.groovie.org/', array('_static' => true));
+ $m->connect('space', '/nasa/images', array('_static' => true));
+ $m->createRegs(array('content', 'blog'));
+
+ $this->assertEquals('http://www.groovie.org/',
+ $utils->urlFor('home'));
+ $this->assertEquals('http://www.groovie.org/?s=stars',
+ $utils->urlFor('home', array('s' => 'stars')));
+ $this->assertEquals('/webapp/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/webapp/nasa/images?search=all',
+ $utils->urlFor('space', array('search' => 'all')));
+ $this->assertEquals('http://example.com/webapp/nasa/images',
+ $utils->urlFor('space', array('protocol' => 'http')));
+ }
+
+ public function testNoNamedPath()
+ {
+ $m = $this->mapper;
+ $m->environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect(':controller/:action/:id');
+ $m->connect('home', 'http://www.groovie.org', array('_static' => true));
+ $m->connect('space', '/nasa/images', array('_static' => true));
+ $m->createRegs(array('content', 'blog'));
+
+ $this->assertEquals('http://www.google.com/search',
+ $utils->urlFor('http://www.google.com/search'));
+ $this->assertEquals('http://www.google.com/search?q=routes',
+ $utils->urlFor('http://www.google.com/search', array('q'=>'routes')));
+ $this->assertEquals('/delicious.jpg',
+ $utils->urlFor('/delicious.jpg'));
+ $this->assertEquals('/delicious/search?v=routes',
+ $utils->urlFor('/delicious/search', array('v'=>'routes')));
+ }
+
+ public function testAppendSlash()
+ {
+ $m = $this->mapper;
+ $m->environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'example.com');
+ $m->appendSlash = true;
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect(':controller/:action/:id');
+ $m->connect('home', 'http://www.groovie.org/', array('_static' => true));
+ $m->connect('space', '/nasa/images', array('_static' => true));
+ $m->createRegs(array('content', 'blog'));
+
+ $this->assertEquals('http://www.google.com/search',
+ $utils->urlFor('http://www.google.com/search'));
+ $this->assertEquals('http://www.google.com/search?q=routes',
+ $utils->urlFor('http://www.google.com/search', array('q'=>'routes')));
+ $this->assertEquals('/delicious.jpg',
+ $utils->urlFor('/delicious.jpg'));
+ $this->assertEquals('/delicious/search?v=routes',
+ $utils->urlFor('/delicious/search', array('v' => 'routes')));
+ $this->assertEquals('/content/list/',
+ $utils->urlFor(array('controller' => '/content', 'action' => 'list')));
+ $this->assertEquals('/content/list/?page=1',
+ $utils->urlFor(array('controller' => '/content', 'action' => 'list', 'page' => '1')));
+ }
+
+ public function testNoNamedPathWithScript()
+ {
+ $m = $this->mapper;
+ $m->environ = array('SCRIPT_NAME' => '/webapp', 'HTTP_HOST' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect(':controller/:action/:id');
+ $m->connect('home', 'http://www.groovie.org/', array('_static' => true));
+ $m->connect('space', '/nasa/images', array('_static' => true));
+ $m->createRegs(array('content', 'blog'));
+
+ $this->assertEquals('http://www.google.com/search',
+ $utils->urlFor('http://www.google.com/search'));
+ $this->assertEquals('http://www.google.com/search?q=routes',
+ $utils->urlFor('http://www.google.com/search', array('q'=>'routes')));
+ $this->assertEquals('/webapp/delicious.jpg',
+ $utils->urlFor('/delicious.jpg'));
+ $this->assertEquals('/webapp/delicious/search?v=routes',
+ $utils->urlFor('/delicious/search', array('v'=>'routes')));
+ }
+
+ // callback used by testRouteFilter
+ // Python version is inlined in test_route_filter
+ public function articleFilter($kargs)
+ {
+ $article = isset($kargs['article']) ? $kargs['article'] : null;
+ unset($kargs['article']);
+
+ if ($article !== null) {
+ $kargs['year'] = isset($article['year']) ? $article['year'] : 2004;
+ $kargs['month'] = isset($article['month']) ? $article['month'] : 12;
+ $kargs['day'] = isset($article['day']) ? $article['day'] : 20;
+ $kargs['slug'] = isset($article['slug']) ? $article['slug'] : 'default';
+ }
+
+ return $kargs;
+ }
+
+ public function testRouteFilter()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $callback = array($this, 'articleFilter');
+
+ $m->connect(':controller/:(action)-:(id).html');
+ $m->connect('archives', 'archives/:year/:month/:day/:slug',
+ array('controller' =>'archives', 'action' =>'view', '_filter' => $callback));
+ $m->createRegs(array('content', 'archives', 'admin/comments'));
+
+ $this->assertNull($utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertNull($utils->urlFor(array('controller' => 'content')));
+
+ $this->assertEquals('/content/view-3.html',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view', 'id' => 3)));
+ $this->assertEquals('/content/index-2.html',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+
+ $this->assertEquals('/archives/2005/10/5/happy',
+ $utils->urlFor('archives', array('year' => 2005, 'month' => 10,
+ 'day' => 5, 'slug' => 'happy')));
+
+ $story = array('year' => 2003, 'month' => 8, 'day' => 2, 'slug' => 'woopee');
+ $empty = array();
+
+ $expected = array('controller' => 'archives', 'action' => 'view', 'year' => '2005',
+ 'month' => '10', 'day' => '5', 'slug' => 'happy');
+ $this->assertEquals($expected, $m->match('/archives/2005/10/5/happy'));
+
+ $this->assertEquals('/archives/2003/8/2/woopee',
+ $utils->urlFor('archives', array('article' => $story)));
+ $this->assertEquals('/archives/2004/12/20/default',
+ $utils->urlFor('archives', array('article' => $empty)));
+ }
+
+ public function testWithSslEnviron()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'HTTPS' => 'on', 'SERVER_PORT' => '443',
+ 'PATH_INFO' => '/', 'HTTP_HOST' => 'example.com',
+ 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'archives', 'admin/comments'));
+
+ // HTTPS is on, but we're running on a different port internally
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+ $this->assertEquals('https://nowhere.com/content',
+ $utils->urlFor(array('host' => 'nowhere.com', 'controller' => 'content')));
+
+ // If HTTPS is on, but the port isn't 443, we'll need to include the port info
+ $m->environ['SERVER_PORT'] = '8080';
+
+ $utils->mapperDict = array();
+
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => '2')));
+ $this->assertEquals('https://nowhere.com/content',
+ $utils->urlFor(array('host' => 'nowhere.com', 'controller' => 'content')));
+ $this->assertEquals('https://nowhere.com:8080/content',
+ $utils->urlFor(array('host' => 'nowhere.com:8080', 'controller' => 'content')));
+ $this->assertEquals('http://nowhere.com/content',
+ $utils->urlFor(array('host' => 'nowhere.com', 'protocol' => 'http',
+ 'controller' => 'content')));
+ }
+
+ public function testWithHttpEnviron()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'SERVER_PORT' => '1080', 'PATH_INFO' => '/',
+ 'HTTP_HOST' => 'example.com', 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'archives', 'admin/comments'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+ $this->assertEquals('https://example.com/content',
+ $utils->urlFor(array('protocol' => 'https', 'controller' => 'content')));
+ }
+
+ public function testSubdomains()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'PATH_INFO' => '/',
+ 'HTTP_HOST' => 'example.com', 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->subDomains = true;
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'archives', 'admin/comments'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+
+ $m->environ['HTTP_HOST'] = 'sub.example.com';
+
+ $utils->mapperDict = array('subDomain' => 'sub');
+
+ $this->assertEquals('/content/view/3',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view', 'id' => 3)));
+ $this->assertEquals('http://new.example.com/content',
+ $utils->urlFor(array('controller' => 'content', 'subDomain' => 'new')));
+ }
+
+ public function testSubdomainsWithExceptions()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'PATH_INFO' => '/',
+ 'HTTP_HOST' => 'example.com', 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->subDomains = true;
+ $m->subDomainsIgnore = array('www');
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'archives', 'admin/comments'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+
+ $m->environ = array('HTTP_HOST' => 'sub.example.com');
+
+ $utils->mapperDict = array('subDomain' => 'sub');
+
+ $this->assertEquals('/content/view/3',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view', 'id' => 3)));
+ $this->assertEquals('http://new.example.com/content',
+ $utils->urlFor(array('controller' => 'content', 'subDomain' => 'new')));
+ $this->assertEquals('http://example.com/content',
+ $utils->urlFor(array('controller' => 'content', 'subDomain' => 'www')));
+
+ $utils->mapperDict = array('subDomain' => 'www');
+ $this->assertEquals('http://example.com/content/view/3',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view', 'id' => 3)));
+ $this->assertEquals('http://new.example.com/content',
+ $utils->urlFor(array('controller' => 'content', 'subDomain' => 'new')));
+ $this->assertEquals('/content',
+ $utils->urlFor(array('controller' => 'content', 'subDomain' => 'sub')));
+ }
+
+ public function testSubdomainsWithNamedRoutes()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'PATH_INFO' => '/',
+ 'HTTP_HOST' => 'example.com', 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->subDomains = true;
+ $m->connect(':controller/:action/:id');
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect('building', 'building/:campus/:building/alljacks',
+ array('controller' => 'building', 'action' => 'showjacks'));
+ $m->createRegs(array('content','blog','admin/comments','building'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/content/index/2',
+ $utils->urlFor(array('controller' => 'content', 'id' => 2)));
+ $this->assertEquals('/category',
+ $utils->urlFor('category_home'));
+ $this->assertEquals('http://new.example.com/category',
+ $utils->urlFor('category_home', array('subDomain' => 'new')));
+ }
+
+ public function testSubdomainsWithPorts()
+ {
+ $m = new Horde_Routes_Mapper();
+ $m->environ = array('SCRIPT_NAME' => '', 'PATH_INFO' => '/',
+ 'HTTP_HOST' => 'example.com:8000', 'SERVER_NAME' => 'example.com');
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->subDomains = true;
+ $m->connect(':controller/:action/:id');
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect('building', 'building/:campus/:building/alljacks',
+ array('controller' => 'building', 'action' => 'showjacks'));
+ $m->createRegs(array('content', 'blog', 'admin/comments', 'building'));
+
+ $this->assertEquals('/content/view',
+ $utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertEquals('/category',
+ $utils->urlFor('category_home'));
+ $this->assertEquals('http://new.example.com:8000/category',
+ $utils->urlFor('category_home', array('subDomain' => 'new')));
+ $this->assertEquals('http://joy.example.com:8000/building/west/merlot/alljacks',
+ $utils->urlFor('building', array('campus' => 'west', 'building' => 'merlot',
+ 'subDomain' => 'joy')));
+
+ $m->environ = array('HTTP_HOST' => 'example.com');
+
+ $this->assertEquals('http://new.example.com/category',
+ $utils->urlFor('category_home', array('subDomain' => 'new')));
+ }
+
+ public function testControllerScan()
+ {
+ $hereDir = dirname(__FILE__);
+ $controllerDir = "$hereDir/fixtures/controllers";
+
+ $controllers = Horde_Routes_Utils::controllerScan($controllerDir);
+
+ $this->assertEquals(3, count($controllers));
+ $this->assertEquals('admin/users', $controllers[0]);
+ $this->assertEquals('content', $controllers[1]);
+ $this->assertEquals('users', $controllers[2]);
+ }
+
+ public function testAutoControllerScan()
+ {
+ $hereDir = dirname(__FILE__);
+ $controllerDir = "$hereDir/fixtures/controllers";
+
+ $m = new Horde_Routes_Mapper(array('directory' => $controllerDir));
+ $m->alwaysScan = true;
+
+ $m->connect(':controller/:action/:id');
+
+ $expected = array('action' => 'index', 'controller' => 'content', 'id' => null);
+ $this->assertEquals($expected, $m->match('/content'));
+
+ $expected = array('action' => 'index', 'controller' => 'users', 'id' => null);
+ $this->assertEquals($expected, $m->match('/users'));
+
+ $expected = array('action' => 'index', 'controller' => 'admin/users', 'id' => null);
+ $this->assertEquals($expected, $m->match('/admin/users'));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Horde Routes package
+ *
+ * This package is heavily inspired by the Python "Routes" library
+ * by Ben Bangert (http://routes.groovie.org). Routes is based
+ * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
+ *
+ * @author Maintainable Software, LLC. (http://www.maintainable.com)
+ * @author Mike Naberezny <mike@maintainable.com>
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @package Horde_Routes
+ */
+
+require_once dirname(__FILE__) . '/TestHelper.php';
+
+/**
+ * @package Horde_Routes
+ */
+class UtilWithExplicitTest extends PHPUnit_Framework_TestCase {
+
+ public function setUp()
+ {
+ $m = new Horde_Routes_Mapper(array('explicit' => true));
+ $m->connect('archive/:year/:month/:day',
+ array('controller' => 'blog',
+ 'action' => 'view',
+ 'month' => null,
+ 'day' => null,
+ 'requirements' => array('month' => '\d{1,2}', 'day' => '\d{1,2}')));
+ $m->connect('viewpost/:id', array('controller' => 'post', 'action' => 'view', 'id' => null));
+ $m->connect(':controller/:action/:id');
+ $m->environ = array('SERVER_NAME' => 'www.test.com');
+ $this->mapper = $m;
+ $this->utils = $m->utils;
+ }
+
+ public function testUrlFor()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array();
+
+ $this->assertNull($utils->urlFor(array('controller' => 'blog')));
+ $this->assertNull($utils->urlFor());
+ $this->assertEquals('/blog/view/3',
+ $utils->urlFor(array('controller' => 'blog', 'action' => 'view',
+ 'id' => 3)));
+ $this->assertEquals('https://www.test.com/viewpost',
+ $utils->urlFor(array('controller' => 'post', 'action' => 'view',
+ 'protocol' => 'https')));
+ $this->assertEquals('http://www.test.org/content/view/2',
+ $utils->urlFor(array('host' => 'www.test.org', 'controller' => 'content',
+ 'action' => 'view', 'id' => 2)));
+ }
+
+ public function testUrlForWithDefaults()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'id' => 4);
+
+ $this->assertNull($utils->urlFor());
+ $this->assertNull($utils->urlFor(array('controller' => 'post')));
+ $this->assertNull($utils->urlFor(array('id' => 2)));
+ $this->assertEquals('/viewpost/4',
+ $utils->urlFor(array('controller' => 'post', 'action' => 'view',
+ 'id' => 4)));
+
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'year' => 2004);
+ $this->assertNull($utils->urlFor(array('month' => 10)));
+ $this->assertNull($utils->urlFor(array('month' => 9, 'day' => 2)));
+ $this->assertNull($utils->urlFor(array('controller' => 'blog', 'year' => null)));
+ }
+
+ public function testUrlForWithMoreDefaults()
+ {
+ $utils = $this->utils;
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'id' => 4);
+
+ $this->assertNull($utils->urlFor());
+ $this->assertNull($utils->urlFor(array('controller' => 'post')));
+ $this->assertNull($utils->urlFor(array('id' => 2)));
+ $this->assertEquals('/viewpost/4',
+ $utils->urlFor(array('controller' => 'post', 'action' => 'view',
+ 'id' => 4)));
+
+ $utils->mapperDict = array('controller' => 'blog', 'action' => 'view', 'year' => 2004);
+ $this->assertNull($utils->urlFor(array('month' => 10)));
+ $this->assertNull($utils->urlFor());
+ }
+
+ public function testUrlForWithDefaultsAndQualified()
+ {
+ $utils = $this->utils;
+
+ $m = $this->mapper;
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect(':controller/:action/:id');
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $environ = array('SCRIPT_NAME' => '', 'SERVER_NAME' => 'www.example.com',
+ 'SERVER_PORT' => '80', 'PATH_INFO' => '/blog/view/4');
+ Horde_Routes_TestHelper::updateMapper($m, $environ);
+
+ $this->assertNull($utils->urlFor());
+ $this->assertNull($utils->urlFor(array('controller' => 'post')));
+ $this->assertNull($utils->urlFor(array('id' => 2)));
+ $this->assertNull($utils->urlFor(array('qualified' => true, 'controller' => 'blog', 'id' => 4)));
+ $this->assertEquals('http://www.example.com/blog/view/4',
+ $utils->urlFor(array('qualified' => true, 'controller' => 'blog',
+ 'action' => 'view', 'id' => 4)));
+ $this->assertEquals('/viewpost/4',
+ $utils->urlFor(array('controller' => 'post', 'action' => 'view', 'id' => 4)));
+
+ $environ = array('SCRIPT_NAME' => '', 'HTTP_HOST' => 'www.example.com:8080', 'PATH_INFO' => '/blog/view/4');
+ Horde_Routes_TestHelper::updateMapper($m, $environ);
+
+ $this->assertNull($utils->urlFor(array('controller' => 'post')));
+ $this->assertEquals('http://www.example.com:8080/blog/view/4',
+ $utils->urlFor(array('qualified' => true, 'controller' => 'blog',
+ 'action' => 'view', 'id' => 4)));
+ }
+
+ public function testWithRouteNames()
+ {
+ $m = $this->mapper;
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->createRegs(array('content', 'blog', 'admin/comments'));
+
+ $this->assertNull($utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertNull($utils->urlFor(array('controller' => 'content')));
+ $this->assertNull($utils->urlFor(array('controller' => 'admin/comments')));
+ $this->assertEquals('/category',
+ $utils->urlFor('category_home'));
+ $this->assertEquals('/category/food',
+ $utils->urlFor('category_home', array('section' => 'food')));
+ $this->assertEquals('/category',
+ $utils->urlFor('home', array('action' => 'view', 'section' => 'home')));
+ $this->assertNull($utils->urlFor('home', array('controller' => 'content')));
+ $this->assertEquals('/content/splash/2',
+ $utils->urlFor('home', array('controller' => 'content', 'action' => 'splash',
+ 'id' => 2)));
+ $this->assertEquals('/', $utils->urlFor('home'));
+ }
+
+ public function testWithRouteNamesAndDefaults()
+ {
+ $m = $this->mapper;
+
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->connect('home', '', array('controller' => 'blog', 'action' => 'splash'));
+ $m->connect('category_home', 'category/:section',
+ array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
+ $m->connect('building', 'building/:campus/:building/alljacks',
+ array('controller' => 'building', 'action' => 'showjacks'));
+ $m->createRegs(array('content', 'blog', 'admin/comments', 'building'));
+
+ $utils->mapperDict = array('controller' => 'building', 'action' => 'showjacks',
+ 'campus' => 'wilma', 'building' => 'port');
+
+ $this->assertNull($utils->urlFor());
+ $this->assertEquals('/building/wilma/port/alljacks',
+ $utils->urlFor(array('controller' => 'building', 'action' => 'showjacks',
+ 'campus' => 'wilma', 'building' => 'port')));
+ $this->assertEquals('/', $utils->urlFor('home'));
+ }
+
+ public function testWithResourceRouteNames()
+ {
+ $m = new Horde_Routes_Mapper();
+ $utils = $m->utils;
+ $utils->mapperDict = array();
+
+ $m->resource('message', 'messages',
+ array('member' => array('mark' => 'GET'),
+ 'collection' => array('rss' => 'GET')));
+ $m->createRegs(array('messages'));
+
+ $this->assertNull($utils->urlFor(array('controller' => 'content', 'action' => 'view')));
+ $this->assertNull($utils->urlFor(array('controller' => 'content')));
+ $this->assertNull($utils->urlFor(array('controller' => 'admin/comments')));
+ $this->assertEquals('/messages',
+ $utils->urlFor('messages'));
+ $this->assertEquals('/messages/rss',
+ $utils->urlFor('rss_messages'));
+ $this->assertEquals('/messages/4',
+ $utils->urlFor('message', array('id' => 4)));
+ $this->assertEquals('/messages/4/edit',
+ $utils->urlFor('edit_message', array('id' => 4)));
+ $this->assertEquals('/messages/4/mark',
+ $utils->urlFor('mark_message', array('id' => 4)));
+ $this->assertEquals('/messages/new',
+ $utils->urlFor('new_message'));
+ $this->assertEquals('/messages.xml',
+ $utils->urlFor('formatted_messages', array('format' => 'xml')));
+ $this->assertEquals('/messages/rss.xml',
+ $utils->urlFor('formatted_rss_messages', array('format' => 'xml')));
+ $this->assertEquals('/messages/4.xml',
+ $utils->urlFor('formatted_message', array('id' => 4, 'format' => 'xml')));
+ $this->assertEquals('/messages/4/edit.xml',
+ $utils->urlFor('formatted_edit_message', array('id' => 4, 'format' => 'xml')));
+ $this->assertEquals('/messages/4/mark.xml',
+ $utils->urlFor('formatted_mark_message', array('id' => 4, 'format' => 'xml')));
+ $this->assertEquals('/messages/new.xml',
+ $utils->urlFor('formatted_new_message', array('format' => 'xml')));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @package Horde_Routes
+ */
--- /dev/null
+<?php
+/**
+ * @package Horde_Routes
+ */
--- /dev/null
+<?php
+/**
+ * @package Horde_Routes
+ */