26 September 2011

Magento - sharing the cart between stores

EDIT: 08/01/13 - Improved store switching changes to Varien.php

When running multiple Magento stores off one install, you will probably want to have your session data share between stores. This of course includes, your cart and login status amongst other things. If you looked at System→Configuration→Web→Session Validation Settings in admin, you would be forgiven for thinking all you need to do is enable Use SID on Frontend to get this working. Unfortunately for many stores this seems often to be buggy and ineffective. In my own experience I have found it sometimes to append, and sometimes not to append the session id to the store switch urls, and when it has been appended and you switch stores it doesn't actually carry the session data over at all.

Buggy and frustrating, however with not too many lines of code you can get your stores sharing the cart and other session data reliably even if they are on different domains.

The first thing we want to do is disable the Use SID on Frontend setting as detailed above, we'll be adding the session id to the store change URL's ourselves.

Next copy
app/design/frontend/base/default/template/page/switch/stores.phtml
to
app/design/yourpackagename/yourtheme/default/template/page/switch/stores.phtml
if it isn't already there, but obviously replace yourpackagename and yourtheme with whatever package name and theme you are using on your particular store.

A lot of people will want to edit the way store switching happens as by default it's a not very exciting drop down form menu, so if you have changed this on your store you just need to target the equivalent url generation code for your store. For the sake of this tutorial I will be referring to the standard, unchanged, store code. So, open the file you just copied and in there you should see a select element:
<select id="select-store" title="<?php echo $this->__('Select Store') ?>" onchange="location.href=this.value">
 
and then a foreach loop creating the select options:
<?php foreach ($this->getGroups() as $_group): ?>
    <?php $_selected = ($_group->getId()==$this->getCurrentGroupId()) ? ' selected="selected"' : '' ?>
    <option value="<?php echo $_group->getHomeUrl() ?>"<?php echo $_selected ?>><?php echo $this->htmlEscape($_group->getName()) ?></option>
<?php endforeach; ?>
So when we select an option the onchange event handler takes us to the url being the value attribute of the option we have selected, this value attribute is what we need to edit.

To do this replace the line:
<option value="<?php echo $_group->getHomeUrl() ?>"<?php echo $_selected ?>><?php echo $this->htmlEscape($_group->getName()) ?></option>
 
with this:
<?php
    $this_session_id = Mage::getSingleton('core/session', array('name' => 'frontend'))->getSessionId();
    $this_store_url = explode('?', $_group->getHomeUrl());
    $this_store_url = $this_store_url[0] . '?SID=' . $this_session_id . '&___store=' . $_group->getCode();
?>
<option value="<?php echo $this_store_url ?>"<?php echo $_selected ?>><?php echo $this->htmlEscape($_group->getName()) ?></option>
The first line of code stores the current session id, the second line stores the url that would have been used for the value attribute, and the third line takes the value attribute url without any GET arguments and appends the session id followed by the store code. The last line of code uses the rebuilt URL as the value attribute in place of the original value.

These are all the changes we are going to need to correctly generate the store switching URLs so you can save and close this file.

Now copy
app/code/core/Mage/Core/Model/Session/Abstract/Varien.php
to
app/code/local/Mage/Core/Model/Session/Abstract/Varien.php
If you have read this post before, this is the updated section with a solution to allow the session to be properly passed across in the first page load.

In this file we are going to be making use of a one liner that is already present in this file but has not been implemented by default for some reason. This allows us to change the session id before the session is initialised so that when session_start() is called data is pulled from the previous stores session rather than a new session.

Open the file you just copied and look for the following line:
$this->setSessionId();
All we need to do here is pass the session id we have appended to the store switch url's to this method and it will set the session id for us, so change that line to the following:
if (isset($_GET['SID'])):
    $this->setSessionId($_GET['SID']);
endif;
So a few workarounds needed to patch up Magento's less than complete session handling when switching stores, but these changes should allow you to correctly pass the cart and other session data between stores even when they are on different domains.

21 September 2011

Magento - remove trailing slash from urls

For SEO reasons store owners will often want to standardise the URL generation in their Magento store. And rightly so, if your store is serving identical content at multiple urls then you could be penalised for having duplicate content on your site which will in turn negatively impact your search engine rankings.

To make sure you don't run into these problems with your Magento store, there are a few changes that are needed:

- Modify the method that creates URLs
- Fix some flawed Magento core logic
- Create an htaccess rewrite rule to remove manually added slashes

Firstly lets modify the getUrl() method to remove any trailing slashes in generated URLs. Copy the file app/code/core/Mage/Core/Block/Abstract.php to app/code/local/Mage/Core/Block/Abstract.php if it's not there already.

Open app/code/local/Mage/Core/Block/Abstract.php and find the getUrl() method, it should just be one line of code that returns the requested URL:
return $this->_getUrlModel()->getUrl($route, $params);
Replace that line with the following:
$return_url = $this->_getUrlModel()->getUrl($route, $params);
if ($return_url != $this->getBaseUrl() && substr($return_url, -1) == '/' && !Mage::getSingleton('admin/session')->isLoggedIn()):
    return substr($return_url, 0, -1);
else:
    return $return_url;
endif;
The above is pretty straignt forward stuff really, firstly we store the URL that would normally get returned from the method into the variable $return_url. We then run some tests to make sure firstly, the url we have is not for the homepage (in which case we do not want to remove the trailing slash), secondly it has a trailing slash as the last character, and thirdly we are not currently logged in to admin. When all three of these conditions are satisfied, we can remove the trailing slash from the end of the URL.

Now in theory that should be all we need to do, however there is, what I can only assume is some failed logic in another Magento core file that means this approach fails in some cases. To correct this, copy the file app/code/core/Mage/Core/Model/Url.php to app/code/local/Mage/Core/Model/Url.php if it's not there already.

Open app/code/local/Mage/Core/Model/Url.php and find the following:
if ($noSid !== true) {
    $this->_prepareSessionUrl($url);
}
A little further up the file the variable $noSid is initialised:
$noSid = null;
It is then set to either boolean true/false if certain conditions are met. If the conditions are not met, $noSid remains as null, the if statement is satisfied and the session id is appended to the URL (even though it should not be). In this case, what is actually returned is a session id of simply the letter U. The addition of this to the end of the URL means the first change we made will fail because the final character is not a slash.

So with the if statement above it seems logical that we only want to append the session id to the URL if $noSid is identically equal to false, thus ruling out null as a pass condition. So go ahead and change the if statement from:
if ($noSid !== true) {
to
if ($noSid === false) {
With this change, the session id will no longer be incorrectly added onto the URL and the first change we made to remove the trailing slash will succeed.

This should now remove all trailing slashes from generated URLs throughout the store, but a trailing slash can still be manually added to serve the same content. To cater for this scenario, we want to create a 301 redirect from all trailing slash URLs to a non trailing slash version of the page. We can do this in the Magento .htaccess file in the install root.

Open .htaccess and find the following rule:
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Immediately after this add the following lines:
RewriteCond %{request_method} ^GET$
RewriteCond %{REQUEST_URI} !^/downloader.*$
RewriteCond %{REQUEST_URI} ^(.+)/$
RewriteRule ^(.+)$ %1 [L,R=301]
The redirect works in the following way. When requesting existing data from the server Magento will only ever use the GET method, and when sending new data to the server, POST will only ever be used (as is the standard). We never want to touch the POST requests (and indeed if you do you will find all kinds of problems like not being able to add to the basket, or even save anything in admin) so we restrict this rule to only ever affect GET requests - which is how pages will always be requested. The second line ensures the rule is not applied for Magento Connect Manager as this will break it. The third line ensures the rule is only ever applied to URLs with a trailing slash, and also stores all of the request URL up until that trailing slash into a back reference (the section in the brackets). With the conditions of the first three lines satisfied, the rule is then applied as a 301 redirect to the URL that has been stored in the RewriteCond backreference (so the url without the trailing slash). Note that if we wanted to redirect to the backreference in the RewriteRule we would use $1 instead of %1.

Integrating these changes will ensure your store never serves duplicate content caused by a trailing slash, and sets up a 301 redirect for any duplicate content already found by search engines.