Archive

Author Archive

Javascript version compare function

May 4th, 2013

Here’s quick javascript function for comparing versions. It works with any type of version strings, but it does not check for suffixes like ‘RC’, ‘alpha’, ‘stable’, etc. This was written in less than 10 minutes, so let me know if you find any bugs. Suggestions are welcomed anytime!

function versionCompare(a, b) {
	var A = a.split('.'), B = b.split('.'), ret = 0, base, sig, x, y;
	for(var i = 0, n = Math.max(A.length, B.length); i < n; i++) {
		x = 'undefined' == typeof A[i] ? 0 : parseInt(A[i]);
		y = 'undefined' == typeof B[i] ? 0 : parseInt(B[i]);
		
		base = Math.pow(10, n - i - 1);
		sig = 0;
		
		if( x < y ) { sig = -1; }
		else if( x > y ) { sig = 1; }
		
		ret += sig * base;
	}

	return ret == 0 ? 0 : ret / Math.abs(ret);
};

Javascript

Convert all MySQL tables and fields to UTF8 charset & collation

October 22nd, 2012

At some point in your web dev life you might face the “Illegal mix of collations” mysql error. Nothing complicated, you just need all your tables to use the same charset and collation and this can be fixed really easy using phpmyadmin or even from the command line. However, when the database has 300+ tables, doing it manually is really not an option, so it needs to be done using some kind of a script. It can be done in perl, php, python or anything for that matter, but I particularly liked this one line command, using bash & awk (always loved awk):

mysql --user=username --password=your_pass --database=dbname -B -N -e "SHOW TABLES"  | awk '{print "ALTER TABLE", $1, "CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;"}' | mysql --user=username --password=your_pass --database=dbname &

Found it on commandlinefu.com and worked like a charm.

Make sure to first read this article about converting charsets in mysql. Executing the command above might break things, depending on the database architecture. It’s always a good idea to create a backup of the database first.

MySQL ,

Magento memory leaks and custom options

August 30th, 2012

Most software, if not all, has memory leaks. Some are really bad at collecting garbage, other manage it better, but not enough. Magento had its problems since the beginning, and a lot of them have been fixed in the latest releases (using 1.6 to test this). However, it still has a long way until all major memory problems are gone. A simple test, like the one below, will reveal what I mean (put it in your magento root and run it from the CLI):

require "app/Mage.php";
Mage::app();

for($k = 0; $k < 100; $k++) {
    Mage::getModel('catalog/product')->load(123);
    echo $k . "\t" . (memory_get_usage()/1024/1024) . "\n";
}

This will show the current iteration and the memory usage. Replace “123″ with a real product id – first, use a simple product, with no custom options. My result looks like this:

0 - 8.3790
...
99 - 8.3794

Looks good, very good actually.

Now try it with a product that has custom options. Not more than 5-6 options, but stuff a dozen values in one or two options. My test shows these numbers:

0 - 8.9398
...
99 - 17.5057

.

Along the way, you’ll see that the garbage collector kicks in and memory is freed, so max memory usage (for me) doesn’t exceed 20M. Cool, everything seems acceptable, and it seems that the old memory leaks have been fixed (read more over here, here and here ).

Now let’s try something different. Importing a few hundred products from a CSV. All products have custom options, usually the same, but with different values. The code could be summarized like this:

$options = $this->_getOptions($row);
$product = $this->initNewProduct(); //just inits the product with all the required data
$product->addData($specificFields)
        ->setAttributeSetId( $attributeSetId )
        ->setTypeId($typeId)
        ->setCategoryIds( array($someIds = 123) );
		
if( $options && count($options) ) {
    $product->setCanSaveCustomOptions(true);
    $product->setProductOptions($options);
}

$product->save();

To me, this looked fine and should have worked. I didn’t care it ran out of memory after 60-70 cycles, I could live with that. However, after approximately 20 hours of intense debugging, trying to figure out why the options aren’t showing up correctly on the frontend I decided to check the products in the admin. I had a huge surprise when I’ve noticed that for the second product, I had the first product’s options and the (correct) second product’s options. For the third, I had the first, second and third option sets. And so on, the last one was a complete mess. At some point Firefox couldn’t even load the interface anymore, there were simply too many options. Now that’s a memory leak for sure!

So, is there a fix? Seems like there was one, tested it for 3 products and it worked fine, no more leftover options. Initially, I was doing this:

$product->clearInstance();
unset($product);
				
gc_collect_cycles();

but it didn’t work, although gc_collect_cycles() helped keeping the memory growth under control.

So I added the following lines:

$product->getOptionInstance()->unsetOptions()->clearInstance();
unset($product, $options); //instead of just unset($product);

and voila – all custom options have been created correctly. Haven’t really analyzed why this did the trick and why it didn’t work without it. Either way, I’m happy the code works and I hope this will help someone else someday.

Magento , ,

The amazing world of Magento

July 5th, 2012

Magento has its positives and negatives, but this one is a must-share issue. While testing a payment gateway problem I discovered the following error (on Magento 1.7):

Item price:   33.15
Item qty:     1
Row subtotal: 33.15
---
Order Subtotal 33.15 - OK
Discount (fixed amount, applied before tax): 33.00 - OK
Tax (24%) 0.03 ( 0.24/1.24 * (33.15 - 33.00) ) - OK
Grand Total: 0.14 - NOT OK

Wow, so let’s see, subtotal 33.15 – discount 33.00 = grand total of 0.14. Not exactly correct, I would dare say.

Magento

Updated version of the Magento CSS & JS Minifier

May 31st, 2012

I’ve released the first version of the magento css&js minifier more than a year and half ago. Since then, it has been downloaded over 200 times – roughly 10 downloads per month. It’s a decent number, considering its utility and the fact that it hasn’t been promoted or included on Magento Connect. The first version was compatible with all Magento versions greater than 1.4. Starting with Magento 1.7, the extension failed to work on the admin pages, because of the -webkit-keyframes css declarations.

What’s new in version 1.0.1

  • Changed namespace from Oxygen to Mandagreen
  • Upgraded JsMin to version 1.1.2
  • Upgraded CssMin to version 3.0.1
  • Added backend options for converting HSL values, converting font-weight values, converting named colors and replacing variables

 

Download & Install

Download Minifier for Magento (31.03 kB), then unzip it and copy the app/ folder to your Magento root folder.

Logout from the admin if you’re already logged in, then log back in. Go to Cache Management, click on the “Flush cache storage” button, then go to Configuration > Developer and Enable javascript minifier and css minifier under CSS & JS Minifier. Also, make sure that “Merge JavaScript Files” and “Merge CSS Files” are enabled, under JavaScript Settings, and CSS Settings respectively. Go back to Cache Management and click the “Flush Javascript/CSS cache” button.

 

Upgrading from 0.1.0

If you’re already using the first version of the extension, you’ll need to remove the old files. Go to your magento root folder, then navigate to the etc/modules folder. Remove the file called Oxygen_Minifier.xml. Then, navigate to app/code/local and remove the Oxygen folder (or Oxygen/Minifier, if you have other modules with this namespace).

Next, issue the following mysql command, using the mysql console or PhpMyAdmin:

update core_config_data set path = replace(path, 'oxy_', 'mg') where path like '%oxy_minifier%';

You can now install the module as explained above.

 

Compatibility

This extension has been tested with all Magento CE versions 1.4 through 1.7.

 

Supporting the Module

If you enjoy using the Magento CSS & JS Minifier, help us improve it and make it even better. Since this project it entirely open source and free for everyone, we welcome any donations and consider them a sign of appreciation. Donate

We also appreciate your feedback, both positive and negative, as long as they are constructive.

If any of you know how to package an extension for Magento Connect, I would appreciate the help. I’ve given up after 2 hours or repeated failures.

Magento , , ,

When JOINs fail to work

May 6th, 2012

Any developer, junior or senior, should know and understand the basic rules of database normalization since they are the foundation of relational DBMS. Edgar Codd introduced the concept in 1970 and since then, most of the architectures have been following his rules. However, for high-traffic websites, sometime de-normalized is better than normalized, resulting in faster queries, less swap space and less storage space.

Although this post is not exactly about normalization, another common recommendation is to use a join rather than multiple single queries. In other words, rather than doing “select * from a where condition1″ then “select * from b where condition2″, if a and b are in a relationship, it’s better to usually run a query like “select * from a inner join b on a.id = b.aid where conditions”. A classic example is when using categories for a certain object, say movies. Let’s say each movie can have a category and you want to display a list of all movies and the category that movie is in. Some developers might write the following php code:

connect_to_database();
$rs = mysql_query('select * from movies');
while( $row = mysql_fetch_array($rs) ) {
  $rs2 = mysql_query('select * from categories where category_id = ' . $row['category_id'];
  $row2 = mysql_fetch_array($rs2);
  $category = $row2['category_name'];
  echo $row['name'] . "<br />Category: " . $category . "<br /><br />";
}

but there’s a simpler and (most of the time) better approach:

connect_to_database();
$rs = mysql_query('select * from movies m inner join categories c on c.category_id = m.category_id');
while( $row = mysql_fetch_array($rs) ) {
  echo $row['name'] . "<br />Category: " . $row['category_name'] . "<br /><br />";
}

The first approach generates 1 + N queries, where N is the number of movies. If N is a big number, things might take a while… The second approach only takes 1 query, leaving the hard part to the internal RDBMS engine. Well, sometimes, in certain scenarios, the first version is the right choice. It’s THE solution.

Let’s assume there are 4+1 tables – the main one, call it objects, one for tags, one for categories, another one called hits which will track the number of times a certain object has been seen by a user, and the last one for storing the n-to-n relationship between tags and objects, call it >code>object_tags. The ERD is rather simple:

Also, let's assume we want to show a list of objects, with all their tags, their current category and the number of hits it has. The results will be paginated, only showing 20 items per page. Simple, right? Anyone would tend to write something like this for the first page:

SELECT * FROM objects o 
INNER JOIN categories c ON c.category_id = o.category_id
INNER JOIN object_tags ot ON ot.object_id = o.object_id
INNER JOIN tags t ON t.tag_id = ot.tag_id
INNER JOIN hits h ON h.object_id = o.object_id
LIMIT 20

This was what I wrote too, more or less. However, after spending many hours trying to figure out why that page was loading in more than 100 seconds, whenever it was loading at all, I've learned that sometimes it's better to break the rules and not follow any recommendations. So I tried the following code, and the results were staggering - less than 1s loading time:

$rs = mysql_query('select * from objects limit 20');
$categories = $tags = array(); //used for locally caching categories and tags
while( $row = mysql_fetch_array($rs) ) {
  if( !isset($categories[$row['category_id']]) {
    $rsCat = mysql_query('SELECT category_name FROM categories WHERE category_id = ' . $row['category_id'] . ' LIMIT 1';
    $rowCat = mysql_fetch_array($rsCat);
    $categories[$row['category_id']] = $rowCat['category_name'];
    unset($rowCat, $rsCat);
  }

  $objectTags = $toLoad = array();
  $rsObjTags = mysql_query('SELECT tag_id FROM object_tags WHERE object_id = ' . $row['object_id']);
  while( $rowObjTags = mysql_fetch_array($rsObjTags) ) {
    if( isset($tags[$rowObjTags['tag_id']]) ) {
      $objectTags[$rowObjTags['tag_id']][] = $tags[$rowObjTags['tag_id']];
    }
    else {
      $toLoad[] = $rowObjTags['tag_id'];
    }
  }
  unset($rsObjTags , $rowObjTags);

  if( count($toLoad) ) {
    $rsTags = 'SELECT tag_id, tag FROM tags WHERE tag_id IN (' . implode(',', $toLoad) . ') LIMIT ' . count($toLoad);
    while( $rowTags  = mysql_fetch_array($rsTags) ) {
      $tags[$rowTags['tag_id']] = $rowTags['tag'];
      $objectTags[$rowTags['tag_id']][] = $rowTags['tag'];
    }
  }
  unset($toLoad, $rsTags, $rowTags);

  $rsHits = 'SELECT today_hits FROM hits WHERE object_id = ' . $row['object_id'] . ' LIMIT 1';
  $hits = mysql_fetch_array($rsHits);

  //ready to display the data
  echo $row['object_name'] . ' is in category ' . $categories[$row['category_id']] . '<br />';
  echo 'Tags: ' . implode(', ', $objectTags) . '<br/>';
  echo 'Hits today: ' . $hits['today_hits'] . '<hr />';
}

I hope there aren't too many mistakes in the code, I wrote everything in here without actually testing it.

Seems like a lot of work and totally not optimized for something quite straight-forward. However, let's assume once more that the tables above have the following number of records:
objects - 50,000
categories - 500
tags - 100,000
objects_tags - 250,000
hits - 20,000,000

In this case, 4 joins would result in a huge dataset, requiring a lot of memory, and eventually ending up on the disk. Also, the LIMIT doesn't help at all, since the dataset is truncated at the end, after scanning all the necessary rows. However, in the unoptimized version we do benefit from the LIMIT, by only getting a fixed set (20 rows) from 50k rows. The rest of the queries are acceptable because they are all primary key based, they are using "LIMIT" as well (3 out of 4), and because their number is rather small - somewhere between 45 and 80 queries.

Hope this helps someone some day. I'd like to know your thoughts, what would have you done? Any other quick fixes for this problem?

MySQL ,

Hardening OpenX

April 18th, 2012

There are times when your website or application might get hacked. The usual reasons include weak passwords, outdated software or poor code (especially on validating user input). The same apply to OpenX – your ad server can be hacked due to an outdated version of OpenX, weak passwords, insecure server settings or a combinations of the above. Most common hacks add an iframe or script to the banner page distributing malware (a virus, trojan horse, or similar). This can get your website (the one that uses your infected openx server) to be blocked on all major browsers and most of the search engines.

To decrease the chances of having your openx hijacked, it’s recommended to follow these quick and easy steps. You’ll notice that some of them apply to any web application, like using strong passwords (at least 6 chars, using both uppercase and lowercase letters, digits and special chars).

Things to do with your OpenX installation
1. Always update the application and plugins to the latest versions. This ensures your ad server has the latest patches and security fixes, but it also makes hacking a bit more difficult. You can check if you have the latest version by going to Configuration / Product Updates in the OpenX administration interface.

2. If your administrator username is admin or administrator (or anything similar), change it to something less common, like your first name or last name. This cannot be done from the openx admin interface, but it can be done directly in mysql (using phpmyadmin for instance) – the table you are looking for is [table_prefix]_users

3. If there are other users who manage the inventory, create separate accounts for them and make them advertisers, not admins. Keep the admin account safe and do not share it with anyone.

4. If you only server banners on your website(s), you can remove all users from Advertisers and Websites. As a general rule, when you create a new advertiser, do not also create a username. If you don’t offer OpenX access to advertisers, there’s really no point in adding new users.

5. Always use the logout link to log out, instead of just closing the tab or window. This prevents session hijacking or keeping an active session on a public computer.

6. Disable all plugins that you don’t use

Things to do on the server side
1. Setup the openx database with its own username and use a really strong password. Make sure the user is only allowed to connect from the server that runs openx (where your web server is located) – this is usually the same machine, so localhost will be just fine. The password should be really strong. You can use an online password generator tool, like this one: http://www.pctools.com/guides/password/

2. Add an extra layer of authentication, using http basic auth for example. Follow the instruction in the links below:

http://httpd.apache.org/docs/2.0/howto/auth.html

http://httpd.apache.org/docs/2.0/programs/htpasswd.html

3. Change the database structure of the banner’s table so it doesn’t allow append & prepend codes anymore. You can do this by changing the two fields in the banners table ([table_prefix]_banners) from varchar(255) to varchar(0) (through phpmyadmin or the cli):
alter table openx_banners change append append varchar(0);
alter table openx_banners change prepend prepend varchar(0);

3a. Change the database structure of the zone’s table so it doesn’t allow append & prepend codes anymore – thanks Stefan for suggesting this.
alter table openx_zones change append append varchar(0);
alter table openx_zones change prepend prepend varchar(0);

Make sure to replace “openx” with the table prefix you use.

4. Set the proper permissions to all files & folders – all files should not be writable by the server by default, except the var/, plugins/ and www/images/ folders.
Your configuration file, located in var/, should not be writable:
chmod 0444 [domain].conf.php

5. Remove all installation files

What if I am already infected?
It’s important to find out all infected files or database entries and then figure out how they got there. Do not remove anything befire understanding how you got infected in the first place. You can disable the openx server temporarily (by adding this line in an .htaccess file in the folder www/delivery: deny from all).
Once you know where the security breach is, fix it, harden the server, change passwords (just to be sure), clean up the infected data and remove the deny all line from the .htaccess file.

We can also help you cleaning, securing and upgrading your openx installation(s) – contact us.

Security ,

Installing Memcache Server for PHP on Windows

February 15th, 2012

Memcached is a free open source, high-performance, distributed memory object caching system. It is currently used by a lot of websites, including Flickr, Twitter, Youtube, Digg and WordPress.

I’ve been using memcached on a few production servers, but never thought it could come in handy on a Windows development machine – in fact I didn’t even thought it was available on Windows. So, a few days ago, while working on yet another Magento project, I ran a Google search for “memcache windows” and it turned out there are a few Win32 ports of the original version. Cool! It can be used on Windows, now all I need is to find the right PHP extension.

Here’s a step-by-step tutorial on how to get memcached running in PHP on a Windows box. There’s are a bunch of really good tutorials out there, but I think another could only be helpful.

1. Go to http://splinedancer.com/memcached-win32/ and download memcached. I’ve used the 09.03.2008 binaries, memcached 1.2.4.
You can also use v1.2.6 that you can download from http://code.jellycan.com/memcached/ – I’ve used this one to update from the previous 1.2.4 and I can confirm that it works.

2. Unzip the downloaded file to any folder (i.e. C:/memcached). I’ve saved in the same place where I have Apache, MySQL and PHP.

3a. If you’re on Windows Vista, navigate to your memcached folder, right click on memcached.exe and click Properties, then click the Compatibility tab. In here, check the “Run this program as an administrator” checkbox.

3. Install the service by running memcached.exe -d install from the command line. (you should be in the folder where you unzipped the file(s), ie: c:/memcached)

By default, memcached has 64M available for caching and runs on port 11211. If you need to change any of these you can do it by editing the Registry – open regedit then search for HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/memcached Server. Change the ImagePath entry from "C:\memcached\memcached.exe" -d runservice to "C:\memcached\memcached.exe" -d runservice -m 256 -p 11222". This gives you 256 megs of memory and change the port to 11222 (on a development machine you probably don’t need to change the port, but who knows). See the full list of memcached options – not sure which ones apply to the Windows version too.

4. Now, that the service is installed and configured, let’s start it: memcached.exe -d start
To test that memcached is running you can try telnet’ing – telnet localhost 11211 (use the port you configured the service to listen to). If you can connect on that port, everything is fine.

5. Memcached is installed, but we need to use in PHP, so we need an extension. Here’s a list of sites where you can download the dll from:
http://downloads.php.net/pierre/
http://www.pureformsolutions.com/pureform.wordpress.com/2008/06/17/php_memcache.dll
http://kromann.info/download.php?strFolder=php5_1-Release_TS&strIndex=PHP5_1
shikii.net/blog/downloads/php_memcache-cvs-20090703-5.3-VC6-x86.zip

I had to install a VC6 dll, so I’ve downloaded the last one. Unzip it and put it in your php extensions folder. If you don’t know where this folder is, try this command: php -i | find "extension_dir". Add the dll in there, then open php.ini (to find it, you can run php -i | find "Loaded Config") and at the end add something like this:

[PHP_MEMCACHE]
extension=php_memcache.dll

Now restart Apache.

6. To test that memcached can now be used in PHP, use the following snippet:

$memcache = new Memcache;
if( !$memcache->connect('localhost', 11211)) { //change 11211 with the port your memcached is configured to listen to
  die('Could not connect!');
}

echo '
';
print_r($memcache->getStats());

If you see an array with a bunch of data in it, you’ve done it. If not, something went wrong at some point. Make sure memcached is running, then make sure you’ve got the correct php settings and extension.

As I mentioned at the beginning of the post, installing memcached was something that just hit me while working on a Magento project – for those familiar with Magento, you know how slow it can be sometimes, especially when you need to constantly refresh the pages to check html/js/css changes. So, I thought memcached would help speed up the parts on which I wasn’t working on – everything except layouts and html block cache. And so it did, development seems MUCH easier now – but all about this in another post, soon to come.

Many thanks for helping me figure this out and (hopefully) speeding up development to:
http://splinedancer.com/memcached-win32/
http://pureform.wordpress.com/2008/01/10/installing-memcache-on-windows-for-php/
http://shikii.net/blog/installing-memcached-for-php-5-3-on-windows-7/

PHP , ,

Workaround for attribute store labels not being displayed in Magento

February 12th, 2012

Here’s a quick workaround for a rather strange issue – sometimes, calling $attribute->getStoreLabel() returns null, although everything looks fine in the admin and in the database. It might be something that is slipping my mind, or it might be a bug, not really sure. However, I had to deal with it and come up with a fix, so I’d thought I’d share.

$_aIds = array();
foreach( $prod->getAttributes() as $attribute ) {
	//need the attribute_id which is missing from the attribute object
	if( !isset($_aIds[$attribute->getAttributeCode()]) ) {
		$_aIds[$attribute->getAttributeCode()] = $attribute->getIdByCode('catalog_product', $attribute->getAttributeCode());
	}
			
	//if there's not store_label try to set it
	if( !$attribute->getStoreLabel() ) {
		$labels = $attribute->getResource()->getStoreLabelsByAttributeId( $_aIds[$attribute->getAttributeCode()] );
		if( array_key_exists(Mage::app()->getStore()->getStoreId(), $labels)) {
			$attribute->setStoreLabel( $labels[Mage::app()->getStore()->getStoreId()] );
		} 
		else {
			$attribute->setStoreLabel( $attribute->getFrontendLabel() );
		}
	}
}
// end workaround

Applies to Magento 1.5.x

Magento ,

Prototype 1.6, Event.stop and IE9 – Quick patch

November 30th, 2011

For those of you using Prototype 1.6 (or Magento prior 1.6), you might wonder why event.stop() or Event.stop(event) doesn’t work in Internet Explorer 9. Here’s a quick explanation:

IE 9 makes major changes to the event system. We had to rewrite the
event code in 1.7 to support it. You can either (a) upgrade to 1.7;
(b) force your site into compatibility mode [1].

So it’s been fixed in 1.7, but what if I can’t upgrade to 1.7 and don’t want to render my website in compatibility mode? Well, after a couple of hours trying to sort this out, here’s quick (and dirty) patch for Prototype 1.6.0.3. Download Prototype 1.6.0.3 Event.stop IE9 fix (448 bytes)

Also, for further reading, here are two external links that you might find useful for this issue.

Note/Disclaimer: This has only been tested on Prototype 1.6.0.3. Use it at your own risk.

Javascript , ,