uc_file.module 57 KB

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