uc_file.module 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743
  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. )));
  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. * Product feature delete function.
  461. */
  462. function uc_file_feature_delete($feature) {
  463. db_delete('uc_file_products')
  464. ->condition('pfid', $feature['pfid'])
  465. ->execute();
  466. }
  467. /**
  468. * Form builder for hook_uc_product_feature.
  469. *
  470. * @see uc_file_feature_form_validate()
  471. * @see uc_file_feature_form_submit()
  472. * @ingroup forms
  473. */
  474. function uc_file_feature_form($form, &$form_state, $node, $feature) {
  475. if (!is_dir(variable_get('uc_file_base_dir', NULL))) {
  476. 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'))), 'error');
  477. unset($form['buttons']);
  478. return $form;
  479. }
  480. if (!db_query_range('SELECT 1 FROM {uc_files}', 0, 1)->fetchField()) {
  481. $form['file']['file_message'] = array(
  482. '#markup' => t(
  483. 'You must add files at the <a href="@url">Ubercart file download administration page</a> in order to attach them to a model.',
  484. array('@url' => url('admin/store/products/files', array('query' => array('destination' => 'node/' . $node->nid . '/edit/features/file/add'))))
  485. ),
  486. );
  487. unset($form['buttons']);
  488. return $form;
  489. }
  490. // Make sure we have an up-to-date list for the autocompletion.
  491. uc_file_refresh();
  492. // Grab all the models on this product.
  493. $models = uc_product_get_models($node->nid);
  494. // Use the feature's values to fill the form, if they exist.
  495. if (!empty($feature)) {
  496. $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();
  497. $default_feature = $feature['pfid'];
  498. $default_model = $file_product->model;
  499. $default_filename = $file_product->filename;
  500. $default_description = $file_product->description;
  501. $default_shippable = $file_product->shippable;
  502. $download_status = $file_product->download_limit != UC_FILE_LIMIT_SENTINEL;
  503. $download_value = $download_status ? $file_product->download_limit : NULL;
  504. $address_status = $file_product->address_limit != UC_FILE_LIMIT_SENTINEL;
  505. $address_value = $address_status ? $file_product->address_limit : NULL;
  506. $time_status = $file_product->time_granularity != UC_FILE_LIMIT_SENTINEL;
  507. $quantity_value = $time_status ? $file_product->time_quantity : NULL;
  508. $granularity_value = $time_status ? $file_product->time_granularity : 'never';
  509. }
  510. else {
  511. $file_product = FALSE;
  512. $default_feature = NULL;
  513. $default_model = '';
  514. $default_filename = '';
  515. $default_description = '';
  516. $default_shippable = $node->shippable;
  517. $download_status = FALSE;
  518. $download_value = NULL;
  519. $address_status = FALSE;
  520. $address_value = NULL;
  521. $time_status = FALSE;
  522. $quantity_value = NULL;
  523. $granularity_value = 'never';
  524. }
  525. $form['nid'] = array(
  526. '#type' => 'value',
  527. '#value' => $node->nid,
  528. );
  529. $form['pfid'] = array(
  530. '#type' => 'value',
  531. '#value' => $default_feature,
  532. );
  533. $form['uc_file_model'] = array(
  534. '#type' => 'select',
  535. '#title' => t('SKU'),
  536. '#default_value' => $default_model,
  537. '#description' => t('This is the SKU that will need to be purchased to obtain the file download.'),
  538. '#options' => $models,
  539. );
  540. $form['uc_file_filename'] = array(
  541. '#type' => 'textfield',
  542. '#title' => t('File download'),
  543. '#default_value' => $default_filename,
  544. '#autocomplete_path' => '_autocomplete_file',
  545. '#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))),
  546. '#maxlength' => 255,
  547. );
  548. $form['uc_file_description'] = array(
  549. '#type' => 'textfield',
  550. '#title' => t('Description'),
  551. '#default_value' => $default_description,
  552. '#maxlength' => 255,
  553. '#description' => t('A description of the download associated with the product.'),
  554. );
  555. $form['uc_file_shippable'] = array(
  556. '#type' => 'checkbox',
  557. '#title' => t('Shippable product'),
  558. '#default_value' => $default_shippable,
  559. '#description' => t('Check if this product model/SKU file download is also associated with a shippable product.'),
  560. );
  561. $form['uc_file_limits'] = array(
  562. '#type' => 'fieldset',
  563. '#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'))))),
  564. '#collapsed' => FALSE,
  565. '#collapsible' => FALSE,
  566. '#title' => t('File limitations'),
  567. );
  568. $form['uc_file_limits']['download_override'] = array(
  569. '#type' => 'checkbox',
  570. '#title' => t('Override download limit'),
  571. '#default_value' => $download_status,
  572. );
  573. $form['uc_file_limits']['download_limit_number'] = array(
  574. '#type' => 'textfield',
  575. '#title' => t('Downloads'),
  576. '#default_value' => $download_value,
  577. '#description' => t("The number of times this file can be downloaded."),
  578. '#maxlength' => 4,
  579. '#size' => 4,
  580. '#states' => array(
  581. 'visible' => array('input[name="download_override"]' => array('checked' => TRUE)),
  582. ),
  583. );
  584. $form['uc_file_limits']['location_override'] = array(
  585. '#type' => 'checkbox',
  586. '#title' => t('Override IP address limit'),
  587. '#default_value' => $address_status,
  588. );
  589. $form['uc_file_limits']['download_limit_addresses'] = array(
  590. '#type' => 'textfield',
  591. '#title' => t('IP addresses'),
  592. '#default_value' => $address_value,
  593. '#description' => t("The number of unique IPs that a file can be downloaded from."),
  594. '#maxlength' => 4,
  595. '#size' => 4,
  596. '#states' => array(
  597. 'visible' => array('input[name="location_override"]' => array('checked' => TRUE)),
  598. ),
  599. );
  600. $form['uc_file_limits']['time_override'] = array(
  601. '#type' => 'checkbox',
  602. '#title' => t('Override time limit'),
  603. '#default_value' => $time_status,
  604. );
  605. $form['uc_file_limits']['download_limit_duration_qty'] = array(
  606. '#type' => 'textfield',
  607. '#title' => t('Time'),
  608. '#default_value' => $quantity_value,
  609. '#size' => 4,
  610. '#maxlength' => 4,
  611. '#prefix' => '<div class="duration">',
  612. '#suffix' => '</div>',
  613. '#states' => array(
  614. 'disabled' => array('select[name="download_limit_duration_granularity"]' => array('value' => 'never')),
  615. 'visible' => array('input[name="time_override"]' => array('checked' => TRUE)),
  616. ),
  617. );
  618. $form['uc_file_limits']['download_limit_duration_granularity'] = array(
  619. '#type' => 'select',
  620. '#default_value' => $granularity_value,
  621. '#options' => array(
  622. 'never' => t('never'),
  623. 'day' => t('day(s)'),
  624. 'week' => t('week(s)'),
  625. 'month' => t('month(s)'),
  626. 'year' => t('year(s)')
  627. ),
  628. '#description' => t('How long after this product has been purchased until this file download expires.'),
  629. '#prefix' => '<div class="duration">',
  630. '#suffix' => '</div>',
  631. '#states' => array(
  632. 'visible' => array('input[name="time_override"]' => array('checked' => TRUE)),
  633. ),
  634. );
  635. return $form;
  636. }
  637. /**
  638. * Sanity check for file download and expiration overrides.
  639. *
  640. * @see uc_file_feature_form()
  641. * @see uc_file_feature_form_submit()
  642. */
  643. function uc_file_feature_form_validate($form, &$form_state) {
  644. // Ensure this is actually a file we control...
  645. if (!db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $form_state['values']['uc_file_filename']))->fetchField()) {
  646. 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'])));
  647. }
  648. // If any of our overrides are set, then we make sure they make sense.
  649. if ($form_state['values']['download_override'] &&
  650. $form_state['values']['download_limit_number'] < 0) {
  651. 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.'));
  652. }
  653. if ($form_state['values']['location_override'] &&
  654. $form_state['values']['download_limit_addresses'] < 0) {
  655. 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.'));
  656. }
  657. if ($form_state['values']['time_override'] &&
  658. $form_state['values']['download_limit_duration_granularity'] != 'never' &&
  659. $form_state['values']['download_limit_duration_qty'] < 1) {
  660. 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)')));
  661. }
  662. }
  663. /**
  664. * Form submission handler for uc_file_feature_form().
  665. *
  666. * @see uc_file_feature_form()
  667. * @see uc_file_feature_form_submit()
  668. */
  669. function uc_file_feature_form_submit($form, &$form_state) {
  670. global $user;
  671. // Build the file_product object from the form values.
  672. $file = uc_file_get_by_name($form_state['values']['uc_file_filename']);
  673. $file_product = array(
  674. 'fid' => $file->fid,
  675. 'filename' => $file->filename,
  676. 'pfid' => $form_state['values']['pfid'],
  677. 'model' => $form_state['values']['uc_file_model'],
  678. 'description' => $form_state['values']['uc_file_description'],
  679. 'shippable' => $form_state['values']['uc_file_shippable'],
  680. // Local limitations... set them if there's an override.
  681. 'download_limit' => $form_state['values']['download_override'] ? $form_state['values']['download_limit_number' ] : UC_FILE_LIMIT_SENTINEL,
  682. 'address_limit' => $form_state['values']['location_override'] ? $form_state['values']['download_limit_addresses' ] : UC_FILE_LIMIT_SENTINEL,
  683. 'time_granularity' => $form_state['values']['time_override' ] ? $form_state['values']['download_limit_duration_granularity'] : UC_FILE_LIMIT_SENTINEL,
  684. 'time_quantity' => $form_state['values']['time_override' ] ? $form_state['values']['download_limit_duration_qty' ] : UC_FILE_LIMIT_SENTINEL,
  685. );
  686. // Build product feature descriptions.
  687. $description = t('<strong>SKU:</strong> !sku<br />', array('!sku' => empty($file_product['model']) ? 'Any' : $file_product['model']));
  688. if (is_dir(variable_get('uc_file_base_dir', NULL) . "/" . $file_product['filename'])) {
  689. $description .= t('<strong>Directory:</strong> !dir<br />', array('!dir' => $file_product['filename']));
  690. }
  691. else {
  692. $description .= t('<strong>File:</strong> !file<br />', array('!file' => basename($file_product['filename'])));;
  693. }
  694. $description .= $file_product['shippable'] ? t('<strong>Shippable:</strong> Yes') : t('<strong>Shippable:</strong> No');
  695. $data = array(
  696. 'pfid' => $file_product['pfid'],
  697. 'nid' => $form_state['values']['nid'],
  698. 'fid' => 'file',
  699. 'description' => $description,
  700. );
  701. $form_state['redirect'] = uc_product_feature_save($data);
  702. $file_product['pfid'] = $data['pfid'];
  703. // Insert or update uc_file_product table.
  704. $key = array();
  705. if ($fpid = _uc_file_get_fpid($file_product['pfid'])) {
  706. $key = 'fpid';
  707. $file_product['fpid'] = $fpid;
  708. }
  709. drupal_write_record('uc_file_products', $file_product, $key);
  710. }
  711. /**
  712. * Gets a file_product id from a product feature id.
  713. */
  714. function _uc_file_get_fpid($pfid) {
  715. return db_query("SELECT fpid FROM {uc_file_products} WHERE pfid = :pfid", array(':pfid' => $pfid))->fetchField();
  716. }
  717. /**
  718. * Form builder for file settings.
  719. *
  720. * @see uc_file_feature_settings_validate()
  721. * @see uc_file_feature_settings_submit()
  722. * @ingroup forms
  723. */
  724. function uc_file_feature_settings($form, &$form_state) {
  725. $statuses = array();
  726. foreach (uc_order_status_list('general') as $status) {
  727. $statuses[$status['id']] = $status['title'];
  728. }
  729. $form['uc_file_base_dir'] = array(
  730. '#type' => 'textfield',
  731. '#title' => t('Files path'),
  732. '#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.'),
  733. '#default_value' => variable_get('uc_file_base_dir', NULL),
  734. );
  735. $form['uc_file_duplicate_warning'] = array(
  736. '#type' => 'checkbox',
  737. '#title' => t('Warn about purchasing duplicate files'),
  738. '#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."),
  739. '#default_value' => variable_get('uc_file_duplicate_warning', TRUE),
  740. );
  741. $form['uc_file_download_limit'] = array(
  742. '#type' => 'fieldset',
  743. '#title' => t('Default download limits'),
  744. '#collapsible' => FALSE,
  745. '#collapsed' => FALSE,
  746. );
  747. $form['uc_file_download_limit']['uc_file_download_limit_number'] = array(
  748. '#type' => 'textfield',
  749. '#title' => t('Downloads'),
  750. '#description' => t("The number of times a file can be downloaded. Leave empty to set no limit."),
  751. '#default_value' => variable_get('uc_file_download_limit_number', NULL),
  752. '#maxlength' => 4,
  753. '#size' => 4,
  754. );
  755. $form['uc_file_download_limit']['uc_file_download_limit_addresses'] = array(
  756. '#type' => 'textfield',
  757. '#title' => t('IP addresses'),
  758. '#description' => t("The number of unique IPs that a file can be downloaded from. Leave empty to set no limit."),
  759. '#default_value' => variable_get('uc_file_download_limit_addresses', NULL),
  760. '#maxlength' => 4,
  761. '#size' => 4,
  762. );
  763. $form['uc_file_download_limit']['uc_file_download_limit_duration_qty'] = array(
  764. '#type' => 'textfield',
  765. '#title' => t('Time'),
  766. '#default_value' => variable_get('uc_file_download_limit_duration_qty', NULL),
  767. '#size' => 4,
  768. '#maxlength' => 4,
  769. '#prefix' => '<div class="duration">',
  770. '#suffix' => '</div>',
  771. '#states' => array(
  772. 'disabled' => array('select[name="uc_file_download_limit_duration_granularity"]' => array('value' => 'never')),
  773. ),
  774. );
  775. $form['uc_file_download_limit']['uc_file_download_limit_duration_granularity'] = array(
  776. '#type' => 'select',
  777. '#options' => array(
  778. 'never' => t('never'),
  779. 'day' => t('day(s)'),
  780. 'week' => t('week(s)'),
  781. 'month' => t('month(s)'),
  782. 'year' => t('year(s)')
  783. ),
  784. '#default_value' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  785. '#description' => t('How long after a product has been purchased until its file download expires.'),
  786. '#prefix' => '<div class="duration">',
  787. '#suffix' => '</div>',
  788. );
  789. return $form;
  790. }
  791. /**
  792. * Sanity check for feature settings.
  793. *
  794. * @see uc_file_feature_settings()
  795. * @see uc_file_feature_settings_submit()
  796. */
  797. function uc_file_feature_settings_validate($form, &$form_state) {
  798. // Make sure our base directory is valid.
  799. 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'])) {
  800. 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'])));
  801. }
  802. // If the user selected a granularity, let's make sure they
  803. // also selected a duration.
  804. if ($form_state['values']['uc_file_download_limit_duration_granularity'] != 'never' &&
  805. $form_state['values']['uc_file_download_limit_duration_qty'] < 1) {
  806. 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)')));
  807. }
  808. // Make sure the download limit makes sense.
  809. if ($form_state['values']['uc_file_download_limit_number'] < 0) {
  810. 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.'));
  811. }
  812. // Make sure the address limit makes sense.
  813. if ($form_state['values']['uc_file_download_limit_addresses'] < 0) {
  814. 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.'));
  815. }
  816. }
  817. /**
  818. * Form submission handler for uc_file_feature_settings().
  819. *
  820. * @see uc_file_feature_settings()
  821. * @see uc_file_feature_settings_validate()
  822. */
  823. function uc_file_feature_settings_submit($form, &$form_state) {
  824. // No directory now; truncate the file list.
  825. if (empty($form_state['values']['uc_file_base_dir'])) {
  826. uc_file_empty();
  827. }
  828. // Refresh file list since the directory changed.
  829. else {
  830. uc_file_refresh();
  831. }
  832. }
  833. /**
  834. * Accumulates numeric limits (as of now, download and address).
  835. *
  836. * We follow a couple simple rules here...
  837. *
  838. * If proposing no limit, it always overrides current.
  839. *
  840. * If proposal and current are limited, then accumulate, but only if it
  841. * wasn't a forced overwrite. (Think on the user account admin page where you
  842. * can set a download limit to '2'... you wouldn't then next time set it to '4'
  843. * and expect it to accumulate to '6' . You'd expect it to overwrite with
  844. * your '4'.)
  845. *
  846. * If current is unlimited, then a limited proposal will only overwrite in the
  847. * case of the forced overwrite explained above.
  848. */
  849. function _uc_file_number_accumulate_equation(&$current, $proposed, $force_overwrite) {
  850. // Right side 'unlimited' always succeeds.
  851. if (!$proposed) {
  852. $current = NULL;
  853. }
  854. // Right side and left side populated
  855. elseif ($current && $proposed) {
  856. // We don't add forced limits...
  857. if ($force_overwrite) {
  858. $current = $proposed;
  859. }
  860. else {
  861. $current += $proposed;
  862. }
  863. }
  864. // If it's a force (not a purchase e.g. user account settings), only then
  865. // will a limit succeed 'unlimited'.
  866. elseif ($force_overwrite && !$current && $proposed) {
  867. $current = $proposed;
  868. }
  869. }
  870. /**
  871. * Accumulates numeric limits (as of now, download and address).
  872. *
  873. * We follow a couple simple rules here...
  874. *
  875. * If proposing no limit, it always overrides current.
  876. *
  877. * If proposal and current are limited, then replace with the new expiration.
  878. *
  879. * If current is unlimited, then a limited proposal will only overwrite in the
  880. * case of the forced overwrited explained above.
  881. */
  882. function _uc_file_time_accumulate_equation(&$current, $proposed, $force_overwrite) {
  883. // Right side 'unlimited' always succeeds.
  884. if (!$proposed) {
  885. $current = NULL;
  886. }
  887. // Right side and left side populated. Replace.
  888. elseif ($current && $proposed) {
  889. $current = $proposed;
  890. }
  891. // If it's a force (not a purchase e.g. user account settings), only then
  892. // will a limit succeed 'unlimited' . We add the current time because our
  893. // expiration time is relative.
  894. elseif ($force_overwrite && !$current && $proposed) {
  895. $current = $proposed;
  896. }
  897. }
  898. /**
  899. * Accumulates limits and store them to the file_user array.
  900. */
  901. function _uc_file_accumulate_limits(&$file_user, $file_limits, $force_overwrite) {
  902. // Accumulate numerics.
  903. _uc_file_number_accumulate_equation($file_user['download_limit'], $file_limits['download_limit'], $force_overwrite);
  904. _uc_file_number_accumulate_equation($file_user['address_limit' ], $file_limits['address_limit' ], $force_overwrite);
  905. // Accumulate time.
  906. _uc_file_time_accumulate_equation($file_user['expiration'], $file_limits['expiration'], $force_overwrite);
  907. }
  908. /**
  909. * Implements Drupal autocomplete textfield.
  910. *
  911. * @return
  912. * Sends string containing javascript array of matched files.
  913. */
  914. function _uc_file_autocomplete_filename() {
  915. $matches = array();
  916. // Catch "/" characters that drupal autocomplete doesn't escape.
  917. $url = explode('_autocomplete_file/', request_uri());
  918. $string = $url[1];
  919. $files = db_query("SELECT filename FROM {uc_files} WHERE filename LIKE :name ORDER BY filename ASC", array(':name' => '%' . db_like($url[1]) . '%'));
  920. while ($filename = $files->fetchField()) {
  921. $matches[$filename] = $filename;
  922. }
  923. drupal_json_output($matches);
  924. }
  925. /**
  926. * Returns a date given an incrementation.
  927. *
  928. * $file_limits['time_polarity'] is either '+' or '-', indicating whether to
  929. * add or subtract the amount of time. $file_limits['time_granularity'] is a
  930. * unit of time like 'day', 'week', or 'never'. $file_limits['time_quantity']
  931. * is an amount of the previously mentioned unit... e.g.
  932. * $file_limits = array('time_polarity => '+', 'time_granularity' => 'day',
  933. * 'time_quantity' => 4); would read "4 days in the future."
  934. *
  935. * @param $file_limits
  936. * A keyed array containing the fields time_polarity, time_quantity,
  937. * and time_granularity.
  938. *
  939. * @return
  940. * A UNIX timestamp representing the amount of time the limits apply.
  941. */
  942. function _uc_file_expiration_date($file_limits, $timestamp) {
  943. // Never expires.
  944. if ($file_limits['time_granularity'] == 'never') {
  945. return NULL;
  946. }
  947. // If there's no change, return the old timestamp
  948. // (strtotime() would return FALSE).
  949. if (!$file_limits['time_quantity']) {
  950. return $timestamp;
  951. }
  952. if (!$timestamp) {
  953. $timestamp = REQUEST_TIME;
  954. }
  955. // Return the new expiration time.
  956. return strtotime($file_limits['time_polarity'] . $file_limits['time_quantity'] . ' ' . $file_limits['time_granularity'], $timestamp);
  957. }
  958. /**
  959. * Removes all downloadable files, as well as their associations.
  960. */
  961. function uc_file_empty() {
  962. $files = db_query("SELECT * FROM {uc_files}");
  963. foreach ($files as $file) {
  964. _uc_file_prune_db($file->fid);
  965. }
  966. }
  967. /**
  968. * Removes all db entries associated with a given $fid.
  969. */
  970. function _uc_file_prune_db($fid) {
  971. $pfids = db_query("SELECT pfid FROM {uc_file_products} WHERE fid = :fid", array(':fid' => $fid));
  972. while ($pfid = $pfids->fetchField()) {
  973. db_delete('uc_product_features')
  974. ->condition('pfid', $pfid)
  975. ->condition('fid', 'file')
  976. ->execute();
  977. db_delete('uc_file_products')
  978. ->condition('pfid', $pfid)
  979. ->execute();
  980. }
  981. db_delete('uc_file_users')
  982. ->condition('fid', $fid)
  983. ->execute();
  984. db_delete('uc_files')
  985. ->condition('fid', $fid)
  986. ->execute();
  987. }
  988. /**
  989. * Removes non-existent files.
  990. */
  991. function _uc_file_prune_files() {
  992. $files = db_query("SELECT * FROM {uc_files}");
  993. foreach ($files as $file) {
  994. $filename = uc_file_qualify_file($file->filename);
  995. // It exists, leave it.
  996. if (is_dir($filename) || is_file($filename)) {
  997. continue;
  998. }
  999. // Remove associated db entries.
  1000. _uc_file_prune_db($file->fid);
  1001. }
  1002. }
  1003. /**
  1004. * Retrieves an updated list of available downloads.
  1005. */
  1006. function _uc_file_gather_files() {
  1007. // Don't bother if the directory isn't set.
  1008. if (!($dir = variable_get('uc_file_base_dir', NULL))) {
  1009. return;
  1010. }
  1011. // Grab files and prepare the base dir for appending.
  1012. $files = file_scan_directory($dir, variable_get('uc_file_file_mask', '/.*/'));
  1013. $dir = (substr($dir, -1) != '/' || substr($dir, -1) != '\\') ? $dir . '/' : $dir;
  1014. foreach ($files as $file) {
  1015. // Cut the base directory out of the path.
  1016. $filename = str_replace($dir, '', $file->uri);
  1017. $file_dir = dirname($filename);
  1018. $fid = NULL;
  1019. // Insert new entries.
  1020. if ($file_dir != '.' && !db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $file_dir . '/'))->fetchField()) {
  1021. $fid = db_insert('uc_files')
  1022. ->fields(array('filename' => $file_dir . '/'))
  1023. ->execute();
  1024. }
  1025. if (!db_query("SELECT fid FROM {uc_files} WHERE filename = :name", array(':name' => $filename))->fetchField()) {
  1026. $fid = db_insert('uc_files')
  1027. ->fields(array('filename' => $filename))
  1028. ->execute();
  1029. }
  1030. // Invoke hook_uc_file_action().
  1031. if (!is_null($fid)) {
  1032. $file_object = uc_file_get_by_id($fid);
  1033. module_invoke_all('uc_file_action', 'insert', array('file_object' => $file_object));
  1034. unset($fid);
  1035. }
  1036. }
  1037. }
  1038. /**
  1039. * Removes non-existent files and update the downloadable list.
  1040. */
  1041. function uc_file_refresh() {
  1042. _uc_file_prune_files();
  1043. _uc_file_gather_files();
  1044. }
  1045. /**
  1046. * Deletes files (or directories).
  1047. *
  1048. * First, the file IDs are gathered according to whether or not we're recurring.
  1049. * The list is sorted in descending file system order (i.e. directories come
  1050. * last) to ensure the directories are empty when we start deleting them.
  1051. * Checks are done to ensure directories are empty before deleting them. All
  1052. * return values from file I/O functions are evaluated, and if they fail
  1053. * (say, because of permissions), then db entries are untouched. However,
  1054. * if the given file/path is deleted correctly, then all associations with
  1055. * products, product features, and users will be deleted, as well as the
  1056. * uc_file db entries.
  1057. *
  1058. * @param $fid
  1059. * An Ubercart file id.
  1060. * @param $recur
  1061. * Whether or not all files below this (if it's a directory) should be
  1062. * deleted as well.
  1063. *
  1064. * @return
  1065. * A boolean stating whether or not all requested operations succeeded.
  1066. */
  1067. function uc_file_remove_by_id($fid, $recur) {
  1068. // Store the overall status. Any fails will return FALSE through this.
  1069. $result = TRUE;
  1070. // Gather file(s) and sort in descending order. We do this
  1071. // to ensure we don't try to remove a directory before it's empty.
  1072. $fids = _uc_file_sort_fids(_uc_file_get_dir_file_ids($fid, $recur));
  1073. foreach ($fids as $fid) {
  1074. $remove_fields = FALSE;
  1075. // Qualify the path for I/O, and delete the files/dirs.
  1076. $filename = db_query("SELECT filename FROM {uc_files} WHERE fid = :fid", array(':fid' => $fid))->fetchField();
  1077. $dir = uc_file_qualify_file($filename);
  1078. if (is_dir($dir)) {
  1079. // Only if it's empty.
  1080. $dir_contents = file_scan_directory($dir, '/.*/', array('recurse' => FALSE));
  1081. if (empty($dir_contents)) {
  1082. if (rmdir($dir)) {
  1083. drupal_set_message(t('The directory %dir was deleted.', array('%dir' => $filename)));
  1084. $remove_fields = TRUE;
  1085. }
  1086. else {
  1087. drupal_set_message(t('The directory %dir could not be deleted.', array('%dir' => $filename)));
  1088. $result = FALSE;
  1089. }
  1090. }
  1091. else {
  1092. drupal_set_message(t('The directory %dir could not be deleted because it is not empty.', array('%dir' => $filename)));
  1093. $result = FALSE;
  1094. }
  1095. }
  1096. else {
  1097. if (unlink($dir)) {
  1098. $remove_fields = TRUE;
  1099. drupal_set_message(t('The file %dir was deleted.', array('%dir' => $filename)));
  1100. }
  1101. else {
  1102. drupal_set_message(t('The file %dir could not be deleted.', array('%dir' => $filename)));
  1103. $result = FALSE;
  1104. }
  1105. }
  1106. // Remove related tables.
  1107. if ($remove_fields) {
  1108. _uc_file_prune_db($fid);
  1109. }
  1110. }
  1111. return $result;
  1112. }
  1113. /**
  1114. * Returns a list of file ids that are in the directory.
  1115. *
  1116. * @param $fid
  1117. * The file id associated with the directory.
  1118. * @param $recursive
  1119. * Whether or not to list recursive directories and their files.
  1120. *
  1121. * @return
  1122. * If there are files in the directory, returns an array of file ids.
  1123. * Else returns FALSE.
  1124. */
  1125. function _uc_file_get_dir_file_ids($fids, $recursive = FALSE) {
  1126. $result = array();
  1127. // Handle an array or just a single.
  1128. if (!is_array($fids)) {
  1129. $fids = array($fids);
  1130. }
  1131. foreach ($fids as $fid) {
  1132. // Get everything inside and below the given directory, or if it's file,
  1133. // just the file. We'll handle recursion later.
  1134. if (!($base = uc_file_get_by_id($fid))) {
  1135. continue;
  1136. }
  1137. $base_name = $base->filename . (is_dir(uc_file_qualify_file($base->filename)) ? '%' : '');
  1138. $files = db_query("SELECT * FROM {uc_files} WHERE filename LIKE :name", array(':name' => $base_name));
  1139. // PHP str_replace() can't replace only N matches, so we use regex. First
  1140. // we escape our file slashes, though, ... using str_replace().
  1141. $base_name = str_replace("\\", "\\\\", $base_name);
  1142. $base_name = str_replace("/", "\/", $base_name);
  1143. foreach ($files as $file) {
  1144. // Make the file path relative to the given directory.
  1145. $filename_change = preg_replace('/' . $base_name . '/', '', $file->filename, 1);
  1146. // Remove any leading slash.
  1147. $filename = (substr($filename_change, 0, 1) == '/') ? substr($filename_change, 1) : $filename_change;
  1148. // Recurring, or a file? Add it.
  1149. if ($recursive || !strpos($filename, '/')) {
  1150. $result[] = $file->fid;
  1151. }
  1152. }
  1153. }
  1154. return array_unique($result);
  1155. }
  1156. /**
  1157. * Sorts by 'filename' values.
  1158. */
  1159. function _uc_file_sort_by_name($l, $r) {
  1160. return strcasecmp($l['filename'], $r['filename']);
  1161. }
  1162. /**
  1163. * Takes a list of file ids and sort the list by the associated filenames.
  1164. *
  1165. * @param $fids
  1166. * The array of file ids.
  1167. *
  1168. * @return
  1169. * The sorted array of file ids.
  1170. */
  1171. function _uc_file_sort_names($fids) {
  1172. $result = $aggregate = array();
  1173. foreach ($fids as $fid) {
  1174. $file = uc_file_get_by_id($fid);
  1175. $aggregate[] = array('filename' => $file->filename, 'fid' => $file->fid);
  1176. }
  1177. usort($aggregate, '_uc_file_sort_by_name');
  1178. foreach ($aggregate as $file) {
  1179. $result[] = $file['fid'];
  1180. }
  1181. return $result;
  1182. }
  1183. /**
  1184. * Takes a list of file ids and sort the list in descending order.
  1185. *
  1186. * @param $fids
  1187. * The array of file ids.
  1188. *
  1189. * @return
  1190. * The sorted array of file ids.
  1191. */
  1192. function _uc_file_sort_fids($fids) {
  1193. $dir_fids = array();
  1194. $output = array();
  1195. foreach ($fids as $fid) {
  1196. $file = uc_file_get_by_id($fid);
  1197. $filename = $file->filename;
  1198. // Store the files first.
  1199. if (substr($filename, -1) != '/') {
  1200. $output[] = $fid;
  1201. }
  1202. // Store the directories for next.
  1203. else {
  1204. $dir_fids[$fid] = $filename;
  1205. }
  1206. }
  1207. // Order the directories using a count of the slashes in each path name.
  1208. while (!empty($dir_fids)) {
  1209. $highest = 0;
  1210. foreach ($dir_fids as $dir_fid => $filename) {
  1211. // Find the most slashes. (Furthest down.)
  1212. if (substr_count($filename, '/') > $highest) {
  1213. $highest = substr_count($filename, '/');
  1214. $highest_fid = $dir_fid;
  1215. }
  1216. }
  1217. // Output the dir and remove it from candidates.
  1218. $output[] = $highest_fid;
  1219. unset($dir_fids[$highest_fid]);
  1220. }
  1221. return $output;
  1222. }
  1223. /**
  1224. * Qualifies a given path with the base Ubercart file download path.
  1225. *
  1226. * @param $filename
  1227. * The name of the path to qualify.
  1228. *
  1229. * @return
  1230. * The qualified path.
  1231. */
  1232. function uc_file_qualify_file($filename) {
  1233. return variable_get('uc_file_base_dir', NULL) . '/' . $filename;
  1234. }
  1235. /**
  1236. * Removes all of a user's downloadable files.
  1237. *
  1238. * @param $uid
  1239. * A Drupal user ID.
  1240. */
  1241. function uc_file_remove_user($user) {
  1242. $query = db_delete('uc_file_users')
  1243. ->condition('uid', $user->uid);
  1244. // Echo the deletion only if something was actually deleted.
  1245. if ($query->execute()) {
  1246. drupal_set_message(t('!user has had all of his/her downloadable files removed.', array(
  1247. '!user' => theme('username', array(
  1248. 'account' => $user,
  1249. 'name' => check_plain($user->name),
  1250. 'link_path' => 'user/' . $user->uid,
  1251. )),
  1252. )));
  1253. }
  1254. }
  1255. /**
  1256. * Removes a user's downloadable file by hash key.
  1257. *
  1258. * @param $uid
  1259. * A Drupal user ID.
  1260. * @param $key
  1261. * The unique hash associated with the file.
  1262. */
  1263. function uc_file_remove_user_file_by_id($user, $fid) {
  1264. $file = uc_file_get_by_id($fid);
  1265. $query = db_delete('uc_file_users')
  1266. ->condition('uid', $user->uid)
  1267. ->condition('fid', $fid);
  1268. // Echo the deletion only if something was actually deleted.
  1269. if ($query->execute()) {
  1270. drupal_set_message(t('!user has had %file removed from his/her downloadable file list.', array(
  1271. '!user' => theme('username', array(
  1272. 'account' => $user,
  1273. 'name' => check_plain($user->name),
  1274. 'link_path' => 'user/' . $user->uid,
  1275. )),
  1276. '%file' => $file->filename,
  1277. )));
  1278. }
  1279. }
  1280. /**
  1281. * Central cache for all file data.
  1282. */
  1283. function &_uc_file_get_cache() {
  1284. static $cache = array();
  1285. return $cache;
  1286. }
  1287. /**
  1288. * Flush our cache.
  1289. */
  1290. function _uc_file_flush_cache() {
  1291. $cache = _uc_file_get_cache();
  1292. $cache = array();
  1293. }
  1294. /**
  1295. * Retrieves a file by name.
  1296. *
  1297. * @param $filename
  1298. * An unqualified file path.
  1299. *
  1300. * @return
  1301. * A uc_file object.
  1302. */
  1303. function &uc_file_get_by_name($filename) {
  1304. $cache = _uc_file_get_cache();
  1305. if (!isset($cache[$filename])) {
  1306. $cache[$filename] = db_query("SELECT * FROM {uc_files} WHERE filename = :name", array(':name' => $filename))->fetchObject();
  1307. }
  1308. return $cache[$filename];
  1309. }
  1310. /**
  1311. * Retrieves a file by file ID.
  1312. *
  1313. * @param $fid
  1314. * A file ID.
  1315. *
  1316. * @return
  1317. * A uc_file object.
  1318. */
  1319. function &uc_file_get_by_id($fid) {
  1320. $cache = _uc_file_get_cache();
  1321. if (!isset($cache[$fid])) {
  1322. $cache[$fid] = db_query("SELECT * FROM {uc_files} WHERE fid = :fid", array(':fid' => $fid))->fetchObject();
  1323. }
  1324. return $cache[$fid];
  1325. }
  1326. /**
  1327. * Retrieves a file by hash key.
  1328. *
  1329. * @param $key
  1330. * A hash key.
  1331. *
  1332. * @return
  1333. * A uc_file object.
  1334. */
  1335. function &uc_file_get_by_key($key) {
  1336. $cache = _uc_file_get_cache();
  1337. if (!isset($cache[$key])) {
  1338. $cache[$key] = db_query("SELECT * FROM {uc_file_users} ufu " .
  1339. "LEFT JOIN {uc_files} uf ON uf.fid = ufu.fid " .
  1340. "WHERE ufu.file_key = :key", array(':key' => $key))->fetchObject();
  1341. $cache[$key]->addresses = unserialize($cache[$key]->addresses);
  1342. }
  1343. return $cache[$key];
  1344. }
  1345. /**
  1346. * Retrieves a file by user ID.
  1347. *
  1348. * @param $uid
  1349. * A user ID.
  1350. * @param $fid
  1351. * A file ID.
  1352. *
  1353. * @return
  1354. * A uc_file object.
  1355. */
  1356. function &uc_file_get_by_uid($uid, $fid) {
  1357. $cache = _uc_file_get_cache();
  1358. if (!isset($cache[$uid][$fid])) {
  1359. $cache[$uid][$fid] = db_query("SELECT * FROM {uc_file_users} ufu " .
  1360. "LEFT JOIN {uc_files} uf ON uf.fid = ufu.fid " .
  1361. "WHERE ufu.fid = :fid AND ufu.uid = :uid", array(':uid' => $uid, ':fid' => $fid))->fetchObject();
  1362. if ($cache[$uid][$fid]) {
  1363. $cache[$uid][$fid]->addresses = unserialize($cache[$uid][$fid]->addresses);
  1364. }
  1365. }
  1366. return $cache[$uid][$fid];
  1367. }
  1368. /**
  1369. * Adds file(s) to a user's list of downloadable files, accumulating limits.
  1370. *
  1371. * First the function sees if the given file ID is a file or a directory,
  1372. * if it's a directory, it gathers all the files under it recursively.
  1373. * Then all the gathered IDs are iterated over, loading each file and
  1374. * aggregating all the data necessary to save a file_user object. Limits derived
  1375. * from the file are accumulated with the current limits for this user on this
  1376. * file (if an association exists yet). The data is then hashed, and the hash
  1377. * is stored in the file_user object. The object is then written to the
  1378. * file_users table.
  1379. *
  1380. * @param $fid
  1381. * A file ID.
  1382. * @param $user
  1383. * A Drupal user object.
  1384. * @param $pfid
  1385. * An Ubercart product feature ID.
  1386. * @param $file_limits
  1387. * The limits inherited from this file.
  1388. * @param $force_overwrite
  1389. * Don't accumulate, assign.
  1390. *
  1391. * @return
  1392. * An array of uc_file objects.
  1393. */
  1394. function uc_file_user_renew($fid, $user, $pfid, $file_limits, $force_overwrite) {
  1395. $result = array();
  1396. // Data shared between all files passed.
  1397. $user_file_global = array(
  1398. 'uid' => $user->uid,
  1399. 'pfid' => $pfid,
  1400. );
  1401. // Get the file(s).
  1402. $fids = _uc_file_get_dir_file_ids($fid, TRUE);
  1403. foreach ($fids as $fid) {
  1404. $file_user = _uc_file_user_get($user, $fid);
  1405. // Doesn't exist yet?
  1406. $key = array();
  1407. if (!$file_user) {
  1408. $file_user = array(
  1409. 'granted' => REQUEST_TIME,
  1410. 'accessed' => 0,
  1411. 'addresses' => array(),
  1412. );
  1413. $force_overwrite = TRUE;
  1414. }
  1415. else {
  1416. $file_user = (array)$file_user;
  1417. $key = 'fuid';
  1418. }
  1419. // Add file data in as well.
  1420. $file_info = (array)uc_file_get_by_id($fid);
  1421. $file_user += $user_file_global + $file_info;
  1422. _uc_file_accumulate_limits($file_user, $file_limits, $force_overwrite);
  1423. // Workaround for d#226264 ...
  1424. $file_user['download_limit'] = $file_user['download_limit'] ? $file_user['download_limit'] : 0;
  1425. $file_user['address_limit'] = $file_user['address_limit'] ? $file_user['address_limit'] : 0;
  1426. $file_user['expiration'] = $file_user['expiration'] ? $file_user['expiration'] : 0;
  1427. // Calculate hash.
  1428. $file_user['file_key'] = isset($file_user['file_key']) && $file_user['file_key'] ? $file_user['file_key'] : drupal_get_token(serialize($file_user));
  1429. // Write and queue the file_user object.
  1430. drupal_write_record('uc_file_users', $file_user, $key);
  1431. if ($key) {
  1432. watchdog('uc_file', '%user has had download privileges of %file renewed.', array('%user' => format_username($user), '%file' => $file_user['filename']));
  1433. }
  1434. else {
  1435. watchdog('uc_file', '%user has been allowed to download %file.', array('%user' => format_username($user), '%file' => $file_user['filename']));
  1436. }
  1437. $result[] = (object)$file_user;
  1438. }
  1439. return $result;
  1440. }
  1441. /**
  1442. * Retrieves a file_user object by user and fid.
  1443. */
  1444. function _uc_file_user_get($user, $fid) {
  1445. $file_user = db_query("SELECT * FROM {uc_file_users} WHERE uid = :uid AND fid = :fid", array(':uid' => $user->uid, ':fid' => $fid))->fetchObject();
  1446. if ($file_user) {
  1447. $file_user->addresses = unserialize($file_user->addresses);
  1448. }
  1449. return $file_user;
  1450. }
  1451. /**
  1452. * Gets the maximum number of downloads for a given file.
  1453. *
  1454. * If there are no file-specific download limits set, the function returns
  1455. * the global limits. Otherwise the limits from the file are returned.
  1456. *
  1457. * @param $file
  1458. * A uc_file_products object.
  1459. *
  1460. * @return
  1461. * The maximum number of downloads.
  1462. */
  1463. function uc_file_get_download_limit($file) {
  1464. if (!isset($file->download_limit) || $file->download_limit == UC_FILE_LIMIT_SENTINEL) {
  1465. return variable_get('uc_file_download_limit_number', NULL);
  1466. }
  1467. else {
  1468. return $file->download_limit;
  1469. }
  1470. }
  1471. /**
  1472. * Gets the maximum number of locations a file can be downloaded from.
  1473. *
  1474. * If there are no file-specific location limits set, the function returns
  1475. * the global limits. Otherwise the limits from the file are returned.
  1476. *
  1477. * @param $file
  1478. * A uc_file_products object.
  1479. *
  1480. * @return
  1481. * The maximum number of locations.
  1482. */
  1483. function uc_file_get_address_limit($file) {
  1484. if (!isset($file->address_limit) || $file->address_limit == UC_FILE_LIMIT_SENTINEL) {
  1485. return variable_get('uc_file_download_limit_addresses', NULL);
  1486. }
  1487. else {
  1488. return $file->address_limit;
  1489. }
  1490. }
  1491. /**
  1492. * Gets the time expiration for a given file.
  1493. *
  1494. * If there are no file-specific time limits set, the function returns the
  1495. * global limits. Otherwise the limits from the file are returned.
  1496. *
  1497. * @param $file
  1498. * A uc_file_products object.
  1499. *
  1500. * @return
  1501. * An array with entries for the granularity and quantity.
  1502. */
  1503. function uc_file_get_time_limit($file) {
  1504. if (!isset($file->time_granularity) || $file->time_granularity == UC_FILE_LIMIT_SENTINEL) {
  1505. return array(
  1506. 'time_polarity' => '+',
  1507. 'time_granularity' => variable_get('uc_file_download_limit_duration_granularity', 'never'),
  1508. 'time_quantity' => variable_get('uc_file_download_limit_duration_qty', NULL),
  1509. );
  1510. }
  1511. else {
  1512. return array(
  1513. 'time_polarity' => '+',
  1514. 'time_granularity' => $file->time_granularity,
  1515. 'time_quantity' => $file->time_quantity,
  1516. );
  1517. }
  1518. }