Using Ubercart to sell files: ui improvements and creating file feature programmatically

uc_file module that allows selling files in Ubercart is definitely not perfect in terms of API, but it is a nice piece of functionality.
We've just finished creating one e-book store which has product-import feature.
Each product has multiple formats (pdf, epub, rtf) - some products can have pdf+epub, some can have just pdf, others have all three formats attached.

On product page, we wanted to show nice block like this:

Ubercart file downloads

where pdf and doc are just text for regular users, and download links for users that already bought this book.

All the code was created for Drupal 7 + Ubercart 7.x-3.x

Creating file feature programmatically

Let's start from

Creating ubercart product node

type = 'product'; node\_object\_prepare($node); $node->language = LANGUAGE_NONE; $node->title = $title; // Set Ubercart variables $node->model = $sku; // the SKU is a required field, so I generated a SKU based on the node title $node->list_price = $price; $node->cost = $price; $node->sell_price = $price; // populate some required ubercart fields foreach (array('width', 'height', 'length', 'weight', 'weight_units', 'length_units', 'shippable') as $attr) { $node->{$attr} = 0; } // get rid of "The quantity cannot be zero." errors foreach (array('pkg_qty', 'default_qty') as $attr) { $node->{$attr} = 1; } $node->body[LANGUAGE_NONE][0]['value'] = $descr; // set body field node_save($node); ?>

Attaching file feature

Now we have $node object ready for adding files.
Make sure that uc_file module is enabled, and you've configured directory for files, and put files there (by uploading to ftp/sftp)

Here is the code:

model; // in my case, filename = product sku + extension $exts = array('zip', 'pdf'); // the code below expects that there are files testSKU.pdf and in my ubercart files directory uc\_file\_refresh(); // copy files from file system to ubercart files table in database foreach ($exts as $ext) { $ext = trim($ext); $fullname = $filename . '.' . $ext; $file = uc_file_get_by_name($fullname); if (!empty($file)) { $existing_file = db_query("SELECT fpid FROM {uc_file_products} WHERE fid=:fid AND model=:sku", array(':fid' => $file->fid, ':sku' => $product->model))->fetchField(); if (!$existing_file) { $feature = array( 'nid' => $product->nid, 'fid' => 'file', 'description' => $product->title, ); drupal_write_record('uc_product_features', $feature); $file_product = array( 'fid' => $file->fid, 'pfid' => $feature['pfid'], 'filename' => $file->filename, 'model' => $product->model, 'shippable' => $product->shippable, ); drupal_write_record('uc_file_products', $file_product); } } } ?>

That's it!

Now put these two pieces of code together to your module and you should get product with attached files.

Showing available formats and downloadable list of files on product node

It's surprising that Ubercart doesn't have such functionality in core - the only interface it has for user is a separate page with complete list of all downloads available for him on the website.

In my project, I just put the code below to preprocessnode with some conditions (like if ($variables['node']->type 'product' && $variables['page'])

and then in node--product.tpl.php I write

Here is the code:

model); // user doesn't have available downloads, get plain text list of extensions if (!$variables['files_table']) { // get files list for product $files = db_query("SELECT f.filename FROM {uc_file_products} p INNER JOIN {uc_files} f ON p.fid=f.fid WHERE p.model = :sku", array(':sku' => $node->model))->fetchCol(); foreach ($files as &$file) { $ext = pathinfo($file, PATHINFO_EXTENSION); $file = ''. $ext .''; } $variables['files_table'] = '
'.theme('item_list', array('items' => $files, 'title' => t('File formats available for download'))).'
'; } // Function inspired by code in uc_file.module function _uc_file_user_downloads_by_sku($account, $sku) { $files = db_query( "SELECT u.granted, f.filename, u.accessed, u.addresses, p.description, u.file_key, f.fid, u.download_limit, u.address_limit, u.expiration FROM {uc_file_users} as u ". "LEFT JOIN {uc_files} as f ON u.fid = f.fid ". "LEFT JOIN {uc_file_products} as p ON p.pfid = u.pfid WHERE uid = :uid AND p.model=:sku", array(':uid' => $account->uid, ':sku' => $sku) )->fetchAllAssoc('fid'); $rows = array(); foreach ($files as $file) { $row = count($rows); $download_limit = $file->download_limit; // Set the JS behavior when this link gets clicked. $onclick = array( 'attributes' => array( 'onclick' => 'uc_file_update_download('. $row .', '. $file->accessed .', '. ((empty($download_limit)) ? -1 : $download_limit) .');', 'id' => 'link-'. $row ), ); $ext = pathinfo($file->filename, PATHINFO_EXTENSION); // Expiration set to 'never' if ($file->expiration FALSE) { $file_link = '' . l($ext, 'download/'. $file->fid .'/'. $file->file_key, $onclick) . ''; } // Expired. elseif (time() > $file->expiration) { $file_link = $ext; } // Able to be downloaded. else { $file_link = '' . l($ext, 'download/'. $file->fid .'/'. $file->file_key, $onclick) . '' .' ('. t('expires on @date', array('@date' => format_date($file->expiration, 'custom', variable_get('uc_date_format_default', 'm/d/Y')))) .')'; } $rows[] = $file_link; } if (!empty($rows)) { $output = theme('item_list', array('title' => t('Files available for download'), 'items' => $rows)); $output .= '

'. t('Once your download is finished, you must refresh the page to download again. (Provided you have permission)') . '

'; return $output; } else { return FALSE; } } ?>

Forgive me that the code is a bit ugly, it can (and needs to!) be refactored and improved.

May be someone is interested in such functionality as module? Tell me by leaving a comment, I will be glad to create a module if it will be useful for someone.

Anton Sidashin

I am a web developer and CTO with 15+ years of experience, and I am passionate about performance, usability, and getting things done.

Samara, Russia