########################### upload_field.info ############################
; $Id$
name = FAPI Upload field
description = A FAPI element to handle file uploads.
package = Other
core = 6.x

########################### upload_field.module ############################

<?php
// $Id$

/**
 * @file upload_field.module
 * 'A module that provides a FAPI element to handle file uploads.
 */

define('UPLOAD_FIELD_NONE', 0);
define('UPLOAD_FIELD_NEW', 1);
define('UPLOAD_FIELD_DELETE', 2);
define('UPLOAD_FIELD_REPLACE', 3);

/**
 * Implementation of hook_help
 */
function upload_field_help($path, $arg) {
  switch ($path) {
    case 'admin/help#upload_field':
      $output = '<p>'. t('A module that provides a FAPI element to handle file uploads.') .'</p>';
      return $output;
  }
}

/**
 * Implementation of hook_theme().
 */
function upload_field_theme() {
  return array(
    'upload_field' => array(
      'arguments' => array('element' => NULL),
    ),
    'image_upload_field' => array(
      'arguments' => array('element' => NULL),
    ),
    'upload_field_preview' => array(
      'arguments' => array('file' => FALSE),
    ),
    'upload_field_image_preview' => array(
      'arguments' => array('file' => FALSE),
    ),
  );
}

/**
 * Implementation of hook_elements
 * 
 * Defines an file form element that is linked into the native Drupal file handling system.
 */
function upload_field_elements() {
  $type['upload_field'] = array(
    '#input' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#default_value' => '',
    '#process' => array('_upload_field_expand'),
    '#element_validate' => array('upload_field_element_validate'),
    '#file_formatter' => 'upload_field_preview',
    '#file_validators' => array(),
  );
  $type['image_upload_field'] = array(
    '#input' => TRUE,
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#default_value' => '',
    '#process' => array('_upload_field_expand'),
    '#element_validate' => array('upload_field_element_validate'),
    '#file_formatter' => 'upload_field_image_preview',
    '#file_validators' => array('file_validate_is_image' => array()),
  );
  return $type;
}

/**
 * Our process callback to expand the control.
 * 
 * There are two main roles performed, to rename the field name
 * into one that is usable by Drupal native file functionality,
 * and to add the extra attributes for fields with is_image set
 */
function _upload_field_expand($element) {
  $element['#original_name'] = $element['#name'];
  $element['#name'] = 'files['. $element['#name'] .']';
  return $element;
}


/**
 * This handles the uploading and workflow
 *
 * @param unknown_type $form
 * @param unknown_type $edit
 * @return unknown
 */
function form_type_image_upload_field_value($form, $edit = FALSE) {
  return form_type_upload_field_value($form, $edit);
}

/**
 * This handles the uploading and workflow
 *
 * @param unknown_type $form
 * @param unknown_type $edit
 * @return unknown
 */
function form_type_upload_field_value($form, $edit = FALSE) {
  if ($edit !== FALSE) {
    $field_name = upload_field_name($form);
    $key = $field_name .'_form_key';
    if (!array_key_exists($key, $form['#post'])) {
      form_set_error($field_name, t('Upload field form error, please try to load the form again. If this error persists, please contact the site administrator.'));
      return $form['#default_value'];
    }
    // handle the extras first
    $field_key = $form['#post'][$key];
    $action = $form['#post'][$field_name][$field_name .'_action'];
    if (is_object($form['#default_value'])) {
      if ($action == 'delete') {
        upload_field_session_handler('delete', $form, $field_key);
      }
      if ($action == 'restore') {
        upload_field_session_handler('restore', $form, $field_key);
      }
    }
    if ($action == 'revert') {
      upload_field_session_handler('revert', $form, $field_key);
    }
    $file = file_save_upload($field_name, $form['#file_validators']);
    return !empty($file) ? 
    upload_field_session_handler('store', $form, $field_key, $file) : upload_field_session_handler('value', $form, $field_key);
  }
  else {
    return $form['#default_value'];
  }
}

/**
 * The custom validation required for files that are marked
 * as deleted, but are required.
 *
 * @param array $element
 * @param array $form_state
 */
function upload_field_element_validate($element, &$form_state) {
  if ($element['#required'] && is_object($element['#value']) && property_exists($element['#value'], 'submit_action') && $element['#value']->submit_action == UPLOAD_FIELD_DELETE) {
    form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
  }
}

function upload_field_equals($a = FALSE, $b = FALSE) {
  if (is_object($a) && is_object($b)) {
    return $a->fid == $b->fid;
  }
  return FALSE;
}

/**
 * Theme function to format the output.
 *
 * We use the container-inline class so that all three of the HTML elements 
 * are placed next to each other, rather than on separate lines.
 */
function theme_image_upload_field($element) {
  return theme_upload_field($element);
}

/**
 * Theme function to format the output.
 *
 * We use the container-inline class so that all three of the HTML elements 
 * are placed next to each other, rather than on separate lines.
 */
function theme_upload_field($element) {
  $key_field = _upload_field_form_key_field($element);
  $action_field = _upload_field_action_field($element);
  $fieldset_attributes = array('class' => '');
  if ($element['#collapsible']) {
    drupal_add_js('misc/collapse.js');
    $fieldset_attributes['class'] .= ' collapsible';
    if ($element['#collapsed']) {
      $fieldset_attributes['class'] .= ' collapsed';
    }
  }
     
  _form_set_class($element, array('form-file'));
  $preview = '';
  if ($element['#file_formatter']) {
    $preview = theme($element['#file_formatter'], $element['#value']);
  }
  return '<fieldset'. drupal_attributes($fieldset_attributes) .'>'
      . ($element['#title'] ? '<legend>'. $element['#title'] .'</legend>' : '')
      . $preview
      .'<div id="'. $element['#id'] .'-wrapper" class="form-item">'
      . drupal_render($action_field)
      . drupal_render($key_field)
      .'<input type="file" name="'. $element['#name'] .'"'
      . ($element['#attributes'] ? ' '. drupal_attributes($element['#attributes']) : '') 
      .' id="'. $element['#id'] .'" size="'. $element['#size'] ."\" /></div>\n"
      . $element['#children'] ."</fieldset>\n";
  
}

function _upload_field_form_key_field($element) {
  $key = upload_field_name($element) .'_form_key';
  $field[$key]= array(
    '#type' => 'hidden',
    '#name' => $key,
    '#value' => _upload_field_key($key, $element['#post'], $element),
  );
  return $field;
}

function _upload_field_action_field($element) {
  $field = array();
  if (is_object($element['#value'])) {
    $field_name = upload_field_name($element);
    $key = $field_name .'_action';
    $field[$key]= array(
      '#type' => 'checkbox',
      '#id' => form_clean_id("edit-{$key}"),
      '#name' => "{$field_name}[{$key}]",
      '#value' => 0,
      '#weight' => -10,
    );
    if (upload_field_equals($element['#value'], $element['#default_value'])) {
      if (property_exists($element['#value'], 'submit_action') && $element['#value']->submit_action == UPLOAD_FIELD_DELETE) {
        $field[$key]['#return_value'] = 'restore';
        $field[$key]['#title'] = t('Restore original');
      }
      else {
        $field[$key]['#return_value'] = 'delete';
        $field[$key]['#title'] = t('Delete original');
        $field[$key]['#value'] = (property_exists($element['#value'], 'submit_action') 
              ? ($element['#value']->submit_action == UPLOAD_FIELD_DELETE ? 'delete' : 0)
              : 0);
      }
    }
    else {
      $field[$key]['#return_value'] = 'revert';
      $field[$key]['#title'] = (is_object($element['#default_value']) ? t('Discard replacment') : t('Discard upload'));
    }
  }
  return $field;
}

function theme_upload_field_preview($file = FALSE) {
  if ($file) {
    $class = 'upload-field-preview';
    if ($file->filemime) {
      list($base_mime, $extended_mime) = explode('/', $file->filemime);
      $class = trim($class .' mime-'. $base_mime .' '. ($extended_mime ? ' mime-'. $base_mime .'-'. $extended_mime : ''));
    }
    $size = ($file->filesize > 1048576) ? sprintf("%01.2f", $file->filesize / 1048576) .'MB' : sprintf("%01.2f", $file->filesize / 1024) .'KB';
    return '<div class="'. $class .'">'. check_plain($file->filename) .' ('. $size .')</div>';
  }
  else {
    return '<div class="upload-field-preview">---</div>';
  }
}

function theme_upload_field_image_preview($file = FALSE) {
  $src = $file ? 'upload_field/'. $file->fid : drupal_get_path('module', 'upload_field') .'/no_image.gif';
  return '<img src="'. base_path() . $src .'" alt="Image Preview" /><br/>';
}

function upload_field_save(&$file, $dest = 0, $replace = FILE_EXISTS_RENAME, $presetname = FALSE) {
  if (is_object($file)) {
    $base = file_directory_path();
    if (strstr($dest, $base) === FALSE) {
      $dest = $base . '/' . ltrim($dest, '/');
    }
    
    file_check_directory($dest, 1);
    if (!property_exists($file, 'submit_action')) {
      $file->submit_action = UPLOAD_FIELD_NONE; 
    }
    switch ($file->submit_action) {
      case UPLOAD_FIELD_NONE:
        return $file->fid;
      case UPLOAD_FIELD_DELETE:
        file_set_status($file, FILE_STATUS_TEMPORARY);
        return 0;
      case UPLOAD_FIELD_REPLACE:
        $_file = clone $file;
        $_file->fid = $file->original_fid;
        file_set_status($_file, FILE_STATUS_TEMPORARY);
        drupal_write_record('files', $_file);
        // fall through
      case UPLOAD_FIELD_NEW:
        $uploaded = FALSE;        
        if($presetname) {
          $destination = file_create_filename($file->filename, $dest);
          if( upload_field_imagecache_action($presetname, $file->filepath, $destination)) {
            $file->filepath = $destination;
            $uploaded = TRUE;
          }
        }
        if (!$uploaded) {
          file_move($file, $dest .'/'. $file->filename, FILE_EXISTS_RENAME);
        }
        if($uploaded) {
          $file->status = FILE_STATUS_PERMANENT;
          drupal_write_record('files', $file, 'fid');
          return $file->fid;
        }
        else {
          drupal_set_message(t('Error occured while saving the image!'), 'error');
          return 0;
        }
    }
  }
  return 0;
}

/**
 * Hooks into imageache preset for save action
 */
function upload_field_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;
}

/**
 * Implementation of hook_menu
 * 
 * This is for node editing only
 */
function upload_field_menu() {
  $items = array();
  $items['upload_field/%'] = array(
    'title' => 'Upload field preview',
    'file' => 'image.preview.inc',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access content'),
    'page callback' => 'upload_field_image_preview',
    'page arguments' => array(1),
  );
  return $items; 
}

function upload_field_preview($fid) {
  //$_SESSION['files']['upload_field'][FORM_FIELD_ID][FIELD_NAME]
  //$_SESSION['files']['upload_field'][FORM_FIELD_ID][FIELD_NAME . '_default']
  if (is_numeric($fid) && is_array($_SESSION['files']['upload_field'])) {
    foreach ($_SESSION['files']['upload_field'] as $form_field_key => $field) {
      if (is_array($field)) {
        foreach ($field as $file) {
          if (is_object($file) && $file->fid == $fid) {
            if (property_exists($file, 'submit_action') && $file->submit_action == UPLOAD_FIELD_DELETE) {
              return 'marked_delete';
            }
            return $file;
          }
        }
      }
    }
  }
  return 'no_image';
}

function upload_field_name($form) {
  return (array_key_exists('#original_name', $form)) ? $form['#original_name'] : $form['#name'];
}

function upload_field_session_handler($op, &$form, $field_key = FALSE, $file = '') {
  $field_name = upload_field_name($form);
  switch ($op) {
    case 'revert':
      $_SESSION['files']['upload_field'][$field_key][$field_name] = $_SESSION['files']['upload_field'][$field_key][$field_name .'_default'];    
      break;
    case 'value':
      $file = (isset($_SESSION['files']['upload_field'][$field_key][$field_name])) ? $_SESSION['files']['upload_field'][$field_key][$field_name] : $_SESSION['files']['upload_field'][$field_key][$field_name .'_default'];
      return is_object($file) ? $file : '';      
      break;
    case 'restore':
    case 'delete':
      $submit_action = ($op == 'restore') ? UPLOAD_FIELD_NONE : UPLOAD_FIELD_DELETE;
      $_SESSION['files']['upload_field'][$field_key][$field_name .'_default']->submit_action = $submit_action;
      unset($_SESSION['files']['upload_field'][$field_key][$field_name]);
      if (is_object($form['#default_value'])) {
        $form['#default_value']->submit_action = $submit_action;
      }
      break;
    case 'store':
      if (is_object(($_SESSION['files']['upload_field'][$field_key][$field_name .'_default']))) {
        $file->submit_action = UPLOAD_FIELD_REPLACE;
        $file->original_fid = $_SESSION['files']['upload_field'][$field_key][$field_name .'_default']->fid;
      }
      else {
        $file->submit_action = UPLOAD_FIELD_NEW;
      }
      $_SESSION['files']['upload_field'][$field_key][$field_name] = $file;
      return $file;
      break;
    case 'store default':
      $_SESSION['files']['upload_field'][$field_key][$field_name .'_default'] = $file;
      $_SESSION['files']['upload_field'][$field_key]['timestamp'] = time();
      return $file;
      break;
  }
}

/**
 * Parses the post array for a valid field key to distingish
 * between different forms that use the same upload_field.
 *
 * @param string $key Field name
 */
function _upload_field_key($key, $post = array(), $form = '') {
  if (isset($post[$key])) {
    return $post[$key];
  }
  else {
    if (empty($_SESSION['files']['upload_field']['last_known_key'])) {
      $_SESSION['files']['upload_field']['last_known_key'] = 1;
    }
    // set the session for formators that need this populated
    // on the original form display
    $key = $_SESSION['files']['upload_field']['last_known_key']++;
    upload_field_session_handler('store default', $form, $key, $form['#value']);
    return $key;
  }
}

########################### image.preview.inc ############################

<?php
// $Id$

/**
 * @file image.preview.inc
 * Handles image previews from both temperary and perminant file directories.
 */

/**
 * Generates a temp image for image preview
 * 
 * Using native image processes here as I didn't see a way to
 * use imagecache for similar functionality
 *
 * @param object $image_file
 */
function upload_field_image_preview($fid) {
  // TODO
  
  $image_file = upload_field_preview($fid);
  $image = $image_file->filepath;
  $info = image_get_info($image);
  if ($info) {
    // create temp file, minimise conflicts by instant touch
    $tmp_file = file_destination(file_directory_temp() .'/upload_field.'. $info['extension'], FILE_EXISTS_RENAME);
    if (!image_scale($image, $tmp_file, 75, 75)) {
      $tmp_file = $image;
    }
    $tmp_info = image_get_info($tmp_file);
    if ($tmp_info) {
      $headers = array(
        'Content-type: '. $tmp_info['mime_type'],
        'Content-Length: '. $tmp_info['file_size'],
        'Cache-Control: max-age=0, no-cache',
        'Expires: '. gmdate('D, d M Y H:i:s', time()) .' GMT',
        'Last-Modified: '. time(),
      );
      ob_end_clean();
      foreach ($headers as $header) {
        $header = preg_replace('/\r?\n(?!\t| )/', '', $header);
        drupal_set_header($header);
      }
    
      // Transfer file in 1024 byte chunks to save memory usage.
      if ($fd = fopen($tmp_file, 'rb')) {
        while (!feof($fd)) {
          print fread($fd, 1024);
        }
        fclose($fd);
      }
    }
    // We're using the original if it is too small - do not delete!
    if ($image != $tmp_file) {
      unlink($tmp_file);
    }    
  }
  if (!headers_sent()) {
    drupal_not_found();
  }
  exit;
}
