<?php
// $Id: inline.module,v 1.35 2009/01/28 21:43:05 sun Exp $

/**
 * @file
 * Inline filter tag macro processing and rendering API for Drupal.
 */

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

/**
 * @defgroup inline_macro Inline input filter/macro parsing
 * @{
 */

/**
 * Implementation of hook_filter().
 */
function inline_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
      return array(0 => t('Inline contents'));

    case 'description':
      return t('Substitutes [inline] tags.');

    case 'no cache':
      // Development. 13/01/2008 sun
      return TRUE;

    case 'prepare':
      return $text;

    case 'process':
      $processed = FALSE;
      foreach (inline_get_macros($text) as $macro => $params) {
        $validation = inline_validate_params($params);
        if (is_bool($validation) && $validation) {
          $text = str_replace($macro, inline_render($params), $text);
          $processed = TRUE;
        }
        else {
          $text = str_replace($macro, (is_string($validation) ? $validation : ''), $text);
        }
      }
      return $text;
  }
}

/**
 * Return all inline macros as an array.
 */
function inline_get_macros($text) {
  $macros = array();

  // Collect possible inline tag names.
  // @todo Use module_invoke_all() to allow more than one Inline implementation
  //   per module, and allow to configure which Inline implementations should
  //   be enabled per filter format.
  $tagnames = '('. implode('|', module_implements('inline')) .')';
  // @todo Add support for escaped [, ] chars
  preg_match_all('@\['. $tagnames .'\s*\|([^\[\]]+)\]@', $text, $matches);
  // Don't process duplicates.
  $tags = array_unique($matches[2]);

  foreach ($tags as $n => $macro) {
    // @todo Add support for escaped | char
    $macro_params = array_map('trim', explode('|', $macro));
    $params = array();
    $params['module'] = $matches[1][$n];
    // @todo Add a macro counter for each found tag *per module* to allow stuff
    //   like odd/even classes (f.e. $params['#count']).
    $args = module_invoke($params['module'], 'inline', 'args');
    foreach ($macro_params as $param) {
      list($key, $value) = explode('=', $param, 2);
      $key = trim($key);
      // All parameter values are strings by default.
      $value = trim($value);
      // Convert numeric values.
      if (is_numeric($value)) {
        if (strpos($value, '.') !== FALSE) {
          $value = (float)$value;
        }
        else {
          $value = (int)$value;
        }
      }
      // Convert boolean values.
      else if (in_array(strtolower($value), array('true', 'false'))) {
        $value = (bool)$value;
      }
      // Stack multiple occurences.
      if (isset($params[$key]) && isset($args[$key]['#multiple']) && $args[$key]['#multiple']) {
        if (!is_array($params[$key])) {
          $params[$key] = array($params[$key]);
        }
        $params[$key][] = $value;
      }
      else {
        $params[$key] = $value;
      }
    }
    // The full unaltered tag is the key for the filter attributes array.
    $macros[$matches[0][$n]] = $params;
  }

  return $macros;
}

/**
 * Retrieve parameter value formats and check user values against them.
 *
 * @param array $params
 *   An array of user supplied values.
 *
 * @return bool
 *   Whether the supplied input is valid (TRUE) or not (FALSE).
 */
function inline_validate_params($params) {
  // Return if no module has been supplied or module does not implement
  // hook_inline().
  if (!isset($params['module']) || !module_hook($params['module'], 'inline')) {
    return FALSE;
  }

  // Perform basic validation of tag arguments.
  $args = module_invoke($params['module'], 'inline', 'args');
  foreach ($args as $arg => $info) {
    // Check if required arguments are set.
    if (isset($info['#required']) && $info['#required'] && !isset($info['#default_value']) && (!isset($params[$arg]) || $params[$arg] === '')) {
      return t('Missing argument %arg.', array('%arg' => $arg));
    }
    if (isset($params[$arg])) {
      // Keep only the first value if multiple flag is not set.
      if ((!isset($info['#multiple']) || !$info['#multiple']) && is_array($params[$arg])) {
        $params[$arg] = $params[$arg][0];
      }
      // Check if supplied arguments are of an expected type.
      if (isset($info['#type'])) {
        $typecheck = 'is_'. $info['#type'];
        if (!function_exists($typecheck) || !$typecheck($params[$arg])) {
          return t('Wrong value type supplied for argument %arg.', array('%arg' => $arg));
        }
      }
    }
  }
  
  // Extended validation check by given module.
  $module_validation = module_invoke($params['module'], 'inline', 'validate', $params);
  if (isset($module_validation) && !$module_validation) {
    return FALSE;
  }

  return TRUE;
}

/**
 * Generate HTML based on Inline tag parameters.
 *
 * @param array $params
 *   An validated array of Inline tag parameters.
 *
 * @return string
 *   HTML presentation of the user supplied values.
 */
function inline_render($params) {
  // Merge in default values.
  $args = module_invoke($params['module'], 'inline', 'args');
  foreach ($args as $arg => $info) {
    if (empty($params[$arg])) {
      // @todo Allow special defaults like 'current user' or 'current node'.
      $params[$arg] = $info['#default_value'];
    }
  }

  // Allow module to prepare tag parameters.
  $params = module_invoke($params['module'], 'inline', 'prepare', $params);

  // @todo Prepare operation may not be successful; final validation before
  //   rendering required.

  // Generate a rendered representation for tag replacement.
  $output = module_invoke($params['module'], 'inline', 'render', $params);

  // If an error occured during rendering, we expect the result to be FALSE.
  if (!is_bool($output)) {
    return $output;
  }
  else {
    return '';
  }
}

/**
 * Replace certain system object ids in inline macros when the object id is known.
 *
 * Currently, ex. Inline v1 filter is the only known input filter that allowed
 * to embed contents of the same node.  Since Drupal's filter system does not
 * provide any context about what system object is actually filtered, we need
 * to update the node id in all inline tags after the node has been saved to the
 * database (so we know its id).
 *
 * This might be enhanced to allow further programmatic replacements.  For example,
 * the same good ol' Inline filter allowed users to reference attached files of
 * a node by numeric pointers that needed to be converted upon node preview/submit.
 *
 * @param &$node
 *   A node object provided by hook_nodeapi().
 * @param $fields
 *   An array of fields in the node object that support Inline filter tags.
 */
function inline_alter_macros(&$node, $fields) {
  if ($node->inline_altered) {
    return;
  }
  // Backup some node properties to undo them later, since we don't know whether
  // other modules are invoked after hook_nodeapi of Inline.
  $properties = array(
    'is_new' => $node->is_new,
    'revision' => $node->revision,
  );
  // Temporarily disable revisions for this node.
  $node->revision = 0;
  // Mark this node as processed to prevent recursive loops.
  $node->inline_altered = TRUE;

  // Process all node fields.
  foreach ($fields as $field) {
    _inline_alter_macros($node, $field);
  }
  // Save the node (again).
  node_save($node);

  // Undo our changes, 
  foreach ($properties as $property => $value) {
    $node->$property = $value;
  }
}

/**
 * Helper function for inline_alter_macros().
 */
function _inline_alter_macros(&$node, $field, $instance = NULL) {
  // Recursively process multiple value fields.
  if (is_array($node->$field) && !isset($instance)) {
    foreach (array_keys($node->$field) as $field_instance) {
      _inline_alter_macros($node, $field, $field_instance);
    }
    return;
  }

  if (isset($instance)) {
    $content = &$node->{$field}[$instance]['value'];
  }
  else {
    $content = &$node->$field;
  }
  foreach (inline_get_macros($content) as $macro => $params) {
    // @todo Use placeholders for #default_value and/or additional properties.
    //   Or introduce a new $op 'alter' for hook_inline?
    // @todo Assign the currently processed field name to $params['#field'] to
    //   allow certain hook_inline implementations to add/ensure this
    //   information in their macros.
    if (isset($params['nid']) && $params['nid'] == 0) {
      $params['nid'] = $node->nid;
      $content = str_replace($macro, inline_build_macro($params), $content);
      // @todo Update of reference table perhaps needed.
    }
  }
}

/**
 * Generate an inline macro/tag based on given parameters.
 *
 * @param array $params
 *   An array of tag parameters. The key 'module' is required.
 *
 * @return string
 *   A string representation of the passed in parameters.
 */
function inline_build_macro($params) {
  $macro_params = array();

  if (isset($params['module'])) {
    $macro_params[] = $params['module'];
    unset($params['module']);
  }
  // @todo Support for #multiple values.
  // @todo Escape |, [, ] chars in values.
  foreach ($params as $key => $value) {
    $macro_params[] = $key .'='. $value;
  }
  return '['. implode('|', $macro_params) .']';
}

/**
 * @} End of "defgroup inline_macro".
 */

/**
 * Implementation of hook_nodeapi().
 *
 * Inline needs to
 * - insert the node id for new nodes in Inline tags that refer to nid=0, but
 *   must not create a new node revision if revisions are enabled.
 * - clear the filter cache if nodes of a certain type are updated, f.e. if an
 *   image node is updated and the image is referenced in other nodes via
 *   img_assist.
 * - allow hook_inline() implementations to react on nodeapi operations, f.e. to
 *   replace numeric file references of inline_upload tags (i.e. file=1) with
 *   named file references (i.e. file=foo.jpg) upon node preview and node save.
 *
 * @see DEVELOPER.txt
 */
function inline_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  // Only nodes with enabled Inline filter in the format may be processed.
  $fields = inline_check_node_format($node);
  if (empty($fields)) {
    return;
  }

  switch ($op) {
    case 'update':
      // @todo Some inline tags are referencing to other system objects (f.e.
      //   nodes). If the referenced object is updated, we need to clear the
      //   filter cache if all contents that are referencing it.  As long as
      //   there is no generic solution, we completely clear the filter cache.
      cache_clear_all(NULL, 'cache_filter');

      // Break intentionally left out.
    case 'insert':
      // Update references to 'current' system objects, f.e. the node id after
      // inserting a new node.
      inline_alter_macros($node, $fields);
      return;

    case 'prepare':
    case 'presave':
      // @todo We should allow Inline implementations to react on node
      //   (pre)view and submit; some might want to alter macro parameters.
      //   inline_alter_macros() should be extended to invoke a corresponding
      //   hook_inline() implementation, f.e.:
      
      // inline_alter_macros($node, $op, $fields);
      
      //   ...which in turn would
      //   module_invoke($params['module'], 'inline', 'alter', $params);
      //   However, we do not want to save a node in $op 'prepare' or 'submit'...
      return;
  }
}

/**
 * Retrieves the field names of a node that are Inline enabled.
 */
function inline_check_node_format(&$node) {
  $fields = array();

  // Check format of node body.
  if (!empty($node->body)) {
    foreach (filter_list_format($node->format) as $filter) {
      if ($filter->module == 'inline') {
        $fields[] = 'body';
        $fields[] = 'teaser';
        break;
      }
    }
  }
  // Check format of CCK fields.
  if (function_exists('content_types')) {
    $type = content_types($node->type);
    if (!empty($type['fields'])) {
      foreach ($type['fields'] as $field) {
        // Skip fields in plain-text format.
        if (!empty($field['text_processing'])) {
          $fields[] = $field['field_name'];
        }
      }
    }
  }
  return $fields;
}

/**
 * @defgroup inline_help Inline help
 * @{
 */

/**
 * 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: !syntax. Parameters: file represents the file uploaded with the node in which to link, assuming that the first uploaded file is labeled as 1 and so on. Title is optional and used instead of the filename.</p>
<p>If the file is an image, it will be displayed inline, otherwise a link to the file will be inserted.</p>', array('!syntax' => '<code>[inline_upload|file=&lt;FILE-ID&gt;|title=&lt;TITLE-TEXT&gt;]</code>'));
  }
}

/**
 * Implementation of hook_filter_tips().
 *
 * @todo Allow hook_inline() implementations to add (long) 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_upload|file=1|title=test]  or  [inline_upload|file=imag1.png|title=test]</pre>
      will be replaced by <em><code>&lt;img src=imag1.png alt=test&gt;</code></em>') .'</p>';
  }
  else {
    return t('You may use <a href="!inline_help">[inline] tags</a> to display contents inline.', array('!inline_help' => url("filter/tips/$format", array('query' => 'filter-inline'))));
  }
}

/**
 * @} End of "defgroup inline_help".
 */

