format_date($file['granted'], 'custom', variable_get('uc_date_format_default', 'm/d/Y')), 'class' => array('date-row'), 'id' => 'date-' . $row),
array('data' => $file['link'], 'class' => array('filename-row'), 'id' => 'filename-' . $row),
array('data' => $file['description'], 'class' => array('description-row'), 'id' => 'description-' . $row),
array('data' => $file['accessed'] . '/' . ($file['download_limit'] ? $file['download_limit'] : t('Unlimited')), 'class' => array('download-row'), 'id' => 'download-' . $row),
array('data' => count(unserialize($file['addresses'])) . '/' . ($file['address_limit'] ? $file['address_limit'] : t('Unlimited')), 'class' => array('addresses-row'), 'id' => 'addresses-' . $row),
);
$row++;
}
if (empty($rows)) {
$rows[] = array(array('data' => t('No downloads found'), 'colspan' => 5));
}
$output = theme('table', array('header' => $header, 'rows' => $rows));
$output .= theme('pager');
$output .= '
';
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();
$query = db_select('uc_file_users', 'u')->extend('PagerDefault')->extend('TableSort')
->condition('uid', $account->uid)
->orderByHeader($header)
->limit(UC_FILE_PAGER_SIZE);
$query->leftJoin('uc_files', 'f', 'u.fid = f.fid');
$query->leftJoin('uc_file_products', 'p', 'p.pfid = u.pfid');
$query->fields('u', array(
'granted',
'accessed',
'addresses',
'file_key',
'download_limit',
'address_limit',
'expiration',
))
->fields('f', array(
'filename',
'fid',
))
->fields('p', array('description'));
$count_query = db_select('uc_file_users')
->condition('uid', $account->uid);
$count_query->addExpression('COUNT(*)');
$query->setCountQuery($count_query);
$result = $query->execute();
$row = 0;
foreach ($result as $file) {
$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 (REQUEST_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++;
}
return array(
'#theme' => 'uc_file_user_downloads',
'#header' => $header,
'#files' => $files,
);
}
/**
* Handle 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. Calls authorization hooks afterwards.
*
* Called when a user requests a file download, function checks download
* limits then checks for any implementation of hook_uc_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' => format_username($user))) : 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', REQUEST_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 && REQUEST_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_uc_download_authorize() calls deny the download
foreach (module_implements('uc_download_authorize') as $module) {
$name = $module . '_uc_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 update 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)
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();
// Only send partial content header if downloading a piece of the file (IE
// workaround).
if ($seek_start > 0 || $seek_end < ($size - 1)) {
drupal_add_http_header('206 Partial Content');
}
// Standard headers, including content-range and length
drupal_add_http_header('Pragma', 'public');
drupal_add_http_header('Cache-Control', 'cache, must-revalidate');
drupal_add_http_header('Accept-Ranges', 'bytes');
drupal_add_http_header('Content-Range', 'bytes ' . $seek_start . '-' . $seek_end . '/' . $size);
drupal_add_http_header('Content-Type', $mimetype);
drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $filename . '"');
drupal_add_http_header('Content-Length', $seek_end - $seek_start + 1);
// Last-Modified is required for content served dynamically
drupal_add_http_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_add_http_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
drupal_set_time_limit(0);
// Push the data to the client.
print(fread($fp, UC_FILE_BYTE_SIZE));
flush();
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);
}
/**
* Process 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');
}
}
}