Issue #1090216: Add MIME type icon compatibility for kml/kmz files.
[project/filefield.git] / filefield_widget.inc
index 48105fc..4970721 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-// $Id$
 
 /**
  * @file
  */
 function filefield_widget_settings_form($widget) {
   $form = array();
+
+  // Convert the extensions list to be a human-friendly comma-separated list.
+  $extensions = is_string($widget['file_extensions']) ? $widget['file_extensions'] : 'txt';
   $form['file_extensions'] = array(
     '#type' => 'textfield',
     '#title' => t('Permitted upload file extensions'),
-    '#default_value' => is_string($widget['file_extensions']) ? $widget['file_extensions'] : 'txt',
+    '#default_value' => $extensions,
     '#size' => 64,
+    '#maxlength' => 512,
     '#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.'),
+    '#element_validate' => array('_filefield_widget_settings_extensions_validate'),
+    '#pre_render' => array('_filefield_widget_settings_extensions_value'),
     '#weight' => 1,
   );
 
+  $form['progress_indicator'] = array(
+    '#type' => 'radios',
+    '#title' => t('Progress indicator'),
+    '#options' => array(
+      'bar' => t('Bar with progress meter'),
+      'throbber' => t('Throbber'),
+    ),
+    '#default_value' => empty($widget['progress_indicator']) ? 'bar' : $widget['progress_indicator'],
+    '#description' => t('Your server supports upload progress capabilities. The "throbber" display does not indicate progress but takes up less room on the form, you may want to use it if you\'ll only be uploading small files or if experiencing problems with the progress bar.'),
+    '#weight' => 5,
+    '#access' => filefield_progress_implementation(),
+  );
+
   $form['path_settings'] = array(
     '#type' => 'fieldset',
     '#title' => t('Path settings'),
@@ -53,7 +71,7 @@ function filefield_widget_settings_form($widget) {
     '#default_value' => is_string($widget['max_filesize_per_file'])
                         ? $widget['max_filesize_per_file']
                          : '',
-    '#description' => t('Specify the size limit that applies to each file separately. Enter a value like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes) in order to restrict the allowed file size. If you leave this this empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit <strong>%limit</strong>).', array('%limit' => format_size(file_upload_max_size()))),
+    '#description' => t('Specify the size limit that applies to each file separately. Enter a value like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes) in order to restrict the allowed file size. If you leave this empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit <strong>%limit</strong>).', array('%limit' => format_size(file_upload_max_size()))),
     '#element_validate' => array('_filefield_widget_settings_max_filesize_per_file_validate'),
   );
   $form['max_filesize']['max_filesize_per_node'] = array(
@@ -73,7 +91,27 @@ function filefield_widget_settings_form($widget) {
  * Implementation of CCK's hook_widget_settings($op == 'save').
  */
 function filefield_widget_settings_save($widget) {
-  return array('file_extensions', 'file_path', 'max_filesize_per_file', 'max_filesize_per_node');
+  return array('file_extensions', 'file_path', 'progress_indicator', 'max_filesize_per_file', 'max_filesize_per_node');
+}
+
+/**
+ * A FAPI #pre_render() function to set a cosmetic default value for extensions.
+ */
+function _filefield_widget_settings_extensions_value($element) {
+  $element['#value'] = implode(', ', array_filter(explode(' ', str_replace(',', ' ', $element['#value']))));
+  return $element;
+}
+
+/**
+ * A FAPI #element_validate callback to strip commas from extension lists.
+ */
+function _filefield_widget_settings_extensions_validate($element, &$form_state) {
+  // Remove commas and leading dots from file extensions.
+  $value = str_replace(',', ' ', $element['#value']);
+  $value = str_replace(' .', ' ', $value);
+  $value = array_filter(explode(' ', $value));
+  $value = implode(' ', $value);
+  form_set_value($element, $value, $form_state);
 }
 
 function _filefield_widget_settings_file_path_validate($element, &$form_state) {
@@ -110,14 +148,28 @@ function _filefield_widget_settings_max_filesize_per_node_validate($element, &$f
  *
  * @param $field
  *   A CCK field array.
+ * @param $account
+ *   The user account object to calculate the file path for.
  * @return
  *   The files directory path, with any tokens replaced.
  */
-function filefield_widget_file_path($field_instance) {
-  $dest = $field_instance['widget']['file_path'];
+function filefield_widget_file_path($field, $account = NULL) {
+  $account = isset($account) ? $account : $GLOBALS['user'];
+  $dest = $field['widget']['file_path'];
+  // Replace user level tokens.
+  // Node level tokens require a lot of complexity like temporary storage
+  // locations when values don't exist. See the filefield_paths module.
   if (module_exists('token')) {
-    global $user;
-    $dest = token_replace($dest, 'user', $user);
+    $dest = token_replace($dest, 'user', $account);
+  }
+  // Replace nasty characters in the path if possible.
+  if (module_exists('transliteration')) {
+    module_load_include('inc', 'transliteration');
+    $dest_array = array_filter(explode('/', $dest));
+    foreach ($dest_array as $key => $directory) {
+      $dest_array[$key] = transliteration_clean_filename($directory);
+    }
+    $dest = implode('/', $dest_array);
   }
 
   return file_directory_path() .'/'. $dest;
@@ -133,14 +185,14 @@ function filefield_widget_file_path($field_instance) {
  *   The FAPI element whose values are being saved.
  */
 function filefield_save_upload($element) {
-  $upload_name = $element['#field_name'] .'_'. $element['#delta'];
-  $field_instance = content_fields($element['#field_name'], $element['#type_name']);
+  $upload_name = implode('_', $element['#array_parents']);
+  $field = content_fields($element['#field_name'], $element['#type_name']);
 
   if (empty($_FILES['files']['name'][$upload_name])) {
     return 0;
   }
 
-  $dest = filefield_widget_file_path($field_instance);
+  $dest = filefield_widget_file_path($field);
   if (!field_file_check_directory($dest, FILE_CREATE_DIRECTORY)) {
     watchdog('filefield', 'The upload directory %directory for the file field %field (content type %type) could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $dest, '%field' => $element['#field_name'], '%type' => $element['#type_name']));
     form_set_error($upload_name, t('The file could not be uploaded.'));
@@ -164,7 +216,7 @@ function filefield_widget_value($element, $edit = FALSE) {
     $item = $element['#default_value'];
   }
   else {
-    $item = array_merge($element['#default_value'], $edit);
+    $item = $edit;
     $field = content_fields($element['#field_name'], $element['#type_name']);
 
     // Uploads take priority over value of fid text field.
@@ -182,20 +234,24 @@ function filefield_widget_value($element, $edit = FALSE) {
     }
 
     // Load file if the FID has changed so that it can be saved by CCK.
-    $file = field_file_load($item['fid']);
+    $file = isset($item['fid']) ? field_file_load($item['fid']) : NULL;
 
     // If the file entry doesn't exist, don't save anything.
     if (empty($file)) {
       $item = array();
+      $file = array();
     }
 
     // Checkboxes loose their value when empty.
     // If the list field is present make sure its unchecked value is saved.
-    if ($field['list_field'] && empty($edit['list'])) {
+    if (!empty($field['list_field']) && empty($edit['list'])) {
       $item['list'] = 0;
     }
   }
   // Merge file and item data so it is available to all widgets.
+  if (isset($item['data']) && isset($file['data'])) {
+    $file['data'] = array_merge($item['data'], $file['data']);
+  }
   $item = array_merge($item, $file);
 
   return $item;
@@ -208,15 +264,26 @@ function filefield_widget_value($element, $edit = FALSE) {
  * remove buttons, and the description field.
  */
 function filefield_widget_process($element, $edit, &$form_state, $form) {
-  // The widget is being presented, so apply the JavaScript.
-  drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js');
+  static $settings_added;
 
   $item = $element['#value'];
   $field_name = $element['#field_name'];
   $delta = $element['#delta'];
   $element['#theme'] = 'filefield_widget_item';
 
-  $field = content_fields($element['#field_name'], $element['#type_name']);
+  $field = $form['#field_info'][$field_name];
+
+  // The widget is being presented, so apply the JavaScript.
+  drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js');
+  if (!isset($settings_added[$field_name]) && isset($element['#upload_validators']['filefield_validate_extensions'])) {
+    $settings_added[$field_name] = TRUE;
+    $settings = array(
+      'filefield' => array(
+        $field_name => $element['#upload_validators']['filefield_validate_extensions'][0],
+      ),
+    );
+    drupal_add_js($settings, 'setting');
+  }
 
   // Title is not necessary for each individual field.
   if ($field['multiple'] > 0) {
@@ -227,7 +294,6 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
   $element['filefield_upload'] = array(
     '#type' => 'submit',
     '#value' => t('Upload'),
-    '#process' => array('form_expand_ahah'),
     '#submit' => array('node_form_submit_build_node'),
     '#ahah' => array( // with JavaScript
        'path' => 'filefield/ahah/'.   $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'],
@@ -243,10 +309,12 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
     '#post' => $element['#post'],
   );
   $element['filefield_remove'] = array(
-    '#name' => $element['#field_name'] .'_'. $element['#delta'] .'_filefield_remove',
+    // With default CCK edit forms, $element['#parents'] is array($element['#field_name'], $element['#delta']).
+    // However, if some module (for example, flexifield) places our widget deeper in the tree, we want to
+    // use that information in constructing the button name.
+    '#name' => implode('_', $element['#parents']) .'_filefield_remove',
     '#type' => 'submit',
     '#value' => t('Remove'),
-    '#process' => array('form_expand_ahah'),
     '#submit' => array('node_form_submit_build_node'),
     '#ahah' => array( // with JavaScript
       'path' => 'filefield/ahah/'.   $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'],
@@ -266,6 +334,14 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
   // $form_state['clicked_button'] is only available after this #process
   // callback is finished.
   if (_form_button_was_clicked($element['filefield_remove'])) {
+    // Delete the file if it is currently unused. Note that field_file_delete()
+    // does a reference check in addition to our basic status check.
+    if (isset($edit['fid'])) {
+      $removed_file = field_file_load($edit['fid']);
+      if ($removed_file['status'] == 0) {
+        field_file_delete($removed_file);
+      }
+    }
     $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => ''));
   }
 
@@ -273,6 +349,31 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
   $element['filefield_upload']['#access'] = empty($item['fid']);
   $element['filefield_remove']['#access'] = !empty($item['fid']);
 
+  // Add progress bar support to the upload if possible.
+  $progress_indicator = isset($field['widget']['progress_indicator']) ? $field['widget']['progress_indicator'] : 'bar';
+  if ($progress_indicator != 'throbber' && $implementation = filefield_progress_implementation()) {
+    $upload_progress_key = md5(mt_rand());
+
+    if ($implementation == 'uploadprogress') {
+      $element['UPLOAD_IDENTIFIER'] = array(
+        '#type' => 'hidden',
+        '#value' => $upload_progress_key,
+        '#attributes' => array('class' => 'filefield-progress'),
+      );
+    }
+    elseif ($implementation == 'apc') {
+      $element['APC_UPLOAD_PROGRESS'] = array(
+        '#type' => 'hidden',
+        '#value' => $upload_progress_key,
+        '#attributes' => array('class' => 'filefield-progress'),
+      );
+    }
+
+    // Add the upload progress callback.
+    $element['filefield_upload']['#ahah']['progress']['type'] = 'bar';
+    $element['filefield_upload']['#ahah']['progress']['path'] = 'filefield/progress/' . $upload_progress_key;
+  }
+
   // Set the FID.
   $element['fid'] = array(
     '#type' => 'hidden',
@@ -286,6 +387,11 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
     );
   }
 
+  // Grant access to temporary files.
+  if ($item['fid'] && isset($item['status']) && $item['status'] == 0 && variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) {
+    $_SESSION['filefield_access'][] = $item['fid'];
+  }
+
   // placeholder.. will be serialized into the data column. this is a place for widgets
   // to put additional data.
   $element['data'] = array(
@@ -293,21 +399,22 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
     '#access' => !empty($item['fid']),
   );
 
-  if ($field['description_field']) {
+  if (!empty($field['description_field'])) {
     $element['data']['description'] = array(
       '#type' => 'textfield',
       '#title' => t('Description'),
       '#value' => isset($item['data']['description']) ? $item['data']['description'] : '',
+      '#type' => variable_get('filefield_description_type', 'textfield'),
+      '#maxlength' => variable_get('filefield_description_length', 128),
     );
   }
 
-  if ($field['list_field']) {
+  if (!empty($field['list_field'])) {
     $element['list'] = array(
-      '#type' => 'checkbox',
+      '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
       '#title' => t('List'),
-      '#value' => isset($item['list']) ? $item['list'] : $field['list_default'],
+      '#value' => isset($item['list']) && !empty($item['fid']) ? $item['list'] : $field['list_default'],
       '#attributes' => array('class' => 'filefield-list'),
-      '#access' => !empty($item['fid']),
     );
   }
   else {
@@ -323,14 +430,12 @@ function filefield_widget_process($element, $edit, &$form_state, $form) {
       $desc[] = call_user_func_array($help_func, $arguments);
     }
   }
+
   $element['upload'] = array(
-    '#name' => 'files['. $element['#field_name'] .'_'. $element['#delta'] .']',
+    '#name' => 'files[' . implode('_', $element['#array_parents']) . ']',
     '#type' => 'file',
     '#description' => implode('<br />', $desc),
     '#size' => 22,
-    '#attributes' => array(
-      'accept' => implode(',', array_filter(explode(' ', $field['widget']['file_extensions']))),
-    ),
     '#access' => empty($item['fid']),
   );
 
@@ -353,11 +458,8 @@ function filefield_widget_validate(&$element, &$form_state) {
     if ($file = field_file_load($element['fid']['#value'])) {
       $file = (object) $file;
       if ($file->status == FILE_STATUS_PERMANENT) {
-        // TODO: We could use field_file_references() here to reference any file
-        // but hook_file_delete() needs to be implemented first.
-        $references = module_invoke('filefield', 'file_references', $file);
-        if ($references['filefield'] == 0) {
-          form_error($element, t('Referencing to the file used the %field field is not allowed.', array('%field' => $element['#title'])));
+        if (field_file_references($file) == 0) {
+          form_error($element, t('Referencing to the file used in the %field field is not allowed.', array('%field' => $element['#title'])));
         }
       }
     }
@@ -371,6 +473,7 @@ function filefield_widget_validate(&$element, &$form_state) {
  * FormAPI theme function. Theme the output of an image field.
  */
 function theme_filefield_widget($element) {
+  $element['#id'] .= '-upload'; // Link the label to the upload field.
   return theme('form_element', $element, $element['#children']);
 }
 
@@ -444,8 +547,7 @@ function theme_filefield_widget_file($element) {
  * properly supported on file fields by Drupal core, so we do this manually.
  */
 function filefield_node_form_validate($form, &$form_state) {
-  $type = content_types($form['type']['#value']);
-  foreach ($type['fields'] as $field_name => $field) {
+  foreach ($form['#field_info'] as $field_name => $field) {
     if (!(in_array($field['module'], array('imagefield', 'filefield')))) continue;
     $empty = $field['module'] .'_content_is_empty';
     $valid = FALSE;
@@ -458,12 +560,12 @@ function filefield_node_form_validate($form, &$form_state) {
       }
     }
 
-    if (!$valid && $field['required']) {
+    if (!$valid && $field['required'] && filefield_edit_access($field['type_name'], $field_name, $form['#node'])) {
       form_set_error($field_name, t('%title field is required.', array('%title' => $field['widget']['label'])));
     }
     $max_filesize = parse_size($field['widget']['max_filesize_per_node']);
     if ($max_filesize && $total_filesize > $max_filesize) {
-      form_set_error($field_name, t('Total filesize for %title, %tsize,  exceeds field settings of %msize.',
+      form_set_error($field_name, t('Total filesize for %title, %tsize, exceeds field settings of %msize.',
                                     array(
                                       '%title' => $field['widget']['label'],
                                       '%tsize' => format_size($total_filesize),
@@ -473,16 +575,3 @@ function filefield_node_form_validate($form, &$form_state) {
     }
   }
 }
-
-/**
- * Additional #submit handler for the node form.
- */
-function filefield_node_form_submit($form, &$form_state) {
-  // Only add additional submit handling on save.
-  if ($form_state['values']['op'] == t('Save')) {
-    // TODO: Immediately delete temporary files that were uploaded but never
-    // saved. This isn't critical to do, since any file not saved with the node
-    // will by cleaned up by system_cron() after 6 hours.
-    return;
-  }
-}