uc_file.module 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749
  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. /**
  15. * The maximum amount of rows shown in tables of file downloads.
  16. */
  17. define('UC_FILE_PAGER_SIZE', 50);
  18. define('UC_FILE_LIMIT_SENTINEL', -1);
  19. /**
  20. * Implements hook_form_FORM_ID_alter() for uc_product_feature_settings_form().
  21. */
  22. function uc_file_form_uc_product_feature_settings_form_alter(&$form, &$form_state) {
  23. $form['#submit'][] = 'uc_file_feature_settings_submit';
  24. $form['#validate'][] = 'uc_file_feature_settings_validate';
  25. }
  26. /**
  27. * Implements hook_menu().
  28. */
  29. function uc_file_menu() {
  30. $items = array();
  31. $items['_autocomplete_file'] = array(
  32. 'page callback' => '_uc_file_autocomplete_filename',
  33. 'access arguments' => array('administer product features'),
  34. 'type' => MENU_CALLBACK,
  35. );
  36. $items['admin/store/products/files'] = array(
  37. 'title' => 'View file downloads',
  38. 'description' => 'View all file download features on products.',
  39. 'page callback' => 'drupal_get_form',
  40. 'page arguments' => array('uc_file_admin_files_form'),
  41. 'access arguments' => array('administer products'),
  42. 'file' => 'uc_file.admin.inc',
  43. );
  44. $items['user/%user/purchased-files'] = array(
  45. 'title' => 'Files',
  46. 'description' => 'View your purchased files.',
  47. 'page callback' => 'uc_file_user_downloads',
  48. 'page arguments' => array(1),
  49. 'access callback' => 'uc_file_user_access',
  50. 'access arguments' => array(1),
  51. 'type' => MENU_LOCAL_TASK,
  52. 'file' => 'uc_file.pages.inc',
  53. );
  54. $items['download/%'] = array(
  55. 'page callback' => '_uc_file_download',
  56. 'page arguments' => array(1),
  57. 'access arguments' => array('download file'),
  58. 'type' => MENU_CALLBACK,
  59. 'file' => 'uc_file.pages.inc',
  60. );
  61. return $items;
  62. }
  63. /**
  64. * Access callback for a user's list of purchased file downloads.
  65. */
  66. function uc_file_user_access($account) {
  67. global $user;
  68. return $user->uid && (user_access('view all downloads') || $user->uid == $account->uid);
  69. }
  70. /**
  71. * Implements hook_permission().
  72. */
  73. function uc_file_permission() {
  74. return array(
  75. 'download file' => array(
  76. 'title' => t('Download file'),
  77. ),
  78. 'view all downloads' => array(
  79. 'title' => t('View all downloads'),
  80. ),
  81. );
  82. }
  83. /**
  84. * Implements hook_theme().
  85. */
  86. function uc_file_theme() {
  87. return array(
  88. 'uc_file_downloads_token' => array(
  89. 'variables' => array('file_downloads' => NULL),
  90. 'file' => 'uc_file.tokens.inc',
  91. ),
  92. 'uc_file_admin_files_form_show' => array(
  93. 'render element' => 'form',
  94. 'file' => 'uc_file.admin.inc',
  95. ),
  96. 'uc_file_hook_user_file_downloads' => array(
  97. 'render element' => 'form',
  98. 'file' => 'uc_file.theme.inc',
  99. ),
  100. 'uc_file_user_downloads' => array(
  101. 'variables' => array('header' => NULL, 'files' => NULL),
  102. 'file' => 'uc_file.pages.inc',
  103. ),
  104. );
  105. }
  106. /**
  107. * Implements hook_user_cancel().
  108. *
  109. * User was deleted, so we delete all the files associated with them.
  110. */
  111. function uc_file_user_cancel($edit, $account, $method) {
  112. uc_file_remove_user($account);
  113. }
  114. /**
  115. * Form builder for per-user file download administration.
  116. *
  117. * @see uc_file_user_form_validate()
  118. * @see uc_file_user_form_submit()
  119. */
  120. function uc_file_user_form($form, &$form_state, $account) {
  121. $form['file'] = array(
  122. '#type' => 'fieldset',
  123. '#title' => t('Administration'),
  124. '#collapsible' => TRUE,
  125. '#collapsed' => TRUE,
  126. );
  127. // Drop out early if we don't even have any files uploaded.
  128. if (!db_query_range('SELECT 1 FROM {uc_files}', 0, 1)->fetchField()) {
  129. $form['file']['file_message'] = array(
  130. '#markup' => '<p>' . t(
  131. 'You must add files at the <a href="!url">Ubercart file download administration page</a> in order to attach them to a user.',
  132. array('!url' => url('admin/store/products/files', array('query' => array('destination' => 'user/' . $account->uid . '/edit'))))
  133. ) . '</p>',
  134. );
  135. return $form;
  136. }
  137. // Table displaying current downloadable files and limits.
  138. $form['file']['download']['#theme'] = 'uc_file_hook_user_file_downloads';
  139. $form['file']['download']['file_download']['#tree'] = TRUE;
  140. $downloadable_files = array();
  141. $file_downloads = db_query("SELECT * FROM {uc_file_users} ufu INNER JOIN {uc_files} uf ON ufu.fid = uf.fid WHERE ufu.uid = :uid ORDER BY uf.filename ASC", array(':uid' => $account->uid));
  142. $behavior = 0;
  143. foreach ($file_downloads as $file_download) {
  144. // Store a flat array so we can array_diff the ones already allowed when
  145. // building the list of which can be attached.
  146. $downloadable_files[$file_download->fid] = $file_download->filename;
  147. $form['file']['download']['file_download'][$file_download->fid] = array(
  148. 'fuid' => array('#type' => 'value', '#value' => $file_download->fuid),
  149. 'expiration' => array('#type' => 'value', '#value' => $file_download->expiration),
  150. 'remove' => array('#type' => 'checkbox'),
  151. 'filename' => array('#markup' => $file_download->filename),
  152. 'expires' => array('#markup' => $file_download->expiration ? format_date($file_download->expiration, 'short') : t('Never')),
  153. 'time_polarity' => array(
  154. '#type' => 'select',
  155. '#default_value' => '+',
  156. '#options' => array(
  157. '+' => '+',
  158. '-' => '-',
  159. ),
  160. ),
  161. 'time_quantity' => array(
  162. '#type' => 'textfield',
  163. '#size' => 2,
  164. '#maxlength' => 2,
  165. ),
  166. 'time_granularity' => array(
  167. '#type' => 'select',
  168. '#default_value' => 'day',
  169. '#options' => array(
  170. 'never' => t('never'),
  171. 'day' => t('day(s)'),
  172. 'week' => t('week(s)'),
  173. 'month' => t('month(s)'),
  174. 'year' => t('year(s)'),
  175. ),
  176. ),
  177. 'downloads_in' => array('#markup' => $file_download->accessed),
  178. 'download_limit' => array(
  179. '#type' => 'textfield',
  180. '#maxlength' => 3,
  181. '#size' => 3,
  182. '#default_value' => $file_download->download_limit ? $file_download->download_limit : NULL
  183. ),
  184. 'addresses_in' => array('#markup' => count(unserialize($file_download->addresses))),
  185. 'address_limit' => array(
  186. '#type' => 'textfield',
  187. '#maxlength' => 2,
  188. '#size' => 2,
  189. '#default_value' => $file_download->address_limit ? $file_download->address_limit : NULL
  190. ),
  191. );
  192. // Incrementally add behaviors.
  193. _uc_file_download_table_behavior($behavior++, $file_download->fid);
  194. // Store old values for comparing to see if we actually made any changes.
  195. $less_reading = &$form['file']['download']['file_download'][$file_download->fid];
  196. $less_reading['download_limit_old'] = array('#type' => 'value', '#value' => $less_reading['download_limit']['#default_value']);
  197. $less_reading['address_limit_old'] = array('#type' => 'value', '#value' => $less_reading['address_limit']['#default_value']);
  198. $less_reading['expiration_old'] = array('#type' => 'value', '#value' => $less_reading['expiration']['#value']);
  199. }
  200. // Create the list of files able to be attached to this user.
  201. $available_files = array();
  202. $files = db_query("SELECT * FROM {uc_files} ORDER BY filename ASC");
  203. foreach ($files as $file) {
  204. if (substr($file->filename, -1) != '/' && substr($file->filename, -1) != '\\') {
  205. $available_files[$file->fid] = $file->filename;
  206. }
  207. }
  208. // Dialog for uploading new files.
  209. $available_files = array_diff($available_files, $downloadable_files);
  210. if (count($available_files)) {
  211. $form['file']['file_add'] = array(
  212. '#type' => 'select',
  213. '#multiple' => TRUE,
  214. '#size' => 6,
  215. '#title' => t('Add file'),
  216. '#description' => t('Select a file to add as a download. Newly added files will inherit the settings at the !url.', array('!url' => l(t('Ubercart product settings page'), 'admin/store/settings/products'))),
  217. '#options' => $available_files,
  218. '#tree' => TRUE,
  219. );
  220. }
  221. $form['file']['submit'] = array(
  222. '#type' => 'submit',
  223. '#value' => t('Save'),
  224. );
  225. return $form;
  226. }
  227. /**
  228. * Validation handler for per-user file download administration.
  229. *
  230. * @see uc_file_user_form()
  231. * @see uc_file_user_form_submit()
  232. */
  233. function uc_file_user_form_validate($form, &$form_state) {
  234. $edit = $form_state['values'];
  235. // Determine if any downloads were modified.
  236. if (isset($edit['file_download'])) {
  237. foreach ((array)$edit['file_download'] as $key => $download_modification) {
  238. // We don't care... it's about to be deleted.
  239. if ($download_modification['remove']) {
  240. continue;
  241. }
  242. if ($download_modification['download_limit'] < 0) {
  243. form_set_error('file_download][' . $key . '][download_limit', t('A negative download limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  244. }
  245. if ($download_modification['address_limit'] < 0) {
  246. form_set_error('file_download][' . $key . '][address_limit', t('A negative address limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  247. }
  248. // Some expirations don't need any validation...
  249. if ($download_modification['time_granularity'] == 'never' || !$download_modification['time_quantity']) {
  250. continue;
  251. }
  252. // Either use the current expiration, or if there's none,
  253. // start from right now.
  254. $new_expiration = _uc_file_expiration_date($download_modification, $download_modification['expiration']);
  255. if ($new_expiration <= REQUEST_TIME) {
  256. form_set_error('file_download][' . $key . '][time_quantity', t('The date %date has already occurred.', array('%date' => format_date($new_expiration, 'short'))));
  257. }
  258. if ($download_modification['time_quantity'] < 0) {
  259. form_set_error('file_download][' . $key . '][time_quantity', t('A negative expiration quantity does not make sense. Use the polarity control to determine if the time should be added or subtracted.'));
  260. }
  261. }
  262. }
  263. }
  264. /**
  265. * Submit handler for per-user file download administration.
  266. *
  267. * @see uc_file_user_form()
  268. * @see uc_file_user_form_validate()
  269. */
  270. function uc_file_user_form_submit($form, &$form_state) {
  271. $account = $form_state['build_info']['args'][0];
  272. $edit = $form_state['values'];
  273. // Check out if any downloads were modified.
  274. if (isset($edit['file_download'])) {
  275. foreach ((array)$edit['file_download'] as $fid => $download_modification) {
  276. // Remove this user download?
  277. if ($download_modification['remove']) {
  278. uc_file_remove_user_file_by_id($account, $fid);
  279. }
  280. // Update the modified downloads.
  281. else {
  282. // Calculate the new expiration.
  283. $download_modification['expiration'] = _uc_file_expiration_date($download_modification, $download_modification['expiration']);
  284. // Don't touch anything if everything's the same.
  285. if ($download_modification['download_limit'] == $download_modification['download_limit_old'] &&
  286. $download_modification['address_limit'] == $download_modification['address_limit_old'] &&
  287. $download_modification['expiration'] == $download_modification['expiration_old']) {
  288. continue;
  289. }
  290. // Renew. (Explicit overwrite.)
  291. uc_file_user_renew($fid, $account, NULL, $download_modification, TRUE);
  292. }
  293. }
  294. }
  295. // Check out if any downloads were added. We pass NULL to file_user_renew,
  296. // because this shouldn't be associated with a random product.
  297. if (isset($edit['file_add'])) {
  298. foreach ((array)$edit['file_add'] as $fid => $data) {
  299. $download_modification['download_limit'] = variable_get('uc_file_download_limit_number', NULL);
  300. $download_modification['address_limit'] = variable_get('uc_file_download_limit_addresses', NULL);
  301. $download_modification['expiration'] = _uc_file_expiration_date(array(
  302. 'time_polarity' => '+',
  303. 'time_quantity' => variable_get('uc_file_download_limit_duration_qty', NULL),
  304. 'time_granularity' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  305. ), REQUEST_TIME);
  306. // Renew. (Explicit overwrite.)
  307. uc_file_user_renew($fid, $account, NULL, $download_modification, TRUE);
  308. }
  309. }
  310. }
  311. /**
  312. * Implements hook_user_view().
  313. */
  314. function uc_file_user_view($account, $view_mode) {
  315. global $user;
  316. // If user has files and permission to view them, put a link
  317. // on the user's profile.
  318. $existing_download = db_query("SELECT fid FROM {uc_file_users} WHERE uid = :uid", array(':uid' => $account->uid))->fetchField();
  319. if (!$existing_download || (!user_access('view all downloads') && $user->uid != $account->uid)) {
  320. return;
  321. }
  322. $item = array(
  323. '#type' => 'user_profile_item',
  324. '#title' => t('File downloads'),
  325. '#markup' => l(t('Click here to view your file downloads.'), 'user/' . $account->uid . '/purchased-files'),
  326. );
  327. $account->content['summary']['uc_file_download'] = $item;
  328. }
  329. /**
  330. * Attaches jQuery behaviors to the file download modification table.
  331. */
  332. function _uc_file_download_table_behavior($id, $fid) {
  333. drupal_add_js( "
  334. Drupal.behaviors.ucUserAccountFileDownload" . $id . " = {
  335. attach: function(context) {
  336. jQuery('#edit-file-download-" . $fid . "-time-granularity:not(.ucUserAccountFileDownload-processed)', context).addClass('ucUserAccountFileDownload-processed').change(
  337. function() {
  338. _uc_file_expiration_disable_check('#edit-file-download-" . $fid . "-time-granularity', '#edit-file-download-" . $fid . "-time-quantity');
  339. _uc_file_expiration_disable_check('#edit-file-download-" . $fid . "-time-granularity', '#edit-file-download-" . $fid . "-time-polarity');
  340. }
  341. );
  342. }
  343. }", 'inline');
  344. }
  345. /**
  346. * Implements hook_uc_add_to_cart().
  347. *
  348. * If specified in the administration interface, notify a customer when
  349. * downloading a duplicate file. Calculate and show the new limits.
  350. */
  351. function uc_file_uc_add_to_cart($nid, $qty, $data) {
  352. // Only warn if it's set in the product admin interface.
  353. if (!variable_get('uc_file_duplicate_warning', TRUE)) {
  354. return;
  355. }
  356. global $user;
  357. // Get all the files on this product.
  358. $product_features = db_query("SELECT * FROM {uc_product_features} upf " .
  359. "INNER JOIN {uc_file_products} ufp ON upf.pfid = ufp.pfid " .
  360. "INNER JOIN {uc_files} uf ON ufp.fid = uf.fid " .
  361. "WHERE upf.nid = :nid", array(':nid' => $nid));
  362. foreach ($product_features as $product_feature) {
  363. // Match up models...
  364. if (!empty($product_feature->model) && isset($data['model']) && $product_feature->model != $data['model']) {
  365. continue;
  366. }
  367. // Get the current limits, and calculate the new limits to show the user.
  368. if ($file_user = _uc_file_user_get($user, $product_feature->fid)) {
  369. $file_user = (array)$file_user;
  370. $old_limits = $file_user;
  371. // Get the limits from the product feature.
  372. // (Or global if it says pass through.)
  373. $file_modification = array(
  374. 'download_limit' => uc_file_get_download_limit($product_feature),
  375. 'address_limit' => uc_file_get_address_limit($product_feature),
  376. 'expiration' => _uc_file_expiration_date(uc_file_get_time_limit($product_feature), max($file_user['expiration'], REQUEST_TIME)),
  377. );
  378. // Calculate the new limits.
  379. _uc_file_accumulate_limits($file_user, $file_modification, FALSE);
  380. // Don't allow the product to be purchased if it won't increase the
  381. // download limit or expiration time.
  382. if ($old_limits['download_limit'] || $old_limits['expiration']) {
  383. // But still show the message if it does.
  384. drupal_set_message(t('You already have privileges to <a href="!url">download %file</a>. If you complete the purchase of this item, your new download limit will be %download_limit, your access location limit will be %address_limit, and your new expiration time will be %expiration.',
  385. array(
  386. '!url' => $user->uid ? url('user/' . $user->uid . '/purchased-files') : url('user/login'),
  387. '%file' => $product_feature->filename,
  388. '%download_limit' => $file_user['download_limit'] ? $file_user['download_limit'] : t('unlimited'),
  389. '%address_limit' => $file_user['address_limit' ] ? $file_user['address_limit' ] : t('unlimited'),
  390. '%expiration' => $file_user['expiration' ] ? format_date($file_user['expiration'], 'small') : t('never'),
  391. )), 'warning');
  392. }
  393. else {
  394. return array(array(
  395. 'success' => FALSE,
  396. 'message' => t('You already have privileges to <a href="!url">download %file</a>.', array(
  397. '!url' => $user->uid ? url('user/' . $user->uid . '/purchased-files') : url('user/login'),
  398. '%file' => $product_feature->filename,
  399. )),
  400. ));
  401. }
  402. }
  403. }
  404. }
  405. /**
  406. * Implements hook_uc_order_product_can_ship().
  407. */
  408. function uc_file_uc_order_product_can_ship($item) {
  409. // Check if this model is shippable as well as a file.
  410. $files = db_query("SELECT shippable, model FROM {uc_file_products} fp INNER JOIN {uc_product_features} pf ON pf.pfid = fp.pfid WHERE nid = :nid", array(':nid' => $item->nid));
  411. foreach ($files as $file) {
  412. // If the model is 'any' then return.
  413. if (empty($file->model)) {
  414. return $file->shippable;
  415. }
  416. else {
  417. // Use the adjusted SKU, or node SKU if there's none.
  418. $sku = empty($item->data['model']) ? $item->model : $item->data['model'];
  419. if ($sku == $file->model) {
  420. return $file->shippable;
  421. }
  422. }
  423. }
  424. }
  425. /**
  426. * Implements hook_uc_product_feature().
  427. */
  428. function uc_file_uc_product_feature() {
  429. $features[] = array(
  430. 'id' => 'file',
  431. 'title' => t('File download'),
  432. 'callback' => 'uc_file_feature_form',
  433. 'delete' => 'uc_file_feature_delete',
  434. 'settings' => 'uc_file_feature_settings',
  435. );
  436. return $features;
  437. }
  438. /**
  439. * Implements hook_uc_store_status().
  440. */
  441. function uc_file_uc_store_status() {
  442. $message = array();
  443. if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
  444. $message[] = array(
  445. 'status' => 'warning',
  446. 'title' => t('File downloads'),
  447. 'desc' => t('The file downloads directory is not valid or set. Set a valid directory in the <a href="!url">product settings</a> under the file download settings tab.', array('!url' => url('admin/store/settings/products'))),
  448. );
  449. }
  450. else {
  451. $message[] = array(
  452. 'status' => 'ok',
  453. 'title' => t('File downloads'),
  454. 'desc' => t('The file downloads directory has been set and is working.'),
  455. );
  456. }
  457. return $message;
  458. }
  459. /**
  460. * Deletes all file data associated with a given product feature.
  461. *
  462. * @param $pfid
  463. * An Ubercart product feature ID.
  464. */
  465. function uc_file_feature_delete($pfid) {
  466. db_delete('uc_file_products')
  467. ->condition('pfid', $pfid)
  468. ->execute();
  469. }
  470. /**
  471. * Form builder for hook_uc_product_feature.
  472. *
  473. * @see uc_file_feature_form_validate()
  474. * @see uc_file_feature_form_submit()
  475. * @ingroup forms
  476. */
  477. function uc_file_feature_form($form, &$form_state, $node, $feature) {
  478. if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
  479. drupal_set_message(t('A file directory needs to be configured in <a href="@url">product settings</a> under the file download settings tab before a file can be selected.', array('@url' => url('admin/store/settings/products'))), 'warning');
  480. unset($form['buttons']);
  481. return $form;
  482. }
  483. // Rescan the file directory to populate {uc_files} with the current list
  484. // because files uploaded via any method other than the Upload button
  485. // (e.g. by FTP) won'b be in {uc_files} yet.
  486. uc_file_refresh();
  487. if (!db_query_range('SELECT 1 FROM {uc_files}', 0, 1)->fetchField()) {
  488. $form['file']['file_message'] = array(
  489. '#markup' => t(
  490. 'You must add files at the <a href="@url">Ubercart file download administration page</a> in order to attach them to a model.',
  491. array('@url' => url('admin/store/products/files', array('query' => array('destination' => 'node/' . $node->nid . '/edit/features/file/add'))))
  492. ),
  493. );
  494. unset($form['buttons']);
  495. return $form;
  496. }
  497. // Grab all the models on this product.
  498. $models = uc_product_get_models($node->nid);
  499. // Use the feature's values to fill the form, if they exist.
  500. if (!empty($feature)) {
  501. $file_product = db_query("SELECT * FROM {uc_file_products} p LEFT JOIN {uc_files} f ON p.fid = f.fid WHERE pfid = :pfid", array(':pfid' => $feature['pfid']))->fetchObject();
  502. $default_feature = $feature['pfid'];
  503. $default_model = $file_product->model;
  504. $default_filename = $file_product->filename;
  505. $default_description = $file_product->description;
  506. $default_shippable = $file_product->shippable;
  507. $download_status = $file_product->download_limit != UC_FILE_LIMIT_SENTINEL;
  508. $download_value = $download_status ? $file_product->download_limit : NULL;
  509. $address_status = $file_product->address_limit != UC_FILE_LIMIT_SENTINEL;
  510. $address_value = $address_status ? $file_product->address_limit : NULL;
  511. $time_status = $file_product->time_granularity != UC_FILE_LIMIT_SENTINEL;
  512. $quantity_value = $time_status ? $file_product->time_quantity : NULL;
  513. $granularity_value = $time_status ? $file_product->time_granularity : 'never';
  514. }
  515. else {
  516. $file_product = FALSE;
  517. $default_feature = NULL;
  518. $default_model = '';
  519. $default_filename = '';
  520. $default_description = '';
  521. $default_shippable = $node->shippable;
  522. $download_status = FALSE;
  523. $download_value = NULL;
  524. $address_status = FALSE;
  525. $address_value = NULL;
  526. $time_status = FALSE;
  527. $quantity_value = NULL;
  528. $granularity_value = 'never';
  529. }
  530. $form['nid'] = array(
  531. '#type' => 'value',
  532. '#value' => $node->nid,
  533. );
  534. $form['pfid'] = array(
  535. '#type' => 'value',
  536. '#value' => $default_feature,
  537. );
  538. $form['uc_file_model'] = array(
  539. '#type' => 'select',
  540. '#title' => t('SKU'),
  541. '#default_value' => $default_model,
  542. '#description' => t('This is the SKU that will need to be purchased to obtain the file download.'),
  543. '#options' => $models,
  544. );
  545. $form['uc_file_filename'] = array(
  546. '#type' => 'textfield',
  547. '#title' => t('File download'),
  548. '#default_value' => $default_filename,
  549. '#autocomplete_path' => '_autocomplete_file',
  550. '#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))),
  551. '#maxlength' => 255,
  552. );
  553. $form['uc_file_description'] = array(
  554. '#type' => 'textfield',
  555. '#title' => t('Description'),
  556. '#default_value' => $default_description,
  557. '#maxlength' => 255,
  558. '#description' => t('A description of the download associated with the product.'),
  559. );
  560. $form['uc_file_shippable'] = array(
  561. '#type' => 'checkbox',
  562. '#title' => t('Shippable product'),
  563. '#default_value' => $default_shippable,
  564. '#description' => t('Check if this product model/SKU file download is also associated with a shippable product.'),
  565. );
  566. $form['uc_file_limits'] = array(
  567. '#type' => 'fieldset',
  568. '#description' => t('Use these options to override any global download limits set at the !url.', array('!url' => l(t('Ubercart product settings page'), 'admin/store/settings/products', array('query' => array('destination' => 'node/' . $node->nid . '/edit/features/file/add'))))),
  569. '#collapsed' => FALSE,
  570. '#collapsible' => FALSE,
  571. '#title' => t('File limitations'),
  572. );
  573. $form['uc_file_limits']['download_override'] = array(
  574. '#type' => 'checkbox',
  575. '#title' => t('Override download limit'),
  576. '#default_value' => $download_status,
  577. );
  578. $form['uc_file_limits']['download_limit_number'] = array(
  579. '#type' => 'textfield',
  580. '#title' => t('Downloads'),
  581. '#default_value' => $download_value,
  582. '#description' => t("The number of times this file can be downloaded."),
  583. '#maxlength' => 4,
  584. '#size' => 4,
  585. '#states' => array(
  586. 'visible' => array('input[name="download_override"]' => array('checked' => TRUE)),
  587. ),
  588. );
  589. $form['uc_file_limits']['location_override'] = array(
  590. '#type' => 'checkbox',
  591. '#title' => t('Override IP address limit'),
  592. '#default_value' => $address_status,
  593. );
  594. $form['uc_file_limits']['download_limit_addresses'] = array(
  595. '#type' => 'textfield',
  596. '#title' => t('IP addresses'),
  597. '#default_value' => $address_value,
  598. '#description' => t("The number of unique IPs that a file can be downloaded from."),
  599. '#maxlength' => 4,
  600. '#size' => 4,
  601. '#states' => array(
  602. 'visible' => array('input[name="location_override"]' => array('checked' => TRUE)),
  603. ),
  604. );
  605. $form['uc_file_limits']['time_override'] = array(
  606. '#type' => 'checkbox',
  607. '#title' => t('Override time limit'),
  608. '#default_value' => $time_status,
  609. );
  610. $form['uc_file_limits']['download_limit_duration_qty'] = array(
  611. '#type' => 'textfield',
  612. '#title' => t('Time'),
  613. '#default_value' => $quantity_value,
  614. '#size' => 4,
  615. '#maxlength' => 4,
  616. '#prefix' => '<div class="duration">',
  617. '#suffix' => '</div>',
  618. '#states' => array(
  619. 'disabled' => array('select[name="download_limit_duration_granularity"]' => array('value' => 'never')),
  620. 'visible' => array('input[name="time_override"]' => array('checked' => TRUE)),
  621. ),
  622. );
  623. $form['uc_file_limits']['download_limit_duration_granularity'] = array(
  624. '#type' => 'select',
  625. '#default_value' => $granularity_value,
  626. '#options' => array(
  627. 'never' => t('never'),
  628. 'day' => t('day(s)'),
  629. 'week' => t('week(s)'),
  630. 'month' => t('month(s)'),
  631. 'year' => t('year(s)')
  632. ),
  633. '#description' => t('How long after this product has been purchased until this file download expires.'),
  634. '#prefix' => '<div class="duration">',
  635. '#suffix' => '</div>',
  636. '#states' => array(
  637. 'visible' => array('input[name="time_override"]' => array('checked' => TRUE)),
  638. ),
  639. );
  640. return $form;
  641. }
  642. /**
  643. * Sanity check for file download and expiration overrides.
  644. *
  645. * @see uc_file_feature_form()
  646. * @see uc_file_feature_form_submit()
  647. */
  648. function uc_file_feature_form_validate($form, &$form_state) {
  649. // Ensure this is actually a file we control...
  650. if (!db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $form_state['values']['uc_file_filename']))->fetchField()) {
  651. form_set_error('uc_file_filename', t('%file is not a valid file or directory inside file download directory.', array('%file' => $form_state['values']['uc_file_filename'])));
  652. }
  653. // If any of our overrides are set, then we make sure they make sense.
  654. if ($form_state['values']['download_override'] &&
  655. $form_state['values']['download_limit_number'] < 0) {
  656. form_set_error('download_limit_number', t('A negative download limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  657. }
  658. if ($form_state['values']['location_override'] &&
  659. $form_state['values']['download_limit_addresses'] < 0) {
  660. form_set_error('download_limit_addresses', t('A negative IP address limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  661. }
  662. if ($form_state['values']['time_override'] &&
  663. $form_state['values']['download_limit_duration_granularity'] != 'never' &&
  664. $form_state['values']['download_limit_duration_qty'] < 1) {
  665. form_set_error('download_limit_duration_qty', t('You set the granularity (%gran), but you did not set how many. Please enter a positive non-zero integer.', array('%gran' => $form_state['values']['download_limit_duration_granularity'] . '(s)')));
  666. }
  667. }
  668. /**
  669. * Form submission handler for uc_file_feature_form().
  670. *
  671. * @see uc_file_feature_form()
  672. * @see uc_file_feature_form_submit()
  673. */
  674. function uc_file_feature_form_submit($form, &$form_state) {
  675. global $user;
  676. // Build the file_product object from the form values.
  677. $file = uc_file_get_by_name($form_state['values']['uc_file_filename']);
  678. $file_product = array(
  679. 'fid' => $file->fid,
  680. 'filename' => $file->filename,
  681. 'pfid' => $form_state['values']['pfid'],
  682. 'model' => $form_state['values']['uc_file_model'],
  683. 'description' => $form_state['values']['uc_file_description'],
  684. 'shippable' => $form_state['values']['uc_file_shippable'],
  685. // Local limitations... set them if there's an override.
  686. 'download_limit' => $form_state['values']['download_override'] ? $form_state['values']['download_limit_number' ] : UC_FILE_LIMIT_SENTINEL,
  687. 'address_limit' => $form_state['values']['location_override'] ? $form_state['values']['download_limit_addresses' ] : UC_FILE_LIMIT_SENTINEL,
  688. 'time_granularity' => $form_state['values']['time_override' ] ? $form_state['values']['download_limit_duration_granularity'] : UC_FILE_LIMIT_SENTINEL,
  689. 'time_quantity' => $form_state['values']['time_override' ] ? $form_state['values']['download_limit_duration_qty' ] : UC_FILE_LIMIT_SENTINEL,
  690. );
  691. // Build product feature descriptions.
  692. $description = t('<strong>SKU:</strong> !sku<br />', array('!sku' => empty($file_product['model']) ? 'Any' : $file_product['model']));
  693. if (is_dir(variable_get('uc_file_base_dir', NULL) . "/" . $file_product['filename'])) {
  694. $description .= t('<strong>Directory:</strong> !dir<br />', array('!dir' => $file_product['filename']));
  695. }
  696. else {
  697. $description .= t('<strong>File:</strong> !file<br />', array('!file' => basename($file_product['filename'])));
  698. }
  699. $description .= $file_product['shippable'] ? t('<strong>Shippable:</strong> Yes') : t('<strong>Shippable:</strong> No');
  700. $data = array(
  701. 'pfid' => $file_product['pfid'],
  702. 'nid' => $form_state['values']['nid'],
  703. 'fid' => 'file',
  704. 'description' => $description,
  705. );
  706. $form_state['redirect'] = uc_product_feature_save($data);
  707. $file_product['pfid'] = $data['pfid'];
  708. // Insert or update uc_file_product table.
  709. $key = array();
  710. if ($fpid = _uc_file_get_fpid($file_product['pfid'])) {
  711. $key = 'fpid';
  712. $file_product['fpid'] = $fpid;
  713. }
  714. drupal_write_record('uc_file_products', $file_product, $key);
  715. }
  716. /**
  717. * Gets a file_product id from a product feature id.
  718. */
  719. function _uc_file_get_fpid($pfid) {
  720. return db_query("SELECT fpid FROM {uc_file_products} WHERE pfid = :pfid", array(':pfid' => $pfid))->fetchField();
  721. }
  722. /**
  723. * Form builder for file settings.
  724. *
  725. * @see uc_file_feature_settings_validate()
  726. * @see uc_file_feature_settings_submit()
  727. * @ingroup forms
  728. */
  729. function uc_file_feature_settings($form, &$form_state) {
  730. $statuses = array();
  731. foreach (uc_order_status_list('general') as $status) {
  732. $statuses[$status['id']] = $status['title'];
  733. }
  734. $form['uc_file_base_dir'] = array(
  735. '#type' => 'textfield',
  736. '#title' => t('Files path'),
  737. '#description' => t('The absolute path (or relative to Drupal root) where files used for file downloads are located. For security reasons, it is recommended to choose a path outside the web root.'),
  738. '#default_value' => variable_get('uc_file_base_dir', NULL),
  739. );
  740. $form['uc_file_duplicate_warning'] = array(
  741. '#type' => 'checkbox',
  742. '#title' => t('Warn about purchasing duplicate files'),
  743. '#description' => t("If a customer attempts to purchase a product containing a file download, warn them and notify them that the download limits will be added onto their current limits."),
  744. '#default_value' => variable_get('uc_file_duplicate_warning', TRUE),
  745. );
  746. $form['uc_file_download_limit'] = array(
  747. '#type' => 'fieldset',
  748. '#title' => t('Default download limits'),
  749. '#collapsible' => FALSE,
  750. '#collapsed' => FALSE,
  751. );
  752. $form['uc_file_download_limit']['uc_file_download_limit_number'] = array(
  753. '#type' => 'textfield',
  754. '#title' => t('Downloads'),
  755. '#description' => t("The number of times a file can be downloaded. Leave empty to set no limit."),
  756. '#default_value' => variable_get('uc_file_download_limit_number', NULL),
  757. '#maxlength' => 4,
  758. '#size' => 4,
  759. );
  760. $form['uc_file_download_limit']['uc_file_download_limit_addresses'] = array(
  761. '#type' => 'textfield',
  762. '#title' => t('IP addresses'),
  763. '#description' => t("The number of unique IPs that a file can be downloaded from. Leave empty to set no limit."),
  764. '#default_value' => variable_get('uc_file_download_limit_addresses', NULL),
  765. '#maxlength' => 4,
  766. '#size' => 4,
  767. );
  768. $form['uc_file_download_limit']['uc_file_download_limit_duration_qty'] = array(
  769. '#type' => 'textfield',
  770. '#title' => t('Time'),
  771. '#default_value' => variable_get('uc_file_download_limit_duration_qty', NULL),
  772. '#size' => 4,
  773. '#maxlength' => 4,
  774. '#prefix' => '<div class="duration">',
  775. '#suffix' => '</div>',
  776. '#states' => array(
  777. 'disabled' => array('select[name="uc_file_download_limit_duration_granularity"]' => array('value' => 'never')),
  778. ),
  779. );
  780. $form['uc_file_download_limit']['uc_file_download_limit_duration_granularity'] = array(
  781. '#type' => 'select',
  782. '#options' => array(
  783. 'never' => t('never'),
  784. 'day' => t('day(s)'),
  785. 'week' => t('week(s)'),
  786. 'month' => t('month(s)'),
  787. 'year' => t('year(s)')
  788. ),
  789. '#default_value' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  790. '#description' => t('How long after a product has been purchased until its file download expires.'),
  791. '#prefix' => '<div class="duration">',
  792. '#suffix' => '</div>',
  793. );
  794. return $form;
  795. }
  796. /**
  797. * Sanity check for feature settings.
  798. *
  799. * @see uc_file_feature_settings()
  800. * @see uc_file_feature_settings_submit()
  801. */
  802. function uc_file_feature_settings_validate($form, &$form_state) {
  803. // Make sure our base directory is valid.
  804. if (!empty($form_state['values']['uc_file_base_dir']) && $form_state['values']['op'] == t('Save configuration') && !is_dir($form_state['values']['uc_file_base_dir'])) {
  805. form_set_error('uc_file_base_dir', t('%dir is not a valid file or directory', array('%dir' => $form_state['values']['uc_file_base_dir'])));
  806. }
  807. // If the user selected a granularity, let's make sure they
  808. // also selected a duration.
  809. if ($form_state['values']['uc_file_download_limit_duration_granularity'] != 'never' &&
  810. $form_state['values']['uc_file_download_limit_duration_qty'] < 1) {
  811. form_set_error('uc_file_download_limit_duration_qty', t('You set the granularity (%gran), but you did not set how many. Please enter a positive non-zero integer.', array('%gran' => $form_state['values']['uc_file_download_limit_duration_granularity'] . '(s)')));
  812. }
  813. // Make sure the download limit makes sense.
  814. if ($form_state['values']['uc_file_download_limit_number'] < 0) {
  815. form_set_error('uc_file_download_limit_number', t('A negative download limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  816. }
  817. // Make sure the address limit makes sense.
  818. if ($form_state['values']['uc_file_download_limit_addresses'] < 0) {
  819. form_set_error('uc_file_download_limit_addresses', t('A negative IP address limit does not make sense. Please enter a positive integer, or leave empty for no limit.'));
  820. }
  821. }
  822. /**
  823. * Form submission handler for uc_file_feature_settings().
  824. *
  825. * @see uc_file_feature_settings()
  826. * @see uc_file_feature_settings_validate()
  827. */
  828. function uc_file_feature_settings_submit($form, &$form_state) {
  829. // No directory now; truncate the file list.
  830. if (empty($form_state['values']['uc_file_base_dir'])) {
  831. uc_file_empty();
  832. }
  833. // Refresh file list since the directory changed.
  834. else {
  835. uc_file_refresh();
  836. }
  837. }
  838. /**
  839. * Accumulates numeric limits (as of now, download and address).
  840. *
  841. * We follow a couple simple rules here...
  842. *
  843. * If proposing no limit, it always overrides current.
  844. *
  845. * If proposal and current are limited, then accumulate, but only if it
  846. * wasn't a forced overwrite. (Think on the user account admin page where you
  847. * can set a download limit to '2'... you wouldn't then next time set it to '4'
  848. * and expect it to accumulate to '6' . You'd expect it to overwrite with
  849. * your '4'.)
  850. *
  851. * If current is unlimited, then a limited proposal will only overwrite in the
  852. * case of the forced overwrite explained above.
  853. */
  854. function _uc_file_number_accumulate_equation(&$current, $proposed, $force_overwrite) {
  855. // Right side 'unlimited' always succeeds.
  856. if (!$proposed) {
  857. $current = NULL;
  858. }
  859. // Right side and left side populated
  860. elseif ($current && $proposed) {
  861. // We don't add forced limits...
  862. if ($force_overwrite) {
  863. $current = $proposed;
  864. }
  865. else {
  866. $current += $proposed;
  867. }
  868. }
  869. // If it's a force (not a purchase e.g. user account settings), only then
  870. // will a limit succeed 'unlimited'.
  871. elseif ($force_overwrite && !$current && $proposed) {
  872. $current = $proposed;
  873. }
  874. }
  875. /**
  876. * Accumulates numeric limits (as of now, download and address).
  877. *
  878. * We follow a couple simple rules here...
  879. *
  880. * If proposing no limit, it always overrides current.
  881. *
  882. * If proposal and current are limited, then replace with the new expiration.
  883. *
  884. * If current is unlimited, then a limited proposal will only overwrite in the
  885. * case of the forced overwrited explained above.
  886. */
  887. function _uc_file_time_accumulate_equation(&$current, $proposed, $force_overwrite) {
  888. // Right side 'unlimited' always succeeds.
  889. if (!$proposed) {
  890. $current = NULL;
  891. }
  892. // Right side and left side populated. Replace.
  893. elseif ($current && $proposed) {
  894. $current = $proposed;
  895. }
  896. // If it's a force (not a purchase e.g. user account settings), only then
  897. // will a limit succeed 'unlimited' . We add the current time because our
  898. // expiration time is relative.
  899. elseif ($force_overwrite && !$current && $proposed) {
  900. $current = $proposed;
  901. }
  902. }
  903. /**
  904. * Accumulates limits and store them to the file_user array.
  905. */
  906. function _uc_file_accumulate_limits(&$file_user, $file_limits, $force_overwrite) {
  907. // Accumulate numerics.
  908. _uc_file_number_accumulate_equation($file_user['download_limit'], $file_limits['download_limit'], $force_overwrite);
  909. _uc_file_number_accumulate_equation($file_user['address_limit' ], $file_limits['address_limit' ], $force_overwrite);
  910. // Accumulate time.
  911. _uc_file_time_accumulate_equation($file_user['expiration'], $file_limits['expiration'], $force_overwrite);
  912. }
  913. /**
  914. * Implements Drupal autocomplete textfield.
  915. *
  916. * @return
  917. * Sends string containing javascript array of matched files.
  918. */
  919. function _uc_file_autocomplete_filename() {
  920. $matches = array();
  921. // Catch "/" characters that drupal autocomplete doesn't escape.
  922. $url = explode('_autocomplete_file/', request_uri());
  923. $string = $url[1];
  924. $files = db_query("SELECT filename FROM {uc_files} WHERE filename LIKE :name ORDER BY filename ASC", array(':name' => '%' . db_like($url[1]) . '%'));
  925. while ($filename = $files->fetchField()) {
  926. $matches[$filename] = $filename;
  927. }
  928. drupal_json_output($matches);
  929. }
  930. /**
  931. * Returns a date given an incrementation.
  932. *
  933. * $file_limits['time_polarity'] is either '+' or '-', indicating whether to
  934. * add or subtract the amount of time. $file_limits['time_granularity'] is a
  935. * unit of time like 'day', 'week', or 'never'. $file_limits['time_quantity']
  936. * is an amount of the previously mentioned unit... e.g.
  937. * $file_limits = array('time_polarity => '+', 'time_granularity' => 'day',
  938. * 'time_quantity' => 4); would read "4 days in the future."
  939. *
  940. * @param $file_limits
  941. * A keyed array containing the fields time_polarity, time_quantity,
  942. * and time_granularity.
  943. *
  944. * @return
  945. * A UNIX timestamp representing the amount of time the limits apply.
  946. */
  947. function _uc_file_expiration_date($file_limits, $timestamp) {
  948. // Never expires.
  949. if ($file_limits['time_granularity'] == 'never') {
  950. return NULL;
  951. }
  952. // If there's no change, return the old timestamp
  953. // (strtotime() would return FALSE).
  954. if (!$file_limits['time_quantity']) {
  955. return $timestamp;
  956. }
  957. if (!$timestamp) {
  958. $timestamp = REQUEST_TIME;
  959. }
  960. // Return the new expiration time.
  961. return strtotime($file_limits['time_polarity'] . $file_limits['time_quantity'] . ' ' . $file_limits['time_granularity'], $timestamp);
  962. }
  963. /**
  964. * Removes all downloadable files, as well as their associations.
  965. */
  966. function uc_file_empty() {
  967. $files = db_query("SELECT * FROM {uc_files}");
  968. foreach ($files as $file) {
  969. _uc_file_prune_db($file->fid);
  970. }
  971. }
  972. /**
  973. * Removes all db entries associated with a given $fid.
  974. */
  975. function _uc_file_prune_db($fid) {
  976. $pfids = db_query("SELECT pfid FROM {uc_file_products} WHERE fid = :fid", array(':fid' => $fid));
  977. while ($pfid = $pfids->fetchField()) {
  978. db_delete('uc_product_features')
  979. ->condition('pfid', $pfid)
  980. ->condition('fid', 'file')
  981. ->execute();
  982. db_delete('uc_file_products')
  983. ->condition('pfid', $pfid)
  984. ->execute();
  985. }
  986. db_delete('uc_file_users')
  987. ->condition('fid', $fid)
  988. ->execute();
  989. db_delete('uc_files')
  990. ->condition('fid', $fid)
  991. ->execute();
  992. }
  993. /**
  994. * Removes non-existent files.
  995. */
  996. function _uc_file_prune_files() {
  997. $files = db_query("SELECT * FROM {uc_files}");
  998. foreach ($files as $file) {
  999. $filename = uc_file_qualify_file($file->filename);
  1000. // It exists, leave it.
  1001. if (is_dir($filename) || is_file($filename)) {
  1002. continue;
  1003. }
  1004. // Remove associated db entries.
  1005. _uc_file_prune_db($file->fid);
  1006. }
  1007. }
  1008. /**
  1009. * Retrieves an updated list of available downloads.
  1010. */
  1011. function _uc_file_gather_files() {
  1012. // Don't bother if the directory isn't set.
  1013. if (!($dir = variable_get('uc_file_base_dir', NULL))) {
  1014. return;
  1015. }
  1016. // Grab files and prepare the base dir for appending.
  1017. $files = file_scan_directory($dir, variable_get('uc_file_file_mask', '/.*/'));
  1018. $dir = (substr($dir, -1) != '/' || substr($dir, -1) != '\\') ? $dir . '/' : $dir;
  1019. foreach ($files as $file) {
  1020. // Cut the base directory out of the path.
  1021. $filename = str_replace($dir, '', $file->uri);
  1022. $file_dir = dirname($filename);
  1023. $fid = NULL;
  1024. // Insert new entries.
  1025. if ($file_dir != '.' && !db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $file_dir . '/'))->fetchField()) {
  1026. $fid = db_insert('uc_files')
  1027. ->fields(array('filename' => $file_dir . '/'))
  1028. ->execute();
  1029. }
  1030. if (!db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $filename))->fetchField()) {
  1031. $fid = db_insert('uc_files')
  1032. ->fields(array('filename' => $filename))
  1033. ->execute();
  1034. }
  1035. // Invoke hook_uc_file_action().
  1036. if (!is_null($fid)) {
  1037. $file_object = uc_file_get_by_id($fid);
  1038. module_invoke_all('uc_file_action', 'insert', array('file_object' => $file_object));
  1039. unset($fid);
  1040. }
  1041. }
  1042. }
  1043. /**
  1044. * Removes non-existent files and update the downloadable list.
  1045. */
  1046. function uc_file_refresh() {
  1047. _uc_file_prune_files();
  1048. _uc_file_gather_files();
  1049. }
  1050. /**
  1051. * Deletes files (or directories).
  1052. *
  1053. * First, the file IDs are gathered according to whether or not we're recurring.
  1054. * The list is sorted in descending file system order (i.e. directories come
  1055. * last) to ensure the directories are empty when we start deleting them.
  1056. * Checks are done to ensure directories are empty before deleting them. All
  1057. * return values from file I/O functions are evaluated, and if they fail
  1058. * (say, because of permissions), then db entries are untouched. However,
  1059. * if the given file/path is deleted correctly, then all associations with
  1060. * products, product features, and users will be deleted, as well as the
  1061. * uc_file db entries.
  1062. *
  1063. * @param $fid
  1064. * An Ubercart file id.
  1065. * @param $recur
  1066. * Whether or not all files below this (if it's a directory) should be
  1067. * deleted as well.
  1068. *
  1069. * @return
  1070. * A boolean stating whether or not all requested operations succeeded.
  1071. */
  1072. function uc_file_remove_by_id($fid, $recur) {
  1073. // Store the overall status. Any fails will return FALSE through this.
  1074. $result = TRUE;
  1075. // Gather file(s) and sort in descending order. We do this
  1076. // to ensure we don't try to remove a directory before it's empty.
  1077. $fids = _uc_file_sort_fids(_uc_file_get_dir_file_ids($fid, $recur));
  1078. foreach ($fids as $fid) {
  1079. $remove_fields = FALSE;
  1080. // Qualify the path for I/O, and delete the files/dirs.
  1081. $filename = db_query("SELECT filename FROM {uc_files} WHERE fid = :fid", array(':fid' => $fid))->fetchField();
  1082. $dir = uc_file_qualify_file($filename);
  1083. if (is_dir($dir)) {
  1084. // Only if it's empty.
  1085. $dir_contents = file_scan_directory($dir, '/.*/', array('recurse' => FALSE));
  1086. if (empty($dir_contents)) {
  1087. if (rmdir($dir)) {
  1088. drupal_set_message(t('The directory %dir was deleted.', array('%dir' => $filename)));
  1089. $remove_fields = TRUE;
  1090. }
  1091. else {
  1092. drupal_set_message(t('The directory %dir could not be deleted.', array('%dir' => $filename)), 'warning');
  1093. $result = FALSE;
  1094. }
  1095. }
  1096. else {
  1097. drupal_set_message(t('The directory %dir could not be deleted because it is not empty.', array('%dir' => $filename)), 'warning');
  1098. $result = FALSE;
  1099. }
  1100. }
  1101. else {
  1102. if (unlink($dir)) {
  1103. $remove_fields = TRUE;
  1104. drupal_set_message(t('The file %dir was deleted.', array('%dir' => $filename)));
  1105. }
  1106. else {
  1107. drupal_set_message(t('The file %dir could not be deleted.', array('%dir' => $filename)), 'error');
  1108. $result = FALSE;
  1109. }
  1110. }
  1111. // Remove related tables.
  1112. if ($remove_fields) {
  1113. _uc_file_prune_db($fid);
  1114. }
  1115. }
  1116. return $result;
  1117. }
  1118. /**
  1119. * Returns a list of file ids that are in the directory.
  1120. *
  1121. * @param $fid
  1122. * The file id associated with the directory.
  1123. * @param $recursive
  1124. * Whether or not to list recursive directories and their files.
  1125. *
  1126. * @return
  1127. * If there are files in the directory, returns an array of file ids.
  1128. * Else returns FALSE.
  1129. */
  1130. function _uc_file_get_dir_file_ids($fids, $recursive = FALSE) {
  1131. $result = array();
  1132. // Handle an array or just a single.
  1133. if (!is_array($fids)) {
  1134. $fids = array($fids);
  1135. }
  1136. foreach ($fids as $fid) {
  1137. // Get everything inside and below the given directory, or if it's file,
  1138. // just the file. We'll handle recursion later.
  1139. if (!($base = uc_file_get_by_id($fid))) {
  1140. continue;
  1141. }
  1142. $base_name = $base->filename . (is_dir(uc_file_qualify_file($base->filename)) ? '%' : '');
  1143. $files = db_query("SELECT * FROM {uc_files} WHERE filename LIKE :name", array(':name' => $base_name));
  1144. // PHP str_replace() can't replace only N matches, so we use regex. First
  1145. // we escape our file slashes, though, ... using str_replace().
  1146. $base_name = str_replace("\\", "\\\\", $base_name);
  1147. $base_name = str_replace("/", "\/", $base_name);
  1148. foreach ($files as $file) {
  1149. // Make the file path relative to the given directory.
  1150. $filename_change = preg_replace('/' . $base_name . '/', '', $file->filename, 1);
  1151. // Remove any leading slash.
  1152. $filename = (substr($filename_change, 0, 1) == '/') ? substr($filename_change, 1) : $filename_change;
  1153. // Recurring, or a file? Add it.
  1154. if ($recursive || !strpos($filename, '/')) {
  1155. $result[] = $file->fid;
  1156. }
  1157. }
  1158. }
  1159. return array_unique($result);
  1160. }
  1161. /**
  1162. * Sorts by 'filename' values.
  1163. */
  1164. function _uc_file_sort_by_name($l, $r) {
  1165. return strcasecmp($l['filename'], $r['filename']);
  1166. }
  1167. /**
  1168. * Takes a list of file ids and sort the list by the associated filenames.
  1169. *
  1170. * @param $fids
  1171. * The array of file ids.
  1172. *
  1173. * @return
  1174. * The sorted array of file ids.
  1175. */
  1176. function _uc_file_sort_names($fids) {
  1177. $result = $aggregate = array();
  1178. foreach ($fids as $fid) {
  1179. $file = uc_file_get_by_id($fid);
  1180. $aggregate[] = array('filename' => $file->filename, 'fid' => $file->fid);
  1181. }
  1182. usort($aggregate, '_uc_file_sort_by_name');
  1183. foreach ($aggregate as $file) {
  1184. $result[] = $file['fid'];
  1185. }
  1186. return $result;
  1187. }
  1188. /**
  1189. * Takes a list of file ids and sort the list in descending order.
  1190. *
  1191. * @param $fids
  1192. * The array of file ids.
  1193. *
  1194. * @return
  1195. * The sorted array of file ids.
  1196. */
  1197. function _uc_file_sort_fids($fids) {
  1198. $dir_fids = array();
  1199. $output = array();
  1200. foreach ($fids as $fid) {
  1201. $file = uc_file_get_by_id($fid);
  1202. $filename = $file->filename;
  1203. // Store the files first.
  1204. if (substr($filename, -1) != '/') {
  1205. $output[] = $fid;
  1206. }
  1207. // Store the directories for next.
  1208. else {
  1209. $dir_fids[$fid] = $filename;
  1210. }
  1211. }
  1212. // Order the directories using a count of the slashes in each path name.
  1213. while (!empty($dir_fids)) {
  1214. $highest = 0;
  1215. foreach ($dir_fids as $dir_fid => $filename) {
  1216. // Find the most slashes. (Furthest down.)
  1217. if (substr_count($filename, '/') > $highest) {
  1218. $highest = substr_count($filename, '/');
  1219. $highest_fid = $dir_fid;
  1220. }
  1221. }
  1222. // Output the dir and remove it from candidates.
  1223. $output[] = $highest_fid;
  1224. unset($dir_fids[$highest_fid]);
  1225. }
  1226. return $output;
  1227. }
  1228. /**
  1229. * Qualifies a given path with the base Ubercart file download path.
  1230. *
  1231. * @param $filename
  1232. * The name of the path to qualify.
  1233. *
  1234. * @return
  1235. * The qualified path.
  1236. */
  1237. function uc_file_qualify_file($filename) {
  1238. return variable_get('uc_file_base_dir', NULL) . '/' . $filename;
  1239. }
  1240. /**
  1241. * Removes all of a user's downloadable files.
  1242. *
  1243. * @param $uid
  1244. * A Drupal user ID.
  1245. */
  1246. function uc_file_remove_user($user) {
  1247. $query = db_delete('uc_file_users')
  1248. ->condition('uid', $user->uid);
  1249. // Echo the deletion only if something was actually deleted.
  1250. if ($query->execute()) {
  1251. drupal_set_message(t('!user has had all of his/her downloadable files removed.', array(
  1252. '!user' => theme('username', array(
  1253. 'account' => $user,
  1254. 'name' => check_plain($user->name),
  1255. 'link_path' => 'user/' . $user->uid,
  1256. )),
  1257. )));
  1258. }
  1259. }
  1260. /**
  1261. * Removes a user's downloadable file by hash key.
  1262. *
  1263. * @param $uid
  1264. * A Drupal user ID.
  1265. * @param $key
  1266. * The unique hash associated with the file.
  1267. */
  1268. function uc_file_remove_user_file_by_id($user, $fid) {
  1269. $file = uc_file_get_by_id($fid);
  1270. $query = db_delete('uc_file_users')
  1271. ->condition('uid', $user->uid)
  1272. ->condition('fid', $fid);
  1273. // Echo the deletion only if something was actually deleted.
  1274. if ($query->execute()) {
  1275. drupal_set_message(t('!user has had %file removed from his/her downloadable file list.', array(
  1276. '!user' => theme('username', array(
  1277. 'account' => $user,
  1278. 'name' => check_plain($user->name),
  1279. 'link_path' => 'user/' . $user->uid,
  1280. )),
  1281. '%file' => $file->filename,
  1282. )));
  1283. }
  1284. }
  1285. /**
  1286. * Central cache for all file data.
  1287. */
  1288. function &_uc_file_get_cache() {
  1289. static $cache = array();
  1290. return $cache;
  1291. }
  1292. /**
  1293. * Flush our cache.
  1294. */
  1295. function _uc_file_flush_cache() {
  1296. $cache = _uc_file_get_cache();
  1297. $cache = array();
  1298. }
  1299. /**
  1300. * Retrieves a file by name.
  1301. *
  1302. * @param $filename
  1303. * An unqualified file path.
  1304. *
  1305. * @return
  1306. * A uc_file object.
  1307. */
  1308. function &uc_file_get_by_name($filename) {
  1309. $cache = _uc_file_get_cache();
  1310. if (!isset($cache[$filename])) {
  1311. $cache[$filename] = db_query("SELECT * FROM {uc_files} WHERE filename = :name", array(':name' => $filename))->fetchObject();
  1312. }
  1313. return $cache[$filename];
  1314. }
  1315. /**
  1316. * Retrieves a file by file ID.
  1317. *
  1318. * @param $fid
  1319. * A file ID.
  1320. *
  1321. * @return
  1322. * A uc_file object.
  1323. */
  1324. function &uc_file_get_by_id($fid) {
  1325. $cache = _uc_file_get_cache();
  1326. if (!isset($cache[$fid])) {
  1327. $cache[$fid] = db_query("SELECT * FROM {uc_files} WHERE fid = :fid", array(':fid' => $fid))->fetchObject();
  1328. }
  1329. return $cache[$fid];
  1330. }
  1331. /**
  1332. * Retrieves a file by hash key.
  1333. *
  1334. * @param $key
  1335. * A hash key.
  1336. *
  1337. * @return
  1338. * A uc_file object.
  1339. */
  1340. function &uc_file_get_by_key($key) {
  1341. $cache = _uc_file_get_cache();
  1342. if (!isset($cache[$key])) {
  1343. $cache[$key] = db_query("SELECT * FROM {uc_file_users} ufu " .
  1344. "LEFT JOIN {uc_files} uf ON uf.fid = ufu.fid " .
  1345. "WHERE ufu.file_key = :key", array(':key' => $key))->fetchObject();
  1346. $cache[$key]->addresses = unserialize($cache[$key]->addresses);
  1347. }
  1348. return $cache[$key];
  1349. }
  1350. /**
  1351. * Retrieves a file by user ID.
  1352. *
  1353. * @param $uid
  1354. * A user ID.
  1355. * @param $fid
  1356. * A file ID.
  1357. *
  1358. * @return
  1359. * A uc_file object.
  1360. */
  1361. function &uc_file_get_by_uid($uid, $fid) {
  1362. $cache = _uc_file_get_cache();
  1363. if (!isset($cache[$uid][$fid])) {
  1364. $cache[$uid][$fid] = db_query("SELECT * FROM {uc_file_users} ufu " .
  1365. "LEFT JOIN {uc_files} uf ON uf.fid = ufu.fid " .
  1366. "WHERE ufu.fid = :fid AND ufu.uid = :uid", array(':uid' => $uid, ':fid' => $fid))->fetchObject();
  1367. if ($cache[$uid][$fid]) {
  1368. $cache[$uid][$fid]->addresses = unserialize($cache[$uid][$fid]->addresses);
  1369. }
  1370. }
  1371. return $cache[$uid][$fid];
  1372. }
  1373. /**
  1374. * Adds file(s) to a user's list of downloadable files, accumulating limits.
  1375. *
  1376. * First the function sees if the given file ID is a file or a directory,
  1377. * if it's a directory, it gathers all the files under it recursively.
  1378. * Then all the gathered IDs are iterated over, loading each file and
  1379. * aggregating all the data necessary to save a file_user object. Limits derived
  1380. * from the file are accumulated with the current limits for this user on this
  1381. * file (if an association exists yet). The data is then hashed, and the hash
  1382. * is stored in the file_user object. The object is then written to the
  1383. * file_users table.
  1384. *
  1385. * @param $fid
  1386. * A file ID.
  1387. * @param $user
  1388. * A Drupal user object.
  1389. * @param $pfid
  1390. * An Ubercart product feature ID.
  1391. * @param $file_limits
  1392. * The limits inherited from this file.
  1393. * @param $force_overwrite
  1394. * Don't accumulate, assign.
  1395. *
  1396. * @return
  1397. * An array of uc_file objects.
  1398. */
  1399. function uc_file_user_renew($fid, $user, $pfid, $file_limits, $force_overwrite) {
  1400. $result = array();
  1401. // Data shared between all files passed.
  1402. $user_file_global = array(
  1403. 'uid' => $user->uid,
  1404. 'pfid' => $pfid,
  1405. );
  1406. // Get the file(s).
  1407. $fids = _uc_file_get_dir_file_ids($fid, TRUE);
  1408. foreach ($fids as $fid) {
  1409. $file_user = _uc_file_user_get($user, $fid);
  1410. // Doesn't exist yet?
  1411. $key = array();
  1412. if (!$file_user) {
  1413. $file_user = array(
  1414. 'granted' => REQUEST_TIME,
  1415. 'accessed' => 0,
  1416. 'addresses' => array(),
  1417. );
  1418. $force_overwrite = TRUE;
  1419. }
  1420. else {
  1421. $file_user = (array)$file_user;
  1422. $key = 'fuid';
  1423. }
  1424. // Add file data in as well.
  1425. $file_info = (array)uc_file_get_by_id($fid);
  1426. $file_user += $user_file_global + $file_info;
  1427. _uc_file_accumulate_limits($file_user, $file_limits, $force_overwrite);
  1428. // Workaround for d#226264 ...
  1429. $file_user['download_limit'] = $file_user['download_limit'] ? $file_user['download_limit'] : 0;
  1430. $file_user['address_limit'] = $file_user['address_limit'] ? $file_user['address_limit'] : 0;
  1431. $file_user['expiration'] = $file_user['expiration'] ? $file_user['expiration'] : 0;
  1432. // Calculate hash.
  1433. $file_user['file_key'] = isset($file_user['file_key']) && $file_user['file_key'] ? $file_user['file_key'] : drupal_get_token(serialize($file_user));
  1434. // Write and queue the file_user object.
  1435. drupal_write_record('uc_file_users', $file_user, $key);
  1436. if ($key) {
  1437. watchdog('uc_file', '%user has had download privileges of %file renewed.', array('%user' => format_username($user), '%file' => $file_user['filename']));
  1438. }
  1439. else {
  1440. watchdog('uc_file', '%user has been allowed to download %file.', array('%user' => format_username($user), '%file' => $file_user['filename']));
  1441. }
  1442. $result[] = (object)$file_user;
  1443. }
  1444. return $result;
  1445. }
  1446. /**
  1447. * Retrieves a file_user object by user and fid.
  1448. */
  1449. function _uc_file_user_get($user, $fid) {
  1450. $file_user = db_query("SELECT * FROM {uc_file_users} WHERE uid = :uid AND fid = :fid", array(':uid' => $user->uid, ':fid' => $fid))->fetchObject();
  1451. if ($file_user) {
  1452. $file_user->addresses = unserialize($file_user->addresses);
  1453. }
  1454. return $file_user;
  1455. }
  1456. /**
  1457. * Gets the maximum number of downloads for a given file.
  1458. *
  1459. * If there are no file-specific download limits set, the function returns
  1460. * the global limits. Otherwise the limits from the file are returned.
  1461. *
  1462. * @param $file
  1463. * A uc_file_products object.
  1464. *
  1465. * @return
  1466. * The maximum number of downloads.
  1467. */
  1468. function uc_file_get_download_limit($file) {
  1469. if (!isset($file->download_limit) || $file->download_limit == UC_FILE_LIMIT_SENTINEL) {
  1470. return variable_get('uc_file_download_limit_number', NULL);
  1471. }
  1472. else {
  1473. return $file->download_limit;
  1474. }
  1475. }
  1476. /**
  1477. * Gets the maximum number of locations a file can be downloaded from.
  1478. *
  1479. * If there are no file-specific location limits set, the function returns
  1480. * the global limits. Otherwise the limits from the file are returned.
  1481. *
  1482. * @param $file
  1483. * A uc_file_products object.
  1484. *
  1485. * @return
  1486. * The maximum number of locations.
  1487. */
  1488. function uc_file_get_address_limit($file) {
  1489. if (!isset($file->address_limit) || $file->address_limit == UC_FILE_LIMIT_SENTINEL) {
  1490. return variable_get('uc_file_download_limit_addresses', NULL);
  1491. }
  1492. else {
  1493. return $file->address_limit;
  1494. }
  1495. }
  1496. /**
  1497. * Gets the time expiration for a given file.
  1498. *
  1499. * If there are no file-specific time limits set, the function returns the
  1500. * global limits. Otherwise the limits from the file are returned.
  1501. *
  1502. * @param $file
  1503. * A uc_file_products object.
  1504. *
  1505. * @return
  1506. * An array with entries for the granularity and quantity.
  1507. */
  1508. function uc_file_get_time_limit($file) {
  1509. if (!isset($file->time_granularity) || $file->time_granularity == UC_FILE_LIMIT_SENTINEL) {
  1510. return array(
  1511. 'time_polarity' => '+',
  1512. 'time_granularity' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  1513. 'time_quantity' => variable_get('uc_file_download_limit_duration_qty', NULL),
  1514. );
  1515. }
  1516. else {
  1517. return array(
  1518. 'time_polarity' => '+',
  1519. 'time_granularity' => $file->time_granularity,
  1520. 'time_quantity' => $file->time_quantity,
  1521. );
  1522. }
  1523. }