'. t('A module that provides two new FAPI elements to handle file uploads.') .'

'; return $output; } } /** * Implementation of hook_menu(). */ function upload_element_menu() { $items = array(); $items['upload_element'] = array( 'title' => t('Upload element preview'), 'file' => 'upload_element.pages.inc', 'type' => MENU_CALLBACK, 'access arguments' => array('access content'), 'page callback' => 'image_upload_element_thumb', 'page arguments' => array(), ); $items['upload_element_js/%/%/%'] = array( 'title' => t('AHAH Callback'), 'file' => 'upload_element.pages.inc', 'type' => MENU_CALLBACK, 'access arguments' => array('access content'), 'page callback' => 'upload_element_js', 'page arguments' => array(1, 2, 3), ); return $items; } /** * Implementation of hook_theme(). */ function upload_element_theme() { return array( 'upload_element' => array( 'arguments' => array('element' => NULL), ), 'image_upload_element' => array( 'arguments' => array('element' => NULL), ), 'upload_element_preview' => array( 'arguments' => array('element' => NULL), ), 'upload_element_image_preview' => array( 'arguments' => array('element' => NULL), ), 'upload_element_file_description' => array( 'arguments' => array('element' => NULL), ), ); } /** * Implementation of hook_init(). */ function upload_element_init() { drupal_add_css(drupal_get_path('module', 'upload_element') .'/upload_element.css'); } /** * Implementation of hook_elements(). * * Defines two file form elements that are linked into the * native Drupal file handling system. * * The elements share four native parameters: * #file_formatter - theming function to preview the element * #file_validators - array of additional validation functions * to perform on the uploaded file element. * * The image_upload_element integrates into imagecache to enable a preset to * be run against the image when saving this to the new location. */ function upload_element_elements() { $type['upload_element'] = array( '#input' => TRUE, '#default_value' => '', '#process' => array('_upload_element_expand'), '#element_validate' => array('upload_element_element_validate'), '#file_formatter' => 'upload_element_preview', '#file_validators' => array(), '#file_validator_seperator' => '
', ); $type['image_upload_element'] = array( '#input' => TRUE, '#default_value' => '', '#process' => array('_upload_element_expand'), '#element_validate' => array('upload_element_element_validate'), '#file_formatter' => 'upload_element_preview', '#file_validators' => array(), '#file_validator_seperator' => '
', '#image_formatter' => 'upload_element_image_preview', '#image_preview_size' => '100x100', ); return $type; } /** * Our #process callback to expand the control. */ function _upload_element_expand($element, $edit, &$form_state, $complete_form) { // We need this to parse the form cache for image preview // and ajax functions. $element['#build_id'] = $complete_form['#build_id']; // A form rebuild changes the form build id if (isset($form_state['#'.$element['#name']])) { if ($form_state['#'.$element['#name']] != $element['#build_id']) { if (isset($_SESSION['files']['upload_element'][$form_state['#'.$element['#name']]])) { $_SESSION['files']['upload_element'][$element['#build_id']] = $_SESSION['files']['upload_element'][$form_state['#'.$element['#name']]]; unset($_SESSION['files']['upload_element'][$form_state['#'.$element['#name']]]); $form_state['rebuild'] = TRUE; } } } $form_state['#'.$element['#name']] = $element['#build_id']; if (!count($complete_form['#post'])) { $_SESSION['files']['upload_element'][$element['#build_id']][$element['#name'] .'_default'] = array_key_exists('#value', $element) ? $element['#value']: $element['#default_value']; } // move things around a bit $children = element_children($element); $element[$element['#name'] .'_custom_elements'] = array(); foreach($children as $child) { $element[$element['#name'] .'_custom_elements'][$child] = $element[$child]; unset($element[$child]); } $element[$element['#name'] .'_upload_element_file'] = array( '#type' => 'file', '#size' => $element['#size'] ? $element['#size'] : 40, '#name' => 'files['. $element['#name'] .']', '#id' => form_clean_id("edit-file-{$element['#name']}"), '#description' => theme('upload_element_file_description', $element), ); $element[$element['#name'] .'_upload_element_ahah'] = array( '#type' => 'submit', '#value' => t('Update'), '#name' => $element['#name'] .'-upload-element', '#prefix' => '
', '#suffix' => '
', '#ahah' => array( 'path' => 'upload_element_js/'. $element['#build_id'] .'/'. $complete_form['form_id']['#value'] .'/'. $element['#name'], 'wrapper' => $element['#id'] .'-ahah-wrapper', 'method' => 'replace', 'progress' => array('type' => 'bar', 'message' => t('Please wait...')), 'effect' => 'none', ), ); return $element; } /** * This is the core function that handles the uploading and workflow * of the submitted files. * * @param array $form The form element whose value is being populated. * @param mixed $edit The incoming POST data to populate the form element. * If this is FALSE, the element's default value should be returned. * * @return mixed The file object or empty string. */ function form_type_image_upload_element_value($form, $edit = FALSE) { return form_type_upload_element_value($form, $edit); } /** * This is the core function that handles the uploading and workflow * of the submitted files. * * @param array $form The form element whose value is being populated. * @param mixed $edit The incoming POST data to populate the form element. * If this is FALSE, the element's default value should be returned. * * @return mixed The file object or empty string. */ function form_type_upload_element_value($form, $edit = FALSE) { if ($edit !== FALSE) { $name = $form['#name']; $action = $form['#post'][$name][$name .'_action']; if (is_object($form['#default_value'])) { if ($action == 'delete') { upload_element_session_handler('delete', $form); } if ($action == 'restore') { upload_element_session_handler('restore', $form); } } if ($action == 'revert') { upload_element_session_handler('revert', $form); } // add image validator, default max size validators have no meaning here // as the form submission will have already failed. if ($form['#type'] == 'image_upload_element') { $form['#file_validators']['file_validate_is_image'] = array(); } $file = file_save_upload($name, $form['#file_validators']); if (!$file && isset($_FILES['files']) && $_FILES['files']['name'][$name]) { switch ($_FILES['files']['error'][$name]) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $source, '%maxsize' => format_size(file_upload_max_size()))), 'error'); } } $value = !empty($file) ? upload_element_session_handler('store', $form, $file) : upload_element_session_handler('value', $form); return $value; } else { return $form['#default_value']; } } /** * A private helper function to parse the real value from * the element. This takes into account the submit_action * when present. * * @param array $element Upload element * @return mixed File object or FALSE. */ function _upload_element_parse_value($element) { if (is_object($element['#value'])) { if (isset($element['#value']->submit_action)) { return ($element['#value']->submit_action == UPLOAD_ELEMENT_DELETE) ? FALSE : $element['#value']; } else { return $element['#value']; } } return FALSE; } /** * The custom validation required for files that are marked * as deleted, but are required. * * @param array $element * @param array $form_state */ function upload_element_element_validate($element, &$form_state) { if ($element['#required'] && !_upload_element_parse_value($element)) { form_error($element, t('!name field is required.', array('!name' => $element['#title']))); } } /** * Simple helper function to compare to file elements * * @param mixed $a file object to compare * @param mixed $b file object to compare * @return bool */ function upload_element_equals($a = FALSE, $b = FALSE) { if (is_object($a) && is_object($b)) { return $a->fid == $b->fid; } return FALSE; } /** * Theme function to format the edit form. */ function theme_image_upload_element($element) { return theme('upload_element', $element); } /** * Theme function to format the edit form. */ function theme_upload_element($element) { _form_set_class($element, array('form-file')); $preview = ''; if ($element['#image_formatter']) { $preview = theme($element['#image_formatter'], $element); // We need to calculate the required margin for the details section. $width = 100; if ($element['#image_preview_size'] && preg_match('/^\d+x\d+$/', $element['#image_preview_size'])) { list($width,) = explode('x', $element['#image_preview_size'], 2); } $details_styles = ' style="margin-left:'. ($width + 10) . 'px;"'; } $file_details = ''; if ($element['#file_formatter']) { $file_details = theme($element['#file_formatter'], $element); } $messages = ''; if (!empty($element['#messages'])) { $messages = '
'. $element['#messages'] ."
\n"; } $action_field = _upload_element_action_field($element); $description = $element['#description'] ? '
'. $element['#description'] .'
' : ''; $value = << {$messages} {$file_details} {$action_field} {$element['#children']}
{$description}
END; $ahah_wrapper_id = $element['#id'] .'-ahah-wrapper'; unset($element['#id']); unset($element['#description']); return isset($element['#is_ahah']) ? $value : theme('form_element', $element, "
". $value .'
'); } /** * Private function to generate the options that go with the * upload element * * @param array $element Parent upload element * @return array $children Child elements */ function _upload_element_action_field($element) { $children = array(); if (is_object($element['#value'])) { $name = $element['#name']; $key = $name .'_action'; $children[$key]= array( '#type' => 'checkbox', '#id' => form_clean_id("edit-{$key}"), '#name' => "{$name}[{$key}]", '#value' => 0, '#weight' => -10, ); // TODO: While this is working, there may be two logical bugs // the negate each other. if (upload_element_equals($element['#value'], $element['#default_value'])) { if (isset($element['#value']->submit_action) && $element['#value']->submit_action == UPLOAD_ELEMENT_DELETE) { $children[$key]['#return_value'] = 'restore'; $children[$key]['#title'] = t('Restore original'); } else { $children[$key]['#return_value'] = 'delete'; $children[$key]['#title'] = t('Delete original'); $children[$key]['#value'] = (isset($element['#value']->submit_action) ? ($element['#value']->submit_action == UPLOAD_ELEMENT_DELETE ? 'delete' : 0) : 0); } } else { $children[$key]['#return_value'] = 'revert'; $children[$key]['#title'] = (is_object($element['#default_value']) ? t('Discard replacment') : t('Discard upload')); } } return drupal_render($children); } /** * Simple theming function to display the uploaded file. * * @param object $file File object * @return string HTML of the filename and size. */ function theme_upload_element_preview($element = NULL) { $label = t('Filename'); $filename = '--'; $filesize = ''; $class = 'upload-element-preview'; if ($file = _upload_element_parse_value($element)) { $filename = check_plain($file->filename); $filesize = '('. format_size($file->filesize) .')'; // Add some nice mime type classes if ($file->filemime) { list($base_mime, $extended_mime) = explode('/', $file->filemime); $class = trim($class .' mime-'. $base_mime .' '. ($extended_mime ? ' mime-'. $base_mime .'-'. $extended_mime : '')); } } return << {$label}: {$filename} {$filesize} END; } /** * This creates the thumbnail preview HTML. * * @param array $element * @return string HTML for image thumbnail. */ function theme_upload_element_image_preview($element = NULL) { $fid = 'no_image'; if ($file = _upload_element_parse_value($element)) { $fid = $file->fid; } $src = url('upload_element/'. $element['#build_id'] .'/'. $element['#name'] .'/'. $fid); return '
'. t('Image preview') .'
'; } /** * Saves an uploaded file * * @param object $file File object * @param string $dest destination directory to move the file to * @param int $replace files move action * @param string $presetname Imagecache preset to perfrom on the uploaded image. * @param bool $delete_original A flag to tell the function how to handle * the existing file when it is deleted or replaced. This is used to * prevent the status flag being set to temperory when the file is still used * by the system somewhere else. For example, if you are saving a new * node revision, with a new file, you would want to ensure that this is * set to FALSE if the old image is still valid for some other revision. * @return int The {files}.fid or 0 */ function upload_element_save(&$file, $dest = 0, $replace = FILE_EXISTS_RENAME, $presetname = FALSE, $delete_original = TRUE) { $fid = 0; if (is_object($file)) { $base = file_directory_path(); if (strstr($dest, $base) === FALSE) { $dest = $base .'/'. ltrim($dest, '/'); } file_check_directory($dest, 1); if (!isset($file->submit_action)) { $file->submit_action = UPLOAD_ELEMENT_NONE; } switch ($file->submit_action) { case UPLOAD_ELEMENT_NONE: $fid = $file->fid; break; case UPLOAD_ELEMENT_DELETE: if ($delete_original) { _upload_element_delete($file->fid); } break; case UPLOAD_ELEMENT_REPLACE: if ($delete_original) { _upload_element_delete($file->original_fid); } // fall through case UPLOAD_ELEMENT_NEW: $uploaded = FALSE; if ($presetname) { $destination = file_create_filename($file->filename, $dest); if (upload_element_imagecache_action($presetname, $file->filepath, $destination)) { $file->filepath = $destination; $uploaded = TRUE; } } if (!$uploaded) { $uploaded = file_move($file, $dest .'/'. $file->filename, $replace); } if ($uploaded) { $file->status = FILE_STATUS_PERMANENT; // Clear PHP stat cache in case filesize has changed. clearstatcache(); if ($file_size = @filesize($file->filepath)) { $file->filesize = $file_size; } drupal_write_record('files', $file, 'fid'); $fid = $file->fid; // Imagecache may need flushing if using the same filepath. if (function_exists('imagecache_image_flush')) { imagecache_image_flush($file->filepath); } } else { drupal_set_message(t('Error occured while saving the image!'), 'error'); } break; } } // This cleans up the session by flushing old values. upload_element_clean_session(); return $fid; } /** * Hooks into imageache preset for save action * * @param string $presetname Imagecache preset to perfrom on the uploaded image. * @param string $path Path to the temp original image file. * @param string $dest Path to save the new image to. * * @return bool FALSE if there was a problem saving the image, TRUE otherwise. */ function upload_element_imagecache_action($presetname, $path, $dst) { if (!function_exists('imagecache_preset_by_name')) { return FALSE; } if (!$preset = imagecache_preset_by_name($presetname)) { return FALSE; } if (is_file($dst)) { return TRUE; } $src = $path; if (!is_file($src) && !is_file($src = file_create_path($src))) { return FALSE; }; if (!getimagesize($src)) { return FALSE; } $lockfile = file_directory_temp() .'/'. $preset['presetname'] . basename($src); if (file_exists($lockfile)) { watchdog('imagecache', 'Imagecache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $lockfile), WATCHDOG_NOTICE); return FALSE; } touch($lockfile); register_shutdown_function('file_delete', realpath($lockfile)); if (file_exists($dst) || imagecache_build_derivative($preset['actions'], $src, $dst)) { return TRUE; } // Generate an error if image could not generate. watchdog('imagecache', 'Failed generating an image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $preset['presetname']), WATCHDOG_ERROR); return FALSE; } /** * The core storage handler for keeping the correct state * of the element in between form submissions/AHAH requests. * * @param string $op Operation key. * @param array $form The upload_element element. * @param mixed $file File object or empty string * @return mixed File object or NULL depending on the $op. */ function upload_element_session_handler($op, &$form, $file = '') { $name = $form['#name']; $form_build_id = $form['#post']['form_build_id']; $session_files = &$_SESSION['files']['upload_element'][$form_build_id]; switch ($op) { case 'revert': unset($session_files[$name]); if (is_object($session_files[$name .'_default'])) { $session_files[$name .'_default']->submit_action = UPLOAD_ELEMENT_NONE; } break; case 'value': $file = (isset($session_files[$name])) ? $session_files[$name] : $session_files[$name .'_default']; return is_object($file) ? $file : ''; case 'restore': // delete op only applies to default file that is stored in the session case 'delete': $submit_action = ($op == 'restore') ? UPLOAD_ELEMENT_NONE : UPLOAD_ELEMENT_DELETE; $session_files[$name .'_default']->submit_action = $submit_action; unset($session_files[$name]); if (is_object($form['#default_value'])) { $form['#default_value']->submit_action = $submit_action; } break; case 'store': if (is_object(($session_files[$name .'_default']))) { $file->submit_action = UPLOAD_ELEMENT_REPLACE; $file->original_fid = $session_files[$name .'_default']->fid; } else { $file->submit_action = UPLOAD_ELEMENT_NEW; } $session_files[$name] = $file; return $file; } } /** * Session cleaning function. * * This is used to prevent excess values getting stored * within the $_SESSION over multiple requests. It uses * the form cache to determine if the value should be * deleted or not. */ function upload_element_clean_session() { $form_state = array('submitted' => FALSE); foreach($_SESSION['files']['upload_element'] as $form_build_id => $values) { if (!$form = form_get_cache($form_build_id, $form_state)) { unset($_SESSION['files']['upload_element'][$form_build_id]); } } } /** * This theming function can be used to assign different text to * the description that is found under the file HTML element. * * @param array $element FAPI upload element */ function theme_upload_element_file_description($element) { // set file validation defaults for the theming functions _upload_element_add_file_validators($element); $validation_messages = array(); if (isset($element['#file_validators']['file_validate_extensions'])) { $ext = _upload_element_allowed_extensions($element); $validation_messages[] = t('Only files with the following extensions are allowed: %ext.', array('%ext' => implode(', ', $ext))); } elseif (isset($element['#file_validators']['file_validate_is_image'])) { $validation_messages[] = t('Only JPEG, PNG and GIF images are allowed.'); } if (isset($element['#file_validators']['file_validate_size'])) { $validation_messages[] = t('The maximum upload size is %filesize.', array('%filesize' => format_size($element['#file_validators']['file_validate_size'][0]))); } $validation_messages[] = t('Changes made are not permanent until you save this form.'); $validator_seperator = isset($element['#file_validator_seperator']) ? $element['#file_validator_seperator'] : '
'; return implode($validator_seperator, $validation_messages); } /** * Private helper function to help pull out the allowed * extensions. * * @param array $element */ function _upload_element_allowed_extensions($element) { if ($element['#type'] == 'upload_element') { return array_filter(explode(' ', strtoupper($element['#file_validators']['file_validate_extensions'][0]))); } elseif (isset($element['#file_validators']['file_validate_extensions'])) { return array_intersect(explode(' ', strtoupper($element['#file_validators']['file_validate_extensions'][0])), array('JPEG', 'GIF', 'PNG')); } else { return array('JPEG', 'GIF', 'PNG'); } } /** * This is used to set the default validators * * We are in a catch-22 here. If we set it in hook_elements, * the chances are that these will be overridden by any user * defined values. If we set these in the #process callback, * the options are not available to form_type_"hook"_value * or theme functions. * * As such, we dynamically add these here. */ function _upload_element_add_file_validators(&$element) { // Add the default validators. if ($element['#type'] == 'image_upload_element') { $element['#file_validators']['file_validate_is_image'] = array(); } // Check user defined max size is not greater than form/post size. $max = file_upload_max_size(); if (isset($element['#file_validators']['file_validate_size'])) { if ($element['#file_validators']['file_validate_size'][0] > $max) { $element['#file_validators']['file_validate_size'] = array($max); } } else { $element['#file_validators']['file_validate_size'] = array($max); } } /** * Private helper function for to delete files during the save process * * @param $fid The fid of a file object to delete. */ function _upload_element_delete($fid) { $result = db_query('SELECT * FROM {files} WHERE fid = %d', $fid); if ($file = db_fetch_object($result)) { if (file_exists($file->filepath)) { // If files that exist cannot be deleted, log it for manual deletion. if (!file_delete($file->filepath)) { watchdog('upload_element', 'Could not delete file "%path".', array('%path' => $file->filepath), 'error'); } } else { watchdog('upload_element', 'Attempting to delete missing file "%path".', array('%path' => $file->filepath), 'error'); } } db_query('DELETE FROM {files} WHERE fid = %d', $fid); }