You are here

uc_file.module in Ubercart 5

Allows products to be associated with downloadable files.

uc_file allows ubercart products to have associated downloadable files. Optionally, after a customer purchases such a product they will be sent a download link via email. Additionally, after logging on a customer can download files via their account page. Optionally, an admininstrator can set restrictions on how and when files are downloaded.

Development sponsored by the Ubercart project. http://www.ubercart.org

File

uc_file/uc_file.module
View source
<?php

/**
 * @file
 * Allows products to be associated with downloadable files.
 *
 * uc_file allows ubercart products to have associated downloadable files.
 * Optionally, after a customer purchases such a product they will be sent a
 * download link via email. Additionally, after logging on a customer can
 * download files via their account page. Optionally, an admininstrator can set
 * restrictions on how and when files are downloaded.
 *
 * Development sponsored by the Ubercart project.  http://www.ubercart.org
 */
define('UC_FILE_PAGER_SIZE', 50);
define('UC_FILE_REQUEST_LIMIT', 50);
define('UC_FILE_BYTE_SIZE', 1024);

/* *****************************************************************************
 *  Hook Functions (Drupal)                                                    *
 * ****************************************************************************/

/**
 * Implementation of hook_form_alter().
 */
function uc_file_form_alter($form_id, &$form) {
  if ($form_id == "uc_product_feature_settings_form") {
    $form['#submit']['uc_file_feature_settings_submit'] = array();
    $form['#validate']['uc_file_feature_settings_validate'] = array();
  }
}

/**
 * Implementation of hook_menu().
 */
function uc_file_menu($may_cache) {
  global $user;
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => '_autocomplete_file',
      'callback' => '_autocomplete_filename',
      'access' => user_access('administer product features'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/products/files',
      'callback' => 'uc_file_files_admin',
      'title' => t('View file downloads'),
      'description' => t('View all file download features on products.'),
      'access' => user_access('administer products'),
      'type' => MENU_NORMAL_ITEM,
    );
  }
  else {
    if (module_exists('uc_notify')) {
      $items[] = array(
        'path' => 'admin/store/settings/notify/edit/file',
        'title' => t('File download'),
        'access' => user_access('administer store'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_file_notify_settings',
        ),
        'description' => t('Edit the notification settings for purchased file downloads.'),
        'type' => MENU_LOCAL_TASK,
      );
    }
    $items[] = array(
      'path' => 'user/' . arg(1) . '/files',
      'title' => t('Files'),
      'description' => t('View your purchased files.'),
      'callback' => 'uc_file_user_downloads',
      'callback arguments' => array(
        arg(1),
      ),
      'access' => (user_access('view all downloads') || $user->uid == arg(1)) && $user->uid,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'download/' . arg(1) . '/' . arg(2),
      'callback' => '_file_download',
      'callback arguments' => array(
        arg(1),
        arg(2),
      ),
      'access' => user_access('download file'),
      'type' => MENU_CALLBACK,
    );
    drupal_add_css(drupal_get_path('module', 'uc_file') . '/uc_file.css');
  }
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function uc_file_perm() {
  return array(
    'download file',
    'view all downloads',
  );
}

/**
 * Implementation of hook_user().
 */
function uc_file_user($op, &$edit, &$account, $category = NULL) {
  global $user;
  switch ($op) {
    case 'delete':
      _user_table_action('remove', NULL, $account->uid);
      break;
    case 'form':
      if (user_access('administer users') && (is_null($category) || $category == 'account')) {
        $file_downloads = db_query("SELECT * FROM {uc_file_users} as u INNER JOIN {uc_files} as f ON u.fid = f.fid WHERE uid = %d", $account->uid);
        $files = db_query("SELECT * FROM {uc_files} ORDER BY filename ASC");
        $available_downloads = array();
        $available_files = array();
        while ($file_download = db_fetch_object($file_downloads)) {
          $available_downloads[$file_download->file_key] = $file_download->filename;
        }
        while ($file = db_fetch_object($files)) {
          if (substr($file->filename, -1) != '/' && substr($file->filename, -1) != '\\') {
            $available_files[$file->fid] = $file->filename;
          }
        }
        $form['file'] = array(
          '#type' => 'fieldset',
          '#title' => t('File downloads'),
          '#collapsible' => TRUE,
          '#collapsed' => TRUE,
          '#weight' => 10,
        );
        $form['file']['remove_file'] = array(
          '#type' => 'select',
          '#title' => t('Remove file'),
          '#multiple' => TRUE,
          '#description' => t('Select a file to remove as a download. Hold Ctrl to select or unselect multiple files.'),
          '#options' => $available_downloads,
        );
        $form['file']['add_file'] = array(
          '#type' => 'select',
          '#title' => t('Add file'),
          '#multiple' => TRUE,
          '#description' => t('Select a file to add as a download. Hold Ctrl to select or unselect multiple files.'),
          '#options' => $available_files,
        );
      }
      return $form;
      break;
    case 'submit':
      if (!empty($edit['remove_file'])) {
        foreach ($edit['remove_file'] as $hash_key) {
          if (!is_null($hash_key)) {
            _user_table_action('remove', NULL, $account->uid, $hash_key);
          }
        }
      }
      if (!empty($edit['add_file'])) {
        foreach ($edit['add_file'] as $fid) {
          $pfid = db_result(db_query("SELECT pfid FROM {uc_file_products} WHERE fid = %d", $fid));
          _user_table_action('allow', $fid, $account->uid, $pfid);
        }
      }
      break;
    case 'view':
      $existing_download = db_result(db_query("SELECT fid FROM {uc_file_users} WHERE uid = %d", $account->uid));
      if ((user_access('view all downloads') || $user->uid == $account->uid) && $user->uid && $existing_download) {
        $items['uc_file_download'] = array(
          'value' => l(t('Click here to view your file downloads.'), 'user/' . $account->uid . '/files'),
          'class' => 'member',
        );
        return array(
          t('File downloads') => $items,
        );
      }
      break;
    default:
      break;
  }
}

/* **************************************************************************** *
 *  Übercart Hooks                                                              *
 * **************************************************************************** */

/**
 * Implementation of hook_cart_item().
 */
function uc_file_cart_item($op, &$item) {
  switch ($op) {
    case 'can_ship':
      $files = db_query("SELECT shippable, model FROM {uc_file_products} as fp INNER JOIN {uc_product_features} as pf ON pf.pfid = fp.pfid WHERE nid = %d", $item->nid);
      while ($file = db_fetch_object($files)) {
        $sku = empty($item->data['model']) ? $item->model : $item->data['model'];
        if ($sku == $file->model || empty($file->model)) {
          return $file->shippable ? TRUE : FALSE;
        }
      }
      break;
  }
}

/**
 * Implementation of hook_order().
 */
function uc_file_order($op, $order, $status) {
  global $user;
  switch ($op) {
    case 'update':

      // Only process file downloads when the order is being updated to the
      // correct status, the status is actually being changed, and a valid user
      // has been assigned to the order.
      if ($status == variable_get('uc_file_default_order_status', 'completed') && $order->order_status != $status && $order->uid > 0 && ($order_user = user_load(array(
        'uid' => $order->uid,
      ))) !== FALSE) {
        foreach ($order->products as $product) {
          $files = db_query("SELECT fp.fid, fp.pfid, model, f.filename FROM {uc_file_products} AS fp INNER JOIN {uc_product_features} AS pf ON pf.pfid = fp.pfid INNER JOIN {uc_files} as f ON f.fid = fp.fid WHERE nid = %d", $product->nid);
          while ($file = db_fetch_object($files)) {
            if ($file->model == $product->model || empty($file->model)) {
              $downloads = _user_table_action('allow', $file->fid, $order_user->uid, $file->pfid);
              $user_downloads = !empty($user_downloads) ? array_merge($user_downloads, $downloads) : $downloads;
              if (_get_dir_file_ids($file->fid)) {
                $comment = t('User can now download files in the directory %dir.', array(
                  '%dir' => $file->filename,
                ));
              }
              else {
                $comment = t('User can now download the file %file.', array(
                  '%file' => basename($file->filename),
                ));
              }
              uc_order_comment_save($order->order_id, $user->uid, $comment);
            }
          }
        }
        if (!is_null($user_downloads)) {
          _email_file_download($order_user, $order, $user_downloads);
        }
      }
      break;
    default:
      break;
  }
}

/**
 * Implementation of hook_product_feature().
 */
function uc_file_product_feature() {
  $features[] = array(
    'id' => 'file',
    'title' => t('File download'),
    'callback' => 'uc_file_feature_form',
    'delete' => 'uc_file_feature_delete',
    'settings' => 'uc_file_feature_settings',
  );
  return $features;
}

/**
 * Implementation of hook_store_status().
 */
function uc_file_store_status() {
  $message = array();
  if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
    $message[] = array(
      'status' => 'warning',
      'title' => t('File Downloads'),
      'desc' => t('The file downloads directory is not valid or set. Set a valid directory in the <a href="!url">product feature settings</a> under the file download settings fieldset.', array(
        '!url' => url('admin/store/settings/products/edit/features'),
      )),
    );
  }
  else {
    $message[] = array(
      'status' => 'ok',
      'title' => t('File Downloads'),
      'desc' => t('The file downloads directory has been set and is working.'),
    );
  }
  return $message;
}

/**
 * Implementation of hook_token_list().
 */
function uc_file_token_list($type = 'all') {
  if ($type == 'uc_file' || $type == 'ubercart' || $type == 'all') {
    $tokens['uc_file']['file-downloads'] = t('The list of file download links (if any) associated with an order');
  }
  return $tokens;
}

/**
 * Implementation of hook_token_values().
 */
function uc_file_token_values($type, $object = NULL) {
  switch ($type) {
    case 'uc_file':
      if (!empty($object)) {
        $values['file-downloads'] = theme('uc_file_downloads_token', $object);
      }
      break;
  }
  return $values;
}

/**
 * Implementation of hook_uc_message().
 */
function uc_file_uc_message() {
  $messages['uc_file_download_subject'] = t("File Downloads for Order [order-id]");
  $messages['uc_file_download_message'] = t("Your order [order-link] at [store-name] included file download(s). You may access them with the following link(s):\n\n[file-downloads]\n\nAfter downloading these files these links will have expired. If you need to download the files again, you can login at [site-login] and visit the \"My Account\" section of the site.\n\nThanks again, \n\n[store-name]\n[site-slogan]");
  return $messages;
}

/* **************************************************************************** *
 *  Callback Functions, Forms, and Tables                                       *
 * **************************************************************************** */

/**
 * Theme file download links token
 */
function theme_uc_file_downloads_token($file_downloads) {
  $output = '';
  foreach ($file_downloads as $file_download) {
    $filename = basename(db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $file_download->fid)));
    $download_url = url('download/' . $file_download->fid . '/' . $file_download->file_key, NULL, NULL, TRUE);
    $output .= '<a href="' . $download_url . '">' . $download_url . "</a>\n";
  }
  return $output;
}

/**
 * product_feature delete function
 */
function uc_file_feature_delete($feature) {
  db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $feature['pfid']);
}

/**
 * Form builder for hook_product_feature
 */
function uc_file_feature_form($node, $feature) {
  if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
    drupal_set_message(t('A file directory needs to be configured in <a href="!url">product feature settings</a> before a file can be selected.', array(
      '!url' => url('admin/store/settings/products/edit/features'),
    )), 'error');
  }
  _file_table_action('insert');
  $models = !_get_adjustment_models($node->nid) ? array(
    NULL => t('Any'),
    $node->model => $node->model,
  ) : array(
    NULL => t('Any'),
    $node->model => $node->model,
  ) + _get_adjustment_models($node->nid);
  if (!empty($feature)) {
    $file_product = db_fetch_object(db_query("SELECT * FROM {uc_file_products} as p LEFT JOIN {uc_files} as f ON p.fid = f.fid WHERE pfid = %d", $feature['pfid']));
    $default_feature = $feature['pfid'];
    $default_model = $file_product->model;
    $default_filename = $file_product->filename;
    $default_description = $file_product->description;
    $default_shippable = $file_product->shippable;
  }
  else {
    $default_shippable = $node->shippable;
  }
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  $form['pfid'] = array(
    '#type' => 'value',
    '#value' => $default_feature,
  );
  $form['uc_file_model'] = array(
    '#type' => 'select',
    '#title' => t('Model/SKU'),
    '#default_value' => $default_model,
    '#description' => t('This is the model/SKU that will need to be purchased to obtain the file download.'),
    '#options' => $models,
  );
  $form['uc_file_filename'] = array(
    '#type' => 'textfield',
    '#title' => t('File download'),
    '#default_value' => $default_filename,
    '#autocomplete_path' => '_autocomplete_file',
    '#description' => t('The file that can be downloaded when product is purchased (enter a path relative to the %dir directory).', array(
      '%dir' => variable_get('uc_file_base_dir', NULL),
    )),
    '#maxlength' => 255,
  );
  $form['uc_file_description'] = array(
    '#type' => 'textfield',
    '#title' => t('Description'),
    '#default_value' => $default_description,
    '#maxlength' => 255,
    '#description' => t('A description of the download associated with the product.'),
  );
  $form['uc_file_shippable'] = array(
    '#type' => 'checkbox',
    '#title' => t('Shippable product'),
    '#default_value' => $default_shippable,
    '#description' => t('Check if this product model/SKU file download is also associated with a shippable product.'),
  );
  return uc_product_feature_form($form);
}
function uc_file_feature_form_validate($form_id, $form_values) {
  if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $form_values['uc_file_filename']))) {
    form_set_error('uc_file_filename', t('%file is not a valid file or directory inside file download directory.', array(
      '%file' => $form_values['uc_file_filename'],
    )));
  }
}
function uc_file_feature_form_submit($form_id, $form_values) {
  global $user;
  $fid = db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $form_values['uc_file_filename']));

  //Build product feature descriptions
  $description = empty($form_values['uc_file_model']) ? t('<strong>SKU:</strong> Any<br />') : t('<strong>SKU:</strong> !sku<br />', array(
    '!sku' => $form_values['uc_file_model'],
  ));
  if (is_dir(variable_get('uc_file_base_dir', NULL) . "/" . $form_values['uc_file_filename'])) {
    $description .= t('<strong>Directory:</strong> !dir<br />', array(
      '!dir' => $form_values['uc_file_filename'],
    ));
  }
  else {
    $description .= t('<strong>File:</strong> !file<br />', array(
      '!file' => basename($form_values['uc_file_filename']),
    ));
  }
  $shippable = $form_values['uc_file_shippable'] ? 1 : 0;
  $description .= $shippable ? t('<strong>Shippable:</strong> Yes') : t('<strong>Shippable:</strong> No');

  //Insert or update uc_file_product table
  if (empty($form_values['pfid'])) {
    $pfid = db_next_id('{uc_product_features}_pfid');
  }
  else {
    $pfid = $form_values['pfid'];
    db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $pfid);
  }
  switch ($GLOBALS['db_type']) {
    case 'mysqli':
    case 'mysql':
      db_query("INSERT INTO {uc_file_products} (pfid, fid, model, description, shippable) VALUES (%d, %d, '%s', '%s', %d)", $pfid, $fid, $form_values['uc_file_model'], $form_values['uc_file_description'], $shippable);
      break;
    case 'pgsql':
      db_query("INSERT INTO {uc_file_products} (pfid, fid, model, description, shippable) VALUES (%d, %d, '%s', '%s', '%d')", $pfid, $fid, $form_values['uc_file_model'], $form_values['uc_file_description'], $shippable);
      break;
  }
  $data = array(
    'pfid' => $pfid,
    'nid' => $form_values['nid'],
    'fid' => 'file',
    'description' => $description,
  );
  return uc_product_feature_save($data);
}

/**
 * Form builder for file settings
 */
function uc_file_feature_settings() {
  uc_add_js('$(document).ready(function() { if ($("#edit-uc-file-download-limit-duration-granularity").val() == "never") {$("#edit-uc-file-download-limit-duration-qty").attr("disabled", "disabled").val("");} });', 'inline');
  $statuses = array();
  foreach (uc_order_status_list('general') as $status) {
    $statuses[$status['id']] = $status['title'];
  }
  $form['uc_file_default_order_status'] = array(
    '#type' => 'select',
    '#title' => t('Order status'),
    '#default_value' => variable_get('uc_file_default_order_status', 'completed'),
    '#description' => t('Where in the order status the user will be given the file download. Be aware that if payments are processed automatically, this happens before anonymous customers have an account created. This order status should not be reached before the user account exists.'),
    '#options' => $statuses,
  );
  $form['uc_file_base_dir'] = array(
    '#type' => 'textfield',
    '#title' => t('Files path'),
    '#description' => t('The absolute path (or relative to Drupal root) where files used for file downloads are located. For security reasons, it is reccommended to choose a path outside the web root.'),
    '#default_value' => variable_get('uc_file_base_dir', NULL),
  );
  $form['uc_file_download_limit'] = array(
    '#type' => 'fieldset',
    '#title' => t('Download limits'),
    '#description' => t('Leave any of these fields empty or unchanged to not enforce a limit with them.'),
  );
  $form['uc_file_download_limit']['uc_file_download_limit_number'] = array(
    '#type' => 'textfield',
    '#title' => t('Downloads'),
    '#description' => t('The number of times a file can be downloaded.'),
    '#default_value' => variable_get('uc_file_download_limit_number', NULL),
    '#maxlength' => 4,
    '#size' => 4,
  );
  $form['uc_file_download_limit']['uc_file_download_limit_addresses'] = array(
    '#type' => 'textfield',
    '#title' => t('IP addresses'),
    '#description' => t('The number of unique IP addresses from which a user can download a file.'),
    '#default_value' => variable_get('uc_file_download_limit_addresses', NULL),
    '#maxlength' => 4,
    '#size' => 4,
  );
  $form['uc_file_download_limit']['uc_file_download_limit_duration_qty'] = array(
    '#type' => 'textfield',
    '#title' => t('Time'),
    '#default_value' => variable_get('uc_file_download_limit_duration_granularity', 'never') == 'never' ? NULL : variable_get('uc_file_download_limit_duration_qty', NULL),
    '#size' => 4,
    '#maxlength' => 4,
    '#prefix' => '<div class="duration">',
    '#suffix' => '</div>',
  );
  $form['uc_file_download_limit']['uc_file_download_limit_duration_granularity'] = array(
    '#type' => 'select',
    '#options' => array(
      'never' => t('never'),
      'day' => t('day(s)'),
      'week' => t('week(s)'),
      'month' => t('month(s)'),
      'year' => t('year(s)'),
    ),
    '#default_value' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
    '#attributes' => array(
      'onchange' => 'if (this.value == "never") {$("#edit-uc-file-download-limit-duration-qty").attr("disabled", "disabled").val("");} else {$("#edit-uc-file-download-limit-duration-qty").removeAttr("disabled");}',
    ),
    '#description' => t('How long after a product has been purchased until its file download expires.'),
    '#prefix' => '<div class="duration">',
    '#suffix' => '</div>',
  );
  $form['uc_file_advanced'] = array(
    '#type' => 'fieldset',
    '#title' => t('Advanced server settings'),
    '#description' => t('The defaults should cover most use cases.  Do not change these unless you know what you are doing.'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['uc_file_advanced']['uc_file_file_mask'] = array(
    '#type' => 'textfield',
    '#title' => t('Files mask'),
    '#description' => t('The regular expression used for masking files in files directory.'),
    '#default_value' => variable_get('uc_file_file_mask', ".*"),
  );
  $form['uc_file_advanced']['uc_file_reverse_proxy_addresses'] = array(
    '#type' => 'textarea',
    '#rows' => 3,
    '#title' => t('Allowed reverse proxy addresses'),
    '#description' => t('Add allowed reverse proxy addresses for the file download system to check for (one per line), otherwise @var will be used as the origin address.', array(
      '@var' => $_SERVER['REMOTE_ADDR'],
    )),
    '#default_value' => implode("\n", variable_get('uc_file_reverse_proxy_addresses', array())),
  );
  return $form;
}
function uc_file_feature_settings_validate($form_id, $form_values) {
  if (!empty($form_values['uc_file_base_dir']) && $form_values['op'] == t('Save configuration') && !is_dir($form_values['uc_file_base_dir'])) {
    form_set_error('uc_file_base_dir', t('%dir is not a valid file or directory', array(
      '%dir' => $form_values['uc_file_base_dir'],
    )));
  }
}
function uc_file_feature_settings_submit($form_id, $form_values) {
  $action = empty($form_values['uc_file_base_dir']) ? 'empty' : 'insert';
  _file_table_action($action);
  _file_table_action('refresh');
  $proxies = variable_get('uc_file_reverse_proxy_addresses', '');
  variable_set('uc_file_reverse_proxy_addresses', explode("\n", $proxies));
}

/**
 * Page builder for file products admin
 */
function uc_file_files_admin() {
  _file_table_action('insert');
  return drupal_get_form('uc_file_files_form');
}

/**
 * Implementation of theme_form($form)
 */
function theme_uc_file_files_form($form) {
  $output = '';

  //Only display files on 1st form step
  if ($form['step']['#value'] == 1) {
    $files = array();
    $args = array(
      'form' => $form,
    );
    $header = tablesort_sql(tapir_get_header('uc_file_files_table', array()));
    $order = empty($header) ? "ORDER BY f.filename ASC" : $header . ", f.filename ASC";
    $count_query = "SELECT COUNT(*) FROM {uc_files}";
    $query = pager_query("SELECT n.nid, f.filename, n.title, fp.model, f.fid, pf.pfid FROM {uc_files} as f LEFT JOIN {uc_file_products} as fp ON (f.fid = fp.fid) LEFT JOIN {uc_product_features} as pf ON (fp.pfid = pf.pfid) LEFT JOIN {node} as n ON (pf.nid = n.nid) " . $order, UC_FILE_PAGER_SIZE, 0, $count_query);
    while ($file = db_fetch_object($query)) {
      $files[] = $file;
    }
    $args['files'] = $files;
    $output .= '<p>' . t('File downloads can be attached to any Ubercart product as a product feature. For security reasons the <a href="!download_url">file downloads directory</a> is separated from the Drupal <a href="!file_url">file system</a>. Here are the list of files (and their associated Ubercart products) that can be used for file downloads.', array(
      '!download_url' => url('admin/store/settings/products/edit/features'),
      '!file_url' => url('admin/settings/file-system'),
    )) . '</p>';
    $output .= drupal_render($form['uc_file_action']);
    $output .= tapir_get_table('uc_file_files_table', $args);
    $output .= theme('pager', NULL, UC_FILE_PAGER_SIZE, 0);
  }

  //Checkboxes already rendered in uc_file_files_table
  foreach ($form as $form_element => $form_data) {
    if (strpos($form_element, 'file_select_') !== FALSE) {
      unset($form[$form_element]);
    }
  }
  $output .= drupal_render($form);
  return $output;
}

/**
 * Form builder for file products admin
 */
function uc_file_files_form($form_values = NULL) {
  $form['step'] = array(
    '#type' => 'hidden',
    '#value' => !isset($form_values) ? 1 : $form_values['step'] + 1,
  );
  switch ($form['step']['#value']) {

    //Display File Options and File checkboxes
    case 1:
      $files = db_query("SELECT * FROM {uc_files}");
      $file_actions = array(
        'uc_file_delete' => t('Delete file(s)'),
        'uc_file_upload' => t('Upload file'),
      );

      //Check any if any hook_file_action('info', $args) are implemented
      foreach (module_implements('file_action') as $module) {
        $name = $module . '_file_action';
        $result = $name('info', NULL);
        if (is_array($result)) {
          foreach ($result as $key => $action) {
            if ($key != 'uc_file_delete' && $key != 'uc_file_upload') {
              $file_actions[$key] = $action;
            }
          }
        }
      }
      while ($file = db_fetch_object($files)) {
        $form['file_select_' . $file->fid] = array(
          '#type' => 'checkbox',
        );
      }
      $form['uc_file_action'] = array(
        '#type' => 'fieldset',
        '#title' => t('File options'),
        '#collapsible' => FALSE,
        '#collapsed' => FALSE,
      );
      $form['uc_file_action']['action'] = array(
        '#type' => 'select',
        '#title' => t('Action'),
        '#options' => $file_actions,
        '#prefix' => '<div class="duration">',
        '#suffix' => '</div>',
      );
      $form['uc_file_action']['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Perform action'),
        '#prefix' => '<div class="duration">',
        '#suffix' => '</div>',
      );
      break;
    case 2:

      //Perform File Action (Upload, Delete, hooked in actions)
      $file_ids = array();
      foreach ($form_values as $name => $form_value) {
        if (strpos($name, 'file_select_') !== FALSE) {
          $file_ids[] = intval(str_replace('file_select_', '', $name));
        }
      }
      $form['file_ids'] = array(
        '#type' => 'value',
        '#value' => $file_ids,
      );
      $form['action'] = array(
        '#type' => 'value',
        '#value' => $form_values['action'],
      );

      //Switch to an action to perform
      switch ($form_values['action']) {
        case 'uc_file_delete':

          //Delete selected files
          foreach ($file_ids as $file_id) {
            $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $file_id));
            $filename = substr($filename, -1) == "/" ? $filename . ' (' . t('directory') . ')' : $filename;
            $file_list[] = $filename;
          }
          $form['files'] = array(
            '#type' => 'markup',
            '#value' => theme_item_list($file_list, NULL, 'ul', array(
              'class' => 'file-name',
            )),
          );
          $form['recurse_directories'] = array(
            '#type' => 'checkbox',
            '#title' => t('Delete selected directories and their sub directories'),
          );
          $form = confirm_form($form, t('Delete the following file(s)?'), 'admin/store/products/files', t('Deleting a file will remove all its associated file downloads and product features. Removing a directory will remove any files it contains and their associated file downloads and product features.'), t('Yes'), t('No'));
          break;
        case 'uc_file_upload':

          //Upload file
          drupal_set_title(t('Upload File'));
          $max_bytes = trim(ini_get('post_max_size'));
          $directories = array(
            '' => '/',
          );
          switch (strtolower($max_bytes[strlen($max_bytes) - 1])) {
            case 'g':
              $max_bytes *= 1024;
            case 'm':
              $max_bytes *= 1024;
            case 'k':
              $max_bytes *= 1024;
          }
          $files = db_query("SELECT * FROM {uc_files}");
          while ($file = db_fetch_object($files)) {
            if (is_dir(variable_get('uc_file_base_dir', NULL) . "/" . $file->filename)) {
              $directories[$file->filename] = $file->filename;
            }
          }
          $form['#attributes']['enctype'] = 'multipart/form-data';
          $form['upload_dir'] = array(
            '#type' => 'select',
            '#title' => t('Directory'),
            '#description' => t('The directory to upload the file to. The default directory is the root of the file downloads directory.'),
            '#options' => $directories,
          );
          $form['upload'] = array(
            '#type' => 'file',
            '#title' => t('File'),
            '#description' => t('The maximum file size that can be uploaded is %size bytes. You will need to use a different method to upload the file to the directory (e.g. FTP, SSH) if your file exceeds this size.', array(
              '%size' => number_format($max_bytes),
            )),
          );
          $form['submit'] = array(
            '#type' => 'submit',
            '#value' => t('Upload'),
          );
          break;
        default:

          //Check any if any hook_file_action('form', $args) are implemented
          foreach (module_implements('file_action') as $module) {
            $name = $module . '_file_action';
            $result = $name('form', array(
              'action' => $form_values['action'],
              'file_ids' => $file_ids,
            ));
            $form = is_array($result) ? array_merge($form, $result) : $form;
          }
          break;
      }
      break;
    default:
      break;
  }
  $form['#multistep'] = TRUE;
  $form['#redirect'] = FALSE;
  return $form;
}
function uc_file_files_form_validate($form_id, $form_values) {
  switch ($form_values['step']) {
    case 2:
      switch ($form_values['action']) {
        case 'uc_file_delete':

          //Nothing to validate for file delete
          break;
        case 'uc_file_upload':

          //Check any if any hook_file_action('validate', $args) are implemented
          if ($temp_file = file_check_upload()) {
            foreach (module_implements('file_action') as $module) {
              $name = $module . '_file_action';
              $result = $name('upload_validate', array(
                'file_object' => $temp_file,
                'form_id' => $form_id,
                'form_values' => $form_values,
              ));
            }
          }
          else {
            form_set_error('', t('An error occurred while uploading the file'));
          }
          break;
        default:

          //Check any if any hook_file_action('validate', $args) are implemented
          foreach (module_implements('file_action') as $module) {
            $name = $module . '_file_action';
            $result = $name('validate', array(
              'form_id' => $form_id,
              'form_values' => $form_values,
            ));
          }
          break;
      }
      break;
    default:
      break;
  }
}
function uc_file_files_form_submit($form_id, $form_values) {
  switch ($form_values['step']) {
    case 2:
      switch ($form_values['action']) {
        case 'uc_file_delete':
          foreach ($form_values['file_ids'] as $file_id) {
            _file_table_action('remove', $file_id, $form_values['recurse_directories'], TRUE);
          }
          drupal_set_message(t('The select file(s) have been deleted.'));
          break;
        case 'uc_file_upload':
          $dir = variable_get('uc_file_base_dir', NULL) . '/';
          $dir = is_null($form_values['upload_dir']) ? $dir : $dir . $form_values['upload_dir'];
          if (is_dir($dir)) {
            if ($file_object = file_save_upload('upload', FALSE)) {
              $temp_file = $file_object->filepath;
              copy($file_object->filepath, $dir . basename($file_object->filepath));
              $file_object->filepath = $dir . basename($file_object->filepath);
              unlink($temp_file);

              //Check any if any hook_file_action('upload', $args) are implemented
              foreach (module_implements('file_action') as $module) {
                $name = $module . '_file_action';
                $result = $name('upload', array(
                  'file_object' => $file_object,
                  'form_id' => $form_id,
                  'form_values' => $form_values,
                ));
              }
              _file_table_action('insert');
              drupal_set_message(t('The %file has been uploaded to %dir', array(
                '%file' => basename($file_object->filepath),
                '%dir' => $dir,
              )));
            }
            else {
              drupal_set_message(t('An error occurred while copying the file to %dir', array(
                '%dir' => $dir,
              )));
            }
          }
          else {
            drupal_set_message(t('Can not move file to %dir', array(
              '%dir' => $dir,
            )));
          }
          break;
        default:

          //Check any if any hook_file_action('validate', $args) are implemented
          foreach (module_implements('file_action') as $module) {
            $name = $module . '_file_action';
            $result = $name('submit', array(
              'form_id' => $form_id,
              'form_values' => $form_values,
            ));
          }
          break;
      }
      drupal_goto('admin/store/products/files');
      break;
    default:
      break;
  }
}

/**
 * Form builder for file download notification settings.
 */
function uc_file_notify_settings() {
  $form['uc_file_download_notification'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send email to customer with file download link(s).'),
    '#default_value' => variable_get('uc_file_download_notification', FALSE),
  );
  $form['uc_file_download_notification_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Message subject'),
    '#default_value' => variable_get('uc_file_download_notification_subject', uc_get_message('uc_file_download_subject')),
  );
  $form['uc_file_download_notification_message'] = array(
    '#type' => 'textarea',
    '#title' => t('Message text'),
    '#default_value' => variable_get('uc_file_download_notification_message', uc_get_message('uc_file_download_message')),
    '#description' => t('The message the user receives after purchasing products with file downloads (<a href="!token_url">uses order, uc_file, and global tokens</a>)', array(
      '!token_url' => url('admin/store/help/tokens'),
    )),
    '#rows' => 10,
  );
  $form['uc_file_download_notification_format'] = filter_form(variable_get('uc_file_download_notification_format', FILTER_FORMAT_DEFAULT), NULL, array(
    'uc_file_download_notification_format',
  ));
  return system_settings_form($form);
}

/**
 * Table builder for file products admin
 */
function uc_file_files_table($op, $args = array()) {
  switch ($op) {
    case 'fields':
      $fields = array();
      $fields[] = array(
        'name' => 'select',
        'title' => t(''),
        'weight' => 0,
        'enabled' => TRUE,
      );
      $fields[] = array(
        'name' => 'filename',
        'title' => t('File'),
        'weight' => 1,
        'enabled' => TRUE,
        'attributes' => array(
          'field' => 'f.filename',
        ),
      );
      $fields[] = array(
        'name' => 'product',
        'title' => t('Product'),
        'weight' => 2,
        'enabled' => TRUE,
        'attributes' => array(
          'field' => 'n.title',
        ),
      );
      $fields[] = array(
        'name' => 'model',
        'title' => t('Model/SKU'),
        'weight' => 3,
        'enabled' => TRUE,
        'attributes' => array(
          'field' => 'fp.model',
        ),
      );
      return $fields;
    case 'data':
      $data = array();
      $files = _group_filenames($args['files']);
      foreach ($files as $file) {
        $data['select'][] = drupal_render($args['form']['file_select_' . $file->fid]);
        $filename = is_dir(variable_get('uc_file_base_dir', NULL) . '/' . $file->filename) ? '<strong>' . $file->filename . '</strong>' : $file->filename;
        $data['filename'][] = $filename == $last_filename ? '' : $filename;
        if ($filename == $last_filename && !empty($data['#attributes'])) {
          $data['#attributes'][count($data['#attributes']) - 1] = array(
            'class' => 'group',
          );
        }
        $last_filename = empty($last_filename) || $filename != $last_filename ? $filename : $last_filename;
        $data['product'][] = !empty($file->title) ? l($file->title, 'node/' . $file->nid) : '';
        $data['model'][] = !empty($file->model) ? $file->model : '';
        $data['#attributes'][] = array();
      }
      return $data;
    case 'attributes':
      return array(
        'class' => 'file-table',
      );
  }
}

/**
 * Table builder for user downloads
 */
function uc_file_user_downloads($uid) {
  drupal_set_title(t('File downloads'));
  uc_add_js(drupal_get_path('module', 'uc_file') . '/uc_file.js');
  $header = array(
    array(
      'data' => t('Purchased'),
      'field' => 'u.granted',
      'sort' => 'desc',
    ),
    array(
      'data' => t('Filename'),
      'field' => 'f.filename',
    ),
    array(
      'data' => t('Size'),
    ),
    array(
      'data' => t('Description'),
      'field' => 'p.description',
    ),
    array(
      'data' => t('Downloads'),
      'field' => 'u.accessed',
    ),
  );
  $sql = "SELECT granted, filename, accessed, description, `file_key`, f.fid 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 = %d";
  $count_query = "SELECT COUNT(*) FROM {uc_file_users} WHERE uid = %d";
  $download_limit = variable_get('uc_file_download_limit_number', NULL);
  $file_ids = array();
  $rows = array();
  $files = pager_query($sql . tablesort_sql($header), UC_FILE_PAGER_SIZE, 0, $count_query, $uid);
  while ($file = db_fetch_object($files)) {
    $row = count($rows);
    $file_path = variable_get('uc_file_base_dir', NULL) . '/' . $file->filename;
    $bytesize = format_size(filesize($file_path));
    $expiration = _file_expiration_date($file->granted);
    $onclick = array(
      'onclick' => 'uc_file_update_download(' . $row . ', ' . $file->accessed . ', ' . (empty($download_limit) ? -1 : $download_limit) . ');',
      'id' => 'link-' . $row,
    );
    if (!$expiration) {
      $file_link = l(basename($file->filename), 'download/' . $file->fid . '/' . $file->file_key, $onclick);
    }
    else {
      if (time() > $expiration) {
        $file_link = basename($file->filename);
      }
      else {
        $file_link = l(basename($file->filename), 'download/' . $file->fid . '/' . $file->file_key, $onclick) . ' (' . t('expires on @date', array(
          '@date' => format_date($expiration, 'custom', variable_get('uc_date_format_default', 'm/d/Y')),
        )) . ')';
      }
    }
    $rows[] = array(
      array(
        'data' => format_date($file->granted, 'custom', variable_get('uc_date_format_default', 'm/d/Y')),
        'class' => 'date-row',
        'id' => 'date-' . $row,
      ),
      array(
        'data' => $file_link,
        'class' => 'filename-row',
        'id' => 'filename-' . $row,
      ),
      array(
        'data' => $bytesize,
        'class' => 'filename-row',
        'id' => 'filesize-' . $row,
      ),
      array(
        'data' => $file->description,
        'class' => 'description-row',
        'id' => 'description-' . $row,
      ),
      array(
        'data' => $file->accessed,
        'class' => 'download-row',
        'id' => 'download-' . $row,
      ),
    );
  }
  if (empty($rows)) {
    $rows[] = array(
      array(
        'data' => t('No downloads found'),
        'colspan' => 4,
      ),
    );
  }
  $output = theme('table', $header, $rows) . theme('pager', NULL, UC_FILE_PAGER_SIZE, 0);
  return $output;
}

/* **************************************************************************** *
 *  Module and Helper Functions                                                 *
 * **************************************************************************** */

/**
 * Implement Drupal autocomplete textfield
 *
 * @return:
 *   Sends string containing javascript array of matched files
 */
function _autocomplete_filename() {

  // Catch "/" characters that drupal autocomplete doesn't escape
  $url = explode('_autocomplete_file/', request_uri());
  $string = $url[1];
  $matches = array();
  $files = db_query("SELECT filename FROM {uc_files} WHERE filename LIKE LOWER('%s')", '%' . $string . '%');
  while ($file = db_fetch_object($files)) {
    $matches[$file->filename] = $file->filename;
  }
  print drupal_to_js($matches);
  exit;
}

/**
 * Email a user with download links for a product file download
 *
 * @param $user
 *   The Drupal user object
 * @param $order
 *   The order object associated with message
 * @param $file_user
 *   An array for user file downloads (uc_file_user row) associated with message
 * @return:
 *   Sends result of drupal_mail
 */
function _email_file_download($user, $order, $file_users) {
  if (!variable_get('uc_file_download_notification', FALSE)) {
    return;
  }
  $token_filters = array(
    'global' => NULL,
    'user' => $user,
    'order' => $order,
    'uc_file' => $file_users,
  );
  $key = 'uc_file_download_notify';
  $to = $order->primary_email;
  $from = uc_store_email_from();
  $subject = token_replace_multiple(variable_get('uc_file_download_notification_subject', uc_get_message('uc_file_download_subject')), $token_filters);
  $body = token_replace_multiple(variable_get('uc_file_download_notification_message', uc_get_message('uc_file_download_message')), $token_filters);
  $body = check_markup($body, variable_get('uc_file_download_notification_format', 3), FALSE);

  //drupal_set_message("Mail Sent<br />key: $key, <br />to: $to, <br />subject: $subject, <br />body: $body, <br />from: $from, <br />");
  return drupal_mail($key, $to, $subject, $body, $from, array(
    'Content-Type' => 'text/html; charset=UTF-8; format=flowed',
  ));
}

// Just a small retrofit of the ip_address() function from Drupal 6.
function _uc_file_ip_address() {
  static $ip_address = NULL;
  if (!isset($ip_address)) {
    $ip_address = $_SERVER['REMOTE_ADDR'];
    if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {

      // If an array of known reverse proxy IPs is provided, then trust
      // the XFF header if request really comes from one of them.
      $reverse_proxy_addresses = variable_get('uc_file_reverse_proxy_addresses', array());
      if (!empty($reverse_proxy_addresses) && in_array($ip_address, $reverse_proxy_addresses, TRUE)) {

        // If there are several arguments, we need to check the most
        // recently added one, i.e. the last one.
        $ip_address = array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
      }
    }
  }
  return $ip_address;
}

/**
 * Perform first-pass authorization. Call authorization hooks afterwards.
 *
 * Called when a user requests a file download, function checks download
 * limits then checks for any implementation of hook_download_authorize.
 * Passing that, the function _file_download_transfer is called.
 *
 * @param $fid
 *   The fid of the file specified to download.
 * @param $key
 *   The hash key of a user's download
 */
function _file_download($fid, $key) {
  global $user;

  // In case the $user doesn't exist (downloading with a Download Manager, or Anonymous) find the $user based on the URL given
  if (!$user->uid) {
    $user_id = db_result(db_query("SELECT uid FROM {uc_file_users} WHERE fid = %d and file_key = '%s'", $fid, $key));
    $user = user_load(array(
      'uid' => $user_id,
    ));
  }
  $ip = _uc_file_ip_address();
  $message_admin = t('Please contact the site administrator if this message has been received in error.');
  $message_user = $user->uid ? t('The user %username ', array(
    '%username' => $user->name,
  )) : t('The IP address %ip ', array(
    '%ip' => $ip,
  ));
  $file_download = db_fetch_object(db_query("SELECT * FROM {uc_file_users} WHERE fid = %d AND `file_key` = '%s'", $fid, $key));
  $request_cache = cache_get('uc_file_' . $ip);
  $requests = $request_cache ? $request_cache->data + 1 : 1;
  if ($requests > UC_FILE_REQUEST_LIMIT) {
    _file_download_deny($user->uid, t('You have attempted to download an incorrect file URL too many times. ') . $message_admin);
  }
  if (!$file_download) {
    cache_set('uc_file_' . $ip, 'cache', $requests, time() + 86400);
    if ($requests == UC_FILE_REQUEST_LIMIT) {
      watchdog('uc_file', t('%username has been temporarily banned from file downloads.', array(
        '%username' => $message_user,
      )), WATCHDOG_WARNING);
    }
    _file_download_deny($user->uid, t("The following URL is not a valid download link. ") . $message_admin);
  }
  else {
    $ip_limit = variable_get('uc_file_download_limit_addresses', NULL);
    $addresses = unserialize($file_download->addresses);
    if (!empty($ip_limit) && !in_array($ip, $addresses) && count($addresses) >= $ip_limit) {
      watchdog('uc_file', t('%username has been denied a file download by downloading it from too many IP addresses.', array(
        '%username' => $message_user,
      )), WATCHDOG_WARNING);
      _file_download_deny($user->uid, t('You have downloaded this file from too many different locations. ') . $message_admin);
    }
    else {
      $download_limit = variable_get('uc_file_download_limit_number', NULL);
      if (!empty($download_limit) && $file_download->accessed >= $download_limit) {
        watchdog('uc_file', t('%username has been denied a file download by downloading it too many times.', array(
          '%username' => $message_user,
        )), WATCHDOG_WARNING);
        _file_download_deny($user->uid, t('You have downloaded this file too many times. ') . $message_admin);
      }
      else {
        $duration_limit = _file_expiration_date($file_download->granted);
        if ($duration_limit !== FALSE && time() >= $duration_limit) {
          watchdog('uc_file', t('%username has been denied an expired file download.', array(
            '%username' => $message_user,
          )), WATCHDOG_WARNING);
          _file_download_deny($user->uid, t("This file download has expired. ") . $message_admin);
        }
        else {

          //Check any if any hook_download_authorize calls deny the download
          foreach (module_implements('download_authorize') as $module) {
            $name = $module . '_download_authorize';
            $result = $name($user, $file_download);
            if (!$result) {
              _file_download_deny($user->uid);
            }
          }
          $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
          watchdog('uc_file', t('%username has started download of the file %filename.', array(
            '%username' => $message_user,
            '%filename' => basename($filename),
          )), WATCHDOG_NOTICE);
          _file_download_transfer($file_download, $ip, $file_download->fid);
        }
      }
    }
  }
}

/**
 * Deny a file download
 *
 * @param $uid
 *   The user id of the person attempting the download
 * @param $message
 *   The optional message to send to the user
 */
function _file_download_deny($uid = NULL, $message = NULL) {
  if (!is_null($message)) {
    drupal_set_message($message, 'error');
  }
  if (is_null($uid) || $uid == 0) {
    drupal_access_denied();
    exit;
  }
  else {
    drupal_goto('user/' . $uid . '/files');
  }
}

/**
 * Send the file's binary data to a user via HTTP and update the uc_file_users table.
 *
 * Supports resume and download managers.
 *
 * @param $file_user
 *   The file_user object from the uc_file_users
 * @param $ip
 *   The string containing the ip address the download is going to
 * @param $fid
 *   The file id of the file to transfer
 */
function _file_download_transfer($file_user, $ip, $fid) {
  $file = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  $file_path = variable_get('uc_file_base_dir', NULL) . '/' . $file;
  if (!is_file($file_path)) {
    drupal_set_message(t('The file %filename could not be found. Please contact the site administrator.', array(
      '%filename' => basename($file),
    )), 'error');
    watchdog('uc_file', t('%username failed to download the file %filename.', array(
      '%username' => $message_user,
      '%filename' => basename($file),
    )), WATCHDOG_NOTICE);
    drupal_not_found();
    exit;
  }
  else {

    //Check any if any hook_file_transfer_alter calls alter the download
    foreach (module_implements('file_transfer_alter') as $module) {
      $name = $module . '_file_transfer_alter';
      $file_path = $name($file_user, $ip, $fid, $file_path);
    }

    //Gather relevent info about file
    $size = filesize($file_path);
    $fileinfo = pathinfo($file_path);

    // Workaround for IE filename bug with multiple periods / multiple dots in filename
    // that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
    if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
      $filename = preg_replace('/\\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1);
    }
    else {
      $filename = $fileinfo['basename'];
    }

    // Compatibility workaround for older versions of Drupal 5
    if (function_exists('file_get_mimetype')) {
      $mimetype = file_get_mimetype($filename);
    }
    else {

      // Set the Content-Type based on file extension.
      $file_extension = strtolower($fileinfo['extension']);
      switch ($file_extension) {
        case 'exe':
          $mimetype = 'application/octet-stream';
          break;
        case 'zip':
          $mimetype = 'application/zip';
          break;
        case 'mp3':
          $mimetype = 'audio/mpeg';
          break;
        case 'mpg':
          $mimetype = 'video/mpeg';
          break;
        case 'avi':
          $mimetype = 'video/x-msvideo';
          break;
        default:
          $mimetype = 'application/force-download';
      }
    }

    // Check if HTTP_RANGE is sent by browser (or download manager)
    if (isset($_SERVER['HTTP_RANGE'])) {
      list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
      if ($size_unit == 'bytes') {

        // Multiple ranges could be specified at the same time, but for simplicity only serve the first range
        // See http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
        list($range, $extra_ranges) = explode(',', $range_orig, 2);
      }
      else {
        $range = '';
      }
    }
    else {
      $range = '';
    }

    // Figure out download piece from range (if set)
    list($seek_start, $seek_end) = explode('-', $range, 2);

    // Set start and end based on range (if set), else set defaults and check for invalid ranges.
    $seek_end = intval(empty($seek_end) ? $size - 1 : min(abs(intval($seek_end)), $size - 1));
    $seek_start = intval(empty($seek_start) || $seek_end < abs(intval($seek_start)) ? 0 : max(abs(intval($seek_start)), 0));
    ob_end_clean();

    // Start building the array of headers
    $http_headers = array();

    //Only send partial content header if downloading a piece of the file (IE workaround)
    if ($seek_start > 0 || $seek_end < $size - 1) {
      drupal_set_header('HTTP/1.1 206 Partial Content');
    }

    // Standard headers, including content-range and length
    drupal_set_header('Pragma: public');
    drupal_set_header('Cache-Control: cache, must-revalidate');
    drupal_set_header('Accept-Ranges: bytes');
    drupal_set_header('Content-Range: bytes ' . $seek_start . '-' . $seek_end . '/' . $size);
    drupal_set_header('Content-Type: ' . $mimetype);
    drupal_set_header('Content-Disposition: attachment; filename="' . $filename . '"');
    drupal_set_header('Content-Length: ' . ($seek_end - $seek_start + 1));

    // Last-modified is required for content served dynamically
    drupal_set_header('Last-modified: ' . format_date(filemtime($file_path), 'large'));

    // Etag header is required for Firefox3 and other managers
    drupal_set_header('ETag: ' . md5($file_path));

    // Open the file and seek to starting byte
    $fp = fopen($file_path, 'rb');
    fseek($fp, $seek_start);

    // Start buffered download
    while (!feof($fp)) {

      // Reset time limit for large files
      set_time_limit(0);
      print fread($fp, 1024 * 8);
      flush();
      ob_flush();
    }

    // Finished serving the file, close the stream and log the download to the user table
    fclose($fp);
    _user_table_action('download', $file_user, $ip);
    exit;
  }
}

/**
 * Return a file expiration date given a purchase date
 *
 * @param $purchase_date
 *   The purchase date for the file
 * @return:
 *   A UNIX timestamp representing the second the file download expires or FALSE
 *   if there won't be an expiration
 */
function _file_expiration_date($purchase_date = NULL) {
  $purchase_date = !is_null($purchase_date) ? $purchase_date : time();
  $quantity = !is_null(variable_get('uc_file_download_limit_duration_qty', NULL)) ? variable_get('uc_file_download_limit_duration_qty', NULL) : 1;
  $operator = $quantity < 0 ? '' : '+';
  $duration = variable_get('uc_file_download_limit_duration_granularity', 'never');
  return $duration != 'never' ? strtotime($operator . $quantity . ' ' . $duration, $purchase_date) : FALSE;
}

/**
 * Perform a specified action on the uc_files table
 *
 * @param $op
 *   The action to perform on uc_files table
 *   - empty: truncate uc_files table
 *   - insert: scan the uc_file base dir and enter new files into the table
 *   - remove: remove the file specified by arguments
 *   - refresh: scan the uc_file base dir and remove files/dir from the table
 *       that don't exist
 * @param $arg1
 *   Specified by op argument
 * @param $arg2
 *   Specified by op argument
 * @param $arg3
 *   Specified by op argument
 */
function _file_table_action($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL) {
  switch ($op) {
    case 'empty':

      //Clear out file table (args not used)
      db_query("TRUNCATE TABLE {uc_files}");
      break;
    case 'insert':

      //Add new items into table (args not used)
      if (!is_null($dir = variable_get('uc_file_base_dir', NULL))) {
        $files = file_scan_directory($dir, variable_get('uc_file_file_mask', '.*'));
        $dir = substr($dir, -1) != '/' || substr($dir, -1) != '\\' ? $dir . '/' : $dir;
        foreach ($files as $file) {
          $filename = str_replace($dir, '', $file->filename);
          $file_dir = dirname($filename);
          if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $file_dir . '/')) && $file_dir != '.') {
            $fid = db_next_id('{uc_files}_fid');
            db_query("INSERT INTO {uc_files} (fid, filename) VALUES (%d, '%s')", $fid, $file_dir . '/');
          }
          if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $filename))) {
            $fid = db_next_id('{uc_files}_fid');
            db_query("INSERT INTO {uc_files} (fid, filename) VALUES (%d, '%s')", $fid, $filename);
          }
          if (!is_null($fid)) {
            $file_object = db_fetch_object(db_query("SELECT * FROM {uc_files} WHERE fid = %d", $fid));

            //Check any if any hook_file_action('insert', $args) are implemented
            foreach (module_implements('file_action') as $module) {
              $name = $module . '_file_action';
              $result = $name('insert', array(
                'file_object' => $file_object,
              ));
            }
            unset($fid);
          }
        }
      }
      break;
    case 'remove':

      //Remove a specific file id (arg1 = file id to delete, arg2 = TRUE = recursively delete directories, arg 3 = TRUE = delete associated rows/files)
      if (!is_null($arg1) && ($filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $arg1)))) {
        $dir = variable_get('uc_file_base_dir', NULL);
        $sub_fids = $arg2 ? _get_dir_file_ids($arg1, TRUE) : _get_dir_file_ids($arg1);
        $selected_fid = is_dir($dir . '/' . $filename) && !$arg2 ? array() : array(
          $arg1,
        );
        $fids = !$sub_fids ? $selected_fid : array_merge($sub_fids, $selected_fid);
        $fids = _sort_fids($fids);
        foreach ($fids as $fid) {
          if ($arg3) {
            $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
            $pfids = db_query("SELECT pfid FROM {uc_file_products} WHERE fid = %d", $fid);
            while ($pfid = db_fetch_object($pfids)) {
              db_query("DELETE FROM {uc_product_features} WHERE pfid = %d AND fid = 'file'", $pfid->pfid);
              db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $pfid->pfid);
            }
            if (is_dir($dir . '/' . $filename)) {
              rmdir($dir . '/' . $filename);
            }
            else {
              unlink($dir . '/' . $filename);
            }
            _user_table_action('remove', $fid);
          }
          db_query("DELETE FROM {uc_files} WHERE fid = %d", $fid);
        }
      }
      break;
    case 'refresh':

      //Remove non-existing items from table (args not used)
      $files = db_query("SELECT * FROM {uc_files}");
      while ($file = db_fetch_object($files)) {
        if (is_dir(variable_get('uc_file_base_dir', NULL) . '/' . $file->filename)) {
          continue;
        }
        if (is_file(variable_get('uc_file_base_dir', NULL) . '/' . $file->filename)) {
          continue;
        }
        db_query("DELETE FROM {uc_files} WHERE fid = %d", $file->fid);
      }
      break;
    default:
      break;
  }
}

/**
 * Generate hash used for unique download URLs
 *
 * @param $values
 *   An array of values that will be used to generate the hash
 * @return:
 *   A string containing the 32 hex character hash
 */
function _generate_hash($values) {
  $input = mt_rand();
  foreach ($values as $value) {
    $input .= $value;
  }
  return md5($input);
}

/**
 * Return a list of model adjustments for a given product node
 *
 * @param $nid
 *   The product node id
 * @return:
 *   An associative array containing the models created by different product
 *   attributes or FALSE if none exist.
 */
function _get_adjustment_models($nid) {
  $models = array();
  if (module_exists('uc_attribute')) {
    $adjustments = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = %d", $nid);
    while ($adjustment = db_fetch_object($adjustments)) {
      if (!in_array($adjustment->model, $models)) {
        $models[$adjustment->model] = $adjustment->model;
      }
    }
  }
  return empty($models) ? FALSE : $models;
}

/**
 * Return a list of file ids that are in the directory
 *
 * @param $fid
 *   The file id associated with the directory
 * @param $recursive
 *   Whether or not to list recursive directories and their files
 * @return:
 *   If there are files in the directory an array of file ids, else return FALSE
 */
function _get_dir_file_ids($fid, $recursive = FALSE) {
  $fids = array();
  $dir = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  $files = db_query("SELECT * FROM {uc_files} WHERE filename LIKE LOWER('%s')", $dir . '%');
  while ($file = db_fetch_object($files)) {
    $filename_change = str_replace($dir, '', $file->filename);
    $filename = substr($filename_change, 0, 1) == '/' ? substr($filename_change, 1) : $filename_change;
    if (!strpos($filename, '/') && !empty($filename)) {
      $fids[] = $file->fid;
    }
    elseif ($recursive && !empty($filename) && $filename_change != $file->filename) {
      $fids[] = $file->fid;
    }
  }
  return empty($fids) ? FALSE : $fids;
}

/**
 * Group filenames by an attribute while maintaining order.
 *
 * @param $files
 *   The array of objects
 * @return:
 *   The sorted array of objects
 */
function _group_filenames($objects) {
  $output = array();
  $existing = array();
  foreach ($objects as $key => $object) {
    $filename = $object->filename;
    if (!in_array($filename, array_keys($existing))) {
      $existing[$filename] = $key;
      $output[] = $object;
    }
    else {
      $inserted_index = $existing[$filename] + 1;
      foreach ($existing as $filename => $index) {
        $existing[$filename] = $index >= $inserted_index ? $index + 1 : $index;
      }
      array_splice($output, $inserted_index, 0, array(
        $inserted_index => $object,
      ));
    }
  }
  return $output;
}

/**
 * Take a list of file ids and sorts the list to where directories are list last
 * and by order of descending depth
 * @param $fids
 *   The array of file ids
 * @return:
 *   The sorted array of file ids
 */
function _sort_fids($fids) {
  $dir_fids = array();
  $output = array();
  foreach ($fids as $fid) {
    $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
    if (substr($filename, -1) == '/') {
      $dir_fids[$fid] = $filename;
    }
    else {
      $output[] = $fid;
    }
  }
  while (!empty($dir_fids)) {
    $highest = 0;
    foreach ($dir_fids as $dir_fid => $filename) {
      if (substr_count($filename, '/') > $highest) {
        $highest = substr_count($filename, '/');
        $highest_fid = $dir_fid;
      }
    }
    $output[] = $highest_fid;
    unset($dir_fids[$highest_fid]);
  }
  return $output;
}

/**
 * Perform a specified action on the uc_file_users table
 *
 * @param $op
 *   The action to perform on uc_file_users table
 *   - allow: insert a new file download for a user
 *   - download: update a row after a download takes places
 *   - remove: remove a file download for specified files, users, keys
 * @param $arg1
 *   Specified by op argument
 * @param $arg2
 *   Specified by op argument
 * @param $arg3
 *   Specified by op argument
 * @return:
 *   Specified by op argument
 */
function _user_table_action($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL) {
  switch ($op) {
    case 'allow':

      //arg1 = file id, arg2 = user id, $arg3 = pfid

      //@return file_user objects inserted into table
      if (!is_null($arg1) && !is_null($arg2)) {
        $output = array();
        $granted = time();
        $fids = _get_dir_file_ids($arg1) ? _get_dir_file_ids($arg1) : array(
          $arg1,
        );
        foreach ($fids as $fid) {
          $values = array(
            $arg1,
            $arg2,
            $arg3,
            '',
            $granted,
            0,
            serialize(array()),
          );
          $hash = _generate_hash($values);
          db_query("INSERT INTO {uc_file_users} (fid, uid, pfid, `file_key`, granted, accessed, addresses) VALUES (%d, %d, %d, '%s', %d, %d, '%s')", $fid, $arg2, $arg3, $hash, $granted, 0, serialize(array()));
          $output[] = db_fetch_object(db_query("SELECT * FROM {uc_file_users} WHERE uid = %d AND `file_key` = '%s'", $arg2, $hash));
        }
      }
      return !is_null($output) ? $output : FALSE;
      break;
    case 'download':

      //arg1 = existing file_user object, arg2 = ip download was made from, arg3 not used
      if (!is_null($arg1) && !is_null($arg2)) {
        $addresses = unserialize($arg1->addresses);
        if (!in_array($arg2, $addresses)) {
          $addresses[] = $arg2;
        }
        $accessed = $arg1->accessed + 1;
        $values = array(
          $arg1->fid,
          $arg1->uid,
          $arg1->pfid,
          $arg1->file_key,
          $arg1->granted,
          $accessed,
          serialize($addresses),
        );
        $hash = _generate_hash($values);
        db_query("UPDATE {uc_file_users} SET accessed = %d, addresses = '%s', `file_key` = '%s' WHERE fid = %d AND uid = %d AND `file_key` = '%s'", $accessed, serialize($addresses), $hash, $arg1->fid, $arg1->uid, $arg1->file_key);
      }
      break;
    case 'remove':

      //arg1 = file id, arg2 = user id, $arg3 = key
      if (!is_null($arg1) || !is_null($arg2)) {
        if (!is_null($arg1) && is_null($arg2) && is_null($arg3)) {

          //Remove a file from download
          db_query("DELETE FROM {uc_file_users} WHERE fid = %d", $arg1);
        }
        if (is_null($arg1) && !is_null($arg2) && is_null($arg3)) {

          //Remove a user's downloads
          db_query("DELETE FROM {uc_file_users} WHERE uid = %d", $arg2);
        }
        if (!is_null($arg1) && !is_null($arg2) && is_null($arg3)) {

          //Remove a certain files from a user
          db_query("DELETE FROM {uc_file_users} WHERE fid = %d AND uid = %d", $arg1, $arg2);
        }
        if (is_null($arg1) && !is_null($arg2) && !is_null($arg3)) {

          //Remove a certain file from a user
          db_query("DELETE FROM {uc_file_users} WHERE uid = %d AND `file_key` = '%s'", $arg2, $arg3);
        }
      }
      break;
    default:
      break;
  }
}

Functions

Namesort descending Description
theme_uc_file_downloads_token Theme file download links token
theme_uc_file_files_form Implementation of theme_form($form)
uc_file_cart_item Implementation of hook_cart_item().
uc_file_feature_delete product_feature delete function
uc_file_feature_form Form builder for hook_product_feature
uc_file_feature_form_submit
uc_file_feature_form_validate
uc_file_feature_settings Form builder for file settings
uc_file_feature_settings_submit
uc_file_feature_settings_validate
uc_file_files_admin Page builder for file products admin
uc_file_files_form Form builder for file products admin
uc_file_files_form_submit
uc_file_files_form_validate
uc_file_files_table Table builder for file products admin
uc_file_form_alter Implementation of hook_form_alter().
uc_file_menu Implementation of hook_menu().
uc_file_notify_settings Form builder for file download notification settings.
uc_file_order Implementation of hook_order().
uc_file_perm Implementation of hook_perm().
uc_file_product_feature Implementation of hook_product_feature().
uc_file_store_status Implementation of hook_store_status().
uc_file_token_list Implementation of hook_token_list().
uc_file_token_values Implementation of hook_token_values().
uc_file_uc_message Implementation of hook_uc_message().
uc_file_user Implementation of hook_user().
uc_file_user_downloads Table builder for user downloads
_autocomplete_filename Implement Drupal autocomplete textfield
_email_file_download Email a user with download links for a product file download
_file_download Perform first-pass authorization. Call authorization hooks afterwards.
_file_download_deny Deny a file download
_file_download_transfer Send the file's binary data to a user via HTTP and update the uc_file_users table.
_file_expiration_date Return a file expiration date given a purchase date
_file_table_action Perform a specified action on the uc_files table
_generate_hash Generate hash used for unique download URLs
_get_adjustment_models Return a list of model adjustments for a given product node
_get_dir_file_ids Return a list of file ids that are in the directory
_group_filenames Group filenames by an attribute while maintaining order.
_sort_fids Take a list of file ids and sorts the list to where directories are list last and by order of descending depth
_uc_file_ip_address
_user_table_action Perform a specified action on the uc_file_users table

Constants

Namesort descending Description
UC_FILE_BYTE_SIZE
UC_FILE_PAGER_SIZE @file Allows products to be associated with downloadable files.
UC_FILE_REQUEST_LIMIT