uc_file.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/ubercart/uc_file/uc_file.module
    2. 6 contributions/ubercart/uc_file/uc_file.module
    3. 7 contributions/ubercart/uc_file/uc_file.module

Allows products to be associated with downloadable files.

uc_file allows ubercart products to have associated downloadable files. Optionally, after a customer purchases such a product they will be sent a download link via email. Additionally, after logging on a customer can download files via their account page. Optionally, an admininstrator can set restrictions on how and when files are downloaded.

Development sponsored by the Ubercart project. http://www.ubercart.org

Functions & methods

NameDescription
theme_uc_file_downloads_tokenTheme file download links token
theme_uc_file_files_formImplementation of theme_form($form)
uc_file_cart_itemImplementation of hook_cart_item().
uc_file_feature_deleteproduct_feature delete function
uc_file_feature_formForm builder for hook_product_feature
uc_file_feature_form_submit
uc_file_feature_form_validate
uc_file_feature_settingsForm builder for file settings
uc_file_feature_settings_submit
uc_file_feature_settings_validate
uc_file_files_adminPage builder for file products admin
uc_file_files_formForm builder for file products admin
uc_file_files_form_submit
uc_file_files_form_validate
uc_file_files_tableTable builder for file products admin
uc_file_form_alterImplementation of hook_form_alter().
uc_file_menuImplementation of hook_menu().
uc_file_notify_settingsForm builder for file download notification settings.
uc_file_orderImplementation of hook_order().
uc_file_permImplementation of hook_perm().
uc_file_product_featureImplementation of hook_product_feature().
uc_file_store_statusImplementation of hook_store_status().
uc_file_token_listImplementation of hook_token_list().
uc_file_token_valuesImplementation of hook_token_values().
uc_file_uc_messageImplementation of hook_uc_message().
uc_file_userImplementation of hook_user().
uc_file_user_downloadsTable builder for user downloads
_autocomplete_filenameImplement Drupal autocomplete textfield
_email_file_downloadEmail a user with download links for a product file download
_file_downloadPerform first-pass authorization. Call authorization hooks afterwards.
_file_download_denyDeny a file download
_file_download_transferSend the file's binary data to a user via HTTP and update the uc_file_users table.
_file_expiration_dateReturn a file expiration date given a purchase date
_file_table_actionPerform a specified action on the uc_files table
_generate_hashGenerate hash used for unique download URLs
_get_adjustment_modelsReturn a list of model adjustments for a given product node
_get_dir_file_idsReturn a list of file ids that are in the directory
_group_filenamesGroup filenames by an attribute while maintaining order.
_sort_fidsTake a list of file ids and sorts the list to where directories are list last and by order of descending depth
_uc_file_ip_address
_user_table_actionPerform a specified action on the uc_file_users table

Constants

NameDescription
UC_FILE_BYTE_SIZE
UC_FILE_PAGER_SIZE
UC_FILE_REQUEST_LIMIT

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Allows products to be associated with downloadable files.
  5. *
  6. * uc_file allows ubercart products to have associated downloadable files.
  7. * Optionally, after a customer purchases such a product they will be sent a
  8. * download link via email. Additionally, after logging on a customer can
  9. * download files via their account page. Optionally, an admininstrator can set
  10. * restrictions on how and when files are downloaded.
  11. *
  12. * Development sponsored by the Ubercart project. http://www.ubercart.org
  13. */
  14. define('UC_FILE_PAGER_SIZE', 50);
  15. define('UC_FILE_REQUEST_LIMIT', 50);
  16. define('UC_FILE_BYTE_SIZE', 1024);
  17. /* *****************************************************************************
  18. * Hook Functions (Drupal) *
  19. * ****************************************************************************/
  20. /**
  21. * Implementation of hook_form_alter().
  22. */
  23. function uc_file_form_alter($form_id, &$form) {
  24. if ($form_id == "uc_product_feature_settings_form") {
  25. $form['#submit']['uc_file_feature_settings_submit'] = array();
  26. $form['#validate']['uc_file_feature_settings_validate'] = array();
  27. }
  28. }
  29. /**
  30. * Implementation of hook_menu().
  31. */
  32. function uc_file_menu($may_cache) {
  33. global $user;
  34. $items = array();
  35. if ($may_cache) {
  36. $items[] = array(
  37. 'path' => '_autocomplete_file',
  38. 'callback' => '_autocomplete_filename',
  39. 'access' => user_access('administer product features'),
  40. 'type' => MENU_CALLBACK,
  41. );
  42. $items[] = array(
  43. 'path' => 'admin/store/products/files',
  44. 'callback' => 'uc_file_files_admin',
  45. 'title' => t('View file downloads'),
  46. 'description' => t('View all file download features on products.'),
  47. 'access' => user_access('administer products'),
  48. 'type' => MENU_NORMAL_ITEM,
  49. );
  50. }
  51. else {
  52. if (module_exists('uc_notify')) {
  53. $items[] = array(
  54. 'path' => 'admin/store/settings/notify/edit/file',
  55. 'title' => t('File download'),
  56. 'access' => user_access('administer store'),
  57. 'callback' => 'drupal_get_form',
  58. 'callback arguments' => array('uc_file_notify_settings'),
  59. 'description' => t('Edit the notification settings for purchased file downloads.'),
  60. 'type' => MENU_LOCAL_TASK,
  61. );
  62. }
  63. $items[] = array(
  64. 'path' => 'user/'. arg(1) .'/files',
  65. 'title' => t('Files'),
  66. 'description' => t('View your purchased files.'),
  67. 'callback' => 'uc_file_user_downloads',
  68. 'callback arguments' => array(arg(1)),
  69. 'access' => (user_access('view all downloads') || $user->uid == arg(1)) && $user->uid,
  70. 'type' => MENU_LOCAL_TASK,
  71. );
  72. $items[] = array(
  73. 'path' => 'download/'. arg(1) .'/'. arg(2),
  74. 'callback' => '_file_download',
  75. 'callback arguments' => array(arg(1), arg(2)),
  76. 'access' => user_access('download file'),
  77. 'type' => MENU_CALLBACK,
  78. );
  79. drupal_add_css(drupal_get_path('module', 'uc_file') .'/uc_file.css');
  80. }
  81. return $items;
  82. }
  83. /**
  84. * Implementation of hook_perm().
  85. */
  86. function uc_file_perm() {
  87. return array('download file', 'view all downloads');
  88. }
  89. /**
  90. * Implementation of hook_user().
  91. */
  92. function uc_file_user($op, &$edit, &$account, $category = NULL) {
  93. global $user;
  94. switch ($op) {
  95. case 'delete':
  96. _user_table_action('remove', NULL, $account->uid);
  97. break;
  98. case 'form':
  99. if (user_access('administer users') && (is_null($category) || $category == 'account')) {
  100. $file_downloads = db_query("SELECT * FROM {uc_file_users} as u INNER JOIN {uc_files} as f ON u.fid = f.fid WHERE uid = %d", $account->uid);
  101. $files = db_query("SELECT * FROM {uc_files} ORDER BY filename ASC");
  102. $available_downloads = array();
  103. $available_files = array();
  104. while ($file_download = db_fetch_object($file_downloads)) {
  105. $available_downloads[$file_download->file_key] = $file_download->filename;
  106. }
  107. while ($file = db_fetch_object($files)) {
  108. if (substr($file->filename, -1) != '/' && substr($file->filename, -1) != '\\') {
  109. $available_files[$file->fid] = $file->filename;
  110. }
  111. }
  112. $form['file'] = array(
  113. '#type' => 'fieldset',
  114. '#title' => t('File downloads'),
  115. '#collapsible' => TRUE,
  116. '#collapsed' => TRUE,
  117. '#weight' => 10
  118. );
  119. $form['file']['remove_file'] = array(
  120. '#type' => 'select',
  121. '#title' => t('Remove file'),
  122. '#multiple' => TRUE,
  123. '#description' => t('Select a file to remove as a download. Hold Ctrl to select or unselect multiple files.'),
  124. '#options' => $available_downloads,
  125. );
  126. $form['file']['add_file'] = array(
  127. '#type' => 'select',
  128. '#title' => t('Add file'),
  129. '#multiple' => TRUE,
  130. '#description' => t('Select a file to add as a download. Hold Ctrl to select or unselect multiple files.'),
  131. '#options' => $available_files,
  132. );
  133. }
  134. return $form;
  135. break;
  136. case 'submit':
  137. if (!empty($edit['remove_file'])) {
  138. foreach ($edit['remove_file'] as $hash_key) {
  139. if (!is_null($hash_key)) {
  140. _user_table_action('remove', NULL, $account->uid, $hash_key);
  141. }
  142. }
  143. }
  144. if (!empty($edit['add_file'])) {
  145. foreach ($edit['add_file'] as $fid) {
  146. $pfid = db_result(db_query("SELECT pfid FROM {uc_file_products} WHERE fid = %d", $fid));
  147. _user_table_action('allow', $fid, $account->uid, $pfid);
  148. }
  149. }
  150. break;
  151. case 'view':
  152. $existing_download = db_result(db_query("SELECT fid FROM {uc_file_users} WHERE uid = %d", $account->uid));
  153. if ((user_access('view all downloads') || $user->uid == $account->uid) && $user->uid && $existing_download) {
  154. $items['uc_file_download'] = array(
  155. 'value' => l(t('Click here to view your file downloads.'), 'user/'. $account->uid .'/files'),
  156. 'class' => 'member',
  157. );
  158. return array(t('File downloads') => $items);
  159. }
  160. break;
  161. default:
  162. break;
  163. }
  164. }
  165. /* **************************************************************************** *
  166. * Übercart Hooks *
  167. * **************************************************************************** */
  168. /**
  169. * Implementation of hook_cart_item().
  170. */
  171. function uc_file_cart_item($op, &$item) {
  172. switch ($op) {
  173. case 'can_ship':
  174. $files = db_query("SELECT shippable, model FROM {uc_file_products} as fp INNER JOIN {uc_product_features} as pf ON pf.pfid = fp.pfid WHERE nid = %d", $item->nid);
  175. while ($file = db_fetch_object($files)) {
  176. $sku = (empty($item->data['model'])) ? $item->model : $item->data['model'];
  177. if ($sku == $file->model || empty($file->model)) {
  178. return ($file->shippable) ? TRUE : FALSE;
  179. }
  180. }
  181. break;
  182. }
  183. }
  184. /**
  185. * Implementation of hook_order().
  186. */
  187. function uc_file_order($op, $order, $status) {
  188. global $user;
  189. switch ($op) {
  190. case 'update':
  191. // Only process file downloads when the order is being updated to the
  192. // correct status, the status is actually being changed, and a valid user
  193. // has been assigned to the order.
  194. if ($status == variable_get('uc_file_default_order_status', 'completed') &&
  195. $order->order_status != $status &&
  196. $order->uid > 0 &&
  197. ($order_user = user_load(array('uid' => $order->uid))) !== FALSE) {
  198. foreach ($order->products as $product) {
  199. $files = db_query("SELECT fp.fid, fp.pfid, model, f.filename FROM {uc_file_products} AS fp INNER JOIN {uc_product_features} AS pf ON pf.pfid = fp.pfid INNER JOIN {uc_files} as f ON f.fid = fp.fid WHERE nid = %d", $product->nid);
  200. while ($file = db_fetch_object($files)) {
  201. if (($file->model == $product->model || empty($file->model))) {
  202. $downloads = _user_table_action('allow', $file->fid, $order_user->uid, $file->pfid);
  203. $user_downloads = (!empty($user_downloads)) ? array_merge($user_downloads, $downloads) : $downloads ;
  204. if (_get_dir_file_ids($file->fid)) {
  205. $comment = t('User can now download files in the directory %dir.', array('%dir' => $file->filename));
  206. }
  207. else {
  208. $comment = t('User can now download the file %file.', array('%file' => basename($file->filename)));
  209. }
  210. uc_order_comment_save($order->order_id, $user->uid, $comment);
  211. }
  212. }
  213. }
  214. if (!is_null($user_downloads)) {
  215. _email_file_download($order_user, $order, $user_downloads);
  216. }
  217. }
  218. break;
  219. default:
  220. break;
  221. }
  222. }
  223. /**
  224. * Implementation of hook_product_feature().
  225. */
  226. function uc_file_product_feature() {
  227. $features[] = array(
  228. 'id' => 'file',
  229. 'title' => t('File download'),
  230. 'callback' => 'uc_file_feature_form',
  231. 'delete' => 'uc_file_feature_delete',
  232. 'settings' => 'uc_file_feature_settings',
  233. );
  234. return $features;
  235. }
  236. /**
  237. * Implementation of hook_store_status().
  238. */
  239. function uc_file_store_status() {
  240. $message = array();
  241. if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
  242. $message[] = array(
  243. 'status' => 'warning',
  244. 'title' => t('File Downloads'),
  245. 'desc' => t('The file downloads directory is not valid or set. Set a valid directory in the <a href="!url">product feature settings</a> under the file download settings fieldset.', array('!url' => url('admin/store/settings/products/edit/features'))),
  246. );
  247. }
  248. else {
  249. $message[] = array(
  250. 'status' => 'ok',
  251. 'title' => t('File Downloads'),
  252. 'desc' => t('The file downloads directory has been set and is working.'),
  253. );
  254. }
  255. return $message;
  256. }
  257. /**
  258. * Implementation of hook_token_list().
  259. */
  260. function uc_file_token_list($type = 'all') {
  261. if ($type == 'uc_file' || $type == 'ubercart' || $type == 'all') {
  262. $tokens['uc_file']['file-downloads'] = t('The list of file download links (if any) associated with an order');
  263. }
  264. return $tokens;
  265. }
  266. /**
  267. * Implementation of hook_token_values().
  268. */
  269. function uc_file_token_values($type, $object = NULL) {
  270. switch ($type) {
  271. case 'uc_file':
  272. if (!empty($object)) {
  273. $values['file-downloads'] = theme('uc_file_downloads_token', $object);
  274. }
  275. break;
  276. }
  277. return $values;
  278. }
  279. /**
  280. * Implementation of hook_uc_message().
  281. */
  282. function uc_file_uc_message() {
  283. $messages['uc_file_download_subject'] = t("File Downloads for Order [order-id]");
  284. $messages['uc_file_download_message'] = t("Your order [order-link] at [store-name] included file download(s). You may access them with the following link(s):\n\n[file-downloads]\n\nAfter downloading these files these links will have expired. If you need to download the files again, you can login at [site-login] and visit the \"My Account\" section of the site.\n\nThanks again, \n\n[store-name]\n[site-slogan]");
  285. return $messages;
  286. }
  287. /* **************************************************************************** *
  288. * Callback Functions, Forms, and Tables *
  289. * **************************************************************************** */
  290. /**
  291. * Theme file download links token
  292. */
  293. function theme_uc_file_downloads_token($file_downloads) {
  294. $output = '';
  295. foreach ($file_downloads as $file_download) {
  296. $filename = basename(db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $file_download->fid)));
  297. $download_url = url('download/'. $file_download->fid .'/'. $file_download->file_key, NULL, NULL, TRUE);
  298. $output .= '<a href="'. $download_url .'">'. $download_url ."</a>\n";
  299. }
  300. return $output;
  301. }
  302. /**
  303. * product_feature delete function
  304. */
  305. function uc_file_feature_delete($feature) {
  306. db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $feature['pfid']);
  307. }
  308. /**
  309. * Form builder for hook_product_feature
  310. */
  311. function uc_file_feature_form($node, $feature) {
  312. if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
  313. drupal_set_message(t('A file directory needs to be configured in <a href="!url">product feature settings</a> before a file can be selected.', array('!url' => url('admin/store/settings/products/edit/features'))), 'error');
  314. }
  315. _file_table_action('insert');
  316. $models = (!_get_adjustment_models($node->nid)) ? array(NULL => t('Any'), $node->model => $node->model) : array(NULL => t('Any'), $node->model => $node->model) + _get_adjustment_models($node->nid);
  317. if (!empty($feature)) {
  318. $file_product = db_fetch_object(db_query("SELECT * FROM {uc_file_products} as p LEFT JOIN {uc_files} as f ON p.fid = f.fid WHERE pfid = %d", $feature['pfid']));
  319. $default_feature = $feature['pfid'];
  320. $default_model = $file_product->model;
  321. $default_filename = $file_product->filename;
  322. $default_description = $file_product->description;
  323. $default_shippable = $file_product->shippable;
  324. }
  325. else {
  326. $default_shippable = $node->shippable;
  327. }
  328. $form['nid'] = array(
  329. '#type' => 'value',
  330. '#value' => $node->nid,
  331. );
  332. $form['pfid'] = array(
  333. '#type' => 'value',
  334. '#value' => $default_feature,
  335. );
  336. $form['uc_file_model'] = array(
  337. '#type' => 'select',
  338. '#title' => t('Model/SKU'),
  339. '#default_value' => $default_model,
  340. '#description' => t('This is the model/SKU that will need to be purchased to obtain the file download.'),
  341. '#options' => $models,
  342. );
  343. $form['uc_file_filename'] = array(
  344. '#type' => 'textfield',
  345. '#title' => t('File download'),
  346. '#default_value' => $default_filename,
  347. '#autocomplete_path' => '_autocomplete_file',
  348. '#description' => t('The file that can be downloaded when product is purchased (enter a path relative to the %dir directory).', array('%dir' => variable_get('uc_file_base_dir', NULL))),
  349. '#maxlength' => 255,
  350. );
  351. $form['uc_file_description'] = array(
  352. '#type' => 'textfield',
  353. '#title' => t('Description'),
  354. '#default_value' => $default_description,
  355. '#maxlength' => 255,
  356. '#description' => t('A description of the download associated with the product.'),
  357. );
  358. $form['uc_file_shippable'] = array(
  359. '#type' => 'checkbox',
  360. '#title' => t('Shippable product'),
  361. '#default_value' => $default_shippable,
  362. '#description' => t('Check if this product model/SKU file download is also associated with a shippable product.'),
  363. );
  364. return uc_product_feature_form($form);
  365. }
  366. function uc_file_feature_form_validate($form_id, $form_values) {
  367. if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $form_values['uc_file_filename']))) {
  368. form_set_error('uc_file_filename', t('%file is not a valid file or directory inside file download directory.', array('%file' => $form_values['uc_file_filename'])));
  369. }
  370. }
  371. function uc_file_feature_form_submit($form_id, $form_values) {
  372. global $user;
  373. $fid = db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $form_values['uc_file_filename']));
  374. //Build product feature descriptions
  375. $description = (empty($form_values['uc_file_model'])) ? t('<strong>SKU:</strong> Any<br />') : t('<strong>SKU:</strong> !sku<br />', array('!sku' => $form_values['uc_file_model']));
  376. if (is_dir(variable_get('uc_file_base_dir', NULL) ."/". $form_values['uc_file_filename'])) {
  377. $description .= t('<strong>Directory:</strong> !dir<br />', array('!dir' => $form_values['uc_file_filename']));
  378. }
  379. else {
  380. $description .= t('<strong>File:</strong> !file<br />', array('!file' => basename($form_values['uc_file_filename'])));;
  381. }
  382. $shippable = ($form_values['uc_file_shippable']) ? 1 : 0;
  383. $description .= ($shippable) ? t('<strong>Shippable:</strong> Yes') : t('<strong>Shippable:</strong> No');
  384. //Insert or update uc_file_product table
  385. if (empty($form_values['pfid'])) {
  386. $pfid = db_next_id('{uc_product_features}_pfid');
  387. }
  388. else {
  389. $pfid = $form_values['pfid'];
  390. db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $pfid);
  391. }
  392. switch ($GLOBALS['db_type']) {
  393. case 'mysqli':
  394. case 'mysql':
  395. db_query("INSERT INTO {uc_file_products} (pfid, fid, model, description, shippable) VALUES (%d, %d, '%s', '%s', %d)", $pfid, $fid, $form_values['uc_file_model'], $form_values['uc_file_description'], $shippable);
  396. break;
  397. case 'pgsql':
  398. db_query("INSERT INTO {uc_file_products} (pfid, fid, model, description, shippable) VALUES (%d, %d, '%s', '%s', '%d')", $pfid, $fid, $form_values['uc_file_model'], $form_values['uc_file_description'], $shippable);
  399. break;
  400. }
  401. $data = array(
  402. 'pfid' => $pfid,
  403. 'nid' => $form_values['nid'],
  404. 'fid' => 'file',
  405. 'description' => $description,
  406. );
  407. return uc_product_feature_save($data);
  408. }
  409. /**
  410. * Form builder for file settings
  411. */
  412. function uc_file_feature_settings() {
  413. uc_add_js('$(document).ready(function() { if ($("#edit-uc-file-download-limit-duration-granularity").val() == "never") {$("#edit-uc-file-download-limit-duration-qty").attr("disabled", "disabled").val("");} });', 'inline');
  414. $statuses = array();
  415. foreach (uc_order_status_list('general') as $status) {
  416. $statuses[$status['id']] = $status['title'];
  417. }
  418. $form['uc_file_default_order_status'] = array(
  419. '#type' => 'select',
  420. '#title' => t('Order status'),
  421. '#default_value' => variable_get('uc_file_default_order_status', 'completed'),
  422. '#description' => t('Where in the order status the user will be given the file download. Be aware that if payments are processed automatically, this happens before anonymous customers have an account created. This order status should not be reached before the user account exists.'),
  423. '#options' => $statuses,
  424. );
  425. $form['uc_file_base_dir'] = array(
  426. '#type' => 'textfield',
  427. '#title' => t('Files path'),
  428. '#description' => t('The absolute path (or relative to Drupal root) where files used for file downloads are located. For security reasons, it is reccommended to choose a path outside the web root.'),
  429. '#default_value' => variable_get('uc_file_base_dir', NULL),
  430. );
  431. $form['uc_file_download_limit'] = array(
  432. '#type' => 'fieldset',
  433. '#title' => t('Download limits'),
  434. '#description' => t('Leave any of these fields empty or unchanged to not enforce a limit with them.'),
  435. );
  436. $form['uc_file_download_limit']['uc_file_download_limit_number'] = array(
  437. '#type' => 'textfield',
  438. '#title' => t('Downloads'),
  439. '#description' => t('The number of times a file can be downloaded.'),
  440. '#default_value' => variable_get('uc_file_download_limit_number', NULL),
  441. '#maxlength' => 4,
  442. '#size' => 4,
  443. );
  444. $form['uc_file_download_limit']['uc_file_download_limit_addresses'] = array(
  445. '#type' => 'textfield',
  446. '#title' => t('IP addresses'),
  447. '#description' => t('The number of unique IP addresses from which a user can download a file.'),
  448. '#default_value' => variable_get('uc_file_download_limit_addresses', NULL),
  449. '#maxlength' => 4,
  450. '#size' => 4,
  451. );
  452. $form['uc_file_download_limit']['uc_file_download_limit_duration_qty'] = array(
  453. '#type' => 'textfield',
  454. '#title' => t('Time'),
  455. '#default_value' => (variable_get('uc_file_download_limit_duration_granularity', 'never') == 'never') ? NULL : variable_get('uc_file_download_limit_duration_qty', NULL),
  456. '#size' => 4,
  457. '#maxlength' => 4,
  458. '#prefix' => '<div class="duration">',
  459. '#suffix' => '</div>',
  460. );
  461. $form['uc_file_download_limit']['uc_file_download_limit_duration_granularity'] = array(
  462. '#type' => 'select',
  463. '#options' => array(
  464. 'never' => t('never'),
  465. 'day' => t('day(s)'),
  466. 'week' => t('week(s)'),
  467. 'month' => t('month(s)'),
  468. 'year' => t('year(s)')
  469. ),
  470. '#default_value' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  471. '#attributes' => array(
  472. 'onchange' => 'if (this.value == "never") {$("#edit-uc-file-download-limit-duration-qty").attr("disabled", "disabled").val("");} else {$("#edit-uc-file-download-limit-duration-qty").removeAttr("disabled");}'
  473. ),
  474. '#description' => t('How long after a product has been purchased until its file download expires.'),
  475. '#prefix' => '<div class="duration">',
  476. '#suffix' => '</div>',
  477. );
  478. $form['uc_file_advanced'] = array(
  479. '#type' => 'fieldset',
  480. '#title' => t('Advanced server settings'),
  481. '#description' => t('The defaults should cover most use cases. Do not change these unless you know what you are doing.'),
  482. '#collapsible' => TRUE,
  483. '#collapsed' => TRUE,
  484. );
  485. $form['uc_file_advanced']['uc_file_file_mask'] = array(
  486. '#type' => 'textfield',
  487. '#title' => t('Files mask'),
  488. '#description' => t('The regular expression used for masking files in files directory.'),
  489. '#default_value' => variable_get('uc_file_file_mask', ".*"),
  490. );
  491. $form['uc_file_advanced']['uc_file_reverse_proxy_addresses'] = array(
  492. '#type' => 'textarea',
  493. '#rows' => 3,
  494. '#title' => t('Allowed reverse proxy addresses'),
  495. '#description' => t('Add allowed reverse proxy addresses for the file download system to check for (one per line), otherwise @var will be used as the origin address.', array('@var' => $_SERVER['REMOTE_ADDR'])),
  496. '#default_value' => implode("\n", variable_get('uc_file_reverse_proxy_addresses', array())),
  497. );
  498. return $form;
  499. }
  500. function uc_file_feature_settings_validate($form_id, $form_values) {
  501. if (!empty($form_values['uc_file_base_dir']) && $form_values['op'] == t('Save configuration') && !is_dir($form_values['uc_file_base_dir'])) {
  502. form_set_error('uc_file_base_dir', t('%dir is not a valid file or directory', array('%dir' => $form_values['uc_file_base_dir'])));
  503. }
  504. }
  505. function uc_file_feature_settings_submit($form_id, $form_values) {
  506. $action = (empty($form_values['uc_file_base_dir'])) ? 'empty' : 'insert';
  507. _file_table_action($action);
  508. _file_table_action('refresh');
  509. $proxies = variable_get('uc_file_reverse_proxy_addresses', '');
  510. variable_set('uc_file_reverse_proxy_addresses', explode("\n", $proxies));
  511. }
  512. /**
  513. * Page builder for file products admin
  514. */
  515. function uc_file_files_admin() {
  516. _file_table_action('insert');
  517. return drupal_get_form('uc_file_files_form');
  518. }
  519. /**
  520. * Implementation of theme_form($form)
  521. */
  522. function theme_uc_file_files_form($form) {
  523. $output = '';
  524. //Only display files on 1st form step
  525. if ($form['step']['#value'] == 1) {
  526. $files = array();
  527. $args = array('form' => $form);
  528. $header = tablesort_sql(tapir_get_header('uc_file_files_table', array()));
  529. $order = (empty($header)) ? "ORDER BY f.filename ASC" : $header .", f.filename ASC";
  530. $count_query = "SELECT COUNT(*) FROM {uc_files}";
  531. $query = pager_query("SELECT n.nid, f.filename, n.title, fp.model, f.fid, pf.pfid FROM {uc_files} as f LEFT JOIN {uc_file_products} as fp ON (f.fid = fp.fid) LEFT JOIN {uc_product_features} as pf ON (fp.pfid = pf.pfid) LEFT JOIN {node} as n ON (pf.nid = n.nid) ". $order, UC_FILE_PAGER_SIZE, 0, $count_query);
  532. while ($file = db_fetch_object($query)) {
  533. $files[] = $file;
  534. }
  535. $args['files'] = $files;
  536. $output .= '<p>'. t('File downloads can be attached to any Ubercart product as a product feature. For security reasons the <a href="!download_url">file downloads directory</a> is separated from the Drupal <a href="!file_url">file system</a>. Here are the list of files (and their associated Ubercart products) that can be used for file downloads.', array('!download_url' => url('admin/store/settings/products/edit/features'), '!file_url' => url('admin/settings/file-system'))) .'</p>';
  537. $output .= drupal_render($form['uc_file_action']);
  538. $output .= tapir_get_table('uc_file_files_table', $args);
  539. $output .= theme('pager', NULL, UC_FILE_PAGER_SIZE, 0);
  540. }
  541. //Checkboxes already rendered in uc_file_files_table
  542. foreach ($form as $form_element => $form_data) {
  543. if (strpos($form_element, 'file_select_') !== FALSE) {
  544. unset($form[$form_element]);
  545. }
  546. }
  547. $output .= drupal_render($form);
  548. return $output;
  549. }
  550. /**
  551. * Form builder for file products admin
  552. */
  553. function uc_file_files_form($form_values = NULL) {
  554. $form['step'] = array(
  555. '#type' => 'hidden',
  556. '#value' => (!isset($form_values)) ? 1 : $form_values['step'] + 1,
  557. );
  558. switch ($form['step']['#value']) {
  559. //Display File Options and File checkboxes
  560. case 1:
  561. $files = db_query("SELECT * FROM {uc_files}");
  562. $file_actions = array(
  563. 'uc_file_delete' => t('Delete file(s)'),
  564. 'uc_file_upload' => t('Upload file')
  565. );
  566. //Check any if any hook_file_action('info', $args) are implemented
  567. foreach (module_implements('file_action') as $module) {
  568. $name = $module .'_file_action';
  569. $result = $name('info', NULL);
  570. if (is_array($result)) {
  571. foreach ($result as $key => $action) {
  572. if ($key != 'uc_file_delete' && $key != 'uc_file_upload') {
  573. $file_actions[$key] = $action;
  574. }
  575. }
  576. }
  577. }
  578. while ($file = db_fetch_object($files)) {
  579. $form['file_select_'. $file->fid] = array('#type' => 'checkbox');
  580. }
  581. $form['uc_file_action'] = array(
  582. '#type' => 'fieldset',
  583. '#title' => t('File options'),
  584. '#collapsible' => FALSE,
  585. '#collapsed' => FALSE,
  586. );
  587. $form['uc_file_action']['action'] = array(
  588. '#type' => 'select',
  589. '#title' => t('Action'),
  590. '#options' => $file_actions,
  591. '#prefix' => '<div class="duration">',
  592. '#suffix' => '</div>',
  593. );
  594. $form['uc_file_action']['submit'] = array(
  595. '#type' => 'submit',
  596. '#value' => t('Perform action'),
  597. '#prefix' => '<div class="duration">',
  598. '#suffix' => '</div>',
  599. );
  600. break;
  601. case 2: //Perform File Action (Upload, Delete, hooked in actions)
  602. $file_ids = array();
  603. foreach ($form_values as $name => $form_value) {
  604. if (strpos($name, 'file_select_') !== FALSE) {
  605. $file_ids[] = intval(str_replace('file_select_', '', $name));
  606. }
  607. }
  608. $form['file_ids'] = array('#type' => 'value', '#value' => $file_ids);
  609. $form['action'] = array('#type' => 'value', '#value' => $form_values['action']);
  610. //Switch to an action to perform
  611. switch ($form_values['action']) {
  612. case 'uc_file_delete': //Delete selected files
  613. foreach ($file_ids as $file_id) {
  614. $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $file_id));
  615. $filename = (substr($filename, -1) == "/") ? $filename .' ('. t('directory') .')' : $filename;
  616. $file_list[] = $filename;
  617. }
  618. $form['files'] = array(
  619. '#type' => 'markup',
  620. '#value' => theme_item_list($file_list, NULL, 'ul', array('class' => 'file-name')),
  621. );
  622. $form['recurse_directories'] = array(
  623. '#type' => 'checkbox',
  624. '#title' => t('Delete selected directories and their sub directories'),
  625. );
  626. $form = confirm_form($form, t('Delete the following file(s)?'), 'admin/store/products/files', t('Deleting a file will remove all its associated file downloads and product features. Removing a directory will remove any files it contains and their associated file downloads and product features.'), t('Yes'), t('No'));
  627. break;
  628. case 'uc_file_upload': //Upload file
  629. drupal_set_title(t('Upload File'));
  630. $max_bytes = trim(ini_get('post_max_size'));
  631. $directories = array('' => '/');
  632. switch (strtolower($max_bytes{strlen($max_bytes)-1})) {
  633. case 'g':
  634. $max_bytes *= 1024;
  635. case 'm':
  636. $max_bytes *= 1024;
  637. case 'k':
  638. $max_bytes *= 1024;
  639. }
  640. $files = db_query("SELECT * FROM {uc_files}");
  641. while ($file = db_fetch_object($files)) {
  642. if (is_dir(variable_get('uc_file_base_dir', NULL) ."/". $file->filename)) {
  643. $directories[$file->filename] = $file->filename;
  644. }
  645. }
  646. $form['#attributes']['enctype'] = 'multipart/form-data';
  647. $form['upload_dir'] = array(
  648. '#type' => 'select',
  649. '#title' => t('Directory'),
  650. '#description' => t('The directory to upload the file to. The default directory is the root of the file downloads directory.'),
  651. '#options' => $directories,
  652. );
  653. $form['upload'] = array(
  654. '#type' => 'file',
  655. '#title' => t('File'),
  656. '#description' => t('The maximum file size that can be uploaded is %size bytes. You will need to use a different method to upload the file to the directory (e.g. FTP, SSH) if your file exceeds this size.', array('%size' => number_format($max_bytes))),
  657. );
  658. $form['submit'] = array(
  659. '#type' => 'submit',
  660. '#value' => t('Upload'),
  661. );
  662. break;
  663. default:
  664. //Check any if any hook_file_action('form', $args) are implemented
  665. foreach (module_implements('file_action') as $module) {
  666. $name = $module .'_file_action';
  667. $result = $name('form', array('action' => $form_values['action'], 'file_ids' => $file_ids));
  668. $form = (is_array($result)) ? array_merge($form, $result) : $form;
  669. }
  670. break;
  671. }
  672. break;
  673. default:
  674. break;
  675. }
  676. $form['#multistep'] = TRUE;
  677. $form['#redirect'] = FALSE;
  678. return $form;
  679. }
  680. function uc_file_files_form_validate($form_id, $form_values) {
  681. switch ($form_values['step']) {
  682. case 2:
  683. switch ($form_values['action']) {
  684. case 'uc_file_delete': //Nothing to validate for file delete
  685. break;
  686. case 'uc_file_upload':
  687. //Check any if any hook_file_action('validate', $args) are implemented
  688. if ($temp_file = file_check_upload()) {
  689. foreach (module_implements('file_action') as $module) {
  690. $name = $module .'_file_action';
  691. $result = $name('upload_validate', array('file_object' => $temp_file, 'form_id' => $form_id, 'form_values' => $form_values));
  692. }
  693. }
  694. else {
  695. form_set_error('', t('An error occurred while uploading the file'));
  696. }
  697. break;
  698. default:
  699. //Check any if any hook_file_action('validate', $args) are implemented
  700. foreach (module_implements('file_action') as $module) {
  701. $name = $module .'_file_action';
  702. $result = $name('validate', array('form_id' => $form_id, 'form_values' => $form_values));
  703. }
  704. break;
  705. }
  706. break;
  707. default:
  708. break;
  709. }
  710. }
  711. function uc_file_files_form_submit($form_id, $form_values) {
  712. switch ($form_values['step']) {
  713. case 2:
  714. switch ($form_values['action']) {
  715. case 'uc_file_delete':
  716. foreach ($form_values['file_ids'] as $file_id) {
  717. _file_table_action('remove', $file_id, $form_values['recurse_directories'], TRUE);
  718. }
  719. drupal_set_message(t('The select file(s) have been deleted.'));
  720. break;
  721. case 'uc_file_upload':
  722. $dir = variable_get('uc_file_base_dir', NULL) .'/';
  723. $dir = (is_null($form_values['upload_dir'])) ? $dir : $dir . $form_values['upload_dir'];
  724. if (is_dir($dir)) {
  725. if ($file_object = file_save_upload('upload', FALSE)) {
  726. $temp_file = $file_object->filepath;
  727. copy($file_object->filepath, $dir . basename($file_object->filepath));
  728. $file_object->filepath = $dir . basename($file_object->filepath);
  729. unlink($temp_file);
  730. //Check any if any hook_file_action('upload', $args) are implemented
  731. foreach (module_implements('file_action') as $module) {
  732. $name = $module .'_file_action';
  733. $result = $name('upload', array('file_object' => $file_object, 'form_id' => $form_id, 'form_values' => $form_values));
  734. }
  735. _file_table_action('insert');
  736. drupal_set_message(t('The %file has been uploaded to %dir', array('%file' => basename($file_object->filepath), '%dir' => $dir)));
  737. }
  738. else {
  739. drupal_set_message(t('An error occurred while copying the file to %dir', array('%dir' => $dir)));
  740. }
  741. }
  742. else {
  743. drupal_set_message(t('Can not move file to %dir', array('%dir' => $dir)));
  744. }
  745. break;
  746. default:
  747. //Check any if any hook_file_action('validate', $args) are implemented
  748. foreach (module_implements('file_action') as $module) {
  749. $name = $module .'_file_action';
  750. $result = $name('submit', array('form_id' => $form_id, 'form_values' => $form_values));
  751. }
  752. break;
  753. }
  754. drupal_goto('admin/store/products/files');
  755. break;
  756. default:
  757. break;
  758. }
  759. }
  760. /**
  761. * Form builder for file download notification settings.
  762. */
  763. function uc_file_notify_settings() {
  764. $form['uc_file_download_notification'] = array(
  765. '#type' => 'checkbox',
  766. '#title' => t('Send email to customer with file download link(s).'),
  767. '#default_value' => variable_get('uc_file_download_notification', FALSE),
  768. );
  769. $form['uc_file_download_notification_subject'] = array(
  770. '#type' => 'textfield',
  771. '#title' => t('Message subject'),
  772. '#default_value' => variable_get('uc_file_download_notification_subject', uc_get_message('uc_file_download_subject')),
  773. );
  774. $form['uc_file_download_notification_message'] = array(
  775. '#type' => 'textarea',
  776. '#title' => t('Message text'),
  777. '#default_value' => variable_get('uc_file_download_notification_message', uc_get_message('uc_file_download_message')),
  778. '#description' => t('The message the user receives after purchasing products with file downloads (<a href="!token_url">uses order, uc_file, and global tokens</a>)', array('!token_url' => url('admin/store/help/tokens'))),
  779. '#rows' => 10,
  780. );
  781. $form['uc_file_download_notification_format'] = filter_form(variable_get('uc_file_download_notification_format', FILTER_FORMAT_DEFAULT), NULL, array('uc_file_download_notification_format'));
  782. return system_settings_form($form);
  783. }
  784. /**
  785. * Table builder for file products admin
  786. */
  787. function uc_file_files_table($op, $args = array()) {
  788. switch ($op) {
  789. case 'fields':
  790. $fields = array();
  791. $fields[] = array('name' => 'select', 'title' => t(''), 'weight' => 0, 'enabled' => TRUE);
  792. $fields[] = array('name' => 'filename', 'title' => t('File'), 'weight' => 1, 'enabled' => TRUE, 'attributes' => array('field' => 'f.filename'));
  793. $fields[] = array('name' => 'product', 'title' => t('Product'), 'weight' => 2, 'enabled' => TRUE, 'attributes' => array('field' => 'n.title'));
  794. $fields[] = array('name' => 'model', 'title' => t('Model/SKU'), 'weight' => 3, 'enabled' => TRUE, 'attributes' => array('field' => 'fp.model'));
  795. return $fields;
  796. case 'data':
  797. $data = array();
  798. $files = _group_filenames($args['files']);
  799. foreach ($files as $file) {
  800. $data['select'][] = drupal_render($args['form']['file_select_'. $file->fid]);
  801. $filename = (is_dir(variable_get('uc_file_base_dir', NULL) .'/'. $file->filename)) ? '<strong>'. $file->filename .'</strong>' : $file->filename;
  802. $data['filename'][] = ($filename == $last_filename) ? '' : $filename;
  803. if ($filename == $last_filename && !empty($data['#attributes'])) {
  804. $data['#attributes'][count($data['#attributes'])-1] = array('class' => 'group');
  805. }
  806. $last_filename = (empty($last_filename) || $filename != $last_filename) ? $filename : $last_filename;
  807. $data['product'][] = (!empty($file->title)) ? l($file->title, 'node/'. $file->nid) : '';
  808. $data['model'][] = (!empty($file->model)) ? $file->model : '';
  809. $data['#attributes'][] = array();
  810. }
  811. return $data;
  812. case 'attributes':
  813. return array('class' => 'file-table');
  814. }
  815. }
  816. /**
  817. * Table builder for user downloads
  818. */
  819. function uc_file_user_downloads($uid) {
  820. drupal_set_title(t('File downloads'));
  821. uc_add_js(drupal_get_path('module', 'uc_file') .'/uc_file.js');
  822. $header = array(
  823. array('data' => t('Purchased'), 'field' => 'u.granted', 'sort' => 'desc'),
  824. array('data' => t('Filename'), 'field' => 'f.filename'),
  825. array('data' => t('Size')),
  826. array('data' => t('Description'), 'field' => 'p.description'),
  827. array('data' => t('Downloads'), 'field' => 'u.accessed'),
  828. );
  829. $sql = "SELECT granted, filename, accessed, description, `file_key`, f.fid 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";
  830. $count_query = "SELECT COUNT(*) FROM {uc_file_users} WHERE uid = %d";
  831. $download_limit = variable_get('uc_file_download_limit_number', NULL);
  832. $file_ids = array();
  833. $rows = array();
  834. $files = pager_query($sql . tablesort_sql($header), UC_FILE_PAGER_SIZE, 0, $count_query, $uid);
  835. while ($file = db_fetch_object($files)) {
  836. $row = count($rows);
  837. $file_path = variable_get('uc_file_base_dir', NULL) .'/'. $file->filename;
  838. $bytesize = format_size(filesize($file_path));
  839. $expiration = _file_expiration_date($file->granted);
  840. $onclick = array('onclick' => 'uc_file_update_download('. $row .', '. $file->accessed .', '. ((empty($download_limit)) ? -1 : $download_limit) .');', 'id' => 'link-'. $row);
  841. if (!$expiration) {
  842. $file_link = l(basename($file->filename), 'download/'. $file->fid .'/'. $file->file_key, $onclick);
  843. }
  844. else {
  845. if (time() > $expiration) {
  846. $file_link = basename($file->filename);
  847. }
  848. else {
  849. $file_link = l(basename($file->filename), 'download/'. $file->fid .'/'. $file->file_key, $onclick) .' ('. t('expires on @date', array('@date' => format_date($expiration, 'custom', variable_get('uc_date_format_default', 'm/d/Y')))) .')';
  850. }
  851. }
  852. $rows[] = array(
  853. array('data' => format_date($file->granted, 'custom', variable_get('uc_date_format_default', 'm/d/Y')), 'class' => 'date-row', 'id' => 'date-'. $row),
  854. array('data' => $file_link, 'class' => 'filename-row', 'id' => 'filename-'. $row),
  855. array('data' => $bytesize, 'class' => 'filename-row', 'id' => 'filesize-'. $row),
  856. array('data' => $file->description, 'class' => 'description-row', 'id' => 'description-'. $row),
  857. array('data' => $file->accessed, 'class' => 'download-row', 'id' => 'download-'. $row),
  858. );
  859. }
  860. if (empty($rows)) {
  861. $rows[] = array(array('data' => t('No downloads found'), 'colspan' => 4));
  862. }
  863. $output = theme('table', $header, $rows) . theme('pager', NULL, UC_FILE_PAGER_SIZE, 0);
  864. return $output;
  865. }
  866. /* **************************************************************************** *
  867. * Module and Helper Functions *
  868. * **************************************************************************** */
  869. /**
  870. * Implement Drupal autocomplete textfield
  871. *
  872. * @return:
  873. * Sends string containing javascript array of matched files
  874. */
  875. function _autocomplete_filename() {
  876. // Catch "/" characters that drupal autocomplete doesn't escape
  877. $url = explode('_autocomplete_file/', request_uri());
  878. $string = $url[1];
  879. $matches = array();
  880. $files = db_query("SELECT filename FROM {uc_files} WHERE filename LIKE LOWER('%s')", '%'. $string .'%');
  881. while ($file = db_fetch_object($files)) {
  882. $matches[$file->filename] = $file->filename;
  883. }
  884. print drupal_to_js($matches);
  885. exit();
  886. }
  887. /**
  888. * Email a user with download links for a product file download
  889. *
  890. * @param $user
  891. * The Drupal user object
  892. * @param $order
  893. * The order object associated with message
  894. * @param $file_user
  895. * An array for user file downloads (uc_file_user row) associated with message
  896. * @return:
  897. * Sends result of drupal_mail
  898. */
  899. function _email_file_download($user, $order, $file_users) {
  900. if (!variable_get('uc_file_download_notification', FALSE)) {
  901. return;
  902. }
  903. $token_filters = array('global' => NULL, 'user' => $user, 'order' => $order, 'uc_file' => $file_users);
  904. $key = 'uc_file_download_notify';
  905. $to = $order->primary_email;
  906. $from = uc_store_email_from();
  907. $subject = token_replace_multiple(variable_get('uc_file_download_notification_subject', uc_get_message('uc_file_download_subject')), $token_filters);
  908. $body = token_replace_multiple(variable_get('uc_file_download_notification_message', uc_get_message('uc_file_download_message')), $token_filters);
  909. $body = check_markup($body, variable_get('uc_file_download_notification_format', 3), FALSE);
  910. //drupal_set_message("Mail Sent<br />key: $key, <br />to: $to, <br />subject: $subject, <br />body: $body, <br />from: $from, <br />");
  911. return drupal_mail($key, $to, $subject, $body, $from, array('Content-Type' => 'text/html; charset=UTF-8; format=flowed'));
  912. }
  913. // Just a small retrofit of the ip_address() function from Drupal 6.
  914. function _uc_file_ip_address() {
  915. static $ip_address = NULL;
  916. if (!isset($ip_address)) {
  917. $ip_address = $_SERVER['REMOTE_ADDR'];
  918. if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
  919. // If an array of known reverse proxy IPs is provided, then trust
  920. // the XFF header if request really comes from one of them.
  921. $reverse_proxy_addresses = variable_get('uc_file_reverse_proxy_addresses', array());
  922. if (!empty($reverse_proxy_addresses) && in_array($ip_address, $reverse_proxy_addresses, TRUE)) {
  923. // If there are several arguments, we need to check the most
  924. // recently added one, i.e. the last one.
  925. $ip_address = array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
  926. }
  927. }
  928. }
  929. return $ip_address;
  930. }
  931. /**
  932. * Perform first-pass authorization. Call authorization hooks afterwards.
  933. *
  934. * Called when a user requests a file download, function checks download
  935. * limits then checks for any implementation of hook_download_authorize.
  936. * Passing that, the function _file_download_transfer is called.
  937. *
  938. * @param $fid
  939. * The fid of the file specified to download.
  940. * @param $key
  941. * The hash key of a user's download
  942. */
  943. function _file_download($fid, $key) {
  944. global $user;
  945. // In case the $user doesn't exist (downloading with a Download Manager, or Anonymous) find the $user based on the URL given
  946. if (!$user->uid) {
  947. $user_id = db_result(db_query("SELECT uid FROM {uc_file_users} WHERE fid = %d and file_key = '%s'", $fid, $key));
  948. $user = user_load(array('uid' => $user_id));
  949. }
  950. $ip = _uc_file_ip_address();
  951. $message_admin = t('Please contact the site administrator if this message has been received in error.');
  952. $message_user = ($user->uid) ? t('The user %username ', array('%username' => $user->name)) : t('The IP address %ip ', array('%ip' => $ip));
  953. $file_download = db_fetch_object(db_query("SELECT * FROM {uc_file_users} WHERE fid = %d AND `file_key` = '%s'", $fid, $key));
  954. $request_cache = cache_get('uc_file_'. $ip);
  955. $requests = ($request_cache) ? $request_cache->data + 1 : 1;
  956. if ($requests > UC_FILE_REQUEST_LIMIT) {
  957. _file_download_deny($user->uid, t('You have attempted to download an incorrect file URL too many times. ') . $message_admin);
  958. }
  959. if (!$file_download) {
  960. cache_set('uc_file_'. $ip, 'cache', $requests, time() + 86400);
  961. if ($requests == UC_FILE_REQUEST_LIMIT) {
  962. watchdog('uc_file', t('%username has been temporarily banned from file downloads.', array('%username' => $message_user)), WATCHDOG_WARNING);
  963. }
  964. _file_download_deny($user->uid, t("The following URL is not a valid download link. ") . $message_admin);
  965. }
  966. else {
  967. $ip_limit = variable_get('uc_file_download_limit_addresses', NULL);
  968. $addresses = unserialize($file_download->addresses);
  969. if (!empty($ip_limit) && !in_array($ip, $addresses) && count($addresses) >= $ip_limit) {
  970. watchdog('uc_file', t('%username has been denied a file download by downloading it from too many IP addresses.', array('%username' => $message_user)), WATCHDOG_WARNING);
  971. _file_download_deny($user->uid, t('You have downloaded this file from too many different locations. ') . $message_admin);
  972. }
  973. else {
  974. $download_limit = variable_get('uc_file_download_limit_number', NULL);
  975. if (!empty($download_limit) && $file_download->accessed >= $download_limit) {
  976. watchdog('uc_file', t('%username has been denied a file download by downloading it too many times.', array('%username' => $message_user)), WATCHDOG_WARNING);
  977. _file_download_deny($user->uid, t('You have downloaded this file too many times. ') . $message_admin);
  978. }
  979. else {
  980. $duration_limit = _file_expiration_date($file_download->granted);
  981. if ($duration_limit !== FALSE && time() >= $duration_limit) {
  982. watchdog('uc_file', t('%username has been denied an expired file download.', array('%username' => $message_user)), WATCHDOG_WARNING);
  983. _file_download_deny($user->uid, t("This file download has expired. ") . $message_admin);
  984. }
  985. else {
  986. //Check any if any hook_download_authorize calls deny the download
  987. foreach (module_implements('download_authorize') as $module) {
  988. $name = $module .'_download_authorize';
  989. $result = $name($user, $file_download);
  990. if (!$result) {
  991. _file_download_deny($user->uid);
  992. }
  993. }
  994. $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  995. watchdog('uc_file', t('%username has started download of the file %filename.', array('%username' => $message_user, '%filename' => basename($filename))), WATCHDOG_NOTICE);
  996. _file_download_transfer($file_download, $ip, $file_download->fid);
  997. }
  998. }
  999. }
  1000. }
  1001. }
  1002. /**
  1003. * Deny a file download
  1004. *
  1005. * @param $uid
  1006. * The user id of the person attempting the download
  1007. * @param $message
  1008. * The optional message to send to the user
  1009. */
  1010. function _file_download_deny($uid = NULL, $message = NULL) {
  1011. if (!is_null($message)) {
  1012. drupal_set_message($message, 'error');
  1013. }
  1014. if (is_null($uid) || $uid == 0) {
  1015. drupal_access_denied();
  1016. exit();
  1017. }
  1018. else {
  1019. drupal_goto('user/'. $uid .'/files');
  1020. }
  1021. }
  1022. /**
  1023. * Send the file's binary data to a user via HTTP and update the uc_file_users table.
  1024. *
  1025. * Supports resume and download managers.
  1026. *
  1027. * @param $file_user
  1028. * The file_user object from the uc_file_users
  1029. * @param $ip
  1030. * The string containing the ip address the download is going to
  1031. * @param $fid
  1032. * The file id of the file to transfer
  1033. */
  1034. function _file_download_transfer($file_user, $ip, $fid) {
  1035. $file = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  1036. $file_path = variable_get('uc_file_base_dir', NULL) .'/'. $file;
  1037. if (!is_file($file_path)) {
  1038. drupal_set_message(t('The file %filename could not be found. Please contact the site administrator.', array('%filename' => basename($file))), 'error');
  1039. watchdog('uc_file', t('%username failed to download the file %filename.', array('%username' => $message_user, '%filename' => basename($file))), WATCHDOG_NOTICE);
  1040. drupal_not_found();
  1041. exit();
  1042. }
  1043. else {
  1044. //Check any if any hook_file_transfer_alter calls alter the download
  1045. foreach (module_implements('file_transfer_alter') as $module) {
  1046. $name = $module .'_file_transfer_alter';
  1047. $file_path = $name($file_user, $ip, $fid, $file_path);
  1048. }
  1049. //Gather relevent info about file
  1050. $size = filesize($file_path);
  1051. $fileinfo = pathinfo($file_path);
  1052. // Workaround for IE filename bug with multiple periods / multiple dots in filename
  1053. // that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
  1054. if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
  1055. $filename = preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1);
  1056. }
  1057. else {
  1058. $filename = $fileinfo['basename'];
  1059. }
  1060. // Compatibility workaround for older versions of Drupal 5
  1061. if (function_exists('file_get_mimetype')) {
  1062. $mimetype = file_get_mimetype($filename); }
  1063. else {
  1064. // Set the Content-Type based on file extension.
  1065. $file_extension = strtolower($fileinfo['extension']);
  1066. switch ($file_extension) {
  1067. case 'exe':
  1068. $mimetype = 'application/octet-stream';
  1069. break;
  1070. case 'zip':
  1071. $mimetype = 'application/zip';
  1072. break;
  1073. case 'mp3':
  1074. $mimetype = 'audio/mpeg';
  1075. break;
  1076. case 'mpg':
  1077. $mimetype = 'video/mpeg';
  1078. break;
  1079. case 'avi':
  1080. $mimetype = 'video/x-msvideo';
  1081. break;
  1082. default:
  1083. $mimetype = 'application/force-download';
  1084. }
  1085. }
  1086. // Check if HTTP_RANGE is sent by browser (or download manager)
  1087. if (isset($_SERVER['HTTP_RANGE'])) {
  1088. list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
  1089. if ($size_unit == 'bytes') {
  1090. // Multiple ranges could be specified at the same time, but for simplicity only serve the first range
  1091. // See http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
  1092. list($range, $extra_ranges) = explode(',', $range_orig, 2);
  1093. }
  1094. else {
  1095. $range = '';
  1096. }
  1097. }
  1098. else {
  1099. $range = '';
  1100. }
  1101. // Figure out download piece from range (if set)
  1102. list($seek_start, $seek_end) = explode('-', $range, 2);
  1103. // Set start and end based on range (if set), else set defaults and check for invalid ranges.
  1104. $seek_end = intval((empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)), ($size - 1)));
  1105. $seek_start = intval((empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)), 0));
  1106. ob_end_clean();
  1107. // Start building the array of headers
  1108. $http_headers = array();
  1109. //Only send partial content header if downloading a piece of the file (IE workaround)
  1110. if ($seek_start > 0 || $seek_end < ($size - 1)) {
  1111. drupal_set_header('HTTP/1.1 206 Partial Content');
  1112. }
  1113. // Standard headers, including content-range and length
  1114. drupal_set_header('Pragma: public');
  1115. drupal_set_header('Cache-Control: cache, must-revalidate');
  1116. drupal_set_header('Accept-Ranges: bytes');
  1117. drupal_set_header('Content-Range: bytes '. $seek_start .'-'. $seek_end .'/'. $size);
  1118. drupal_set_header('Content-Type: '. $mimetype);
  1119. drupal_set_header('Content-Disposition: attachment; filename="'. $filename .'"');
  1120. drupal_set_header('Content-Length: '. ($seek_end - $seek_start + 1));
  1121. // Last-modified is required for content served dynamically
  1122. drupal_set_header('Last-modified: '. format_date(filemtime($file_path), 'large'));
  1123. // Etag header is required for Firefox3 and other managers
  1124. drupal_set_header('ETag: '. md5($file_path));
  1125. // Open the file and seek to starting byte
  1126. $fp = fopen($file_path, 'rb');
  1127. fseek($fp, $seek_start);
  1128. // Start buffered download
  1129. while (!feof($fp)) {
  1130. // Reset time limit for large files
  1131. set_time_limit(0);
  1132. print(fread($fp, 1024 * 8));
  1133. flush();
  1134. ob_flush();
  1135. }
  1136. // Finished serving the file, close the stream and log the download to the user table
  1137. fclose($fp);
  1138. _user_table_action('download', $file_user, $ip);
  1139. exit();
  1140. }
  1141. }
  1142. /**
  1143. * Return a file expiration date given a purchase date
  1144. *
  1145. * @param $purchase_date
  1146. * The purchase date for the file
  1147. * @return:
  1148. * A UNIX timestamp representing the second the file download expires or FALSE
  1149. * if there won't be an expiration
  1150. */
  1151. function _file_expiration_date($purchase_date = NULL) {
  1152. $purchase_date = (!is_null($purchase_date)) ? $purchase_date : time();
  1153. $quantity = (!is_null(variable_get('uc_file_download_limit_duration_qty', NULL))) ? variable_get('uc_file_download_limit_duration_qty', NULL) : 1;
  1154. $operator = ($quantity < 0) ? '' : '+';
  1155. $duration = variable_get('uc_file_download_limit_duration_granularity', 'never');
  1156. return ($duration != 'never') ? strtotime($operator . $quantity .' '. $duration, $purchase_date) : FALSE;
  1157. }
  1158. /**
  1159. * Perform a specified action on the uc_files table
  1160. *
  1161. * @param $op
  1162. * The action to perform on uc_files table
  1163. * - empty: truncate uc_files table
  1164. * - insert: scan the uc_file base dir and enter new files into the table
  1165. * - remove: remove the file specified by arguments
  1166. * - refresh: scan the uc_file base dir and remove files/dir from the table
  1167. * that don't exist
  1168. * @param $arg1
  1169. * Specified by op argument
  1170. * @param $arg2
  1171. * Specified by op argument
  1172. * @param $arg3
  1173. * Specified by op argument
  1174. */
  1175. function _file_table_action($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL) {
  1176. switch ($op) {
  1177. case 'empty': //Clear out file table (args not used)
  1178. db_query("TRUNCATE TABLE {uc_files}");
  1179. break;
  1180. case 'insert': //Add new items into table (args not used)
  1181. if (!is_null($dir = variable_get('uc_file_base_dir', NULL))) {
  1182. $files = file_scan_directory($dir, variable_get('uc_file_file_mask', '.*'));
  1183. $dir = (substr($dir, -1) != '/' || substr($dir, -1) != '\\') ? $dir .'/' : $dir;
  1184. foreach ($files as $file) {
  1185. $filename = str_replace($dir, '', $file->filename);
  1186. $file_dir = dirname($filename);
  1187. if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $file_dir .'/')) && $file_dir != '.') {
  1188. $fid = db_next_id('{uc_files}_fid');
  1189. db_query("INSERT INTO {uc_files} (fid, filename) VALUES (%d, '%s')", $fid, $file_dir .'/');
  1190. }
  1191. if (!db_result(db_query("SELECT fid FROM {uc_files} WHERE filename = '%s'", $filename))) {
  1192. $fid = db_next_id('{uc_files}_fid');
  1193. db_query("INSERT INTO {uc_files} (fid, filename) VALUES (%d, '%s')", $fid, $filename);
  1194. }
  1195. if (!is_null($fid)) {
  1196. $file_object = db_fetch_object(db_query("SELECT * FROM {uc_files} WHERE fid = %d", $fid));
  1197. //Check any if any hook_file_action('insert', $args) are implemented
  1198. foreach (module_implements('file_action') as $module) {
  1199. $name = $module .'_file_action';
  1200. $result = $name('insert', array('file_object' => $file_object));
  1201. }
  1202. unset($fid);
  1203. }
  1204. }
  1205. }
  1206. break;
  1207. case 'remove': //Remove a specific file id (arg1 = file id to delete, arg2 = TRUE = recursively delete directories, arg 3 = TRUE = delete associated rows/files)
  1208. if (!is_null($arg1) && $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $arg1))) {
  1209. $dir = variable_get('uc_file_base_dir', NULL);
  1210. $sub_fids = ($arg2) ? _get_dir_file_ids($arg1, TRUE) : _get_dir_file_ids($arg1);
  1211. $selected_fid = (is_dir($dir .'/'. $filename) && !$arg2) ? array() : array($arg1);
  1212. $fids = (!$sub_fids) ? $selected_fid : array_merge($sub_fids, $selected_fid);
  1213. $fids = _sort_fids($fids);
  1214. foreach ($fids as $fid) {
  1215. if ($arg3) {
  1216. $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  1217. $pfids = db_query("SELECT pfid FROM {uc_file_products} WHERE fid = %d", $fid);
  1218. while ($pfid = db_fetch_object($pfids)) {
  1219. db_query("DELETE FROM {uc_product_features} WHERE pfid = %d AND fid = 'file'", $pfid->pfid);
  1220. db_query("DELETE FROM {uc_file_products} WHERE pfid = %d", $pfid->pfid);
  1221. }
  1222. if (is_dir($dir .'/'. $filename)) {
  1223. rmdir($dir .'/'. $filename);
  1224. }
  1225. else {
  1226. unlink($dir .'/'. $filename);
  1227. }
  1228. _user_table_action('remove', $fid);
  1229. }
  1230. db_query("DELETE FROM {uc_files} WHERE fid = %d", $fid);
  1231. }
  1232. }
  1233. break;
  1234. case 'refresh': //Remove non-existing items from table (args not used)
  1235. $files = db_query("SELECT * FROM {uc_files}");
  1236. while ($file = db_fetch_object($files)) {
  1237. if (is_dir(variable_get('uc_file_base_dir', NULL) .'/'. $file->filename)) {
  1238. continue;
  1239. }
  1240. if (is_file(variable_get('uc_file_base_dir', NULL) .'/'. $file->filename)) {
  1241. continue;
  1242. }
  1243. db_query("DELETE FROM {uc_files} WHERE fid = %d", $file->fid);
  1244. }
  1245. break;
  1246. default:
  1247. break;
  1248. }
  1249. }
  1250. /**
  1251. * Generate hash used for unique download URLs
  1252. *
  1253. * @param $values
  1254. * An array of values that will be used to generate the hash
  1255. * @return:
  1256. * A string containing the 32 hex character hash
  1257. */
  1258. function _generate_hash($values) {
  1259. $input = mt_rand();
  1260. foreach ($values as $value) {
  1261. $input .= $value;
  1262. }
  1263. return md5($input);
  1264. }
  1265. /**
  1266. * Return a list of model adjustments for a given product node
  1267. *
  1268. * @param $nid
  1269. * The product node id
  1270. * @return:
  1271. * An associative array containing the models created by different product
  1272. * attributes or FALSE if none exist.
  1273. */
  1274. function _get_adjustment_models($nid) {
  1275. $models = array();
  1276. if (module_exists('uc_attribute')) {
  1277. $adjustments = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = %d", $nid);
  1278. while ($adjustment = db_fetch_object($adjustments)) {
  1279. if (!in_array($adjustment->model, $models)) {
  1280. $models[$adjustment->model] = $adjustment->model;
  1281. }
  1282. }
  1283. }
  1284. return (empty($models)) ? FALSE : $models;
  1285. }
  1286. /**
  1287. * Return a list of file ids that are in the directory
  1288. *
  1289. * @param $fid
  1290. * The file id associated with the directory
  1291. * @param $recursive
  1292. * Whether or not to list recursive directories and their files
  1293. * @return:
  1294. * If there are files in the directory an array of file ids, else return FALSE
  1295. */
  1296. function _get_dir_file_ids($fid, $recursive = FALSE) {
  1297. $fids = array();
  1298. $dir = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  1299. $files = db_query("SELECT * FROM {uc_files} WHERE filename LIKE LOWER('%s')", $dir .'%');
  1300. while ($file = db_fetch_object($files)) {
  1301. $filename_change = str_replace($dir, '', $file->filename);
  1302. $filename = (substr($filename_change, 0, 1) == '/') ? substr($filename_change, 1) : $filename_change;
  1303. if (!strpos($filename, '/') && !empty($filename)) {
  1304. $fids[] = $file->fid;
  1305. }
  1306. elseif ($recursive && !empty($filename) && $filename_change != $file->filename) {
  1307. $fids[] = $file->fid;
  1308. }
  1309. }
  1310. return (empty($fids)) ? FALSE : $fids;
  1311. }
  1312. /**
  1313. * Group filenames by an attribute while maintaining order.
  1314. *
  1315. * @param $files
  1316. * The array of objects
  1317. * @return:
  1318. * The sorted array of objects
  1319. */
  1320. function _group_filenames($objects) {
  1321. $output = array();
  1322. $existing = array();
  1323. foreach ($objects as $key => $object) {
  1324. $filename = $object->filename;
  1325. if (!in_array($filename, array_keys($existing))) {
  1326. $existing[$filename] = $key;
  1327. $output[] = $object;
  1328. }
  1329. else {
  1330. $inserted_index = $existing[$filename] + 1;
  1331. foreach ($existing as $filename => $index) {
  1332. $existing[$filename] = ($index >= $inserted_index) ? $index + 1 : $index;
  1333. }
  1334. array_splice($output, $inserted_index, 0, array($inserted_index => $object));
  1335. }
  1336. }
  1337. return $output;
  1338. }
  1339. /**
  1340. * Take a list of file ids and sorts the list to where directories are list last
  1341. * and by order of descending depth
  1342. * @param $fids
  1343. * The array of file ids
  1344. * @return:
  1345. * The sorted array of file ids
  1346. */
  1347. function _sort_fids($fids) {
  1348. $dir_fids = array();
  1349. $output = array();
  1350. foreach ($fids as $fid) {
  1351. $filename = db_result(db_query("SELECT filename FROM {uc_files} WHERE fid = %d", $fid));
  1352. if (substr($filename, -1) == '/') {
  1353. $dir_fids[$fid] = $filename;
  1354. }
  1355. else {
  1356. $output[] = $fid;
  1357. }
  1358. }
  1359. while (!empty($dir_fids)) {
  1360. $highest = 0;
  1361. foreach ($dir_fids as $dir_fid => $filename) {
  1362. if (substr_count($filename, '/') > $highest) {
  1363. $highest = substr_count($filename, '/');
  1364. $highest_fid = $dir_fid;
  1365. }
  1366. }
  1367. $output[] = $highest_fid;
  1368. unset($dir_fids[$highest_fid]);
  1369. }
  1370. return $output;
  1371. }
  1372. /**
  1373. * Perform a specified action on the uc_file_users table
  1374. *
  1375. * @param $op
  1376. * The action to perform on uc_file_users table
  1377. * - allow: insert a new file download for a user
  1378. * - download: update a row after a download takes places
  1379. * - remove: remove a file download for specified files, users, keys
  1380. * @param $arg1
  1381. * Specified by op argument
  1382. * @param $arg2
  1383. * Specified by op argument
  1384. * @param $arg3
  1385. * Specified by op argument
  1386. * @return:
  1387. * Specified by op argument
  1388. */
  1389. function _user_table_action($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL) {
  1390. switch ($op) {
  1391. case 'allow': //arg1 = file id, arg2 = user id, $arg3 = pfid
  1392. //@return file_user objects inserted into table
  1393. if (!is_null($arg1) && !is_null($arg2)) {
  1394. $output = array();
  1395. $granted = time();
  1396. $fids = (_get_dir_file_ids($arg1)) ? _get_dir_file_ids($arg1) : array($arg1);
  1397. foreach ($fids as $fid) {
  1398. $values = array($arg1, $arg2, $arg3, '', $granted, 0, serialize(array()));
  1399. $hash = _generate_hash($values);
  1400. db_query("INSERT INTO {uc_file_users} (fid, uid, pfid, `file_key`, granted, accessed, addresses) VALUES (%d, %d, %d, '%s', %d, %d, '%s')", $fid, $arg2, $arg3, $hash, $granted, 0, serialize(array()));
  1401. $output[] = db_fetch_object(db_query("SELECT * FROM {uc_file_users} WHERE uid = %d AND `file_key` = '%s'", $arg2, $hash));
  1402. }
  1403. }
  1404. return (!is_null($output)) ? $output : FALSE;
  1405. break;
  1406. case 'download': //arg1 = existing file_user object, arg2 = ip download was made from, arg3 not used
  1407. if (!is_null($arg1) && !is_null($arg2)) {
  1408. $addresses = unserialize($arg1->addresses);
  1409. if (!in_array($arg2, $addresses)) {
  1410. $addresses[] = $arg2;
  1411. }
  1412. $accessed = $arg1->accessed+1;
  1413. $values = array($arg1->fid, $arg1->uid, $arg1->pfid, $arg1->file_key, $arg1->granted, $accessed, serialize($addresses));
  1414. $hash = _generate_hash($values);
  1415. db_query("UPDATE {uc_file_users} SET accessed = %d, addresses = '%s', `file_key` = '%s' WHERE fid = %d AND uid = %d AND `file_key` = '%s'", $accessed, serialize($addresses), $hash, $arg1->fid, $arg1->uid, $arg1->file_key);
  1416. }
  1417. break;
  1418. case 'remove': //arg1 = file id, arg2 = user id, $arg3 = key
  1419. if (!is_null($arg1) || !is_null($arg2)) {
  1420. if (!is_null($arg1) && is_null($arg2) && is_null($arg3)) { //Remove a file from download
  1421. db_query("DELETE FROM {uc_file_users} WHERE fid = %d", $arg1);
  1422. }
  1423. if (is_null($arg1) && !is_null($arg2) && is_null($arg3)) { //Remove a user's downloads
  1424. db_query("DELETE FROM {uc_file_users} WHERE uid = %d", $arg2);
  1425. }
  1426. if (!is_null($arg1) && !is_null($arg2) && is_null($arg3)) { //Remove a certain files from a user
  1427. db_query("DELETE FROM {uc_file_users} WHERE fid = %d AND uid = %d", $arg1, $arg2);
  1428. }
  1429. if (is_null($arg1) && !is_null($arg2) && !is_null($arg3)) { //Remove a certain file from a user
  1430. db_query("DELETE FROM {uc_file_users} WHERE uid = %d AND `file_key` = '%s'", $arg2, $arg3);
  1431. }
  1432. }
  1433. break;
  1434. default:
  1435. break;
  1436. }
  1437. }