eZ Publish Workflow - electronic media delivery

I’m helping on a site that sells music.

I wanted a way to bundle a person’s purchases into a single file that could be downloaded once. There are two types of electronic media, mp3_product and album. Each may have an image associated with it. An mp3_product is an .mp3 files, an album is a .zip file, containing .mp3s.

The link above is a great resource. Using it, I created a custom workflow:

class CreateDownloadFileType extends eZWorkflowEventType
    const WORKFLOW_TYPE_STRING = "createdownloadfile";
    public function __construct()
        parent::__construct( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'Create Download File');
    public function execute( $process, $event )
        $parameters = $process->attribute( 'parameter_list' );

	$order_id = $parameters['order_id'];
	$user_id = $parameters['user_id'];

	$order = eZShopFunctionCollection::fetchOrder ($order_id);

	$items = eZProductCollectionItem::fetchList( array( 'productcollection_id' => $order['result']->ProductCollectionID ) );

	$order_number = $order['result']->OrderNr;

	$zip = new ZipArchive();
	if ($zip->open($zip_file,ZIPARCHIVE::CREATE)!==true) {
		eZDebug::writeError('Order: '.$order_id.' - could not create zip file '.$zip_file);

	$storageDir = eZSys::storageDirectory();
	$attributes_to_include = array('file','image');

	foreach ($items as $item)
		$product = eZContentObject::fetch($item->ContentObjectID);

		if (in_array($product->ClassIdentifier,array('mp3_product','album')))
			$product_data = $product->dataMap();
			$name = preg_replace('/[^\w]/','_',trim($product_data['name']->DataText));

			foreach ($attributes_to_include as $v)
				if ($product_data[$v]!==null)
					$attribute_data = $product_data[$v]->Content();
					$file = $attribute_data->Filename;
					$mime_type = $attribute_data->MimeType;
				        list( $group, $type ) = explode( '/', $mime_type );
					$filePath = $storageDir . '/original/' . $group . '/' . $file;
					$pathInfo = pathinfo($filePath);
					if ($zip->addFile($filePath,$name.'.'.$pathInfo['extension'])!==true) {
						eZDebug::writeError('Order: '.$order_id.' - failed to add '.$filePath.' to '.$zip_file);

        return eZWorkflowType::STATUS_ACCEPTED;
eZWorkflowEventType::registerEventType( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'createdownloadfiletype' );

The zip file created is named with an md5 of the order number and user id, and placed in a download directory. It’s probably not the most secure approach, but I think it is okay for this application, and it can be changed later.

This triggers on shop/checkout/after, which means the person has paid.

The download link for the zip file is placed in both the order view and order email templates.

    {def $current_user=fetch( 'user', 'current_user' )}
    {if $current_user.is_logged_in}
    <p><a href="/download/{concat($order.order_nr,$current_user.contentobject_id)|md5()}.zip">{"Click to download"|i18n("design/base/shop")}</
    {undef $current_user}

You may need an Alias and a RewriteRule:

Alias download /home/account/www/download
RewriteRule ^download/.*\.zip - [L]

The final step was to create a mechanism to delete the files after they are downloaded. There are at least two ways to do this. The first approach is to use an Apache filter, with a bash script:

Apache configuration code:

ExtFilterDefine delete mode=output

<Directory /home/account/www/download>
SetOutputFilter delete

cat -
if [ -e $FILENAME ]; then
        rm $FILENAME;

The Apache filter code is courtesy of Lyrix, Inc..

If you can’t use a filter - which is likely if you are using shared hosting, you can also use a PHP script. This uses a RewriteRule to route the request through PHP.

Options -Indexes
<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteRule ^(.*\.zip)$ zip.php?path=$1 [L]

Finally, this is the PHP code that will deliver the file, then delete it.

// downloading a file and then deleting it
$filename = $_GET['path'];
if (is_file($filename))
        // fix for IE catching or PHP bug issue
        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Cache-Control: public");
        header("Content-Description: File Transfer");
        // browser must download file from server instead of cache

        // force download dialog
        header("Content-Type: application/zip");

        // use the Content-Disposition header to supply a recommended filename and
        // force the browser to display the save dialog.
        header("Content-Disposition: attachment; filename=".basename($filename).";");

        The Content-transfer-encoding header should be binary, since the file will be read
        directly from the disk and the raw bytes passed to the downloading computer.
        The Content-length header is useful to set for downloads. The browser will be able to
        show a progress meter as a file downloads. The content-lenght can be determines by
        filesize function returns the size of a file.
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: ".filesize($filename));

<!DOCTYPE html>
<html lang="en">
<title>Requested Content Not Available</title>
<h1>Requested Content Not Available</h1>
<p>The content you requested is not available.</p>
<p>If you were unable to download your purchases, please contact us for assistance.</p>