Zend Framework

Empty static route (empty string) will NEVER match (sample from docs, Route_Hostname, Route_Static)

Details

  • Type: Bug Bug
  • Status: Resolved Resolved
  • Priority: Blocker Blocker
  • Resolution: Fixed
  • Affects Version/s: 1.7.1, 1.7.2, 1.7.3, 1.7.4, 1.7.5, 1.7.6, 1.7.7, 1.7.8, 1.8.0, 1.8.1, 1.8.2, 1.8.3, 1.8.4, 1.9.0, 1.9.1, 1.9.2, 1.9.3
  • Fix Version/s: 1.11.0
  • Component/s: Zend_Controller
  • Labels:
    None

Description

The following is suggested in docs:

$hostnameRoute = new Zend_Controller_Router_Route_Hostname(
    ':username.users.example.com',
    array(
        'controller' => 'profile',
        'action'     => 'userinfo'
    )
);

$plainPathRoute = new Zend_Controller_Router_Route_Static('');

$router->addRoute('user', $hostnameRoute->chain($plainPathRoute);

The purpose of $plainPathRoute: to create a default route for users visiting the hostname *.users.example.com.
Unfortunatelly this will never work, because static route of [empty string] will never match(). Unfortunatelly, leaving only the Route_Hostname is not an option - because as stated in the manual, lone Route_Hostname will catch each and every request .

The bug is here: (line 78 in Zend/Controller/Router/Route/Static.php)

Zend/Controller/Router/Route/Static.php:76
public function match($path, $partial = false)
    {
        if ($partial) {
            if (substr($path, 0, strlen($this->_route)) === $this->_route) {
                $this->setMatchedPath($this->_route);
                return $this->_defaults;
            }
        } else {
            if (trim($path, '/') == $this->_route) {
                return $this->_defaults;
            }
        }

Why? substr() of empty string always returns false - thus this route will never match and the whole chain is ommited.

Fix:

FIX FOR Zend/Controller/Router/Route/Static.php:76
public function match($path, $partial = false)
    {
        if ($partial) {
            if (($this->_route === '' && $path === '') || substr($path, 0, strlen($this->_route)) === $this->_route) {
                $this->setMatchedPath($this->_route);
                return $this->_defaults;
            }
        } else {
            if (($this->_route === '' && $path === '') || trim($path, '/') == $this->_route) {
                return $this->_defaults;
            }
        }

Issue Links

Activity

Hide
Artur Bodera added a comment -

Modified fix to work with non-partial matches.

Show
Artur Bodera added a comment - Modified fix to work with non-partial matches.
Hide
Artur Bodera added a comment -

UPDATE

It is also broken for simple chains, like the one in link above:

<?xml version="1.0" encoding="UTF-8"?>
<routes>
    <admin>
        <route>admin</route>
        <defaults>
                <module>admin</module>
                <controller>index</controller>
                <action>index</action>
        </defaults>
        <chains>
            <index type="Zend_Controller_Router_Route_Static">
                <route></route>
                <defaults module="admin" controller="index" action="index" />
            </index>
            <login>
                <route>login</route>
                <defaults>
                    <module>admin</module>
                    <controller>login</controller>
                    <action>index</action>
                </defaults>
            </login>
        </chains>
    </admin>
</routes>

Expected:
To work for urls /admin and /admin/login.

Actual:
It will only match for /admin/login.

Workaround:
It's caused by the following snippet in Zend_Controller_Router_Route_Chain:

Zend/Controller/Router/Route/Chain.php:666
/**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if ($separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = substr($subPath, strlen($separator));
            }

Below is a quick fix which takes into account the behaviour of substr() on empty strings, as in this case when chain has already consumed admin and an empty '' $subpath is left for matching.

Zend/Controller/Router/Route/Chain.php:666
/**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if (($subPath !== '') && $separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = (string)substr($subPath, strlen($separator));
            }

What happens is that we check for empty string '' and then force (string) as a result of substr(), because other routes would fail to match agains false.

This allows "default" routes to work with simple non-host-based chains!

Cheers!

Show
Artur Bodera added a comment - UPDATE It is also broken for simple chains, like the one in link above:
<?xml version="1.0" encoding="UTF-8"?>
<routes>
    <admin>
        <route>admin</route>
        <defaults>
                <module>admin</module>
                <controller>index</controller>
                <action>index</action>
        </defaults>
        <chains>
            <index type="Zend_Controller_Router_Route_Static">
                <route></route>
                <defaults module="admin" controller="index" action="index" />
            </index>
            <login>
                <route>login</route>
                <defaults>
                    <module>admin</module>
                    <controller>login</controller>
                    <action>index</action>
                </defaults>
            </login>
        </chains>
    </admin>
</routes>
Expected: To work for urls /admin and /admin/login. Actual: It will only match for /admin/login. Workaround: It's caused by the following snippet in Zend_Controller_Router_Route_Chain:
Zend/Controller/Router/Route/Chain.php:666
/**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if ($separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = substr($subPath, strlen($separator));
            }
Below is a quick fix which takes into account the behaviour of substr() on empty strings, as in this case when chain has already consumed admin and an empty '' $subpath is left for matching.
Zend/Controller/Router/Route/Chain.php:666
/**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if (($subPath !== '') && $separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = (string)substr($subPath, strlen($separator));
            }
What happens is that we check for empty string '' and then force (string) as a result of substr(), because other routes would fail to match agains false. This allows "default" routes to work with simple non-host-based chains! Cheers!
Hide
Maurice Fonk added a comment -

In 1.10 alpha this patch does not give the required result.

Show
Maurice Fonk added a comment - In 1.10 alpha this patch does not give the required result.
Hide
Artur Bodera added a comment -

Thank you for info.

I am willing to analyze it and contribute a new patch as long as there is any chance of (finally) commiting and fixing it permanently!

Show
Artur Bodera added a comment - Thank you for info. I am willing to analyze it and contribute a new patch as long as there is any chance of (finally) commiting and fixing it permanently!
Hide
Edvin Seferovic added a comment -

Artur it would be great if you could look into this problem, because it is a real blocker (for me).

I need hostname based routing for my modules and the only "half"-workaround Ive managed to produce is following...

$hostnameRoute = new Zend_Controller_Router_Route_Hostname('admin.test.local',array('module' => 'admin'));

// Instead of empty static route...
// $oRoute = new Zend_Controller_Router_Route_Static('');

$oRoute = new Zend_Controller_Router_Route('/:controller/:action/*',
array('module' => 'admin',
'controller' => 'index',
'action' => 'index'));

$router->addRoute('admin', $hostnameRoute->chain($oRoute));

With this Ive managed the routing part... but in the routing process the path information is build without "/" at the beginning. $this->_request->getPathInfo() returns "controller/action" instead of "/controller/action" This breaks the Zend_Navigation component !!

Maybe there is another workaround I am not aware of?

Show
Edvin Seferovic added a comment - Artur it would be great if you could look into this problem, because it is a real blocker (for me). I need hostname based routing for my modules and the only "half"-workaround Ive managed to produce is following... $hostnameRoute = new Zend_Controller_Router_Route_Hostname('admin.test.local',array('module' => 'admin')); // Instead of empty static route... // $oRoute = new Zend_Controller_Router_Route_Static(''); $oRoute = new Zend_Controller_Router_Route('/:controller/:action/*', array('module' => 'admin', 'controller' => 'index', 'action' => 'index')); $router->addRoute('admin', $hostnameRoute->chain($oRoute)); With this Ive managed the routing part... but in the routing process the path information is build without "/" at the beginning. $this->_request->getPathInfo() returns "controller/action" instead of "/controller/action" This breaks the Zend_Navigation component !! Maybe there is another workaround I am not aware of?
Hide
Artur Bodera added a comment -

Hey Edvin!

Which version are you using?
Have you patched your ZF with the snippets I provided?

I use these routes (suggested by docs, sic!) every day and they work fine.

Show
Artur Bodera added a comment - Hey Edvin! Which version are you using? Have you patched your ZF with the snippets I provided? I use these routes (suggested by docs, sic!) every day and they work fine.
Hide
John Kleijn added a comment -

Same issue here. I really don't want to patch ZF. I can probably override match() and the method in the standard router to use the child class instead of Zend_Controller_Router_Route_Chain on "chain" in the config, but obviously that's hardly ideal.

Show
John Kleijn added a comment - Same issue here. I really don't want to patch ZF. I can probably override match() and the method in the standard router to use the child class instead of Zend_Controller_Router_Route_Chain on "chain" in the config, but obviously that's hardly ideal.
Hide
John Kleijn added a comment -

I fixed this by overriding some methods in the involved route classes (thanks Artur), but one issue remains: it never produces a 404. It always goes to the index action of the default module. This is probably a separate related issue, but just wanted to check if people experiencing the issue in this ticket are also having this issue..

Show
John Kleijn added a comment - I fixed this by overriding some methods in the involved route classes (thanks Artur), but one issue remains: it never produces a 404. It always goes to the index action of the default module. This is probably a separate related issue, but just wanted to check if people experiencing the issue in this ticket are also having this issue..
Hide
Steven Young added a comment -

I've just come up against the same issue.

Is there a reason Artur's patches can not be commited in the trunk? I'm surprised more people aren't complaining about this problem.

Show
Steven Young added a comment - I've just come up against the same issue. Is there a reason Artur's patches can not be commited in the trunk? I'm surprised more people aren't complaining about this problem.
Hide
Matthew Weier O'Phinney added a comment -

I actually get the exact opposite of what the reporter and several commenters have discovered (when testing against current trunk, which is 1.11.0beta1): I can match the /admin route, but not the /admin/login route. I'm attempting to fix this issue now.

Show
Matthew Weier O'Phinney added a comment - I actually get the exact opposite of what the reporter and several commenters have discovered (when testing against current trunk, which is 1.11.0beta1): I can match the /admin route, but not the /admin/login route. I'm attempting to fix this issue now.
Hide
Matthew Weier O'Phinney added a comment -

Additionally, the behavior does not change with the "patch" applied.

Show
Matthew Weier O'Phinney added a comment - Additionally, the behavior does not change with the "patch" applied.
Hide
Kim Blomqvist added a comment -

Matthew: check if this is related to ZF-8812.

Show
Kim Blomqvist added a comment - Matthew: check if this is related to ZF-8812.
Hide
Matthew Weier O'Phinney added a comment -

Kim – nope. (I've applied your patches locally; doesn't change anything in regards to the environment and expectations presented here.)

Show
Matthew Weier O'Phinney added a comment - Kim – nope. (I've applied your patches locally; doesn't change anything in regards to the environment and expectations presented here.)
Hide
Matthew Weier O'Phinney added a comment -

Fixed in trunk and release branch. Patch had to change due to changes that have already been introduced; basic gist was that a check for (empty($path) && empty($this->_route)) had to ORd to the existing partial conditional in the Static route.

Show
Matthew Weier O'Phinney added a comment - Fixed in trunk and release branch. Patch had to change due to changes that have already been introduced; basic gist was that a check for (empty($path) && empty($this->_route)) had to ORd to the existing partial conditional in the Static route.

People

Vote (10)
Watch (10)

Dates

  • Created:
    Updated:
    Resolved: