1: <?php defined('_JOOS_CORE') or exit();
2:
3: /**
4: * Работа с ссылками и роутингом
5: *
6: * @version 1.0
7: * @package Core\Libraries
8: * @subpackage Route
9: * @category Libraries
10: * @author Joostina Team <info@joostina.ru>
11: * @copyright (C) 2007-2012 Joostina Team
12: * @license MIT License http://www.opensource.org/licenses/mit-license.php
13: * Информация об авторах и лицензиях стороннего кода в составе Joostina CMS: docs/copyrights
14: *
15: * */
16: class joosRoute extends Route
17: {
18: private static $current_url;
19:
20: public static function init()
21: {
22: /**
23: *
24: * @todo файл с пользовательскими роутами, должен конфигурироваться и подключаться в bootstrap.php
25: */
26: $routes = require(JPATH_APP_CONFIG . DS . 'routes.php');
27:
28: foreach ($routes as $route_name => $route) {
29: self::set($route_name, $route['href'], (isset($route['params_rules']) ? $route['params_rules'] : null))->defaults($route['defaults']);
30: }
31:
32: //$uri = $_SERVER['QUERY_STRING'] = rtrim($_SERVER['QUERY_STRING'], '/');
33: $uri = $_SERVER['REQUEST_URI'] = trim($_SERVER['REQUEST_URI'], '/');
34: self::$current_url = urldecode($uri);
35:
36: }
37:
38: public static function route()
39: {
40: self::init();
41:
42: $routes = self::all();
43: $params = NULL;
44:
45: foreach ($routes as $name => $route) {
46: // We found something suitable
47: if (($params = $route->matches(self::$current_url))) {
48: joosController::$activroute = $name;
49: joosController::$controller = $params['controller'];
50: joosController::$task = $params['action'];
51: joosController::$param = $params;
52:
53: return;
54: }
55: }
56:
57: // если включена отладка - скажем что именно не так
58: if (JDEBUG) {
59: throw new joosException('Не найдено правило роутинга для ссылки :location', array(':location' => self::$current_url));
60: } else {
61: // отладка не включена - просто перекинем на 404 страницу с понятным текстом
62: joosPages::page404('Такая ссылка на сайте невозможна');
63: }
64: }
65:
66: /**
67: * Формирование ссылки
68: *
69: * @param string $route_name название правила роутинга
70: * @param array $params массив параметров для формирования ссылки
71: * @return string
72: */
73: public static function href($route_name, array $params = array())
74: {
75: return JPATH_SITE . '/' . self::get($route_name)->uri($params);
76: }
77:
78: /**
79: * Системный 301 редирект
80: *
81: * @param string $url ссылка, на которую надо перейти
82: * @param string $msg текст сообщения, отображаемый после перехода
83: * @param string $type тип перехода - ошибка, предупреждение, сообщение и т.д.
84: * @return void
85: */
86: public static function redirect($url, $msg = '', $type = 'success')
87: {
88: $iFilter = joosInputFilter::instance();
89: $url = $iFilter->process($url);
90:
91: empty($msg) ? null : joosFlashMessage::add($iFilter->process($msg), $type);
92:
93: $url = preg_split("/[\r\n]/", $url);
94: $url = $url[0];
95:
96: if ($iFilter->badAttributeValue(array('href', $url))) {
97: $url = JPATH_SITE;
98: }
99:
100: if (headers_sent()) {
101: echo "<script>document.location.href='$url';</script>\n";
102: } else {
103: !ob_get_level() ? : ob_end_clean();
104: joosRequest::send_headers_by_code(301);
105: joosRequest::send_headers("Location: " . $url);
106: }
107:
108: exit();
109: }
110:
111: /**
112: * Получение название текущего активного правила роутинга
113: *
114: * @return string
115: */
116: public static function get_active_route()
117: {
118: return joosController::$activroute;
119: }
120:
121: /**
122: * Получение текущий ссылки ( в адресной сроке браузера )
123: *
124: * @return string
125: */
126: public static function get_current_url()
127: {
128: return self::$current_url == '' ? JPATH_SITE : JPATH_SITE . '/' . self::$current_url;
129: }
130:
131: }
132:
133: /**
134: * Базовый класс роутинга
135: * Базируется на оригинальной работе Kohana Team
136: */
137: class Route
138: {
139: // Defines the pattern of a <segment>
140:
141: const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
142:
143: // What can be part of a <segment> value
144: const REGEX_SEGMENT = '[^/.,;?\n]++';
145:
146: // What must be escaped in the route regex
147: const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
148:
149: /**
150: * @var string default protocol for all routes
151: *
152: * @tutorial 'http://'
153: */
154: public static $default_protocol = 'http://';
155:
156: /**
157: * @var string default action for all routes
158: */
159: public static $default_action = 'index';
160:
161: /**
162: * @var bool Indicates whether routes are cached
163: */
164: public static $cache = FALSE;
165:
166: /**
167: * @var array
168: */
169: protected static $_routes = array();
170:
171: /**
172: * Stores a named route and returns it. The "action" will always be set to
173: * "index" if it is not defined.
174: *
175: * self::set('default', '(<controller>(/<action>(/<id>)))')
176: * ->defaults(array(
177: * 'controller' => 'welcome',
178: * ));
179: *
180: * @param string route name
181: * @param string URI pattern
182: * @param array regex patterns for route keys
183: *
184: * @return Route
185: */
186: protected static function set($name, $uri_callback = NULL, $regex = NULL)
187: {
188: return self::$_routes[$name] = new self($uri_callback, $regex);
189: }
190:
191: /**
192: * Retrieves a named route.
193: *
194: * $route = self::get('default');
195: *
196: * @param string route name
197: *
198: * @return Route
199: * @throws joosException
200: */
201: protected static function get($name)
202: {
203: if (!isset(self::$_routes[$name])) {
204: throw new joosException('Не найдено правило роутинга: :route', array(':route' => $name));
205: }
206:
207: return self::$_routes[$name];
208: }
209:
210: /**
211: * Retrieves all named routes.
212: *
213: * $routes = self::all();
214: *
215: * @return array routes by name
216: */
217: protected static function all()
218: {
219: return self::$_routes;
220: }
221:
222: /**
223: * Returns the compiled regular expression for the route. This translates
224: * keys and optional groups to a proper PCRE regular expression.
225: *
226: * $compiled = self::compile(
227: * '<controller>(/<action>(/<id>))',
228: * array(
229: * 'controller' => '[a-z]+',
230: * 'id' => '\d+',
231: * )
232: * );
233: *
234: * @return string
235: * @uses self::REGEX_ESCAPE
236: * @uses self::REGEX_SEGMENT
237: */
238: private static function compile($uri, array $regex = NULL)
239: {
240: if (!is_string($uri)) {
241: return;
242: }
243:
244: // The URI should be considered literal except for keys and optional parts
245: // Escape everything preg_quote would escape except for : ( ) < >
246: $expression = preg_replace('#' . self::REGEX_ESCAPE . '#', '\\\\$0', $uri);
247:
248: if (strpos($expression, '(') !== FALSE) {
249: // Make optional parts of the URI non-capturing and optional
250: $expression = str_replace(array('(', ')'), array('(?:', ')?'), $expression);
251: }
252:
253: // Insert default regex for keys
254: $expression = str_replace(array('<', '>'), array('(?P<', '>' . self::REGEX_SEGMENT . ')'), $expression);
255:
256: // правила краткой записи регулярок роутинга
257: $rules = array(':any' => '.+?', ':maybe' => '.*?', ':digit' => '[\d]+', ':alpha' => '[a-zA-Z]+', ':rus_alpha' => '[a-zA-Zа-яА-ЯёЁ]+', ':word' => '[\w-_]+', ':slug' => '[a-zA-Zа-яА-ЯёЁ0-9\-]+',);
258:
259: if ($regex) {
260: $search = $replace = array();
261: foreach ($regex as $key => $value) {
262:
263: $value = strtr($value, $rules);
264:
265: $search[] = "<$key>" . self::REGEX_SEGMENT;
266: $replace[] = "<$key>$value";
267: }
268:
269: // Replace the default regex with the user-specified regex
270: $expression = str_replace($search, $replace, $expression);
271: }
272:
273: return '#^' . $expression . '$#uD';
274: }
275:
276: /**
277: * @var string route URI
278: */
279: protected $_uri = '';
280:
281: /**
282: * @var array
283: */
284: protected $_regex = array();
285:
286: /**
287: * @var array
288: */
289: protected $_defaults = array('action' => 'index', 'host' => FALSE);
290:
291: /**
292: * @var string
293: */
294: protected $_route_regex;
295:
296: /**
297: * Creates a new route. Sets the URI and regular expressions for keys.
298: * Routes should always be created with [self::set] or they will not
299: * be properly stored.
300: *
301: * $route = new Route($uri, $regex);
302: *
303: * @param mixed route URI pattern
304: * @param array key patterns
305: *
306: * @return void
307: * @uses self::_compile
308: */
309: public function __construct($uri = NULL, $regex = NULL)
310: {
311: if ($uri === NULL) {
312: // заморочка с кешем
313: return;
314: }
315:
316: if (!empty($uri)) {
317: $this->_uri = $uri;
318: }
319:
320: if (!empty($regex)) {
321: $this->_regex = $regex;
322: }
323:
324: // Store the compiled regex locally
325: $this->_route_regex = self::compile($uri, $regex);
326: }
327:
328: /**
329: * Provides default values for keys when they are not present. The default
330: * action will always be "index" unless it is overloaded here.
331: *
332: * $route->defaults(array(
333: * 'controller' => 'welcome',
334: * 'action' => 'index'
335: * ));
336: *
337: * @param array key values
338: *
339: * @return $this
340: */
341: protected function defaults(array $defaults = NULL)
342: {
343: $this->_defaults = $defaults;
344:
345: return $this;
346: }
347:
348: /**
349: * Tests if the route matches a given URI. A successful match will return
350: * all of the routed parameters as an array. A failed match will return
351: * boolean FALSE.
352: *
353: * // Params: controller = users, action = edit, id = 10
354: * $params = $route->matches('users/edit/10');
355: *
356: * This method should almost always be used within an if/else block:
357: *
358: * if ($params = $route->matches($uri))
359: * {
360: * // Parse the parameters
361: * }
362: *
363: * @param string URI to match
364: *
365: * @return array on success
366: * @return FALSE on failure
367: */
368: protected function matches($uri)
369: {
370: if (!preg_match($this->_route_regex, $uri, $matches)) {
371: return FALSE;
372: }
373:
374: $params = array();
375: foreach ($matches as $key => $value) {
376: if (is_int($key)) {
377: // Skip all unnamed keys
378: continue;
379: }
380:
381: // Set the value for all matched keys
382: $params[$key] = $value;
383: }
384:
385: foreach ($this->_defaults as $key => $value) {
386: if (!isset($params[$key]) OR $params[$key] === '') {
387: // Set default values for any key that was not matched
388: $params[$key] = $value;
389: }
390: }
391:
392: return $params;
393: }
394:
395: /**
396: * Generates a URI for the current route based on the parameters given.
397: *
398: * // Using the "default" route: "users/profile/10"
399: * $route->uri(array(
400: * 'controller' => 'users',
401: * 'action' => 'profile',
402: * 'id' => '10'
403: * ));
404: *
405: * @param array URI parameters
406: *
407: * @return string
408: * @throws joosException
409: * @uses self::REGEX_Key
410: */
411: protected function uri(array $params = NULL)
412: {
413: // Start with the routed URI
414: $uri = $this->_uri;
415:
416: // если в ссылке нет динамических параметров - сразу её возвратим
417: if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE) {
418: return $uri;
419: }
420:
421: while (preg_match('#' . self::REGEX_KEY . '#', $uri, $match)) {
422: list($key, $param) = $match;
423: if (!isset($params[$param])) {
424: // Look for a default
425: if (isset($this->_defaults[$param])) {
426: $params[$param] = $this->_defaults[$param];
427: } else {
428: // отсутствуют требуемые параметры
429: throw new joosException('Требуемый параметр :param не найден в полученных данных для условия :uri', array(':param' => $param, ':uri' => joosFilter::htmlspecialchars($this->_uri)));
430: }
431: }
432:
433: $uri = str_replace($key, $params[$param], $uri);
434: }
435:
436: // чистка от лишних и дублирующихся /
437: $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
438:
439: return $uri;
440: }
441:
442: }
443: