uc_file.pages.inc 16 KB

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