From 654eede6f2d99ba63bb34649ac0557f173d3725c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guibert?= Date: Mon, 25 Feb 2013 13:05:17 +0100 Subject: [PATCH] Fix case --- classes/Multilang.php | 9 + classes/Multilang/Core.php | 159 ++++++++++++++++ classes/Multilang/Request.php | 199 +++++++++++++++++++ classes/Multilang/Route.php | 346 ++++++++++++++++++++++++++++++++++ classes/Multilang/Routes.php | 77 ++++++++ classes/Multilang/URL.php | 22 +++ classes/Request.php | 8 + classes/Route.php | 8 + classes/Routes.php | 8 + classes/URL.php | 5 + 10 files changed, 841 insertions(+) create mode 100644 classes/Multilang.php create mode 100644 classes/Multilang/Core.php create mode 100644 classes/Multilang/Request.php create mode 100644 classes/Multilang/Route.php create mode 100644 classes/Multilang/Routes.php create mode 100644 classes/Multilang/URL.php create mode 100644 classes/Request.php create mode 100644 classes/Route.php create mode 100644 classes/Routes.php create mode 100644 classes/URL.php diff --git a/classes/Multilang.php b/classes/Multilang.php new file mode 100644 index 0000000..d15e454 --- /dev/null +++ b/classes/Multilang.php @@ -0,0 +1,9 @@ +load('multilang'); + + if($config->auto_detect) + { + // Get the list of supported languages + $languages = $config->languages; + $cookie = $config->cookie; + + // Look for language cookie first + if($lang = Cookie::get($cookie)) + { + // Valid language found in cookie + if(isset($languages[$lang])) + { + return $lang; + } + + // Delete cookie with unset language + Cookie::delete($cookie); + } + + // Parse HTTP Accept-Language headers + foreach(Request::accept_lang() as $lang => $quality) + { + // Return the first language found (the language with the highest quality) + if(isset($languages[$lang])) + { + return $lang; + } + } + } + // Return the hard-coded default language as final fallback + return $config->default; + } + + /** + * Initialize the config and cookies + */ + static public function init() + { + $config = Kohana::$config->load('multilang'); + + // Get the list of supported languages + $langs = $config->languages; + + // Set the language in I18n + I18n::lang($langs[Request::$lang]['i18n']); + + // Set locale + setlocale(LC_ALL, $langs[Request::$lang]['locale']); + + $cookie = $config->cookie; + // Update language cookie if needed + if(Cookie::get($cookie) !== Request::$lang) + { + Cookie::set($cookie, Request::$lang); + } + } + + /** + * Return a language selector menu + * @param boolean $current Display the current language or not + * @return View + */ + static public function selector($current = TRUE) + { + $config = Kohana::$config->load('multilang'); + + $languages = $config->languages; + + // Get the current route name + $current_route = Route::name(Request::initial()->route()); + + + $params = Request::initial()->param(); + + if($current_route !== 'default' && strpos($current_route, '.') !== FALSE) + { + // Split the route path + list($lang, $name) = explode('.', $current_route, 2); + } + else + { + $name = $current_route; + } + + // Create uris for each language + foreach($languages as $lang => &$language) + { + // If it's the current language + if($lang === Request::$lang) + { + // We only display it when required + if($current) + { + $selectors[$lang] = ''.$languages[$lang]['label'].''; + } + } + else + { + $route = NULL; + + // If it's the default route, it's unique and special (like you <3) + if($current_route === 'default') + { + // We juste need to change the language parameter + $route = Request::initial()->route(); + $params['lang'] = NULL; + if(!$config->hide_default || $config->default !== $lang) + { + $params['lang'] = $lang; + } + + + } + else + { + if(Arr::get(Route::all(), $lang.'.'.$name)) + { + $route = Route::get($name, $lang); + } + } + + if($route !== NULL) + { + $selectors[$lang] = HTML::anchor($route->uri($params), $languages[$lang]['label'], array('class' => 'multilang-selectable multilang-'.$lang, 'title' => $languages[$lang]['label'])); + } + } + } + + // We display the menu only if we can select another language for this page + if(count($selectors) > 1) + { + return View::factory('multilang/selector') + ->bind('selectors', $selectors); + } + return ''; + } +} \ No newline at end of file diff --git a/classes/Multilang/Request.php b/classes/Multilang/Request.php new file mode 100644 index 0000000..b85f29c --- /dev/null +++ b/classes/Multilang/Request.php @@ -0,0 +1,199 @@ +load('multilang'); + + // If we don't hide the default language, we must look for a language code for the root uri + if(Request::detect_uri() === '' AND $config->auto_detect AND $uri === TRUE) + { + $lang = Multilang::find_user_language(); + if(!$config->hide_default OR $lang != $config->default) + { + // Use the default server protocol + $protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; + + // Redirect to the root URI, but with language prepended + header($protocol.' 302 Found'); + header('Location: '.URL::base(TRUE, TRUE).$lang.'/'); + exit; + } + } + + return parent::factory($uri, $client_params, $allow_external, $injected_routes); + } + + /** + * We don't want to remove the trailing slash from the uri + */ + public function __construct($uri, $client_params = array(), $allow_external = TRUE, $injected_routes = array()) + { + $client_params = is_array($client_params) ? $client_params : array(); + + // Initialise the header + $this->_header = new HTTP_Header(array()); + + // Assign injected routes + $this->_routes = $injected_routes; + + // Cleanse query parameters from URI (faster that parse_url()) + $split_uri = explode('?', $uri); + $uri = array_shift($split_uri); + + // Initial request has global $_GET already applied + if (Request::$initial !== NULL) + { + if ($split_uri) + { + parse_str($split_uri[0], $this->_get); + } + } + + // Detect protocol (if present) + // $allow_external = FALSE prevents the default index.php from + // being able to proxy external pages. + if ( ! $allow_external OR strpos($uri, '://') === FALSE) + { + // Remove trailing slashes from the URI (We don't want that) + //$this->_uri = trim($uri, '/'); + $this->_uri = ltrim($uri, '/'); + //$this->_route = new Route($uri); + // Apply the client + $this->_client = new Request_Client_Internal($client_params); + } + else + { + // Create a route + $this->_route = new Route($uri); + + // Store the URI + $this->_uri = $uri; + + // Set the security setting if required + if (strpos($uri, 'https://') === 0) + { + $this->secure(TRUE); + } + + // Set external state + $this->_external = TRUE; + + // Setup the client + $this->_client = Request_Client_External::factory($client_params); + } + } + + /** + * Altered to detect the language in the uri + * + * @return Response + * @throws Request_Exception + * @throws HTTP_Exception_404 + * @uses [Kohana::$profiling] + * @uses [Profiler] + */ + public function execute() + { + if ( ! $this->_external) + { + $processed = Request::process($this, $this->_routes); + + if ($processed) + { + // Store the matching route + $this->_route = $processed['route']; + $params = $processed['params']; + + // Is this route external? + $this->_external = $this->_route->is_external(); + + if (isset($params['directory'])) + { + // Controllers are in a sub-directory + $this->_directory = $params['directory']; + } + + // Store the controller + $this->_controller = $params['controller']; + + // Store the action + $this->_action = (isset($params['action'])) + ? $params['action'] + : Route::$default_action; + + // These are accessible as public vars and can be overloaded + unset($params['controller'], $params['action'], $params['directory']); + + // Params cannot be changed once matched + $this->_params = $params; + } + } + + if ( ! $this->_route instanceof Route) + { + return HTTP_Exception::factory(404, 'Unable to find a route to match the URI: :uri', array( + ':uri' => $this->_uri, + ))->request($this) + ->get_response(); + } + + if ( ! $this->_client instanceof Request_Client) + { + throw new Request_Exception('Unable to execute :uri without a Kohana_Request_Client', array( + ':uri' => $this->_uri, + )); + } + + // Multilang part + if(Request::$lang === NULL) + { + Request::$lang = $this->_route->lang; + } + + $config = Kohana::$config->load('multilang'); + if($config->hide_default AND $this->param('lang') === NULL OR $this->_route->lang === NULL AND $this->param('lang') === NULL) + { + Request::$lang = $config->default; + } + else + { + Request::$lang = $this->param('lang'); + } + Multilang::init(); + + return $this->_client->execute($this); + } + + +} \ No newline at end of file diff --git a/classes/Multilang/Route.php b/classes/Multilang/Route.php new file mode 100644 index 0000000..d94f483 --- /dev/null +++ b/classes/Multilang/Route.php @@ -0,0 +1,346 @@ +load('multilang'); + + if(!$config->hide_default || $config->default != $lang) + { + if($lang !== NULL) + { + $uri_callback = '/'.$uri_callback; + $regex['lang'] = $lang; + } + } + + if($lang !== NULL) + { + $name = $lang.'.'.$name; + } + + return Route::$_routes[$name] = new Route($uri_callback, $regex, $lang); + } + + + /** + * Retrieves a named route. + * + * $route = Route::get('default'); + * + * @param string route name + * @return Route + * @throws Kohana_Exception + */ + static public function get($name, $lang = NULL) + { + // We use the current language if none given + if($lang === NULL) + { + $lang = Request::$lang; + } + + // We first look for a "given_language.name" route. + if(isset(Route::$_routes[$lang.'.'.$name])) + { + $name = $lang.'.'.$name; + + } // then the default language + elseif(isset(Route::$_routes[Kohana::$config->load('multilang')->default.'.'.$name])) { + $name = $config->default.'.'.$name; + } + // And if we don't have any for this language, it means that route is neither defined nor multilingual + return parent::get($name); + } + + /** + * Altered constructor to handle multilingual routes + * + * Creates a new route. Sets the URI and regular expressions for keys. + * Routes should always be created with [Route::set] or they will not + * be properly stored. + * + * $route = new Route($uri, $regex); + * + * The $uri parameter can either be a string for basic regex matching or it + * can be a valid callback or anonymous function (php 5.3+). If you use a + * callback or anonymous function, your method should return an array + * containing the proper keys for the route. If you want the route to be + * "reversable", you need to return a 'uri' key in the standard syntax. + * + * $route = new Route(function($uri) + * { + * if (list($controller, $action, $param) = explode('/', $uri) AND $controller == 'foo' AND $action == 'bar') + * { + * return array( + * 'controller' => 'foobar', + * 'action' => $action, + * 'id' => $param, + * 'uri' => 'foo/bar/.html + * ); + * } + * }); + * + * @param mixed route URI pattern or lambda/callback function + * @param array key patterns + * @param + * @return void + * @uses Route::_compile + */ + public function __construct($uri = NULL, array $regex = NULL, $lang = NULL) + { + $this->lang = $lang; + return parent::__construct($uri, $regex); + } + + + /** + * Generates a URI for the current route based on the parameters given. + * + * // Using the "default" route: "users/profile/10" + * $route->uri(array( + * 'controller' => 'users', + * 'action' => 'profile', + * 'id' => '10' + * )); + * + * @param array $params URI parameters + * @return string + * @throws Kohana_Exception + * @uses Route::REGEX_Key + */ + public function uri(array $params = NULL, $lang = NULL) + { + // We define the language if required + if($this->lang !== NULL) + { + $params['lang'] = ($lang === NULL ? $this->lang : $lang); + } + + // Start with the routed URI + $uri = $this->_uri; + + if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE) + { + // This is a static route, no need to replace anything + + if ( ! $this->is_external()) + return $uri; + + // If the localhost setting does not have a protocol + if (strpos($this->_defaults['host'], '://') === FALSE) + { + // Use the default defined protocol + $params['host'] = Route::$default_protocol.$this->_defaults['host']; + } + else + { + // Use the supplied host with protocol + $params['host'] = $this->_defaults['host']; + } + + // Compile the final uri and return it + return rtrim($params['host'], '/').'/'.$uri; + } + + // Keep track of whether an optional param was replaced + $provided_optional = FALSE; + + while (preg_match('#\([^()]++\)#', $uri, $match)) + { + + // Search for the matched value + $search = $match[0]; + + // Remove the parenthesis from the match as the replace + $replace = substr($match[0], 1, -1); + + while (preg_match('#'.Route::REGEX_KEY.'#', $replace, $match)) + { + list($key, $param) = $match; + + if (isset($params[$param]) AND $params[$param] !== Arr::get($this->_defaults, $param)) + { + // Future optional params should be required + $provided_optional = TRUE; + + // Replace the key with the parameter value + $replace = str_replace($key, $params[$param], $replace); + } + elseif ($provided_optional) + { + // Look for a default + if (isset($this->_defaults[$param])) + { + $replace = str_replace($key, $this->_defaults[$param], $replace); + } + else + { + // Ungrouped parameters are required + throw new Kohana_Exception('Required route parameter not passed: :param', array( + ':param' => $param, + )); + } + } + else + { + // This group has missing parameters + $replace = ''; + break; + } + } + + // Replace the group in the URI + $uri = str_replace($search, $replace, $uri); + } + + while (preg_match('#'.Route::REGEX_KEY.'#', $uri, $match)) + { + list($key, $param) = $match; + + if ( ! isset($params[$param])) + { + // Look for a default + if (isset($this->_defaults[$param])) + { + $params[$param] = $this->_defaults[$param]; + } + else + { + // Ungrouped parameters are required + throw new Kohana_Exception('Required route parameter not passed: :param', array( + ':param' => $param, + )); + } + } + + $uri = str_replace($key, $params[$param], $uri); + } + + // Trim all extra slashes from the URI + //$uri = preg_replace('#//+#', '/', rtrim($uri, '/')); + $uri = preg_replace('#//+#', '/', $uri); + + if ($this->is_external()) + { + // Need to add the host to the URI + $host = $this->_defaults['host']; + + if (strpos($host, '://') === FALSE) + { + // Use the default defined protocol + $host = Route::$default_protocol.$host; + } + + // Clean up the host and prepend it to the URI + $uri = rtrim($host, '/').'/'.$uri; + } + + return $uri; + } + + /** + * Altered method to handle multilingual parameter + * + * Create a URL from a route name. This is a shortcut for: + * + * echo URL::site(Route::get($name)->uri($params), $protocol); + * + * @param string route name + * @param array URI parameters + * @param mixed protocol string or boolean, adds protocol and domain + * @return string + * @since 3.0.7 + * @uses URL::site + */ + static public function url($name, array $params = NULL, $protocol = NULL, $lang = NULL) + { + // Create a URI with the route and convert it to a URL + return URL::site(Route::get($name, $lang)->uri($params), $protocol); + } + + /** + * We don't want to remove the trailing slash. + */ + public function matches(Request $request) + { + // Get the URI from the Request + //$uri = trim($request->uri(), '/'); + $uri = ltrim($request->uri(), '/'); + + if ( ! preg_match($this->_route_regex, $uri, $matches)) + return FALSE; + + $params = array(); + foreach ($matches as $key => $value) + { + if (is_int($key)) + { + // Skip all unnamed keys + continue; + } + + // Set the value for all matched keys + $params[$key] = $value; + } + + foreach ($this->_defaults as $key => $value) + { + if ( ! isset($params[$key]) OR $params[$key] === '') + { + // Set default values for any key that was not matched + $params[$key] = $value; + } + } + + if ( ! empty($params['controller'])) + { + // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore + $params['controller'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['controller']))); + } + + if ( ! empty($params['directory'])) + { + // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore + $params['directory'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['directory']))); + } + + if ($this->_filters) + { + foreach ($this->_filters as $callback) + { + // Execute the filter giving it the route, params, and request + $return = call_user_func($callback, $this, $params, $request); + + if ($return === FALSE) + { + // Filter has aborted the match + return FALSE; + } + elseif (is_array($return)) + { + // Filter has modified the parameters + $params = $return; + } + } + } + + return $params; + } + +} \ No newline at end of file diff --git a/classes/Multilang/Routes.php b/classes/Multilang/Routes.php new file mode 100644 index 0000000..4be4498 --- /dev/null +++ b/classes/Multilang/Routes.php @@ -0,0 +1,77 @@ + 'home', + * 'fr' => 'accueil', + * ))->defaults(array( + * 'controller' => 'homepage', + * 'action' => 'index', + * )); + * + * @param string route name + * @param array URI patterns (array of "language code" => "uri") + * @param array regex patterns for route keys + * @return Routes + */ + static public function set($name, $uris = array(), $regex = NULL) + { + $config = Kohana::$config->load('multilang'); + $routes = new Routes(); + + // We add the routes for each language and set their names to lang.name (en.homepage for example). + // The segment is also added on the uri if it's not hidden + + $default_lang = $config->default; + $languages = $config->languages; + + // We first look for the default language uri which is obviously compulsory + $default_uri = Arr::get($uris, $default_lang); + if($default_uri === NULL) + { + throw new Kohana_Exception('The default language route uri is required for the route: :route', array(':route' => $name)); + } + else + { + $routes->_routes[$default_lang.'.'.$name] = Route::set($name, $default_uri, $regex, $default_lang); + } + unset($languages[$default_lang]); + + // Then we add the routes for all the other languages + foreach($languages as $lang => $settings) + { + $uri = (Arr::get($uris, $lang) ? $uris[$lang] : $uris[$default_lang]); + + // For the uri, we use the one given or the default one + $routes->_routes[$lang.'.'.$name] = Route::set($name, $uri, $regex, $lang); + } + return $routes; + } + + + /** + * Set the defaults values for each route + * @param array $defaults + * @return Multilang_Routes + */ + public function defaults(array $defaults = NULL) + { + foreach($this->_routes as $route) + { + $route->defaults($defaults); + } + return $this; + } + + +} \ No newline at end of file diff --git a/classes/Multilang/URL.php b/classes/Multilang/URL.php new file mode 100644 index 0000000..570f754 --- /dev/null +++ b/classes/Multilang/URL.php @@ -0,0 +1,22 @@ +