Commit | Line | Data |
---|---|---|
d71e5a29 AE |
1 | <?php |
2 | namespace wcf\system\request; | |
be1101a0 | 3 | use wcf\system\application\ApplicationHandler; |
d71e5a29 | 4 | use wcf\system\exception\SystemException; |
e6e54af2 | 5 | use wcf\system\menu\page\PageMenu; |
be1101a0 | 6 | use wcf\system\WCF; |
d71e5a29 AE |
7 | |
8 | /** | |
9 | * Route implementation to resolve HTTP requests. | |
10 | * | |
11 | * Inspired by routing mechanism used by ASP.NET MVC and released under the terms of | |
12 | * the Microsoft Public License (MS-PL) http://www.opensource.org/licenses/ms-pl.html | |
13 | * | |
9f959ced | 14 | * @author Alexander Ebert |
f3aa5021 | 15 | * @copyright 2001-2015 WoltLab GmbH |
d71e5a29 AE |
16 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
17 | * @package com.woltlab.wcf | |
18 | * @subpackage system.request | |
9f959ced | 19 | * @category Community Framework |
d71e5a29 | 20 | */ |
f3aa5021 | 21 | class Route implements IRoute { |
2a74eea2 | 22 | /** |
3df09842 | 23 | * route controller if controller is no part of the route schema |
2a74eea2 MS |
24 | * @var string |
25 | */ | |
26 | protected $controller = null; | |
27 | ||
d71e5a29 AE |
28 | /** |
29 | * route is restricted to ACP | |
30 | * @var boolean | |
31 | */ | |
32 | protected $isACP = false; | |
33 | ||
34 | /** | |
35 | * schema component options | |
36 | * @var array | |
37 | */ | |
38 | protected $parameterOptions = array(); | |
39 | ||
40 | /** | |
41 | * route name | |
42 | * @var string | |
43 | */ | |
44 | protected $routeName = ''; | |
45 | ||
46 | /** | |
47 | * route schema data | |
48 | * @var array | |
49 | */ | |
50 | protected $routeSchema = array(); | |
51 | ||
52 | /** | |
53 | * parsed route data | |
54 | * @var array | |
55 | */ | |
56 | protected $routeData = null; | |
57 | ||
be1101a0 AE |
58 | /** |
59 | * list of application abbreviation and default controller name | |
60 | * @var array<string> | |
61 | */ | |
62 | protected static $defaultControllers = null; | |
63 | ||
d71e5a29 AE |
64 | /** |
65 | * Creates a new route. | |
66 | * | |
67 | * @param string $routeName | |
68 | * @param boolean $isACP | |
69 | */ | |
70 | public function __construct($routeName, $isACP = false) { | |
71 | $this->isACP = $isACP; | |
72 | $this->routeName = $routeName; | |
73 | } | |
74 | ||
75 | /** | |
3df09842 | 76 | * Sets route schema, e.g. /{controller}/{id}. |
d71e5a29 AE |
77 | * |
78 | * @param string $routeSchema | |
2a74eea2 | 79 | * @param string $controller |
d71e5a29 | 80 | */ |
2a74eea2 | 81 | public function setSchema($routeSchema, $controller = null) { |
d71e5a29 AE |
82 | $schemaParts = $this->getParts($routeSchema); |
83 | $hasController = false; | |
84 | $pattern = '~^{[a-zA-Z]+}$~'; | |
2a74eea2 MS |
85 | |
86 | if ($controller !== null) { | |
87 | $this->controller = $controller; | |
88 | $hasController = true; | |
89 | } | |
90 | ||
d71e5a29 AE |
91 | foreach ($schemaParts as &$part) { |
92 | if (!preg_match($pattern, $part)) { | |
93 | throw new SystemException("Placeholder expected, but invalid string '" . $part . "' given."); | |
94 | } | |
95 | ||
96 | $part = str_replace(array('{', '}'), '', $part); | |
97 | if ($part == 'controller') { | |
2a74eea2 | 98 | if ($this->controller !== null) { |
3df09842 | 99 | throw new SystemException('Controller may not be part of the scheme if a route controller is given.'); |
2a74eea2 | 100 | } |
3df09842 MS |
101 | |
102 | $hasController = true; | |
d71e5a29 AE |
103 | } |
104 | } | |
105 | ||
106 | // each route must define a controller | |
107 | if (!$hasController) { | |
108 | throw new SystemException('Route schema does not provide a valid placeholder for controller.'); | |
109 | } | |
110 | ||
111 | $this->routeSchema = $schemaParts; | |
112 | } | |
113 | ||
114 | /** | |
115 | * Sets options for a route parameter. | |
116 | * | |
117 | * @param string $key | |
118 | * @param string $default | |
119 | * @param string $regexPattern | |
120 | * @param boolean $isOptional | |
121 | */ | |
122 | public function setParameterOption($key, $default = null, $regexPattern = null, $isOptional = false) { | |
d71e5a29 AE |
123 | $this->parameterOptions[$key] = array( |
124 | 'default' => $default, | |
125 | 'isOptional' => $isOptional, | |
126 | 'regexPattern' => $regexPattern | |
127 | ); | |
128 | } | |
129 | ||
130 | /** | |
f3aa5021 | 131 | * @see \wcf\system\request\IRoute::matches() |
d71e5a29 AE |
132 | */ |
133 | public function matches($requestURL) { | |
134 | $urlParts = $this->getParts($requestURL); | |
135 | $data = array(); | |
136 | ||
137 | // handle each route schema component | |
138 | for ($i = 0, $size = count($this->routeSchema); $i < $size; $i++) { | |
139 | $schemaPart = $this->routeSchema[$i]; | |
140 | ||
141 | if (isset($urlParts[$i])) { | |
142 | if (isset($this->parameterOptions[$schemaPart])) { | |
143 | // validate parameter against a regex pattern | |
144 | if ($this->parameterOptions[$schemaPart]['regexPattern'] !== null) { | |
145 | $pattern = '~^' . $this->parameterOptions[$schemaPart]['regexPattern'] . '$~'; | |
146 | if (!preg_match($pattern, $urlParts[$i])) { | |
147 | return false; | |
148 | } | |
149 | } | |
150 | } | |
151 | ||
152 | // url component passed previous validation | |
153 | $data[$schemaPart] = $urlParts[$i]; | |
154 | } | |
155 | else { | |
156 | if (isset($this->parameterOptions[$schemaPart])) { | |
157 | // default value is provided | |
158 | if ($this->parameterOptions[$schemaPart]['default'] !== null) { | |
b07d3301 AE |
159 | if ($schemaPart == 'controller') { |
160 | $data['isDefaultController'] = true; | |
161 | } | |
162 | ||
d71e5a29 AE |
163 | $data[$schemaPart] = $this->parameterOptions[$schemaPart]['default']; |
164 | continue; | |
165 | } | |
166 | ||
167 | // required parameter is missing | |
168 | if (!$this->parameterOptions[$schemaPart]['isOptional']) { | |
169 | return false; | |
170 | } | |
171 | } | |
172 | } | |
173 | } | |
174 | ||
b07d3301 | 175 | if (!isset($data['isDefaultController'])) { |
5bded211 | 176 | $data['isDefaultController'] = false; |
b07d3301 AE |
177 | } |
178 | ||
d71e5a29 | 179 | $this->routeData = $data; |
2a74eea2 | 180 | |
3df09842 | 181 | // adds route controller if given |
2a74eea2 MS |
182 | if ($this->controller !== null) { |
183 | $this->routeData['controller'] = $this->controller; | |
184 | } | |
185 | ||
5bded211 AE |
186 | if (!isset($this->routeData['controller'])) { |
187 | $this->routeData['isDefaultController'] = true; | |
188 | } | |
189 | ||
d71e5a29 AE |
190 | return true; |
191 | } | |
192 | ||
193 | /** | |
f3aa5021 | 194 | * @see \wcf\system\request\IRoute::getRouteData() |
d71e5a29 AE |
195 | */ |
196 | public function getRouteData() { | |
197 | return $this->routeData; | |
198 | } | |
199 | ||
200 | /** | |
201 | * Returns non-empty URL components. | |
202 | * | |
203 | * @param string $requestURL | |
204 | * @return array | |
205 | */ | |
206 | protected function getParts($requestURL) { | |
207 | $urlParts = preg_split('~(\/|\-|\_|\.)~', $requestURL); | |
208 | foreach ($urlParts as $index => $part) { | |
30178358 | 209 | if (!mb_strlen($part)) { |
d71e5a29 AE |
210 | unset($urlParts[$index]); |
211 | } | |
212 | } | |
213 | ||
214 | // re-index parts | |
215 | return array_values($urlParts); | |
216 | } | |
217 | ||
f98e7cf2 | 218 | /** |
f3aa5021 | 219 | * @see \wcf\system\request\IRoute::canHandle() |
f98e7cf2 AE |
220 | */ |
221 | public function canHandle(array $components) { | |
222 | foreach ($this->routeSchema as $schemaPart) { | |
223 | if (isset($components[$schemaPart])) { | |
224 | // validate parameter against a regex pattern | |
225 | if ($this->parameterOptions[$schemaPart]['regexPattern'] !== null) { | |
226 | $pattern = '~^' . $this->parameterOptions[$schemaPart]['regexPattern'] . '$~'; | |
227 | if (!preg_match($pattern, $components[$schemaPart])) { | |
228 | return false; | |
229 | } | |
230 | } | |
231 | } | |
232 | else { | |
233 | if (isset($this->parameterOptions[$schemaPart])) { | |
234 | // default value is provided | |
235 | if ($this->parameterOptions[$schemaPart]['default'] !== null) { | |
236 | continue; | |
237 | } | |
238 | ||
239 | // required parameter is missing | |
240 | if (!$this->parameterOptions[$schemaPart]['isOptional']) { | |
241 | return false; | |
242 | } | |
243 | } | |
244 | } | |
245 | } | |
246 | ||
247 | return true; | |
248 | } | |
249 | ||
250 | /** | |
f3aa5021 | 251 | * @see \wcf\system\request\IRoute::buildLink() |
f98e7cf2 AE |
252 | */ |
253 | public function buildLink(array $components) { | |
9bfbe274 | 254 | $application = (isset($components['application'])) ? $components['application'] : null; |
be1101a0 AE |
255 | self::loadDefaultControllers(); |
256 | ||
2ef8b744 AE |
257 | // drop application component to avoid being appended as query string |
258 | unset($components['application']); | |
259 | ||
6dcaf901 AE |
260 | $link = ''; |
261 | ||
262 | // handle default values for controller | |
263 | $buildRoute = true; | |
5bded211 AE |
264 | if (count($components) == 1 && isset($components['controller'])) { |
265 | $ignoreController = false; | |
6dcaf901 AE |
266 | if (isset($this->parameterOptions['controller']) && strcasecmp($this->parameterOptions['controller']['default'], $components['controller']) == 0) { |
267 | // only the controller was given and matches default, omit routing | |
5bded211 AE |
268 | $ignoreController = true; |
269 | } | |
e6e54af2 | 270 | else if (!RequestHandler::getInstance()->isACPRequest()) { |
5bded211 | 271 | $landingPage = PageMenu::getInstance()->getLandingPage(); |
a97905ff | 272 | if ($landingPage !== null && strcasecmp($landingPage->getController(), $components['controller']) == 0) { |
5bded211 AE |
273 | $ignoreController = true; |
274 | } | |
9bfbe274 AE |
275 | |
276 | // check if this is the default controller of the requested application | |
277 | if (!URL_LEGACY_MODE && !$ignoreController && $application !== null) { | |
278 | if (isset(self::$defaultControllers[$application]) && self::$defaultControllers[$application] == $components['controller']) { | |
279 | // check if this is the primary application and the landing page originates to the same application | |
280 | $primaryApplication = ApplicationHandler::getInstance()->getPrimaryApplication(); | |
281 | $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($primaryApplication->packageID); | |
282 | if ($abbreviation != $application || $landingPage === null || $landingPage->getApplication() != 'wcf') { | |
283 | $ignoreController = true; | |
284 | } | |
285 | } | |
be1101a0 AE |
286 | } |
287 | } | |
288 | ||
5bded211 AE |
289 | // drops controller from route |
290 | if ($ignoreController) { | |
6dcaf901 | 291 | $buildRoute = false; |
ba6982ae | 292 | |
be1101a0 | 293 | // unset the controller, since it would otherwise be added with http_build_query() |
ba6982ae | 294 | unset($components['controller']); |
490a2ce9 | 295 | } |
6dcaf901 AE |
296 | } |
297 | ||
298 | if ($buildRoute) { | |
299 | foreach ($this->routeSchema as $component) { | |
300 | if (!isset($components[$component])) { | |
301 | continue; | |
302 | } | |
fd4213fd | 303 | |
97b6b000 AE |
304 | // handle built-in SEO |
305 | if ($component === 'id' && isset($components['title'])) { | |
306 | $link .= $components[$component] . '-' . $components['title'] . '/'; | |
307 | unset($components['title']); | |
308 | } | |
309 | else { | |
310 | $link .= $components[$component] . '/'; | |
311 | } | |
6dcaf901 AE |
312 | unset($components[$component]); |
313 | } | |
f98e7cf2 AE |
314 | } |
315 | ||
7f9d94ab AE |
316 | // enforce non-legacy URLs for ACP and disregard rewrite settings |
317 | if ($this->isACP()) { | |
6d61eb12 AE |
318 | if (!empty($link)) { |
319 | $link = '?' . $link; | |
320 | } | |
5bded211 | 321 | } |
7f9d94ab | 322 | else if (!URL_OMIT_INDEX_PHP) { |
be1101a0 | 323 | $link = (URL_LEGACY_MODE ? 'index.php/' : (!empty($link) ? '?' : '')) . $link; |
7f9d94ab | 324 | } |
6dcaf901 | 325 | |
f98e7cf2 | 326 | if (!empty($components)) { |
6d61eb12 | 327 | if (strpos($link, '?') === false) $link .= '?'; |
d1d83d36 AE |
328 | else $link .= '&'; |
329 | ||
6d61eb12 | 330 | $link .= http_build_query($components, '', '&'); |
f98e7cf2 AE |
331 | } |
332 | ||
333 | return $link; | |
334 | } | |
335 | ||
d71e5a29 | 336 | /** |
f3aa5021 | 337 | * @see \wcf\system\request\IRoute::isACP() |
d71e5a29 AE |
338 | */ |
339 | public function isACP() { | |
340 | return $this->isACP; | |
341 | } | |
be1101a0 AE |
342 | |
343 | /** | |
344 | * Loads the default controllers for each active application. | |
345 | */ | |
346 | protected static function loadDefaultControllers() { | |
347 | if (self::$defaultControllers === null) { | |
348 | self::$defaultControllers = array(); | |
349 | ||
350 | foreach (ApplicationHandler::getInstance()->getApplications() as $application) { | |
22736672 MM |
351 | $app = WCF::getApplicationObject($application); |
352 | ||
353 | if (!$app) { | |
354 | continue; | |
355 | } | |
356 | ||
357 | $controller = $app->getPrimaryController(); | |
358 | ||
be1101a0 AE |
359 | if (!$controller) { |
360 | continue; | |
361 | } | |
362 | ||
363 | $controller = explode('\\', $controller); | |
364 | $controllerName = preg_replace('~(Action|Form|Page)$~', '', array_pop($controller)); | |
365 | if (URL_TO_LOWERCASE) $controllerName = mb_strtolower($controllerName); | |
366 | ||
367 | self::$defaultControllers[$controller[0]] = $controllerName; | |
368 | } | |
369 | } | |
370 | } | |
d71e5a29 | 371 | } |