Pixelastic

You can clip our wings but we will always remember what it was like to fly.

Posts from August 2010

One thing that may turn me to Ruby : LESS

I often hear how Ruby is better than PHP, that cakePHP is only a port of the Ruby on Rail framework and that the original is much more powerful.

Well, I've been tempted to try and learn Ruby for some quite time now. But I've been a little scared by the fact of having to learn a whole new language when I'm now getting pretty good with cake.

That and the shortage of hosts with a support for Ruby out there keep me away from it.

LESS, a Ruby only little gem

But one thing that may change my mind is LESS, the CSS as it should have been written since day one. It allows, in an extremely simple syntax, to define variables, functions, nested definitions, easier selectors.

Unfortunatly, the parser only exists as a Ruby gem, too bad for my php apps.

Why I don't use the javascript version

There is Javascript version of the parser, but I don't like the idea of having CSS rendering based on script execution. It means that if I'm browsing without scripts enabled, I won't have any styling.

Styling and Scripting are two very different things, one should work without the other and vice versa

So maybe, when the syntax will be parsed directly by browsers (one could dream...), or if a PHP parser is released I'll try it right away, but until them, I'll stick with my rusty CSS.

LESS in PHP ?

Well, some hours after posting this I just stumbled upon that. I guess I'll try LESS in php world after all !

The all-in-one method to get mimetypes with PHP

Getting the correct mimetype from a file in PHP is not an easy task. I used to find it through extension sniffing of the file combined with a known list of mimetypes.

Today I needed to find the correct mimetype to do some security checks on file uploaded by users. I couldn't rely on an extension-based approach because the filename could easily be faked by an uploader.

So I needed an other way. In fact, in PHP world there are at least 4 methods I'm aware of to get this information.

mime_content_type

The classic PHP function mime_content_type is supposed to return just that. Unfortunatly, it is deprecated. And not supported by Dreamhost, my host.

The FileInfo functions

We are now encouraged to use the Fileinfo functions instead of mime_content_type. Unfortunatly, they seems to be returning strange results. Alternatively, they are not supported by Dreamhost either (but it seems that you can ask them to install it on your server).

It is bundled into EasyPHP for Windows, but you need to enable it by uncommenting the line extension=php_fileinfo.dll in your php.ini

And you use it like this :

$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filepath);
finfo_close($finfo);

Also note that the mimetype may be returned in a text/plain; charset=us-ascii form. You may need to parse the result to get it in the format you need.

The getimagesize function

The getimagesize function can be called on any image file. It will return an array containing image informations like width, height and of course the mimetype.

Unfortunatly, it will cause a E_WARNING if called on a non-image file. You can't even catch that using a try/catch. You can suppress the error using @, tho.

Here's how I use it :

$imageData = @getimagesize($filepath);
if (!empty($imageData['mime'])) {
$mimeType = $imageData['mime'];
}

Calling the system file

The last method I'm aware of is simply calling the file command on a unix system through exec.

$mimeType = exec("/usr/bin/file -i -b $filepath");

Merging all that into one do-it-all method

If you're not sure what your system is capable of or if you plan on distributing your code, you'd better test for all alternatives. Here's the code I'm using :

/**
*    mimetype
*    Returns a file mimetype. Note that it is a true mimetype fetch, using php and OS methods. It will NOT
*    revert to a guessed mimetype based on the file extension if it can't find the type.
*    In that case, it will return false
**/
function mimetype($filepath) {
// Check only existing files
if (!file_exists($filepath) || !is_readable($filepath)) return false;

// Trying finfo
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filepath);
finfo_close($finfo);
// Mimetype can come in text/plain; charset=us-ascii form
if (strpos($mimeType, ';')) list($mimeType,) = explode(';', $mimeType);
return $mimeType;
}

// Trying mime_content_type
if (function_exists('mime_content_type')) {
return mime_content_type($filepath);
}

// Trying exec
if (function_exists('exec')) {
$mimeType = exec("/usr/bin/file -i -b $filepath");
if (!empty($mimeType)) return $mimeType;
}

// Trying to get mimetype from images
$imageData = @getimagesize($filepath);
if (!empty($imageData['mime'])) {
return $imageData['mime'];
}

return false;
}

Hope that helps !

Disabling PHP scripts in a directory

Say you want to disable PHP scripts in a whole directory. Like your upload/ directory, because you don't want your users to upload .php files with direct webroot access on your server...

Just drop a .htaccess file in it, and add the following rules

# We don't want php files from being parsed by the server in this directory, so we will return them as plain text
AddType text/plain .php .php3 .php4 .php5 .php6 .phtml

# Or, if the first rule does not work on your server, you may want to completely turn off PHP
#php_flag engine off

 

Calling a parent method with arguments in PHP

For testing purpose I just needed to overwrite an existing class to add some more logic to some methods before returning a call to its parent method.

I needed to do that in a test case to save in an inner variable the result so I can clean up my mess after each test ran.

The key is calling call_user_func_array with the right syntax. It seems that some version of PHP prior to 5.3 choke (like in segmentation fault) if an alternate syntax is given.

Here is what worked for me :

class TestDocument extends Document {
    function foo() {
        return $this->fooResult= call_user_func_array(array('parent', 'foo'), func_get_args());
    }
}

 

Testing file uploads in PHP

I just had to write unit tests for a file upload script I had to write. As it is not that easy to do, I'll share my findings with you.

My problem was on how I was going to simulate a file upload in a test case. Sure I could simulate a whole post request either using curl of simpleTest webtester. But that would only give me a full overview of the upload process, not its inner details.

There was a way to do that using PHPT, which seems to be the unit tests used by PHP itself. It is supposed to simulate any kind of query. Unfortunatly, setting that up seemed a little to complex for me.

So, how did I do ?

I finally found a way to do that by :

  1. Spoofing the $_FILES array and putting arbitrary test data inside
  2. Copying a test file to the tmp/ directory for testing purpose. Actually the directory does not matter (see 3.)
  3. Wrapping all calls to move_uploaded_file and is_uploaded_file to its own methods. Those two php methods won't work with dummy upload because they weren't really uploaded through POST

So, instead of calling move_uploaded_file, I called $this->moveUploadedFile(), and instead of calling is_uploaded_file(), I called $this->isUploadedFile()

And when times comes to test my class, I just extends the class, overwrite those two methods with new one that uses rename() and file_exists() instead.

What does that change ?

The fondamental difference between the former and the latter functions is that the former checks that the target really was uploaded through POST while the latter does not care.

It is extremely important that you use the correct upload method because it provide an additional security check. If you just blindly rename() files specified by your user, you'll ending up putting the database.php and config.php files in the webroot renamed as i_want_to_be_hacked.txt

The other good news is that by wrapping those methods around those functions, you can create mock objects and test all the various return scenarios.

Creating dirs with correct chmod in PHP

One trick I've been dragging with me on all this years of PHP programing is a little snippet to correctly create directories with the chmod I want.

By simply calling mkdir('my_dir', 0777) I used to often end up with directories that I can't write to nor delete, even if I was correctly setting the chmod.

The trick was to reset the mask (using umask(0)) before the mkdir() call and then reapplying the old mask after.

$tmpUmask = umask(0);
mkdir('my_dir', 0777);
umask($tmpUmask);

I must admit that I've never really understand why it was working better than simply calling mkdir() but hey, it's been years that I'm using that now and I never run into access rights issues since.

SWFUpload and cakePHP

One thing that always sent me an awful hours of debugging is the fact that the Flash player identify itself as a whole different browser that the one it is integrated into.

I've ran into this issue multiple times when dealing with SWFUpload and today was one more. As I always spent quite a lot of time debugging issues arising from this, I guess I should post about it here.

The Flash player uses a different userAgent that your browser

Most of the time, this is not a problem. But when dealing with restricted areas of a website built on top of cakePHP, it can become one.

The thing is that as the userAgent string used by the Flash player is not the same as the one used by your browser. So, when you make a request to your site, cake will see that as whole new request and you won't be able to access your current session variables.

As you can't overwrite the userAgent hash sent, you need to send the sessionId along with your flash request. That way, you'll be able to call a

$this->Session->id($sessionId);
$this->Session->start();

in your controller beforeFilter (or component initialize)

In cake 1.2, you'll also have to call $this->Session->destroy() before to delete the Flash session created.

The Flash player uses a different cookie pool that your browser

This used to be an issue in 1.2 but not longer is.

Cake stores in a cookie the name of the session you're supposed to use. Flash having its own cookie pool, it will save its own cookie (usually somewhere you don't even know) and will always check for the session specified inside.

This took me quite a long time to found out why the flash request where reading an other request that the one I was setting.

In 1.2, you needed to delete the cookie created by cake whenever you made a flash request to avoid conflicts

setcookie(Configure::read('Session.cookie'), '', time() - 42000, $this->Session->path);

cakePHP used to overwrite the userAgent string in session

In cake 1.2, when you manually change the current session, cake would update the inner userAgent hash saved to your current userAgent hash.

This meant that whenever you were moving from the Flash session to the correct session, you had to overwrite the userAgent hash to the correct one.

$this->Session->write('Config.userAgent', $userAgent);

This is no longer the case in 1.3, changing the current session does not alter it anymore.

Doesn't all that stuff alter security ?

Well, most of the answers I found online were among the lines of "Just disable the check of the userAgent", so I think my method is a little bit more secure.

Anyway, passing a sessionId as a parameter seems a little risky, even to me. I guess there should be a way of encrypting it to not pass it as clear text

UPDATE !

I had to fiddle with this script some more. It appears that, as we are changing the sessionId, we need to do that BEFORE we access the session. It means that any call to Session::read() or Session::check(), or almost any other Session method BEFORE setting the id will block this trick from working. So, make sure that this code is executed before anything else, put it in a component that will be loaded first.

It also appears that if you follow my advice, you'll only have to call id($sessionId), and none of the hocum pocum about destroy, write and userAgent hashes I talked about...

I just lost some hours finding this out. I add a call to Session in my I18n component that was rendering this whole fix useless. It was driving me crazy...