<?php
// $Id: file_attach.module,v 1.49 2009/10/16 12:43:39 miglius Exp $

/**
 * @file
 * Allows attaching uploaded files to nodes and comments
 */

//////////////////////////////////////////////////////////////////////////////

define('FILE_ATTACH_REUSE',            variable_get('file_attach_reuse', 1));
define('FILE_ATTACH_REUSE_NODES',      variable_get('file_attach_reuse_nodes', '0'));
define('FILE_ATTACH_SHOW_ATTACHMENTS', variable_get('file_attach_show_attachments', 1));
define('FILE_ATTACH_SHOW_PREVIEWS',    variable_get('file_attach_show_previews', 1));
define('FILE_ATTACH_VOCABULARIES_ALL', variable_get('file_attach_vocabularies_all', 1));
define('FILE_ATTACH_VOCABULARIES',     serialize(variable_get('file_attach_vocabularies', array())));
define('FILE_ATTACH_OG_INHERITANCE',   variable_get('file_attach_og_inheritance', 0));
define('FILE_ATTACH_OG_VOCABULARIES',  variable_get('file_attach_og_vocabularies', 0));
define('FILE_ATTACH_POPUP_SIZE',       variable_get('file_attach_popup_size', '590x500'));
define('FILE_ATTACH_POPUP_PER_PAGE',   variable_get('file_attach_popup_per_page', '6'));

define('FILE_ATTACH_TITLE_LENGTH', 50);

//////////////////////////////////////////////////////////////////////////////
// Core API hooks

/**
 * Implementation of hook_theme().
 */
function file_attach_theme() {
  return array(
    'file_attachments' => array(
      'arguments' => array('files' => NULL, 'pid' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
    'file_attach_form_current' => array(
      'arguments' => array('form' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
    'file_attach_form_new' => array(
      'arguments' => array('form' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
    'file_attach_show' => array(
      'arguments' => array('id' => NULL, 'data' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
    'file_attach_backreferences' => array(
      'arguments' => array('nodes' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
    'file_attach_autocomplete' => array(
      'arguments' => array('matches' => NULL),
      'file' => 'file_attach.theme.inc'
    ),
  );
}

/**
 * Implementation of hook_perm().
 */
function file_attach_perm() {
  return array('attach files', 'view attached files');
}

/**
 * Implementation of hook_menu().
 */
function file_attach_menu() {
  return array(
    'admin/settings/file/attach' => array(
      'title' => 'Attachments',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('file_attach_admin_settings'),
      'access arguments' => array('administer site configuration'),
      'file' => 'file_attach.admin.inc',
    ),
    'file_attach/js' => array(
      'page callback' => 'file_attach_js',
      'access arguments' => array('attach files'),
      'type' => MENU_CALLBACK,
    ),
    'file_attach/autocomplete' => array(
      'page callback' => 'file_attach_autocomplete',
      'access arguments' => array('attach files'),
      'type' => MENU_CALLBACK,
    ),
  );
}

/**
 * Implementation of hook_block().
 */
function file_attach_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $block = array(
        array(
          'info' => t('File references'),
          'status' => 1,
          'region' => 'right',
          'weight' => -3,
        ),
      );
      return $block;
    case 'view':
      switch ($delta) {
        case 0:
          $block['subject'] = t('File references');
          $block['content'] = _file_attach_load_backreferences();
          break;
      }
      return $block;
  }
}

/**
 * Implementation of hook_init().
 */
function file_attach_init() {
  if (isset($_POST['form_id']) && $_POST['form_id'] == 'comment_form' && isset($_POST['op'])) {
    // AHAH sets #cache to TRUE and then hook_form_alter()
    // is not executed.
    // This is the only place to save the upload and alter the saved
    // cached form to add the latest preview.

    // Following include might be not loaded yet.
    require_once drupal_get_path('module', 'file') .'/file.inc';

    // Load the form from the Form API cache.
    $cache = cache_get('form_'. $_POST['form_build_id'], 'cache_form');

    $form_state = array('values' => $_POST);
    unset($form_state['values']['files']['upload']);

    // Handle new uploads, and merge tmp files into node-files.
    $form = array();
    file_attach_form_submit($form, $form_state);

    $files = array();
    if (isset($form_state['values']['files'])) {
      foreach ($form_state['values']['files'] as $fid => $file) {
        $files[$fid] = (object)$file;
      }
    }
    $object->files = $files;
    $form = _file_attach_form($object);

    // Expand attachments.
    if (!empty($files)) {
      $cache->data['attachments']['#collapsed'] = 0;
    }

    // Adding Save button.
    $cache->data['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 19);

    // Building preview. form_builder() changes the arguments,
    // so therefore pssing the copies of them.
    $form_state_tmp = $form_state;
    $form_tmp = form_builder('comment_form', $cache->data, $form_state_tmp);
    unset($form_tmp['#suffix']);
    $form_state['values'] = array_merge($form_state_tmp['values'], $form_state['values']);
    $form_tmp = comment_form_add_preview($form_tmp, $form_state);
    $cache->data['comment_preview'] = $form_tmp['comment_preview'];
    $cache->data['comment_preview_below'] = $form_tmp['comment_preview_below'];
    $cache->data['#suffix'] = $form_tmp['#suffix'];

    // Add the new element to the stored form state and resave.
    $cache->data['attachments']['wrapper'] = is_array($cache->data['attachments']['wrapper']) ? array_merge($cache->data['attachments']['wrapper'], $form) : $form;
    cache_set('form_'. $_POST['form_build_id'], $cache->data, 'cache_form', $cache->expire);

    // Add embed file attachments link.
    if (module_exists('file_embed') && !empty($form_state['values']['cid'])) {
      $node = node_load($form_state['values']['nid']);
      file_embed_textarea_process(array('element' => 'comment', 'type' => $node->type, 'nid' => $form_state['values']['nid'], 'cid' => $form_state['values']['cid']));
    }
  }
}

/**
 * Implementation of hook_link().
 */
function file_attach_link($type, $node, $teaser = NULL) {
  $links = array();
  // Display a link with the number of attachments when the node teaser is shown
  if (isset($teaser) && $type == 'node' && !empty($node->files) && user_access('view attached files')) {
    $num_files = isset($node->files) ? count($node->files) : 0;
    if ($num_files) {
      $type = node_get_types('type', $node);
      $links['attachments'] = array(
        'title' => format_plural($num_files, '1 attachment', '@count attachments'),
        'href' => 'node/'. $node->nid,
        'fragment' => 'attachments',
        'attributes' => array('title' => t('Read full') .' '. t($type->name) .' '. t('to view attachments.')),
      );
    }
  }

  return $links;
}

/**
 * Implementation of hook_form_alter().
 */
function file_attach_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'node_type_form') {

    // Node type configuration pages.
    $type = $form['#node_type']->type;
    $form['attachment'] = array(
      '#type' => 'fieldset', '#title' => t('File attachments'),
      '#collapsible' => TRUE, '#collapsed' => TRUE
    );
    $form['attachment']['file_attach_node'] = array(
      '#type' => 'radios', '#title' => t('Node file attachments'),
      '#default_value' => variable_get('file_attach_node_'. $type, 1),
      '#options' => array(t('Disabled'), t('Enabled')),
    );
    $form['attachment']['file_attach_comment'] = array(
      '#type' => 'radios', '#title' => t('File attachments on comments'),
      '#default_value' => variable_get('file_attach_comment_'. $type, 1),
      '#options' => array(t('Disabled'), t('Enabled')), '#weight' => 20,
    );
  }
  else if (isset($form['type']) && isset($form['#node'])) {

    // Node form.
    $node = $form['#node'];
    if ($form['type']['#value'] .'_node_form' == $form_id && variable_get('file_attach_node_'. $node->type, 1)) {
      _file_attach_form_fieldset($form, $node);
    }
  }
  else if ($form_id == 'comment_form') {

    // Comments API does not have a 'prepare' handler, so here we have
    // to clean a session from old files when user navigated away from the
    // comment preview page with unsaved files.
    if (!isset($_POST)) {
      $_SESSION['file_attach_files'] = array();
    }

    // Comment form. It does not execute file_attach_form_submit function
    // when #submit form element is set. Have to do it manually from here.
    $node = node_load($form['nid']['#value']);
    if (variable_get('file_attach_comment_'. $node->type, 1) && user_access('attach files')) {
      $cid = $form['cid']['#value'];
      $object_files = isset($cid) ? _file_attach_load($node->nid, $node->vid, $cid) : array();

      // Handle new uploads, and merge tmp files into node-files.
      if (!empty($form_state['post'])) {
        $form_state['values'] = $form_state['post'];
        file_attach_form_submit($form, $form_state);
      }

      $object = (object)array('files' => empty($form_state['post']) ? $object_files : $form_state['values']['files']);
      _file_attach_form_fieldset($form, $object);
    }
  }
}

/**
 * Generates attachments fieldset and adds it to the form.
 *
 * @param $form
 *   A form array.
 * @param $files
 *   An array of files which are already attached. They will stay at the top of the list.
 * @param $files_current
 *   An array with thw user input showing which attachmwnts are marked fordeletion.
 */
function _file_attach_form_fieldset(&$form, $object) {
  if (!user_access('attach files'))
    return;

  // File attachments fieldset
  $form['attachments'] = array(
    '#type'        => 'fieldset',
    '#title'       => t('File attachments'),
    '#collapsible' => TRUE,
    '#collapsed'   => empty($object->files),
    //'#description' => t('Changes made to the attachments are not permanent until you save this post. "Delete" only removes the attachment from the node, but does not delete the attached file. The first "listed" file will be included in RSS feeds.'),
    '#prefix'      => '<div class="attachments">',
    '#suffix'      => '</div>',
    '#weight'      => 1,
  );

  $form['attachments']['wrapper'] = array(
    '#prefix' => '<div id="file-attach-wrapper">',
    '#suffix' => '</div>',
  );
  $form['attachments']['wrapper'] += _file_attach_form($object);
  $form['#attributes']['enctype'] = 'multipart/form-data';
  $form['#submit'][] = 'file_attach_form_submit';
}

/**
 * Generates form inside attachments fieldset.
 *
 * @param $object
 *   A node or comment object
 *
 * @return
 *   A form array.
 */
function _file_attach_form($object) {

  $form = array(
    '#theme' => 'file_attach_form_new',
    //'#cache' => TRUE,
  );

  $form['files'] = array(
    '#theme' => 'file_attach_form_current',
    '#tree' => TRUE,
  );

  if (!empty($object->files)) {
    foreach ($object->files as $fid => $file) {
      $file = (object)$file;

      // Nodes and comments are cached differently. Nodes
      // needs #default_value', but comments - #value.
      $value = isset($object->type) ? '#default_value' : '#value';
      $form['files'][$fid] = array(
        'nid'      => array('#type' => 'value', '#value' => $file->nid),
        'vid'      => array('#type' => 'value', '#value' => $file->vid),
        'sid'      => array('#type' => 'value', '#value' => isset($file->sid) ? $file->sid : NULL),
        'uri'      => array('#type' => 'value', '#value' => $file->uri),
        'name'     => array('#type' => 'textfield', $value => $file->name, '#size' => 40, '#maxlength' => 256, '#disabled' => isset($file->nochange) ? TRUE : FALSE),
        'nochange' => array('#type' => 'value', '#value' => isset($file->nochange) ? $file->nochange : NULL),
        'type'     => array('#type' => 'value', '#value' => $file->type),
        'size'     => array('#type' => 'value', '#value' => $file->size),
        'remove'   => array('#type' => 'checkbox', $value => !empty($file->remove)),
        'list'     => array('#type' => 'checkbox', $value => $file->list),
        'weight'   => array('#type' => 'weight', '#delta' => count($object->files), $value => $file->weight),
      );
    }
  }

  if (user_access('attach files')) {
    if (module_exists('file_restriction'))
      file_restriction_js('#edit-upload');

    $form['file_attach_more']['#weight'] = 10;
    $form['file_attach_more']['upload'] = array(
      '#type' => 'file',
      '#title' => t('Attach new file'),
      '#description' => module_exists('file_restriction') ? implode(' ', file_restriction_description()) : t('The maximum upload size is %filesize.', array('%filesize' => format_size(file_upload_max_size()))),
      '#size' => 40,
    );
    $form['file_attach_more']['attach'] = array(
      '#type' => 'submit',
      '#value' => t('Update attachments'),
      '#name' => 'attach',
      '#ahah' => array(
        'path' => 'file_attach/js',
        'wrapper' => 'file-attach-wrapper',
        'progress' => array('type' => 'bar', 'message' => t('Please wait...')),
      ),
      '#submit' => array('node_form_submit_build_node'),
      '#prefix' => '<div id="file-attach-update-attachments">',
      '#suffix' => '</div>',
    );

    if (FILE_ATTACH_REUSE) {
      $form['attach_saved_description'] = array(
        '#type' => 'item',
        '#title' => t('Attach existing file'),
        '#description' => t('Put a file node id in square brackets, like [123], or type in a file name for the selection.'),
      );
      $form['attach_saved'] = array(
        '#type' => 'textfield',
        '#autocomplete_path' => 'file_attach/autocomplete',
        '#maxlength' => 255,
      );

      if (module_exists('file_gallery')) {
        drupal_add_js(drupal_get_path('module', 'file_attach') .'/file_attach.js');
        drupal_add_js(array('file_attach' => array(
          'popupTitle' => t('Attach an existing file'),
        )), 'setting');
        list($width, $height) = explode('x', FILE_ATTACH_POPUP_SIZE);
        $query = array('module' => 'file_attach', 'layout' => 'off');
        $query += array('TB_iframe' => 'true', 'height' => $height, 'width' => $width);
        $query = array_merge(file_attach_file_gallery('query'), $query);
        $form['attach_saved_description']['#description'] .= t(' You can also click on "Browse..." to choose a file.');

        $form['attach_browse'] = array(
          '#type'  => 'submit',
          '#value' => t('Browse...'),
        );
        $form['attach_browse_url'] = array('#type' => 'hidden', '#value' => url('file_gallery', array('query' => $query)));

        // Add stylesheets and javascript. Thickbox must come before the module's stylesheet:
        drupal_add_css(drupal_get_path('module', 'file') .'/jquery/thickbox/thickbox.css', 'module');
        drupal_add_js(drupal_get_path('module', 'file') .'/jquery/thickbox/thickbox.js', 'module');

        // Cluetip for metadata.
        drupal_add_css(drupal_get_path('module', 'file') .'/jquery/cluetip/jquery.cluetip.css', 'module');
        drupal_add_js(drupal_get_path('module', 'file') .'/jquery/cluetip/jquery.dimensions-1.2.js', 'module');
        drupal_add_js(drupal_get_path('module', 'file') .'/jquery/cluetip/jquery.cluetip.js', 'module');

        drupal_add_js('tb_pathToImage='. drupal_to_js('/'. drupal_get_path('module', 'file') .'/jquery/thickbox/loadingAnimation.gif') .';', 'inline');
      }
    }
  }
  return $form;
}

/**
 * Save new uploads and store them in the session to be associated to the node or comment.
 *
 * @param $form
 *   A form array.
 * @param $form_state
 *   User ubmitted data.
 */
function file_attach_form_submit($form, &$form_state) {
  global $user;

  // Loop through submitted data and fill in missing
  // default values, such as bitcache URI, etc.
  if (isset($form_state['values']['files'])) {
    foreach ($form_state['values']['files'] as $fid => $file) {

      // Make sure that all submitted form elements are set, otherwise they
      // will be overwritten with the default values.
      $file['list'] = isset($file['list']) ? $file['list'] : 0;
      $file['remove'] = isset($file['remove']) ? $file['remove'] : 0;

      if (preg_match('/^n_(\d+)$/', $fid, $matches)) {
        $node = node_load($matches[1]);
        $form_state['values']['files'][$fid] = array_merge((array)$node->file, $file);
        if (!node_access('update', $node)) {
          $form_state['values']['files'][$fid]['nochange'] = 1;
        }
      }
      else {
        $form_state['values']['files'][$fid] = array_merge((isset($_SESSION['file_attach_files']) && isset($_SESSION['file_attach_files'][$fid])) ? (array) $_SESSION['file_attach_files'][$fid] : array(), $file);
      }
    }
  }

  // Handle the file uploads. The do cycle is needed if we use several file
  // upload elements when ahah submission is disabled.
  if (($user->uid == 1 || user_access('attach files')) && ($upload = file_save_upload('upload', file_get_validators()))) {
    $node = (object)array('nosave' => TRUE);
    if (!file_node_save($node, $upload)) {
      drupal_set_message(t("Error saving file %file. Please, contact site administrator.", array('%file' => $upload->filename)), 'error');
    }
    else {

      // Find the next session id and save it with the uploaded file.
      // Remove 's_' when calculating next key.
      $node->file->sid = 's_'. (!empty($_SESSION['file_attach_files']) ? max(array_map(create_function('$a', 'return substr($a, 2);'), array_keys($_SESSION['file_attach_files']))) + 1 : '0');
      _file_attach_file($form_state, $node->file);
    }
  }

  // Attach already saved file node.
  if (isset($form_state['values']['attach_saved']) && preg_match('/^\[(\d+)\]/', $form_state['values']['attach_saved'], $matches)) {
    if (($node = node_load($matches[1])) && is_object($node->file)) {
      if (node_access('view', $node)) {
        $node->file->name = $node->title;
        if (!node_access('update', $node)) {
          $node->file->nochange = 1;
        }
        _file_attach_file($form_state, $node->file);
      }
      else {
        drupal_set_message(t("You don't have rights to access node with the nid=%nid.", array('%nid' => $matches[1])), 'error');
      }
    }
    else {
      drupal_set_message(t("There is no file node with the nid=%nid.", array('%nid' => $matches[1])), 'error');
    }
  }
  else if (!empty($form_state['values']['attach_saved'])) {
    drupal_set_message(t("A file name %file was no found.", array('%file' => $form_state['values']['attach_saved'])), 'error');
  }

  // Order the form according to the set file weight values.
  if (!empty($form_state['values']['files'])) {
    $microweight = 0.001;
    foreach ($form_state['values']['files'] as $fid => $file) {

      // For the attached saved files the user does not have permision to change
      // node name the form name element is dissabled and the name is not passed
      // in the $form_state. Have to merge back the default name.
      if (isset($file['nochange']) && $file['nochange'] == 1) {
        $node = node_load($file['nid']);
        $form_state['values']['files'][$fid]['name'] = isset($node->title) ? $node->title : '';
      }

      $form_state['values']['files'][$fid]['#weight'] = $file['weight'] + $microweight;
      $microweight += 0.001;
    }
    uasort($form_state['values']['files'], 'element_sort');
  }
}

/**
 * Attaches a file to the current files list.
 *
 * @param $form_state
 *   A reference to the form state array.
 * @param $file
 *   A file object to be attached.
 */
function _file_attach_file(&$form_state, $file) {
  // We do not allow the same file to be uploaded several times.
  $is_uploaded = FALSE;
  if (isset($form_state['values']['files'])) {
    foreach ($form_state['values']['files'] as $fid => $file_attached) {
      if ($file_attached['uri'] == $file->uri) {
        $is_uploaded = TRUE;
        drupal_set_message(t("The file %file is already attached.", array('%file' => $file->name)), 'notice');
        break;
      }
    }
  }
  if (!$is_uploaded) {
    // Add default list and weight parameters.
    $file->list = 1;
    $file->weight = isset($form_state['values']['files']) ? count($form_state['values']['files']) + 1 : 1;

    $fid = isset($file->sid) ? $file->sid : 'n_'. $file->nid;
    $_SESSION['file_attach_files'][$fid] = $file;
    $form_state['values']['files'][$fid] = (array)$file;
  }
}

/**
 * Menu callback for AHAH additions.
 */
function file_attach_js() {

  // Load the form from the Form API cache.
  $cache = cache_get('form_'. $_POST['form_build_id'], 'cache_form');

  $object = (object)array();
  if (isset($cache->data['type']['#value'])) {
    $object->type = $cache->data['type']['#value'];
  }

  $form = $cache->data;
  $form_state = array('values' => $_POST);
  unset($form_state['values']['files']['upload']);

  // Handle new uploads, and merge tmp files into node-files.
  file_attach_form_submit($form, $form_state);

  $files = array();
  if (isset($form_state['values']['files'])) {
    foreach ($form_state['values']['files'] as $fid => $file) {
      $files[$fid] = (object)$file;
    }
  }
  $object->files = $files;
  $form = _file_attach_form($object);

  // Add the new element to the stored form state and resave.
  $cache->data['attachments']['wrapper'] = array_merge($cache->data['attachments']['wrapper'], $form);
  cache_set('form_'. $_POST['form_build_id'], $cache->data, 'cache_form', $cache->expire);

  // Render the form for output.
  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
    '#tree' => FALSE,
    '#parents' => array(),
  );

  // Remember file gallery page.
  if (FILE_ATTACH_REUSE && module_exists('file_gallery')) {
    $form['attach_browse_url']['#value'] = $form_state['values']['attach_browse_url'];
  }

  drupal_alter('form', $form, array(), 'file_attach_js');
  $form_state = array('submitted' => FALSE);
  $form = form_builder('file_attach_js', $form, $form_state);
  $output = theme('status_messages') . drupal_render($form);

  if (module_exists('file_restriction'))
    $output .= file_restriction_js('#edit-upload', array(), TRUE);

  // We send the updated file attachments form.
  // Don't call drupal_json(). ahah.js uses an iframe and
  // the header output by drupal_json() causes problems in some browsers.

  print drupal_to_js(array('status' => TRUE, 'data' => $output));
  exit;
}

/**
 * Menu callback for AJAX additions.
 */
function file_attach_autocomplete($string = '') {
  $string = trim($string);
  $string = preg_replace('/^(\[\d+\]\s*)/', '', $string);
  $matches = array();

  if (drupal_strlen($string) > 2) {
    $result = db_query(db_rewrite_sql("SELECT n.nid, fn.type, fn.size FROM {node} n INNER JOIN {file_nodes} fn ON n.nid = fn.nid WHERE n.type = 'file' AND LOWER(n.title) LIKE LOWER('%%%s%%')"), $string);
    $i = 0;
    while (($row = db_fetch_object($result)) && $i < 10) {
      $node = node_load($row->nid);
      if (node_access('view', $node)) {
        $matches[] = array(
          'nid'     => $node->nid,
          'title'   => check_plain($node->title),
          'created' => $node->created,
          'changed' => $node->changed,
          'type'    => $row->type,
          'size'    => $row->size,
          'uid'     => $node->uid,
        );
        $i++;
      }
    }
  }
  drupal_json(theme('file_attach_autocomplete', $matches));
}

//////////////////////////////////////////////////////////////////////////////
// Node API hooks

/**
 * Implementation of hook_nodeapi().
 */
function file_attach_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'load':
      if (variable_get('file_attach_node_'. $node->type, TRUE) == TRUE) {
        return array('files' => _file_attach_load($node->nid, $node->vid));
      }
      break;

    case 'prepare':
      // Initialize $_SESSION['file_attach_files'] if no post occurred.
      // This clears the variable from old forms and makes sure it
      // is an array to prevent notices and errors in other parts.
      if (!isset($_POST)) {
        $_SESSION['file_attach_files'] = array();
      }
      break;

    case 'validate':
      // Validate that the file descriptions are not empty.
      if (!empty($node->files)) {
        foreach ($node->files as $fid => $file) {
          $name = trim($file['name']);
          if (empty($name) && empty($file['nochange'])) {
            form_set_error('files]['. $fid .'][name', t('A file description can not be empty.'), 'error');
          }
        }
      }
      break;

    case 'view':
      // For the view $a3 is a $teaser and $a4 is a $page variables. When neither is defined
      // we have a preview page.
      if (!empty($node->files) && user_access('view attached files')) {
        if (!$a3) {

          // There is no teaser so we are showing attachment list and file previews.
          $content = _file_attach_view($node);
          $node->content = is_array($node->content) ? array_merge($node->content, $content) : $content;
        }
      }
      break;

    case 'insert':
    case 'update':
      if (user_access('attach files') && variable_get('file_attach_node_'. $node->type, TRUE)) {
        // nodeapi hooks can be fired several times, but
        // the attachments should be saved only once.
        static $file_attach_saved = FALSE;
        if (!$file_attach_saved) {
          _file_attach_save($node);
          $file_attach_saved = TRUE;
        }
      }
      break;

    case 'delete':
      if ($node->type == 'file' && is_object($node->file)) {
        db_query('DELETE FROM {file_attachments} WHERE fnid = %d', $node->file->nid);
      }
      // Deletes all attachment relationships for this node and its comments.
      db_query('DELETE FROM {file_attachments} WHERE nid = %d', $node->nid);
      break;

    case 'delete revision':
      db_query('DELETE FROM {file_attachments} WHERE nid = %d AND vid = %d', $node->nid, $node->vid);
      break;

    case 'search result':
      return isset($node->files) ? format_plural(count($node->files), '1 attachment', '@count attachments') : NULL;

    case 'rss item':
      if ($node->type != 'file' && !empty($node->files)) {
        // RSS only allows one enclosure per item
        $file = array_shift($node->files);
        return array(array('key' => 'enclosure', 'attributes' => array('url' => url('file/'. $file->nid .'/download', array('absolute' => TRUE)), 'length' => $file->size, 'type' => $file->type)));
      }
      return array();
  }
}

//////////////////////////////////////////////////////////////////////////////
// Comment API hooks

/**
 * Implementation of hook_comment().
 */
function file_attach_comment(&$a1, $op) {
  switch ($op) {
    case 'form':
      // This operation is not working in D6. The form altering code is moved to
      // hook_form_alter().
      break;

    case 'validate':
      // $a1 is a comment array with a user input.

      // Validate that the file descriptions are not empty.
      if (!empty($a1['files'])) {
        foreach ($a1['files'] as $fid => $file) {
          $name = trim($file['name']);
          if (empty($name) && empty($file['nochange'])) {
            form_set_error('files]['. $fid .'][name', t('A file description can not be empty.'), 'error');
          }
        }
      }
      break;

    case 'insert':
    case 'update':
      // $a1 is a comment array with a user input.

      $node = node_load($a1['nid']);
      if (user_access('attach files') && variable_get('file_attach_comment_'. $node->type, TRUE)) {
        _file_attach_save($node, (object)$a1);
      }
      break;

    case 'delete':
      // $a1 is a comment object.
      db_query('DELETE FROM {file_attachments} WHERE cid = %d', $a1->cid);
      break;

    case 'view':
      // $a1 is a comment object.
      if (user_access('view attached files')) {

        // There is no 'load' operation for the comments. Have to load files manually.
        if (empty($a1->files) && isset($a1->cid)) {
          $node = node_load($a1->nid);

          $a1->files = _file_attach_load($a1->nid, $node->vid, $a1->cid);
        }
        foreach (_file_attach_view($a1) as $key => $data) {
          $a1->comment .= $data['#value'];
        }
      }
      break;

  }
}

//////////////////////////////////////////////////////////////////////////////
// Node API callbacks

/**
 * Loads files which are attached to the nodes or comments.
 *
 * @param $nid
 *   An node ID.
 * @param $vid
 *   An node version ID.
 * @param $cid
 *   An comment ID.
 *
 * @return
 *   An array of the attached file objects.
 */
function _file_attach_load($nid, $vid, $cid = 0) {
  $files = array();
  $args = array_merge(array($cid, $nid), $cid == 0 ? array($vid) : array());
  $result = db_query('SELECT fa.* FROM {file_attachments} fa WHERE fa.cid = %d AND fa.nid = %d'. ($cid == 0 ? ' AND fa.vid = %d' : '') .' ORDER BY fa.weight', $args);
  while ($row = db_fetch_object($result)) {
    if ($cid > 0 && isset($files['n_'. $row->fnid])) {
      // Since comments are not tied to the node revisions, we need to
      // use the latest node revision available for a correct weight.
      if ($row->vid > $files['n_'. $row->fnid]->vid)
        unset($files['n_'. $row->fnid]);
      else
        continue;
    }

    $node_file = node_load($row->fnid);
    if (node_access('view', $node_file)) {
      $extras = array('name' => $node_file->title, 'list' => $row->list, 'weight' => $row->weight);
      if (!node_access('update', $node_file)) {
        $extras['nochange'] = 1;
      }
      $files['n_'. $row->fnid] = (object)array_merge((array)$node_file->file, $extras);
    }
  }
  return $files;
}

/**
 * Displays an attachments list, file previews and generated (converted) files.
 *
 * @param $object
 *   A node or comment object. Shuld have files and files_current properties.
 *
 * @return
 *   Returns an array which can be put into $node->content.
 */
function _file_attach_view($object) {
  drupal_add_css(drupal_get_path('module', 'file_attach') .'/file_attach.css', 'module');

  $pid = isset($object->type) ? 0 : $object->cid;
  $files = array();

  // Hide not listed or marked for deletion files.
  if (isset($object->files)) {
    foreach ($object->files as $file) {
      if (!empty($file->list) && empty($file->remove)) {
        $files[] = (object)$file;
      }
    }
  }

  // Add the attachments list to node body.
  if (!empty($files)) {
    // Attachments list.
    if (FILE_ATTACH_SHOW_ATTACHMENTS) {
      $content['attachments'] = array(
        '#value' => theme('file_attachments', $files, $pid),
        '#weight' => 1,
      );
    }
    // Add attachment previews. We need to ensure that each file on each
    // commnet gets a unique id to make sure jquery is not confused.
    if (FILE_ATTACH_SHOW_PREVIEWS) {
      $content['files_container_start_'. $pid] = array(
        '#value' => '<div id="file-container-'. $pid .'">',
        '#weight' => 2,
      );
      $id = 0;
      foreach ($files as $file) {
        $data = array(
          'preview' => FILE_SHOW_PREVIEWS ? file_render_previews($file, 'files-'. $pid .'-'. $id) : '',
          'generated' => FILE_SHOW_GENERATED ? file_render_generated($file, 'files-'. $pid .'-'. $id) : '',
          'file' => FILE_SHOW_FILE ? theme('file_render', $file) : '',
        );
        $data = theme('file_show', $data);
        if (!empty($data)) {
          $content['files_'. $pid .'_'. $id] = array(
            '#value' => theme('file_attach_show', $pid .'-'. $id, $data),
            '#weight' => $id + 3,
          );
        }
        else {
          drupal_add_js('$(document).ready(function() { $("#file-attach-preview-button-'. $pid .'-'. $id .'").hide(); });', 'inline');
        }
        $id++;
      }
      $content['files_container_end_'. $pid] = array(
        '#value' => '</div>',
        '#weight' => $id + 3,
      );
    }
  }
  return isset($content) ? $content : array();
}

/**
 * Saves the attachments.
 *
 * @param $node
 *   A node object.
 * @param $comment
 *   A comment object.
 */
function _file_attach_save($node, $comment = NULL) {
  $files = isset($comment) ? (isset($comment->files) ? $comment->files : array()) : (isset($node->files) ? $node->files : array());
  $cid = isset($comment) ? $comment->cid : 0;
  if (empty($files))
    return;

  // An array of the file node nids currently attached to the node.
  $node_files = array();
  $result = db_query('SELECT fa.fnid FROM {file_attachments} fa WHERE fa.nid = %d AND fa.vid = %d AND fa.cid = %d', $node->nid, isset($node->old_vid) ? $node->old_vid : $node->vid, $cid);
  while ($row = db_fetch_object($result)) {
    $node_files[] = $row->fnid;
  }

  foreach ($files as $file) {
    // Convert file to object for compatibility
    $file = (object)$file;

    // Remove file. Process removals first since no further processing
    // will be required.
    if (!empty($file->remove)) {
      if (is_numeric($file->nid)) {
        db_query("DELETE FROM {file_attachments} WHERE nid = %d AND vid = %d AND cid = %d AND fnid = %d", $node->nid, $node->vid, $cid, $file->nid);
        watchdog('file_attach', 'The file attachment with the nid=%fnid was removed from the node nid=%nid, cid=%cid.', array('%fnid' => $file->nid, '%nid' => $node->nid, '%cid' => $cid), WATCHDOG_NOTICE, isset($node->nid) ? l(t('view'), 'node/'. $node->nid) : NULL);
      }
      continue;
    }

    if (!empty($file->sid) || (!FILE_ATTACH_REUSE_NODES && is_numeric($file->nid) && !in_array($file->nid, $node_files))) {

      // Create new node.
      $file->name = trim($file->name);
      $file_node = (object)array('file' => $file);
      if (FILE_ATTACH_OG_INHERITANCE)
        _file_propagate_og($node, $file_node);
      if (!$comment)
        _file_propagate_terms($node, $file_node, array('all' => FILE_ATTACH_VOCABULARIES_ALL, 'og' => FILE_ATTACH_OG_VOCABULARIES, 'vocabs' => unserialize(FILE_ATTACH_VOCABULARIES)));
      $file_node = file_node_create((array)$file_node);
      $file_attachment = (object)array('fnid' => $file_node->nid, 'nid' => $node->nid, 'vid' => $node->vid, 'cid' => $cid, 'list' => $file->list, 'weight' => $file->weight);
      drupal_write_record('file_attachments', $file_attachment);
      watchdog('file_attach', 'The file attachment with the nid=%fnid was created for the node nid=%nid.', array('%fnid' => $file_node->nid, '%nid' => $node->nid), WATCHDOG_NOTICE, isset($node->nid) ? l(t('view'), 'node/'. $node->nid) : NULL);
    }
    else if (is_numeric($file->nid)) {

      // Update already saved files' data.
      $result = db_query('SELECT fa.fnid FROM {file_attachments} fa WHERE fa.fnid = %d AND fa.nid = %d AND fa.vid = %d AND fa.cid = %d', $file->nid, $node->nid, $node->vid, $cid);
      $file_update = (object)array('fnid' => $file->nid, 'nid' => $node->nid, 'vid' => $node->vid, 'cid' => $cid, 'list' => $file->list, 'weight' => $file->weight);
      drupal_write_record('file_attachments', $file_update, db_result($result) ? array('fnid', 'nid', 'vid', 'cid') : array());

      // Save a new file node title.
      $file_node = node_load($file->nid);
      if (node_access('update', $file_node)) {
        $file_node->title = trim($file->name);
        if (FILE_ATTACH_OG_INHERITANCE)
          _file_propagate_og($node, $file_node);
        if (!$comment)
          _file_propagate_terms($node, $file_node, array('all' => FILE_ATTACH_VOCABULARIES_ALL, 'og' => FILE_ATTACH_OG_VOCABULARIES, 'vocabs' => unserialize(FILE_ATTACH_VOCABULARIES)));
        node_save($file_node);
      }
    }
  }

  // Empty the session storage after save. We use this variable to track files
  // that haven't been related to the node yet.
  unset($_SESSION['file_attach_files']);
}

/**
 * Returns an array of nodes to which the given file record is attached.
 *
 * @return
 *   An array of nodes which reference a given file.
 */
function _file_attach_load_backreferences() {
  $nodes = array();
  if (arg(0) == 'node' && preg_match('/(\d+)/', arg(1), $matches)) {
    $file_node = node_load($matches[1]);
    if (is_object($file_node) && $file_node->type == 'file' && !empty($file_node->file)) {
      $result = db_query(db_rewrite_sql("SELECT n.* FROM {node} n INNER JOIN {file_attachments} fa  ON n.nid = fa.nid WHERE n.vid = fa.vid AND fa.list = '1' AND fa.fnid = %d"), $file_node->nid);
      while ($node = db_fetch_object($result)) {
        if (node_access('view', $node)) {
          $nodes[] = $node;
        }
      }
    }
  }
  return theme('file_attach_backreferences', $nodes);
}

//////////////////////////////////////////////////////////////////////////////
// File gallery integration

/**
 * Implementation of hook_file_gallery().
 */
function file_attach_file_gallery($op) {
  $args = array_slice(func_get_args(), 1); // skip $op.
  switch ($op) {
    case 'query':
      $query = $_GET;
      unset($query['q']);
      unset($query['page']);
      return $query;
    case 'node_link': return array('link' => 'file_attach/'. array_shift($args), 'attributes' => array('class' => 'file-attach-node'));
    case 'node_js': return "$(document).ready(function() { $('a.file-attach-node').click(function() { parent.$('#edit-attach-saved').val('[' + String(this).match(/\/([\d]+)$/)[1] + ']'); window.parent.tb_remove(); return false; }); });";
    case 'file_links': return array('show' => array(), 'open' => array(), 'view' => array(), 'attach' => array('title' => t('Attach'), 'href' => 'file_attach/'. array_shift($args), 'attributes' => array('title' => t('Attach file to the node.'), 'class' => 'file-attach-node')));
    case 'pager': return FILE_ATTACH_POPUP_PER_PAGE;
    case 'thickbox': return FALSE;
    case 'filter': return TRUE;
    case 'navigation': return TRUE;
    case 'show_tabs': return TRUE;
    case 'breadcrumb_home': return FALSE;
    case 'breadcrumb_root': return array('file_gallery/all' => t('All files'));
    case 'page_js': return "$(document).ready(function() { parent.$('#edit-attach-browse-url').val(String(parent.$('#edit-attach-browse-url').val()).replace(/(file_gallery.*)\?/, String(location.href).match(/(file_gallery.*)\?/)[1] + '?')); });";
  }
}

//////////////////////////////////////////////////////////////////////////////
// Services API hooks

/**
 * Implementation of hook_service().
 */
function file_attach_service() {
  return array(
    array(
      '#method' => 'file_attach.attach',
      '#callback' => 'file_attach_service_attach',
      '#access callback' => 'file_attach_service_attach_access',
      '#args' => array(
        array(
          '#name' => 'nid',
          '#type' => 'int',
          '#optional' => FALSE,
          '#description' => t('A parent node ID.'),
        ),
        array(
          '#name' => 'fnid',
          '#type' => 'int',
          '#optional' => FALSE,
          '#description' => t('A file node ID.'),
        ),
        array(
          '#name' => 'cid',
          '#type' => 'int',
          '#optional' => TRUE,
          '#description' => t('A comment ID.'),
        ),
      ),
      '#return' => 'boolean',
      '#help' => t('Attaches a file node to the parent node.'),
    ),
  );
}

/**
 * Access check.
 *
 * @param $nid
 *   A parent node ID.
 *
 * @return
 *   TRUE if access granted.
 */
function file_attach_service_attach_access($nid) {
  $node = node_load($nid);
  return user_access('attach files') && node_access('edit', $node);
}

/**
 * Attaches a file node to the parent node.
 *
 * @param $nid
 *   A parent node ID.
 * @param $fnid
 *   A file node ID.
 * @param $cid
 *   A comment ID.
 *
 * @return
 *   TRUE if a file was successfully attached.
 */
function file_attach_service_attach($nid, $fnid, $cid = 0) {
  $node = node_load($nid);
  if ($cid == 0 && !variable_get('file_attach_node_'. $node->type, 1) || $cid > 0 && !variable_get('file_attach_comment_'. $node->type, 1))
    return FALSE;

  if (!($file_node = node_load($fnid)) || $file_node->type != 'file')
    return FALSE;

  // File is already attached.
  if (db_result(db_query("SELECT fnid FROM {file_attachments} WHERE nid = %d AND vid = %d AND cid = %d AND fnid = %d", $node->nid, $node->vid, $cid, $file_node->nid)))
    return TRUE;

  $record = (object)array('nid' => $node->nid, 'vid' => $node->vid, 'cid' => $cid, 'fnid' => $file_node->nid, 'list' => 1);
  return !!drupal_write_record('file_attachments', $record);
}

