<?php
// $Id: inline.module,v 1.34 2008/08/17 14:43:39 sun Exp $

/**
 * @file
 * Provides a Drupal filter to render uploaded attachments inline.
 */

/**
 * Implementation of hook_menu().
 */
function inline_menu() {
  $items = array();
  $items['admin/settings/inline'] = array(
    'title' => 'Inline',
    'description' => 'Manage automatic and manual inclusion of attachments in the content of your posts.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('inline_settings'),
    'access arguments' => array('administer inline settings'),
  );
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function inline_perm() {
  return array('administer inline settings');
}

/**
 * Implementation of hook_help().
 */
function inline_help($path, $arg) {
  switch ($path) {
    case 'admin/help#inline':
      return t('<p>Sometimes a user may want to add an image or a file inside the body of a node. This can be done with special tags that are replaced by links to the corresponding uploaded file. If the file is an image, it will be displayed inline, otherwise a link to the file will be inserted. To enable this feature and learn the proper syntax, visit the <a href="!filters">filters configuration screen</a>.</p>', array('!filters' => url('admin/filters')));

    case 'filter#short-tip':
      return t('You may add links to files uploaded with this node <a href="!explanation-url">using special tags</a>', array('!explanation-url' => url('filter/tips', array('fragment' => 'image'))));

    case 'filter#long-tip':
      return t('<p>You may link to files uploaded with the current node using special tags. The tags will be replaced by the corresponding files. Syntax: <code>[inline:file_id]</code>. Parameter: file_id represents the file uploaded with the node in which to link, assuming that the first uploaded file is labeled as 1 and so on.</p>
    <p>If the file is an image, it will be displayed inline, otherwise a link to the file will be inserted.</p> ');
  }
}

/**
 * Inline settings form builder function.
 */
function inline_settings() {
  $form = array();

  // Check if Inline filter is enabled
  $inline_activated = FALSE;
  foreach (filter_formats() as $format) {
    foreach (filter_list_format($format->format) as $filter) {
      if ($filter->module == 'inline') {
        $inline_activated = TRUE;
        break 2;
      }
    }
  }
  if ($inline_activated == FALSE) {
    drupal_set_message(t('Inline filter is not yet enabled for at least one <a href="!formats">input format</a>.', array('!formats' => url('admin/settings/filters'))), 'error');
  }

  $form['inline']['upload']['image_link'] = array(
    '#type' => 'fieldset',
    '#title' => t('Image output'),
    '#collapsible' => TRUE,
    '#description' => t('<strong>Note:</strong> Images are only processed, if a tag is referencing them. However, there is a auto-inline feature to inline all uploaded images automatically. Auto-inline can be enabled for certain <a href="!content-types">content types</a>.', array('!content-types' => url('admin/content/types'))),
  );
  $form['inline']['upload']['image_link']['inline_link_img'] = array(
    '#type' => 'radios',
    '#title' => t('Link to images'),
    '#default_value' => variable_get('inline_link_img', 1),
    '#options' => array(
      '0' => t('Display image only'),
      '1' => t('Display image with a link to the image file')
    ),
  );

  $imagecache_path = '';
  $presets = array();
  if (module_exists('imagecache')) {
    // ImageCache v2 API.
    if (function_exists('imagecache_presets')) {
      $imagecache_path = url('admin/build/imagecache');
      $rules = imagecache_presets();
      foreach ($rules as $preset_id => $preset_info) {
        $presets[$preset_id] = $preset_info['presetname'];
      }
    }
    // ImageCache v1 API (deprecated).
    else {
      $imagecache_path = url('admin/settings/imagecache');
      $presets = _imagecache_get_presets();
    }
  }
  $form['inline']['upload']['image_scaling'] = array(
    '#type' => 'fieldset',
    '#title' => t('Image dimensions and scaling'),
    '#collapsible' => TRUE,
    '#description' => (module_exists('imagecache') ? t('Select the <a href="!presets">Imagecache presets</a> to use for inlined images.', array('!presets' => $imagecache_path)) : t('<strong>Note:</strong> If <a href="!imagecache">Imagecache</a> module is installed, Inline provides support for image scaling.', array('!imagecache' => url('http://drupal.org/project/imagecache')))),
  );

  // If Imagecache module exists and is enabled, we assume that we want to use
  // the improved image handling instead of our own.
  if ($presets) {
    $options     = array();
    $options[''] = 'No Imagecache processing';
    foreach ($presets as $id => $name) {
      $options[$name] = $name;
    }
    $form['inline']['upload']['image_scaling']['inline_teaser_preset'] = array(
      '#title' => t('Teaser preset'),
      '#description' => t('Select the Imagecache preset to use for inlined images in teaser view.'),
      '#type' => 'select',
      '#options' => $options,
      '#default_value' => variable_get('inline_teaser_preset', ''),
    );
    $form['inline']['upload']['image_scaling']['inline_full_preset'] = array(
      '#title' => t('Full preset'),
      '#description' => t('Select the Imagecache preset to use for inlined images in full view.'),
      '#type' => 'select',
      '#options' => $options,
      '#default_value' => variable_get('inline_full_preset', ''),
    );
  }
  else {
    $form['inline']['upload']['image_scaling']['inline_img_dim'] = array(
      '#type' => 'textfield',
      '#title' => t('Maximum width and height for inline images (format: XXX,YYY)'),
      '#size' => 10,
      '#maxlength' => 10,
      '#required' => TRUE,
      '#default_value' => variable_get('inline_img_dim', '150,150'),
      '#description' => t('This setting limits the dimensions of displayed images in pixels. They will not be resized. Images exceeding these dimensions are automatically not displayed.', array('!content-types' => url('admin/content/types'))),
    );
  }

  return system_settings_form($form);
}

/**
 * Implementation of hook_form_alter().
 *
 * Allows to enable/disable auto-inline support for each content type.
 */
function inline_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'node_type_form') {
    $node_type = $form['orig_type']['#value'];
    $form['workflow']['upload_inline'] = array(
      '#type' => 'radios',
      '#title' => t('Display attachments inline automatically'),
      '#default_value' => variable_get('upload_inline_'. $node_type, 0),
      '#options' => array(
        0 => t('Disabled'),
        1 => t('Only in teaser view'),
        2 => t('Only in body view'),
        3 => t('In teaser and body view')),
      '#description' => t('Choose whether uploaded images should be shown inline automatically. Make sure you set the dimensions at !settings_url', array('!settings_url' => l(t('inline settings'), 'admin/settings/inline'))),
    );
  }
}

/**
 * Implementation of hook_filter().
 *
 * Since Inline needs to know which files are attached to a processed node, the
 * original text is simply returned here.
 *
 * @see inline_nodeapi()
 */
function inline_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
      return array(0 => t('Inline file filter'));

    case 'description':
      return t('Substitutes [inline:xx] tags with the xxth file uploaded with the node.');

    case 'prepare':
      return $text;

    case 'process':
      return $text;
  }
}

/**
 * Implementation of hook_filter_tips().
 */
function inline_filter_tips($delta, $format, $long = FALSE) {
  if ($long) {
    return '<p><a id="filter-inline" name="filter-inline"></a>'. t('
      You may link to files uploaded with the current node using special tags. The tags will be replaced by the corresponding files. For example:

      Suppose you uploaded three files (in this order):
      <ul>
      <li>imag1.png (referred as file #1)
      <li>file1.pdf (referred as file #2)
      <li>imag2.png (referred as file #3)
      </ul>

      <pre>[inline:1=test]  or  [inline:imag1.png=test]</pre>
      will be replaced by <em><code>&lt;img src=imag1.png alt=test&gt;</code></em>

      <pre>[file:1=test]  or  [file:imag1.png=test]</pre>
      will be replaced by <em><code>&lt;a href=imag1.png&gt;test&lt;/a&gt;</code></em>

      <pre>[attachment:2=test]  or  [attachment:file1.pdf=test]</pre>
      will be replaced by <em><code>&lt;a href=file1.pdf.png&gt;test&lt;/a&gt;</code></em>') .'</p>';
  }
  else {
    return t('You may use <a href="!inline_help">[inline:xx] tags</a> to display uploaded files or images inline.', array('!inline_help' => url("filter/tips/$format", array('fragment' => 'filter-inline'))));
  }
}

/**
 * Implementation of hook_nodeapi().
 *
 * Substitutes Inline tags with the corresponding files or images in front of
 * node_view().
 * Replaces numeric file references in Inline tags (i.e. [inline:1]) with named
 * file references (i.e. [inline:foo.jpg]) upon node preview and node save.
 *
 * @todo Break processing at all if Inline filter is not enabled.
 */
function inline_nodeapi(&$node, $op, $arg) {
  if (!(isset($node->files) && is_array($node->files))) {
    return;
  }
  switch ($op) {
    case 'alter':
    case 'print':
    case 'rss item':
      // Only nodes with Inline filter in the format may be processed.
      foreach (filter_list_format($node->format) as $filter) {
        if ($filter->module == 'inline') {
          if (isset($node->teaser)) {
            $node->teaser = _inline_substitute_tags($node, 'teaser');
          }
          if (isset($node->body)) {
            $node->body = _inline_substitute_tags($node, 'body');
          }
          break;
        }
      }
      if (variable_get('upload_inline_'. $node->type, 0)) {
        $node = _inline_auto_add($node);
      }
      return;

    case 'prepare':
    case 'presave':
      if (isset($node->teaser)) {
        $node->teaser = _inline_replace_numbers($node, 'teaser');
      }
      if (isset($node->body)) {
        $node->body = _inline_replace_numbers($node, 'body');
      }
      return;
  }
}

/**
 * Return the corresponding file object of an Inline tag.
 */
function _inline_fileobj(&$node, $id) {
  if (is_numeric($id)) {
    // Numeric file reference (deprecated, see #38359).
    $n = 1;
    foreach ($node->files as $file) {
      if ($n == $id) {
        return (object)$file;
      }
      ++$n;
    }
    return NULL;
  }
  else {
    // Named file reference.
    foreach ($node->files as $file) {
      $file = (object)$file;
      if ($file->filename == $id) {
        return $file;
      }
    }
    return NULL;
  }
}

/**
 * Change file path of new files for previews.
 *
 * New files are stored in a temporary upload directory until the content
 * is saved. We alter the file object accordingly, so such files may be
 * displayed if the temporary directory is publicly accessible.
 *
 * @todo Prepend 'system/' for private files support.
 */
function inline_prepare_file_object($file) {
  $file = (object)$file;
  $tmp = file_directory_temp();
  if (strpos($file->filepath, $tmp) === 0) {
    $file->real_path = $file->filepath;
    $file->filepath  = $file->filename;
    $file->preview   = TRUE;
  }
  return $file;
}

/**
 * Implementation of hook_theme().
 */
function inline_theme() {
  return array(
    'inline_as_link' => array(
      'arguments' => array('link' => NULL),
      'file' => 'inline.theme.inc',
    ),
    'inline_img' => array(
      'arguments' => array('file' => NULL, 'field' => NULL),
      'file' => 'inline.theme.inc',
    ),
    'inline_add_to_teaser' => array(
      'arguments' => array('node' => NULL, 'file' => NULL, 'field' => NULL),
      'file' => 'inline.theme.inc',
    ),
    'inline_add_to_body' => array(
      'arguments' => array('node' => NULL, 'file' => NULL, 'field' => NULL),
      'file' => 'inline.theme.inc',
    )
  );
}

/**
 * Automatically add all images to configured node views.
 *
 * This feature can be configured per content-type.
 */
function _inline_auto_add($node) {
  switch (variable_get('upload_inline_'. $node->type, 0)) {
    case 1:
      // Display only in teaser.
      foreach ($node->files as $fid => $file) {
        $file = inline_prepare_file_object($file);
        if (_inline_decide_img_tag($file)) {
          $node->files[$fid]->inline = TRUE;
          $node->teaser = theme('inline_add_to_teaser', $node, $file, 'teaser');
        }
        else {
          $node->files[$fid]->inline = FALSE;
        }
      }
      break;

    case 2:
      // Display only in body.
      foreach ($node->files as $fid => $file) {
        $file = inline_prepare_file_object($file);
        if (_inline_decide_img_tag($file)) {
          $node->files[$fid]->inline = TRUE;
          $node->body = theme('inline_add_to_body', $node, $file, 'body');
        }
        else {
          $node->files[$fid]->inline = FALSE;
        }
      }
      break;

    case 3:
      // Display in teaser and body.
      foreach ($node->files as $fid => $file) {
        $file = inline_prepare_file_object($file);
        if (_inline_decide_img_tag($file)) {
          $node->files[$fid]->inline = TRUE;
          $node->teaser = theme('inline_add_to_teaser', $node, $file, 'teaser');
          $node->body = theme('inline_add_to_body', $node, $file, 'body');
        }
        else {
          $node->files[$fid]->inline = FALSE;
        }
      }
      break;
  }
  return $node;
}

/**
 * Replace all Inline tags with their corresponding files or images.
 *
 * @param object $node
 *   The node to process.
 * @param string $field
 *   The node field to process.
 *
 * @return string
 *   The processed content of the given node field.
 */
function _inline_substitute_tags(&$node, $field) {
  if (preg_match_all("/\[(inline|file|attachment):([^=\\]]+)=?([^\\]]*)?\]/i", $node->$field, $match)) {
    $s = $r = array();
    foreach ($match[2] as $key => $value) {
      // Ensure that we deal with a file object.
      $file = inline_prepare_file_object(_inline_fileobj($node, $value));
      if ($file->fid != NULL) {
        // Set user defined file title if given.
        $title = $match[3][$key];
        if (!empty($title)) {
          $file->title = $title;
        }
        // Decide whether to show a link or an image tag.
        if (_inline_decide_img_tag($file)) {
          $replace = theme('inline_img', $file, $field);
        }
        else {
          $replace = theme('inline_as_link', $file);
        }
      }
      else {
        $replace = '<span style="color: red; font-weight: bold;">NOT FOUND: '. $value .'</span>';
      }
      $s[] = $match[0][$key];
      $r[] = $replace;
    }
    // Perform the replacements and return processed field.
    return str_replace($s, $r, $node->$field);
  }
  return $node->$field;
}

/**
 * Replaces numeric file references with their corresponding file names.
 *
 * @param object $node
 *   The node object to process.
 * @param string $field
 *   A field name of the node to process.
 *
 * @return
 *   The processed content of the given node field.
 */
function _inline_replace_numbers($node, $field) {
  $tag = '/\[(inline|file|attachment):(\d+?)(=.+?)?\]/i';
  // Look if there are any numeric inline tags.
  preg_match_all($tag, $node->$field, $matches, PREG_SET_ORDER);
  if (!empty($matches)) {
    foreach ($matches as $match) {
      // The array key of the attachment is the file ID (fid).
      $filekeys = array_keys($node->files);
      // Because a user starts counting files from 1, we substract 1 here.
      $key = $filekeys[$match[2] - 1];
      // If user entered a non-existent number, continue with next tag.
      if (!isset($node->files[$key])) {
        continue;
      }
      // If a corresponding file does exist, perform the replacement.
      if (is_array($node->files[$key])) {
        // Node form submit is an array.
        $filename = $node->files[$key]['filename'];
      }
      else {
        // Node form prepare is an object.
        $filename = $node->files[$key]->filename;
      }
      if (!isset($match[3])) {
        $match[3] = '';
      }
      $node->$field = str_replace($match[0], '['. $match[1] .':'. $filename . $match[3] .']', $node->$field);
    }
  }
  return $node->$field;
}

/**
 * Decide if an image tag (&lt;IMG&gt;) or a link to a file should be rendered.
 *
 * @param $file
 *   A file object.
 *
 * @return
 *   TRUE in case an image tag should be generated.
 */
function _inline_decide_img_tag($file) {
  $inlined = array('jpg', 'jpeg', 'pjpeg', 'gif', 'png');
  $mime = array_pop(explode('/', $file->filemime));
  if (in_array($mime, $inlined)) {
    if (module_exists('imagecache')) {
      return TRUE;
    }
    else {
      // Read maximum dimension settings.
      list($maxwidth, $maxheight) = explode(',', variable_get('inline_img_dim', '150,150'));

      if (!empty($file->preview)) {
        list($width, $height) = getimagesize($file->real_path);
      }
      else {
        list($width, $height) = getimagesize($file->filepath);
      }

      if (($width && $height) && ($width <= $maxwidth && $height <= $maxheight)) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

