uc_file.pages.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <?php
  2. /**
  3. * @file
  4. * File menu items.
  5. */
  6. /**
  7. * The number of bogus requests one IP address can make before being banned.
  8. */
  9. define('UC_FILE_REQUEST_LIMIT', 50);
  10. /**
  11. * Download file chunk.
  12. */
  13. define('UC_FILE_BYTE_SIZE', 8192);
  14. /**
  15. * Download statuses.
  16. */
  17. define('UC_FILE_ERROR_OK' , 0);
  18. define('UC_FILE_ERROR_NOT_A_FILE' , 1);
  19. define('UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS', 2);
  20. define('UC_FILE_ERROR_INVALID_DOWNLOAD' , 3);
  21. define('UC_FILE_ERROR_TOO_MANY_LOCATIONS' , 4);
  22. define('UC_FILE_ERROR_TOO_MANY_DOWNLOADS' , 5);
  23. define('UC_FILE_ERROR_EXPIRED' , 6);
  24. define('UC_FILE_ERROR_HOOK_ERROR' , 7);
  25. /**
  26. * Themes user file downloads page.
  27. *
  28. * @param $variables
  29. * An associative array containing:
  30. * - header: Table header array, in format required by theme_table().
  31. * - files: Associative array of downloadable files, containing:
  32. * - granted: Timestamp of file grant.
  33. * - link: URL of file.
  34. * - description: File name, as it should appear to user after downloading.
  35. * - accessed: Integer number of times file has been downloaded.
  36. * - download_limit: Integer limit on downloads.
  37. * - addresses: Integer number of IP addresses used.
  38. * - address_limit: Integer limit on IP addresses.
  39. *
  40. * @see theme_table()
  41. * @ingroup themeable
  42. */
  43. function theme_uc_file_user_downloads($variables) {
  44. $header = $variables['header'];
  45. $files = $variables['files'];
  46. $rows = array();
  47. $row = 0;
  48. foreach ($files as $file) {
  49. $rows[] = array(
  50. array('data' => format_date($file['granted'], 'uc_store'), 'class' => array('date-row'), 'id' => 'date-' . $row),
  51. array('data' => $file['link'], 'class' => array('filename-row'), 'id' => 'filename-' . $row),
  52. array('data' => $file['description'], 'class' => array('description-row'), 'id' => 'description-' . $row),
  53. array('data' => $file['accessed'] . '/' . ($file['download_limit'] ? $file['download_limit'] : t('Unlimited')), 'class' => array('download-row'), 'id' => 'download-' . $row),
  54. array('data' => count(unserialize($file['addresses'])) . '/' . ($file['address_limit'] ? $file['address_limit'] : t('Unlimited')), 'class' => array('addresses-row'), 'id' => 'addresses-' . $row),
  55. );
  56. $row++;
  57. }
  58. $output = theme('table', array(
  59. 'header' => $header,
  60. 'rows' => $rows,
  61. 'empty' => t('No downloads found'),
  62. ));
  63. $output .= theme('pager');
  64. $output .= '<div class="form-item"><p class="description">' .
  65. t('Once your download is finished, you must refresh the page to download again. (Provided you have permission)') .
  66. '<br />' . t('Downloads will not be counted until the file is finished transferring, even though the number may increment when you click.') .
  67. '<br /><b>' . t('Do not use any download acceleration feature to download the file, or you may lock yourself out of the download.') . '</b>' .
  68. '</p></div>';
  69. return $output;
  70. }
  71. /**
  72. * Table builder for user downloads.
  73. */
  74. function uc_file_user_downloads($account) {
  75. // Create a header and the pager it belongs to.
  76. $header = array(
  77. array('data' => t('Purchased' ), 'field' => 'u.granted', 'sort' => 'desc'),
  78. array('data' => t('Filename' ), 'field' => 'f.filename'),
  79. array('data' => t('Description'), 'field' => 'p.description'),
  80. array('data' => t('Downloads' ), 'field' => 'u.accessed'),
  81. array('data' => t('Addresses' )),
  82. );
  83. drupal_set_title(t('File downloads'));
  84. $files = array();
  85. $query = db_select('uc_file_users', 'u')->extend('PagerDefault')->extend('TableSort')
  86. ->condition('uid', $account->uid)
  87. ->orderByHeader($header)
  88. ->limit(UC_FILE_PAGER_SIZE);
  89. $query->leftJoin('uc_files', 'f', 'u.fid = f.fid');
  90. $query->leftJoin('uc_file_products', 'p', 'p.pfid = u.pfid');
  91. $query->fields('u', array(
  92. 'granted',
  93. 'accessed',
  94. 'addresses',
  95. 'file_key',
  96. 'download_limit',
  97. 'address_limit',
  98. 'expiration',
  99. ))
  100. ->fields('f', array(
  101. 'filename',
  102. 'fid',
  103. ))
  104. ->fields('p', array('description'));
  105. $count_query = db_select('uc_file_users')
  106. ->condition('uid', $account->uid);
  107. $count_query->addExpression('COUNT(*)');
  108. $query->setCountQuery($count_query);
  109. $result = $query->execute();
  110. $row = 0;
  111. foreach ($result as $file) {
  112. $download_limit = $file->download_limit;
  113. // Set the JS behavior when this link gets clicked.
  114. $onclick = array(
  115. 'attributes' => array(
  116. 'onclick' => 'uc_file_update_download(' . $row . ', ' . $file->accessed . ', ' . ((empty($download_limit)) ? -1 : $download_limit) . ');', 'id' => 'link-' . $row
  117. ),
  118. );
  119. // Expiration set to 'never'
  120. if ($file->expiration == FALSE) {
  121. $file_link = l(basename($file->filename), 'download/' . $file->fid, $onclick);
  122. }
  123. // Expired.
  124. elseif (REQUEST_TIME > $file->expiration) {
  125. $file_link = basename($file->filename);
  126. }
  127. // Able to be downloaded.
  128. else {
  129. $file_link = l(basename($file->filename), 'download/' . $file->fid, $onclick) . ' (' . t('expires on @date', array('@date' => format_date($file->expiration, 'uc_store'))) . ')';
  130. }
  131. $files[] = array(
  132. 'granted' => $file->granted,
  133. 'link' => $file_link,
  134. 'description' => $file->description,
  135. 'accessed' => $file->accessed,
  136. 'download_limit' => $file->download_limit,
  137. 'addresses' => $file->addresses,
  138. 'address_limit' => $file->address_limit,
  139. );
  140. $row++;
  141. }
  142. $build['downloads'] = array(
  143. '#theme' => 'uc_file_user_downloads',
  144. '#header' => $header,
  145. '#files' => $files,
  146. );
  147. if (user_access('administer users')) {
  148. $build['admin'] = drupal_get_form('uc_file_user_form', $account);
  149. }
  150. return $build;
  151. }
  152. /**
  153. * Handles file downloading and error states.
  154. *
  155. * @param $fid
  156. * The fid of the file specified to download.
  157. * @param $key
  158. * The hash key of a user's download.
  159. *
  160. * @see _uc_file_download_validate()
  161. */
  162. function _uc_file_download($fid) {
  163. global $user;
  164. // Error messages for various failed download states.
  165. $admin_message = t('Please contact the site administrator if this message has been received in error.');
  166. $error_messages = array(
  167. UC_FILE_ERROR_NOT_A_FILE => t('The file you requested does not exist.'),
  168. UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS => t('You have attempted to download an incorrect file URL too many times.'),
  169. UC_FILE_ERROR_INVALID_DOWNLOAD => t('The following URL is not a valid download link.') . ' ',
  170. UC_FILE_ERROR_TOO_MANY_LOCATIONS => t('You have downloaded this file from too many different locations.'),
  171. UC_FILE_ERROR_TOO_MANY_DOWNLOADS => t('You have reached the download limit for this file.'),
  172. UC_FILE_ERROR_EXPIRED => t('This file download has expired.') . ' ',
  173. UC_FILE_ERROR_HOOK_ERROR => t('A hook denied your access to this file.') . ' ',
  174. );
  175. $ip = ip_address();
  176. if (user_access('view all downloads')) {
  177. $file_download = uc_file_get_by_id($fid);
  178. }
  179. else {
  180. $file_download = uc_file_get_by_uid($user->uid, $fid);
  181. }
  182. if (isset($file_download->filename)) {
  183. $file_download->full_path = uc_file_qualify_file($file_download->filename);
  184. }
  185. else {
  186. return MENU_ACCESS_DENIED;
  187. }
  188. // If it's ok, we push the file to the user.
  189. $status = UC_FILE_ERROR_OK;
  190. if (!user_access('view all downloads')) {
  191. $status = _uc_file_download_validate($file_download, $user, $ip);
  192. }
  193. if ($status == UC_FILE_ERROR_OK) {
  194. _uc_file_download_transfer($file_download, $ip);
  195. }
  196. // Some error state came back, so report it.
  197. else {
  198. drupal_set_message($error_messages[$status] . $admin_message, 'error');
  199. // Kick 'em to the curb. >:)
  200. _uc_file_download_redirect($user->uid);
  201. }
  202. drupal_exit();
  203. }
  204. /**
  205. * Performs first-pass authorization. Calls authorization hooks afterwards.
  206. *
  207. * Called when a user requests a file download, function checks download
  208. * limits then checks for any implementation of hook_uc_download_authorize().
  209. * Passing that, the function _uc_file_download_transfer() is called.
  210. *
  211. * @param $fid
  212. * The fid of the file specified to download.
  213. * @param $key
  214. * The hash key of a user's download.
  215. */
  216. function _uc_file_download_validate($file_download, &$user, $ip) {
  217. $request_cache = cache_get('uc_file_' . $ip);
  218. $requests = ($request_cache) ? $request_cache->data + 1 : 1;
  219. $message_user = ($user->uid) ? t('The user %username', array('%username' => format_username($user))) : t('The IP address %ip', array('%ip' => $ip));
  220. if ($requests > UC_FILE_REQUEST_LIMIT) {
  221. return UC_FILE_ERROR_TOO_MANY_BOGUS_REQUESTS;
  222. }
  223. // Must be a valid file.
  224. if (!$file_download || !is_readable($file_download->full_path)) {
  225. cache_set('uc_file_' . $ip, $requests, 'cache', REQUEST_TIME + 86400);
  226. if ($requests == UC_FILE_REQUEST_LIMIT) {
  227. // $message_user has already been passed through check_plain()
  228. watchdog('uc_file', '!username has been temporarily banned from file downloads.', array('!username' => $message_user), WATCHDOG_WARNING);
  229. }
  230. return UC_FILE_ERROR_INVALID_DOWNLOAD;
  231. }
  232. $addresses = $file_download->addresses;
  233. // Check the number of locations.
  234. if (!empty($file_download->address_limit) && !in_array($ip, $addresses) && count($addresses) >= $file_download->address_limit) {
  235. // $message_user has already been passed through check_plain()
  236. watchdog('uc_file', '!username has been denied a file download by downloading it from too many IP addresses.', array('!username' => $message_user), WATCHDOG_WARNING);
  237. return UC_FILE_ERROR_TOO_MANY_LOCATIONS;
  238. }
  239. // Check the downloads so far.
  240. if (!empty($file_download->download_limit) && $file_download->accessed >= $file_download->download_limit) {
  241. // $message_user has already been passed through check_plain()
  242. watchdog('uc_file', '!username has been denied a file download by downloading it too many times.', array('!username' => $message_user), WATCHDOG_WARNING);
  243. return UC_FILE_ERROR_TOO_MANY_DOWNLOADS;
  244. }
  245. // Check if it's expired.
  246. if ($file_download->expiration && REQUEST_TIME >= $file_download->expiration) {
  247. // $message_user has already been passed through check_plain()
  248. watchdog('uc_file', '!username has been denied an expired file download.', array('!username' => $message_user), WATCHDOG_WARNING);
  249. return UC_FILE_ERROR_EXPIRED;
  250. }
  251. // Check any if any hook_uc_download_authorize() calls deny the download
  252. foreach (module_implements('uc_download_authorize') as $module) {
  253. $name = $module . '_uc_download_authorize';
  254. $result = $name($user, $file_download);
  255. if (!$result) {
  256. return UC_FILE_ERROR_HOOK_ERROR;
  257. }
  258. }
  259. // Everything's ok!
  260. // $message_user has already been passed through check_plain()
  261. watchdog('uc_file', '!username has started download of the file %filename.', array('!username' => $message_user, '%filename' => basename($file_download->filename)), WATCHDOG_NOTICE);
  262. }
  263. /**
  264. * Sends the file's binary data to a user via HTTP and updates the database.
  265. *
  266. * @param $file_user
  267. * The file_user object from the uc_file_users.
  268. * @param $ip
  269. * The string containing the IP address the download is going to.
  270. */
  271. function _uc_file_download_transfer($file_user, $ip) {
  272. // Check if any hook_uc_file_transfer_alter() calls alter the download.
  273. foreach (module_implements('uc_file_transfer_alter') as $module) {
  274. $name = $module . '_uc_file_transfer_alter';
  275. $file_user->full_path = $name($file_user, $ip, $file_user->fid, $file_user->full_path);
  276. }
  277. // This could get clobbered, so make a copy.
  278. $filename = $file_user->filename;
  279. // Gather relevant info about the file.
  280. $size = filesize($file_user->full_path);
  281. $mimetype = file_get_mimetype($filename);
  282. // Workaround for IE filename bug with multiple periods / multiple dots
  283. // in filename that adds square brackets to filename -
  284. // eg. setup.abc.exe becomes setup[1].abc.exe
  285. if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
  286. $filename = preg_replace('/\./', '%2e', $filename, substr_count($filename, '.') - 1);
  287. }
  288. // Check if HTTP_RANGE is sent by browser (or download manager)
  289. $range = NULL;
  290. if (isset($_SERVER['HTTP_RANGE'])) {
  291. if (substr($_SERVER['HTTP_RANGE'], 0, 6) == 'bytes=') {
  292. // Multiple ranges could be specified at the same time,
  293. // but for simplicity only serve the first range
  294. // See http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
  295. list($range, $extra_ranges) = explode(',', substr($_SERVER['HTTP_RANGE'], 6), 2);
  296. }
  297. else {
  298. drupal_add_http_header('Status', '416 Requested Range Not Satisfiable');
  299. drupal_add_http_header('Content-Range', 'bytes */' . $size);
  300. exit;
  301. }
  302. }
  303. // Figure out download piece from range (if set)
  304. if (isset($range)) {
  305. list($seek_start, $seek_end) = explode('-', $range, 2);
  306. }
  307. // Set start and end based on range (if set),
  308. // else set defaults and check for invalid ranges.
  309. $seek_end = intval((empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)), ($size - 1)));
  310. $seek_start = intval((empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)), 0));
  311. // Only send partial content header if downloading a piece of the file (IE
  312. // workaround).
  313. if ($seek_start > 0 || $seek_end < ($size - 1)) {
  314. drupal_add_http_header('Status', '206 Partial Content');
  315. }
  316. // Standard headers, including content-range and length
  317. drupal_add_http_header('Pragma', 'public');
  318. drupal_add_http_header('Cache-Control', 'cache, must-revalidate');
  319. drupal_add_http_header('Accept-Ranges', 'bytes');
  320. drupal_add_http_header('Content-Range', 'bytes ' . $seek_start . '-' . $seek_end . '/' . $size);
  321. drupal_add_http_header('Content-Type', $mimetype);
  322. drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $filename . '"');
  323. drupal_add_http_header('Content-Length', $seek_end - $seek_start + 1);
  324. // Last-Modified is required for content served dynamically
  325. drupal_add_http_header('Last-Modified', gmdate("D, d M Y H:i:s", filemtime($file_user->full_path)) . " GMT");
  326. // Etag header is required for Firefox3 and other managers
  327. drupal_add_http_header('ETag', md5($file_user->full_path));
  328. // Open the file and seek to starting byte
  329. $fp = fopen($file_user->full_path, 'rb');
  330. fseek($fp, $seek_start);
  331. // Start buffered download
  332. while (!feof($fp)) {
  333. // Reset time limit for large files
  334. drupal_set_time_limit(0);
  335. // Push the data to the client.
  336. print(fread($fp, UC_FILE_BYTE_SIZE));
  337. flush();
  338. // Suppress PHP notice that occurs when output buffering isn't enabled.
  339. // The ob_flush() is needed because if output buffering *is* enabled,
  340. // clicking on the file download link won't download anything if the buffer
  341. // isn't flushed.
  342. @ob_flush();
  343. }
  344. // Finished serving the file, close the stream and log the download
  345. // to the user table.
  346. fclose($fp);
  347. _uc_file_log_download($file_user, $ip);
  348. }
  349. /**
  350. * Processes a file download.
  351. */
  352. function _uc_file_log_download($file_user, $ip) {
  353. // Add the address if it doesn't exist.
  354. $addresses = $file_user->addresses;
  355. if (!in_array($ip, $addresses)) {
  356. $addresses[] = $ip;
  357. }
  358. $file_user->addresses = $addresses;
  359. // Accessed again.
  360. $file_user->accessed++;
  361. // Calculate hash
  362. $file_user->file_key = drupal_get_token(serialize($file_user));
  363. drupal_write_record('uc_file_users', $file_user, 'fuid');
  364. }
  365. /**
  366. * Send 'em packin.
  367. */
  368. function _uc_file_download_redirect($uid = NULL) {
  369. // Shoo away anonymous users.
  370. if ($uid == 0) {
  371. drupal_access_denied();
  372. }
  373. // Redirect users back to their file page.
  374. else {
  375. if (!headers_sent()) {
  376. drupal_goto('user/' . $uid . '/purchased-files');
  377. }
  378. }
  379. }