summaryrefslogtreecommitdiff
path: root/framework/Web/TUrlMapping.php
blob: 278e459f19f33ece10f99cd0fe83fba79fe3a87c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
<?php
/**
 * TUrlMapping, TUrlMappingPattern and TUrlMappingPatternSecureConnection class file.
 *
 * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2014 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @package Prado\Web
 */

namespace Prado\Web;
use Prado\Exceptions\TConfigurationException;
use Prado\Prado;
use Prado\TApplication;
use Prado\TPropertyValue;
use Prado\Xml\TXmlDocument;
use Prado\Xml\TXmlElement;

/**
 * TUrlMapping Class
 *
 * The TUrlMapping module allows PRADO to construct and recognize URLs
 * based on specific patterns.
 *
 * TUrlMapping consists of a list of URL patterns which are used to match
 * against the currently requested URL. The first matching pattern will then
 * be used to decompose the URL into request parameters (accessible through
 * <code>$this->Request['paramname']</code>).
 *
 * The patterns can also be used to construct customized URLs. In this case,
 * the parameters in an applied pattern will be replaced with the corresponding
 * GET variable values.
 *
 * Since it is derived from {@link TUrlManager}, it should be configured globally
 * in the application configuration like the following,
 * <code>
 *  <module id="request" class="THttpRequest" UrlManager="friendly-url" />
 *  <module id="friendly-url" class="System.Web.TUrlMapping" EnableCustomUrl="true">
 *    <url ServiceParameter="Posts.ViewPost" pattern="post/{id}/" parameters.id="\d+" />
 *    <url ServiceParameter="Posts.ListPost" pattern="archive/{time}/" parameters.time="\d{6}" />
 *    <url ServiceParameter="Posts.ListPost" pattern="category/{cat}/" parameters.cat="\d+" />
 *  </module>
 * </code>
 *
 * In the above, each <tt>&lt;url&gt;</tt> element specifies a URL pattern represented
 * as a {@link TUrlMappingPattern} internally. You may create your own pattern classes
 * by extending {@link TUrlMappingPattern} and specifying the <tt>&lt;class&gt;</tt> attribute
 * in the element.
 *
 * The patterns can be also be specified in an external file using the {@link setConfigFile ConfigFile} property.
 *
 * The URL mapping are evaluated in order, only the first mapping that matches
 * the URL will be used. Cascaded mapping can be achieved by placing the URL mappings
 * in particular order. For example, placing the most specific mappings first.
 *
 * Only the PATH_INFO part of the URL is used to match the available patterns. The matching
 * is strict in the sense that the whole pattern must match the whole PATH_INFO of the URL.
 *
 * From PRADO v3.1.1, TUrlMapping also provides support for constructing URLs according to
 * the specified pattern. You may enable this functionality by setting {@link setEnableCustomUrl EnableCustomUrl} to true.
 * When you call THttpRequest::constructUrl() (or via TPageService::constructUrl()),
 * TUrlMapping will examine the available URL mapping patterns using their {@link TUrlMappingPattern::getServiceParameter ServiceParameter}
 * and {@link TUrlMappingPattern::getPattern Pattern} properties. A pattern is applied if its
 * {@link TUrlMappingPattern::getServiceParameter ServiceParameter} matches the service parameter passed
 * to constructUrl() and every parameter in the {@link getPattern Pattern} is found
 * in the GET variables.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @package Prado\Web
 * @since 3.0.5
 */
class TUrlMapping extends TUrlManager
{
	/**
	 * @var TUrlMappingPattern[] list of patterns.
	 */
	protected $_patterns=array();
	/**
	 * @var TUrlMappingPattern matched pattern.
	 */
	private $_matched;
	/**
	 * @var string external configuration file
	 */
	private $_configFile=null;
	/**
	 * @var boolean whether to enable custom contructUrl
	 */
	private $_customUrl=false;
	/**
	 * @var array rules for constructing URLs
	 */
	protected $_constructRules=array();

	private $_urlPrefix='';

	private $_defaultMappingClass='TUrlMappingPattern';

	/**
	 * Initializes this module.
	 * This method is required by the IModule interface.
	 * @param mixed configuration for this module, can be null
	 * @throws TConfigurationException if module is configured in the global scope.
	 */
	public function init($config)
	{
		parent::init($config);
		if($this->getRequest()->getRequestResolved())
			throw new TConfigurationException('urlmapping_global_required');
		if($this->_configFile!==null)
			$this->loadConfigFile();
		$this->loadUrlMappings($config);
		if($this->_urlPrefix==='')
		{
			$request=$this->getRequest();
			if($request->getUrlFormat()===THttpRequestUrlFormat::HiddenPath)
			{
				$this->_urlPrefix=dirname($request->getApplicationUrl());
			} else {
				$this->_urlPrefix=$request->getApplicationUrl();
			}
		}
		$this->_urlPrefix=rtrim($this->_urlPrefix,'/');
	}

	/**
	 * Initialize the module from configuration file.
	 * @throws TConfigurationException if {@link getConfigFile ConfigFile} is invalid.
	 */
	protected function loadConfigFile()
	{
		if(is_file($this->_configFile))
		{
			if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP)
			{
				$config = include $this->_configFile;
				$this->loadUrlMappings($dom);
			}
			else
			{
				$dom=new TXmlDocument;
				$dom->loadFromFile($this->_configFile);
				$this->loadUrlMappings($dom);
			}
		}
		else
			throw new TConfigurationException('urlmapping_configfile_inexistent',$this->_configFile);
	}

	/**
	 * Returns a value indicating whether to enable custom constructUrl.
	 * If true, constructUrl() will make use of the URL mapping rules to
	 * construct valid URLs.
	 * @return boolean whether to enable custom constructUrl. Defaults to false.
	 * @since 3.1.1
	 */
	public function getEnableCustomUrl()
	{
		return $this->_customUrl;
	}

	/**
	 * Sets a value indicating whether to enable custom constructUrl.
	 * If true, constructUrl() will make use of the URL mapping rules to
	 * construct valid URLs.
	 * @param boolean whether to enable custom constructUrl.
	 * @since 3.1.1
	 */
	public function setEnableCustomUrl($value)
	{
		$this->_customUrl=TPropertyValue::ensureBoolean($value);
	}

	/**
	 * @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)
	 * @since 3.1.1
	 */
	public function getUrlPrefix()
	{
		return $this->_urlPrefix;
	}

	/**
	 * @param string the part that will be prefixed to the constructed URLs. This is used by constructUrl() when EnableCustomUrl is set true.
	 * @see getUrlPrefix
	 * @since 3.1.1
	 */
	public function setUrlPrefix($value)
	{
		$this->_urlPrefix=$value;
	}

	/**
	 * @return string external configuration file. Defaults to null.
	 */
	public function getConfigFile()
	{
		return $this->_configFile;
	}

	/**
	 * @param string external configuration file in namespace format. The file
	 * must be suffixed with '.xml'.
	 * @throws TInvalidDataValueException if the file is invalid.
	 */
	public function setConfigFile($value)
	{
		if(($this->_configFile=Prado::getPathOfNamespace($value,$this->getApplication()->getConfigurationFileExt()))===null)
			throw new TConfigurationException('urlmapping_configfile_invalid',$value);
	}

	/**
	 * @return string the default class of URL mapping patterns. Defaults to TUrlMappingPattern.
	 * @since 3.1.1
	 */
	public function getDefaultMappingClass()
	{
		return $this->_defaultMappingClass;
	}

	/**
	 * Sets the default class of URL mapping patterns.
	 * When a URL matching pattern does not specify "class" attribute, it will default to the class
	 * 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.)
	 * @param string the default class of URL mapping patterns.
	 * @since 3.1.1
	 */
	public function setDefaultMappingClass($value)
	{
		$this->_defaultMappingClass=$value;
	}

	/**
	 * Load and configure each url mapping pattern.
	 * @param mixed configuration node
	 * @throws TConfigurationException if specific pattern class is invalid
	 */
	protected function loadUrlMappings($config)
	{
		$defaultClass = $this->getDefaultMappingClass();

		if(is_array($config))
		{
			if(isset($config['urls']) && is_array($config['urls']))
			{
				foreach($config['urls'] as $url)
				{
					$class=isset($url['class'])?$url['class']:$defaultClass;
					$properties = isset($url['properties'])?$url['properties']:array();
					$this->buildUrlMapping($class,$properties,$url);
				}
			}
		}
		else
		{
			foreach($config->getElementsByTagName('url') as $url)
			{
				$properties=$url->getAttributes();
				if(($class=$properties->remove('class'))===null)
					$class=$defaultClass;
				$this->buildUrlMapping($class,$properties,$url);
			}
		}
	}

	private function buildUrlMapping($class, $properties, $url)
	{
		$pattern=Prado::createComponent($class,$this);
		if(!($pattern instanceof TUrlMappingPattern))
			throw new TConfigurationException('urlmapping_urlmappingpattern_required');
		foreach($properties as $name=>$value)
			$pattern->setSubproperty($name,$value);

		if($url instanceof TXmlElement) {
			$text = $url -> getValue();
			if($text) {
				$text = preg_replace('/(\s+)/S', '', $text);
				if(($regExp = $pattern->getRegularExpression()) !== '')
				trigger_error(sPrintF('%s.RegularExpression property value "%s" for ServiceID="%s" and ServiceParameter="%s" was replaced by node value "%s"',
				get_class($pattern),
				$regExp,
				$pattern->getServiceID(),
				$pattern->getServiceParameter(),
				$text),
				E_USER_NOTICE);
				$pattern->setRegularExpression($text);
			}
		}

		$this->_patterns[]=$pattern;
		$pattern->init($url);

		$key=$pattern->getServiceID().':'.$pattern->getServiceParameter();
		$this->_constructRules[$key][]=$pattern;
	}

	/**
	 * Parses the request URL and returns an array of input parameters.
	 * This method overrides the parent implementation.
	 * The input parameters do not include GET and POST variables.
	 * This method uses the request URL path to find the first matching pattern. If found
	 * the matched pattern parameters are used to return as the input parameters.
	 * @return array list of input parameters
	 */
	public function parseUrl()
	{
		$request=$this->getRequest();
		foreach($this->_patterns as $pattern)
		{
			$matches=$pattern->getPatternMatches($request);
			if(count($matches)>0)
			{
				$this->_matched=$pattern;
				$params=array();
				foreach($matches as $key=>$value)
				{
					if(is_string($key))
						$params[$key]=$value;
				}
				if (!$pattern->getIsWildCardPattern())
					$params[$pattern->getServiceID()]=$pattern->getServiceParameter();
				return $params;
			}
		}
		return parent::parseUrl();
	}

	/**
	 * Constructs a URL that can be recognized by PRADO.
	 *
	 * This method provides the actual implementation used by {@link THttpRequest::constructUrl}.
	 * Override this method if you want to provide your own way of URL formatting.
	 * If you do so, you may also need to override {@link parseUrl} so that the URL can be properly parsed.
	 *
	 * The URL is constructed as the following format:
	 * /entryscript.php?serviceID=serviceParameter&get1=value1&...
	 * If {@link THttpRequest::setUrlFormat THttpRequest.UrlFormat} is 'Path',
	 * the following format is used instead:
	 * /entryscript.php/serviceID/serviceParameter/get1,value1/get2,value2...
	 * If {@link THttpRequest::setUrlFormat THttpRequest.UrlFormat} is 'HiddenPath',
	 * the following format is used instead:
	 * /serviceID/serviceParameter/get1,value1/get2,value2...
	 * @param string service ID
	 * @param string service parameter
	 * @param array GET parameters, null if not provided
	 * @param boolean whether to encode the ampersand in URL
	 * @param boolean whether to encode the GET parameters (their names and values)
	 * @return string URL
	 * @see parseUrl
	 * @since 3.1.1
	 */
	public function constructUrl($serviceID,$serviceParam,$getItems,$encodeAmpersand,$encodeGetItems)
	{
		if($this->_customUrl)
		{
			if(!(is_array($getItems) || ($getItems instanceof \Traversable)))
				$getItems=array();
			$key=$serviceID.':'.$serviceParam;
			$wildCardKey = ($pos=strrpos($serviceParam,'.'))!==false ?
				$serviceID.':'.substr($serviceParam,0,$pos).'.*' : $serviceID.':*';
			if(isset($this->_constructRules[$key]))
			{
				foreach($this->_constructRules[$key] as $rule)
				{
					if($rule->supportCustomUrl($getItems))
						return $rule->constructUrl($getItems,$encodeAmpersand,$encodeGetItems);
				}
			}
			elseif(isset($this->_constructRules[$wildCardKey]))
			{
				foreach($this->_constructRules[$wildCardKey] as $rule)
				{
					if($rule->supportCustomUrl($getItems))
					{
						$getItems['*']= $pos ? substr($serviceParam,$pos+1) : $serviceParam;
						return $rule->constructUrl($getItems,$encodeAmpersand,$encodeGetItems);
					}
				}
			}
		}
		return parent::constructUrl($serviceID,$serviceParam,$getItems,$encodeAmpersand,$encodeGetItems);
	}

	/**
	 * @return TUrlMappingPattern the matched pattern, null if not found.
	 */
	public function getMatchingPattern()
	{
		return $this->_matched;
	}
}