Empty order emails (no subject, no body)

One of my customers had a problem this week:

Emails looked like this:
empty email, no body, no subject

It was literally empty. The server filled a few head fields, but there was nothing left of the content we want to send (new order email).

It took a while, but finally I think I found the problem (unfortunatelly not sure about the cause).

Magento saves stuff while going down this trace:

\Mage_Core_Model_Abstract::save
\Mage_Core_Model_Resource_Db_Abstract::save
\Mage_Core_Model_Resource_Db_Abstract::_prepareDataForSave
\Mage_Core_Model_Resource_Abstract::_prepareDataForTable
\Varien_Db_Adapter_Pdo_Mysql::describeTable

This method should return an array like this:

...
[created_at] => Array
        (
            [SCHEMA_NAME] =>
            [TABLE_NAME] => core_email_queue
            [COLUMN_NAME] => created_at
            [COLUMN_POSITION] => 8
            [DATA_TYPE] => timestamp
            [DEFAULT] =>
            [NULLABLE] => 1
            [LENGTH] =>
            [SCALE] =>
            [PRECISION] =>
            [UNSIGNED] =>
            [PRIMARY] =>
            [PRIMARY_POSITION] =>
            [IDENTITY] =>
        )

    [processed_at] => Array
        (
            [SCHEMA_NAME] =>
            [TABLE_NAME] => core_email_queue
            [COLUMN_NAME] => processed_at
            [COLUMN_POSITION] => 9
            [DATA_TYPE] => timestamp
            [DEFAULT] =>
            [NULLABLE] => 1
            [LENGTH] =>
            [SCALE] =>
            [PRECISION] =>
            [UNSIGNED] =>
            [PRIMARY] =>
            [PRIMARY_POSITION] =>
            [IDENTITY] =>
        )
...

But it returned

[data] => a:9:{s:10:"message_id";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:16:"core_email_queue";s:11:"COLUMN_NAME";s:10:"message_id";s:15:"COLUMN_POSITION";i:1;s:9:"DATA_TYPE";s:3:"int";s:7:"DEFAULT";N;s:8:"NULLABLE";b:0;s:6:"LENGTH";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";b:1;s:7:"PRIMARY";b:1;s:16:"PRIMARY_POSITION";i:1;s:8:"IDENTITY";b:1;}s:9:"entity_id";a:14...

As you might guess, this is a serialized array.

When we have a look into \Varien_Db_Adapter_Pdo_Mysql::loadDdlCache we see, that the schema is cached, but I think there is no way, the serialized data are returned:

// lib/Varien/Db/Adapter/Pdo/Mysql.php:1548
$data = $this->_cacheAdapter->load($cacheId);
if ($data !== false) {  
    $data = unserialize($data);

To be continued...

REAL file sizes on Mac OS X

I'm searching for exceptions containing logic, not only class ... extends ..., so I searched for a way to get the real file size.

What didn't work

# only lists directories
find . -type f | du | grep Exception

# lists files in block size (4kb) which isn't accurate enough
find . -type f | du -a | grep Exception

What I ended up with:

find . -type f -print0 | xargs -0 stat -f "%z - %N" | grep Exception | sort

(x)Debug PHP CLI script via SSH without port forwarding

There are lots of examples on the net which all tell you that you should forward port 9000 to your local machine. This is helpful and needed if you want to debug a "real" remote machine, like a production or staging server.

But I want to debug a CLI script on a virtual machine (VM) and want a script which connects back on my local network to my host machine.

I know puphpet is doing this with the builtin xdebug script, but I can't find it on the net, so here it is:

#!/bin/bash
XDEBUG_CONFIG="idekey=xdebug" php -dxdebug.remote_host=`echo $SSH_CLIENT | cut -d "=" -f 2 | awk '{print    $1}'` "$@"

Just put it in a file ~/bin/xdebug and make it executable, then you can run your cli file with

xdebug file.php

and it should reconnect to your PHPStorm (or whatever other IDE you use).

Magento Core Cache Bug

Thanks very much to my colleagues from iWelt who found this bug and allow me to publish it here to get all the reputation ;-)

Magento can overwrite a cached block with a different one.

Magento Cache Key

Magento generates the cache key by default using the array returned by getCacheKeyInfo:

\Mage_Core_Block_Abstract::getCacheKey
public function getCacheKey()
{
    if ($this->hasData('cache_key')) {
        return $this->getData('cache_key');
    }
    $key = $this->getCacheKeyInfo();
    ...
}

The default implementation of getCacheKeyInfo only takes the name in the layout in consideration:

\Mage_Core_Block_Abstract::getCacheKeyInfo
public function getCacheKeyInfo()
{
    return array(
        $this->getNameInLayout()
    );
}

So far so good.

What is the name of a block, which is created WITHOUT a name?

Block name if no name is given

public function createBlock($type, $name='', array $attributes = array())
{
    ...  
    $block = $this->_getBlockInstance($type, $attributes);
    ...
    $name = 'ANONYMOUS_'.sizeof($this->_blocks);
    ...
    $block->setNameInLayout($name);
    ...
    $this->_blocks[$name] = $block;
    ...
    }
}

As you can see, the name of the block is set automatically. In case you don't know sizeof: it's an alias for count.

So the raising number is the current block count of the whole page.

This means, the name of the block is ANONYMOUS_1, ANONYMOUS_2, etc.

This means, the cache key is generated from [ANONYMOUS_1].

The problem

Now imagine, you have two different pages, which have a block, without a name. These two blocks accidentally have the same sequential number during generation.

Same cache key means: one entry

So in the end you'll get the same cache key and this means: Magento treats it as the same block.

In our case the homepage content block was overwritten by a block of the sidebar.

Solution

My colleague came up with this solution:

We add the current action to the block name:

$name = 'ANONYMOUS_'.sizeof($this->_blocks).Mage::app()->getFrontController()->getRequest()->getPathInfo();

We gave our best to fix the problem itself (which is a wrong generated cache key), but there is no event somewhere in the context of name generation and cache key generation. And because the key is generated in abstract class Mage_Core_Block_Abstract the only solution would be to copy the class to local/Mage which is no solution by definition.

So the rewrite of Mage_Core_Model_Layout is our best bet.

How anchor <a> work

I googled for a MageOverflow post how anchor work, so I don't have to write it down myself. But I couldn't find any.

If you only want to know, how a anchor (JS/CSS script link) needs to be built to work, scroll down.

So I'll do it myself, please send me links on Twitter or via mail if you find a better tutorial, so I can link it here.

Parts of a URI

As you can read on the parse_url doc every URI consists of the following parts:

  • scheme
  • host
  • port
  • user
  • pass
  • path
  • query
  • fragment

Some of them can be omit, but they exists, even if they are empty.

                    hierarchical part
        ┌───────────────────┴─────────────────────┐
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
  abc://username:password@example.com:123/path/data?key=value#fragid1
  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └───┬───┘ └──┬──┘
scheme  user information     host     port            query   fragment

  urn:example:mammal:monotreme:echidna
  └┬┘ └──────────────┬───────────────┘
scheme              path  

Thankfully borrowed from wikipedia

Schema

There are a lot of schemes, like file://, tcp://, tel://, mailto:.

Username and password

Usernames are used for example in ftp links. You can use them also in http links, to submit them to a .htaccess auth.

Hostname

The hostname is often a domain, like magento.com, but it can use an IP address too: 66.211.190.110.

Port

Many schemes define default ports:

  • http: 80
  • ftp: 21
  • ssh: 22

So you can omit the port.

Path

The parts before, including port are important to find the server (at least for http), so called authority. Starting with the path, we are on our way through the server to the right place.

We need to distinguish between directories and files, everything ending in a / is a directory, everything else is a file:

/directory/directory2/file
/file

Query

We can add additional informations, so the server knows, what we want

Fragment

And we can link into whatever comes back.

What is the problem with all this?

Default values or omitting parts

For the following I'm only talking about http because the problems which motivate me to write this are all http related.

  • scheme - the same as the page we are on (http(s))
  • host - the same we are on, e.g. magento.com
  • port - 80/443 (or the same we are on)
  • user - I think none, but I'm not sure
  • pass - I think none, but I'm not sure
  • path - "/"
  • query - none
  • fragment - none

The following examples assume we are on the page:

http://blog.fabian-blechschmidt.de/how-anchor-work

You can only omit from left to right. This means of you want to omit the port, you have to go without scheme and host too!

Omitting scheme

When we omit the scheme, we use the same, as the page we are on.

If we link to //google.com/ we will use http, because we are currently on an http served page. Please note the two // of the starting url.

But this means especially, we can include (third party) js and css files, based on the current scheme!

Don't use

http://code.jquery.com/jquery-2.1.4.min.js

but instead

//code.jquery.com/jquery-2.1.4.min.js

(or even better
https://code.jquery.com/jquery-2.1.4.min.js)

Omitting host

We can omit the host, it would look like this:

/another-cool-blogpost

Then we assume the scheme is http and the host is blog.fabian-blechschmidt.de. It is important to understand, that this url is absolute. It takes the scheme, the host and the path and builds it together:

http://blog.fabian-blechschmidt.de/another-cool-blogpost

So if we move the blog from blog.fabian-blechschmidt.de to lets say fabian-blechschmidt.de/blog all links will break, because

this is built:
http://fabian-blechschmidt.de/another-cool-blogpost
http://fabian-blechschmidt.de/blog/another-cool-blogpost
and here the blog post lives now

Omit the path

if you want to link relative you need to omit the first /, like so:

another-cool-blogpost

Here it is important to understand, that the relative link is relative to the current directory we are in. If our blog post urls are "directories", like this:

http://blog.fabian-blechschmidt.de/how-anchor-work/

We have a problem, because the created link appends the new path the the current one:

http://blog.fabian-blechschmidt.de/how-anchor-work/another-cool-blogpost

and this URL is most likely wrong and broken.

As far as I know is there no solution for this problem, except knowing your base url and built your link upon this base url.

We get the same problem with css and js files! If we make them absolute:

/css/styles.css

we can't easily move them into a subdirectory, if we make them relative:

js/script.js

they will break on "deeper" pages, like:

nerd-shirts/people/always-be-marius

Because the script doesn't live in

nerd-shirts/people/js/script.js

Omit the other parts

Port

You can't omit the port (I think). It would look like this:

:123/my-path/my-file    

But this is rendered like a relative path (omitting the path).

User and password

User and password is "removed" by the browser from the current url, so the browser doesn't "know" anymore these details. If you want to use them, do it explicit.

Querys

Querys as fragments are part of a single request, therefore they are not removed from the url as user and password, but still not used in any other url on the page.

Hope this helps. If I have forgotten anything, please let me know: @Fabian_ikono

Messages are not shown

When you are working with sessions, you might have the problem, that your message is not shown.

First add them

We have different sessions:

  • core
  • checkout
  • sales
  • customer
  • admin
  • a lot more.

For each of these sessions, we have four methods to add a message:

Mage::getSingleton('*/session')->addSuccess('Woho! It worked!');
Mage::getSingleton('*/session')->addNotice('Task was executed.');
Mage::getSingleton('*/session')->addWarning('Something went wrong.');
Mage::getSingleton('*/session')->addError('Something went horribly wrong!');

And now get them

Getting the message is easy, get the right session and call getMessages():

public function getMessages($clear=false)

Mage::getSingleton('core/session')->getMessages(true)

The parameter decides whether the messages are deleted or not.

The problem

The message block

<block type="core/messages" name="global_messages" as="global_messages"/>
<block type="core/messages" name="messages" as="messages"/>

loads only the messages of core/session:

app/code/core/Mage/Core/Block/Messages.php:79
public function _prepareLayout()
{
    $this->addMessages(Mage::getSingleton('core/session')->getMessages(true));
    parent::_prepareLayout();
}

There are a few places, where other session are loaded, e.g. \Mage_Cms_Helper_Page::_renderPage:

foreach (array('catalog/session', 'checkout/session', 'customer/session') as $storageType) {
    $storage = Mage::getSingleton($storageType);
    if ($storage) {
        $messageBlock->addStorageType($storageType);
        $messageBlock->addMessages($storage->getMessages(true));
    }
}

But unfortunately not on the product view.

The solution

In my last project we already had an observer which makes sure, that the customer is redirected after sending a contact form, therefore we used the same method to just rewrite the message to the "correct" session.

$customerSession = Mage::getSingleton('customer/session');
$coreSession = Mage::getSingleton('core/session');
foreach ($customerSession->getMessages()->getItems() as $message) {
    $coreSession->addMessage($message);
}
$customerSession->getMessages(true);

Better(?) alternative

It is possible to just add all the messages as done in \Mage_Cms_Helper_Page::_renderPage.

Make sure this is done AFTER $action->loadLayoutUpdates() / $this->loadLayoutUpdates()