'filefield/js', 'callback' => 'filefield_js', //'access' => user_access(), 'access' => TRUE, 'type' => MENU_CALLBACK ); } else if ($_SESSION['filefield']) { // Add handlers for previewing new uploads. foreach ($_SESSION['filefield'] as $fieldname => $files) { if (is_array($files)) { foreach($files as $delta => $file) { if ($file['preview']) { $items[] = array( 'path' => $file['preview'], 'callback' => '_filefield_preview', 'access' => TRUE, 'type' => MENU_CALLBACK ); } } } } } return $items; } /** * transfer a file that is in a 'preview' state. * @todo multiple support */ function _filefield_preview() { foreach ($_SESSION['filefield'] as $fieldname => $files) { foreach ($files as $delta => $file) { if ($file['preview'] == $_GET['q']) { file_transfer($file['filepath'], array('Content-Type: '. mime_header_encode($file['filemime']), 'Content-Length: '. $file['filesize'])); exit(); } } } } function filefield_perm() { return array('view filefield uploads'); } /** * Implementation of hook_field_info(). */ function filefield_field_info() { return array( 'file' => array('label' => 'File'), ); } /** * Implementation of hook_field_settings(). */ function filefield_field_settings($op, $field) { switch ($op) { case 'form': $form = array(); $form['force_list'] = array( '#type' => 'checkbox', '#title' => t('Always list files'), '#default_value' => isset($field['force_list']) ? $field['force_list'] : 0, '#description' => t('If enabled, the "List" checkbox will be hidden and files are always shown. Otherwise, the user can choose for each file whether it should be listed or not.'), ); return $form; case 'validate': break; case 'save': return array('force_list'); case 'database columns': $columns = array( 'fid' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'), 'description' => array('type' => 'varchar', length => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE), 'list' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'), ); return $columns; case 'filters': return array( 'not null' => array( 'operator' => array('=' => t('Has Image')), 'list' => 'views_handler_operator_yesno', 'list-type' => 'select', 'handler' => 'filefield_views_handler_filter_is_not_null', ), ); } } function filefield_default_item() { return array( 'fid' => 0, 'description' => '', 'list' => 0, ); } /** * insert a file into the database. * @param $node * node object file will be associated with. * @param $file * file to be inserted, passed by reference since fid should be attached. * */ function filefield_file_insert($node, $field, &$file) { $fieldname = $field['field_name']; // allow tokenized paths. if (function_exists('token_replace')) { global $user; $widget_file_path = token_replace($field['widget']['file_path'], 'user', $user); } else { $widget_file_path = $field['widget']['file_path']; } $filepath = file_create_path($widget_file_path) . '/' . $file['filename']; if (filefield_check_directory($widget_file_path) && $file = file_save_upload((object)$file, $filepath)) { $file = (array)$file; $file['fid'] = db_next_id('{files}_fid'); $file['filemime'] = mimedetect_mime($file); db_query('INSERT into {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, "%s","%s","%s",%d)', $file['fid'], $node->nid, $file['filename'], $file['filepath'], $file['filemime'], $file['filesize']); module_invoke_all('filefield', 'file_save', $node, $field, $file); return (array)$file; } else { // Include file name in upload error. form_set_error(NULL, t('File upload was unsuccessful.')); return FALSE; } } /** * update the file record if necessary * @param $node * @param $file * @param $field */ function filefield_file_update($node, $field, &$file) { $file = (array)$file; if ($file['delete'] == TRUE) { module_invoke_all('filefield', 'file_delete', $node, $field, $file); _filefield_file_delete($node, $field, $file); return array(); } if ($file['fid'] == 'upload') { return filefield_file_insert($node, $field, $file); } else { // if fid is not numeric here we should complain. // else we update the file table. } return $file; } /** * Implementation of hook_field(). */ function filefield_field($op, &$node, $field, &$items, $teaser, $page) { $fieldname = $field['field_name']; switch ($op) { // called after content.module loads default data. case 'load': if (is_array($items)) { $items = array_filter($items); // drop empty deltas, cuz cck sends 'em some times. } if (empty($items)) { return array(); } foreach ($items as $delta => $item) { if (!empty($item['fid'])) { // otherwise, merge our info with CCK's, and all is fine. $items[$delta] = array_merge($item, _filefield_file_load($item['fid'])); } } $items = array_values($items); // compact deltas return array($fieldname => $items); // called before content.module defaults. case 'insert': foreach ($items as $delta => $item) { $items[$delta] = filefield_file_insert($node, $field, $item); } $items = array_values($items); // compact deltas filefield_clear_field_session($fieldname); break; // called before content.module defaults. case 'update': foreach ($items as $delta => $item) { $items[$delta] = filefield_file_update($node, $field, $item); } $items = array_filter($items); // unset empty items. $items = array_values($items); // compact deltas filefield_clear_field_session($fieldname); break; case 'delete': foreach ($items as $delta => $item) { _filefield_file_delete($node, $field, $item); } break; } } /** * Implementation of hook_widget_info(). */ function filefield_widget_info() { return array( 'file' => array( 'label' => 'File', 'field types' => array('file'), ), ); } /** * Implementation of hook_widget_settings(). */ function filefield_widget_settings($op, $widget) { switch ($op) { case 'callbacks': return array('default value' => CONTENT_CALLBACK_CUSTOM); case 'form': $form = array(); $form['file_extensions'] = array ( '#type' => 'textfield', '#title' => t('Permitted upload file extensions'), '#default_value' => isset($widget['file_extensions']) ? $widget['file_extensions'] : 'txt', '#size' => 64, '#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.'), ); $form['file_path'] = array( '#type' => 'textfield', '#title' => t('File path'), '#default_value' => $widget['file_path'] ? $widget['file_path'] : '', '#description' => t('Optional subdirectory within the "%dir" directory where files will be stored. Do not include trailing slash.', array('%dir' => variable_get('file_directory_path', 'files'))), ); if (function_exists('token_replace')) { $form['file_path']['#description'] .= theme('token_help', 'user'); } return $form; case 'validate': break; case 'save': return array('file_extensions', 'file_path'); } } function filefield_clear_session() { if (is_array($_SESSION['filefield']) && count($_SESSION['filefield'])) { foreach (array_keys($_SESSION['filefield']) as $fieldname) { filefield_clear_field_session($fieldname); } unset($_SESSION['filefield']); } } function filefield_clear_field_session($fieldname) { if (is_array($_SESSION['filefield'][$fieldname]) && count($_SESSION['filefield'][$fieldname])) { foreach ($_SESSION['filefield'][$fieldname] as $delta => $file) { if (is_file($file['filepath'])) { file_delete($file['filepath']); } } unset($_SESSION['filefield'][$fieldname]); } } function _filefield_file_delete($node, $field, $file) { if (is_numeric($file['fid'])) { db_query('DELETE FROM {files} WHERE fid = %d', $file['fid']); } else { unset($_SESSION['filefield'][$field['field_name']][$file['sessionid']]); } module_invoke_all('filefield', 'delete', $node, $field, $file); return file_delete($file['filepath']); } /** * Implementation of hook_widget(). */ function filefield_widget($op, $node, $field, &$items) { $fieldname = $field['field_name']; switch ($op) { case 'default value': return array(); case 'prepare form values': _filefield_widget_prepare_form_values($node, $field, $items); break; case 'form': return _filefield_widget_form($node, $field, $items); case 'validate': _filefield_widget_validate($node, $field, $items); break; } } function _filefield_widget_prepare_form_values($node, $field, &$items) { $fieldname = $field['field_name']; // @todo split this into its own function. determine if we can make it a form element. if (!count($_POST)) { filefield_clear_session(); } // Attach new files if ($file = file_check_upload($fieldname . '_upload')) { $file = (array)$file; // test allowed extensions. We do this when the file is uploaded, rather than waiting for the // field itseld to reach op==validate. $last_ext = array_pop(explode('.', $file['filename'])); $valid = TRUE; // only check extensions if there extensions to check. // @todo: trim & strtolower file_extenstions with a formapi validate callback. if (strlen(trim($field['widget']['file_extensions']))) { $allowed_extensions = array_unique(explode(' ', strtolower(trim($field['widget']['file_extensions'])))); $ext = array_pop(explode('.', strtolower($file['filename']))); if (!in_array($ext, $allowed_extensions)) { $valid = FALSE; form_set_error($field['field_name'] .'_upload', t('Files with the extension %ext are not allowed. Please upload a file with an extension from the following list: %allowed_extensions', array('%ext' => $last_ext, '%allowed_extensions' => $field['widget']['file_extensions']))); } } // let extended validation from other module happen so we get all error messages. // if you implement hook_filefield_file() return FALSE to stop the upload. if (!$valid || in_array(FALSE, module_invoke_all('filefield', 'file_validate', $node, $field, $file))) { return FALSE; } // let modules massage act on the file. foreach(module_implements('filefield') as $module) { $function = $module .'_filefield'; $function('file_prepare', $node, $field, $file); } $filepath = file_create_filename($file['filename'], file_create_path($field['widget']['file_path'])); if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) { if (strpos($filepath, file_directory_path()) !== FALSE) { $filepath = trim(substr($filepath, strlen(file_directory_path())), '\\/'); } $filepath = 'system/files/'. $filepath; }; // prepare file array. $file['fid'] = 'upload'; $file['preview'] = $filepath; // if this is a single value filefield mark any other files for deletion. if (!$field['multiple']) { if (is_array($items)) { foreach($items as $delta => $session_file) { $items[$delta]['delete'] = TRUE; } } // Remove old temporary file from session. filefield_clear_field_session($fieldname); } $file_id = count($items) + count($_SESSION['filefield'][$fieldname]); $_SESSION['filefield'][$fieldname][$file_id] = $file; } // Load files from preview state. before committing actions. if (!empty($_SESSION['filefield'][$fieldname])) { foreach($_SESSION['filefield'][$fieldname] as $delta => $file) { $items[] = $file; } } } function _filefield_widget_form($node, $field, &$items) { drupal_add_js('misc/progress.js'); drupal_add_js('misc/upload.js'); drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js'); $fieldname = $field['field_name']; drupal_add_css(drupal_get_path('module', 'filefield') .'/filefield.css'); $form = array(); $form[$fieldname] = array( '#type' => 'fieldset', '#title' => t($field['widget']['label']), '#description' => t('Changes made to the attachments are not permanent until you save this post.'), '#weight' => $field['widget']['weight'], '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, '#prefix' => '
', '#suffix' => '
', ); $form[$fieldname]['new'] = array( '#tree' => FALSE, '#prefix' => '
', '#suffix' => '
', '#weight' => 100, ); $extensions_description = empty($field['widget']['file_extensions']) ? '' : t('
Allowed extensions: %ext', array('%ext' => $field['widget']['file_extensions'])); // Separate from tree becase of that silly things won't be displayed // if they are a child of '#type' = form issue $form[$fieldname]['new'][$fieldname .'_upload'] = array( '#type' => 'file', '#title' => t('Attach new file'), '#description' => $field['widget']['description'] . $extensions_description, '#weight' => 9, '#tree' => FALSE, '#attributes' => array('accept' => str_replace(' ', '|', trim($field['widget']['file_extensions']))), ); $form[$fieldname]['new']['upload'] = array( '#type' => 'button', '#value' => t('Upload'), '#name' => 'cck_filefield_'. $fieldname .'_op', '#id' => form_clean_id($fieldname .'-attach-button'), '#tree' => FALSE, '#weight' => 10, ); if (is_array($items) && count($items)) { $form[$fieldname]['files'] = array( '#parents' => array($fieldname), '#theme' => 'filefield_form_current', // remember the force_list setting so that the theme function knows '#force_list' => $field['force_list'], ); foreach($items as $delta => $file) { // @todo: split into its own form and theme functions per file like imagefield if ($file['filepath'] && !$file['delete']) { $form[$fieldname]['files'][$delta] = _filefield_file_form($node, $field, $file); } else if ($file['filepath'] && $file['delete']) { $form[$fieldname]['files'][$delta]['delete'] = array( '#type' => 'hidden', '#value' => $file['delete'], ); } } // Special handling for single value fields. if (!$field['multiple']) { $form[$fieldname]['replace'] = array( '#type' => 'markup', '#value' => '

'. t('If a new file is uploaded, this file will be replaced upon submitting this form.') .'

', '#prefix' => '
', '#suffix' => '
', ); } } // The class triggers the js upload behaviour. $form[$fieldname.'-attach-url'] = array( '#type' => 'hidden', '#value' => url('filefield/js', NULL, NULL, TRUE), '#attributes' => array('class' => 'upload'), ); // Some useful info for our js callback. $form['vid'] = array( '#type' => 'hidden', '#value' => $node->vid, '#tree' => FALSE, ); $form['nid'] = array( '#type' => 'hidden', '#value' => $node->nid, '#tree' => FALSE, ); $form['type'] = array( '#type' => 'hidden', '#value' => $node->type, '#tree' => FALSE, ); return $form; } function _filefield_file_form($node, $field, $file) { // Lets be a good boy and initialize our variables. $form = array(); $form['#after_build'] = array('_filefield_file_form_description_reset'); $form['icon'] = array( '#type' => 'markup', '#value' => theme('filefield_icon', $file), ); $form['file_preview'] = array(); $filepath = ($file['fid'] == 'upload') ? file_create_filename($file['filename'], file_create_path($field['widget']['file_path'])) : $file['filepath']; $url = file_create_url($filepath); $url = "". check_plain($url) .""; $form['description'] = array( '#type' => 'textfield', '#default_value' => (strlen($file['description'])) ? $file['description'] : $file['filename'], '#maxlength' => 256, ); $form['url'] = array( '#type' => 'markup', '#value' => t('URL: !url', array('!url' => $url)), '#prefix' => '
', '#suffix' => '
', ); $form['size'] = array( '#type' => 'markup', '#value' => format_size($file['filesize']), '#prefix' => '
', '#suffix' => '
', ); $form['delete'] = array( '#type' => 'checkbox', '#default_value' => $file['delete'], ); // Only show the list checkbox if files are not forced to be listed. if (!$field['force_list']) { $form['list'] = array( '#type' => 'checkbox', '#default_value' => $file['list'], ); } else { $form['list'] = array( '#type' => 'value', '#value' => isset($file['list']) ? $file['list'] : 1, ); } $form['filename'] = array('#type' => 'value', '#value' => $file['filename']); $form['filepath'] = array('#type' => 'value', '#value' => $file['filepath']); $form['filemime'] = array('#type' => 'value', '#value' => $file['filemime']); $form['filesize'] = array('#type' => 'value', '#value' => $file['filesize']); $form['fid'] = array('#type' => 'value', '#value' => $file['fid']); // Remember the current filename for the check in // _filefield_file_form_description_reset() that happens after submission. $form['previous_filepath'] = array('#type' => 'hidden', '#value' => $file['filepath']); foreach (module_implements('filefield') as $module) { $function = $module .'_filefield'; $function('file_form', $node, $field, $file, $form); } return $form; } /** * This after_build function is needed as fix for tricky Form API behaviour: * When using filefield without AJAX uploading, the description field was not * updated to a new '#default_value' because the textfield has been submitted, * which causes Form API to override the '#default_value'. * * That bug is fixed with this function by comparing the previous filename * to the new one, and resetting the description to the '#default_value' * if the filename has changed. */ function _filefield_file_form_description_reset($form, $form_values) { // Don't bother resetting the description of files that stay the same if ($form['fid']['#value'] != 'upload') { return $form; } // Get the previous filename for comparison with the current one. $previous = $form['previous_filepath']['#post']; foreach ($form['previous_filepath']['#parents'] as $parent) { $previous = isset($previous[$parent]) ? $previous[$parent] : NULL; } // If a new file was uploaded (the file path changed), reset the description. if ($previous != $form['filepath']['#value']) { $form['description']['#value'] = $form['description']['#default_value']; } return $form; } /** * Validate the form widget. */ function _filefield_widget_validate($node, $field, $items) { if (!$field['required']) { return; } // if there aren't any items.. throw an error. if (!count($items)) { form_set_error($field['field_name'], t('@field is required. Please upload a file.', array('@field' => $field['widget']['label']))); } else { // Sum all the items marked for deletion, so we can make sure the end user // isn't deleting all of the files. $count_deleted = 0; foreach($items as $item) { $count_deleted += isset($item['delete']) && $item['delete']; } if (count($items) == $count_deleted) { form_set_error($field['field_name'], t('@field is required. Please keep at least one file or upload a new one.', array('@field' => $field['widget']['label']))); } } } /** * Implementation of hook_field formatter. * @todo: finish transformer.module and integrate like imagecache with imagefield. */ function filefield_field_formatter_info() { $formatters = array( 'default' => array( 'label' => t('Default'), 'field types' => array('file'), ), ); return $formatters; } function filefield_field_formatter($field, $item, $formatter) { if($field['force_list']) { $item['list'] = 1; // always show the files if that option is enabled } if(!empty($item['fid'])) { $item = array_merge($item, _filefield_file_load($item['fid'])); } if (!empty($item['filepath'])) { return theme('filefield', $item); } } function _filefield_file_load($fid = NULL) { // Don't bother if we weren't passed and fid. if (!empty($fid) && is_numeric($fid)) { $result = db_query('SELECT * FROM {files} WHERE fid = %d', $fid); $file = db_fetch_array($result); if ($file) { // let modules load extended attributes. $file += module_invoke_all('filefield', 'file_load', $node, $field, $file); return $file; } } // return an empty array if nothing was found. return array(); } function theme_filefield_form_current($form) { $header = $form['#force_list'] ? array('', t('Delete'), t('Description'), t('Size')) : array('', t('Delete'), t('List'), t('Description'), t('Size')); foreach (element_children($form) as $key) { // Don't display (hidden) replaced items. if ($form[$key]['delete']['#type'] == 'hidden') { continue; } $row = array(); // we just going to lose this for now until we figure out how to handle it... $row[] = drupal_render($form[$key]['file_preview']); $row[] = drupal_render($form[$key]['delete']); if (!$form['#force_list']) { $row[] = drupal_render($form[$key]['list']); } $row[] = drupal_render($form[$key]['icon']). drupal_render($form[$key]['description']). drupal_render($form[$key]['url']); $row[] = drupal_render($form[$key]['size']); $rows[] = $row; } $output = theme('table', $header, $rows); $output .= drupal_render($form); return $output; } function theme_filefield_icon($file) { $ext = array_pop(explode('.',$file['filename'])); $known_extensions = array('0','ace','aif','ai','ani','asf','asp','avi','bak','bat','bin','bmp','bz2','bz','cab','cdr','cfg','com','conf','cpt','css','cur','dat','db','dcr','dic','diff','dir','dll','dmg','doc','dwg','edir','eml','eps','exe','fla','flv','fon','gif','gz','hqx','html','htm','ico','inc','ini','iso','jpeg','jpg','js','lnk','log','m3u','mdb','midi','mid','mov','mp3','mpeg','mpg','nfo','odb','odc','odf','odg','odm','odp','ods','odt','ogg','otg','oth','otp','ots','ott','patch','pdf','php3','php','phtml','pl','png','pps','ppt','psd','pwl','qt','ram','ra','rar','reg','rpm','rtf','sh','shtml','sit','sql','svg','swf','sxc','sxi','sxw','sys','tar','tgz','tiff','tif','tmp','tpl','ttf','txt','wav','wma','wmv','wp','xls','xml','zip'); if (!in_array($ext, $known_extensions)) { $ext = 0; } $icon = ''; $iconpath = base_path() .'/'. drupal_get_path('module','filefield') .'/ico/'. $ext .'.png'; if (file_exists($iconpath)) { $icon = ''; } return '
'. $icon .'
'; } function theme_filefield_view_file($file) { return theme('filefield', $file); } function theme_filefield($file) { if (user_access('view filefield uploads') && is_file($file['filepath']) && $file['list']) { $path = ($file['fid'] == 'upload') ? file_create_filename($file['filename'], file_create_path($field['widget']['file_path'])) : $file['filepath']; $url = file_create_url($path); $desc = $file['description']; return l($desc, $url); } return ''; } function filefield_file_download($file) { $file = file_create_path($file); $result = db_query("SELECT * FROM {files} WHERE filepath = '%s'", $file); if (!$file = db_fetch_object($result)) { // We don't really care about this file. return; } $node = node_load($file->nid); if (!node_access('view', $node)) { // You don't have permission to view the node // this file is attached to. return -1; } // @todo: check the node for this file to be referenced in a field // to determine if it is managed by filefield. and do the access denied part here. if (!user_access('view filefield uploads')) { // sorry you do not have the proper permissions to view // filefield uploads. return -1; } // Well I guess you can see this file. $name = mime_header_encode($file->filename); $type = mime_header_encode($file->filemime); // Serve images and text inline for the browser to display rather than download. $disposition = ereg('^(text/|image/)', $file->filemime) ? 'inline' : 'attachment'; return array( 'Content-Type: '. $type .'; name='. $name, 'Content-Length: '. $file->filesize, 'Content-Disposition: '. $disposition .'; filename='. $name, 'Cache-Control: private', ); } /** * Create the file directory relative to the 'files' dir recursively for every * directory in the path. * * @param $directory * The directory path under files to check, such as 'photo/path/here' * @param $form_element * A form element to throw an error on if the directory is not writable */ function filefield_check_directory($directory, $form_element = array()) { foreach(explode('/', $directory) as $dir) { $dirs[] = $dir; $path = file_create_path(implode($dirs,'/')); file_check_directory($path, FILE_CREATE_DIRECTORY, $form_element['#parents'][0]); } return TRUE; } /** * Menu callback for JavaScript-based uploads. */ function filefield_js() { // Parse fieldname from submit button. $matches = array(); foreach(array_keys($_POST) as $key) { if (preg_match('/cck_filefield_(.*)_op/', $key, $matches)) { $fieldname = $matches[1]; break; } } $node = (object)$_POST; $field = content_fields($fieldname, $node->type); // load field data // Load fids stored by content.module. $items = array(); $values = content_field('load', $node, $field, $items, FALSE, FALSE); $items = $values[$fieldname]; // Load additional field data. filefield_field('load', $node, $field, $items, FALSE, FALSE); // Handle uploads and validation. _filefield_widget_prepare_form_values($node, $field, $items); _filefield_widget_validate($node, $field, $items); // Get our new form baby, yeah tiger, get em! $form = _filefield_widget_form($node, $field, $items); foreach (module_implements('form_alter') as $module) { $function = $module .'_form_alter'; $function('filefield_js', $form); } $form = form_builder('filefield_js', $form); $output = theme('status_messages') . drupal_render($form); // Send the updated file attachments form. print drupal_to_js(array('status' => TRUE, 'data' => $output)); exit(); } function filefield_token_list($type = 'all') { if ($type == 'field' || $type == 'all') { $tokens = array(); $tokens['file']['fid'] = t("File ID"); $tokens['file']['title'] = t("File title"); $tokens['file']['filename'] = t("File name"); $tokens['file']['filepath'] = t("File path"); $tokens['file']['filemime'] = t("File MIME type"); $tokens['file']['filesize'] = t("File size"); $tokens['file']['view'] = t("Fully formatted HTML file tag"); return $tokens; } } function filefield_token_values($type, $object = NULL) { if ($type == 'field') { $item = $object[0]; $tokens['fid'] = $item['fid']; $tokens['title'] = $item['title']; $tokens['filename'] = $item['filename']; $tokens['filepath'] = $item['filepath']; $tokens['filemime'] = $item['filemime']; $tokens['filesize'] = $item['filesize']; $tokens['view'] = $item['view']; return $tokens; } } /** * Custom filter for imagefield NOT NULL */ function filefield_views_handler_filter_is_not_null($op, $filter, $filterinfo, &$query) { if ($op == 'handler') { $query->ensure_table($filterinfo['table']); if ($filter['value']) { $qs = '%s.%s > 0'; } else { $qs = '%s.%s = 0 OR %s.%s IS NULL'; } $query->add_where($qs, $filterinfo['table'], $filterinfo['field'], $filterinfo['table'], $filterinfo['field']); } }