<?php
// $Id: file.module,v 1.37.2.7.2.4 2007/03/20 05:00:52 neclimdul Exp $

/********************************************************************
 * Drupal Hooks
 ********************************************************************/

/**
 * Implementation of hook_help().
 */
function file_help($section = 'admin/help#file') {
  global $user;

  switch ($section) {
    case 'admin/modules#description':
      return t('<b>EC:</b> Creates a file product. Dependency: product.module ec_recurring.module');
    case 'node/add/product#file':
      return t('A file can be downloaded by purchasers.');
    case "store/myfiles/$user->uid":
      return t('Some files may not be available until your transactions has finished being processed.');
  }
}

/**
 * Implementation of hook_menu().
 */
function file_menu($may_cache) {

  global $user;
  $items  = array();
  $access = user_access('administer store');

  if ($may_cache) {
    if ($user->uid) {
      $items[] = array(
        'path' => 'ec_file/autocomplete', 
        'title' => t('file autocomplete'),
        'callback' => 'ec_file_autocomplete',
        'access' => $access, 
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'store/myfiles/'. $user->uid, 
        'title' => t('my files'),
        'callback' => 'file_my_overview',
        'callback arguments' => array($user->uid),
        'access' => user_access('access content'),
        'type' => MENU_DYNAMIC_ITEM,
      );
      $items[] = array(
        'path' => 'store/myfiles/'. $user->uid .'/download', 
        'title' => t('download'),
        'callback' => 'ec_product_download', 
        'access' => user_access('access content'),
        'type' => MENU_CALLBACK,
      );
    }
    $items[] = array(
      'path' => 'admin/store/products/files', 
      'title' => t('product file quicklist'),
      'callback' => 'file_quicklist', 
      'access' => $access, 
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/settings/file',
      'title' => 'file',
      'callback' => 'file_ec_settings',
      'access' => $access,
      'type' => MENU_NORMAL_ITEM,
    );
  }
  else {
    if ($access) {
      $items[] = array(
        'path' => 'store/myfiles/', 
        'title' => t('user Files'),
        'callback' => 'file_my_overview', 
        'access' => TRUE,
        'type' => MENU_LOCAL_TASK,
      );
    }
  }
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function file_perm() {
  return array('create file products', 'edit own file products');
}

/********************************************************************
 * Module Functions
 ********************************************************************/

/**
 * Implementation of hook_access().
 */
function file_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create file products');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own file products') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

/**
 * Implementation of the productapi_hook()
 */
function file_productapi(&$node, $op, $a3 = null, $a4 = null) {

  switch ($op) {

    /* Due to the way we have to build our node, we have to pass an array of values this module collects. */
    case 'fields':
      return array('fpath' => $node->fpath, 'size' => $node->size);
      break;

    case 'validate':
      // is_null provides a mechanism for us to determine if this is the first viewing of the form.
      if (!is_null($node->fpath)) {

        if (empty($node->fpath)) {
          form_set_error('fpath', t('Please enter the path to the file.'));
        }
        else {
          $full_path = ec_file_create_path($node->fpath);
          if (!file_exists($full_path)) {
            form_set_error('fpath', t('The file doesn\'t exist.'));
          }
          else {
            // This is new stuff for 4.7. http://drupal.org/node/51104. Please leave this comment in for now.
            form_set_value($a3['product']['f_settings']['edit_size'], array(filesize($full_path)));
          }
        }
      }
      break;

    /* Not necessary, but this offers a chance to provide an user-friendly
       name for the product type selection box. So in this
       case, instead of showing 'file', we show 'file download' */
    case 'wizard_select':
      return array('file' => t('File download product'));
      break;

    case 'attributes':
      return array('in_stock');
      break;

    case 'form':
      if ($a3['fpath']) {
        drupal_set_message($a3['fpath'], 'error');
      }
      if (_ec_file_is_managed($node)) {
        $description = t('Do not edit.  File is managed by %nodetype node.', array('%nodetype' => $node->type));
        $disabled = TRUE;
      }
      else {
        $description = t('Enter the filesystem path to this file (not the URL). This path will be prefixed with <b>!file_path</b> Here is a <a href="!file_quicklist" onclick="window.open(this.href, \'!file_quicklist\', \'width=480,height=480,scrollbars=yes,status=yes\'); return false">list of files</a> in this directory. You may need to FTP your file to this directory before you can create the file product.', array('!file_quicklist' => url('admin/store/products/files'), '!file_path' => ec_file_create_path()));
        $disabled = FALSE;
      }

      $form['f_settings'] = array(
        '#type' => 'fieldset',
        '#title' => t('File settings'),
      );
      $form['f_settings']['fpath'] = array(
        '#type' => 'textfield',
        '#title' => t('File path'),
        '#default_value' => $node->fpath,
        '#size' => 50,
        '#maxlength' => 200,
        '#autocomplete_path' => 'ec_file/autocomplete',
        '#description' => $description,
        '#disabled' => $disabled,
      );
      $form['f_settings']['edit_size'] = array('#type' => 'value', '#value' => array());
      // Pass some info to ec_recurring.
      $form['recurring']['sid'] = array( '#allow_renewals' => FALSE, );

      return $form;

     /* Similar to node_load */
    case 'load':
      return db_fetch_object(db_query('SELECT * FROM {ec_product_file} WHERE nid = %d', $node->nid));

    /* Node has been saved, write to product tables. */
    case 'insert':
      if (!_ec_file_is_managed($node)) {
        return db_query("INSERT INTO {ec_product_file} (nid, fpath, size) VALUES (%d, '%s', '%s')", $node->nid, $node->fpath, $node->edit_size[0]);
      }
      break;

    case 'update':
      if (!_ec_file_is_managed($node)) {
        return db_query("UPDATE {ec_product_file} SET fpath = '%s', size = '%s' WHERE nid = %d", $node->fpath, $node->edit_size[0], $node->nid);
      }
      break;

    case 'delete':
      if (!_ec_file_is_managed($node)) {
        return db_query('DELETE FROM {ec_product_file} WHERE nid = %d', $node->nid);
      }
      break;
  }
}

/**
 * Implementation of hook_checkoutapi().
 */
function file_checkoutapi(&$txn, $op, $arg3 = NULL, $arg4 = NULL) {
  switch ($op) {

    case 'review':
      // Check if we have shippable items
      if ($txn == 'file') {
        return TRUE;
      }
      $has_file_item = FALSE;
      foreach($txn->items as $product) {
        if ($product->ptype == 'file') {
          $has_file_item = TRUE;
          break;
        }
      }
      if ($txn->shippable && $has_file_item) {
        drupal_set_message(t('There are shippable items in your cart that may cause your files to not be immediately available.'), 'warning');
      }
  }
}

/**
 * Implementation of hook_settings().
 */
function file_ec_settings() {
  drupal_set_title(t('Ecommerce file download settings (%revision)', array('%revision' => '$Revision: 1.37.2.7.2.4 $')));

  if (!isset($_POST['op'])) {
    $file_path = variable_get('ec_file_directory_path', 'files');
    file_check_directory($file_path, 0, 'ec_file_directory_path');
  }
  $form['ec_file_directory_path'] = array(
    '#type' => 'textfield',
    '#title' => t('File path for products'),
    '#default_value' => variable_get('ec_file_directory_path', 'files'),
    '#size' => 70,
    '#maxlength' => 255,
    '#description' => t('A file system path where the product files are stored. This directory has to exist and be writable by Drupal. This directory should not be accessible over the web. Changing this location after the site has been in use will cause problems so only change this setting on an existing site if you know what you are doing.'),
    '#validate' => array('file_directory_validate' => array()),
  );
  $slist = array_merge(array(0 => '--'), ec_recurring_get_list(FALSE));
  // TODO Implement the code to use this feature. 
  $form['ec_file_default_schedule'] = array(
    '#type' => 'select',
    '#title' => t('Default renewal schedule'),
    '#default_value' => variable_get('ec_file_default_schedule', 0),
    '#description' => t('Select a default renewal schedule to be used for files. Note: This is not used yet.'),
    '#options' => $slist,
    '#weight' => -1
  );
  return system_settings_form('file_ec_settings_form', $form);
}

/**
 * Validate file_directory element.
 */
function file_directory_validate($form) {
  file_check_directory($form['#value'], 1, 'ec_file_directory_path');
  return $form;
}

/**
 * Implementation of hook_user().
 */
function file_user($type, &$edit, &$user) {
  // Only display the link for the user who owns it.
  if ($type == 'view' && $GLOBALS['user']->uid == $user->uid) {
    $files[] = array(
      'class' => 'item',
      'title' => t('Downloads'),
      'value' => l(t('View your downloads'), 'store/myfiles/'. $user->uid)
    );
    return array(t('History') => $files);
  }
}

/********************************************************************
 * Module Functions
 ********************************************************************/

/**
 * Scan the file directory and list the files it finds.
 */
function file_quicklist() {

  global $base_url;
  $base_url .= '/';

  $html = "<html>\n<head>\n<base href=\"$base_url\" />\n<style type=\"text/css\" media=\"all\">\n@import url(misc/drupal.css);\n</style>\n</head>\n<body>\n";
  $html .= '<p>Full path: '. variable_get('ec_file_directory_path', 'files')  ."</p>\n";

  $header = array(t('name'), t('size'), t('changed'));
  $rows = array();

  $files = file_scan_directory(variable_get('ec_file_directory_path', 'files'), '.*');
  foreach ($files as $f) {
    $f->size    = filesize($f->filename);
    $f->changed = filemtime($f->filename);
    $filename = str_replace(variable_get('ec_file_directory_path', 'files'), '', $f->filename);
    $rows[] = array(trim($filename, '/'), format_size($f->size), format_date($f->changed, 'small'));
  }

  $html .= theme('table', $header, $rows) .'</body></html>';

  print $html;
}

/**
 * Print the files for the user to download.
 *
 * If the expired parameter is true, this will output a list of expired files instead.
 *
 * Note:
 * Expiration date is found via the sitewide file expiration date OR the
 * special case where a store admin has extended the expiration date for a given
 * transaction. The 'special case' always take precedence, so you can disable
 * downloads before the sitewide expiration date is reached.
 * 
 * TODO: this should really be a view.  this is a waste of a lot of code :)
 * TODO: until we add views this would be nice if it was a pager query.
 */
function file_my_overview($uid = FALSE, $expired = FALSE) {

  $name = db_result(db_query('SELECT name FROM {users} WHERE uid = %d', $uid));

  if ($expired) {
    $output = t('<p><a href="!files-uri">Click here to view your files.</a></p>', array('!files-uri' => url('store/myfiles/'. $uid)));
    $title = t('%username\'s expired files', array('%username' => $name));
    $status = ECRECURRING_STATUS_EXPIRED;
  }
  else {
    $output = t('<p><a href="!expired-files-uri">Click here to view your expired files.</a></p>', array('!expired-files-uri' => url("store/myfiles/$uid/expired")));
    $title = t('%username\'s files', array('%username' => $name));
    $status = ECRECURRING_STATUS_ACTIVE;
  }
  drupal_set_title($title);

  $header = array(
    array('data' => t('filename'), 'field' => 't.txnid', 'sort' => 'desc'),
    array('data' => t('size'), 'field' => 'epf.size'),
    array('data' => ($expired) ? t('expired') : t('expires'), 'field' => 'ere.expiry'),
    t('operations')
  );

  // Checking for file to download involves at least 2 queries to the store
  // transaction table. 1) Check if the user explicitly purchased a file download
  // as 2) Check if the user purchased a multi-product which contains a file download.

  // Check 1: Grab all explicit file downloads.
  $result = db_query('SELECT etp.title, epf.size, epf.fpath, ere.expiry, t.workflow
    FROM {ec_transaction} t
      JOIN {ec_transaction_product} etp ON etp.txnid = t.txnid
      JOIN {ec_product_file} epf ON epf.nid = etp.nid
      LEFT OUTER JOIN {ec_recurring_expiration} ere ON ere.vid = etp.vid AND ere.txnid = etp.txnid
    WHERE t.uid = %d AND ((ere.status = 0 AND ere.rid <= 0) OR ere.status IS NULL)'. tablesort_sql($header), $uid, $status);
  while ($data = db_fetch_object($result)) {
    if ($data->workflow == EC_WORKFLOW_COMPLETED) {
      $dload_link = t('<a href="!file-download-uri">download</a>', array('!file-download-uri' => ec_file_create_url($data->fpath)));
    }
    else {
      $dload_link = t('Transaction %status', array('%status' => transaction_get_workflow($data->workflow)));
    }

    if ($expired) {
      $rows[] = array(theme('placeholder', $data->title), format_size($data->size), t('%interval ago', array('%interval' => format_interval($data->expiry - time(), 3))), '');
    }
    else {
      $rows[] = array(theme('placeholder', $data->title), format_size($data->size), format_interval($data->expiry - time(), 3), $dload_link);
    }
  }

  // Check 2: Check for multi products that contain file downloads.
  // Multi products(parcels) don't work 100% with subscriptions so we can't use them currently.

  $output .= ($rows) ? theme('table', $header, $rows) : t('You have no files to download.');

  return $output;
}

/**
 * IE for Mac chokes on long filenames. This function determines if the user is using IE for Mac.
 */
function file_iemac_hack() {
  global $HTTP_USER_AGENT;

  return !((strpos($HTTP_USER_AGENT, 'MSIE 4') === FALSE) && (strpos($HTTP_USER_AGENT, 'MSIE 5') === FALSE) && (strpos($HTTP_USER_AGENT, 'Mac') === FALSE));
}

/********************************************************************
 * Module Functions :: File Downloads
 ********************************************************************/
/**
 * Create the download path to a file.
 *
 * @param $path Path to the file to generate URL for
 * @return URL pointing to the file
 */
function ec_file_create_url($path) {
  global $user;

  $uid = $user->uid;
  $arg2 = arg(2);
  if (user_access('administer store') && is_int($arg2)) {
    $uid = $arg2;
  }
  return url("store/myfiles/$uid/download", 'file='. urlencode($path));
}

/**
 * Make sure the destination is a complete path and resides in the
 * file system directory, if it is not prepend the
 * file system directory.
 *
 * @param $dest Path to verify
 * @return Path to file with file system directory prepended if necessary.
 *         Returns FALSE if the path is invalid (i.e. outside the configured 'files'-directory).
 */
function ec_file_create_path($dest = 0) {
  $file_path = variable_get('ec_file_directory_path', 'files');
  if (!$dest) {
    return $file_path;
  }
  // file_check_location() checks whether the destination is inside the Drupal files directory.
  if (file_check_location($dest, $file_path)) {
    return $dest;
  }
  // check if the destination is instead inside the Drupal temporary files directory.
  else if (file_check_location($dest, variable_get('file_directory_temp', FILE_DIRECTORY_TEMP))) {
    return $dest;
  }
  // Not found, try again with prefixed dirctory path.
  else if (file_check_location($file_path .'/'. $dest, $file_path)) {
    return $file_path .'/'. $dest;
  }
  // File not found.
  return FALSE;
}

/**
 * Call modules to find out if a file is accessible for a given user.
 */
function ec_product_download() {
  $file = urldecode($_GET['file']);
  if (file_exists(ec_file_create_path($file))) {
    $headers = ec_file_build_download($file);
    if (!$headers) {
      drupal_access_denied();
      return;
    }
    elseif (is_array($headers)) {
      ec_file_transfer($file, $headers);
    }
  }
  drupal_not_found();
}

function ec_file_may_download($file, $uid) {
  // TODO: cache results to avoid repeated queries

  $data = db_fetch_object(db_query("SELECT st.txnid, stp.vid FROM {ec_transaction} AS st
      JOIN {ec_transaction_product} AS stp ON stp.txnid = st.txnid
      JOIN {ec_product_file} AS pf ON pf.nid = stp.nid
    WHERE st.uid = %d
      AND st.workflow = %d
      AND pf.fpath = '%s'", $uid, EC_WORKFLOW_COMPLETED, $file));

  return !ec_recurring_product_get_expiry($data->txnid, $data->vid);
}

/**
 * Generate headers necessary for a file download.  First check that user is
 * able to download the file.
 */
function ec_file_build_download($file) {

  global $user;

  $uid = $user->uid;
  if (user_access('administer store')) {
    $uid = arg(2);
  }

  if (ec_file_may_download($file, $uid)) {
    $filename = basename($file);
    if (file_iemac_hack() && strlen($filename) > 30) {
      $filename = substr($filename, strlen($filename) - 30);
    }

    $file = ec_file_create_path($file);
    $header[] = 'Content-type: application/x-download';
    $header[] = 'Content-Disposition: attachment; filename="'. $filename .'";';
    $header[] = 'Accept-Ranges: bytes';
    $header[] = 'Content-Length: '. filesize($file);

    return $header;
  }

  return FALSE;
}

/**
 * Transfer file using http to client. Pipes a file through Drupal to the
 * client.
 *
 * @param $source File to transfer.
 * @param $headers An array of http headers to send along with file.
 */
function ec_file_transfer($source, $headers) {
  ob_end_clean();

  foreach ($headers as $header) {
    header($header);
  }

  $source = ec_file_create_path($source);

  // Transfer file in 1024 byte chunks to save memory usage.
  $fd = fopen($source, 'rb');
  while (!feof($fd)) {
    print fread($fd, 1024);
  }
  fclose($fd);
  exit();
}

/**
 * Retrieve a pipe delimited string of autocomplete suggestions for existing files
 */
function ec_file_autocomplete($string) {
  $matches = array();
  $files = file_scan_directory(variable_get('ec_file_directory_path', 'files'), '.*');
  foreach ($files as $f) {
    $trimmed = trim(str_replace(variable_get('ec_file_directory_path', 'files') .'/', '', $f->filename));
    if (!(strpos($trimmed, $string) === FALSE)) {
      $matches[$trimmed] = check_plain($trimmed);
    }
  }
  print drupal_to_js($matches);
  exit();
}

/**
 * Node-managed file functions
 *
 * The following hooks allow third-party modules which contain files to treat
 * their files as products.  This module allows downloads only when the
 * products have been purchased.
 *
 * See also the ec_media module (ecommerce/contrib/ec_media).
 */

/**
 * Determine which node types are eligible to be treated as products.
 *
 * Invokes hook_ec_file_nodetypes.  Third-party modules must implement that
 * hook to be treated as products.
 */
function _ec_file_get_managed_types() {
  static $cache;
  if (!$cache) {
    $cache = module_invoke_all('ec_file_nodetypes');
  }
  return $cache;
}

/**
 * Determine whether a node is an instance of a node-managed product.
 */
function _ec_file_is_managed($node) {
  // sometimes node passed in is not really a valid node
  if (!$node->type && $node->nid)
    $node = node_load($node->nid);

  return in_array($node->type, array_keys(_ec_file_get_managed_types()));
}

/**
 * Returns settings specific to nodes of the given type.
 */
function _ec_file_managed_settings($node) {
  $settings = _ec_file_get_managed_types();
  return $settings[$node->type];
}

/**
 * hook_file_ec_file_event
 *
 * Notification of a file-related event.
 *
 * @param $fid
 *   Numerical file ID.
 * @param $realm
 *   Textual file-type identifier.  The $fid and $realm combine to form a unique ID for the file.
 * @param $node
 *   Node with which the file is associated.
 * @param $op
 *   Type of event which has occurred.  (I.e. 'insert', 'update', 'delete')
 * @param $info
 *   Associative array of information about the file.  Should contain at a minimum 'filepath' and 'filesize'.
 *
 *   @return
 *     none
 */
function file_ec_file_event($fid, $realm, $node, $op, $info) {
  if (!_ec_file_is_managed($node))
    return;

  if (($settings = _ec_file_managed_settings($node)) &&
      ($settings['realm'] == $realm)) {
    // The file managed by the node type has changed.  Keep our data in sync.
    switch ($op) {
      case 'insert':
        db_query("INSERT INTO {ec_product_file} (nid, fpath, size) VALUES (%d, '%s', '%s')", $node->nid, $info->filepath, $info->filesize);
        break;
      case 'update':
        db_query("UPDATE {ec_product_file} SET fpath = '%s', size = '%s' WHERE nid = %d", $info->filepath, $info->filesize, $node->nid);
        break;
      case 'delete':
        db_query('DELETE FROM {ec_product_file} WHERE nid = %d', $node->nid);
        break;
    }
  }
}

/**
 * hook_file_ec_file_access
 *
 * Grant permission to access files.  This implementation allows us to grant
 * downloads only to users who have purchased files.
 *
 * @param $fid
 *   Numerical file ID.
 * @param $realm
 *   Textual file-type identifier.  The $fid and $realm combine to form a unique ID for the file.
 * @param $node
 *   Node with which the file is associated.
 * @param $op
 *   Type of access requested.  (I.e. 'view', 'download')
 * @param $info
 *   Associative array of information about the file.  Should contain at a minimum 'filepath'.
 * @param $account
 *   The user data structure for the account requesting permission.
 *
 *   @return
 *     TRUE, only if user is allowed access to the file.
 */
function file_ec_file_access($fid, $realm, $node, $op, $info, $account) {
  if ($node->ptype != 'file')
    return;

  if (($settings = _ec_file_managed_settings($node)) &&
      ($settings['realm'] == $realm)) {
    // The file is one we oversee.
    if ($op == 'view' || $op == 'download') {
      //Check that user may download.
      return ec_file_may_download($info->filepath, $account->uid);
    }
  }
}
