1: <?php
2: /*****************************************************************************************
3: * X2Engine Open Source Edition is a customer relationship management program developed by
4: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
5: *
6: * This program is free software; you can redistribute it and/or modify it under
7: * the terms of the GNU Affero General Public License version 3 as published by the
8: * Free Software Foundation with the addition of the following permission added
9: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
11: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12: *
13: * This program is distributed in the hope that it will be useful, but WITHOUT
14: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16: * details.
17: *
18: * You should have received a copy of the GNU Affero General Public License along with
19: * this program; if not, see http://www.gnu.org/licenses or write to the Free
20: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21: * 02110-1301 USA.
22: *
23: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
24: * California 95067, USA. or at email address [email protected].
25: *
26: * The interactive user interfaces in modified source and object code versions
27: * of this program must display Appropriate Legal Notices, as required under
28: * Section 5 of the GNU Affero General Public License version 3.
29: *
30: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31: * these Appropriate Legal Notices must retain the display of the "Powered by
32: * X2Engine" logo. If the display of the logo is not reasonably feasible for
33: * technical reasons, the Appropriate Legal Notices must display the words
34: * "Powered by X2Engine".
35: *****************************************************************************************/
36: /**
37: * Custom URL parsing class
38: *
39: * The only change which has been made is the modification of a regex flag in URL
40: * parsing. Ctrl + F for "X2CHANGE" to see the location of this change.
41: *
42: * @package application.components
43: */
44: class X2UrlRule extends CUrlRule
45: {
46: /**
47: * @var string the URL suffix used for this rule.
48: * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
49: * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}.
50: */
51: public $urlSuffix;
52: /**
53: * @var boolean whether the rule is case sensitive. Defaults to null, meaning
54: * using the value of {@link CUrlManager::caseSensitive}.
55: */
56: public $caseSensitive;
57: /**
58: * @var array the default GET parameters (name=>value) that this rule provides.
59: * When this rule is used to parse the incoming request, the values declared in this property
60: * will be injected into $_GET.
61: */
62: public $defaultParams=array();
63: /**
64: * @var boolean whether the GET parameter values should match the corresponding
65: * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value
66: * of {@link CUrlManager::matchValue}. When this property is false, it means
67: * a rule will be used for creating a URL if its route and parameter names match the given ones.
68: * If this property is set true, then the given parameter values must also match the corresponding
69: * parameter sub-patterns. Note that setting this property to true will degrade performance.
70: * @since 1.1.0
71: */
72: public $matchValue;
73: /**
74: * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match.
75: * If this rule can match multiple verbs, please separate them with commas.
76: * If this property is not set, the rule can match any verb.
77: * Note that this property is only used when parsing a request. It is ignored for URL creation.
78: * @since 1.1.7
79: */
80: public $verb;
81: /**
82: * @var boolean whether this rule is only used for request parsing.
83: * Defaults to false, meaning the rule is used for both URL parsing and creation.
84: * @since 1.1.7
85: */
86: public $parsingOnly=false;
87: /**
88: * @var string the controller/action pair
89: */
90: public $route;
91: /**
92: * @var array the mapping from route param name to token name (e.g. _r1=><1>)
93: */
94: public $references=array();
95: /**
96: * @var string the pattern used to match route
97: */
98: public $routePattern;
99: /**
100: * @var string regular expression used to parse a URL
101: */
102: public $pattern;
103: /**
104: * @var string template used to construct a URL
105: */
106: public $template;
107: /**
108: * @var array list of parameters (name=>regular expression)
109: */
110: public $params=array();
111: /**
112: * @var boolean whether the URL allows additional parameters at the end of the path info.
113: */
114: public $append;
115: /**
116: * @var boolean whether host info should be considered for this rule
117: */
118: public $hasHostInfo;
119:
120: /**
121: * Constructor.
122: * @param string $route the route of the URL (controller/action)
123: * @param string $pattern the pattern for matching the URL
124: */
125: public function __construct($route,$pattern)
126: {
127: if(is_array($route))
128: {
129: foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name)
130: {
131: if(isset($route[$name]))
132: $this->$name=$route[$name];
133: }
134: if(isset($route['pattern']))
135: $pattern=$route['pattern'];
136: $route=$route[0];
137: }
138: $this->route=trim($route,'/');
139:
140: $tr2['/']=$tr['/']='\\/';
141:
142: if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2))
143: {
144: foreach($matches2[1] as $name)
145: $this->references[$name]="<$name>";
146: }
147:
148: $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8);
149:
150: if($this->verb!==null)
151: $this->verb=preg_split('/[\s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY);
152:
153: if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches))
154: {
155: $tokens=array_combine($matches[1],$matches[2]);
156: foreach($tokens as $name=>$value)
157: {
158: if($value==='')
159: $value='[^\/]+';
160: $tr["<$name>"]="(?P<$name>$value)";
161: if(isset($this->references[$name]))
162: $tr2["<$name>"]=$tr["<$name>"];
163: else
164: $this->params[$name]=$value;
165: }
166: }
167: $p=rtrim($pattern,'*');
168: $this->append=$p!==$pattern;
169: $p=trim($p,'/');
170: $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p);
171: $this->pattern='/^'.strtr($this->template,$tr).'\/';
172: if($this->append)
173: $this->pattern.='/u';
174: else
175: $this->pattern.='$/u';
176:
177: // X2CHANGE
178: /*
179: * Because of the way our URLs work that we want to hide module/controller
180: * names from our path so that we see "contacts/index" instead of
181: * "contacts/contacts/index" we need to allow for multiple named subpatterns
182: * in a regex with the same name. The only way to do this is to add the
183: * "?J" modifier to the start of the pattern, which has been added in the
184: * routePattern property below.
185: */
186: if($this->references!==array())
187: $this->routePattern='/^(?J)'.strtr($this->route,$tr2).'$/u';
188:
189: if(YII_DEBUG && @preg_match($this->pattern,'test')===false)
190: throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.',
191: array('{route}'=>$route,'{pattern}'=>$pattern)));
192: }
193:
194: /**
195: * Creates a URL based on this rule.
196: * @param CUrlManager $manager the manager
197: * @param string $route the route
198: * @param array $params list of parameters
199: * @param string $ampersand the token separating name-value pairs in the URL.
200: * @return mixed the constructed URL or false on error
201: */
202: public function createUrl($manager,$route,$params,$ampersand)
203: {
204: if($this->parsingOnly)
205: return false;
206:
207: if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive)
208: $case='';
209: else
210: $case='i';
211:
212: $tr=array();
213: if($route!==$this->route)
214: {
215: if($this->routePattern!==null && preg_match($this->routePattern.$case,$route,$matches))
216: {
217: foreach($this->references as $key=>$name)
218: $tr[$name]=$matches[$key];
219: }
220: else
221: return false;
222: }
223:
224: foreach($this->defaultParams as $key=>$value)
225: {
226: if(isset($params[$key]))
227: {
228: if($params[$key]==$value)
229: unset($params[$key]);
230: else
231: return false;
232: }
233: }
234:
235: foreach($this->params as $key=>$value)
236: if(!isset($params[$key]))
237: return false;
238:
239: if($manager->matchValue && $this->matchValue===null || $this->matchValue)
240: {
241: foreach($this->params as $key=>$value)
242: {
243: if(!preg_match('/'.$value.'/'.$case,$params[$key]))
244: return false;
245: }
246: }
247:
248: foreach($this->params as $key=>$value)
249: {
250: $tr["<$key>"]=urlencode($params[$key]);
251: unset($params[$key]);
252: }
253:
254: $suffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix;
255:
256: $url=strtr($this->template,$tr);
257:
258: if($this->hasHostInfo)
259: {
260: $hostInfo=Yii::app()->getRequest()->getHostInfo();
261: if(stripos($url,$hostInfo)===0)
262: $url=substr($url,strlen($hostInfo));
263: }
264:
265: if(empty($params))
266: return $url!=='' ? $url.$suffix : $url;
267:
268: if(false)
269: $url.='/'.$manager->createPathInfo($params,'/','/').$suffix;
270: else
271: {
272: if($url!=='')
273: $url.=$suffix;
274: $url.='?'.$manager->createPathInfo($params,'=',$ampersand);
275: }
276:
277: return $url;
278: }
279:
280: /**
281: * Parses a URL based on this rule.
282: * @param CUrlManager $manager the URL manager
283: * @param CHttpRequest $request the request object
284: * @param string $pathInfo path info part of the URL
285: * @param string $rawPathInfo path info that contains the potential URL suffix
286: * @return mixed the route that consists of the controller ID and action ID or false on error
287: */
288: public function parseUrl($manager,$request,$pathInfo,$rawPathInfo)
289: {
290: if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true))
291: return false;
292:
293: if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive)
294: $case='';
295: else
296: $case='i';
297:
298: if($this->urlSuffix!==null)
299: $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix);
300:
301: // URL suffix required, but not found in the requested URL
302: if($manager->useStrictParsing && $pathInfo===$rawPathInfo)
303: {
304: $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix;
305: if($urlSuffix!='' && $urlSuffix!=='/')
306: return false;
307: }
308:
309: if($this->hasHostInfo)
310: $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/');
311:
312: $pathInfo.='/';
313:
314: if(preg_match($this->pattern.$case,$pathInfo,$matches))
315: {
316: foreach($this->defaultParams as $name=>$value)
317: {
318: if(!isset($_GET[$name]))
319: $_REQUEST[$name]=$_GET[$name]=$value;
320: }
321: $tr=array();
322: foreach($matches as $key=>$value)
323: {
324: if(isset($this->references[$key]))
325: $tr[$this->references[$key]]=$value;
326: else if(isset($this->params[$key]))
327: $_REQUEST[$key]=$_GET[$key]=$value;
328: }
329: if($pathInfo!==$matches[0]) // there're additional GET params
330: $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/'));
331: if($this->routePattern!==null)
332: return strtr($this->route,$tr);
333: else
334: return $this->route;
335: }
336: else
337: return false;
338: }
339: }
340: