Magento Routing: Using the same frontname for admin and frontend routes

I recently noticed an issue with a module, Devinc_Dailydeal, where one of it’s pages was redirecting to the same page under the base URL of the admin store. For example, I would visit http://www.myfrontend.com/mymodule and get redirected to https://www.myadmin.com/mymodule.

I looked into the module’s config.xml file to check the defined routes. I noticed that there was a route defined under the “frontend” node as well as a route defined under the “admin” node with the frontname “dailydeal”.

# File: app/code/community/Devinc/Dailydeal/etc/config.xml
<?xml version="1.0"?>
<config>
    ...
    <frontend>
        <routers>
            <dailydeal>
               <use>standard</use>
               <args>
                   <module>Devinc_Dailydeal</module>
                   <frontName>dailydeal</frontName>
               </args>
           </dailydeal>
       </routers>
    </frontend>
    ...
    <admin>
        <routers>
            <dailydeal>
                 <use>admin</use>
                 <args>
                     <module>Devinc_Dailydeal</module>
                     <frontName>dailydeal</frontName>
                 </args>
             </dailydeal>
        </routers>
    </admin> 
    ...
</config>

At first glance, this seemed ok since they are using separate routers. Closer inspection revealed that the Admin router will always be matched first. Routers are processed in a stack on every request. The default routers are Admin, Standard, Cms, then Default. (For more info on Magento’s routers, check Alan Storm’s blog post). This means that the Admin router runs on every page, not just pages starting with “admin”. The Admin router runs first and hits a match first on “dailydeal”. It does not know that “dailydeal” has also been specified as a frontname for the Standard router. It just knows that it has found a match and proceeds to route it.

While the Admin router is routing the request, it checks if the URL should be secure. This checks against the Admin store’s settings, not the frontend store. If the Admin is set to use secure pages and the admin secure base URL is https and is different from the current URL, a redirect will be issued. This is correct behavior but can cause a lot of confusion.

I looked into a number of other third party modules we have used and noticed a significant number of them use the same frontname for the Standard router and the Admin router. This means that under this set of circumstances, these will all break. In all likelyhood, these modules were never tested in a multi store setup with SSL implemented and never will be.

This behavior only occurs if:

The frontend base URL is different from the admin base URL (If not, it will just redirect to https, which probably won’t cause any issues other than possibly broken SSL)
The admin is set to use secure URLs
The secure URL for the admin is actually secure (starts with https)
No redirect is issued if the admin is not set to use secure URLs, even if the base URL is different. This seems like a logic error to me, but we’ll leave that be.

The Fix:

Beware, there is a lot of work to be done here and a lot of updates made to third party code, which is sub-optimal. Only do this if your site meets the aforementioned conditions and you are seeing this issue.

There is no simple solution to this. You cannot just change the frontname of the admin route. You must also change the route name. This is because Magento expects both route frontnames and route names to be unique across all routers. Specifically, Mage_Core_Model_Url::getUrl() eventually calls a method on the front controller which retreives the router from the route name, which must be unique or there will be conflicts.

# File: app/code/core/Mage/Core/Controller/Varien/Front.php
public function getRouterByRoute($routeName)
{
    // empty route supplied - return base url
    if (empty($routeName)) {
        $router = $this->getRouter('standard');
    } elseif ($this->getRouter('admin')->getFrontNameByRoute($routeName)) {
        // try standard router url assembly
        $router = $this->getRouter('admin');
    } elseif ($this->getRouter('standard')->getFrontNameByRoute($routeName)) {
        // try standard router url assembly
        $router = $this->getRouter('standard');
    } elseif ($router = $this->getRouter($routeName)) {
        // try custom router url assembly
    } else {
        // get default router url
        $router = $this->getRouter('default');
    }

    return $router;
}

Here, if the Admin router and the Standard router both have a “dailydeal” route defined, the Admin router will always win, even on frontend pages. Could this be any more convoluted Magento?

Once you change the route name, you will also have to update the layout handles in the adminhtml layout file to match, since they are prefixed with the route name. If you are going to do all of this, why not just fix it correctly…

So here’s how to fix it.

Replace the admin route with an injection of your module into the existing adminhtml route.

# File: app/code/community/Devinc/Dailydeal/etc/config.xml
<admin>
    <routers>
        <dailydeal>
            <use>admin</use>
            <args>
                <module>Devinc_Dailydeal</module>
                <frontName>dailydeal</frontName>
            </args>
        </dailydeal>
    </routers>
</admin>

Becomes:

<admin>
    <routers>
        <adminhtml>
            <args>
                <modules>
                    <Devinc_Dailydeal_Adminhtml before="Mage_Adminhtml">Devinc_Dailydeal_Adminhtml</Devinc_Dailydeal_Adminhtml>
                </modules>
            </args>
        </adminhtml>
    </routers>
</admin>

Update the adminhtml menu actions:

# File: app/code/community/Devinc/Dailydeal/etc/config.xml
<adminhtml>
    <menu>
        <dailydeal module="dailydeal">
             <title>Daily Deal</title>
             <sort_order>71</sort_order>
             <children>
                 <add module="dailydeal">
                     <title>Add Deal</title>
                     <sort_order>0</sort_order>
                     <action>dailydeal/adminhtml_dailydeal/new/</action>
                </add>
                ...
            </children>
        </dailydeal>
    </menu>
</adminhtml>

Becomes:

<adminhtml>
    <menu>
        <dailydeal module="dailydeal">
            <title>Daily Deal</title>
            <sort_order>71</sort_order>
            <children>
                <add module="dailydeal">
                    <title>Add Deal</title>
                    <sort_order>0</sort_order>
                    <action>adminhtml/dailydeal/new/</action>
                </add>
                ...
            </children>
        </dailydeal>
    </menu>
</adminhtml>

Replace the adminhtml layout handles:

# File: app/design/frontend/default/default/layout/dailydeal.xml
<dailydeal_adminhtml_dailydeal_index>
    <reference name="content">
        <block type="dailydeal/adminhtml_dailydeal" name="dailydeal" />
    </reference>
</dailydeal_adminhtml_dailydeal_index>

Becomes:

<adminhtml_dailydeal_index>
    <reference name="content">
        <block type="dailydeal/adminhtml_dailydeal" name="dailydeal" />
    </reference>
</adminhtml_dailydeal_index>

When working in the admin, the url will now be https://www.myadmin.com/admin/dailydeal

This could mean that you need to make changes elsewhere if there are hardcoded URLs anywhere. I noticed that I had to hard set a form action in one of the modules I was working with.

What can be learned from this?

When writing a module, do not use the same frontname for the Standard and Admin routers. In fact, don’t even create an admin router. All URLs in the admin should start with “/admin” (or whatever the admin frontname is configured to). This makes it clear and consistent to users that they are still in the admin.

Instead, inject controllers into the existing “adminhtml” route like this:

<?xml version="1.0"?>
<config>
    ...
    <admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <MyNamespace_MyModule_Adminhtml before="Mage_Adminhtml">MyNamespace_MyModule_Adminhtml</MyNamespace_MyModule_Adminhtml>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>
    ...
</config>

Then, create your admin controllers at MyNamespace/MyModule/controllers/Adminhtml.

The only caveat with doing this is that you must ensure you don’t create any naming conflicts with other admin controllers in core or other third party code. Use a specific and unique controller class name to avoid this.