filefield.module

Tracking 5.x-2.x branch
  1. drupal
    1. 5 contributions/filefield/filefield.module
    2. 6 contributions/filefield/filefield.module

Defines a file field type.

uses content.module to store the fid, and the drupal files table to store the actual file data.

Functions & methods

NameDescription
filefield_check_directoryCreate the file directory relative to the 'files' dir recursively for every directory in the path.
filefield_clear_field_session
filefield_clear_session
filefield_default_item
filefield_fieldImplementation of hook_field().
filefield_field_formatter
filefield_field_formatter_infoImplementation of hook_field formatter. @todo: finish transformer.module and integrate like imagecache with imagefield.
filefield_field_infoImplementation of hook_field_info().
filefield_field_settingsImplementation of hook_field_settings().
filefield_file_download
filefield_file_insertinsert a file into the database.
filefield_file_updateupdate the file record if necessary
filefield_icon_urlDetermine the most appropriate icon for the given file's mimetype.
filefield_jsMenu callback for JavaScript-based uploads.
filefield_menu
filefield_perm
filefield_requirementsImplementation of hook_requirements().
filefield_token_list
filefield_token_values
filefield_views_handler_filter_is_not_nullCustom filter for filefield NOT NULL
filefield_widgetImplementation of hook_widget().
filefield_widget_infoImplementation of hook_widget_info().
filefield_widget_settingsImplementation of hook_widget_settings().
theme_filefield
theme_filefield_form_current
theme_filefield_icon
theme_filefield_view_file
_filefield_create_icon_path
_filefield_file_delete
_filefield_file_form
_filefield_file_form_description_resetThis after_build function is needed as fix for tricky Form API behaviour: When using filefield without AJAX uploading, the description field was not updated to a new '#default_value' because the textfield has been submitted, which causes…
_filefield_file_load
_filefield_generic_icon_map
_filefield_icon_path
_filefield_previewtransfer a file that is in a 'preview' state. @todo multiple support
_filefield_widget_form
_filefield_widget_prepare_form_values
_filefield_widget_validateValidate the form widget.

Constants

NameDescription
FILEFIELD_MINIMUM_PHP

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Defines a file field type.
  5. *
  6. * uses content.module to store the fid, and the drupal files
  7. * table to store the actual file data.
  8. */
  9. define('FILEFIELD_MINIMUM_PHP', '5.0');
  10. function filefield_menu($may_cache) {
  11. $items = array();
  12. if ($may_cache) {
  13. $items[] = array(
  14. 'path' => 'filefield/js',
  15. 'callback' => 'filefield_js',
  16. //'access' => user_access(),
  17. 'access' => TRUE,
  18. 'type' => MENU_CALLBACK
  19. );
  20. }
  21. else if ($_SESSION['filefield']) {
  22. // Add handlers for previewing new uploads.
  23. foreach ($_SESSION['filefield'] as $fieldname => $files) {
  24. if (is_array($files)) {
  25. foreach($files as $delta => $file) {
  26. if ($file['preview']) {
  27. $items[] = array(
  28. 'path' => $file['preview'],
  29. 'callback' => '_filefield_preview',
  30. 'access' => TRUE,
  31. 'type' => MENU_CALLBACK
  32. );
  33. }
  34. }
  35. }
  36. }
  37. }
  38. return $items;
  39. }
  40. /**
  41. * Implementation of hook_requirements().
  42. */
  43. function filefield_requirements($phase) {
  44. $requirements = array();
  45. // Ensure translations don't break at install time
  46. $t = get_t();
  47. if ($phase == 'runtime' && version_compare(phpversion(), FILEFIELD_MINIMUM_PHP) < 0) {
  48. $requirements['filefield_php'] = array(
  49. 'title' => $t('FileField PHP'),
  50. 'description' => $t('FileField recommends at least PHP %version. Older versions of PHP are not supported but may function.', array('%version' => FILEFIELD_MINIMUM_PHP)),
  51. 'severity' => REQUIREMENT_WARNING
  52. );
  53. }
  54. return $requirements;
  55. }
  56. /**
  57. * transfer a file that is in a 'preview' state.
  58. * @todo multiple support
  59. */
  60. function _filefield_preview() {
  61. foreach ($_SESSION['filefield'] as $fieldname => $files) {
  62. foreach ($files as $delta => $file) {
  63. if ($file['preview'] == $_GET['q']) {
  64. file_transfer($file['filepath'], array('Content-Type: '. mime_header_encode($file['filemime']),
  65. 'Content-Length: '. $file['filesize']));
  66. exit();
  67. }
  68. }
  69. }
  70. }
  71. function filefield_perm() {
  72. return array('view filefield uploads');
  73. }
  74. /**
  75. * Implementation of hook_field_info().
  76. */
  77. function filefield_field_info() {
  78. return array(
  79. 'file' => array('label' => 'File'),
  80. );
  81. }
  82. /**
  83. * Implementation of hook_field_settings().
  84. */
  85. function filefield_field_settings($op, $field) {
  86. switch ($op) {
  87. case 'form':
  88. $form = array();
  89. $form['force_list'] = array(
  90. '#type' => 'checkbox',
  91. '#title' => t('Always list files'),
  92. '#default_value' => isset($field['force_list']) ? $field['force_list'] : 0,
  93. '#description' => t('If enabled, the "List" checkbox will be hidden and files are always shown. Otherwise, the user can choose for each file whether it should be listed or not.'),
  94. );
  95. return $form;
  96. case 'validate':
  97. break;
  98. case 'save':
  99. return array('force_list');
  100. case 'database columns':
  101. $columns = array(
  102. 'fid' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'),
  103. 'description' => array('type' => 'varchar', length => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
  104. 'list' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'),
  105. );
  106. return $columns;
  107. case 'filters':
  108. return array(
  109. 'not null' => array(
  110. 'operator' => array('=' => t('Has file')),
  111. 'list' => 'views_handler_operator_yesno',
  112. 'list-type' => 'select',
  113. 'handler' => 'filefield_views_handler_filter_is_not_null',
  114. ),
  115. );
  116. }
  117. }
  118. function filefield_default_item() {
  119. return array(
  120. 'fid' => 0,
  121. 'description' => '',
  122. 'list' => 0,
  123. );
  124. }
  125. /**
  126. * insert a file into the database.
  127. * @param $node
  128. * node object file will be associated with.
  129. * @param $file
  130. * file to be inserted, passed by reference since fid should be attached.
  131. *
  132. */
  133. function filefield_file_insert($node, $field, &$file) {
  134. $fieldname = $field['field_name'];
  135. // allow tokenized paths.
  136. if (function_exists('token_replace')) {
  137. global $user;
  138. $widget_file_path = token_replace($field['widget']['file_path'], 'user', $user);
  139. }
  140. else {
  141. $widget_file_path = $field['widget']['file_path'];
  142. }
  143. if (filefield_check_directory($widget_file_path)){
  144. $filepath = file_create_path($widget_file_path) . '/' . $file['filename'];
  145. if ($file = file_save_upload((object) $file, $filepath)) {
  146. $file = (array) $file;
  147. $file['fid'] = db_next_id('{files}_fid');
  148. $file['filemime'] = mimedetect_mime($file);
  149. db_query("INSERT into {files} (fid, nid, filename, filepath, filemime, filesize)
  150. VALUES (%d, %d, '%s','%s','%s',%d)",
  151. $file['fid'], $node->nid, $file['filename'], $file['filepath'],
  152. $file['filemime'], $file['filesize']);
  153. module_invoke_all('filefield', 'file_save', $node, $field, $file);
  154. return (array) $file;
  155. }
  156. else {
  157. // Include file name in upload error.
  158. form_set_error(NULL, t('File upload was unsuccessful.'));
  159. return FALSE;
  160. }
  161. }
  162. else {
  163. // Include file name in upload error.
  164. form_set_error(NULL, t('File upload was unsuccessful.'));
  165. return FALSE;
  166. }
  167. }
  168. /**
  169. * update the file record if necessary
  170. * @param $node
  171. * @param $file
  172. * @param $field
  173. */
  174. function filefield_file_update($node, $field, &$file) {
  175. $file = (array)$file;
  176. if ($file['delete'] == TRUE) {
  177. // don't delete files if we're creating new revisions,
  178. // but still return an empty array...
  179. if ($node->old_vid) {
  180. return array();
  181. }
  182. if (_filefield_file_delete($node, $field, $file)) {
  183. return array();
  184. }
  185. }
  186. if ($file['fid'] == 'upload') {
  187. return filefield_file_insert($node, $field, $file);
  188. }
  189. else {
  190. // if fid is not numeric here we should complain.
  191. // else we update the file table.
  192. }
  193. return $file;
  194. }
  195. /**
  196. * Implementation of hook_field().
  197. */
  198. function filefield_field($op, &$node, $field, &$items = array()) {
  199. $fieldname = $field['field_name'];
  200. switch ($op) {
  201. // called after content.module loads default data.
  202. case 'load':
  203. if (is_array($items)) {
  204. $items = array_filter($items); // drop empty deltas, cuz cck sends 'em some times.
  205. }
  206. if (empty($items)) {
  207. return array();
  208. }
  209. foreach ($items as $delta => $item) {
  210. if (!empty($item['fid'])) { // otherwise, merge our info with CCK's, and all is fine.
  211. $items[$delta] = array_merge($item, _filefield_file_load($item['fid']));
  212. }
  213. }
  214. $items = array_values($items); // compact deltas
  215. return array($fieldname => $items);
  216. // called before content.module defaults.
  217. case 'insert':
  218. foreach ($items as $delta => $item) {
  219. $items[$delta] = filefield_file_insert($node, $field, $item);
  220. }
  221. $items = array_values($items); // compact deltas
  222. filefield_clear_field_session($fieldname);
  223. break;
  224. // called before content.module defaults.
  225. case 'update':
  226. foreach ($items as $delta => $item) {
  227. $items[$delta] = filefield_file_update($node, $field, $item);
  228. }
  229. $items = array_filter($items); // unset empty items.
  230. $items = array_values($items); // compact deltas
  231. filefield_clear_field_session($fieldname);
  232. break;
  233. case 'delete revision':
  234. $db_info = content_database_info($field);
  235. foreach ($items as $delta => $item) {
  236. $references = db_result(db_query(
  237. "SELECT COUNT(vid) FROM {" . $db_info['table'] . "}
  238. WHERE nid = %d AND vid != %d
  239. AND " . $db_info['columns']['fid']['column'] . " = %d",
  240. $node->nid, $node->vid, $item['fid']
  241. ));
  242. if ($references || _filefield_file_delete($node, $field, $item)) {
  243. $items[$delta] = array();
  244. }
  245. }
  246. $items = array_values($items); // compact deltas
  247. break;
  248. case 'delete':
  249. foreach ($items as $delta => $item) {
  250. _filefield_file_delete($node, $field, $item);
  251. }
  252. break;
  253. }
  254. }
  255. /**
  256. * Implementation of hook_widget_info().
  257. */
  258. function filefield_widget_info() {
  259. return array(
  260. 'file' => array(
  261. 'label' => 'File',
  262. 'field types' => array('file'),
  263. ),
  264. );
  265. }
  266. /**
  267. * Implementation of hook_widget_settings().
  268. */
  269. function filefield_widget_settings($op, $widget) {
  270. switch ($op) {
  271. case 'callbacks':
  272. return array('default value' => CONTENT_CALLBACK_CUSTOM);
  273. case 'form':
  274. $form = array();
  275. $form['file_extensions'] = array (
  276. '#type' => 'textfield',
  277. '#title' => t('Permitted upload file extensions'),
  278. '#default_value' => isset($widget['file_extensions']) ? $widget['file_extensions'] : 'txt',
  279. '#size' => 64,
  280. '#description' => t('Extensions a user can upload to this field. Separate extensions with a space and do not include the leading dot. Leaving this blank will allow users to upload a file with any extension.'),
  281. );
  282. $form['file_path'] = array(
  283. '#type' => 'textfield',
  284. '#title' => t('File path'),
  285. '#default_value' => $widget['file_path'] ? $widget['file_path'] : '',
  286. '#description' => t('Optional subdirectory within the "%dir" directory where files will be stored. Do not include trailing slash.', array('%dir' => variable_get('file_directory_path', 'files'))),
  287. );
  288. if (function_exists('token_replace')) {
  289. $form['file_path']['#description'] .= theme('token_help', 'user');
  290. }
  291. // Let extension modules add their settings to the form.
  292. foreach (module_implements('filefield_widget_settings') as $module) {
  293. $function = $module .'_filefield_widget_settings';
  294. $function('form_alter', $widget, $form);
  295. }
  296. return $form;
  297. case 'validate':
  298. module_invoke_all('filefield_widget_settings', $op, $widget, NULL);
  299. break;
  300. case 'save':
  301. $core_settings = array('file_extensions', 'file_path');
  302. $additional_settings = module_invoke_all(
  303. 'filefield_widget_settings', $op, $widget, NULL
  304. );
  305. return array_merge($core_settings, $additional_settings);
  306. }
  307. }
  308. function filefield_clear_session() {
  309. if (is_array($_SESSION['filefield']) && count($_SESSION['filefield'])) {
  310. foreach (array_keys($_SESSION['filefield']) as $fieldname) {
  311. filefield_clear_field_session($fieldname);
  312. }
  313. unset($_SESSION['filefield']);
  314. }
  315. }
  316. function filefield_clear_field_session($fieldname) {
  317. if (is_array($_SESSION['filefield'][$fieldname]) && count($_SESSION['filefield'][$fieldname])) {
  318. foreach ($_SESSION['filefield'][$fieldname] as $delta => $file) {
  319. if (is_file($file['filepath'])) {
  320. file_delete($file['filepath']);
  321. }
  322. }
  323. unset($_SESSION['filefield'][$fieldname]);
  324. }
  325. }
  326. function _filefield_file_delete($node, $field, $file) {
  327. if (is_numeric($file['fid'])) {
  328. db_query('DELETE FROM {files} WHERE fid = %d', $file['fid']);
  329. }
  330. else {
  331. unset($_SESSION['filefield'][$field['field_name']][$file['sessionid']]);
  332. }
  333. module_invoke_all('filefield', 'file_delete', $node, $field, $file);
  334. return file_delete($file['filepath']);
  335. }
  336. /**
  337. * Implementation of hook_widget().
  338. */
  339. function filefield_widget($op, $node, $field, &$items) {
  340. $fieldname = $field['field_name'];
  341. switch ($op) {
  342. case 'default value':
  343. return array();
  344. case 'prepare form values':
  345. _filefield_widget_prepare_form_values($node, $field, $items);
  346. break;
  347. case 'form':
  348. return _filefield_widget_form($node, $field, $items);
  349. case 'validate':
  350. _filefield_widget_validate($node, $field, $items);
  351. break;
  352. }
  353. }
  354. function _filefield_widget_prepare_form_values($node, $field, &$items) {
  355. $fieldname = $field['field_name'];
  356. // @todo split this into its own function. determine if we can make it a form element.
  357. if (!count($_POST)) {
  358. filefield_clear_session();
  359. }
  360. // Attach new files
  361. if ($file = file_check_upload($fieldname . '_upload')) {
  362. $file = (array)$file;
  363. // test allowed extensions. We do this when the file is uploaded, rather than waiting for the
  364. // field itseld to reach op==validate.
  365. $last_ext = array_pop(explode('.', $file['filename']));
  366. $valid = TRUE;
  367. // only check extensions if there extensions to check.
  368. // @todo: trim & strtolower file_extenstions with a formapi validate callback.
  369. if (strlen(trim($field['widget']['file_extensions']))) {
  370. $allowed_extensions = array_unique(explode(' ', strtolower(trim($field['widget']['file_extensions']))));
  371. $ext = strtolower(array_pop(explode('.', $file['filename'])));
  372. if (!in_array($ext, $allowed_extensions)) {
  373. $valid = FALSE;
  374. form_set_error($field['field_name'] .'_upload', t('Files with the extension %ext are not allowed. Please upload a file with an extension from the following list: %allowed_extensions', array('%ext' => $last_ext, '%allowed_extensions' => $field['widget']['file_extensions'])));
  375. }
  376. }
  377. // let extended validation from other module happen so we get all error messages.
  378. // if you implement hook_filefield_file() return FALSE to stop the upload.
  379. if (!$valid || in_array(FALSE, module_invoke_all('filefield', 'file_validate', $node, $field, $file))) {
  380. return FALSE;
  381. }
  382. // let modules massage act on the file.
  383. foreach(module_implements('filefield') as $module) {
  384. $function = $module .'_filefield';
  385. $function('file_prepare', $node, $field, $file);
  386. }
  387. $filepath = file_create_filename($file['filename'], file_create_path($field['widget']['file_path']));
  388. if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) {
  389. if (strpos($filepath, file_directory_path()) !== FALSE) {
  390. $filepath = trim(substr($filepath, strlen(file_directory_path())), '\\/');
  391. }
  392. $filepath = 'system/files/'. $filepath;
  393. };
  394. // prepare file array.
  395. $file['fid'] = 'upload';
  396. $file['preview'] = $filepath;
  397. // if this is a single value filefield mark any other files for deletion.
  398. if (!$field['multiple']) {
  399. if (is_array($items)) {
  400. foreach($items as $delta => $session_file) {
  401. $items[$delta]['delete'] = TRUE;
  402. }
  403. }
  404. // Remove old temporary file from session.
  405. filefield_clear_field_session($fieldname);
  406. }
  407. $file_id = count($items) + count($_SESSION['filefield'][$fieldname]);
  408. $_SESSION['filefield'][$fieldname][$file_id] = $file;
  409. }
  410. // Load files from preview state. before committing actions.
  411. if (!empty($_SESSION['filefield'][$fieldname])) {
  412. foreach($_SESSION['filefield'][$fieldname] as $delta => $file) {
  413. $items[] = $file;
  414. }
  415. }
  416. }
  417. function _filefield_widget_form($node, $field, &$items) {
  418. drupal_add_js('misc/progress.js');
  419. drupal_add_js('misc/upload.js');
  420. drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js');
  421. $fieldname = $field['field_name'];
  422. drupal_add_css(drupal_get_path('module', 'filefield') .'/filefield.css');
  423. $form = array();
  424. $form[$fieldname] = array(
  425. '#type' => 'fieldset',
  426. '#title' => t($field['widget']['label']),
  427. '#description' => t('Changes made to the attachments are not permanent until you save this post.'),
  428. '#weight' => $field['widget']['weight'],
  429. '#collapsible' => TRUE,
  430. '#collapsed' => FALSE,
  431. '#tree' => TRUE,
  432. '#prefix' => '<div id="'. form_clean_id($fieldname .'-attach-wrapper') .'">',
  433. '#suffix' => '</div>',
  434. );
  435. $form[$fieldname]['new'] = array(
  436. '#tree' => FALSE,
  437. '#prefix' => '<div id="'. form_clean_id($fieldname .'-attach-hide') .'">',
  438. '#suffix' => '</div>',
  439. '#weight' => 100,
  440. );
  441. // Construct the upload description out of user supplied text,
  442. // maximum upload file size, and (optionally) allowed extensions.
  443. $upload_description = empty($field['widget']['description'])
  444. ? '' : ($field['widget']['description'] . '<br/>');
  445. $upload_description .= t('Maximum file size: !size.', array(
  446. '!size' => format_size(file_upload_max_size()),
  447. ));
  448. if (!empty($field['widget']['file_extensions'])) {
  449. $upload_description .= ' ' . t('Allowed extensions: %ext.', array(
  450. '%ext' => $field['widget']['file_extensions']
  451. ));
  452. }
  453. // Separate from tree becase of that silly things won't be displayed
  454. // if they are a child of '#type' = form issue
  455. $form[$fieldname]['new'][$fieldname .'_upload'] = array(
  456. '#type' => 'file',
  457. '#title' => t('Attach new file'),
  458. '#description' => $upload_description,
  459. '#weight' => 9,
  460. '#tree' => FALSE,
  461. '#attributes' => array('accept' => str_replace(' ', ',', trim($field['widget']['file_extensions']))),
  462. );
  463. $form[$fieldname]['new']['upload'] = array(
  464. '#type' => 'button',
  465. '#value' => t('Upload'),
  466. '#name' => 'cck_filefield_'. $fieldname .'_op',
  467. '#id' => form_clean_id($fieldname .'-attach-button'),
  468. '#tree' => FALSE,
  469. '#weight' => 10,
  470. );
  471. if (is_array($items) && count($items)) {
  472. $form[$fieldname]['files'] = array(
  473. '#parents' => array($fieldname),
  474. '#theme' => 'filefield_form_current',
  475. // remember the force_list setting so that the theme function knows
  476. '#force_list' => $field['force_list'],
  477. );
  478. foreach($items as $delta => $file) {
  479. // @todo: split into its own form and theme functions per file like imagefield
  480. if ($file['filepath'] && !$file['delete']) {
  481. $form[$fieldname]['files'][$delta] = _filefield_file_form($node, $field, $file);
  482. }
  483. else if ($file['filepath'] && $file['delete']) {
  484. $form[$fieldname]['files'][$delta]['delete'] = array(
  485. '#type' => 'hidden',
  486. '#value' => $file['delete'],
  487. );
  488. }
  489. }
  490. // Special handling for single value fields.
  491. if (!$field['multiple']) {
  492. $form[$fieldname]['replace'] = array(
  493. '#type' => 'markup',
  494. '#value' => '<p>'. t('If a new file is uploaded, this file will be replaced upon submitting this form.') .'</p>',
  495. '#prefix' => '<div class="description">',
  496. '#suffix' => '</div>',
  497. );
  498. }
  499. }
  500. // The class triggers the js upload behaviour.
  501. $form[$fieldname.'-attach-url'] = array(
  502. '#type' => 'hidden',
  503. '#value' => url('filefield/js', NULL, NULL, TRUE),
  504. '#attributes' => array('class' => 'upload'),
  505. );
  506. // Some useful info for our js callback.
  507. $form['vid'] = array(
  508. '#type' => 'hidden',
  509. '#value' => $node->vid,
  510. '#tree' => FALSE,
  511. );
  512. $form['nid'] = array(
  513. '#type' => 'hidden',
  514. '#value' => $node->nid,
  515. '#tree' => FALSE,
  516. );
  517. $form['type'] = array(
  518. '#type' => 'hidden',
  519. '#value' => $node->type,
  520. '#tree' => FALSE,
  521. );
  522. return $form;
  523. }
  524. function _filefield_file_form($node, $field, $file) {
  525. // Lets be a good boy and initialize our variables.
  526. $form = array();
  527. $form['#after_build'] = array('_filefield_file_form_description_reset');
  528. $form['icon'] = array(
  529. '#type' => 'markup',
  530. '#value' => theme('filefield_icon', $file),
  531. );
  532. $form['file_preview'] = array();
  533. $filepath = ($file['fid'] == 'upload')
  534. ? file_create_filename($file['filename'], file_create_path($field['widget']['file_path']))
  535. : $file['filepath'];
  536. $url = file_create_url($filepath);
  537. $form['description'] = array(
  538. '#type' => 'textfield',
  539. '#default_value' => (strlen($file['description'])) ? $file['description'] : $file['filename'], '#maxlength' => 256,
  540. '#size' => 40,
  541. '#attributes' => array('class' => 'filefield-description', 'size' => '40'),
  542. );
  543. $form['url'] = array(
  544. '#type' => 'markup',
  545. '#value' => l($url, $url),
  546. '#prefix' => '<div class="filefield-edit-file-url">',
  547. '#suffix' => '</div>',
  548. );
  549. $form['size'] = array(
  550. '#type' => 'markup',
  551. '#value' => format_size($file['filesize']),
  552. '#prefix' => '<div class="filefield-edit-file-size">',
  553. '#suffix' => '</div>',
  554. );
  555. $form['delete'] = array(
  556. '#type' => 'checkbox',
  557. '#default_value' => $file['delete'],
  558. );
  559. // Only show the list checkbox if files are not forced to be listed.
  560. if (!$field['force_list']) {
  561. $form['list'] = array(
  562. '#type' => 'checkbox',
  563. '#default_value' => $file['list'],
  564. );
  565. }
  566. else {
  567. $form['list'] = array(
  568. '#type' => 'value',
  569. '#value' => isset($file['list']) ? $file['list'] : 1,
  570. );
  571. }
  572. $form['filename'] = array('#type' => 'value', '#value' => $file['filename']);
  573. $form['filepath'] = array('#type' => 'value', '#value' => $file['filepath']);
  574. $form['filemime'] = array('#type' => 'value', '#value' => $file['filemime']);
  575. $form['filesize'] = array('#type' => 'value', '#value' => $file['filesize']);
  576. $form['fid'] = array('#type' => 'value', '#value' => $file['fid']);
  577. // Remember the current filename for the check in
  578. // _filefield_file_form_description_reset() that happens after submission.
  579. $form['previous_filepath'] = array('#type' => 'hidden', '#value' => $file['filepath']);
  580. foreach (module_implements('filefield') as $module) {
  581. $function = $module .'_filefield';
  582. $function('file_form', $node, $field, $file, $form);
  583. }
  584. return $form;
  585. }
  586. /**
  587. * This after_build function is needed as fix for tricky Form API behaviour:
  588. * When using filefield without AJAX uploading, the description field was not
  589. * updated to a new '#default_value' because the textfield has been submitted,
  590. * which causes Form API to override the '#default_value'.
  591. *
  592. * That bug is fixed with this function by comparing the previous filename
  593. * to the new one, and resetting the description to the '#default_value'
  594. * if the filename has changed.
  595. */
  596. function _filefield_file_form_description_reset($form, $form_values) {
  597. // Don't bother resetting the description of files that stay the same
  598. if ($form['fid']['#value'] != 'upload') {
  599. return $form;
  600. }
  601. // Get the previous filename for comparison with the current one.
  602. $previous = $form['previous_filepath']['#post'];
  603. foreach ($form['previous_filepath']['#parents'] as $parent) {
  604. $previous = isset($previous[$parent]) ? $previous[$parent] : NULL;
  605. }
  606. // If a new file was uploaded (the file path changed), reset the description.
  607. if ($previous != $form['filepath']['#value']) {
  608. $form['description']['#value'] = $form['description']['#default_value'];
  609. }
  610. return $form;
  611. }
  612. /**
  613. * Validate the form widget.
  614. */
  615. function _filefield_widget_validate($node, $field, $items) {
  616. if (!$field['required']) {
  617. return;
  618. }
  619. // if there aren't any items.. throw an error.
  620. if (!count($items)) {
  621. form_set_error($field['field_name'], t('@field is required. Please upload a file.', array('@field' => $field['widget']['label'])));
  622. }
  623. else {
  624. // Sum all the items marked for deletion, so we can make sure the end user
  625. // isn't deleting all of the files.
  626. $count_deleted = 0;
  627. foreach($items as $item) {
  628. $count_deleted += isset($item['delete']) && $item['delete'];
  629. }
  630. if (count($items) == $count_deleted) {
  631. form_set_error($field['field_name'], t('@field is required. Please keep at least one file or upload a new one.', array('@field' => $field['widget']['label'])));
  632. }
  633. }
  634. }
  635. /**
  636. * Implementation of hook_field formatter.
  637. * @todo: finish transformer.module and integrate like imagecache with imagefield.
  638. */
  639. function filefield_field_formatter_info() {
  640. $formatters = array(
  641. 'default' => array(
  642. 'label' => t('Default'),
  643. 'field types' => array('file'),
  644. ),
  645. );
  646. return $formatters;
  647. }
  648. function filefield_field_formatter($field, $item, $formatter) {
  649. if($field['force_list']) {
  650. $item['list'] = 1; // always show the files if that option is enabled
  651. }
  652. if(!empty($item['fid'])) {
  653. $item = array_merge($item, _filefield_file_load($item['fid']));
  654. }
  655. if (!empty($item['filepath'])) {
  656. drupal_add_css(drupal_get_path('module', 'filefield') .'/filefield.css');
  657. return theme('filefield', $item);
  658. }
  659. }
  660. function _filefield_file_load($fid = NULL) {
  661. // Don't bother if we weren't passed and fid.
  662. if (!empty($fid) && is_numeric($fid)) {
  663. $result = db_query('SELECT * FROM {files} WHERE fid = %d', $fid);
  664. $file = db_fetch_array($result);
  665. if ($file) {
  666. // let modules load extended attributes.
  667. $file += module_invoke_all('filefield', 'file_load', $node, $field, $file);
  668. return $file;
  669. }
  670. }
  671. // return an empty array if nothing was found.
  672. return array();
  673. }
  674. function theme_filefield_form_current($form) {
  675. $header = $form['#force_list']
  676. ? array('', t('Delete'), '', t('Description'), t('Size'))
  677. : array('', t('Delete'), t('List'), '', t('Description'), t('Size'));
  678. foreach (element_children($form) as $key) {
  679. // Don't display (hidden) replaced items.
  680. if ($form[$key]['delete']['#type'] == 'hidden') {
  681. continue;
  682. }
  683. $row = array();
  684. // we just going to lose this for now until we figure out how to handle it...
  685. $row[] = drupal_render($form[$key]['file_preview']);
  686. $row[] = drupal_render($form[$key]['delete']);
  687. if (!$form['#force_list']) {
  688. $row[] = drupal_render($form[$key]['list']);
  689. }
  690. $row[] = drupal_render($form[$key]['icon']);
  691. $row[] = drupal_render($form[$key]['description']).
  692. drupal_render($form[$key]['url']);
  693. $row[] = drupal_render($form[$key]['size']);
  694. $rows[] = $row;
  695. }
  696. $output = theme('table', $header, $rows, array('class' => 'filefield-filebrowser'));
  697. $output .= drupal_render($form);
  698. return $output;
  699. }
  700. function theme_filefield_icon($file) {
  701. $mime = check_plain($file['filemime']);
  702. $dashed_mime = strtr($mime, array('/' => '-'));
  703. if ($icon_url = filefield_icon_url($file)) {
  704. $icon = '<img class="field-icon-'. $dashed_mime .'" alt="'. $mime .' icon" src="'. $icon_url .'" />';
  705. }
  706. return '<div class="filefield-icon field-icon-'. $dashed_mime .'">'. $icon .'</div>';
  707. }
  708. function theme_filefield_view_file($file) {
  709. return theme('filefield', $file);
  710. }
  711. function theme_filefield($file) {
  712. if (user_access('view filefield uploads') && is_file($file['filepath']) && $file['list']) {
  713. $path = ($file['fid'] == 'upload')
  714. ? file_create_filename($file['filename'], file_create_path($field['widget']['file_path']))
  715. : $file['filepath'];
  716. $icon = theme('filefield_icon', $file);
  717. $url = file_create_url($path);
  718. $desc = $file['description'];
  719. return '<div class="filefield-item clear-block">'. $icon . l($desc, $url) .'</div>';
  720. }
  721. return '';
  722. }
  723. function filefield_file_download($file) {
  724. $file = file_create_path($file);
  725. $result = db_query("SELECT * FROM {files} WHERE filepath = '%s'", $file);
  726. if (!$file = db_fetch_object($result)) {
  727. // We don't really care about this file.
  728. return;
  729. }
  730. $node = node_load($file->nid);
  731. if (!node_access('view', $node)) {
  732. // You don't have permission to view the node
  733. // this file is attached to.
  734. return -1;
  735. }
  736. // @todo: check the node for this file to be referenced in a field
  737. // to determine if it is managed by filefield. and do the access denied part here.
  738. if (!user_access('view filefield uploads')) {
  739. // sorry you do not have the proper permissions to view
  740. // filefield uploads.
  741. return -1;
  742. }
  743. // Well I guess you can see this file.
  744. $name = mime_header_encode($file->filename);
  745. $type = mime_header_encode($file->filemime);
  746. // Serve images and text inline for the browser to display rather than download.
  747. $disposition = (ereg('^(text/|image/)', $file->filemime) || ereg('flash$', $file->filemime)) ? 'inline' : 'attachment';
  748. return array(
  749. 'Content-Type: '. $type .'; name='. $name,
  750. 'Content-Length: '. $file->filesize,
  751. 'Content-Disposition: '. $disposition .'; filename='. $name,
  752. 'Cache-Control: private',
  753. );
  754. }
  755. /**
  756. * Create the file directory relative to the 'files' dir recursively for every
  757. * directory in the path.
  758. *
  759. * @param $directory
  760. * The directory path under files to check, such as 'photo/path/here'
  761. * @param $form_element
  762. * A form element to throw an error on if the directory is not writable
  763. */
  764. function filefield_check_directory($directory, $form_element = array()) {
  765. foreach(explode('/', $directory) as $dir) {
  766. $dirs[] = $dir;
  767. $path = file_create_path(implode($dirs,'/'));
  768. if (!file_check_directory($path, FILE_CREATE_DIRECTORY, $form_element['#parents'][0])){
  769. return FALSE;
  770. }
  771. }
  772. return TRUE;
  773. }
  774. /**
  775. * Menu callback for JavaScript-based uploads.
  776. */
  777. function filefield_js() {
  778. // Parse fieldname from submit button.
  779. $matches = array();
  780. foreach(array_keys($_POST) as $key) {
  781. if (preg_match('/cck_filefield_(.*)_op/', $key, $matches)) {
  782. $fieldname = $matches[1];
  783. break;
  784. }
  785. }
  786. $node = (object)$_POST;
  787. $field = content_fields($fieldname, $node->type); // load field data
  788. // Load fids stored by content.module.
  789. $items = array();
  790. $values = content_field('load', $node, $field, $items, FALSE, FALSE);
  791. $items = $values[$fieldname];
  792. // Load additional field data.
  793. filefield_field('load', $node, $field, $items, FALSE, FALSE);
  794. // Handle uploads and validation.
  795. _filefield_widget_prepare_form_values($node, $field, $items);
  796. _filefield_widget_validate($node, $field, $items);
  797. // Get our new form baby, yeah tiger, get em!
  798. $form = _filefield_widget_form($node, $field, $items);
  799. foreach (module_implements('form_alter') as $module) {
  800. $function = $module .'_form_alter';
  801. $function('filefield_js', $form);
  802. }
  803. $form = form_builder('filefield_js', $form);
  804. $output = theme('status_messages') . drupal_render($form);
  805. // Send the updated file attachments form.
  806. $GLOBALS['devel_shutdown'] = false;
  807. print drupal_to_js(array('status' => TRUE, 'data' => $output));
  808. exit();
  809. }
  810. function filefield_token_list($type = 'all') {
  811. if ($type == 'field' || $type == 'all') {
  812. $tokens = array();
  813. $tokens['file']['fid'] = t("File ID");
  814. $tokens['file']['description'] = t("File description");
  815. $tokens['file']['filename'] = t("File name");
  816. $tokens['file']['filepath'] = t("File path");
  817. $tokens['file']['filemime'] = t("File MIME type");
  818. $tokens['file']['filesize'] = t("File size");
  819. $tokens['file']['view'] = t("Fully formatted HTML file tag");
  820. return $tokens;
  821. }
  822. }
  823. function filefield_token_values($type, $object = NULL) {
  824. if ($type == 'field') {
  825. $item = $object[0];
  826. $tokens['fid'] = $item['fid'];
  827. $tokens['description'] = check_plain($item['description']);
  828. $tokens['filename'] = check_plain($item['filename']);
  829. $tokens['filepath'] = check_plain($item['filepath']);
  830. $tokens['filemime'] = $item['filemime'];
  831. $tokens['filesize'] = $item['filesize'];
  832. $tokens['view'] = $item['view'];
  833. return $tokens;
  834. }
  835. }
  836. /**
  837. * Custom filter for filefield NOT NULL
  838. */
  839. function filefield_views_handler_filter_is_not_null($op, $filter, $filterinfo, &$query) {
  840. if ($op == 'handler') {
  841. $query->ensure_table($filterinfo['table']);
  842. if ($filter['value']) {
  843. $qs = '%s.%s > 0';
  844. }
  845. else {
  846. $qs = '%s.%s = 0 OR %s.%s IS NULL';
  847. }
  848. $query->add_where($qs, $filterinfo['table'], $filterinfo['field'], $filterinfo['table'], $filterinfo['field']);
  849. }
  850. }
  851. /**
  852. * Determine the most appropriate icon for the given file's mimetype.
  853. *
  854. * @return The URL of the icon image file, or FALSE if no icon could be found.
  855. */
  856. function filefield_icon_url($file) {
  857. global $base_url;
  858. $theme = variable_get('filefield_icon_theme', 'protocons');
  859. if ($iconpath = _filefield_icon_path($file, $theme)) {
  860. return $base_url .'/'. $iconpath;
  861. }
  862. return FALSE;
  863. }
  864. function _filefield_icon_path($file, $theme = 'protocons') {
  865. // If there's an icon matching the exact mimetype, go for it.
  866. $dashed_mime = strtr($file['filemime'], array('/' => '-'));
  867. if ($iconpath = _filefield_create_icon_path($dashed_mime, $theme)) {
  868. return $iconpath;
  869. }
  870. // For a couple of mimetypes, we can "manually" tell a generic icon.
  871. if ($generic_name = _filefield_generic_icon_map($file)) {
  872. if ($iconpath = _filefield_create_icon_path($generic_name, $theme)) {
  873. return $iconpath;
  874. }
  875. }
  876. // Use generic icons for each category that provides such icons.
  877. foreach (array('audio', 'image', 'text', 'video') as $category) {
  878. if (strpos($file['filemime'], $category .'/') === 0) {
  879. if ($iconpath = _filefield_create_icon_path($category .'-x-generic', $theme)) {
  880. return $iconpath;
  881. }
  882. }
  883. }
  884. // Try application-octet-stream as last fallback.
  885. if ($iconpath = _filefield_create_icon_path('application-octet-stream', $theme)) {
  886. return $iconpath;
  887. }
  888. // Sorry, no icon can be found...
  889. return FALSE;
  890. }
  891. function _filefield_create_icon_path($iconname, $theme = 'protocons') {
  892. $iconpath = drupal_get_path('module', 'filefield')
  893. .'/icons/'. $theme .'/16x16/mimetypes/'. $iconname .'.png';
  894. if (file_exists($iconpath)) {
  895. return $iconpath;
  896. }
  897. return FALSE;
  898. }
  899. function _filefield_generic_icon_map($file) {
  900. switch ($file['filemime']) {
  901. // Word document types.
  902. case 'application/msword':
  903. case 'application/vnd.ms-word.document.macroEnabled.12':
  904. case 'application/vnd.oasis.opendocument.text':
  905. case 'application/vnd.oasis.opendocument.text-template':
  906. case 'application/vnd.oasis.opendocument.text-master':
  907. case 'application/vnd.oasis.opendocument.text-web':
  908. case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
  909. case 'application/vnd.stardivision.writer':
  910. case 'application/vnd.sun.xml.writer':
  911. case 'application/vnd.sun.xml.writer.template':
  912. case 'application/vnd.sun.xml.writer.global':
  913. case 'application/vnd.wordperfect':
  914. case 'application/x-abiword':
  915. case 'application/x-applix-word':
  916. case 'application/x-kword':
  917. case 'application/x-kword-crypt':
  918. return 'x-office-document';
  919. // Spreadsheet document types.
  920. case 'application/vnd.ms-excel':
  921. case 'application/vnd.ms-excel.sheet.macroEnabled.12':
  922. case 'application/vnd.oasis.opendocument.spreadsheet':
  923. case 'application/vnd.oasis.opendocument.spreadsheet-template':
  924. case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
  925. case 'application/vnd.stardivision.calc':
  926. case 'application/vnd.sun.xml.calc':
  927. case 'application/vnd.sun.xml.calc.template':
  928. case 'application/vnd.lotus-1-2-3':
  929. case 'application/x-applix-spreadsheet':
  930. case 'application/x-gnumeric':
  931. case 'application/x-kspread':
  932. case 'application/x-kspread-crypt':
  933. return 'x-office-spreadsheet';
  934. // Presentation document types.
  935. case 'application/vnd.ms-powerpoint':
  936. case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
  937. case 'application/vnd.oasis.opendocument.presentation':
  938. case 'application/vnd.oasis.opendocument.presentation-template':
  939. case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
  940. case 'application/vnd.stardivision.impress':
  941. case 'application/vnd.sun.xml.impress':
  942. case 'application/vnd.sun.xml.impress.template':
  943. case 'application/x-kpresenter':
  944. return 'x-office-presentation';
  945. // Compressed archive types.
  946. case 'application/zip':
  947. case 'application/x-zip':
  948. case 'application/stuffit':
  949. case 'application/x-stuffit':
  950. case 'application/x-7z-compressed':
  951. case 'application/x-ace':
  952. case 'application/x-arj':
  953. case 'application/x-bzip':
  954. case 'application/x-bzip-compressed-tar':
  955. case 'application/x-compress':
  956. case 'application/x-compressed-tar':
  957. case 'application/x-cpio-compressed':
  958. case 'application/x-deb':
  959. case 'application/x-gzip':
  960. case 'application/x-java-archive':
  961. case 'application/x-lha':
  962. case 'application/x-lhz':
  963. case 'application/x-lzop':
  964. case 'application/x-rar':
  965. case 'application/x-rpm':
  966. case 'application/x-tzo':
  967. case 'application/x-tar':
  968. case 'application/x-tarz':
  969. case 'application/x-tgz':
  970. return 'package-x-generic';
  971. // Script file types.
  972. case 'application/ecmascript':
  973. case 'application/javascript':
  974. case 'application/mathematica':
  975. case 'application/vnd.mozilla.xul+xml':
  976. case 'application/x-asp':
  977. case 'application/x-awk':
  978. case 'application/x-cgi':
  979. case 'application/x-csh':
  980. case 'application/x-m4':
  981. case 'application/x-perl':
  982. case 'application/x-php':
  983. case 'application/x-ruby':
  984. case 'application/x-shellscript':
  985. case 'text/vnd.wap.wmlscript':
  986. case 'text/x-emacs-lisp':
  987. case 'text/x-haskell':
  988. case 'text/x-literate-haskell':
  989. case 'text/x-lua':
  990. case 'text/x-makefile':
  991. case 'text/x-matlab':
  992. case 'text/x-python':
  993. case 'text/x-sql':
  994. case 'text/x-tcl':
  995. return 'text-x-script';
  996. // HTML aliases.
  997. case 'application/xhtml+xml':
  998. return 'text-html';
  999. // Executable types.
  1000. case 'application/x-macbinary':
  1001. case 'application/x-ms-dos-executable':
  1002. case 'application/x-pef-executable':
  1003. return 'application-x-executable';
  1004. default:
  1005. return FALSE;
  1006. }
  1007. }