#407410: Add variable for description field type.
[project/filefield.git] / filefield_widget.inc
CommitLineData
9412a7cc
JP
1<?php
2// $Id$
a679aaae 3
9412a7cc
JP
4/**
5 * @file
90a92972 6 * This file contains CCK widget related functionality.
9412a7cc
JP
7 *
8 * Uses content.module to store the fid and field specific metadata,
90a92972 9 * and Drupal's files table to store the actual file data.
9412a7cc
JP
10 */
11
22e736e6 12/**
87d3e491 13 * Implementation of CCK's hook_widget_settings($op == 'form').
22e736e6 14 */
22e736e6
DP
15function filefield_widget_settings_form($widget) {
16 $form = array();
17 $form['file_extensions'] = array(
18 '#type' => 'textfield',
19 '#title' => t('Permitted upload file extensions'),
20 '#default_value' => is_string($widget['file_extensions']) ? $widget['file_extensions'] : 'txt',
21 '#size' => 64,
22 '#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.'),
23 '#weight' => 1,
24 );
87d3e491 25
22e736e6
DP
26 $form['path_settings'] = array(
27 '#type' => 'fieldset',
28 '#title' => t('Path settings'),
e76d77f0
NH
29 '#collapsible' => TRUE,
30 '#collapsed' => TRUE,
22e736e6
DP
31 '#weight' => 6,
32 );
22e736e6
DP
33 $form['path_settings']['file_path'] = array(
34 '#type' => 'textfield',
35 '#title' => t('File path'),
36 '#default_value' => is_string($widget['file_path']) ? $widget['file_path'] : '',
af2b0845 37 '#description' => t('Optional subdirectory within the "%directory" directory where files will be stored. Do not include preceding or trailing slashes.', array('%directory' => variable_get('file_directory_path', 'files') . '/')),
22e736e6
DP
38 '#element_validate' => array('_filefield_widget_settings_file_path_validate'),
39 '#suffix' => theme('token_help', 'user'),
40 );
41
42 $form['max_filesize'] = array(
43 '#type' => 'fieldset',
44 '#title' => t('File size restrictions'),
45 '#description' => t('Limits for the size of files that a user can upload. Note that these settings only apply to newly uploaded files, whereas existing files are not affected.'),
46 '#weight' => 6,
47 '#collapsible' => TRUE,
48 '#collapsed' => TRUE,
49 );
50 $form['max_filesize']['max_filesize_per_file'] = array(
51 '#type' => 'textfield',
52 '#title' => t('Maximum upload size per file'),
53 '#default_value' => is_string($widget['max_filesize_per_file'])
54 ? $widget['max_filesize_per_file']
55 : '',
a2db4a45 56 '#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()))),
22e736e6
DP
57 '#element_validate' => array('_filefield_widget_settings_max_filesize_per_file_validate'),
58 );
59 $form['max_filesize']['max_filesize_per_node'] = array(
60 '#type' => 'textfield',
61 '#title' => t('Maximum upload size per node'),
62 '#default_value' => is_string($widget['max_filesize_per_node'])
63 ? $widget['max_filesize_per_node']
64 : '',
65 '#description' => t('Specify the total size limit for all files in field on a given node. Enter a value like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes) in order to restrict the total size of a node. Leave this empty if there should be no size restriction.'),
66 '#element_validate' => array('_filefield_widget_settings_max_filesize_per_node_validate'),
67 );
87d3e491 68
22e736e6
DP
69 return $form;
70}
71
87d3e491
NH
72/**
73 * Implementation of CCK's hook_widget_settings($op == 'save').
74 */
22e736e6 75function filefield_widget_settings_save($widget) {
95de22f1 76 return array('file_extensions', 'file_path', 'max_filesize_per_file', 'max_filesize_per_node');
22e736e6
DP
77}
78
79function _filefield_widget_settings_file_path_validate($element, &$form_state) {
80 // Strip slashes from the beginning and end of $widget['file_path']
81 $form_state['values']['file_path'] = trim($form_state['values']['file_path'], '\\/');
5322fc46
NH
82
83 // Do not allow the file path to be the same as the file_directory_path().
84 // This causes all sorts of problems with things like file_create_url().
85 if (strpos($form_state['values']['file_path'], file_directory_path()) === 0) {
86 form_error($element, t('The file path (@file_path) cannot start with the system files directory (@files_directory), as this may cause conflicts when building file URLs.', array('@file_path' => $form_state['values']['file_path'], '@files_directory' => file_directory_path())));
87 }
22e736e6
DP
88}
89
90function _filefield_widget_settings_max_filesize_per_file_validate($element, &$form_state) {
91 if (empty($form_state['values']['max_filesize_per_file'])) {
92 return; // Empty means no size restrictions, so don't throw an error.
93 }
87d3e491
NH
94 elseif (!is_numeric(parse_size($form_state['values']['max_filesize_per_file']))) {
95 form_error($element, t('The "@field" option must contain a valid value. You can either leave the text field empty or enter a string like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes).', array('@field' => t('Maximum upload size per file'))));
22e736e6
DP
96 }
97}
98
99function _filefield_widget_settings_max_filesize_per_node_validate($element, &$form_state) {
100 if (empty($form_state['values']['max_filesize_per_node'])) {
101 return; // Empty means no size restrictions, so don't throw an error.
102 }
87d3e491
NH
103 elseif (!is_numeric(parse_size($form_state['values']['max_filesize_per_node']))) {
104 form_error($element, t('The "@field" option must contain a valid value. You can either leave the text field empty or enter a string like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes).', array('@field' => t('Maximum upload size per node'))));
22e736e6
DP
105 }
106}
107
bf602673 108/**
109 * Determine the widget's files directory
110 *
87d3e491
NH
111 * @param $field
112 * A CCK field array.
25f2014e
NH
113 * @param $account
114 * The user account object to calculate the file path for.
87d3e491
NH
115 * @return
116 * The files directory path, with any tokens replaced.
bf602673 117 */
25f2014e
NH
118function filefield_widget_file_path($field, $account = NULL) {
119 $account = isset($account) ? $account : $GLOBALS['user'];
120 $dest = $field['widget']['file_path'];
14907c69
NH
121 // Replace user level tokens.
122 // Node level tokens require a lot of complexity like temporary storage
123 // locations when values don't exist. See the filefield_paths module.
bf602673 124 if (module_exists('token')) {
25f2014e 125 $dest = token_replace($dest, 'user', $account);
bf602673 126 }
14907c69
NH
127 // Replace nasty characters in the path if possible.
128 if (module_exists('transliteration')) {
f21581e9 129 module_load_include('inc', 'transliteration');
14907c69
NH
130 $dest_array = array_filter(explode('/', $dest));
131 foreach ($dest_array as $key => $directory) {
132 $dest_array[$key] = transliteration_clean_filename($directory);
133 }
134 $dest = implode('/', $dest_array);
135 }
bf602673 136
137 return file_directory_path() .'/'. $dest;
138}
139
87d3e491
NH
140/**
141 * Given a FAPI element, save any files that may have been uploaded into it.
142 *
143 * This function should only be called during validate, submit, or
144 * value_callback functions.
145 *
146 * @param $element
147 * The FAPI element whose values are being saved.
148 */
8ffd8e3d
DP
149function filefield_save_upload($element) {
150 $upload_name = $element['#field_name'] .'_'. $element['#delta'];
25f2014e 151 $field = content_fields($element['#field_name'], $element['#type_name']);
8ffd8e3d
DP
152
153 if (empty($_FILES['files']['name'][$upload_name])) {
154 return 0;
155 }
156
25f2014e 157 $dest = filefield_widget_file_path($field);
8ffd8e3d 158 if (!field_file_check_directory($dest, FILE_CREATE_DIRECTORY)) {
32984cd2 159 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']));
8ffd8e3d
DP
160 form_set_error($upload_name, t('The file could not be uploaded.'));
161 return 0;
162 }
163
164 if (!$file = field_file_save_upload($upload_name, $element['#upload_validators'], $dest)) {
32984cd2 165 watchdog('filefield', 'The file upload failed. %upload', array('%upload' => $upload_name));
29194778 166 form_set_error($upload_name, t('The file in the @field field was unable to be uploaded.', array('@field' => $element['#title'])));
8ffd8e3d
DP
167 return 0;
168 }
169 return $file['fid'];
170}
171
22e736e6 172/**
87d3e491 173 * The #value_callback for the filefield_widget type element.
22e736e6 174 */
d2f124d5 175function filefield_widget_value($element, $edit = FALSE) {
7e784adc 176 if (!$edit) {
692a0c14 177 $file = field_file_load($element['#default_value']['fid']);
d2f124d5
DP
178 $item = $element['#default_value'];
179 }
180 else {
9811a70a 181 $item = array_merge($element['#default_value'], $edit);
692a0c14
NH
182 $field = content_fields($element['#field_name'], $element['#type_name']);
183
184 // Uploads take priority over value of fid text field.
8ffd8e3d 185 if ($fid = filefield_save_upload($element)) {
9811a70a 186 $item['fid'] = $fid;
7e784adc 187 }
01c15c1d
NH
188 // Check for #filefield_value_callback values.
189 // Because FAPI does not allow multiple #value_callback values like it does
190 // for #element_validate and #process, this fills the missing functionality
191 // to allow FileField to be extended purely through FAPI.
192 elseif (isset($element['#filefield_value_callback'])) {
193 foreach ($element['#filefield_value_callback'] as $callback) {
194 $callback($element, $item);
195 }
196 }
5322fc46 197
692a0c14 198 // Load file if the FID has changed so that it can be saved by CCK.
9811a70a 199 $file = field_file_load($item['fid']);
692a0c14 200
3ecf9d5c
NH
201 // If the file entry doesn't exist, don't save anything.
202 if (empty($file)) {
203 $item = array();
204 }
205
692a0c14
NH
206 // Checkboxes loose their value when empty.
207 // If the list field is present make sure its unchecked value is saved.
208 if ($field['list_field'] && empty($edit['list'])) {
209 $item['list'] = 0;
210 }
d2f124d5 211 }
692a0c14 212 // Merge file and item data so it is available to all widgets.
6e7782b4 213 $item = array_merge($item, $file);
1bffec9e 214
6e7782b4 215 return $item;
d2f124d5
DP
216}
217
87d3e491
NH
218/**
219 * An element #process callback for the filefield_widget field type.
220 *
221 * Expands the filefield_widget type to include the upload field, upload and
222 * remove buttons, and the description field.
223 */
d2f124d5 224function filefield_widget_process($element, $edit, &$form_state, $form) {
9c0b75b0 225 // The widget is being presented, so apply the JavaScript.
226 drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js');
7e784adc 227
8ffd8e3d
DP
228 $item = $element['#value'];
229 $field_name = $element['#field_name'];
230 $delta = $element['#delta'];
3897ed27 231 $element['#theme'] = 'filefield_widget_item';
8ffd8e3d 232
ad6c5ff0
DP
233 $field = content_fields($element['#field_name'], $element['#type_name']);
234
3897ed27
NH
235 // Title is not necessary for each individual field.
236 if ($field['multiple'] > 0) {
237 unset($element['#title']);
238 }
239
4b58ccde
NH
240 // Set up the buttons first since we need to check if they were clicked.
241 $element['filefield_upload'] = array(
242 '#type' => 'submit',
243 '#value' => t('Upload'),
244 '#process' => array('form_expand_ahah'),
245 '#submit' => array('node_form_submit_build_node'),
246 '#ahah' => array( // with JavaScript
247 'path' => 'filefield/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'],
248 'wrapper' => $element['#id'] .'-ahah-wrapper',
249 'method' => 'replace',
250 'effect' => 'fade',
251 ),
252 '#field_name' => $element['#field_name'],
253 '#delta' => $element['#delta'],
254 '#type_name' => $element['#type_name'],
255 '#upload_validators' => $element['#upload_validators'],
256 '#weight' => 100,
257 '#post' => $element['#post'],
258 );
259 $element['filefield_remove'] = array(
260 '#name' => $element['#field_name'] .'_'. $element['#delta'] .'_filefield_remove',
261 '#type' => 'submit',
262 '#value' => t('Remove'),
263 '#process' => array('form_expand_ahah'),
264 '#submit' => array('node_form_submit_build_node'),
265 '#ahah' => array( // with JavaScript
266 'path' => 'filefield/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'],
267 'wrapper' => $element['#id'] .'-ahah-wrapper',
268 'method' => 'replace',
269 'effect' => 'fade',
270 ),
271 '#field_name' => $element['#field_name'],
272 '#delta' => $element['#delta'],
273 '#weight' => 101,
274 '#post' => $element['#post'],
275 );
276
277 // Because the output of this field changes depending on the button clicked,
278 // we need to ask FAPI immediately if the remove button was clicked.
279 // It's not good that we call this private function, but
280 // $form_state['clicked_button'] is only available after this #process
281 // callback is finished.
282 if (_form_button_was_clicked($element['filefield_remove'])) {
319a4dc5
NH
283 // Delete the file if it is currently unused. Note that field_file_delete()
284 // does a reference check in addition to our basic status check.
285 if (isset($edit['fid'])) {
286 $removed_file = field_file_load($edit['fid']);
287 if ($removed_file['status'] == 0) {
288 field_file_delete($removed_file);
289 }
290 }
3a35dc81 291 $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => ''));
8ffd8e3d 292 }
7e784adc 293
4b58ccde
NH
294 // Set access on the buttons.
295 $element['filefield_upload']['#access'] = empty($item['fid']);
296 $element['filefield_remove']['#access'] = !empty($item['fid']);
297
e3623331
NH
298 // Add progress bar support to the upload if possible.
299 if ($implementation = filefield_progress_implementation()) {
300 $upload_progress_key = md5(mt_rand());
301
302 if ($implementation == 'uploadprogress') {
303 $element['UPLOAD_IDENTIFIER'] = array(
304 '#type' => 'hidden',
305 '#value' => $upload_progress_key,
306 '#attributes' => array('class' => 'filefield-progress'),
307 );
308 }
309 elseif ($implementation == 'apc') {
310 $element['APC_UPLOAD_PROGRESS'] = array(
311 '#type' => 'hidden',
312 '#value' => $upload_progress_key,
313 '#attributes' => array('class' => 'filefield-progress'),
314 );
315 }
316
317 // Add the upload progress callback.
318 $element['filefield_upload']['#ahah']['progress']['type'] = 'bar';
319 $element['filefield_upload']['#ahah']['progress']['path'] = 'filefield/progress/' . $upload_progress_key;
320 }
321
123172a6
NH
322 // Set the FID.
323 $element['fid'] = array(
324 '#type' => 'hidden',
325 '#value' => $item['fid'],
326 );
d2f124d5 327
6e7782b4 328 if ($item['fid'] != 0) {
e49d0f07
NH
329 $element['preview'] = array(
330 '#type' => 'markup',
331 '#value' => theme('filefield_widget_preview', $item),
332 );
d2f124d5 333 }
28b1c58e
DP
334
335 // placeholder.. will be serialized into the data column. this is a place for widgets
336 // to put additional data.
a679aaae
NH
337 $element['data'] = array(
338 '#tree' => 'true',
339 '#access' => !empty($item['fid']),
340 );
2635f5af 341
35873ed9 342 if ($field['description_field']) {
2635f5af 343 $element['data']['description'] = array(
344 '#type' => 'textfield',
345 '#title' => t('Description'),
346 '#value' => isset($item['data']['description']) ? $item['data']['description'] : '',
994c57b0 347 '#type' => variable_get('filefield_description_type', 'textfield'),
f56c699b 348 '#maxlength' => variable_get('filefield_description_length', 128),
2635f5af 349 );
350 }
28b1c58e 351
35873ed9 352 if ($field['list_field']) {
28b1c58e
DP
353 $element['list'] = array(
354 '#type' => 'checkbox',
355 '#title' => t('List'),
8ffd8e3d 356 '#value' => isset($item['list']) ? $item['list'] : $field['list_default'],
28b1c58e 357 '#attributes' => array('class' => 'filefield-list'),
3897ed27 358 '#access' => !empty($item['fid']),
28b1c58e
DP
359 );
360 }
35873ed9
NH
361 else {
362 $element['list'] = array(
363 '#type' => 'hidden',
364 '#value' => '1',
365 );
366 }
d2f124d5 367
8ffd8e3d
DP
368 foreach ($element['#upload_validators'] as $callback => $arguments) {
369 $help_func = $callback .'_help';
ad6bb31f
DP
370 if (function_exists($help_func)) {
371 $desc[] = call_user_func_array($help_func, $arguments);
372 }
8ffd8e3d
DP
373 }
374 $element['upload'] = array(
375 '#name' => 'files['. $element['#field_name'] .'_'. $element['#delta'] .']',
376 '#type' => 'file',
8ffd8e3d 377 '#description' => implode('<br />', $desc),
3897ed27 378 '#size' => 22,
8ffd8e3d 379 '#attributes' => array(
f976317f 380 'accept' => implode(',', array_filter(explode(' ', $field['widget']['file_extensions']))),
3897ed27
NH
381 ),
382 '#access' => empty($item['fid']),
8ffd8e3d
DP
383 );
384
f613b75e 385 $element['#attributes']['id'] = $element['#id'] .'-ahah-wrapper';
f613b75e 386 $element['#prefix'] = '<div '. drupal_attributes($element['#attributes']) .'>';
aa3a8ca0 387 $element['#suffix'] = '</div>';
d2f124d5 388
d2f124d5
DP
389 return $element;
390}
391
87d3e491
NH
392/**
393 * An #element_validate callback for the filefield_widget field.
394 */
123172a6
NH
395function filefield_widget_validate(&$element, &$form_state) {
396 // If referencing an existing file, only allow if there are existing
397 // references. This prevents unmanaged files (outside of FileField) from
398 // being deleted if this node were to be deleted.
399 if (!empty($element['fid']['#value'])) {
400 $field = content_fields($element['#field_name'], $element['#type_name']);
401 if ($file = field_file_load($element['fid']['#value'])) {
402 $file = (object) $file;
ab6a2015
NH
403 if ($file->status == FILE_STATUS_PERMANENT) {
404 // TODO: We could use field_file_references() here to reference any file
405 // but hook_file_delete() needs to be implemented first.
406 $references = module_invoke('filefield', 'file_references', $file);
407 if ($references['filefield'] == 0) {
408 form_error($element, t('Referencing to the file used the %field field is not allowed.', array('%field' => $element['#title'])));
409 }
123172a6
NH
410 }
411 }
412 else {
413 form_error($element, t('The file referenced by the %field field does not exist.', array('%field' => $element['#title'])));
414 }
415 }
d2f124d5
DP
416}
417
d2f124d5
DP
418/**
419 * FormAPI theme function. Theme the output of an image field.
420 */
bad2b17f 421function theme_filefield_widget($element) {
c6e8be79 422 return theme('form_element', $element, $element['#children']);
d2f124d5
DP
423}
424
22e736e6 425function theme_filefield_widget_preview($item) {
96f6f7a9
NH
426 // Remove the current description so that we get the filename as the link.
427 if (isset($item['data']['description'])) {
428 unset($item['data']['description']);
429 }
430
839679c0
NH
431 return '<div class="filefield-file-info">'.
432 '<div class="filename">'. theme('filefield_file', $item) .'</div>'.
433 '<div class="filesize">'. format_size($item['filesize']) .'</div>'.
434 '<div class="filemime">'. $item['filemime'] .'</div>'.
435 '</div>';
22e736e6
DP
436}
437
438function theme_filefield_widget_item($element) {
3897ed27 439 // Put the upload button directly after the upload field.
ad98c19d 440 $element['upload']['#field_suffix'] = drupal_render($element['filefield_upload']);
3897ed27
NH
441 $element['upload']['#theme'] = 'filefield_widget_file';
442
443 $output = '';
444 $output .= '<div class="filefield-element clear-block">';
445
446 if ($element['fid']['#value'] != 0) {
839679c0 447 $output .= '<div class="widget-preview">';
3897ed27
NH
448 $output .= drupal_render($element['preview']);
449 $output .= '</div>';
450 }
451
839679c0 452 $output .= '<div class="widget-edit">';
3897ed27
NH
453 $output .= drupal_render($element);
454 $output .= '</div>';
455 $output .= '</div>';
456
457 return $output;
458}
459
460/**
ab322a18
NH
461 * Custom theme function for FileField upload elements.
462 *
463 * This function allows us to put the "Upload" button immediately after the
464 * file upload field by respecting the #field_suffix property.
3897ed27
NH
465 */
466function theme_filefield_widget_file($element) {
467 $output = '';
468
469 $output .= '<div class="filefield-upload clear-block">';
470
471 if (isset($element['#field_prefix'])) {
472 $output .= $element['#field_prefix'];
473 }
474
475 _form_set_class($element, array('form-file'));
476 $output .= '<input type="file" name="'. $element['#name'] .'"'. ($element['#attributes'] ? ' '. drupal_attributes($element['#attributes']) : '') .' id="'. $element['#id'] .'" size="'. $element['#size'] ."\" />\n";
477
478 if (isset($element['#field_suffix'])) {
479 $output .= $element['#field_suffix'];
480 }
481
482 $output .= '</div>';
483
484 return theme('form_element', $element, $output);
bad2b17f 485}
d2f124d5 486
2be7e4b0 487/**
87d3e491
NH
488 * Additional #validate handler for the node form.
489 *
490 * This function checks the #required properties on file fields and calculates
491 * node upload totals for all file fields. The #required property is not
492 * properly supported on file fields by Drupal core, so we do this manually.
2be7e4b0 493 */
2be7e4b0 494function filefield_node_form_validate($form, &$form_state) {
2be7e4b0
DP
495 $type = content_types($form['type']['#value']);
496 foreach ($type['fields'] as $field_name => $field) {
b6644f47 497 if (!(in_array($field['module'], array('imagefield', 'filefield')))) continue;
2be7e4b0 498 $empty = $field['module'] .'_content_is_empty';
e76d77f0 499 $valid = FALSE;
f92ea317 500 $total_filesize = 0;
9b86f834 501 if (!empty($form_state['values'][$field_name])) {
e76d77f0 502 foreach ($form_state['values'][$field_name] as $delta => $item) {
9b86f834
NH
503 if ($empty($item, $field)) continue;
504 $valid = TRUE;
505 $total_filesize += (int)$item['filesize'];
506 }
2be7e4b0 507 }
9412a7cc 508
b6644f47 509 if (!$valid && $field['required']) {
f92ea317
DP
510 form_set_error($field_name, t('%title field is required.', array('%title' => $field['widget']['label'])));
511 }
512 $max_filesize = parse_size($field['widget']['max_filesize_per_node']);
5af49125 513 if ($max_filesize && $total_filesize > $max_filesize) {
7e784adc 514 form_set_error($field_name, t('Total filesize for %title, %tsize, exceeds field settings of %msize.',
f92ea317 515 array(
7e784adc 516 '%title' => $field['widget']['label'],
517 '%tsize' => format_size($total_filesize),
f92ea317
DP
518 '%msize' => format_size($max_filesize)
519 )
520 ));
521 }
2be7e4b0
DP
522 }
523}