#286834 by eojthebrave and drewish,
[project/filefield.git] / filefield.module
CommitLineData
d2f124d5
DP
1<?php // $Id$
2
d93afe47
DP
3/**
4 * @file
0da420f9 5 * FileField: Defines a CCK file field type.
6413f0f6 6 *
945e9f37
JP
7 * Uses content.module to store the fid and field specific metadata,
8 * and Drupal's {files} table to store the actual file data.
d93afe47
DP
9 */
10
d2f124d5
DP
11/**
12 * Implementation of hook_init().
13 */
db33b245 14function filefield_init() {
6f0f10d4
DP
15 // field hooks and callbacks.
16 module_load_include('inc', 'filefield', 'filefield_field');
db33b245 17 // widget hooks and callbacks.
d2f124d5 18 module_load_include('inc', 'filefield', 'filefield_widget');
d2f124d5
DP
19 // file hooks and callbacks.
20 module_load_include('inc', 'filefield', 'filefield_file');
6f0f10d4 21 module_load_include('inc', 'filefield', 'field_file');
6c352642
DP
22
23 drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js');
bad2b17f 24 drupal_add_css(drupal_get_path('module', 'filefield') .'/filefield.css');
d2f124d5 25}
9412a7cc 26
382b6b6f
JP
27/**
28 * Implementation of hook_menu().
29 */
30function filefield_menu() {
d93afe47
DP
31 $items = array();
32
c985d8bc 33 $items['filefield/js/upload/%/%/%'] = array(
382b6b6f 34 'page callback' => 'filefield_js',
6c352642 35 'page arguments' => array(3, 4, 5, '_filefield_file_upload'),
f1113abd 36 'access callback' => 'filefield_edit_access',
d4b75f37 37 'access arguments' => array(3),
c985d8bc 38 'type' => MENU_CALLBACK,
db33b245 39 'file' => 'filefield_widget.inc',
c985d8bc
JP
40 );
41 $items['filefield/js/delete/%/%/%'] = array(
42 'page callback' => 'filefield_js',
6c352642 43 'page arguments' => array(3, 4, 5, '_filefield_file_delete'),
f1113abd 44 'access callback' => 'filefield_edit_access',
d4b75f37 45 'access arguments' => array(3),
382b6b6f 46 'type' => MENU_CALLBACK,
db33b245 47 'file' => 'filefield_widget.inc',
382b6b6f 48 );
d93afe47
DP
49 return $items;
50}
51
52/**
0c08790f 53 * Implementation of hook_elements().
22e736e6 54 * @todo: autogenerate element registry entries for widgets.
0c08790f
JP
55 */
56function filefield_elements() {
57 $elements = array();
d2f124d5 58 $elements['filefield_widget'] = array(
0c08790f 59 '#input' => TRUE,
28b1c58e 60 '#columns' => array('fid', 'list', 'data'),
d2f124d5 61 '#process' => array('filefield_widget_process'),
d2f124d5 62 '#value_callback' => 'filefield_widget_value',
ecbb2c26 63 '#element_validate' => array('filefield_widget_validate'),
d2f124d5 64 '#description' => t('Changes made to the attachments are not permanent until you save this post.'),
0c08790f 65 );
d2f124d5 66 $elements['filefield_extensible'] = array(
0c08790f 67 '#input' => TRUE,
28b1c58e 68 '#columns' => array('fid', 'list', 'data'),
6e7782b4
DP
69 '#process' => array('filefield_process'),
70 '#value_callback' => 'filefield_value',
d2f124d5 71 '#description' => t('Changes made to the attachments are not permanent until you save this post.'),
6db07116 72 );
0c08790f
JP
73 return $elements;
74}
75
76/**
382b6b6f 77 * Implementation of hook_theme().
22e736e6 78 * @todo: autogenerate theme registry entrys for widgets.
382b6b6f
JP
79 */
80function filefield_theme() {
81 return array(
c147cceb
DP
82 'filefield_file' => array(
83 'arguments' => array('file' => NULL),
84 'file' => 'filefield_formatter.inc',
85 ),
86 'filefield_icon' => array(
87 'arguments' => array('file' => NULL),
88 'file' => 'filefield.theme.inc',
89 ),
d2f124d5 90 'filefield_widget' => array(
5983a6bc 91 'arguments' => array('element' => NULL),
d2f124d5 92 'file' => 'filefield_widget.inc',
5983a6bc 93 ),
c4c7e990 94 'filefield_widget_item' => array(
bad2b17f
DP
95 'arguments' => array('element' => NULL),
96 'file' => 'filefield_widget.inc',
97 ),
c4c7e990 98 'filefield_widget_preview' => array(
8a154cf2 99 'arguments' => array('element' => NULL),
db33b245 100 'file' => 'filefield_widget.inc',
8a154cf2 101 ),
c4c7e990
DP
102
103
104 'filefield_formatter_filefield_default' => array(
382b6b6f 105 'arguments' => array('element' => NULL),
c147cceb 106 'file' => 'filefield_formatter.inc',
382b6b6f 107 ),
c4c7e990 108 'filefield_item' => array(
a1f1332d 109 'arguments' => array('file' => NULL, 'field' => NULL),
c147cceb 110 'file' => 'filefield_formatter.inc',
b85662ff 111 ),
c4c7e990
DP
112 'filefield_file' => array(
113 'arguments' => array('file' => NULL),
c147cceb 114 'file' => 'filefield_formatter.inc',
382b6b6f 115 ),
c4c7e990
DP
116
117 );
cdf9e275
JP
118}
119
120/**
f1113abd
JP
121 * Implementation of hook_file_download(). Yes, *that* hook that causes
122 * any attempt for file upload module interoperability to fail spectacularly.
123 */
cdf9e275
JP
124function filefield_file_download($file) {
125 $file = file_create_path($file);
8a29f595 126
cdf9e275
JP
127 $result = db_query("SELECT * FROM {files} WHERE filepath = '%s'", $file);
128 if (!$file = db_fetch_object($result)) {
129 // We don't really care about this file.
130 return;
131 }
8a29f595 132
f1113abd
JP
133 // Find out if any filefield contains this file, and if so, which field
134 // and node it belongs to. Required for later access checking.
135 $cck_files = array();
136 foreach (content_fields() as $field) {
137 if ($field['type'] == 'file') {
138 $db_info = content_database_info($field);
139 $table = $db_info['table'];
140 $fid_column = $db_info['columns']['fid']['column'];
141
142 $columns = array('vid', 'nid');
143 foreach ($db_info['columns'] as $property_name => $column_info) {
144 $columns[] = $column_info['column'] .' AS '. $property_name;
145 }
146 $result = db_query("SELECT ". implode(', ', $columns) ."
147 FROM {". $table ."}
148 WHERE ". $fid_column ." = %d", $file->fid);
149
150 while ($content = db_fetch_array($result)) {
151 $content['field'] = $field;
152 $cck_files[$field['field_name']][$content['vid']] = $content;
153 }
154 }
382b6b6f 155 }
f1113abd
JP
156 // If no filefield item is involved with this file, we don't care about it.
157 if (empty($cck_files)) {
158 return;
159 }
160
2de8fe0e
JP
161 // If any node includes this file but the user may not view this field,
162 // then deny the download.
163 foreach ($cck_files as $field_name => $field_files) {
164 if (!filefield_view_access($field_name)) {
165 return -1;
f1113abd
JP
166 }
167 }
168
169 // So the overall field view permissions are not denied, but if access is
170 // denied for a specific node containing the file, deny the download as well.
171 // It's probably a little too restrictive, but I can't think of a
172 // better way at the moment. Input appreciated.
173 // (And yeah, node access checks also include checking for 'access content'.)
174 $nodes = array();
175 foreach ($cck_files as $field_name => $field_files) {
176 foreach ($field_files as $revision_id => $content) {
177 // Checking separately for each revision is probably not the best idea -
178 // what if 'view revisions' is disabled? So, let's just check for the
179 // current revision of that node.
180 if (isset($nodes[$content['nid']])) {
181 continue; // don't check the same node twice
182 }
183 $node = node_load($content['nid']);
184 if (!node_access('view', $node)) {
185 // You don't have permission to view the node this file is attached to.
186 return -1;
187 }
188 $nodes[$content['nid']] = $node;
189 }
59b7732d 190 }
cdf9e275
JP
191
192 // Well I guess you can see this file.
193 $name = mime_header_encode($file->filename);
194 $type = mime_header_encode($file->filemime);
195 // Serve images and text inline for the browser to display rather than download.
196 $disposition = ereg('^(text/|image/)', $file->filemime) ? 'inline' : 'attachment';
197 return array(
198 'Content-Type: '. $type .'; name='. $name,
199 'Content-Length: '. $file->filesize,
200 'Content-Disposition: '. $disposition .'; filename='. $name,
201 'Cache-Control: private',
202 );
59b7732d
DP
203}
204
c985d8bc 205
45f2943f 206/**
d2f124d5
DP
207 * Implementation of CCK's hook_field_info().
208 */
209function filefield_field_info() {
210 return array(
211 'filefield' => array(
212 'label' => 'File',
213 'description' => t('Store an arbitrary file.'),
214 ),
215 );
216}
217
218/**
219 * Implementation of hook_field_settings().
220 */
221function filefield_field_settings($op, $field) {
b4620340
DP
222 $return = array();
223
22e736e6 224 module_load_include('inc','filefield','filefield_field');
d2f124d5
DP
225 $op = str_replace(' ', '_', $op);
226 // add filefield specific handlers...
227 $function = 'filefield_field_settings_'. $op;
228 if (function_exists($function)) {
b4620340 229 $return = $function($field);
d2f124d5 230 }
b4620340 231
b4620340
DP
232 // dynamically load widgets file and callbacks.
233 module_load_include('inc', $field['module'], $field['type'] .'_field');
fc38be5c 234 $function = $field['module'] .'_'. $field['type'] .'_field_settings_'. $op;
b4620340 235 if (function_exists($function)) {
fc38be5c 236 $return = array_merge($return, $function($field));
b4620340
DP
237 }
238
239 return $return;
240
d2f124d5
DP
241}
242
243/**
244 * Implementtation of CCK's hook_field().
45f2943f 245 */
d2f124d5
DP
246function filefield_field($op, $node, $field, &$items, $teaser, $page) {
247 module_load_include('inc','filefield','filefield_field');
248 $op = str_replace(' ', '_', $op);
249 // add filefield specific handlers...
250 $function = 'filefield_field_'. $op;
251 if (function_exists($function)) {
252 return $function($node, $field, $items, $teaser, $page);
cdf9e275
JP
253 }
254}
255
c985d8bc 256/**
d2f124d5 257 * Implementation of CCK's hook_widget_settings().
c985d8bc 258 */
d2f124d5 259function filefield_widget_settings($op, $widget) {
29d050ce
DP
260 $return = array();
261 // load our widget settings callbacks..
22e736e6 262 module_load_include('inc','filefield','filefield_widget');
29d050ce
DP
263 $op = str_replace(' ', '_', $op);
264 $function = 'filefield_widget_settings_'. $op;
265 if (function_exists($function)) {
266 $return = $function($widget);
267 }
268
269 // sometimes widget_settings is called with widget, sometimes with field.
270 // CCK needs to make up it's mind here or get with the new hook formats.
271 $widget_type = isset($widget['widget_type']) ? $widget['widget_type'] : $widget['type'];
22e736e6 272 $widget_module = isset($widget['widget_module']) ? $widget['widget_module'] : $widget['module'];
29d050ce
DP
273
274 // dynamically load widgets file and callbacks.
6f0f10d4 275 module_load_include('inc', $widget_module, $widget_module .'_widget');
29d050ce
DP
276
277 $function = $widget_type .'_widget_settings_'. $op;
d2f124d5 278 if (function_exists($function)) {
29d050ce
DP
279 $return = array_merge($return, $function($widget));
280 }
29d050ce
DP
281
282 return $return;
cdf9e275 283}
d2f124d5
DP
284
285/**
286 * Implementation of hook_widget().
287 */
288function filefield_widget(&$form, &$form_state, $field, $items, $delta = 0) {
2be7e4b0
DP
289 // CCK doesn't give a validate callback at the field level...
290 // and FAPI's #require is naieve to complex structures...
291 // we validate at the field level ourselves.
292 if (!in_array('filefield_node_form_validate', $form['#validate'])) {
293 $form['#validate'][] = 'filefield_node_form_validate';
294 }
295
28b1c58e 296 $default = array('fid' => 0, 'list' => 0, 'data' => array('description' => ''));
4ed56fae 297 // assign defaults..
1bffec9e 298 if (empty($items[$delta])) $items[$delta] = $default;
4ed56fae 299
6f0f10d4
DP
300 module_load_include('inc', 'filefield', 'field_widget');
301
d2f124d5 302 $form['#attributes'] = array('enctype' => 'multipart/form-data');
d6caa409
DP
303 $max_filesize = file_upload_max_size();
304 if (!empty($field['widget']['max_filesize_per_file'])
305 && $field['widget']['max_filesize_per_file'] < $max_filesize) {
306 $max_filesize = $field['widget']['max_filesize_per_file'];
307 }
308
d2f124d5 309 $element = array(
6e7782b4 310 '#title' => $field['widget']['label'],
d2f124d5 311 '#type' => $field['widget']['type'],
4ed56fae 312 '#default_value' => array_merge($default, $items[$delta]),
d6caa409
DP
313 '#upload_validators' => array(
314 'file_validate_size' => array($max_filesize),
315 // override core since it excludes uid 1 on this.. I only want to
316 // excuse uid 1 of quota requirements.
317 'filefield_validate_extensions' => array($field['widget']['file_extensions']),
318 ),
d2f124d5 319 );
6f0f10d4 320 module_load_include('inc', $field['widget']['module'], $field['widget']['module'] .'_widget');
d2f124d5
DP
321 return $element;
322}
323
324/**
325 * Implementation of CCK's hook_content_is_empty().
326 *
327 * The result of this determines whether content.module will save
328 * the value of the field.
329 */
330function filefield_content_is_empty($item, $field) {
6f0f10d4 331 return empty($item['fid']) || (int)$item['fid'] == 0;
d2f124d5
DP
332}
333
d2f124d5
DP
334/**
335 * Implementation of CCK's hook_widget_info().
336 */
337function filefield_widget_info() {
338 return array(
339 'filefield_widget' => array(
340 'label' => t('File Upload'),
57d2397f 341 'field types' => array('filefield'),
d2f124d5
DP
342 'multiple values' => CONTENT_HANDLE_CORE,
343 'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM),
344 'description' => t('A plain file upload widget.'),
345 ),
346 'filefield_combo' => array(
347 'label' => 'Extensible File',
57d2397f 348 'field types' => array('filefield'),
d2f124d5
DP
349 'multiple values' => CONTENT_HANDLE_CORE,
350 'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM),
351 'description' => t('(Experimental)An extensible file upload widget.'),
352 ),
353 );
354}
355
356/**
357 * Implementation of CCK's hook_field_formatter_info().
358 */
359function filefield_field_formatter_info() {
360 return array(
361 'filefield_default' => array(
362 'label' => t('Generic files'),
363 'suitability callback' => TRUE,
c147cceb 364 'field types' => array('filefield','image'),
d2f124d5
DP
365 'multiple values' => CONTENT_HANDLE_CORE,
366 'description' => t('Displays all kinds of files with an icon and a linked file description.'),
367 ),
368 'filefield_dynamic' => array(
369 'label' => t('Dynamic file formatters'),
370 'suitability callback' => TRUE,
371 'field types' => array('file'),
372 'multiple values' => CONTENT_HANDLE_CORE,
373 'description' => t('(experimental) An extensible formatter for filefield.'),
374 ),
375 );
376}
377
d2f124d5
DP
378/**
379 * Determine the most appropriate icon for the given file's mimetype.
380 *
381 * @return The URL of the icon image file, or FALSE if no icon could be found.
382 */
d6caa409 383
d2f124d5
DP
384function filefield_icon_url($file) {
385 include_once(drupal_get_path('module', 'filefield') .'/filefield.theme.inc');
386 return _filefield_icon_url($file);
387}
388
d2f124d5
DP
389/**
390 * Access callback for the JavaScript upload and deletion AHAH callbacks.
391 * The content_permissions module provides nice fine-grained permissions for
392 * us to check, so we can make sure that the user may actually edit the file.
393 */
394function filefield_edit_access($field_name) {
395 if (module_exists('content_permissions')) {
396 return user_access('edit '. $field_name);
397 }
398 // No content permissions to check, so let's fall back to a more general permission.
399 return user_access('access content');
400}
401
402/**
403 * Access callback that checks if the current user may view the filefield.
404 */
405function filefield_view_access($field_name) {
406 if (module_exists('content_permissions')) {
407 return user_access('view '. $field_name);
408 }
409 // No content permissions to check, so let's fall back to a more general permission.
410 return user_access('access content');
411}
412
22e736e6
DP
413/**
414 * Shared AHAH callback for uploads and deletions. It just differs in a few
415 * unimportant details (what happens to the file, and which form is used as
416 * a replacement) so these details are taken care of by a form callback.
417 */
418function filefield_js($field_name, $type_name, $delta, $form_callback) {
419 $field = content_fields($field_name, $type_name);
420
421 if (empty($field) || empty($_POST['form_build_id'])) {
422 // Invalid request.
423 print drupal_to_js(array('data' => ''));
424 exit;
425 }
426
427 // Build the new form.
428 $form_state = array('submitted' => FALSE);
429 $form_build_id = $_POST['form_build_id'];
430 $form = form_get_cache($form_build_id, $form_state);
431
432 if (!$form) {
433 // Invalid form_build_id.
434 print drupal_to_js(array('data' => ''));
435 exit;
436 }
437 // form_get_cache() doesn't yield the original $form_state,
438 // but form_builder() does. Needed for retrieving the file array.
439 $built_form = $form;
440 $built_form_state = $form_state;
441 $built_form += array('#post' => $_POST);
442 $built_form = form_builder($_POST['form_id'], $built_form, $built_form_state);
443
444 // Clean ids, so that the same element doesn't get a different element id
445 // when rendered once more further down.
446 form_clean_id(NULL, TRUE);
447
448 // Perform the action for this AHAH callback.
449 $form_callback($built_form, $built_form_state, $field, $delta);
450
451 // Ask CCK for the replacement form element. Going through CCK gets us
452 // the benefit of nice stuff like '#required' merged in correctly.
453 module_load_include('inc', 'content', 'includes/content.node_form');
454 $field_element = content_field_form($form, $built_form_state, $field, $delta);
455 $delta_element = $field_element[$field_name][0]; // there's only one element in there
456
457 // Add the new element at the right place in the form.
458 if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type_name, $field_name))) {
459 $form[$group_name][$field_name][$delta] = $delta_element;
460 }
461 else {
462 $form[$field_name][$delta] = $delta_element;
463 }
464
465 // Write the (unbuilt, updated) form back to the form cache.
466 form_set_cache($form_build_id, $form, $form_state);
467
468 // Render the form for output.
469 $form += array(
470 '#post' => $_POST,
471 '#programmed' => FALSE,
472 );
473 drupal_alter('form', $form, array(), 'filefield_js');
474 $form_state = array('submitted' => FALSE);
475 $form = form_builder('filefield_js', $form, $form_state);
476 $field_form = empty($group_name) ? $form[$field_name] : $form[$group_name][$field_name];
477
478 // We add a div around the new content to tell AHAH to let this fade in.
479 $field_form[$delta]['#prefix'] = '<div class="ahah-new-content">'. (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : '');
480 $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') .'</div>';
481
482 $output = theme('status_messages') . drupal_render($field_form[$delta]);
483
484 // AHAH is not being nice to us and doesn't know the "other" button (that is,
485 // either "Upload" or "Delete") yet. Which in turn causes it not to attach
486 // AHAH behaviours after replacing the element. So we need to tell it first.
487 $javascript = drupal_add_js(NULL, NULL);
488 if (isset($javascript['setting'])) {
489 $output .= '<script type="text/javascript">jQuery.extend(Drupal.settings, '. drupal_to_js(call_user_func_array('array_merge_recursive', $javascript['setting'])) .');</script>';
490 }
491
492 // For some reason, file uploads don't like drupal_json() with its manual
493 // setting of the text/javascript HTTP header. So use this one instead.
494 print drupal_to_js(array('status' => TRUE, 'data' => $output));
495 exit;
496}
1bffec9e 497
1a05ff77
DP
498/**
499 * set the default values for imagefield.
500 * This seems to work for all but add a new item on unlimited values which doesn't
501 * get assigned a proper default.
502 */
1bffec9e 503function filefield_default_value(&$form, &$form_state, $field, $delta) {
1a05ff77
DP
504 $items = array();
505 $field_name = $field['field_name'];
506
507 switch ($field['multiple']) {
508 case 0:
509 $max = 1;
510 break;
511 case 1:
512 $max = isset($form_state['item_count'][$field_name]) ? $form_state['item_count'][$field_name] : 2;
513 break;
514 default:
515 $max = $field['multiple'];
516 break;
517 }
518
519 for ($delta = 0; $delta < $max; $delta++) {
520 $items[$delta] = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => ''));
521 }
522 return $items;
1bffec9e 523}
d6caa409
DP
524
525/**
526 * Check that the filename ends with an allowed extension. This check is
527 * enforced for the user #1.
528 *
529 * @param $file
530 * A Drupal file object.
531 * @param $extensions
532 * A string with a space separated
533 * @return
534 * An array. If the file extension is not allowed, it will contain an error message.
535 */
536function filefield_validate_extensions($file, $extensions) {
537 global $user;
538 $errors = array();
539
540 $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i';
541 if (!preg_match($regex, $file->filename)) {
542 $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
543 }
544 return $errors;
545}
546
547
548
549// These two functions return messages for file_validate_size, and file_validate_extensions.
550// They're a neat hack that gets the job done.
551function filefield_validate_extensions_help($extensions) {
552 return t('Allowed Extensions: %ext', array('%ext' => $extensions));
553}
554
555function file_validate_size_help($size) {
556 return t('Maximum Filesize: %size', array('%size' => format_size($size)));
557}
558
559