Difference between #4 and #3 of URL management for Websites with secure and nonsecure pages

unchanged
Title
URL management in secure and nonsecure pages
unchanged
Category
How-tos
unchanged
Tags
URL
changed
Content
In this article, I will describe how to manage URLs for a Website that has both
secure and nonsecure content. 

Secure content are sent via the `https` protocol using SSL (secure socket
layer), while nonsecure content via the normal `http` protocol. For simplicity
of description, let's call the former `https` content/page while the latter
`http` content/page. A serious Website often needs to serve some pages in
`https` and some in `http`. For example, to prevent password sniffing, we would
like to serve the login page using `https`; and to save server processing power,
we would like to serve insensitive content (e.g. home page) using `http`. 

A requirement is that when we are in an `https` page we want to generate URLs to
`http` pages, and vice versa. For example, a Web site has a main menu shared by
all pages, and the main menu contains links to both `http` page (e.g. About
page) and `https` page (e.g. Login page). If we are now in an `http` page, we
can use relative URLs (e.g. /about) to link to other `http` pages, but we have
to use absolute URLs with `https` protocol to link to the `https` pages. And if
we are in an `https` page, the situation will be the other way around.

Another requirement is that if a secure page is requested via an `http` request,
we should redirect the browser to use `https` protocol; and vice versa. The
redirection usually should be 301 permanent redirection. It is possible to
achieve this goal via the rewrite rules of Web servers. But the rewrite rules
could become very complex if we want fine-grained control of secure and
nonsecure content.

To achieve the above two requirements, we can extend [CUrlManager] as follows,

~~~
[php]

class UrlManager extends CUrlManager
{
	/**
	 * @var string the host info used in non-SSL mode
	 */
	public $hostInfo = 'http://localhost';
	/**
	 * @var string the host info used in SSL mode
	 */
	public $secureHostInfo = 'https://localhost';
	/**
	 * @var array list of routes that should work only in SSL mode.
	 * Each array element can be either a URL route (e.g. 'site/create') 
	 * or a controller ID (e.g. 'settings'). The latter means all actions
	 * of that controller should be secured.
	 */
	public $secureRoutes = array();
	
	public function createUrl($route, $params = array(), $ampersand = '&')
	{
		$url = parent::createUrl($route, $params, $ampersand);
		
		// If already an absolute URL, return it directly
		if (strpos($url, 'http') === 0) {
			return $url;  
		}
		
		// Check if the current protocol matches the expected protocol of the route
		// If not, prefix the generated URL with the correct host info.
		$secureRoute = $this->isSecureRoute($route);
		if (Yii::app()->request->isSecureConnection) {
			return $secureRoute ? $url : $this->hostInfo . $url;
		} else {
			return $secureRoute ? $this->secureHostInfo . $url : $url;
		}
	}

	public function parseUrl($request)
	{
		$route = parent::parseUrl($request);
		
		// Perform a 301 redirection if the current protocol 
		// does not match the expected protocol
		$secureRoute = $this->isSecureRoute($route);
		$sslRequest = $request->isSecureConnection;
		if ($secureRoute !== $sslRequest) {
			$hostInfo = $secureRoute ? $this->secureHostInfo : $this->hostInfo;
			if ((strpos($hostInfo, 'https') === 0) xor $sslRequest) {
				$request->redirect($hostInfo . DIRECTORY_SEPARATOR .
$route,$request->url, true, 301);
			}
		}
		return $route;
	}

	private $_secureMap;

	/**
	 * @param string the URL route to be checked
	 * @return boolean if the give route should be serviced in SSL mode
	 */
	protected function isSecureRoute($route)
	{
		if ($this->_secureMap === null) {
			foreach ($this->secureRoutes as $r) {
				$this->_secureMap[strtolower($r)] = true;
			}
		}
		$route = strtolower($route);
		if (isset($this->_secureMap[$route])) {
			return true;
		} else {
			return ($pos = strpos($route, '/')) !== false 
				&& isset($this->_secureMap[substr($route, 0, $pos)]);
		}
	}
}
~~~

Now in the application configuration, we specify `UrlManager` as our URL manager
instead of the default [CUrlManager]:

~~~
[php]
return array(
    // ....
    'components' => array(
        'urlManager' => array(
            'class' => 'UrlManager',
            'urlFormat' => 'path',
            'hostInfo' => 'http://example.com',
            'secureHostInfo' => 'https://example.com',
            'secureRoutes' => array(
            	'site/login',   // site/login action
            	'site/signup',  // site/signup action
            	'settings',     // all actions of SettingsController
            ),
        ),
    ),
);
~~~

In the above code, we configure `UrlManager` to secure the login, sign-up and
settings pages. If you want to secure other pages, just add the corresponding
routes to the `secureRoutes` array shown above.

We can now use `Yii::app()->createUrl()` as usual in places where we need to
create URLs. Our `UrlManager` will automatically determine if the generated URLs
need to be prefixed with proper host info. The `UrlManager` will also do the 301
redirection work if needed.