root/trunk/framework/Web/TUrlMapping.php

Revision 2527, 20.3 kB (checked in by mikl, 5 weeks ago)

Fixed #922

  • Property svn:keywords set to Id
Line 
1<?php
2/**
3 * TUrlMapping and TUrlMappingPattern class file.
4 *
5 * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
6 * @link http://www.pradosoft.com/
7 * @copyright Copyright &copy; 2005-2008 PradoSoft
8 * @license http://www.pradosoft.com/license/
9 * @version $Id$
10 * @package System.Web
11 */
12
13Prado::using('System.Web.TUrlManager');
14Prado::using('System.Collections.TAttributeCollection');
15
16/**
17 * TUrlMapping Class
18 *
19 * The TUrlMapping module allows PRADO to construct and recognize URLs
20 * based on specific patterns.
21 *
22 * TUrlMapping consists of a list of URL patterns which are used to match
23 * against the currently requested URL. The first matching pattern will then
24 * be used to decompose the URL into request parameters (accessible through
25 * <code>$this->Request['paramname']</code>).
26 *
27 * The patterns can also be used to construct customized URLs. In this case,
28 * the parameters in an applied pattern will be replaced with the corresponding
29 * GET variable values.
30 *
31 * Since it is derived from {@link TUrlManager}, it should be configured globally
32 * in the application configuration like the following,
33 * <code>
34 *  <module id="request" class="THttpRequest" UrlManager="friendly-url" />
35 *  <module id="friendly-url" class="System.Web.TUrlMapping" EnableCustomUrl="true">
36 *    <url ServiceParameter="Posts.ViewPost" pattern="post/{id}/" parameters.id="\d+" />
37 *    <url ServiceParameter="Posts.ListPost" pattern="archive/{time}/" parameters.time="\d{6}" />
38 *    <url ServiceParameter="Posts.ListPost" pattern="category/{cat}/" parameters.cat="\d+" />
39 *  </module>
40 * </code>
41 *
42 * In the above, each <tt>&lt;url&gt;</tt> element specifies a URL pattern represented
43 * as a {@link TUrlMappingPattern} internally. You may create your own pattern classes
44 * by extending {@link TUrlMappingPattern} and specifying the <tt>&lt;class&gt;</tt> attribute
45 * in the element.
46 *
47 * The patterns can be also be specified in an external file using the {@link setConfigFile ConfigFile} property.
48 *
49 * The URL mapping are evaluated in order, only the first mapping that matches
50 * the URL will be used. Cascaded mapping can be achieved by placing the URL mappings
51 * in particular order. For example, placing the most specific mappings first.
52 *
53 * Only the PATH_INFO part of the URL is used to match the available patterns. The matching
54 * is strict in the sense that the whole pattern must match the whole PATH_INFO of the URL.
55 *
56 * From PRADO v3.1.1, TUrlMapping also provides support for constructing URLs according to
57 * the specified pattern. You may enable this functionality by setting {@link setEnableCustomUrl EnableCustomUrl} to true.
58 * When you call THttpRequest::constructUrl() (or via TPageService::constructUrl()),
59 * TUrlMapping will examine the available URL mapping patterns using their {@link TUrlMappingPattern::getServiceParameter ServiceParameter}
60 * and {@link TUrlMappingPattern::getPattern Pattern} properties. A pattern is applied if its
61 * {@link TUrlMappingPattern::getServiceParameter ServiceParameter} matches the service parameter passed
62 * to constructUrl() and every parameter in the {@link getPattern Pattern} is found
63 * in the GET variables.
64 *
65 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
66 * @version $Id$
67 * @package System.Web
68 * @since 3.0.5
69 */
70class TUrlMapping extends TUrlManager
71{
72    /**
73     * File extension of external configuration file
74     */
75    const CONFIG_FILE_EXT='.xml';
76    /**
77     * @var TUrlMappingPattern[] list of patterns.
78     */
79    protected $_patterns=array();
80    /**
81     * @var TUrlMappingPattern matched pattern.
82     */
83    private $_matched;
84    /**
85     * @var string external configuration file
86     */
87    private $_configFile=null;
88    /**
89     * @var boolean whether to enable custom contructUrl
90     */
91    private $_customUrl=false;
92    /**
93     * @var array rules for constructing URLs
94     */
95    protected $_constructRules=array();
96
97    private $_urlPrefix='';
98
99    private $_defaultMappingClass='TUrlMappingPattern';
100
101    /**
102     * Initializes this module.
103     * This method is required by the IModule interface.
104     * @param TXmlElement configuration for this module, can be null
105     * @throws TConfigurationException if module is configured in the global scope.
106     */
107    public function init($xml)
108    {
109        parent::init($xml);
110        if($this->getRequest()->getRequestResolved())
111            throw new TConfigurationException('urlmapping_global_required');
112        if($this->_configFile!==null)
113            $this->loadConfigFile();
114        $this->loadUrlMappings($xml);
115        if($this->_urlPrefix==='')
116            $this->_urlPrefix=$this->getRequest()->getApplicationUrl();
117        $this->_urlPrefix=rtrim($this->_urlPrefix,'/');
118    }
119
120    /**
121     * Initialize the module from configuration file.
122     * @throws TConfigurationException if {@link getConfigFile ConfigFile} is invalid.
123     */
124    protected function loadConfigFile()
125    {
126        if(is_file($this->_configFile))
127         {
128            $dom=new TXmlDocument;
129            $dom->loadFromFile($this->_configFile);
130            $this->loadUrlMappings($dom);
131        }
132        else
133            throw new TConfigurationException('urlmapping_configfile_inexistent',$this->_configFile);
134    }
135
136    /**
137     * Returns a value indicating whether to enable custom constructUrl.
138     * If true, constructUrl() will make use of the URL mapping rules to
139     * construct valid URLs.
140     * @return boolean whether to enable custom constructUrl. Defaults to false.
141     * @since 3.1.1
142     */
143    public function getEnableCustomUrl()
144    {
145        return $this->_customUrl;
146    }
147
148    /**
149     * Sets a value indicating whether to enable custom constructUrl.
150     * If true, constructUrl() will make use of the URL mapping rules to
151     * construct valid URLs.
152     * @param boolean whether to enable custom constructUrl.
153     * @since 3.1.1
154     */
155    public function setEnableCustomUrl($value)
156    {
157        $this->_customUrl=TPropertyValue::ensureBoolean($value);
158    }
159
160    /**
161     * @return string the part that will be prefixed to the constructed URLs. Defaults to the requested script path (e.g. /path/to/index.php for a URL http://hostname/path/to/index.php)
162     * @since 3.1.1
163     */
164    public function getUrlPrefix()
165    {
166        return $this->_urlPrefix;
167    }
168
169    /**
170     * @param string the part that will be prefixed to the constructed URLs. This is used by constructUrl() when EnableCustomUrl is set true.
171     * @see getUrlPrefix
172     * @since 3.1.1
173     */
174    public function setUrlPrefix($value)
175    {
176        $this->_urlPrefix=$value;
177    }
178
179    /**
180     * @return string external configuration file. Defaults to null.
181     */
182    public function getConfigFile()
183    {
184        return $this->_configFile;
185    }
186
187    /**
188     * @param string external configuration file in namespace format. The file
189     * must be suffixed with '.xml'.
190     * @throws TInvalidDataValueException if the file is invalid.
191     */
192    public function setConfigFile($value)
193    {
194        if(($this->_configFile=Prado::getPathOfNamespace($value,self::CONFIG_FILE_EXT))===null)
195            throw new TConfigurationException('urlmapping_configfile_invalid',$value);
196    }
197
198    /**
199     * @return string the default class of URL mapping patterns. Defaults to TUrlMappingPattern.
200     * @since 3.1.1
201     */
202    public function getDefaultMappingClass()
203    {
204        return $this->_defaultMappingClass;
205    }
206
207    /**
208     * Sets the default class of URL mapping patterns.
209     * When a URL matching pattern does not specify "class" attribute, it will default to the class
210     * specified by this property. You may use either a class name or a namespace format of class (if the class needs to be included first.)
211     * @param string the default class of URL mapping patterns.
212     * @since 3.1.1
213     */
214    public function setDefaultMappingClass($value)
215    {
216        $this->_defaultMappingClass=$value;
217    }
218
219    /**
220     * Load and configure each url mapping pattern.
221     * @param TXmlElement configuration node
222     * @throws TConfigurationException if specific pattern class is invalid
223     */
224    protected function loadUrlMappings($xml)
225    {
226        foreach($xml->getElementsByTagName('url') as $url)
227        {
228            $properties=$url->getAttributes();
229            if(($class=$properties->remove('class'))===null)
230                $class=$this->getDefaultMappingClass();
231            $pattern=Prado::createComponent($class,$this);
232            if(!($pattern instanceof TUrlMappingPattern))
233                throw new TConfigurationException('urlmapping_urlmappingpattern_required');
234            foreach($properties as $name=>$value)
235                $pattern->setSubproperty($name,$value);
236            $this->_patterns[]=$pattern;
237            $pattern->init($url);
238
239            $key=$pattern->getServiceID().':'.$pattern->getServiceParameter();
240            $this->_constructRules[$key][]=$pattern;
241        }
242    }
243
244    /**
245     * Parses the request URL and returns an array of input parameters.
246     * This method overrides the parent implementation.
247     * The input parameters do not include GET and POST variables.
248     * This method uses the request URL path to find the first matching pattern. If found
249     * the matched pattern parameters are used to return as the input parameters.
250     * @return array list of input parameters
251     */
252    public function parseUrl()
253    {
254        $request=$this->getRequest();
255        foreach($this->_patterns as $pattern)
256        {
257            $matches=$pattern->getPatternMatches($request);
258            if(count($matches)>0)
259            {
260                $this->_matched=$pattern;
261                $params=array();
262                foreach($matches as $key=>$value)
263                {
264                    if(is_string($key))
265                        $params[$key]=$value;
266                }
267                $params[$pattern->getServiceID()]=$pattern->getServiceParameter();
268                return $params;
269            }
270        }
271        return parent::parseUrl();
272    }
273
274    /**
275     * Constructs a URL that can be recognized by PRADO.
276     *
277     * This method provides the actual implementation used by {@link THttpRequest::constructUrl}.
278     * Override this method if you want to provide your own way of URL formatting.
279     * If you do so, you may also need to override {@link parseUrl} so that the URL can be properly parsed.
280     *
281     * The URL is constructed as the following format:
282     * /entryscript.php?serviceID=serviceParameter&get1=value1&...
283     * If {@link THttpRequest::setUrlFormat THttpRequest.UrlFormat} is 'Path',
284     * the following format is used instead:
285     * /entryscript.php/serviceID/serviceParameter/get1,value1/get2,value2...
286     * @param string service ID
287     * @param string service parameter
288     * @param array GET parameters, null if not provided
289     * @param boolean whether to encode the ampersand in URL
290     * @param boolean whether to encode the GET parameters (their names and values)
291     * @return string URL
292     * @see parseUrl
293     * @since 3.1.1
294     */
295    public function constructUrl($serviceID,$serviceParam,$getItems,$encodeAmpersand,$encodeGetItems)
296    {
297        if($this->_customUrl)
298        {
299             if(!(is_array($getItems) || ($getItems instanceof Traversable)))
300                 $getItems=array();
301            $key=$serviceID.':'.$serviceParam;
302            if(isset($this->_constructRules[$key]))
303            {
304                foreach($this->_constructRules[$key] as $rule)
305                {
306                    if($rule->supportCustomUrl($getItems))
307                        return $rule->constructUrl($getItems,$encodeAmpersand,$encodeGetItems);
308                }
309            }
310        }
311        return parent::constructUrl($serviceID,$serviceParam,$getItems,$encodeAmpersand,$encodeGetItems);
312    }
313
314    /**
315     * @return TUrlMappingPattern the matched pattern, null if not found.
316     */
317    public function getMatchingPattern()
318    {
319        return $this->_matched;
320    }
321}
322
323/**
324 * TUrlMappingPattern class.
325 *
326 * TUrlMappingPattern represents a pattern used to parse and construct URLs.
327 * If the currently requested URL matches the pattern, it will alter
328 * the THttpRequest parameters. If a constructUrl() call matches the pattern
329 * parameters, the pattern will generate a valid URL. In both case, only the PATH_INFO
330 * part of a URL is parsed/constructed using the pattern.
331 *
332 * To specify the pattern, set the {@link setPattern Pattern} property.
333 * {@link setPattern Pattern} takes a string expression with
334 * parameter names enclosed between a left brace '{' and a right brace '}'.
335 * The patterns for each parameter can be set using {@link getParameters Parameters}
336 * attribute collection. For example
337 * <code>
338 * <url ... pattern="articles/{year}/{month}/{day}"
339 *          parameters.year="\d{4}" parameters.month="\d{2}" parameters.day="\d+" />
340 * </code>
341 *
342 * In the above example, the pattern contains 3 parameters named "year",
343 * "month" and "day". The pattern for these parameters are, respectively,
344 * "\d{4}" (4 digits), "\d{2}" (2 digits) and "\d+" (1 or more digits).
345 * Essentially, the <tt>Parameters</tt> attribute name and values are used
346 * as substrings in replacing the placeholders in the <tt>Pattern</tt> string
347 * to form a complete regular expression string.
348 *
349 * For more complicated patterns, one may specify the pattern using a regular expression
350 * by {@link setRegularExpression RegularExpression}. For example, the above pattern
351 * is equivalent to the following regular expression-based pattern:
352 * <code>
353 * /^articles\/(?P<year>\d{4})\/(?P<month>\d{2})\/(?P<day>\d+)$/u
354 * </code>
355 * The above regular expression used the "named group" feature available in PHP.
356 * Notice that you need to escape the slash in regular expressions.
357 *
358 * Thus, only an url that matches the pattern will be valid. For example,
359 * a URL <tt>http://example.com/index.php/articles/2006/07/21</tt> will match the above pattern,
360 * while <tt>http://example.com/index.php/articles/2006/07/hello</tt> will not
361 * since the "day" parameter pattern is not satisfied.
362 *
363 * The parameter values are available through the <tt>THttpRequest</tt> instance (e.g.
364 * <tt>$this->Request['year']</tt>).
365 *
366 * The {@link setServiceParameter ServiceParameter} and {@link setServiceID ServiceID}
367 * (the default ID is 'page') set the service parameter and service id respectively.
368 *
369 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
370 * @version $Id$
371 * @package System.Web
372 * @since 3.0.5
373 */
374class TUrlMappingPattern extends TComponent
375{
376    /**
377     * @var string service parameter such as Page class name.
378     */
379    private $_serviceParameter;
380    /**
381     * @var string service ID, default is 'page'.
382     */
383    private $_serviceID='page';
384    /**
385     * @var string url pattern to match.
386     */
387    private $_pattern;
388    /**
389     * @var TMap parameter regular expressions.
390     */
391    private $_parameters;
392    /**
393     * @var string regular expression pattern.
394     */
395    private $_regexp='';
396
397    private $_customUrl=true;
398
399    private $_manager;
400
401    private $_caseSensitive=true;
402
403    /**
404     * Constructor.
405     * @param TUrlManager the URL manager instance
406     */
407    public function __construct(TUrlManager $manager)
408    {
409        $this->_manager=$manager;
410        $this->_parameters=new TAttributeCollection;
411        $this->_parameters->setCaseSensitive(true);
412    }
413
414    /**
415     * @return TUrlManager the URL manager instance
416     */
417    public function getManager()
418    {
419        return $this->_manager;
420    }
421
422    /**
423     * Initializes the pattern.
424     * @param TXmlElement configuration for this module.
425     * @throws TConfigurationException if service parameter is not specified
426     */
427    public function init($config)
428    {
429        if($this->_serviceParameter===null)
430            throw new TConfigurationException('urlmappingpattern_serviceparameter_required', $this->getPattern());
431    }
432
433    /**
434     * Substitute the parameter key value pairs as named groupings
435     * in the regular expression matching pattern.
436     * @return string regular expression pattern with parameter subsitution
437     */
438    protected function getParameterizedPattern()
439    {
440        $params=array();
441        $values=array();
442        foreach($this->_parameters as $key=>$value)
443        {
444            $params[]='{'.$key.'}';
445            $values[]='(?P<'.$key.'>'.$value.')';
446        }
447        $params[]='/';
448        $values[]='\\/';
449        $regexp=str_replace($params,$values,trim($this->getPattern(),'/').'/');
450        $regexp='/^'.$regexp.'$/u';
451        if(!$this->getCaseSensitive())
452            $regexp.='i';
453        return $regexp;
454    }
455
456    /**
457     * @return string full regular expression mapping pattern
458     */
459    public function getRegularExpression()
460    {
461        return $this->_regexp;
462    }
463
464    /**
465     * @param string full regular expression mapping pattern.
466     */
467    public function setRegularExpression($value)
468    {
469        $this->_regexp=$value;
470    }
471
472    /**
473     * @return boolean whether the {@link getPattern Pattern} should be treated as case sensititve. Defaults to true.
474     */
475    public function getCaseSensitive()
476    {
477        return $this->_caseSensitive;
478    }
479
480    /**
481     * @param boolean whether the {@link getPattern Pattern} should be treated as case sensititve.
482     */
483    public function setCaseSensitive($value)
484    {
485        $this->_caseSensitive=TPropertyValue::ensureBoolean($value);
486    }
487
488    /**
489     * @param string service parameter, such as page class name.
490     */
491    public function setServiceParameter($value)
492    {
493        $this->_serviceParameter=$value;
494    }
495
496    /**
497     * @return string service parameter, such as page class name.
498     */
499    public function getServiceParameter()
500    {
501        return $this->_serviceParameter;
502    }
503
504    /**
505     * @param string service id to handle.
506     */
507    public function setServiceID($value)
508    {
509        $this->_serviceID=$value;
510    }
511
512    /**
513     * @return string service id.
514     */
515    public function getServiceID()
516    {
517        return $this->_serviceID;
518    }
519
520    /**
521     * @return string url pattern to match. Defaults to ''.
522     */
523    public function getPattern()
524    {
525        return $this->_pattern;
526    }
527
528    /**
529     * @param string url pattern to match.
530     */
531    public function setPattern($value)
532    {
533        $this->_pattern = $value;
534    }
535
536    /**
537     * @return TAttributeCollection parameter key value pairs.
538     */
539    public function getParameters()
540    {
541        return $this->_parameters;
542    }
543
544    /**
545     * @param TAttributeCollection new parameter key value pairs.
546     */
547    public function setParameters($value)
548    {
549        $this->_parameters=$value;
550    }
551
552    /**
553     * Uses URL pattern (or full regular expression if available) to
554     * match the given url path.
555     * @param THttpRequest the request module
556     * @return array matched parameters, empty if no matches.
557     */
558    public function getPatternMatches($request)
559    {
560        $matches=array();
561        if(($pattern=$this->getRegularExpression())!=='')
562            preg_match($pattern,$request->getPathInfo(),$matches);
563        else
564            preg_match($this->getParameterizedPattern(),trim($request->getPathInfo(),'/').'/',$matches);
565        return $matches;
566    }
567
568    /**
569     * Returns a value indicating whether to use this pattern to construct URL.
570     * @return boolean whether to enable custom constructUrl. Defaults to true.
571     * @since 3.1.1
572     */
573    public function getEnableCustomUrl()
574    {
575        return $this->_customUrl;
576    }
577
578    /**
579     * Sets a value indicating whether to enable custom constructUrl using this pattern
580     * @param boolean whether to enable custom constructUrl.
581     */
582    public function setEnableCustomUrl($value)
583    {
584        $this->_customUrl=TPropertyValue::ensureBoolean($value);
585    }
586
587    /**
588     * @param array list of GET items to be put in the constructed URL
589     * @return boolean whether this pattern IS the one for constructing the URL with the specified GET items.
590     * @since 3.1.1
591     */
592    public function supportCustomUrl($getItems)
593    {
594        if(!$this->_customUrl || $this->getPattern()===null)
595            return false;
596        foreach($this->_parameters as $key=>$value)
597        {
598            if(!isset($getItems[$key]))
599                return false;
600        }
601        return true;
602    }
603
604    /**
605     * Constructs a URL using this pattern.
606     * @param array list of GET variables
607     * @param boolean whether the ampersand should be encoded in the constructed URL
608     * @param boolean whether the GET variables should be encoded in the constructed URL
609     * @return string the constructed URL
610     * @since 3.1.1
611     */
612    public function constructUrl($getItems,$encodeAmpersand,$encodeGetItems)
613    {
614        $extra=array();
615        $replace=array();
616        // for the GET variables matching the pattern, put them in the URL path
617        foreach($getItems as $key=>$value)
618        {
619            if($encodeGetItems)
620                $value=rawurlencode($value);
621            if($this->_parameters->contains($key))
622                $replace['{'.$key.'}']=$value;
623            else
624                $extra[$key]=$value;
625        }
626
627        $url=$this->_manager->getUrlPrefix().'/'.ltrim(strtr($this->getPattern(),$replace),'/');
628
629        // for the rest of the GET variables, put them in the query string
630        if(count($extra)>0)
631        {
632            $url2='';
633            $amp=$encodeAmpersand?'&amp;':'&';
634            if($encodeGetItems)
635            {
636                foreach($extra as $name=>$value)
637                {
638                    if(is_array($value))
639                    {
640                        $name=rawurlencode($name.'[]');
641                        foreach($value as $v)
642                            $url2.=$amp.$name.'='.rawurlencode($v);
643                    }
644                    else
645                        $url2.=$amp.rawurlencode($name).'='.rawurlencode($value);
646                }
647            }
648            else
649            {
650                foreach($extra as $name=>$value)
651                {
652                    if(is_array($value))
653                    {
654                        foreach($value as $v)
655                            $url2.=$amp.$name.'[]='.$v;
656                    }
657                    else
658                        $url2.=$amp.$name.'='.$value;
659                }
660            }
661            $url=$url.'?'.substr($url2,strlen($amp));
662        }
663        return $url;
664    }
665