Projet

Général

Profil

Paste
Télécharger (34,8 ko) Statistiques
| Branche: | Révision:

root / htmltest / modules / file / file.field.inc @ 85ad3d82

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
    // By making this field required, we prevent a potential security issue
96
    // that would allow files of any type to be uploaded.
97
    '#required' => TRUE,
98
  );
99

    
100
  $form['max_filesize'] = array(
101
    '#type' => 'textfield',
102
    '#title' => t('Maximum upload size'),
103
    '#default_value' => $settings['max_filesize'],
104
    '#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()))),
105
    '#size' => 10,
106
    '#element_validate' => array('_file_generic_settings_max_filesize'),
107
    '#weight' => 5,
108
  );
109

    
110
  $form['description_field'] = array(
111
    '#type' => 'checkbox',
112
    '#title' => t('Enable <em>Description</em> field'),
113
    '#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
114
    '#description' => t('The description field allows users to enter a description about the uploaded file.'),
115
    '#parents' => array('instance', 'settings', 'description_field'),
116
    '#weight' => 11,
117
  );
118

    
119
  return $form;
120
}
121

    
122
/**
123
 * Element validate callback for the maximum upload size field.
124
 *
125
 * Ensure a size that can be parsed by parse_size() has been entered.
126
 */
127
function _file_generic_settings_max_filesize($element, &$form_state) {
128
  if (!empty($element['#value']) && !is_numeric(parse_size($element['#value']))) {
129
    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']))));
130
  }
131
}
132

    
133
/**
134
 * Element validate callback for the allowed file extensions field.
135
 *
136
 * This doubles as a convenience clean-up function and a validation routine.
137
 * Commas are allowed by the end-user, but ultimately the value will be stored
138
 * as a space-separated list for compatibility with file_validate_extensions().
139
 */
140
function _file_generic_settings_extensions($element, &$form_state) {
141
  if (!empty($element['#value'])) {
142
    $extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
143
    $extensions = array_filter(explode(' ', $extensions));
144
    $extensions = implode(' ', array_unique($extensions));
145
    if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
146
      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.'));
147
    }
148
    else {
149
      form_set_value($element, $extensions, $form_state);
150
    }
151
  }
152
}
153

    
154
/**
155
 * Element validate callback for the file destination field.
156
 *
157
 * Remove slashes from the beginning and end of the destination value and ensure
158
 * that the file directory path is not included at the beginning of the value.
159
 */
160
function _file_generic_settings_file_directory_validate($element, &$form_state) {
161
  // Strip slashes from the beginning and end of $widget['file_directory'].
162
  $value = trim($element['#value'], '\\/');
163
  form_set_value($element, $value, $form_state);
164
}
165

    
166
/**
167
 * Implements hook_field_load().
168
 */
169
function file_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
170

    
171
  $fids = array();
172
  foreach ($entities as $id => $entity) {
173
    // Load the files from the files table.
174
    foreach ($items[$id] as $delta => $item) {
175
      if (!empty($item['fid'])) {
176
        $fids[] = $item['fid'];
177
      }
178
    }
179
  }
180
  $files = file_load_multiple($fids);
181

    
182
  foreach ($entities as $id => $entity) {
183
    foreach ($items[$id] as $delta => $item) {
184
      // If the file does not exist, mark the entire item as empty.
185
      if (empty($item['fid']) || !isset($files[$item['fid']])) {
186
        $items[$id][$delta] = NULL;
187
      }
188
      else {
189
        $items[$id][$delta] = array_merge((array) $files[$item['fid']], $item);
190
      }
191
    }
192
  }
193
}
194

    
195
/**
196
 * Implements hook_field_prepare_view().
197
 */
198
function file_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
199
  // Remove files specified to not be displayed.
200
  foreach ($entities as $id => $entity) {
201
    foreach ($items[$id] as $delta => $item) {
202
      if (!file_field_displayed($item, $field)) {
203
        unset($items[$id][$delta]);
204
      }
205
    }
206
    // Ensure consecutive deltas.
207
    $items[$id] = array_values($items[$id]);
208
  }
209
}
210

    
211
/**
212
 * Implements hook_field_presave().
213
 */
214
function file_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
215
  // Make sure that each file which will be saved with this object has a
216
  // permanent status, so that it will not be removed when temporary files are
217
  // cleaned up.
218
  foreach ($items as $delta => $item) {
219
    if (empty($item['fid'])) {
220
      unset($items[$delta]);
221
      continue;
222
    }
223
    $file = file_load($item['fid']);
224
    if (empty($file)) {
225
      unset($items[$delta]);
226
      continue;
227
    }
228
    if (!$file->status) {
229
      $file->status = FILE_STATUS_PERMANENT;
230
      file_save($file);
231
    }
232
  }
233
}
234

    
235
/**
236
 * Implements hook_field_insert().
237
 */
238
function file_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
239
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
240

    
241
  // Add a new usage of each uploaded file.
242
  foreach ($items as $item) {
243
    $file = (object) $item;
244
    file_usage_add($file, 'file', $entity_type, $id);
245
  }
246
}
247

    
248
/**
249
 * Implements hook_field_update().
250
 *
251
 * Checks for files that have been removed from the object.
252
 */
253
function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
254
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
255

    
256
  // On new revisions, all files are considered to be a new usage and no
257
  // deletion of previous file usages are necessary.
258
  if (!empty($entity->revision)) {
259
    foreach ($items as $item) {
260
      $file = (object) $item;
261
      file_usage_add($file, 'file', $entity_type, $id);
262
    }
263
    return;
264
  }
265

    
266
  // Build a display of the current FIDs.
267
  $current_fids = array();
268
  foreach ($items as $item) {
269
    $current_fids[] = $item['fid'];
270
  }
271

    
272
  // Compare the original field values with the ones that are being saved. Use
273
  // $entity->original to check this when possible, but if it isn't available,
274
  // create a bare-bones entity and load its previous values instead.
275
  if (isset($entity->original)) {
276
    $original = $entity->original;
277
  }
278
  else {
279
    $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle));
280
    field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id']));
281
  }
282
  $original_fids = array();
283
  if (!empty($original->{$field['field_name']}[$langcode])) {
284
    foreach ($original->{$field['field_name']}[$langcode] as $original_item) {
285
      $original_fids[] = $original_item['fid'];
286
      if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) {
287
        // Decrement the file usage count by 1 and delete the file if possible.
288
        file_field_delete_file($original_item, $field, $entity_type, $id);
289
      }
290
    }
291
  }
292

    
293
  // Add new usage entries for newly added files.
294
  foreach ($items as $item) {
295
    if (!in_array($item['fid'], $original_fids)) {
296
      $file = (object) $item;
297
      file_usage_add($file, 'file', $entity_type, $id);
298
    }
299
  }
300
}
301

    
302
/**
303
 * Implements hook_field_delete().
304
 */
305
function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
306
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
307

    
308
  // Delete all file usages within this entity.
309
  foreach ($items as $delta => $item) {
310
    file_field_delete_file($item, $field, $entity_type, $id, 0);
311
  }
312
}
313

    
314
/**
315
 * Implements hook_field_delete_revision().
316
 */
317
function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
318
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
319
  foreach ($items as $delta => $item) {
320
    // Decrement the file usage count by 1 and delete the file if possible.
321
    if (file_field_delete_file($item, $field, $entity_type, $id)) {
322
      $items[$delta] = NULL;
323
    }
324
  }
325
}
326

    
327
/**
328
 * Decrements the usage count for a file and attempts to delete it.
329
 *
330
 * This function only has an effect if the file being deleted is used only by
331
 * File module.
332
 *
333
 * @param $item
334
 *   The field item that contains a file array.
335
 * @param $field
336
 *   The field structure for the operation.
337
 * @param $entity_type
338
 *   The type of $entity.
339
 * @param $id
340
 *   The entity ID which contains the file being deleted.
341
 * @param $count
342
 *   (optional) The number of references to decrement from the object
343
 *   containing the file. Defaults to 1.
344
 *
345
 * @return
346
 *   Boolean TRUE if the file was deleted, or an array of remaining references
347
 *   if the file is still in use by other modules. Boolean FALSE if an error
348
 *   was encountered.
349
 */
350
function file_field_delete_file($item, $field, $entity_type, $id, $count = 1) {
351
  // To prevent the file field from deleting files it doesn't know about, check
352
  // the file reference count. Temporary files can be deleted because they
353
  // are not yet associated with any content at all.
354
  $file = (object) $item;
355
  $file_usage = file_usage_list($file);
356
  if ($file->status == 0 || !empty($file_usage['file'])) {
357
    file_usage_delete($file, 'file', $entity_type, $id, $count);
358
    return file_delete($file);
359
  }
360

    
361
  // Even if the file is not deleted, return TRUE to indicate the file field
362
  // record can be removed from the field database tables.
363
  return TRUE;
364
}
365

    
366
/**
367
 * Implements hook_field_is_empty().
368
 */
369
function file_field_is_empty($item, $field) {
370
  return empty($item['fid']);
371
}
372

    
373
/**
374
 * Determines whether a file should be displayed when outputting field content.
375
 *
376
 * @param $item
377
 *   A field item array.
378
 * @param $field
379
 *   A field array.
380
 *
381
 * @return
382
 *   Boolean TRUE if the file will be displayed, FALSE if the file is hidden.
383
 */
384
function file_field_displayed($item, $field) {
385
  if (!empty($field['settings']['display_field'])) {
386
    return (bool) $item['display'];
387
  }
388
  return TRUE;
389
}
390

    
391
/**
392
 * Implements hook_field_formatter_info().
393
 */
394
function file_field_formatter_info() {
395
  return array(
396
    'file_default' => array(
397
      'label' => t('Generic file'),
398
      'field types' => array('file'),
399
    ),
400
    'file_table' => array(
401
      'label' => t('Table of files'),
402
      'field types' => array('file'),
403
    ),
404
    'file_url_plain' => array(
405
      'label' => t('URL to file'),
406
      'field types' => array('file'),
407
    ),
408
  );
409
}
410

    
411
/**
412
 * Implements hook_field_widget_info().
413
 */
414
function file_field_widget_info() {
415
  return array(
416
    'file_generic' => array(
417
      'label' => t('File'),
418
      'field types' => array('file'),
419
      'settings' => array(
420
        'progress_indicator' => 'throbber',
421
      ),
422
      'behaviors' => array(
423
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
424
        'default value' => FIELD_BEHAVIOR_NONE,
425
      ),
426
    ),
427
  );
428
}
429

    
430
/**
431
 * Implements hook_field_widget_settings_form().
432
 */
433
function file_field_widget_settings_form($field, $instance) {
434
  $widget = $instance['widget'];
435
  $settings = $widget['settings'];
436

    
437
  $form['progress_indicator'] = array(
438
    '#type' => 'radios',
439
    '#title' => t('Progress indicator'),
440
    '#options' => array(
441
      'throbber' => t('Throbber'),
442
      'bar' => t('Bar with progress meter'),
443
    ),
444
    '#default_value' => $settings['progress_indicator'],
445
    '#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.'),
446
    '#weight' => 16,
447
    '#access' => file_progress_implementation(),
448
  );
449

    
450
  return $form;
451
}
452

    
453
/**
454
 * Implements hook_field_widget_form().
455
 */
456
function file_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
457

    
458
  $defaults = array(
459
    'fid' => 0,
460
    'display' => !empty($field['settings']['display_default']),
461
    'description' => '',
462
  );
463

    
464
  // Load the items for form rebuilds from the field state as they might not be
465
  // in $form_state['values'] because of validation limitations. Also, they are
466
  // only passed in as $items when editing existing entities.
467
  $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
468
  if (isset($field_state['items'])) {
469
    $items = $field_state['items'];
470
  }
471

    
472
  // Essentially we use the managed_file type, extended with some enhancements.
473
  $element_info = element_info('managed_file');
474
  $element += array(
475
    '#type' => 'managed_file',
476
    '#upload_location' => file_field_widget_uri($field, $instance),
477
    '#upload_validators' => file_field_widget_upload_validators($field, $instance),
478
    '#value_callback' => 'file_field_widget_value',
479
    '#process' => array_merge($element_info['#process'], array('file_field_widget_process')),
480
    '#progress_indicator' => $instance['widget']['settings']['progress_indicator'],
481
    // Allows this field to return an array instead of a single value.
482
    '#extended' => TRUE,
483
  );
484

    
485
  if ($field['cardinality'] == 1) {
486
    // Set the default value.
487
    $element['#default_value'] = !empty($items) ? $items[0] : $defaults;
488
    // If there's only one field, return it as delta 0.
489
    if (empty($element['#default_value']['fid'])) {
490
      $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
491
    }
492
    $elements = array($element);
493
  }
494
  else {
495
    // If there are multiple values, add an element for each existing one.
496
    foreach ($items as $item) {
497
      $elements[$delta] = $element;
498
      $elements[$delta]['#default_value'] = $item;
499
      $elements[$delta]['#weight'] = $delta;
500
      $delta++;
501
    }
502
    // And then add one more empty row for new uploads except when this is a
503
    // programmed form as it is not necessary.
504
    if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
505
      $elements[$delta] = $element;
506
      $elements[$delta]['#default_value'] = $defaults;
507
      $elements[$delta]['#weight'] = $delta;
508
      $elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
509
    }
510
    // The group of elements all-together need some extra functionality
511
    // after building up the full list (like draggable table rows).
512
    $elements['#file_upload_delta'] = $delta;
513
    $elements['#theme'] = 'file_widget_multiple';
514
    $elements['#theme_wrappers'] = array('fieldset');
515
    $elements['#process'] = array('file_field_widget_process_multiple');
516
    $elements['#title'] = $element['#title'];
517
    $elements['#description'] = $element['#description'];
518
    $elements['#field_name'] = $element['#field_name'];
519
    $elements['#language'] = $element['#language'];
520
    $elements['#display_field'] = $field['settings']['display_field'];
521

    
522
    // Add some properties that will eventually be added to the file upload
523
    // field. These are added here so that they may be referenced easily through
524
    // a hook_form_alter().
525
    $elements['#file_upload_title'] = t('Add a new file');
526
    $elements['#file_upload_description'] = theme('file_upload_help', array('description' => '', 'upload_validators' => $elements[0]['#upload_validators']));
527
  }
528

    
529
  return $elements;
530
}
531

    
532
/**
533
 * Retrieves the upload validators for a file field.
534
 *
535
 * @param $field
536
 *   A field array.
537
 *
538
 * @return
539
 *   An array suitable for passing to file_save_upload() or the file field
540
 *   element's '#upload_validators' property.
541
 */
542
function file_field_widget_upload_validators($field, $instance) {
543
  // Cap the upload size according to the PHP limit.
544
  $max_filesize = parse_size(file_upload_max_size());
545
  if (!empty($instance['settings']['max_filesize']) && parse_size($instance['settings']['max_filesize']) < $max_filesize) {
546
    $max_filesize = parse_size($instance['settings']['max_filesize']);
547
  }
548

    
549
  $validators = array();
550

    
551
  // There is always a file size limit due to the PHP server limit.
552
  $validators['file_validate_size'] = array($max_filesize);
553

    
554
  // Add the extension check if necessary.
555
  if (!empty($instance['settings']['file_extensions'])) {
556
    $validators['file_validate_extensions'] = array($instance['settings']['file_extensions']);
557
  }
558

    
559
  return $validators;
560
}
561

    
562
/**
563
 * Determines the URI for a file field instance.
564
 *
565
 * @param $field
566
 *   A field array.
567
 * @param $instance
568
 *   A field instance array.
569
 * @param $data
570
 *   An array of token objects to pass to token_replace().
571
 *
572
 * @return
573
 *   A file directory URI with tokens replaced.
574
 *
575
 * @see token_replace()
576
 */
577
function file_field_widget_uri($field, $instance, $data = array()) {
578
  $destination = trim($instance['settings']['file_directory'], '/');
579

    
580
  // Replace tokens.
581
  $destination = token_replace($destination, $data);
582

    
583
  return $field['settings']['uri_scheme'] . '://' . $destination;
584
}
585

    
586
/**
587
 * The #value_callback for the file_generic field element.
588
 */
589
function file_field_widget_value($element, $input = FALSE, $form_state) {
590
  if ($input) {
591
    // Checkboxes lose their value when empty.
592
    // If the display field is present make sure its unchecked value is saved.
593
    $field = field_widget_field($element, $form_state);
594
    if (empty($input['display'])) {
595
      $input['display'] = $field['settings']['display_field'] ? 0 : 1;
596
    }
597
  }
598

    
599
  // We depend on the managed file element to handle uploads.
600
  $return = file_managed_file_value($element, $input, $form_state);
601

    
602
  // Ensure that all the required properties are returned even if empty.
603
  $return += array(
604
    'fid' => 0,
605
    'display' => 1,
606
    'description' => '',
607
  );
608

    
609
  return $return;
610
}
611

    
612
/**
613
 * An element #process callback for the file_generic field type.
614
 *
615
 * Expands the file_generic type to include the description and display fields.
616
 */
617
function file_field_widget_process($element, &$form_state, $form) {
618
  $item = $element['#value'];
619
  $item['fid'] = $element['fid']['#value'];
620

    
621
  $field = field_widget_field($element, $form_state);
622
  $instance = field_widget_instance($element, $form_state);
623
  $settings = $instance['widget']['settings'];
624

    
625
  $element['#theme'] = 'file_widget';
626

    
627
  // Add the display field if enabled.
628
  if (!empty($field['settings']['display_field']) && $item['fid']) {
629
    $element['display'] = array(
630
      '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
631
      '#title' => t('Include file in display'),
632
      '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
633
      '#attributes' => array('class' => array('file-display')),
634
    );
635
  }
636
  else {
637
    $element['display'] = array(
638
      '#type' => 'hidden',
639
      '#value' => '1',
640
    );
641
  }
642

    
643
  // Add the description field if enabled.
644
  if (!empty($instance['settings']['description_field']) && $item['fid']) {
645
    $element['description'] = array(
646
      '#type' => variable_get('file_description_type', 'textfield'),
647
      '#title' => t('Description'),
648
      '#value' => isset($item['description']) ? $item['description'] : '',
649
      '#maxlength' => variable_get('file_description_length', 128),
650
      '#description' => t('The description may be used as the label of the link to the file.'),
651
    );
652
  }
653

    
654
  // Adjust the Ajax settings so that on upload and remove of any individual
655
  // file, the entire group of file fields is updated together.
656
  if ($field['cardinality'] != 1) {
657
    $parents = array_slice($element['#array_parents'], 0, -1);
658
    $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
659
    $field_element = drupal_array_get_nested_value($form, $parents);
660
    $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
661
    foreach (element_children($element) as $key) {
662
      if (isset($element[$key]['#ajax'])) {
663
        $element[$key]['#ajax']['path'] = $new_path;
664
        $element[$key]['#ajax']['wrapper'] = $new_wrapper;
665
      }
666
    }
667
    unset($element['#prefix'], $element['#suffix']);
668
  }
669

    
670
  // Add another submit handler to the upload and remove buttons, to implement
671
  // functionality needed by the field widget. This submit handler, along with
672
  // the rebuild logic in file_field_widget_form() requires the entire field,
673
  // not just the individual item, to be valid.
674
  foreach (array('upload_button', 'remove_button') as $key) {
675
    $element[$key]['#submit'][] = 'file_field_widget_submit';
676
    $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
677
  }
678

    
679
  return $element;
680
}
681

    
682
/**
683
 * An element #process callback for a group of file_generic fields.
684
 *
685
 * Adds the weight field to each row so it can be ordered and adds a new Ajax
686
 * wrapper around the entire group so it can be replaced all at once.
687
 */
688
function file_field_widget_process_multiple($element, &$form_state, $form) {
689
  $element_children = element_children($element, TRUE);
690
  $count = count($element_children);
691

    
692
  foreach ($element_children as $delta => $key) {
693
    if ($key != $element['#file_upload_delta']) {
694
      $description = _file_field_get_description_from_element($element[$key]);
695
      $element[$key]['_weight'] = array(
696
        '#type' => 'weight',
697
        '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
698
        '#title_display' => 'invisible',
699
        '#delta' => $count,
700
        '#default_value' => $delta,
701
      );
702
    }
703
    else {
704
      // The title needs to be assigned to the upload field so that validation
705
      // errors include the correct widget label.
706
      $element[$key]['#title'] = $element['#title'];
707
      $element[$key]['_weight'] = array(
708
        '#type' => 'hidden',
709
        '#default_value' => $delta,
710
      );
711
    }
712
  }
713

    
714
  // Add a new wrapper around all the elements for Ajax replacement.
715
  $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
716
  $element['#suffix'] = '</div>';
717

    
718
  return $element;
719
}
720

    
721
/**
722
 * Retrieves the file description from a field field element.
723
 *
724
 * This helper function is used by file_field_widget_process_multiple().
725
 *
726
 * @param $element
727
 *   The element being processed.
728
 *
729
 * @return
730
 *   A description of the file suitable for use in the administrative interface.
731
 */
732
function _file_field_get_description_from_element($element) {
733
  // Use the actual file description, if it's available.
734
  if (!empty($element['#default_value']['description'])) {
735
    return $element['#default_value']['description'];
736
  }
737
  // Otherwise, fall back to the filename.
738
  if (!empty($element['#default_value']['filename'])) {
739
    return $element['#default_value']['filename'];
740
  }
741
  // This is probably a newly uploaded file; no description is available.
742
  return FALSE;
743
}
744

    
745
/**
746
 * Form submission handler for upload/remove button of file_field_widget_form().
747
 *
748
 * This runs in addition to and after file_managed_file_submit().
749
 *
750
 * @see file_managed_file_submit()
751
 * @see file_field_widget_form()
752
 * @see file_field_widget_process()
753
 */
754
function file_field_widget_submit($form, &$form_state) {
755
  // During the form rebuild, file_field_widget_form() will create field item
756
  // widget elements using re-indexed deltas, so clear out $form_state['input']
757
  // to avoid a mismatch between old and new deltas. The rebuilt elements will
758
  // have #default_value set appropriately for the current state of the field,
759
  // so nothing is lost in doing this.
760
  $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
761
  drupal_array_set_nested_value($form_state['input'], $parents, NULL);
762

    
763
  $button = $form_state['triggering_element'];
764

    
765
  // Go one level up in the form, to the widgets container.
766
  $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
767
  $field_name = $element['#field_name'];
768
  $langcode = $element['#language'];
769
  $parents = $element['#field_parents'];
770

    
771
  $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2));
772
  foreach ($submitted_values as $delta => $submitted_value) {
773
    if (!$submitted_value['fid']) {
774
      unset($submitted_values[$delta]);
775
    }
776
  }
777

    
778
  // Re-index deltas after removing empty items.
779
  $submitted_values = array_values($submitted_values);
780

    
781
  // Update form_state values.
782
  drupal_array_set_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2), $submitted_values);
783

    
784
  // Update items.
785
  $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
786
  $field_state['items'] = $submitted_values;
787
  field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
788
}
789

    
790
/**
791
 * Returns HTML for an individual file upload widget.
792
 *
793
 * @param $variables
794
 *   An associative array containing:
795
 *   - element: A render element representing the widget.
796
 *
797
 * @ingroup themeable
798
 */
799
function theme_file_widget($variables) {
800
  $element = $variables['element'];
801
  $output = '';
802

    
803
  // The "form-managed-file" class is required for proper Ajax functionality.
804
  $output .= '<div class="file-widget form-managed-file clearfix">';
805
  if ($element['fid']['#value'] != 0) {
806
    // Add the file size after the file name.
807
    $element['filename']['#markup'] .= ' <span class="file-size">(' . format_size($element['#file']->filesize) . ')</span> ';
808
  }
809
  $output .= drupal_render_children($element);
810
  $output .= '</div>';
811

    
812
  return $output;
813
}
814

    
815
/**
816
 * Returns HTML for a group of file upload widgets.
817
 *
818
 * @param $variables
819
 *   An associative array containing:
820
 *   - element: A render element representing the widgets.
821
 *
822
 * @ingroup themeable
823
 */
824
function theme_file_widget_multiple($variables) {
825
  $element = $variables['element'];
826

    
827
  // Special ID and classes for draggable tables.
828
  $weight_class = $element['#id'] . '-weight';
829
  $table_id = $element['#id'] . '-table';
830

    
831
  // Build up a table of applicable fields.
832
  $headers = array();
833
  $headers[] = t('File information');
834
  if ($element['#display_field']) {
835
    $headers[] = array(
836
      'data' => t('Display'),
837
      'class' => array('checkbox'),
838
    );
839
  }
840
  $headers[] = t('Weight');
841
  $headers[] = t('Operations');
842

    
843
  // Get our list of widgets in order (needed when the form comes back after
844
  // preview or failed validation).
845
  $widgets = array();
846
  foreach (element_children($element) as $key) {
847
    $widgets[] = &$element[$key];
848
  }
849
  usort($widgets, '_field_sort_items_value_helper');
850

    
851
  $rows = array();
852
  foreach ($widgets as $key => &$widget) {
853
    // Save the uploading row for last.
854
    if ($widget['#file'] == FALSE) {
855
      $widget['#title'] = $element['#file_upload_title'];
856
      $widget['#description'] = $element['#file_upload_description'];
857
      continue;
858
    }
859

    
860
    // Delay rendering of the buttons, so that they can be rendered later in the
861
    // "operations" column.
862
    $operations_elements = array();
863
    foreach (element_children($widget) as $sub_key) {
864
      if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
865
        hide($widget[$sub_key]);
866
        $operations_elements[] = &$widget[$sub_key];
867
      }
868
    }
869

    
870
    // Delay rendering of the "Display" option and the weight selector, so that
871
    // each can be rendered later in its own column.
872
    if ($element['#display_field']) {
873
      hide($widget['display']);
874
    }
875
    hide($widget['_weight']);
876

    
877
    // Render everything else together in a column, without the normal wrappers.
878
    $widget['#theme_wrappers'] = array();
879
    $information = drupal_render($widget);
880

    
881
    // Render the previously hidden elements, using render() instead of
882
    // drupal_render(), to undo the earlier hide().
883
    $operations = '';
884
    foreach ($operations_elements as $operation_element) {
885
      $operations .= render($operation_element);
886
    }
887
    $display = '';
888
    if ($element['#display_field']) {
889
      unset($widget['display']['#title']);
890
      $display = array(
891
        'data' => render($widget['display']),
892
        'class' => array('checkbox'),
893
      );
894
    }
895
    $widget['_weight']['#attributes']['class'] = array($weight_class);
896
    $weight = render($widget['_weight']);
897

    
898
    // Arrange the row with all of the rendered columns.
899
    $row = array();
900
    $row[] = $information;
901
    if ($element['#display_field']) {
902
      $row[] = $display;
903
    }
904
    $row[] = $weight;
905
    $row[] = $operations;
906
    $rows[] = array(
907
      'data' => $row,
908
      'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
909
    );
910
  }
911

    
912
  drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
913

    
914
  $output = '';
915
  $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
916
  $output .= drupal_render_children($element);
917
  return $output;
918
}
919

    
920

    
921
/**
922
 * Returns HTML for help text based on file upload validators.
923
 *
924
 * @param $variables
925
 *   An associative array containing:
926
 *   - description: The normal description for this field, specified by the
927
 *     user.
928
 *   - upload_validators: An array of upload validators as used in
929
 *     $element['#upload_validators'].
930
 *
931
 * @ingroup themeable
932
 */
933
function theme_file_upload_help($variables) {
934
  $description = $variables['description'];
935
  $upload_validators = $variables['upload_validators'];
936

    
937
  $descriptions = array();
938

    
939
  if (strlen($description)) {
940
    $descriptions[] = $description;
941
  }
942
  if (isset($upload_validators['file_validate_size'])) {
943
    $descriptions[] = t('Files must be less than !size.', array('!size' => '<strong>' . format_size($upload_validators['file_validate_size'][0]) . '</strong>'));
944
  }
945
  if (isset($upload_validators['file_validate_extensions'])) {
946
    $descriptions[] = t('Allowed file types: !extensions.', array('!extensions' => '<strong>' . check_plain($upload_validators['file_validate_extensions'][0]) . '</strong>'));
947
  }
948
  if (isset($upload_validators['file_validate_image_resolution'])) {
949
    $max = $upload_validators['file_validate_image_resolution'][0];
950
    $min = $upload_validators['file_validate_image_resolution'][1];
951
    if ($min && $max && $min == $max) {
952
      $descriptions[] = t('Images must be exactly !size pixels.', array('!size' => '<strong>' . $max . '</strong>'));
953
    }
954
    elseif ($min && $max) {
955
      $descriptions[] = t('Images must be between !min and !max pixels.', array('!min' => '<strong>' . $min . '</strong>', '!max' => '<strong>' . $max . '</strong>'));
956
    }
957
    elseif ($min) {
958
      $descriptions[] = t('Images must be larger than !min pixels.', array('!min' => '<strong>' . $min . '</strong>'));
959
    }
960
    elseif ($max) {
961
      $descriptions[] = t('Images must be smaller than !max pixels.', array('!max' => '<strong>' . $max . '</strong>'));
962
    }
963
  }
964

    
965
  return implode('<br />', $descriptions);
966
}
967

    
968
/**
969
 * Implements hook_field_formatter_view().
970
 */
971
function file_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
972
  $element = array();
973

    
974
  switch ($display['type']) {
975
    case 'file_default':
976
      foreach ($items as $delta => $item) {
977
        $element[$delta] = array(
978
          '#theme' => 'file_link',
979
          '#file' => (object) $item,
980
        );
981
      }
982
      break;
983

    
984
    case 'file_url_plain':
985
      foreach ($items as $delta => $item) {
986
        $element[$delta] = array('#markup' => empty($item['uri']) ? '' : file_create_url($item['uri']));
987
      }
988
      break;
989

    
990
    case 'file_table':
991
      if (!empty($items)) {
992
        // Display all values in a single element..
993
        $element[0] = array(
994
          '#theme' => 'file_formatter_table',
995
          '#items' => $items,
996
        );
997
      }
998
      break;
999
  }
1000

    
1001
  return $element;
1002
}
1003

    
1004
/**
1005
 * Returns HTML for a file attachments table.
1006
 *
1007
 * @param $variables
1008
 *   An associative array containing:
1009
 *   - items: An array of file attachments.
1010
 *
1011
 * @ingroup themeable
1012
 */
1013
function theme_file_formatter_table($variables) {
1014
  $header = array(t('Attachment'), t('Size'));
1015
  $rows = array();
1016
  foreach ($variables['items'] as $delta => $item) {
1017
    $rows[] = array(
1018
      theme('file_link', array('file' => (object) $item)),
1019
      format_size($item['filesize']),
1020
    );
1021
  }
1022

    
1023
  return empty($rows) ? '' : theme('table', array('header' => $header, 'rows' => $rows));
1024
}