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:

PHP

<?php
 
class CreateDownloadFileType extends eZWorkflowEventType
{
    const WORKFLOW_TYPE_STRING "createdownloadfile";
    public function __construct()
    {
        parent::__constructCreateDownloadFileType::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::fetchListarray'productcollection_id' => $order['result']->ProductCollectionID ) );
 
    $order_number $order['result']->OrderNr;
 
    $zip_file='/home/account/www/download/'.md5($order_number.$user_id).'.zip';
    $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);
                    }
                }
            }
        }
        $zip->close();
    }
 
        return eZWorkflowType::STATUS_ACCEPTED;
    }
}
eZWorkflowEventType::registerEventTypeCreateDownloadFileType::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.

Code

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

You may need an Alias and a RewriteRule:

Code

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:

Code

ExtFilterDefine delete mode=output
cmd="/home/account/webfilter.sh"
 
<Directory /home/account/www/download>
SetOutputFilter delete
</Directory>

Code

FILENAME="/home/account/www$DOCUMENT_URI";
cat -
if [ -e $FILENAME ]; then
        rm $FILENAME;
fi

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.

Code

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

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

PHP

<?php
// 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));
 
        @readfile($filename);
        unlink($filename);
        exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Requested Content Not Available</title>
</head>
<body>
<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>
</body>
</html>