Issues (1474)

framework/Web/TUrlMapping.php (2 issues)

Severity
1
<?php
2
3
/**
4
 * TUrlMapping, TUrlMappingPattern and TUrlMappingPatternSecureConnection class file.
5
 *
6
 * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Web;
12
13
use Prado\Exceptions\TConfigurationException;
14
use Prado\Prado;
15
use Prado\TApplication;
16
use Prado\TPropertyValue;
17
use Prado\Xml\TXmlDocument;
18
use Prado\Xml\TXmlElement;
19
20
/**
21
 * TUrlMapping Class
22
 *
23
 * The TUrlMapping module allows PRADO to construct and recognize URLs
24
 * based on specific patterns.
25
 *
26
 * TUrlMapping consists of a list of URL patterns which are used to match
27
 * against the currently requested URL. The first matching pattern will then
28
 * be used to decompose the URL into request parameters (accessible through
29
 * ```php
30
 * $this->Request['paramname']
31
 * ```
32
 * )
33
 *
34
 * The patterns can also be used to construct customized URLs. In this case,
35
 * the parameters in an applied pattern will be replaced with the corresponding
36
 * GET variable values.
37
 *
38
 * Since it is derived from {@see \Prado\Web\TUrlManager}, it should be configured globally
39
 * in the application configuration like the following,
40
 * ```xml
41
 *  <module id="request" class="THttpRequest" UrlManager="friendly-url" />
42
 *  <module id="friendly-url" class="Prado\Web.TUrlMapping" EnableCustomUrl="true">
43
 *    <url ServiceParameter="Posts.ViewPost" pattern="post/{id}/" parameters.id="\d+" />
44
 *    <url ServiceParameter="Posts.ListPost" pattern="archive/{time}/" parameters.time="\d{6}" />
45
 *    <url ServiceParameter="Posts.ListPost" pattern="category/{cat}/" parameters.cat="\d+" />
46
 *  </module>
47
 * ```
48
 *
49
 * In the above, each <tt>&lt;url&gt;</tt> element specifies a URL pattern represented
50
 * as a {@see \Prado\Web\TUrlMappingPattern} internally. You may create your own pattern classes
51
 * by extending {@see \Prado\Web\TUrlMappingPattern} and specifying the <tt>&lt;class&gt;</tt> attribute
52
 * in the element.
53
 *
54
 * The patterns can be also be specified in an external file using the {@see setConfigFile ConfigFile} property.
55
 *
56
 * The URL mapping are evaluated in order, only the first mapping that matches
57
 * the URL will be used. Cascaded mapping can be achieved by placing the URL mappings
58
 * in particular order. For example, placing the most specific mappings first.
59
 *
60
 * Only the PATH_INFO part of the URL is used to match the available patterns. The matching
61
 * is strict in the sense that the whole pattern must match the whole PATH_INFO of the URL.
62
 *
63
 * From PRADO v3.1.1, TUrlMapping also provides support for constructing URLs according to
64
 * the specified pattern. You may enable this functionality by setting {@see setEnableCustomUrl EnableCustomUrl} to true.
65
 * When you call THttpRequest::constructUrl() (or via TPageService::constructUrl()),
66
 * TUrlMapping will examine the available URL mapping patterns using their {@see \Prado\Web\TUrlMappingPattern::getServiceParameter ServiceParameter}
67
 * and {@see \Prado\Web\TUrlMappingPattern::getPattern Pattern} properties. A pattern is applied if its
68
 * {@see \Prado\Web\TUrlMappingPattern::getServiceParameter ServiceParameter} matches the service parameter passed
69
 * to constructUrl() and every parameter in the {@see getPattern Pattern} is found
70
 * in the GET variables.
71
 *
72
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
73
 * @since 3.0.5
74
 */
75
class TUrlMapping extends TUrlManager
76
{
77
	/**
78
	 * @var TUrlMappingPattern[] list of patterns.
79
	 */
80
	protected $_patterns = [];
81
	/**
82
	 * @var TUrlMappingPattern matched pattern.
83
	 */
84
	private $_matched;
85
	/**
86
	 * @var string external configuration file
87
	 */
88
	private $_configFile;
89
	/**
90
	 * @var bool whether to enable custom contructUrl
91
	 */
92
	private $_customUrl = false;
93
	/**
94
	 * @var array rules for constructing URLs
95
	 */
96
	protected $_constructRules = [];
97
98
	private $_urlPrefix = '';
99
100
	private $_defaultMappingClass = \Prado\Web\TUrlMappingPattern::class;
101
102
	/**
103
	 * Initializes this module.
104
	 * This method is required by the IModule interface.
105
	 * @param mixed $config configuration for this module, can be null
106 1
	 * @throws TConfigurationException if module is configured in the global scope.
107
	 */
108 1
	public function init($config)
109 1
	{
110
		parent::init($config);
111
		if ($this->getRequest()->getRequestResolved()) {
112 1
			throw new TConfigurationException('urlmapping_global_required');
113
		}
114
		if ($this->_configFile !== null) {
115 1
			$this->loadConfigFile();
116 1
		}
117 1
		$this->loadUrlMappings($config);
118 1
		if ($this->_urlPrefix === '') {
119
			$request = $this->getRequest();
120
			if ($request->getUrlFormat() === THttpRequestUrlFormat::HiddenPath) {
0 ignored issues
show
The condition $request->getUrlFormat()...stUrlFormat::HiddenPath is always false.
Loading history...
121 1
				$this->_urlPrefix = dirname($request->getApplicationUrl());
122
			} else {
123
				$this->_urlPrefix = $request->getApplicationUrl();
124 1
			}
125 1
		}
126
		$this->_urlPrefix = rtrim($this->_urlPrefix, '/');
127
	}
128
129
	/**
130
	 * Initialize the module from configuration file.
131
	 * @throws TConfigurationException if {@see getConfigFile ConfigFile} is invalid.
132
	 */
133
	protected function loadConfigFile()
134
	{
135
		if (is_file($this->_configFile)) {
136
			if ($this->getApplication()->getConfigurationType() == TApplication::CONFIG_TYPE_PHP) {
137
				$config = include $this->_configFile;
138
				$this->loadUrlMappings($config);
139
			} else {
140
				$dom = new TXmlDocument();
141
				$dom->loadFromFile($this->_configFile);
142
				$this->loadUrlMappings($dom);
143
			}
144
		} else {
145
			throw new TConfigurationException('urlmapping_configfile_inexistent', $this->_configFile);
146
		}
147
	}
148
149
	/**
150
	 * Returns a value indicating whether to enable custom constructUrl.
151
	 * If true, constructUrl() will make use of the URL mapping rules to
152
	 * construct valid URLs.
153
	 * @return bool whether to enable custom constructUrl. Defaults to false.
154
	 * @since 3.1.1
155
	 */
156
	public function getEnableCustomUrl()
157
	{
158
		return $this->_customUrl;
159
	}
160
161
	/**
162
	 * Sets a value indicating whether to enable custom constructUrl.
163
	 * If true, constructUrl() will make use of the URL mapping rules to
164
	 * construct valid URLs.
165
	 * @param bool $value whether to enable custom constructUrl.
166
	 * @since 3.1.1
167
	 */
168
	public function setEnableCustomUrl($value)
169
	{
170
		$this->_customUrl = TPropertyValue::ensureBoolean($value);
171
	}
172
173
	/**
174
	 * @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)
175
	 * @since 3.1.1
176
	 */
177
	public function getUrlPrefix()
178
	{
179
		return $this->_urlPrefix;
180
	}
181
182
	/**
183
	 * @param string $value the part that will be prefixed to the constructed URLs. This is used by constructUrl() when EnableCustomUrl is set true.
184
	 * @see getUrlPrefix
185
	 * @since 3.1.1
186
	 */
187
	public function setUrlPrefix($value)
188
	{
189
		$this->_urlPrefix = $value;
190
	}
191
192
	/**
193
	 * @return string external configuration file. Defaults to null.
194
	 */
195
	public function getConfigFile()
196
	{
197
		return $this->_configFile;
198
	}
199
200
	/**
201
	 * @param string $value external configuration file in namespace format. The file
202
	 * must be suffixed with '.xml'.
203
	 * @throws TConfigurationException if the file is invalid.
204
	 */
205
	public function setConfigFile($value)
206
	{
207
		if (($this->_configFile = Prado::getPathOfNamespace($value, $this->getApplication()->getConfigurationFileExt())) === null) {
208
			throw new TConfigurationException('urlmapping_configfile_invalid', $value);
209
		}
210
	}
211
212
	/**
213
	 * @return string the default class of URL mapping patterns. Defaults to TUrlMappingPattern.
214 1
	 * @since 3.1.1
215
	 */
216 1
	public function getDefaultMappingClass()
217
	{
218
		return $this->_defaultMappingClass;
219
	}
220
221
	/**
222
	 * Sets the default class of URL mapping patterns.
223
	 * When a URL matching pattern does not specify "class" attribute, it will default to the class
224
	 * 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.)
225
	 * @param string $value the default class of URL mapping patterns.
226
	 * @since 3.1.1
227
	 */
228
	public function setDefaultMappingClass($value)
229
	{
230
		$this->_defaultMappingClass = $value;
231
	}
232
233
	/**
234
	 * Load and configure each url mapping pattern.
235
	 * @param mixed $config configuration node
236 1
	 * @throws TConfigurationException if specific pattern class is invalid
237
	 */
238 1
	protected function loadUrlMappings($config)
239
	{
240 1
		$defaultClass = $this->getDefaultMappingClass();
241
242
		if (is_array($config)) {
243
			if (isset($config['urls']) && is_array($config['urls'])) {
244
				foreach ($config['urls'] as $url) {
245
					$class = $url['class'] ?? $defaultClass;
246
					$properties = $url['properties'] ?? [];
247
					$this->buildUrlMapping($class, $properties, $url);
248
				}
249 1
			}
250 1
		} else {
251 1
			foreach ($config->getElementsByTagName('url') as $url) {
252 1
				$properties = $url->getAttributes();
253
				if (($class = $properties->remove('class')) === null) {
254 1
					$class = $defaultClass;
255
				}
256
				$this->buildUrlMapping($class, $properties, $url);
257 1
			}
258
		}
259 1
	}
260
261 1
	private function buildUrlMapping($class, $properties, $url)
262 1
	{
263
		$pattern = Prado::createComponent($class, $this);
264
		if (!($pattern instanceof TUrlMappingPattern)) {
265 1
			throw new TConfigurationException('urlmapping_urlmappingpattern_required');
266 1
		}
267
		foreach ($properties as $name => $value) {
268
			$pattern->setSubproperty($name, $value);
269 1
		}
270 1
271 1
		if ($url instanceof TXmlElement) {
272
			$text = $url -> getValue();
273
			if ($text) {
274
				$text = preg_replace('/(\s+)/S', '', $text);
275
				if (($regExp = $pattern->getRegularExpression()) !== '') {
276
					trigger_error(
277
						sPrintF(
278
							'%s.RegularExpression property value "%s" for ServiceID="%s" and ServiceParameter="%s" was replaced by node value "%s"',
279
							$pattern::class,
280
							$regExp,
281
							$pattern->getServiceID(),
282
							$pattern->getServiceParameter(),
283
							$text
284
						),
285
						E_USER_NOTICE
286
					);
287
				}
288
				$pattern->setRegularExpression($text);
289
			}
290 1
		}
291 1
292
		$this->_patterns[] = $pattern;
293 1
		$pattern->init($url);
294 1
295 1
		$key = $pattern->getServiceID() . ':' . $pattern->getServiceParameter();
296
		$this->_constructRules[$key][] = $pattern;
297
	}
298
299
	/**
300
	 * Parses the request URL and returns an array of input parameters.
301
	 * This method overrides the parent implementation.
302
	 * The input parameters do not include GET and POST variables.
303
	 * This method uses the request URL path to find the first matching pattern. If found
304
	 * the matched pattern parameters are used to return as the input parameters.
305 1
	 * @return array list of input parameters
306
	 */
307 1
	public function parseUrl()
308 1
	{
309 1
		$request = $this->getRequest();
310 1
		foreach ($this->_patterns as $pattern) {
311 1
			$matches = $pattern->getPatternMatches($request);
312 1
			if (count($matches) > 0) {
313 1
				$this->_matched = $pattern;
314 1
				$params = [];
315 1
				foreach ($matches as $key => $value) {
316
					if (is_string($key)) {
317
						$params[$key] = $value;
318 1
					}
319 1
				}
320
				if (!$pattern->getIsWildCardPattern()) {
321 1
					$params[$pattern->getServiceID()] = $pattern->getServiceParameter();
322
				}
323
				return $params;
324
			}
325
		}
326
		return parent::parseUrl();
327
	}
328
329
	/**
330
	 * Constructs a URL that can be recognized by PRADO.
331
	 *
332
	 * This method provides the actual implementation used by {@see \Prado\Web\THttpRequest::constructUrl}.
333
	 * Override this method if you want to provide your own way of URL formatting.
334
	 * If you do so, you may also need to override {@see parseUrl} so that the URL can be properly parsed.
335
	 *
336
	 * The URL is constructed as the following format:
337
	 * /entryscript.php?serviceID=serviceParameter&get1=value1&...
338
	 * If {@see \Prado\Web\THttpRequest::setUrlFormat THttpRequest.UrlFormat} is 'Path',
339
	 * the following format is used instead:
340
	 * /entryscript.php/serviceID/serviceParameter/get1,value1/get2,value2...
341
	 * If {@see \Prado\Web\THttpRequest::setUrlFormat THttpRequest.UrlFormat} is 'HiddenPath',
342
	 * the following format is used instead:
343
	 * /serviceID/serviceParameter/get1,value1/get2,value2...
344
	 * @param string $serviceID service ID
345
	 * @param string $serviceParam service parameter
346
	 * @param array $getItems GET parameters, null if not provided
347
	 * @param bool $encodeAmpersand whether to encode the ampersand in URL
348
	 * @param bool $encodeGetItems whether to encode the GET parameters (their names and values)
349
	 * @return string URL
350
	 * @see parseUrl
351
	 * @since 3.1.1
352
	 */
353
	public function constructUrl($serviceID, $serviceParam, $getItems, $encodeAmpersand, $encodeGetItems)
354
	{
355
		if ($this->_customUrl) {
356
			if (!(is_array($getItems) || ($getItems instanceof \Traversable))) {
0 ignored issues
show
The condition is_array($getItems) is always true.
Loading history...
357
				$getItems = [];
358
			}
359
			$key = $serviceID . ':' . $serviceParam;
360
			$wildCardKey = ($pos = strrpos($serviceParam, '.')) !== false ?
361
				$serviceID . ':' . substr($serviceParam, 0, $pos) . '.*' : $serviceID . ':*';
362
			if (isset($this->_constructRules[$key])) {
363
				foreach ($this->_constructRules[$key] as $rule) {
364
					if ($rule->supportCustomUrl($getItems)) {
365
						return $rule->constructUrl($getItems, $encodeAmpersand, $encodeGetItems);
366
					}
367
				}
368
			} elseif (isset($this->_constructRules[$wildCardKey])) {
369
				foreach ($this->_constructRules[$wildCardKey] as $rule) {
370
					if ($rule->supportCustomUrl($getItems)) {
371
						$getItems['*'] = $pos ? substr($serviceParam, $pos + 1) : $serviceParam;
372
						return $rule->constructUrl($getItems, $encodeAmpersand, $encodeGetItems);
373
					}
374
				}
375
			}
376
		}
377
		return parent::constructUrl($serviceID, $serviceParam, $getItems, $encodeAmpersand, $encodeGetItems);
378
	}
379
380
	/**
381
	 * @return TUrlMappingPattern the matched pattern, null if not found.
382
	 */
383
	public function getMatchingPattern()
384
	{
385
		return $this->_matched;
386
	}
387
}
388