<?php

/**
 * @file
 * File menu items.
 */

/**
 * The number of bogus requests one IP address can make before being banned.
 */
define('UC_FILE_REQUEST_LIMIT', 50);

/**
 * Download file chunk.
 */
define('UC_FILE_BYTE_SIZE', 8192);

/**
 * Download statuses.
 */
define('UC_FILE_ERROR_OK'                     , 0);
define('UC_FILE_ERROR_NOT_A_FILE'             , 1);
define('UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS', 2);
define('UC_FILE_ERROR_INVALID_DOWNLOAD'       , 3);
define('UC_FILE_ERROR_TOO_MANY_LOCATIONS'     , 4);
define('UC_FILE_ERROR_TOO_MANY_DOWNLOADS'     , 5);
define('UC_FILE_ERROR_EXPIRED'                , 6);
define('UC_FILE_ERROR_HOOK_ERROR'             , 7);

/**
 * Themes user file downloads page.
 *
 * @param $files
 *   Array of the files.
 *
 * @ingroup themeable
 */
function theme_uc_file_user_downloads($header, $files) {
  $rows = array();
  $row = 0;
  foreach ($files as $file) {
    $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' => $file['description'], 'class' => 'description-row', 'id' => 'description-'. $row),
      array('data' => $file['accessed'] .'/'. ($file['download_limit'] ? $file['download_limit'] : t('Unlimited')), 'class' => 'download-row', 'id' => 'download-'. $row),
      array('data' => count(unserialize($file['addresses'])) .'/'. ($file['address_limit'] ? $file['address_limit'] : t('Unlimited')), 'class' => 'addresses-row', 'id' => 'addresses-'. $row),
    );
    $row++;
  }
  if (empty($rows)) {
    $rows[] = array(array('data' => t('No downloads found'), 'colspan' => 5));
  }

  $output  = theme('table', $header, $rows);
  $output .= theme('pager', NULL, UC_FILE_PAGER_SIZE, 0);
  $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)') .
  '<br />'. t('Downloads will not be counted until the file is finished transferring, even though the number may increment when you click.') .
  '<br /><b>'. t('Do not use any download acceleration feature to download the file, or you may lock yourself out of the download.') .'</b>'.
  '</p></div>';
  return $output;
}

/**
 * Table builder for user downloads.
 */
function uc_file_user_downloads($account) {
  // Create a header and the pager it belongs to.
  $header = array(
    array('data' => t('Purchased'  ), 'field' => 'u.granted', 'sort' => 'desc'),
    array('data' => t('Filename'   ), 'field' => 'f.filename'),
    array('data' => t('Description'), 'field' => 'p.description'),
    array('data' => t('Downloads'  ), 'field' => 'u.accessed'),
    array('data' => t('Addresses'  )),
  );

  drupal_set_title(t('File downloads'));

  $files = array();

  $result = pager_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 = %d".
    tablesort_sql($header),
    UC_FILE_PAGER_SIZE,
    0,
    "SELECT COUNT(*) FROM {uc_file_users} WHERE uid = %d",
    $account->uid
  );

  $row = 0;
  while ($file = db_fetch_object($result)) {

    $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
      ),
    );

    // Expiration set to 'never'
    if ($file->expiration == FALSE) {
      $file_link = l(basename($file->filename), 'download/'. $file->fid .'/'. $file->file_key, $onclick);
    }

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

    // Able to be downloaded.
    else {
      $file_link = l(basename($file->filename), 'download/'. $file->fid .'/'. $file->file_key, $onclick) .' ('. t('expires on @date', array('@date' => format_date($file->expiration, 'custom', variable_get('uc_date_format_default', 'm/d/Y')))) .')';
    }

    $files[] = array(
      'granted' => $file->granted,
      'link' => $file_link,
      'description' => $file->description,
      'accessed' => $file->accessed,
      'download_limit' => $file->download_limit,
      'addresses' => $file->addresses,
      'address_limit' => $file->address_limit,
    );
    $row++;
  }

  $output = theme('uc_file_user_downloads', $header, $files);

  if (user_access('administer users')) {
    $output .= drupal_get_form('uc_file_user_form', $account);
  }

  return $output;
}

/**
 * Handles file downloading and error states.
 *
 * @param $fid
 *   The fid of the file specified to download.
 * @param $key
 *   The hash key of a user's download.
 *
 * @see _uc_file_download_validate()
 */
function _uc_file_download($fid, $key) {

  global $user;

  // Error messages for various failed download states.
  $admin_message = t('Please contact the site administrator if this message has been received in error.');
  $error_messages = array(
    UC_FILE_ERROR_NOT_A_FILE              => t('The file you requested does not exist. '),
    UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS => t('You have attempted to download an incorrect file URL too many times. '),
    UC_FILE_ERROR_INVALID_DOWNLOAD        => t("The following URL is not a valid download link. "),
    UC_FILE_ERROR_TOO_MANY_LOCATIONS      => t('You have downloaded this file from too many different locations. '),
    UC_FILE_ERROR_TOO_MANY_DOWNLOADS      => t('You have reached the download limit for this file. '),
    UC_FILE_ERROR_EXPIRED                 => t("This file download has expired. "),
    UC_FILE_ERROR_HOOK_ERROR              => t("A hook denied your access to this file. "),
  );

  $ip = ip_address();
  $file_download = uc_file_get_by_key($key);

  $file_download->full_path = uc_file_qualify_file($file_download->filename);

  // If it's ok, we push the file to the user.
  $status = _uc_file_download_validate($file_download, $user, $ip);
  if ($status == UC_FILE_ERROR_OK) {
    _uc_file_download_transfer($file_download, $ip);
  }

  // Some error state came back, so report it.
  else {
    drupal_set_message($error_messages[$status] . $admin_message, 'error');
  }

  // Kick 'em to the curb. >:)
  _uc_file_download_redirect($user->uid);
}

/**
 * Performs 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 _uc_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 _uc_file_download_validate($file_download, &$user, $ip) {

  $request_cache = cache_get('uc_file_'. $ip);
  $requests = ($request_cache) ? $request_cache->data + 1 : 1;

  $message_user = ($user->uid) ? t('The user %username ', array('%username' => $user->name)) : t('The IP address %ip ', array('%ip' => $ip));

  if ($requests > UC_FILE_REQUEST_LIMIT) {
    return UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS;
  }

  // Must be a valid file.
  if (!$file_download || !is_file($file_download->full_path)) {
    cache_set('uc_file_'. $ip, $requests, 'cache', time() + 86400);
    if ($requests == UC_FILE_REQUEST_LIMIT) {
      watchdog('uc_file', '%username has been temporarily banned from file downloads.', array('%username' => $message_user), WATCHDOG_WARNING);
    }

    return UC_FILE_ERROR_INVALID_DOWNLOAD;
  }

  $addresses = $file_download->addresses;

  // Check the number of locations.
  if (!empty($file_download->address_limit) && !in_array($ip, $addresses) && count($addresses) >= $file_download->address_limit) {
    watchdog('uc_file', '%username has been denied a file download by downloading it from too many IP addresses.', array('%username' => $message_user), WATCHDOG_WARNING);

    return UC_FILE_ERROR_TOO_MANY_LOCATIONS;
  }

  // Check the downloads so far.
  if (!empty($file_download->download_limit) && $file_download->accessed >= $file_download->download_limit) {
    watchdog('uc_file', '%username has been denied a file download by downloading it too many times.', array('%username' => $message_user), WATCHDOG_WARNING);

    return UC_FILE_ERROR_TOO_MANY_DOWNLOADS;
  }

  // Check if it's expired.
  if ($file_download->expiration && time() >= $file_download->expiration) {
    watchdog('uc_file', '%username has been denied an expired file download.', array('%username' => $message_user), WATCHDOG_WARNING);

    return UC_FILE_ERROR_EXPIRED;
  }

  // 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) {
      return UC_FILE_ERROR_HOOK_ERROR;
    }
  }

  // Everything's ok!
  watchdog('uc_file', '%username has started download of the file %filename.', array('%username' => $message_user, '%filename' => basename($file_download->filename)), WATCHDOG_NOTICE);
}

/**
 * Sends the file's binary data to a user via HTTP and updates the
 * uc_file_users table.
 *
 * @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.
 */
function _uc_file_download_transfer($file_user, $ip) {

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

  // This could get clobbered, so make a copy.
  $filename = $file_user->filename;

  // Gather relevant info about the file.
  $size = filesize($file_user->full_path);
  $mimetype = file_get_mimetype($filename);

  // 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', $filename, substr_count($filename, '.') - 1);
  }

  // Check if HTTP_RANGE is sent by browser (or download manager)
  $range = NULL;
  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);
    }
  }

  // Figure out download piece from range (if set)
  if (isset($range)) {
    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));

  // 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: '. gmdate("D, d M Y H:i:s", filemtime($file_user->full_path)) ." GMT");

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

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

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

    // Reset time limit for large files
    set_time_limit(0);

    // Push the data to the client.
    print(fread($fp, UC_FILE_BYTE_SIZE));
    flush();

    // Not sure why ob_flush() is required here, but leaving it out breaks
    // some systems (clicking on link doesn't download anything without it).
    ob_flush();
  }

  // Finished serving the file, close the stream and log the download
  // to the user table.
  fclose($fp);

  _uc_file_log_download($file_user, $ip);

}

/**
 * Processes a file download.
 */
function _uc_file_log_download($file_user, $ip) {

  // Add the address if it doesn't exist.
  $addresses = $file_user->addresses;
  if (!in_array($ip, $addresses)) {
    $addresses[] = $ip;
  }
  $file_user->addresses = $addresses;

  // Accessed again.
  $file_user->accessed++;

  // Calculate hash
  $file_user->file_key = drupal_get_token(serialize($file_user));

  drupal_write_record('uc_file_users', $file_user, 'fuid');
}

/**
 * Send 'em packin.
 */
function _uc_file_download_redirect($uid = NULL) {

  // Shoo away anonymous users.
  if ($uid == 0) {
    drupal_access_denied();
  }
  // Redirect users back to their file page.
  else {
    if (!headers_sent()) {
      drupal_goto('user/'. $uid .'/purchased-files');
    }
  }
}
