From: Chuck Hagenbuch Date: Sat, 29 Nov 2008 01:25:57 +0000 (-0500) Subject: add Routes X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=d17dddd19aaa63b21c6c4dbc9da299773a0f88bc;p=horde.git add Routes --- diff --git a/framework/Routes/doc/Horde/Routes/integration.txt b/framework/Routes/doc/Horde/Routes/integration.txt new file mode 100644 index 000000000..818ef6a49 --- /dev/null +++ b/framework/Routes/doc/Horde/Routes/integration.txt @@ -0,0 +1,114 @@ +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)); diff --git a/framework/Routes/doc/Horde/Routes/manual.txt b/framework/Routes/doc/Horde/Routes/manual.txt new file mode 100644 index 000000000..a571ddf0b --- /dev/null +++ b/framework/Routes/doc/Horde/Routes/manual.txt @@ -0,0 +1,730 @@ +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. diff --git a/framework/Routes/lib/Horde/Routes/Exception.php b/framework/Routes/lib/Horde/Routes/Exception.php new file mode 100644 index 000000000..e6f4a0df0 --- /dev/null +++ b/framework/Routes/lib/Horde/Routes/Exception.php @@ -0,0 +1,22 @@ + + * @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 +{} diff --git a/framework/Routes/lib/Horde/Routes/Mapper.php b/framework/Routes/lib/Horde/Routes/Mapper.php new file mode 100644 index 000000000..742e433dc --- /dev/null +++ b/framework/Routes/lib/Horde/Routes/Mapper.php @@ -0,0 +1,1152 @@ + + * @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 + * "/:_id". + * + * If ``parentResource`` is supplied and ``namePrefix`` isn't, + * ``namePrefix`` will be generated from ``parentResource`` as + * "_". + * + * 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; + } + +} + diff --git a/framework/Routes/lib/Horde/Routes/Route.php b/framework/Routes/lib/Horde/Routes/Route.php new file mode 100644 index 000000000..fb7c29844 --- /dev/null +++ b/framework/Routes/lib/Horde/Routes/Route.php @@ -0,0 +1,828 @@ + + * @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; + } + +} diff --git a/framework/Routes/lib/Horde/Routes/Utils.php b/framework/Routes/lib/Horde/Routes/Utils.php new file mode 100644 index 000000000..eb13ec298 --- /dev/null +++ b/framework/Routes/lib/Horde/Routes/Utils.php @@ -0,0 +1,418 @@ + + * @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; + } +} diff --git a/framework/Routes/package.xml b/framework/Routes/package.xml new file mode 100644 index 000000000..5dc546129 --- /dev/null +++ b/framework/Routes/package.xml @@ -0,0 +1,68 @@ + + + Routes + pear.horde.org + Horde Routes URL mapping system + 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). + + + Mike Naberezny + mnaberez + mike@maintainable.com + yes + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2008-09-19 + + 0.3.1 + 0.3.0 + + + beta + beta + + BSD + * Fixed callbacks for PHP versions prior to PHP 5.2. + + + + + + + + + + + + + + + + + + 5.1.0 + + + 1.4.0b1 + + + + + + + + + + + + diff --git a/framework/Routes/test/Horde/Routes/AllTests.php b/framework/Routes/test/Horde/Routes/AllTests.php new file mode 100644 index 000000000..17712a0c3 --- /dev/null +++ b/framework/Routes/test/Horde/Routes/AllTests.php @@ -0,0 +1,65 @@ + + * @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(); +} diff --git a/framework/Routes/test/Horde/Routes/GenerationTest.php b/framework/Routes/test/Horde/Routes/GenerationTest.php new file mode 100644 index 000000000..92ba3e75e --- /dev/null +++ b/framework/Routes/test/Horde/Routes/GenerationTest.php @@ -0,0 +1,985 @@ + + * @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')))); + } + +} diff --git a/framework/Routes/test/Horde/Routes/RecognitionTest.php b/framework/Routes/test/Horde/Routes/RecognitionTest.php new file mode 100644 index 000000000..3eabe5db6 --- /dev/null +++ b/framework/Routes/test/Horde/Routes/RecognitionTest.php @@ -0,0 +1,1097 @@ + + * @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)); + } + +} diff --git a/framework/Routes/test/Horde/Routes/TestHelper.php b/framework/Routes/test/Horde/Routes/TestHelper.php new file mode 100644 index 000000000..dda49bb1d --- /dev/null +++ b/framework/Routes/test/Horde/Routes/TestHelper.php @@ -0,0 +1,39 @@ + + * @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; + } + } + +} diff --git a/framework/Routes/test/Horde/Routes/UtilTest.php b/framework/Routes/test/Horde/Routes/UtilTest.php new file mode 100644 index 000000000..7dad84600 --- /dev/null +++ b/framework/Routes/test/Horde/Routes/UtilTest.php @@ -0,0 +1,601 @@ + + * @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')); + } + +} diff --git a/framework/Routes/test/Horde/Routes/UtilWithExplicitTest.php b/framework/Routes/test/Horde/Routes/UtilWithExplicitTest.php new file mode 100644 index 000000000..161dfb3ab --- /dev/null +++ b/framework/Routes/test/Horde/Routes/UtilWithExplicitTest.php @@ -0,0 +1,217 @@ + + * @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'))); + } + +} diff --git a/framework/Routes/test/Horde/Routes/fixtures/controllers/admin/users.php b/framework/Routes/test/Horde/Routes/fixtures/controllers/admin/users.php new file mode 100644 index 000000000..cadde9990 --- /dev/null +++ b/framework/Routes/test/Horde/Routes/fixtures/controllers/admin/users.php @@ -0,0 +1,4 @@ +