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

<?php
$title
= 'My test product';
$price = 100;
$sku = 'testSKU';
$descr = 'Product description is here';

$node = new StdClass();
$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:

<?php
$filename
= $node->model; // in my case, filename = product sku + extension
$exts = array('zip', 'pdf'); // the code below expects that there are files testSKU.pdf and testSKU.zip 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 _preprocess_node with some conditions (like if ($variables[‘node’]->type == ‘product’ && $variables[‘page’])

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

<?php
echo $files_table;
?>

Here is the code:

<?php
$node
= $variables['node']; // that looks like this because the code was used
// in preprocess function - feel free to replace by other node loading code
$variables['files_table'] = _uc_file_user_downloads_by_sku($GLOBALS['user'], $node->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 = '<span class="' . $ext . '">'. $ext .'</span>';
      }
     
$variables['files_table'] = '<div class="available-files">'.theme('item_list', array('items' => $files, 'title' => t('File formats available for download'))).'</div>';
     
    }


// 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 = '<span class="'. $ext .'">' . l($ext, 'download/'. $file->fid .'/'. $file->file_key, $onclick) . '</span>';
    }

   
// Expired.
   
elseif (time() > $file->expiration) {
     
$file_link = $ext;
    }

   
// Able to be downloaded.
   
else {
     
$file_link = '<span class="'. $ext .'">' . l($ext, 'download/'. $file->fid .'/'. $file->file_key, $onclick) . '</span>' .' ('. 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 .= '<div class="form-item"><p class="description">'.
   
t('Once your download is finished, you must refresh the page to download again. (Provided you have permission)') .  
   
'</p></div>';
    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.

Comments

nice, that's was something I was looking for, I still have to try it, than i'll let you know how it works.

11 January, 2012

Hey

Just wondering if I could grab this write up for a tutorial section on my Ubercart promo site. I'd give you full credit and link back to this site. I'm also looking for developers who are familiar with Ubercart for the Dev For Hire section on my site.

Regards,
end user

08 February, 2012

Hi! yes, you can :)

02 March, 2012

I could not resist commenting. Perfectly written!

15 June, 2014

Hi
I need to import product features into Ubercart via the node import module.

23 October, 2012

Helo Anton Sidashin

Of course this code would very useful for many people by creating a module.

Maybe it can be also integrated with feeds_import module.

11 February, 2013

This would be awesome as a module. The current Ubercart file-selling functionality is pretty atrocious - it makes things have to happen from within two separate interfaces, and is utterly useless except for a one-seller-one-site paradigm. I'm working on a site where the idea is that multiple people would be able to upload and sell their own e-books.

Even if you don't create a module (which there is seriously a need for!) the above code should be enough to let a developer hack together their own implementation.

Thanks so much! I thought I was going to have to go through the Ubercart code line-by-line, in the absence of friendly documentation.

06 June, 2013

Hello it's me, I am also visiting this website regularly, this
website is truly good and the people are in fact sharing good thoughts.

21 October, 2014

Post new comment

Private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

Note for potential spammers: all links in your comment will not be indexed by search engines.

Anton Sidashin

Anton Sidashin senior developer, Pixeljets co-founder

I'm a web developer specializing in PHP and Javascript, and Drupal, of course. I'm building Drupal projects since 2005, and I was working as full-time senior engineer in CS-Cart for a while, building revolutionary e-commerce software. In my free time, I enjoy playing soccer, building my body in gym, and playing guitar.

Drupal.org ID: restyler
Drupal association member