<?php
// $Id$

/**
 *  The MAPI filtering tag system.
 *
 *  This provides for specification of various forms of tags, including command code processes
 *  Examples:
 *  [media], [media:10], [media:10;20], [media:"http://localhost/view.swf"], [media::height="200";width="320"],
 *  [media:1::transform="article"]
 *
 *  The basic concept with the filtering system, is that commonly formatted tag systems, can easily be established to also
 *  provide specific filtering of text, within a context, such as when performing a filter against a node body.
 */
define('MAPI_FILTER', '/\[\[([\w-]+)((?::(?:(?:(?:[\w\-]+="(?:\\\\"|[^"]|)+"|[\w\-]+|\d+|"(?:\\\\"|[^"]|)+"));*|)+)*)\]\]/');

function mapi_filter_process($text, $functions) {
  if (preg_match_all(MAPI_FILTER, $text, $matches, PREG_SET_ORDER)) {
    $strings = array();
    $replace = array();

    // for every match within the text listed
    foreach ($matches as $match) {
      $output = '';

			// make sure we have a way to handle the function
			if (!array_key_exists($match[1], $functions)) {
				continue;
			}

			$details = array();

			// split up the tag into the appropriate details
			$split = preg_split('/([\w\-]+="(?:\\\\"|[^"]|)+"|[\w\-]+|\d+|"(?:\\\\"|[^"]|)+"|:)/', $match[2], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);

			$details = _mapi_filter_build_details($split);

      // call the function with the details
			$info = $functions[$match[1]];
			$func = array_shift($info);
			array_unshift($info, $details);

			// generate the output
			$output = call_user_func_array($func, $info);
			if (!$output) {
				$output = '';
			}

			// only replace the string if there is a match
			$strings[] = $match[0];
			$replace[] = $output;
    }

    $text = str_replace($strings, $replace, $text);
  }

  return $text;
}

function mapi_filter_prepare($text) {
  // converts all images and embeds
  $text = preg_replace_callback(array('|<img[^>]+>|is', '|<embed([^>]+)>(.*?)</embed>|is', '|<embed([^>]+)/>|is'), '_mapi_filter_replace', $text);

  // deal with nested objects
  while (preg_match('|<object\b[^>]*>(.*?<object\b[^>]*>)+</object>|', $text)) {
    // gets the innermost objects and replaces that
    $text = preg_replace_callback('|<object\b[^>]*>(?!<object\b[^>]*>)(?:[\S\s](?!<object\b[^>]*>))*?</object>|i', '_mapi_filter_replace_object', $text);
  }
  // replace the details from what are effectively singlely left objects
  $text = preg_replace_callback('|<object\b[^>]*>.*?</object>|', create_function('$matches', 'return _mapi_filter_replace_object($matches, true);'), $text);

  return $text;
}

/**
 *   Special functions for filtering out media related HTML tags, and converting them into something that
 *   can be reprocessed back later, in a clean, XHTML safe way into sites, hopefully with less cross-scripting
 *   involved.
 */
function _mapi_filter_replace($matches) {
  // all images or embed tags must have an attribute called "src", and we check that it's a file.
  $params = _mapi_filter_construct_params($matches[0]);
  if (empty($params['src'])) {
    // NEEDS WORK: uncomment below for is_file check. Not sure whether to put this in, but I think it should go in.
    // it should prevent some wierd XSS attacks using javascript.
    //  if (empty($params['src']) || !is_file($params['src'])) {
    return '';
  }

  // find ID in database, if not make into special "url".
  $src = '"'. $params['src'] .'"';
  unset($params['src']);

  $attrs = '';
  foreach ($params as $k => $v) {
    $attrs .= "${k}=\"{$v}\";";
  }

  // return a standard filter tag
  return '[[embed:'. $src . ($attrs ? ':'. $attrs : '') .']]';
}

function _mapi_filter_replace_object($matches, $finish = false) {
//  $text = _replace_param($matches[0]);
  // get the general attributes of the object
  preg_match('|<object\b(.*?)>|i', $matches[0], $params);
  $object = _mapi_filter_settings($params[1]);

  // determine the parameters of an object, as well as the ones specified as attributes.
  if (preg_match_all('|<param\b(.*?)>|i', $matches[0], $params, PREG_SET_ORDER)) {
    foreach ($params as $param) {
      $p = _mapi_filter_construct_params($param[1]);
      if (!empty($p['name']) && !empty($p['value'])) {
        $object[$p['name']] = $p['value'];
      }
    }
  }

  // only if this is the last object to handle in nested objects
  if ($finish) {
    // must have some kind of url specifier set
    $src = '';
    foreach (array('movie', 'src', 'url', 'data') as $type) {
      if (!empty($object[$type])) {
        $src = '"'. $object[$type] .'"';
      }
      unset($object[$type]);
    }
    if (!$src) {
      return '';
    }

    $attrs = '';
    foreach ($object as $k => $v) {
      $attrs .= "${k}=\"{$v}\";";
    }

    $output = "[[embed:". $src . ($attrs ? ":". $attrs : '') ."]]";
  }
  else {
    // simply construct the output as more parameters for the ultimate object
    $output = '';
    foreach ($object as $k => $v) {
      $output .= "<param name=\"${k}\" value=\"{$v}\" />";
    }
  }

  return $output;
}

/**
 *  Get tag attributes as per the HTML 4 specifications.
 */
function _mapi_filter_settings($details) {
  // matches definition of values according to the HTML4 settings
  preg_match_all("/(\w+)=((?:[a-zA-Z0-9_:.\-]+)|(?:\"(?:.*?|)\")|(?:'(?:.*?|)'))/", $details, $list, PREG_SET_ORDER);
  $attr = array();
  foreach ($list as $item) {
    // remove quotation marks
    if (substr($item[2], 0, 1) == '"' || substr($item[2], 0, 1) == "'") {
      $value = substr($item[2], 1, strlen($item[2])-2);
    }
    // value is standard unquoted
    else {
      $value = $item[2];
    }

    $attr[$item[1]] = check_plain($value);
  }
  return $attr;
}

function _mapi_filter_construct_params($details) {
  // matches definition of values according to the HTML4 settings
  preg_match_all("/(\w+)=((?:[a-zA-Z0-9_:.\-]+)|(?:\"(?:.*?|)\")|(?:'(?:.*?|)'))/", $details, $list, PREG_SET_ORDER);
  $attr = array();
  foreach ($list as $item) {
    // remove quotation marks
    if (substr($item[2], 0, 1) == '"' || substr($item[2], 0, 1) == "'") {
      $value = substr($item[2], 1, strlen($item[2])-2);
    }
    // value is standard unquoted
    else {
      $value = $item[2];
    }

    $attr[$item[1]] = check_plain($value);
  }
  return $attr;
}

/**
 *  This provides a details array which can be referenced by the appropriate
 *  function to generate the output.
 */
function _mapi_filter_build_details($list) {
	$details = array();

  $first = true;
	$last = false;
	$current = array();

	// cycle through the entire list of details
	while ($value = array_shift($list)) {
		if ($value == ':') {
      if (!$first) {
        // special index into the details
        if ($last) {
          $details[$last] = $current;
          $last = false;
        }
        // we have an index specified
        elseif (count($current) == 1 && !count(array_filter(array_keys($current)))&& !is_numeric($current[0])) {
          $last = $current[0];
        }
        // otherwise it's a simple list.
        else {
          $details[] = $current;
        }
      }
      $current = array();
		}
		// simply add the numeric value or special word to the current list
		elseif (is_numeric($value) || preg_match('/^[\w-]+$/', $value)) {
			$current[] = $value;
		}
		// it's an index of an array
		elseif (preg_match('/([\w-]+)="((?:\\\\"|[^"]|)+)"/', $value, $settings)) {
			$current[$settings[1]] = $settings[2];
		}
    elseif (preg_match('/^"((?:\\\\"|[^"]|)+)"$/', $value, $settings)) {
      $current[] = $settings[1];
    }
    $first = false;
	}

	// there will always be something to add at the end
	if ($last) {
		$details[$last] = $current;
	}
	else {
		$details[] = $current;
	}
	return $details;
}
