diff --git a/lib/router.php b/lib/router.php index 180d8f791b..8fbb6eb1f4 100644 --- a/lib/router.php +++ b/lib/router.php @@ -31,73 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once 'Net/URL/Mapper.php'; - -class StatusNet_URL_Mapper extends Net_URL_Mapper -{ - private static $_singleton = null; - private $_actionToPath = array(); - - private function __construct() - { - } - - public static function getInstance($id = '__default__') - { - if (empty(self::$_singleton)) { - self::$_singleton = new StatusNet_URL_Mapper(); - } - return self::$_singleton; - } - - public function connect($path, $defaults = array(), $rules = array()) - { - $result = null; - if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) { - $result = parent::connect($path, $defaults, $rules); - if (array_key_exists('action', $defaults)) { - $action = $defaults['action']; - } elseif (array_key_exists('action', $rules)) { - $action = $rules['action']; - } else { - $action = null; - } - $this->_mapAction($action, $result); - Event::handle('EndConnectPath', array($path, $defaults, $rules, $result)); - } - return $result; - } - - protected function _mapAction($action, $path) - { - if (!array_key_exists($action, $this->_actionToPath)) { - $this->_actionToPath[$action] = array(); - } - $this->_actionToPath[$action][] = $path; - return; - } - - public function generate($values = array(), $qstring = array(), $anchor = '') - { - if (!array_key_exists('action', $values)) { - return parent::generate($values, $qstring, $anchor); - } - - $action = $values['action']; - - if (!array_key_exists($action, $this->_actionToPath)) { - return parent::generate($values, $qstring, $anchor); - } - - $oldPaths = $this->paths; - $this->paths = $this->_actionToPath[$action]; - $result = parent::generate($values, $qstring, $anchor); - $this->paths = $oldPaths; - - return $result; - } -} - /** * URL Router * @@ -159,8 +92,8 @@ class Router * you're running and the plugins that are enabled. To avoid having bad routes * get stuck in the cache, the key includes a list of plugins and the software * version. - * - * There can still be problems with a) differences in versions of the plugins and + * + * There can still be problems with a) differences in versions of the plugins and * b) people running code between official versions, but these tend to be more * sophisticated users who can grok what's going on and clear their caches. * @@ -183,7 +116,7 @@ class Router function initialize() { - $m = StatusNet_URL_Mapper::getInstance(); + $m = new URLMapper(); if (Event::handle('StartInitializeRouter', array(&$m))) { @@ -1139,7 +1072,7 @@ class Router { try { $match = $this->m->match($path); - } catch (Net_URL_Mapper_InvalidException $e) { + } catch (Exception $e) { common_log(LOG_ERR, "Problem getting route for $path - " . $e->getMessage()); // TRANS: Client error on action trying to visit a non-existing page. @@ -1161,7 +1094,6 @@ class Router } $url = $this->m->generate($args, $params, $fragment); - // Due to a bug in the Net_URL_Mapper code, the returned URL may // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We // repair that here rather than modifying the upstream code... diff --git a/lib/urlmapper.php b/lib/urlmapper.php new file mode 100644 index 0000000000..dffb32c814 --- /dev/null +++ b/lib/urlmapper.php @@ -0,0 +1,246 @@ +. + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * URL mapper + * + * Converts a path into a set of parameters, and vice versa + * + * We used to use Net_URL_Mapper, so there's a wrapper class at Router, q.v. + * + * NUM's vagaries are the main reason we have weirdnesses here. + * + * @category URL + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class URLMapper +{ + const ACTION = 'action'; + + protected $statics = array(); + protected $variables = array(); + protected $reverse = array(); + + function connect($path, $args, $paramPatterns=null) + { + if (!array_key_exists(self::ACTION, $args)) { + throw new Exception(sprintf("Can't connect %s; path has no action.", $path)); + } + + $action = $args[self::ACTION]; + + $paramNames = $this->getParamNames($path); + + if (empty($paramNames)) { + $this->statics[$path] = $args; + if (array_key_exists($action, $this->reverse)) { + $this->reverse[$args[self::ACTION]][] = array($args, $path); + } else { + $this->reverse[$args[self::ACTION]] = array(array($args, $path)); + } + } else { + + // Eff if I understand why some go here and some go there. + // Anyways, fixup my preconceptions + + foreach ($paramNames as $name) { + if (!array_key_exists($name, $paramPatterns) && + array_key_exists($name, $args)) { + $paramPatterns[$name] = $args[$name]; + unset($args[$name]); + } + } + + $regex = $this->makeRegex($path, $paramPatterns); + + $this->variables[] = array($args, $regex, $paramNames); + + $format = $this->makeFormat($path, $paramPatterns); + + if (array_key_exists($action, $this->reverse)) { + $this->reverse[$args[self::ACTION]][] = array($args, $format, $paramNames); + } else { + $this->reverse[$args[self::ACTION]] = array(array($args, $format, $paramNames)); + } + } + } + + function match($path) + { + if (array_key_exists($path, $this->statics)) { + return $this->statics[$path]; + } + + foreach ($this->variables as $pattern) { + list($args, $regex, $paramNames) = $pattern; + if (preg_match($regex, $path, $match)) { + $results = $args; + foreach ($paramNames as $name) { + $results[$name] = $match[$name]; + } + return $results; + } + } + + throw new Exception(sprintf('No match for path "%s"', $path)); + } + + function generate($args, $qstring, $fragment) + { + if (!array_key_exists(self::ACTION, $args)) { + throw new Exception("Every path needs an action."); + } + + $action = $args[self::ACTION]; + + if (!array_key_exists($action, $this->reverse)) { + throw new Exception(sprintf('No candidate paths for action "%s"', $action)); + } + + $candidates = $this->reverse[$action]; + + foreach ($candidates as $candidate) { + if (count($candidate) == 2) { // static + list($tryArgs, $tryPath) = $candidate; + foreach ($tryArgs as $key => $value) { + if (!array_key_exists($key, $args) || $args[$key] != $value) { + // next candidate + continue 2; + } + } + // success + return $tryPath; + } else { + list($tryArgs, $format, $paramNames) = $candidate; + + foreach ($tryArgs as $key => $value) { + if (!array_key_exists($key, $args) || $args[$key] != $value) { + // next candidate + continue 2; + } + } + + // success + + $toFormat = array(); + + foreach ($paramNames as $name) { + if (!array_key_exists($name, $args)) { + // next candidate + continue 2; + } + $toFormat[] = $args[$name]; + } + + $path = vsprintf($format, $toFormat); + + if (!empty($qstring)) { + $path .= '?' . http_build_query($qstring); + } + + return $path; + } + } + + unset($args['action']); + + if (empty($args)) { + throw new Exception(sprintf('No matches for action "%s"', $action)); + } + + $argstring = ''; + + foreach ($args as $key => $value) { + $argstring .= "$key=$value "; + } + + throw new Exception(sprintf('No matches for action "%s" with arguments "%s"', $action, $argstring)); + } + + protected function getParamNames($path) + { + preg_match_all('/:(?P\w+)/', $path, $match); + return $match['name']; + } + + protected function makeRegex($path, $paramPatterns) + { + $pr = new PatternReplacer($paramPatterns); + + $regex = preg_replace_callback('/:(\w+)/', + array($pr, 'toPattern'), + $path); + + $regex = '#' . str_replace('#', '\#', $regex) . '#'; + + return $regex; + } + + protected function makeFormat($path, $paramPatterns) + { + $format = preg_replace('/(:\w+)/', '%s', $path); + + return $format; + } +} + +class PatternReplacer +{ + private $patterns; + + function __construct($patterns) + { + $this->patterns = $patterns; + } + + function toPattern($matches) + { + // trim out the : + $name = $matches[1]; + if (array_key_exists($name, $this->patterns)) { + $pattern = $this->patterns[$name]; + } else { + // ??? + $pattern = '\w+'; + } + return '(?P<'.$name.'>'.$pattern.')'; + } +} diff --git a/lib/util.php b/lib/util.php index ba705eb7fa..3658e3ceea 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1237,12 +1237,12 @@ function common_local_url($action, $args=null, $params=null, $fragment=null, $ad $ssl = common_is_sensitive($action); if (common_config('site','fancy')) { - $url = common_path(mb_substr($path, 1), $ssl, $addSession); + $url = common_path($path, $ssl, $addSession); } else { if (mb_strpos($path, '/index.php') === 0) { - $url = common_path(mb_substr($path, 1), $ssl, $addSession); + $url = common_path($path, $ssl, $addSession); } else { - $url = common_path('index.php'.$path, $ssl, $addSession); + $url = common_path('index.php/'.$path, $ssl, $addSession); } } Event::handle('EndLocalURL', array(&$action, &$params, &$fragment, &$addSession, &$url));