cms/drupal/modules/file/file.field.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * @file
       
     5  * Field module functionality for the File module.
       
     6  */
       
     7 
       
     8 /**
       
     9  * Implements hook_field_info().
       
    10  */
       
    11 function file_field_info() {
       
    12   return array(
       
    13     'file' => array(
       
    14       'label' => t('File'),
       
    15       'description' => t('This field stores the ID of a file as an integer value.'),
       
    16       'settings' => array(
       
    17         'display_field' => 0,
       
    18         'display_default' => 0,
       
    19         'uri_scheme' => variable_get('file_default_scheme', 'public'),
       
    20       ),
       
    21       'instance_settings' => array(
       
    22         'file_extensions' => 'txt',
       
    23         'file_directory' => '',
       
    24         'max_filesize' => '',
       
    25         'description_field' => 0,
       
    26       ),
       
    27       'default_widget' => 'file_generic',
       
    28       'default_formatter' => 'file_default',
       
    29     ),
       
    30   );
       
    31 }
       
    32 
       
    33 /**
       
    34  * Implements hook_field_settings_form().
       
    35  */
       
    36 function file_field_settings_form($field, $instance, $has_data) {
       
    37   $defaults = field_info_field_settings($field['type']);
       
    38   $settings = array_merge($defaults, $field['settings']);
       
    39 
       
    40   $form['#attached']['js'][] = drupal_get_path('module', 'file') . '/file.js';
       
    41 
       
    42   $form['display_field'] = array(
       
    43     '#type' => 'checkbox',
       
    44     '#title' => t('Enable <em>Display</em> field'),
       
    45     '#default_value' => $settings['display_field'],
       
    46     '#description' => t('The display option allows users to choose if a file should be shown when viewing the content.'),
       
    47   );
       
    48   $form['display_default'] = array(
       
    49     '#type' => 'checkbox',
       
    50     '#title' => t('Files displayed by default'),
       
    51     '#default_value' => $settings['display_default'],
       
    52     '#description' => t('This setting only has an effect if the display option is enabled.'),
       
    53   );
       
    54 
       
    55   $scheme_options = array();
       
    56   foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) {
       
    57     $scheme_options[$scheme] = $stream_wrapper['name'];
       
    58   }
       
    59   $form['uri_scheme'] = array(
       
    60     '#type' => 'radios',
       
    61     '#title' => t('Upload destination'),
       
    62     '#options' => $scheme_options,
       
    63     '#default_value' => $settings['uri_scheme'],
       
    64     '#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
       
    65     '#disabled' => $has_data,
       
    66   );
       
    67 
       
    68   return $form;
       
    69 }
       
    70 
       
    71 /**
       
    72  * Implements hook_field_instance_settings_form().
       
    73  */
       
    74 function file_field_instance_settings_form($field, $instance) {
       
    75   $settings = $instance['settings'];
       
    76 
       
    77   $form['file_directory'] = array(
       
    78     '#type' => 'textfield',
       
    79     '#title' => t('File directory'),
       
    80     '#default_value' => $settings['file_directory'],
       
    81     '#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'),
       
    82     '#element_validate' => array('_file_generic_settings_file_directory_validate'),
       
    83     '#weight' => 3,
       
    84   );
       
    85 
       
    86   // Make the extension list a little more human-friendly by comma-separation.
       
    87   $extensions = str_replace(' ', ', ', $settings['file_extensions']);
       
    88   $form['file_extensions'] = array(
       
    89     '#type' => 'textfield',
       
    90     '#title' => t('Allowed file extensions'),
       
    91     '#default_value' => $extensions,
       
    92     '#description' => t('Separate extensions with a space or comma and do not include the leading dot.'),
       
    93     '#element_validate' => array('_file_generic_settings_extensions'),
       
    94     '#weight' => 1,
       
    95     '#maxlength' => 256,
       
    96     // By making this field required, we prevent a potential security issue
       
    97     // that would allow files of any type to be uploaded.
       
    98     '#required' => TRUE,
       
    99   );
       
   100 
       
   101   $form['max_filesize'] = array(
       
   102     '#type' => 'textfield',
       
   103     '#title' => t('Maximum upload size'),
       
   104     '#default_value' => $settings['max_filesize'],
       
   105     '#description' => t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left 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()))),
       
   106     '#size' => 10,
       
   107     '#element_validate' => array('_file_generic_settings_max_filesize'),
       
   108     '#weight' => 5,
       
   109   );
       
   110 
       
   111   $form['description_field'] = array(
       
   112     '#type' => 'checkbox',
       
   113     '#title' => t('Enable <em>Description</em> field'),
       
   114     '#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
       
   115     '#description' => t('The description field allows users to enter a description about the uploaded file.'),
       
   116     '#parents' => array('instance', 'settings', 'description_field'),
       
   117     '#weight' => 11,
       
   118   );
       
   119 
       
   120   return $form;
       
   121 }
       
   122 
       
   123 /**
       
   124  * Element validate callback for the maximum upload size field.
       
   125  *
       
   126  * Ensure a size that can be parsed by parse_size() has been entered.
       
   127  */
       
   128 function _file_generic_settings_max_filesize($element, &$form_state) {
       
   129   if (!empty($element['#value']) && !is_numeric(parse_size($element['#value']))) {
       
   130     form_error($element, t('The "!name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', array('!name' => t($element['title']))));
       
   131   }
       
   132 }
       
   133 
       
   134 /**
       
   135  * Element validate callback for the allowed file extensions field.
       
   136  *
       
   137  * This doubles as a convenience clean-up function and a validation routine.
       
   138  * Commas are allowed by the end-user, but ultimately the value will be stored
       
   139  * as a space-separated list for compatibility with file_validate_extensions().
       
   140  */
       
   141 function _file_generic_settings_extensions($element, &$form_state) {
       
   142   if (!empty($element['#value'])) {
       
   143     $extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
       
   144     $extensions = array_filter(explode(' ', $extensions));
       
   145     $extensions = implode(' ', array_unique($extensions));
       
   146     if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
       
   147       form_error($element, t('The list of allowed extensions is not valid, be sure to exclude leading dots and to separate extensions with a comma or space.'));
       
   148     }
       
   149     else {
       
   150       form_set_value($element, $extensions, $form_state);
       
   151     }
       
   152   }
       
   153 }
       
   154 
       
   155 /**
       
   156  * Element validate callback for the file destination field.
       
   157  *
       
   158  * Remove slashes from the beginning and end of the destination value and ensure
       
   159  * that the file directory path is not included at the beginning of the value.
       
   160  */
       
   161 function _file_generic_settings_file_directory_validate($element, &$form_state) {
       
   162   // Strip slashes from the beginning and end of $widget['file_directory'].
       
   163   $value = trim($element['#value'], '\\/');
       
   164   form_set_value($element, $value, $form_state);
       
   165 }
       
   166 
       
   167 /**
       
   168  * Implements hook_field_load().
       
   169  */
       
   170 function file_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
       
   171 
       
   172   $fids = array();
       
   173   foreach ($entities as $id => $entity) {
       
   174     // Load the files from the files table.
       
   175     foreach ($items[$id] as $delta => $item) {
       
   176       if (!empty($item['fid'])) {
       
   177         $fids[] = $item['fid'];
       
   178       }
       
   179     }
       
   180   }
       
   181   $files = file_load_multiple($fids);
       
   182 
       
   183   foreach ($entities as $id => $entity) {
       
   184     foreach ($items[$id] as $delta => $item) {
       
   185       // If the file does not exist, mark the entire item as empty.
       
   186       if (empty($item['fid']) || !isset($files[$item['fid']])) {
       
   187         $items[$id][$delta] = NULL;
       
   188       }
       
   189       else {
       
   190         $items[$id][$delta] = array_merge((array) $files[$item['fid']], $item);
       
   191       }
       
   192     }
       
   193   }
       
   194 }
       
   195 
       
   196 /**
       
   197  * Implements hook_field_prepare_view().
       
   198  */
       
   199 function file_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
       
   200   // Remove files specified to not be displayed.
       
   201   foreach ($entities as $id => $entity) {
       
   202     foreach ($items[$id] as $delta => $item) {
       
   203       if (!file_field_displayed($item, $field)) {
       
   204         unset($items[$id][$delta]);
       
   205       }
       
   206     }
       
   207     // Ensure consecutive deltas.
       
   208     $items[$id] = array_values($items[$id]);
       
   209   }
       
   210 }
       
   211 
       
   212 /**
       
   213  * Implements hook_field_presave().
       
   214  */
       
   215 function file_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
       
   216   // Make sure that each file which will be saved with this object has a
       
   217   // permanent status, so that it will not be removed when temporary files are
       
   218   // cleaned up.
       
   219   foreach ($items as $delta => $item) {
       
   220     if (empty($item['fid'])) {
       
   221       unset($items[$delta]);
       
   222       continue;
       
   223     }
       
   224     $file = file_load($item['fid']);
       
   225     if (empty($file)) {
       
   226       unset($items[$delta]);
       
   227       continue;
       
   228     }
       
   229     if (!$file->status) {
       
   230       $file->status = FILE_STATUS_PERMANENT;
       
   231       file_save($file);
       
   232     }
       
   233   }
       
   234 }
       
   235 
       
   236 /**
       
   237  * Implements hook_field_insert().
       
   238  */
       
   239 function file_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
       
   240   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
       
   241 
       
   242   // Add a new usage of each uploaded file.
       
   243   foreach ($items as $item) {
       
   244     $file = (object) $item;
       
   245     file_usage_add($file, 'file', $entity_type, $id);
       
   246   }
       
   247 }
       
   248 
       
   249 /**
       
   250  * Implements hook_field_update().
       
   251  *
       
   252  * Checks for files that have been removed from the object.
       
   253  */
       
   254 function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
       
   255   // Check whether the field is defined on the object.
       
   256   if (!isset($entity->{$field['field_name']})) {
       
   257     // We cannot check for removed files if the field is not defined.
       
   258     return;
       
   259   }
       
   260 
       
   261   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
       
   262 
       
   263   // On new revisions, all files are considered to be a new usage and no
       
   264   // deletion of previous file usages are necessary.
       
   265   if (!empty($entity->revision)) {
       
   266     foreach ($items as $item) {
       
   267       $file = (object) $item;
       
   268       file_usage_add($file, 'file', $entity_type, $id);
       
   269     }
       
   270     return;
       
   271   }
       
   272 
       
   273   // Build a display of the current FIDs.
       
   274   $current_fids = array();
       
   275   foreach ($items as $item) {
       
   276     $current_fids[] = $item['fid'];
       
   277   }
       
   278 
       
   279   // Compare the original field values with the ones that are being saved. Use
       
   280   // $entity->original to check this when possible, but if it isn't available,
       
   281   // create a bare-bones entity and load its previous values instead.
       
   282   if (isset($entity->original)) {
       
   283     $original = $entity->original;
       
   284   }
       
   285   else {
       
   286     $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle));
       
   287     field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id']));
       
   288   }
       
   289   $original_fids = array();
       
   290   if (!empty($original->{$field['field_name']}[$langcode])) {
       
   291     foreach ($original->{$field['field_name']}[$langcode] as $original_item) {
       
   292       $original_fids[] = $original_item['fid'];
       
   293       if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) {
       
   294         // Decrement the file usage count by 1 and delete the file if possible.
       
   295         file_field_delete_file($original_item, $field, $entity_type, $id);
       
   296       }
       
   297     }
       
   298   }
       
   299 
       
   300   // Add new usage entries for newly added files.
       
   301   foreach ($items as $item) {
       
   302     if (!in_array($item['fid'], $original_fids)) {
       
   303       $file = (object) $item;
       
   304       file_usage_add($file, 'file', $entity_type, $id);
       
   305     }
       
   306   }
       
   307 }
       
   308 
       
   309 /**
       
   310  * Implements hook_field_delete().
       
   311  */
       
   312 function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
       
   313   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
       
   314 
       
   315   // Delete all file usages within this entity.
       
   316   foreach ($items as $delta => $item) {
       
   317     file_field_delete_file($item, $field, $entity_type, $id, 0);
       
   318   }
       
   319 }
       
   320 
       
   321 /**
       
   322  * Implements hook_field_delete_revision().
       
   323  */
       
   324 function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
       
   325   list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
       
   326   foreach ($items as $delta => $item) {
       
   327     // Decrement the file usage count by 1 and delete the file if possible.
       
   328     if (file_field_delete_file($item, $field, $entity_type, $id)) {
       
   329       $items[$delta] = NULL;
       
   330     }
       
   331   }
       
   332 }
       
   333 
       
   334 /**
       
   335  * Decrements the usage count for a file and attempts to delete it.
       
   336  *
       
   337  * This function only has an effect if the file being deleted is used only by
       
   338  * File module.
       
   339  *
       
   340  * @param $item
       
   341  *   The field item that contains a file array.
       
   342  * @param $field
       
   343  *   The field structure for the operation.
       
   344  * @param $entity_type
       
   345  *   The type of $entity.
       
   346  * @param $id
       
   347  *   The entity ID which contains the file being deleted.
       
   348  * @param $count
       
   349  *   (optional) The number of references to decrement from the object
       
   350  *   containing the file. Defaults to 1.
       
   351  *
       
   352  * @return
       
   353  *   Boolean TRUE if the file was deleted, or an array of remaining references
       
   354  *   if the file is still in use by other modules. Boolean FALSE if an error
       
   355  *   was encountered.
       
   356  */
       
   357 function file_field_delete_file($item, $field, $entity_type, $id, $count = 1) {
       
   358   // To prevent the file field from deleting files it doesn't know about, check
       
   359   // the file reference count. Temporary files can be deleted because they
       
   360   // are not yet associated with any content at all.
       
   361   $file = (object) $item;
       
   362   $file_usage = file_usage_list($file);
       
   363   if ($file->status == 0 || !empty($file_usage['file'])) {
       
   364     file_usage_delete($file, 'file', $entity_type, $id, $count);
       
   365     return file_delete($file);
       
   366   }
       
   367 
       
   368   // Even if the file is not deleted, return TRUE to indicate the file field
       
   369   // record can be removed from the field database tables.
       
   370   return TRUE;
       
   371 }
       
   372 
       
   373 /**
       
   374  * Implements hook_field_is_empty().
       
   375  */
       
   376 function file_field_is_empty($item, $field) {
       
   377   return empty($item['fid']);
       
   378 }
       
   379 
       
   380 /**
       
   381  * Determines whether a file should be displayed when outputting field content.
       
   382  *
       
   383  * @param $item
       
   384  *   A field item array.
       
   385  * @param $field
       
   386  *   A field array.
       
   387  *
       
   388  * @return
       
   389  *   Boolean TRUE if the file will be displayed, FALSE if the file is hidden.
       
   390  */
       
   391 function file_field_displayed($item, $field) {
       
   392   if (!empty($field['settings']['display_field'])) {
       
   393     return (bool) $item['display'];
       
   394   }
       
   395   return TRUE;
       
   396 }
       
   397 
       
   398 /**
       
   399  * Implements hook_field_formatter_info().
       
   400  */
       
   401 function file_field_formatter_info() {
       
   402   return array(
       
   403     'file_default' => array(
       
   404       'label' => t('Generic file'),
       
   405       'field types' => array('file'),
       
   406     ),
       
   407     'file_table' => array(
       
   408       'label' => t('Table of files'),
       
   409       'field types' => array('file'),
       
   410     ),
       
   411     'file_url_plain' => array(
       
   412       'label' => t('URL to file'),
       
   413       'field types' => array('file'),
       
   414     ),
       
   415   );
       
   416 }
       
   417 
       
   418 /**
       
   419  * Implements hook_field_widget_info().
       
   420  */
       
   421 function file_field_widget_info() {
       
   422   return array(
       
   423     'file_generic' => array(
       
   424       'label' => t('File'),
       
   425       'field types' => array('file'),
       
   426       'settings' => array(
       
   427         'progress_indicator' => 'throbber',
       
   428       ),
       
   429       'behaviors' => array(
       
   430         'multiple values' => FIELD_BEHAVIOR_CUSTOM,
       
   431         'default value' => FIELD_BEHAVIOR_NONE,
       
   432       ),
       
   433     ),
       
   434   );
       
   435 }
       
   436 
       
   437 /**
       
   438  * Implements hook_field_widget_settings_form().
       
   439  */
       
   440 function file_field_widget_settings_form($field, $instance) {
       
   441   $widget = $instance['widget'];
       
   442   $settings = $widget['settings'];
       
   443 
       
   444   $form['progress_indicator'] = array(
       
   445     '#type' => 'radios',
       
   446     '#title' => t('Progress indicator'),
       
   447     '#options' => array(
       
   448       'throbber' => t('Throbber'),
       
   449       'bar' => t('Bar with progress meter'),
       
   450     ),
       
   451     '#default_value' => $settings['progress_indicator'],
       
   452     '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
       
   453     '#weight' => 16,
       
   454     '#access' => file_progress_implementation(),
       
   455   );
       
   456 
       
   457   return $form;
       
   458 }
       
   459 
       
   460 /**
       
   461  * Implements hook_field_widget_form().
       
   462  */
       
   463 function file_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
       
   464 
       
   465   $defaults = array(
       
   466     'fid' => 0,
       
   467     'display' => !empty($field['settings']['display_default']),
       
   468     'description' => '',
       
   469   );
       
   470 
       
   471   // Load the items for form rebuilds from the field state as they might not be
       
   472   // in $form_state['values'] because of validation limitations. Also, they are
       
   473   // only passed in as $items when editing existing entities.
       
   474   $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
       
   475   if (isset($field_state['items'])) {
       
   476     $items = $field_state['items'];
       
   477   }
       
   478 
       
   479   // Essentially we use the managed_file type, extended with some enhancements.
       
   480   $element_info = element_info('managed_file');
       
   481   $element += array(
       
   482     '#type' => 'managed_file',
       
   483     '#upload_location' => file_field_widget_uri($field, $instance),
       
   484     '#upload_validators' => file_field_widget_upload_validators($field, $instance),
       
   485     '#value_callback' => 'file_field_widget_value',
       
   486     '#process' => array_merge($element_info['#process'], array('file_field_widget_process')),
       
   487     '#progress_indicator' => $instance['widget']['settings']['progress_indicator'],
       
   488     // Allows this field to return an array instead of a single value.
       
   489     '#extended' => TRUE,
       
   490   );
       
   491 
       
   492   if ($field['cardinality'] == 1) {
       
   493     // Set the default value.
       
   494     $element['#default_value'] = !empty($items) ? $items[0] : $defaults;
       
   495     // If there's only one field, return it as delta 0.
       
   496     if (empty($element['#default_value']['fid'])) {
       
   497       $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
       
   498     }
       
   499     $elements = array($element);
       
   500   }
       
   501   else {
       
   502     // If there are multiple values, add an element for each existing one.
       
   503     foreach ($items as $item) {
       
   504       $elements[$delta] = $element;
       
   505       $elements[$delta]['#default_value'] = $item;
       
   506       $elements[$delta]['#weight'] = $delta;
       
   507       $delta++;
       
   508     }
       
   509     // And then add one more empty row for new uploads except when this is a
       
   510     // programmed form as it is not necessary.
       
   511     if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
       
   512       $elements[$delta] = $element;
       
   513       $elements[$delta]['#default_value'] = $defaults;
       
   514       $elements[$delta]['#weight'] = $delta;
       
   515       $elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
       
   516     }
       
   517     // The group of elements all-together need some extra functionality
       
   518     // after building up the full list (like draggable table rows).
       
   519     $elements['#file_upload_delta'] = $delta;
       
   520     $elements['#theme'] = 'file_widget_multiple';
       
   521     $elements['#theme_wrappers'] = array('fieldset');
       
   522     $elements['#process'] = array('file_field_widget_process_multiple');
       
   523     $elements['#title'] = $element['#title'];
       
   524     $elements['#description'] = $element['#description'];
       
   525     $elements['#field_name'] = $element['#field_name'];
       
   526     $elements['#language'] = $element['#language'];
       
   527     $elements['#display_field'] = $field['settings']['display_field'];
       
   528 
       
   529     // Add some properties that will eventually be added to the file upload
       
   530     // field. These are added here so that they may be referenced easily through
       
   531     // a hook_form_alter().
       
   532     $elements['#file_upload_title'] = t('Add a new file');
       
   533     $elements['#file_upload_description'] = theme('file_upload_help', array('description' => '', 'upload_validators' => $elements[0]['#upload_validators']));
       
   534   }
       
   535 
       
   536   return $elements;
       
   537 }
       
   538 
       
   539 /**
       
   540  * Retrieves the upload validators for a file field.
       
   541  *
       
   542  * @param $field
       
   543  *   A field array.
       
   544  *
       
   545  * @return
       
   546  *   An array suitable for passing to file_save_upload() or the file field
       
   547  *   element's '#upload_validators' property.
       
   548  */
       
   549 function file_field_widget_upload_validators($field, $instance) {
       
   550   // Cap the upload size according to the PHP limit.
       
   551   $max_filesize = parse_size(file_upload_max_size());
       
   552   if (!empty($instance['settings']['max_filesize']) && parse_size($instance['settings']['max_filesize']) < $max_filesize) {
       
   553     $max_filesize = parse_size($instance['settings']['max_filesize']);
       
   554   }
       
   555 
       
   556   $validators = array();
       
   557 
       
   558   // There is always a file size limit due to the PHP server limit.
       
   559   $validators['file_validate_size'] = array($max_filesize);
       
   560 
       
   561   // Add the extension check if necessary.
       
   562   if (!empty($instance['settings']['file_extensions'])) {
       
   563     $validators['file_validate_extensions'] = array($instance['settings']['file_extensions']);
       
   564   }
       
   565 
       
   566   return $validators;
       
   567 }
       
   568 
       
   569 /**
       
   570  * Determines the URI for a file field instance.
       
   571  *
       
   572  * @param $field
       
   573  *   A field array.
       
   574  * @param $instance
       
   575  *   A field instance array.
       
   576  * @param $data
       
   577  *   An array of token objects to pass to token_replace().
       
   578  *
       
   579  * @return
       
   580  *   A file directory URI with tokens replaced.
       
   581  *
       
   582  * @see token_replace()
       
   583  */
       
   584 function file_field_widget_uri($field, $instance, $data = array()) {
       
   585   $destination = trim($instance['settings']['file_directory'], '/');
       
   586 
       
   587   // Replace tokens.
       
   588   $destination = token_replace($destination, $data);
       
   589 
       
   590   return $field['settings']['uri_scheme'] . '://' . $destination;
       
   591 }
       
   592 
       
   593 /**
       
   594  * The #value_callback for the file_generic field element.
       
   595  */
       
   596 function file_field_widget_value($element, $input = FALSE, $form_state) {
       
   597   if ($input) {
       
   598     // Checkboxes lose their value when empty.
       
   599     // If the display field is present make sure its unchecked value is saved.
       
   600     $field = field_widget_field($element, $form_state);
       
   601     if (empty($input['display'])) {
       
   602       $input['display'] = $field['settings']['display_field'] ? 0 : 1;
       
   603     }
       
   604   }
       
   605 
       
   606   // We depend on the managed file element to handle uploads.
       
   607   $return = file_managed_file_value($element, $input, $form_state);
       
   608 
       
   609   // Ensure that all the required properties are returned even if empty.
       
   610   $return += array(
       
   611     'fid' => 0,
       
   612     'display' => 1,
       
   613     'description' => '',
       
   614   );
       
   615 
       
   616   return $return;
       
   617 }
       
   618 
       
   619 /**
       
   620  * An element #process callback for the file_generic field type.
       
   621  *
       
   622  * Expands the file_generic type to include the description and display fields.
       
   623  */
       
   624 function file_field_widget_process($element, &$form_state, $form) {
       
   625   $item = $element['#value'];
       
   626   $item['fid'] = $element['fid']['#value'];
       
   627 
       
   628   $field = field_widget_field($element, $form_state);
       
   629   $instance = field_widget_instance($element, $form_state);
       
   630   $settings = $instance['widget']['settings'];
       
   631 
       
   632   $element['#theme'] = 'file_widget';
       
   633 
       
   634   // Add the display field if enabled.
       
   635   if (!empty($field['settings']['display_field'])) {
       
   636     $element['display'] = array(
       
   637       '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
       
   638       '#title' => t('Include file in display'),
       
   639       '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
       
   640       '#attributes' => array('class' => array('file-display')),
       
   641     );
       
   642   }
       
   643   else {
       
   644     $element['display'] = array(
       
   645       '#type' => 'hidden',
       
   646       '#value' => '1',
       
   647     );
       
   648   }
       
   649 
       
   650   // Add the description field if enabled.
       
   651   if (!empty($instance['settings']['description_field']) && $item['fid']) {
       
   652     $element['description'] = array(
       
   653       '#type' => variable_get('file_description_type', 'textfield'),
       
   654       '#title' => t('Description'),
       
   655       '#value' => isset($item['description']) ? $item['description'] : '',
       
   656       '#maxlength' => variable_get('file_description_length', 128),
       
   657       '#description' => t('The description may be used as the label of the link to the file.'),
       
   658     );
       
   659   }
       
   660 
       
   661   // Adjust the Ajax settings so that on upload and remove of any individual
       
   662   // file, the entire group of file fields is updated together.
       
   663   if ($field['cardinality'] != 1) {
       
   664     $parents = array_slice($element['#array_parents'], 0, -1);
       
   665     $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
       
   666     $field_element = drupal_array_get_nested_value($form, $parents);
       
   667     $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
       
   668     foreach (element_children($element) as $key) {
       
   669       if (isset($element[$key]['#ajax'])) {
       
   670         $element[$key]['#ajax']['path'] = $new_path;
       
   671         $element[$key]['#ajax']['wrapper'] = $new_wrapper;
       
   672       }
       
   673     }
       
   674     unset($element['#prefix'], $element['#suffix']);
       
   675   }
       
   676 
       
   677   // Add another submit handler to the upload and remove buttons, to implement
       
   678   // functionality needed by the field widget. This submit handler, along with
       
   679   // the rebuild logic in file_field_widget_form() requires the entire field,
       
   680   // not just the individual item, to be valid.
       
   681   foreach (array('upload_button', 'remove_button') as $key) {
       
   682     $element[$key]['#submit'][] = 'file_field_widget_submit';
       
   683     $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
       
   684   }
       
   685 
       
   686   return $element;
       
   687 }
       
   688 
       
   689 /**
       
   690  * An element #process callback for a group of file_generic fields.
       
   691  *
       
   692  * Adds the weight field to each row so it can be ordered and adds a new Ajax
       
   693  * wrapper around the entire group so it can be replaced all at once.
       
   694  */
       
   695 function file_field_widget_process_multiple($element, &$form_state, $form) {
       
   696   $element_children = element_children($element, TRUE);
       
   697   $count = count($element_children);
       
   698 
       
   699   foreach ($element_children as $delta => $key) {
       
   700     if ($key != $element['#file_upload_delta']) {
       
   701       $description = _file_field_get_description_from_element($element[$key]);
       
   702       $element[$key]['_weight'] = array(
       
   703         '#type' => 'weight',
       
   704         '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
       
   705         '#title_display' => 'invisible',
       
   706         '#delta' => $count,
       
   707         '#default_value' => $delta,
       
   708       );
       
   709     }
       
   710     else {
       
   711       // The title needs to be assigned to the upload field so that validation
       
   712       // errors include the correct widget label.
       
   713       $element[$key]['#title'] = $element['#title'];
       
   714       $element[$key]['_weight'] = array(
       
   715         '#type' => 'hidden',
       
   716         '#default_value' => $delta,
       
   717       );
       
   718     }
       
   719   }
       
   720 
       
   721   // Add a new wrapper around all the elements for Ajax replacement.
       
   722   $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
       
   723   $element['#suffix'] = '</div>';
       
   724 
       
   725   return $element;
       
   726 }
       
   727 
       
   728 /**
       
   729  * Retrieves the file description from a field field element.
       
   730  *
       
   731  * This helper function is used by file_field_widget_process_multiple().
       
   732  *
       
   733  * @param $element
       
   734  *   The element being processed.
       
   735  *
       
   736  * @return
       
   737  *   A description of the file suitable for use in the administrative interface.
       
   738  */
       
   739 function _file_field_get_description_from_element($element) {
       
   740   // Use the actual file description, if it's available.
       
   741   if (!empty($element['#default_value']['description'])) {
       
   742     return $element['#default_value']['description'];
       
   743   }
       
   744   // Otherwise, fall back to the filename.
       
   745   if (!empty($element['#default_value']['filename'])) {
       
   746     return $element['#default_value']['filename'];
       
   747   }
       
   748   // This is probably a newly uploaded file; no description is available.
       
   749   return FALSE;
       
   750 }
       
   751 
       
   752 /**
       
   753  * Form submission handler for upload/remove button of file_field_widget_form().
       
   754  *
       
   755  * This runs in addition to and after file_managed_file_submit().
       
   756  *
       
   757  * @see file_managed_file_submit()
       
   758  * @see file_field_widget_form()
       
   759  * @see file_field_widget_process()
       
   760  */
       
   761 function file_field_widget_submit($form, &$form_state) {
       
   762   // During the form rebuild, file_field_widget_form() will create field item
       
   763   // widget elements using re-indexed deltas, so clear out $form_state['input']
       
   764   // to avoid a mismatch between old and new deltas. The rebuilt elements will
       
   765   // have #default_value set appropriately for the current state of the field,
       
   766   // so nothing is lost in doing this.
       
   767   $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
       
   768   drupal_array_set_nested_value($form_state['input'], $parents, NULL);
       
   769 
       
   770   $button = $form_state['triggering_element'];
       
   771 
       
   772   // Go one level up in the form, to the widgets container.
       
   773   $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
       
   774   $field_name = $element['#field_name'];
       
   775   $langcode = $element['#language'];
       
   776   $parents = $element['#field_parents'];
       
   777 
       
   778   $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2));
       
   779   foreach ($submitted_values as $delta => $submitted_value) {
       
   780     if (!$submitted_value['fid']) {
       
   781       unset($submitted_values[$delta]);
       
   782     }
       
   783   }
       
   784 
       
   785   // Re-index deltas after removing empty items.
       
   786   $submitted_values = array_values($submitted_values);
       
   787 
       
   788   // Update form_state values.
       
   789   drupal_array_set_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2), $submitted_values);
       
   790 
       
   791   // Update items.
       
   792   $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
       
   793   $field_state['items'] = $submitted_values;
       
   794   field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
       
   795 }
       
   796 
       
   797 /**
       
   798  * Returns HTML for an individual file upload widget.
       
   799  *
       
   800  * @param $variables
       
   801  *   An associative array containing:
       
   802  *   - element: A render element representing the widget.
       
   803  *
       
   804  * @ingroup themeable
       
   805  */
       
   806 function theme_file_widget($variables) {
       
   807   $element = $variables['element'];
       
   808   $output = '';
       
   809 
       
   810   // The "form-managed-file" class is required for proper Ajax functionality.
       
   811   $output .= '<div class="file-widget form-managed-file clearfix">';
       
   812   if ($element['fid']['#value'] != 0) {
       
   813     // Add the file size after the file name.
       
   814     $element['filename']['#markup'] .= ' <span class="file-size">(' . format_size($element['#file']->filesize) . ')</span> ';
       
   815   }
       
   816   $output .= drupal_render_children($element);
       
   817   $output .= '</div>';
       
   818 
       
   819   return $output;
       
   820 }
       
   821 
       
   822 /**
       
   823  * Returns HTML for a group of file upload widgets.
       
   824  *
       
   825  * @param $variables
       
   826  *   An associative array containing:
       
   827  *   - element: A render element representing the widgets.
       
   828  *
       
   829  * @ingroup themeable
       
   830  */
       
   831 function theme_file_widget_multiple($variables) {
       
   832   $element = $variables['element'];
       
   833 
       
   834   // Special ID and classes for draggable tables.
       
   835   $weight_class = $element['#id'] . '-weight';
       
   836   $table_id = $element['#id'] . '-table';
       
   837 
       
   838   // Build up a table of applicable fields.
       
   839   $headers = array();
       
   840   $headers[] = t('File information');
       
   841   if ($element['#display_field']) {
       
   842     $headers[] = array(
       
   843       'data' => t('Display'),
       
   844       'class' => array('checkbox'),
       
   845     );
       
   846   }
       
   847   $headers[] = t('Weight');
       
   848   $headers[] = t('Operations');
       
   849 
       
   850   // Get our list of widgets in order (needed when the form comes back after
       
   851   // preview or failed validation).
       
   852   $widgets = array();
       
   853   foreach (element_children($element) as $key) {
       
   854     $widgets[] = &$element[$key];
       
   855   }
       
   856   usort($widgets, '_field_sort_items_value_helper');
       
   857 
       
   858   $rows = array();
       
   859   foreach ($widgets as $key => &$widget) {
       
   860     // Save the uploading row for last.
       
   861     if ($widget['#file'] == FALSE) {
       
   862       $widget['#title'] = $element['#file_upload_title'];
       
   863       $widget['#description'] = $element['#file_upload_description'];
       
   864       continue;
       
   865     }
       
   866 
       
   867     // Delay rendering of the buttons, so that they can be rendered later in the
       
   868     // "operations" column.
       
   869     $operations_elements = array();
       
   870     foreach (element_children($widget) as $sub_key) {
       
   871       if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
       
   872         hide($widget[$sub_key]);
       
   873         $operations_elements[] = &$widget[$sub_key];
       
   874       }
       
   875     }
       
   876 
       
   877     // Delay rendering of the "Display" option and the weight selector, so that
       
   878     // each can be rendered later in its own column.
       
   879     if ($element['#display_field']) {
       
   880       hide($widget['display']);
       
   881     }
       
   882     hide($widget['_weight']);
       
   883 
       
   884     // Render everything else together in a column, without the normal wrappers.
       
   885     $widget['#theme_wrappers'] = array();
       
   886     $information = drupal_render($widget);
       
   887 
       
   888     // Render the previously hidden elements, using render() instead of
       
   889     // drupal_render(), to undo the earlier hide().
       
   890     $operations = '';
       
   891     foreach ($operations_elements as $operation_element) {
       
   892       $operations .= render($operation_element);
       
   893     }
       
   894     $display = '';
       
   895     if ($element['#display_field']) {
       
   896       unset($widget['display']['#title']);
       
   897       $display = array(
       
   898         'data' => render($widget['display']),
       
   899         'class' => array('checkbox'),
       
   900       );
       
   901     }
       
   902     $widget['_weight']['#attributes']['class'] = array($weight_class);
       
   903     $weight = render($widget['_weight']);
       
   904 
       
   905     // Arrange the row with all of the rendered columns.
       
   906     $row = array();
       
   907     $row[] = $information;
       
   908     if ($element['#display_field']) {
       
   909       $row[] = $display;
       
   910     }
       
   911     $row[] = $weight;
       
   912     $row[] = $operations;
       
   913     $rows[] = array(
       
   914       'data' => $row,
       
   915       'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
       
   916     );
       
   917   }
       
   918 
       
   919   drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
       
   920 
       
   921   $output = '';
       
   922   $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
       
   923   $output .= drupal_render_children($element);
       
   924   return $output;
       
   925 }
       
   926 
       
   927 
       
   928 /**
       
   929  * Returns HTML for help text based on file upload validators.
       
   930  *
       
   931  * @param $variables
       
   932  *   An associative array containing:
       
   933  *   - description: The normal description for this field, specified by the
       
   934  *     user.
       
   935  *   - upload_validators: An array of upload validators as used in
       
   936  *     $element['#upload_validators'].
       
   937  *
       
   938  * @ingroup themeable
       
   939  */
       
   940 function theme_file_upload_help($variables) {
       
   941   $description = $variables['description'];
       
   942   $upload_validators = $variables['upload_validators'];
       
   943 
       
   944   $descriptions = array();
       
   945 
       
   946   if (strlen($description)) {
       
   947     $descriptions[] = $description;
       
   948   }
       
   949   if (isset($upload_validators['file_validate_size'])) {
       
   950     $descriptions[] = t('Files must be less than !size.', array('!size' => '<strong>' . format_size($upload_validators['file_validate_size'][0]) . '</strong>'));
       
   951   }
       
   952   if (isset($upload_validators['file_validate_extensions'])) {
       
   953     $descriptions[] = t('Allowed file types: !extensions.', array('!extensions' => '<strong>' . check_plain($upload_validators['file_validate_extensions'][0]) . '</strong>'));
       
   954   }
       
   955   if (isset($upload_validators['file_validate_image_resolution'])) {
       
   956     $max = $upload_validators['file_validate_image_resolution'][0];
       
   957     $min = $upload_validators['file_validate_image_resolution'][1];
       
   958     if ($min && $max && $min == $max) {
       
   959       $descriptions[] = t('Images must be exactly !size pixels.', array('!size' => '<strong>' . $max . '</strong>'));
       
   960     }
       
   961     elseif ($min && $max) {
       
   962       $descriptions[] = t('Images must be between !min and !max pixels.', array('!min' => '<strong>' . $min . '</strong>', '!max' => '<strong>' . $max . '</strong>'));
       
   963     }
       
   964     elseif ($min) {
       
   965       $descriptions[] = t('Images must be larger than !min pixels.', array('!min' => '<strong>' . $min . '</strong>'));
       
   966     }
       
   967     elseif ($max) {
       
   968       $descriptions[] = t('Images must be smaller than !max pixels.', array('!max' => '<strong>' . $max . '</strong>'));
       
   969     }
       
   970   }
       
   971 
       
   972   return implode('<br />', $descriptions);
       
   973 }
       
   974 
       
   975 /**
       
   976  * Implements hook_field_formatter_view().
       
   977  */
       
   978 function file_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
       
   979   $element = array();
       
   980 
       
   981   switch ($display['type']) {
       
   982     case 'file_default':
       
   983       foreach ($items as $delta => $item) {
       
   984         $element[$delta] = array(
       
   985           '#theme' => 'file_link',
       
   986           '#file' => (object) $item,
       
   987         );
       
   988       }
       
   989       break;
       
   990 
       
   991     case 'file_url_plain':
       
   992       foreach ($items as $delta => $item) {
       
   993         $element[$delta] = array('#markup' => empty($item['uri']) ? '' : file_create_url($item['uri']));
       
   994       }
       
   995       break;
       
   996 
       
   997     case 'file_table':
       
   998       if (!empty($items)) {
       
   999         // Display all values in a single element..
       
  1000         $element[0] = array(
       
  1001           '#theme' => 'file_formatter_table',
       
  1002           '#items' => $items,
       
  1003         );
       
  1004       }
       
  1005       break;
       
  1006   }
       
  1007 
       
  1008   return $element;
       
  1009 }
       
  1010 
       
  1011 /**
       
  1012  * Returns HTML for a file attachments table.
       
  1013  *
       
  1014  * @param $variables
       
  1015  *   An associative array containing:
       
  1016  *   - items: An array of file attachments.
       
  1017  *
       
  1018  * @ingroup themeable
       
  1019  */
       
  1020 function theme_file_formatter_table($variables) {
       
  1021   $header = array(t('Attachment'), t('Size'));
       
  1022   $rows = array();
       
  1023   foreach ($variables['items'] as $delta => $item) {
       
  1024     $rows[] = array(
       
  1025       theme('file_link', array('file' => (object) $item)),
       
  1026       format_size($item['filesize']),
       
  1027     );
       
  1028   }
       
  1029 
       
  1030   return empty($rows) ? '' : theme('table', array('header' => $header, 'rows' => $rows));
       
  1031 }