Sending files with the Zend Framework

In an ideal world we would never need to send a file for download in PHP, and rather write the file out and let Apache handle it all. However, it’s not always that simple, and sometimes we need to use PHP, for example if we are creating files on the fly that will only be accessed once, or we want to secure downloads to authenticated sessions only. Jani Hartikainen recently posted an article on using the X-Sendfile header to send files rather than reading and outputting the file with PHP. I was also planning to write an article about it myself but Jani beat me to the punch, so instead I thought I would follow up his post with a handy little Zend Framework action helper I wrote to make sending files easy.

I have put my action helper on GitHub, so please take a look and feel free to use it in your own projects.

Noginn_Controller_Action_Helper_SendFile

The basic usage is incredibly simple and just requires the path to the file and mime-type.

$this->_helper->sendFile('/path/to/file.zip', 'application/zip');

This will send the correct headers and output the file using readfile. Alternatively if you would rather make us of the X-Sendfile response header then we can set the xsendfile option.

$this->_helper->sendFile('/path/to/file.zip', 'application/zip', array('xsendfile' => true));

For more information on the X-Sendfile response header, take a look at Jani’s blog post, the mod_xsendfile Apache module or the Lighttp documentation.

The options available to the sendFile method are:

  • filename - the filename the file will be sent as.
  • modified - when the file was last modified in unix timestamp format.
  • disposition - whether to send the file as an attachment or inline.
  • xsendfile - whether to send the file using the X-Sendfile header.
  • cache - these options will make up the Cache-Control response header.
    • public - marks authenticated responses as cacheable; normally, if HTTP authentication is required, responses are automatically un-cacheable (for shared caches).
    • no-cache - forces browsers to submit the request to the origin server for validation before releasing a cached copy, every time.
    • no-store - never cache under any circumstances.
    • must-revalidate - tells the browser that it must obey any freshness information you give it.
    • proxy-validate - similar to must-revalidate except it only applies to proxy cache.
    • max-age - specifies the amount of seconds the file will remain in cache, this is relative and will also set the Expires response header.
    • s-maxage - similar to max-age except it only applies to proxy cache.

As well as sending the file I also wanted to allow browser caching and therefore needed to validate If-Modified-Since request headers, using the example below we can make the file cacheable for 1 hour.

$this->_helper->sendFile('/path/to/image.jpg', 'image/jpeg', array(
    'cache' => array(
        'must-revalidate' => true,
        'max-age' => 3600,
    )
));

When the browser sends an If-Modified-Since request header, the helper will check if the file modified time is greater and if so continue to send the file, otherwise a 304 not modified header will be sent.

/**
 * Validate the cache using the If-Modified-Since request header
 *
 * @param int $modified When the file was last modified as a unix timestamp
 * @return bool
 */
public function notModifiedSince($modified)
{
    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $modified < = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        // Send a 304 Not Modified header
        $response = $this->getResponse();
        $response->setHttpResponseCode(304);
        $response->sendHeaders();
        return true;
    }

    return false;
}

In comparison if we never want the file to be cached we can utilise the no-store Cache-Control header.

$this->_helper->sendFile('/path/to/image.jpg', 'image/jpeg', array('cache' => array('no-store' => true)));

If you are creating files on the fly then the sendData method may be useful, it works the same as sendFile except it takes binary data rather than a file path.

$data = 'My file contents';
$this->_helper->sendFile->sendData($data, 'text/plain', 'filename.txt');

Just like the sendFile method, sendData also supports supports browser caching, just remember you need to provide a valid modified timestamp.

$data = 'My cached file';
$this->_helper->sendFile->sendData($data, 'text/plain', 'filename.txt', array(
    'modified' => 1235952735,
    'cache' => array(
        'must-revalidate' => true,
        'max-age' => 3600
    )
));

One small caveat of my implementation of sendData is that it requires the data to be passed directly into the method, however the output of the GD library functions cannot be assigned to a variable. This can be worked around by using ob_start and ob_get_contents, which is probably not the most elegant solution, but it works.

$image = imagecreatetruecolor(150, 30);
$backgroundColour = imagecolorallocate($image, 50, 255, 125);
imagefilledrectangle($image, 0, 0, 150, 30, $backgroundColour);
ob_start();
imagejpeg($image);
$data = ob_get_contents();
ob_end_clean();
$this->_helper->sendFile->sendData($data, 'image/jpeg', 'green.jpg');

I hope this can be of some use, and I’m open to all suggestions and improvements in my implementation. I’ve just started to use git and the helper is now hosted on GitHub, feel free to fork it and make modifications.

Continue reading this article or post a comment (4)

PHPUK 2009

On Friday I had the pleasure of attending the PHP UK conference in London. While I didn’t take any photos myself, I have found proof that I was there, since I was wearing a bright blue hoody it was easy to find myself in the phpuk2009 tagged photos on Flickr. Thanks to Sebastian Bergmann and Kirth.

3317887555_d377e1f12d

The day kicked off with a talk by Aral Balkan, which was a bit of an inspirational talk. There was lots of talk about how we as developers have it a lot easier now, and that we should take advantage of it. The one thing I took away from the talk is not to spend too much time worrying about best practices. This is often the case when using a framework, as you can get caught up in making sure you do things the “right” way rather than just getting them done and worrying about it later.

The next talk we saw was Sharding Architectures by David Soria Parra - unfortunately it was really busy and there were no seats left by the time we got there, but it was certainly worth standing for. Sharding is something I have little knowledge about, since I’ve never worked on a website with enough traffic to need to. If I ever need to, I now have a better understanding of what to do and not to do. David gave some great examples of how to use consistent hashing to shard data into tables, and also how sometimes you need to go against what we get taught and de-normalize data.

3317076350_0be978c149

Next up was Sebastian Bergmann who gave a talk on lambda functions, closures and traits. I haven’t given PHP 5.3 much of a look yet, but I am definitely now going to find the time to get it installed and start playing around with the new features. Some good examples were given and some interesting questions were asked, I like the idea of using an anonymous function rather than creating throw-away named functions to be used with usort, array_map etc.

Mihai Corlan’s talk on Flex and AIR development for PHP programmers wasn’t initially something I thought I would be greatly interested in since I’ve never been a huge fan of Flash. However, I really enjoyed the talk and have since downloaded the Flex SDK to start playing around with creating some simple Flex/AIR applications. What impressed me most was how Mihai was given 1 minute to finish his talk, and in that time he managed to turn his Flex web application into a fully functioning AIR desktop application, showing just how simple AIR development can be. Another bonus of the talk is that Adobe were giving away a book on Flex 3 development, I was unable to get a copy on the day but one should now be on it’s way to me after a quick e-mail to Mihai.

The last talk of the day, and probably the most inspiring was by Chris Shiflett entitled security-centred design. Rather than the expected security talk on XSS, CSRF, SQLi instead Chris explained how he was slightly bored with the technical aspect of security and more interested in the psychological aspect.

I thoroughly enjoyed the day and can’t wait for next year. Maybe I’ll even try and submit something to the call for papers.

Continue reading this article or post a comment (1)

Preventing CSRF properly

If you have an application built using the Zend Framework you are probably using the hash form element to prevent cross-site request forgery attacks. If you aren’t then I reccomend reading Chris Shiflett’s post on CSRF, then read up on the Zend_Form_Element_Hash component, and finally come back here and continue reading.

In the documentation the Zend_Form_Element_Hash component is described as follows:

This element provides protection from CSRF attacks on forms, ensuring the data is submitted by the user session that generated the form and not by a rogue script. Protection is achieved by adding a hash element to a form and verifying it when the form is submitted.

A simple usage example is given, adding the token to the form with a unique salt.

$form->addElement('hash', 'request_token', array('salt' => 'unique'));

This works perfectly in preventing CSRF attacks, however, could be improved to make some click-jacking attacks more difficult. Since the hash element is just another form field, posting to the form with an invalid token displays an error message and the rest of the form is populated with the posted data (unless of course the a field has been explicitly ignored). The problem with this is that an attacker now has a way of populating the form in preparation for a click-jacking attack, which was the reason why Twitter was so easy to exploit with click-jacking.

Both Ruby on Rails and Django handle invalid tokens in a better way by just displaying a simple “403 Forbidden” page. While this doesn’t prevent click-jacking (which is whole different issue), it does make it impossible for an attacker to get arbitrary values in form fields.

I have now started to do the same with my Zend Framework applications, here is a quick example:

$form = new Form();
$form->addElement('hash', 'request_token', array('salt' => 'and-pepper'));
if ($this->_request->isPost() && $form->isValid($this->_request->getPost())) {
    // Valid
} else if (count($form->getErrors('request_token')) > 0) {
    $this->_forward('csrf-forbidden', 'error');
    return;
}

If the form is not valid, and the request_token has some errors then I will forward to my error controller to display a simple error page.

Forbidden CSRF

As click-jacking becomes more common, I hope this helps someone improve the security of their application. In a future blog post I will be taking a look at methods of preventing click-jacking for both the user and website.

Continue reading this article or post a comment (6)

Expanding tinyurl links

I noticed a neat little feature on Twitter search the other day when a shortened URL is posted.

twitter-tinyurl-expand

Notice the "expand" link next to the tinyurl, when this is clicked an animation starts and the real URL is then displayed instead.

twitter-tinyurl-expand2

twitter-tinyurl-expand3

I hope Twitter implement this feature on the main website, and maybe some twitter clients will start doing it too.

Continue reading this article or post a comment (2)

Twitter click-jacking fun

A couple of weeks ago a french blogger pulled a click jacking prank on unsuspecting Twitter users that tricked them into tweeting the message “Don’t Click: http://tinyurl.com/amgzs6″. Since the message included a link to the page where the click-jacking exploit took place it was extremely viral after a few high profile users were stupid enough to click.

Shiflett and other security researchers looked at different methods of bypassing Twitters initial frame busting JavaScript and helped Twitter get to a point where it was no longer possible to display the site in an iframe, stumping click jacking attempts on the standard interface. However, after a little bit of investigation I noticed that the mobile version of Twitter had no click-jacking protection.

Take a look at a quick proof of concept that I unleashed on a few of my followers for a short while before making the iframe visible. If the mobile version of the site isn’t in the iframe then it means you have previously selected the standard view using the link at the bottom of the site. If you’d like to see it working, just select the mobile site or clear your cookies.

Twitter click-jack

The mobile site currently has no JavaScript on it at all, which is probably for good reason as most mobile phones don’t support it. So it begs the question, how should Twitter prevent this click-jacking exploit? Adding the same JavaScript used on the standard site would work, but could it break compatibility with older mobile devices? How about disabling the status field pre-filling on the mobile site? Or only displaying the mobile site for mobile user-agents?

Until it gets fixed, I suggest everyone uses Firefox and installs the NoScript extension.

Continue reading this article or post a comment (9)

Revisiting hostname routing with the Zend Framework

A few months ago I wrote about hostname routing and how to use sub-domains as an account key. Since then I have come across a few problems with my implementation so thought I would give a quick update.

The first problem was with the routing setup, which didn’t behave as I expected.


$pathRoute = new Zend_Controller_Router_Route(
    ':controller/:action/*',
    array(
        'controller' => 'index',
        'action' => 'index'
    )
);
$accountRoute = new Zend_Controller_Router_Route_Hostname(
    ':account.myapp.com',
    array(
        'module' => 'account',
    ),
    array(
        'account' => '([a-z0-9]+)',
    )
);
$router->addRoute('account', $accountRoute->chain($pathRoute));

With the above routes setup, matching http://account1.myapp.com/index/index we expect the account parameter to be account1, which it is, however matching http://account1.myapp.com/index/index/account/account2 the account parameter will now be account2. This is because the inner most route has a higher priority so the parameters matched will take precedence, reversing the chain will solve this problem but cause another as the route will be assembled incorrectly as /index/index/account/account1/http://account1.myapp.com.

One possible solution I thought of was to extend Zend_Controller_Router_Route_Hostname to make the outer most route have the higher priority when matching. However, after discussing my problem with Ben Scholzen (the author the hostname route component) I decided take his advice and not use the wildcard parameter.

While it may seem like a lot more work add lots of individual routes it does make sense to me now, and wildcard routes don’t. If had a route with the wildcard parameter, for example :controller/:action/*, then the marketing homepage would be accessible not only from http://www.myapp.com, but also http://www.myapp.com/index/index and even http://www.myapp.com/index/index/buy-viagra-buy-viagra/call-1800-VIAGRA-NOW. Personally I would want the latter two to return 404 so they wouldn’t get indexed by Google, which is exactly why I am banishing the wildcard parameter from my application’s routes.

Example implementation


// Host routes
$accountHostRoute = new Zend_Controller_Router_Route_Hostname(
    ':account.myapp.dev',
    array('module' => 'account'),
    array('account' => '([a-z0-9]+)')
);

$adminHostRoute = new Zend_Controller_Router_Route_Hostname(
    'admin.myapp.dev',
    array('module' => 'admin')
);

$marketingHostRoute = new Zend_Controller_Router_Route_Hostname(
    'www.myapp.dev',
    array('module' => 'marketing')
);

// Account module routes
$router->addRoute('account-homepage', $accountHostRoute->chain(
    new Zend_Controller_Router_Route_Static(
        '',
        array(
            'controller' => 'index',
            'action' => 'index'
        )
    )
));

// Account admin module routes
$router->addRoute('account-admin-dashboard', $accountHostRoute->chain(
    new Zend_Controller_Router_Route_Static(
        'admin',
        array(
            'module' => 'account-admin',
            'controller' => 'index',
            'action' => 'index'
        )
    )
));

// Admin module routes
$router->addRoute('admin-dashboard', $adminHostRoute->chain(
    new Zend_Controller_Router_Route_Static(
        '',
        array(
            'controller' => 'index',
            'action' => 'index'
        )
    )
));

// Marketing module routes
$router->addRoute('marketing-homepage', $marketingHostRoute->chain(
    new Zend_Controller_Router_Route_Static(
        '',
        array(
            'controller' => 'index',
            'action' => 'index'
        )
    )
));

In the comments of my previous post a few people were asking about how to setup hostname routing with Zend_Config, so here is the example above rewritten to be setup from an XML config file.


<?xml version="1.0" encoding="UTF-8"?>
<routes>
    <account type="Zend_Controller_Router_Route_Hostname">
        <route>:account.myapp.dev</route>
        <defaults module="account" />
        <chains>
            <homepage type="Zend_Controller_Router_Route_Static">
                <route></route>
                <defaults controller="index" action="index" />
            </homepage>
            <admin-dashboard type="Zend_Controller_Router_Route_Static">
                <route>admin</route>
                <defaults module="account-admin" controller="index" action="index" />
            </admin-dashboard>
        </chains>
    </account>
    <admin type="Zend_Controller_Router_Route_Hostname">
        <route>admin.myapp.dev</route>
        <defaults module="admin" />
        <chains>
            <dashboard type="Zend_Controller_Router_Route_Static">
                <route></route>
                <defaults controller="index" action="index" />
            </dashboard>
        </chains>
    </admin>
    <marketing type="Zend_Controller_Router_Route_Hostname">
        <route>www.myapp.dev</route>
        <defaults module="marketing" />
        <chains>
            <homepage type="Zend_Controller_Router_Route_Static">
                <route></route>
                <defaults controller="index" action="index" />
            </homepage>
        </chains>
    </marketing>
</routes>

Continue reading this article or post a comment (7)

Models and Zend_Paginator

I have been following the discussion of models recently, in particular the blog posts by Rob Allen and Matthew Weier O’Phinney. I’m not going to go into detail about my model implementation, for that I suggest you have a read of the aforementioned blogs. The basic idea is that when fetching data from a model the objects returned should be independent of the data provider, thus the consumer of the object doesn’t care if we are using a database or web-service.

I began to refactor some of my code to use the domain model pattern but came across a problem when trying to use Zend_Paginator. The DbSelect adapter returns Zend_Db_Row objects and obviously is no longer independent of the data provider.


class Model_UserGateway extends Noginn_Model_Gateway
{
    public function fetchUsers($criteria = array(), $sort = array(), $limit = null, $offset = null, $paginate = false)
    {
        $db = Zend_Registry::get('db');
        $select = $db->select()->from(array('user'));
        $this->_setCriteria($select, $criteria);
        $this->_setSort($select, $sort);
        $this->_setLimit($select, $limit, $offset);

        if ($paginate) {
            return new Zend_Paginator($select);
        }

        return new Noginn_Model_ResultSet($db->fetchAll($select), $this);
    }

    public function countUsers($criteria = array())
    {
        $db = Zend_Registry::get('db');
        $select = $db->select()->from(array('user'), array('count' => 'COUNT(*)'));

        return $db->fetchOne($select);
    }
}

To solve this problem I created a simple Zend_Paginator adapter that takes a gateway object, method name and some arguments. When getting the items for the current page the paginator will call the gateway method and a Noginn_Model_ResultSet object will be returned. Therefore this simple implementation will work for fetching data from a database, web-service or any other type of data provider.


class Noginn_Paginator_Adapter_Model
{
    protected $_gateway;

    protected $_method;

    protected $_options;

    protected $_count;

    public function __construct(Noginn_Model_Gateway $gateway, $method, $arguments, $count)
    {
        $this->_gateway = $gateway;
        $this->_method = $method;
        $this->_arguments = $arguments;
        $this->_count = $count;
    }

    public function count()
    {
        return $this->_count;
    }

    public function getItems($offset, $limit)
    {
        $arguments = $this->_arguments;
        array_push($arguments, $limit, $offset, false);

        return call_user_func_array(array($this->_gateway, $this->_method), $arguments);
    }
}

Here I have updated the fetchUsers method to use model paginator adapter.


class Model_UserGateway extends Noginn_Model_Gateway
{
    public function fetchUsers($criteria = array(), $sort = array(), $limit = null, $offset = null, $paginate = false)
    {
        if ($paginate) {
            $count = $this->countUsers($criteria);
            return new Zend_Paginator(
                new Noginn_Paginator_Adapter_Model($this, 'fetchUsers', array($criteria, $sort), $count)
            );
        }

        $db = Zend_Registry::get('db');
        $select = $db->select()->from(array('user'));
        $this->_setCriteria($select, $criteria);
        $this->_setSort($select, $sort);
        $this->_setLimit($select, $limit, $offset);

        return new Noginn_Model_ResultSet($db->fetchAll($select), $this);
    }

    /* ... */
}

Caching

Another benefit of using this method is that caching the result of the paginator is easy, just add caching to your model methods and the paginator will indirectly fetch from the cache too.


class Model_UserGateway extends Noginn_Model_Gateway
{
    public function fetchUsers($criteria = array(), $sort = array(), $limit = null, $offset = null, $paginate = false)
    {
        if ($paginate) {
            $count = $this->countUsers($criteria);
            return new Zend_Paginator(
                new Noginn_Paginator_Adapter_Model($this, 'fetchUsers', array($criteria, $sort), $count)
            );
        }

        $cache = Zend_Registry::get('cache');
        $cacheId = $this->_getCacheId(__METHOD__, array($criteria, $sort, $limit, $offset));
        if (!$cache->test($cacheId)) {
            $db = Zend_Registry::get('db');
            $select = $db->select()->from(array('user'));
            $this->_setCriteria($select, $criteria);
            $this->_setSort($select, $sort);
            $this->_setLimit($select, $limit, $offset);

            $resultSet = new Noginn_Model_ResultSet($db->fetchAll($select), $this);
            $cache->save($resultSet, $cacheId, array('user'));
        } else {
            $resultSet = $cache->load($cacheId);
            $resultSet->setGateway($this);
        }

        return $resultSet;
    }

    public function countUsers($criteria = array())
    {
        $cache = Zend_Registry::get('cache');
        $cacheId = $this->_getCacheId(__METHOD__, $criteria);
        if (!$cache->test($cacheId)) {
            $db = Zend_Registry::get('db');
            $select = $db->select()->from(array('user'), array('count' => 'COUNT(*)'));

            $count = $db->fetchOne($select);
            $cache->save($count, $cacheId, array('user'));
        } else {
            $count = $cache->load($cacheId);
        }

        return $count;
    }
}

Summary

Although most model implementations will vary, I hope that my paginator adapter example will be useful to someone. I am also interested to find out how others have tackled this problem, so all comments and suggestions are welcomed.

Continue reading this article or post a comment (2)

PHPNW08

It’s been over two weeks since I attended the PHPNW08 conference in Manchester and I thought it was about time I wrote about it. With it being my first conference it was all a little daunting not knowing anyone there, however I met some great people and thoroughly enjoyed it.

Two of my favourite talks of the day were MySQL EXPLAIN Explained by Adrian Hardy and Exploiting PHP with PHP by Arpad Ray. While I have some experience with EXPLAIN, Adrian described it well and gave some nice real world examples of using it to dramatically optimize queries. Arpad’s talk took a different angle to the usual security talks and showed how to use PHP for ‘exploiting’ or as I saw it, penetration testing. I took away some great tips and have already starting putting some into action.

There were a few t-shirts and elePHPants being thrown around in the morning but I never managed to get my hands on any. However, there was a PHP5 online test where the highest scorer won a prize. I somehow managed to get the highest score won the cost of my ticket, a conference t-shirt and a book! The book I won was Practices of an Agile Developer by Venkat Subramaniam and Andy Hunt, which looks like a great read.

I’m now looking forward to the PHP London conference in February 09.

Continue reading this article or post a comment (0)

PHPNW08 Excitement

I’ve just finished getting my things together ready for the PHPNW conference in Manchester this weekend. This will be my first conference attendance so I’m really looking forward to it, especially so as there is a nice community feel to it.

I have spent awhile deciding which talks to attend, this is my plan:

  • KISS (Keep It Simple, Stupid) - Derick Rethans
  • MySQL EXPLAIN Explained - Adrian Hardy
  • Regular Expression Basics - Ciarán Walsh
  • The Power of Refactoring - Stefan Koopmanschap
  • Index and Search, options for PHP programmers - Zoe Slattery (although a little torn as I’m also really interested in Johannes’ talk on PHP 5.3)
  • Exploiting PHP with PHP - Arpad Ray
  • Panel Discussion

I plan on live blogging my thoughts on each of the talks, so if you’re not attending then get subscribed to my RSS feed.

Aside from all the great talks, I’m looking forward to meeting lots of interesting people. So anyone reading this who is attending, please say hello if you see me. Here are a range of expressions so I can be recognised no matter how happy, crazy or confused I look.

  • Very happy
  • Happy

Continue reading this article or post a comment (0)

Zend_Paginator_Adapter_TableSelect

Jurriën Stutterheim has just announced on the Zend Framework mailing list that a new paginator adapter, Zend_Paginator_Adapter_TableSelect, has been committed. It allows Zend_Db_Table_Select’s to be used and the corresponding Zend_Db_Rowset returned. I had previously written a post with an example of a custom adapter to do a similar task but I suggest everyone takes a look at this implementation.

Currently the paginator factory method will always return a Zend_Paginator_Adapter_DbSelect even if a Zend_Db_Table_Select is passed so that backwards compatibility is not broken. To use the new adapter simply pass it into the paginator constructor:

$table = new Zend_Db_Table();
$select = $table->select();
$paginator = new Zend_Paginator(new Zend_Paginator_Adapter_TableSelect($select);

Note that this new adapter also requires the latest revision of Zend_Db_Table_Select too, as it uses the new getTable() method.

Have fun!

Continue reading this article or post a comment (0)

About the author

Tom Graham is a web developer living in Leicester, UK. This blog is his vent for all things web design and development. Read more about Tom.

Categories

Pages