code:
<?php
/**
 * @package Habari
 *
 */

/**
 *
 * Habari HiEngine class
 *
 * The HiEngine is a subclass of the RawPHPEngine class
 * which is intended for those theme designers who want to use
 * simple {hi:*} tags four output instead of PHP
 *
 * To use this engine, specify "hiengine" in the theme.xml of a theme.
 *
 * This engine behaves exactly like RawPHPEngine after the template tags are
 * processed, so if an existing RawPHPEngine template is switched to HiEngine,
 * it should still run without issue.
 */
class HiEngine extends RawPHPEngine {
	/**
	 *
	 * Constructor for HiEngine
	 *
	 * Sets up the stream protocol handler
	 */
	public function __construct()
	{
		$streams = stream_get_wrappers();
		if ( ! in_array( 'hi', $streams ) ) {
			stream_wrapper_register( "hi", "HiEngineParser" )
			or die( _t( "Failed to register HiEngine stream protocol" ) );
		}
	}

	/**
	 *
	 * A function which outputs the result of a transposed
	 * template to the output stream
	 * @param template $ Name of template to display
	 */
	public function display( $template )
	{
		extract( $this->engine_vars );
		//Utils::debug($this->engine_vars);die();
		if ( $this->template_exists( $template ) ) {
			$template_file = isset( $this->template_map[$template] ) ? $this->template_map[$template] : null;
			$template_file = Plugins::filter( 'include_template_file', $template_file, $template, __CLASS__ );
			$template_file = 'hi://' . $template_file;
			$fc = file_get_contents( $template_file );
			//echo($fc);
			eval( '?'.'>' . $fc );
			//include $template_file;  // stopped working properly in PHP 5.2.8
		}
	}
}

/**
 * HiEngineParser - A stream filtering class for the HiEngine
 */
class HiEngineParser
{
	protected $file;
	protected $filename;
	protected $position;
	protected $contexts;
	protected $strings = array();

	/**
	 * Open a HiEngineParser stream
	 *
	 * @param string $path Path of the opened resource, including the protocol specifier
	 * @param string $mode Mode used to open the file
	 * @param integer $options Bitmask options for opening this stream
	 * @param string $opened_path The actual path opened if using relative path, by reference
	 * @return boolean true on success
	 */
	function stream_open( $path, $mode, $options, &$opened_path )
	{
		$this->filename = substr( $path, 5 );
		$this->file = file_get_contents( $this->filename );

		// This processed value should cache and invalidate if a checksum of the template changes!  :)
		$this->file = $this->process( $this->file );

		$this->position = 0;

		return true;
	}

	/**
	 * Read data from a HiEngineParser stream
	 *
	 * @param integer $count Number of characters to read from the current position
	 * @return string Characters read from the stream
	 */
	function stream_read( $count )
	{
		if ( $this->stream_eof() ) {
			return false;
		}
		$ret = substr( $this->file, $this->position, $count );
		$this->position += strlen( $ret );
		return $ret;
	}

	/**
	 * Srite data to a HiEngineParser stream
	 *
	 * @param string $data Data to write
	 * @return boolean false, since this stream type is read-only
	 */
	function stream_write( $data )
	{
		// HiEngineParser streams are read-only
		return false;
	}

	/**
	 * Report the position in the stream
	 *
	 * @return integer the position in the stream
	 */
	function stream_tell()
	{
		return $this->position;
	}

	/**
	 * Report whether the stream is at the end of the file
	 *
	 * @return boolean true if the file pointer is at or beyond the end of the file
	 */
	function stream_eof()
	{
		return $this->position >= strlen( $this->file );
	}

	/**
	 * Seek to a specific position within the stream
	 *
	 * @param integer $offset The offset from the specified position
	 * @param integer $whence The position to seek from
	 * @return boolean true if seek was successful
	 */
	function stream_seek( $offset, $whence )
	{
		switch ( $whence ) {
			case SEEK_SET:
				if ( $offset < strlen( $this->file ) && $offset >= 0 ) {
					$this->position = $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			case SEEK_CUR:
				if ( $offset >= 0 ) {
					$this->position += $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			case SEEK_END:
				if ( strlen( $this->file ) + $offset >= 0 ) {
					$this->position = strlen( $this->file ) + $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			default:
				return false;
		}

	}

	/**
	 * Return fstat() info as required when calling stats on the stream
	 * @return array An array of stat info
	 */
	function stream_stat()
	{
		return array();
	}

	/**
	 * Process the template file for template tags
	 *
	 * @param string $template The template file contents
	 * @return string The processed template
	 */
	function process( $template )
	{
		$template = preg_replace_callback( '/\{hi:(".+?")((?:\s*[\w\.]+){0,2})\s*\}/smu', array( $this, 'hi_quote' ), $template );
		$template = preg_replace_callback( '%\{hi:([^\?]+?)\}(.+?)\{/hi:\1\}%ism', array( $this, 'hi_loop' ), $template );
		$template = preg_replace_callback( '%\{hi:\?\s*(.+?)\}(.+?)\{/hi:\?\}%ismu', array( $this, 'hi_if' ), $template );
		$template = preg_replace_callback( '%\{hi:([^:}]+?:.+?)\}%i', array( $this, 'hi_command' ), $template );
		$template = preg_replace_callback( '%\{hi:(.+?)\}%i', array( $this, 'hi_var' ), $template );
		return $template;
	}

	/**
	 * Replace a single function template tag with its PHP counterpart
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the function template tag
	 */
	function hi_command( $matches )
	{
		$cmd = trim( $matches[1] );

		// Catch tags in the format {hi:command:parameter}
		if ( preg_match( '/^(\w+):(.+)$/u', $cmd, $cmd_matches ) ) {
			switch ( strtolower( $cmd_matches[1] ) ) {
				case 'area':
					return '<?php echo $theme->area(\'' . $cmd_matches[2] . '\'); ?>';
				case 'display':
					return '<?php $theme->display(\'' . $cmd_matches[2] . '\'); ?>';
				case 'option':
				case 'options':
					return '<?php Options::out(\'' . $cmd_matches[2] . '\'); ?>';
				case 'siteurl':
					return '<?php Site::out_url( \'' . $cmd_matches[2] . '\' ); ?>';
				case 'url':
					return '<?php URL::out( \'' . $cmd_matches[2] . '\' ); ?>';
				case 'session':
					switch ( $cmd_matches[2] ) {
						case 'messages':
							return '<?php if (Session::has_messages()){Session::messages_out();} ?>';
						case 'errors':
							return '<?php if (Session::has_errors()){Session::messages_out();} ?>';
					}
				// this is an internal match
				case 'context':
					return $this->hi_to_var( $cmd_matches[2] );
				case 'escape':
					return '<?php echo Utils::htmlspecialchars( ' . $this->hi_to_var( $cmd_matches[2] ) . ' ); ?>';
			}
		}

		return $matches[0];
	}

	/**
	 * Replace a single template tag with its PHP counterpart
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_var( $matches )
	{
		$cmd = trim( $matches[1] );
		$params = array();
		$returnval = false;

		if ( preg_match_all( '/(?<=\s)(?P<name>[@a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*)\s*=\s*(?P<value>(?P<quot>["\']).+?\3|[^"\'\s]+)/i', $cmd, $foundparams, PREG_SET_ORDER ) ) {
			foreach ( $foundparams as $p ) {
				$params[$p['name']] = trim( $p['value'], $p['quot'] );
			}
		}

		// Straight variable or property output, ala {hi:variable_name} or {hi:post.title}
		if ( preg_match( '%(^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*)%i', $cmd, $cmdmatch ) ) {
			$cmdmatch = str_replace( '.', '->', $cmdmatch[1] );
			if ( count( $this->contexts ) ) {
				// Build a conditional that checks for the most specific, then the least
				// eg.- $a->b->c->d->x, then $a->b->c->x, down to just $x
				$ctx = $this->contexts;
				$prefixes = array();
				foreach ( $ctx as $void ) {
					$prefixes[] = implode( '->', $this->contexts );
					array_pop( $ctx );
				}
				$output = '';
				foreach ( $prefixes as $prefix ) {
					$output .= '(is_object($' . $prefix . ') && !'.'is_null($' . $prefix . '->' . $cmdmatch . ')) ? $' . $prefix . '->' . $cmdmatch . ' : ';
				}
				$output .= '$' . $cmdmatch;
				$returnval = $output;
			}
			else {
				$returnval = '$'. $cmdmatch;
			}
		}
		if ( $returnval !== false ) {
			$returnval = $this->apply_parameters( $returnval, $params );
			return '<?php echo '. $returnval . '; ?>';
		}

		// Use tags in the format {hi:@foo} as theme functions, ala $theme->foo();
		if ( $cmd[0] == '@' ) {
			return '<?php $theme->' . substr( $cmd, 1 ) . '(); ?>';
		}

		// Didn't match anything we support so far
		return $matches[0];
	}

	/**
	 * Take the found paramters on a variable tag and apply them to the output
	 *
	 * @param array $returnval The expression to be output
	 * @param array $params An associative array of parameters
	 * @return string The PHP expression with the paramters applied
	 */
	function apply_parameters( $returnval, $params )
	{
		foreach ( $params as $k => $v ) {
			if ( $k[0] == '@' ) {
				$returnval = '$theme->' . substr( $cmd, 1 ) . '(' . $returnval . ", '" . $v . "')";
			}
			switch ( $k ) {
				case 'dateformat':
					$returnval = "call_user_func(array(" . $returnval . ", 'format'), '" . addslashes( $v ) . "')";
					break;
			}
		}
		return $returnval;
	}

	/**
	 * Replace a loop tag section with its PHP counterpart, and add the context to the stack
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_loop( $matches )
	{
		$hivar = $matches[1];
		$phpvar = $this->hi_to_var( $hivar );
		$iterator = strpos( $hivar, '.' ) ? substr( $hivar, strrpos( $hivar, '.' ) + 1 ) : $hivar;
		$output = '<?php foreach(' . $phpvar . ' as $' . $iterator . '_index => $' . $iterator . '_1): ?>';
		$this->contexts[] = "{$iterator}_1";
		$output .= $this->process( $matches[2] );
		$output .= '<?php endforeach; ?>';
		array_pop( $this->contexts );
		return $output;
	}

	/**
	 * Replace variables in the hiengine syntax with PHP varaibles
	 * @param array $matches The match array found in hi_if ()
	 * @returns string A PHP variable string to use as the replacement
	 */
	function var_replace( $matches )
	{
		$var = $matches[1];

		if ( is_callable( $var ) ) {
			return $var;
		}
		if ( preg_match( '/true|false|null|isset|empty/i', $var ) ) {
			return $var;
		}

		$var = $this->hi_to_var( $var );

		return $var;
	}

	function hi_to_var( $hisyntax )
	{
		$var = str_replace( '.', '->', $hisyntax );
		if ( count( $this->contexts ) ) {
			// Build a conditional that checks for the most specific, then the least
			// eg.- $a->b->c->d->x, then $a->b->c->x, down to just $x
			$ctx = $this->contexts;
			$prefixes = array();
			foreach ( $ctx as $void ) {
				$prefixes[] = implode( '->', $this->contexts );
				array_pop( $ctx );
			}
			$output = '';
			foreach ( $prefixes as $prefix ) {
				$output .= '(!is_null($' . $prefix . '->' . $var . ') ? $' . $prefix . '->' . $var . ' : ';
			}
			$output .= '$' . $var . ')';
			return $output;
		}
		else {
			return '$'. $var;
		}
	}

	/**
	 * Creates a table of static strings in hiengine expressions to be replaced in later
	 * @param array $matches The match found in hi_if ()
	 * @returns string An uncommon string index for the stored static string.
	 */
	function string_stack( $matches )
	{
		$key = chr( 0 ) . count( $this->strings ) . chr( 1 );
		$this->strings[$key] = $matches[0];
		return $key;
	}

	/**
	 * Replace an if tag section with its PHP counterpart
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_if ( $matches )
	{
		list( $void, $eval, $context ) = $matches;

		$eval = preg_replace_callback( '/([\'"]).*?(?<!\\\\)\1/i', array( $this, 'string_stack' ), $eval );
		$eval = preg_replace_callback( '/\b((?<!::)[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*(?!::))\b/i', array( $this, 'var_replace' ), $eval );
		$eval = preg_replace( '/(?<!=)=(?!=)/i', '==', $eval );
		$eval = str_replace( array_keys( $this->strings ), $this->strings, $eval );

		$context = preg_replace( '/\{hi:\?else\?\}/i', '<?php else: ?>', $context );

		$output = '<?php if (' . $eval . '): ?>';
		$output .= $this->process( $context );
		$output .= '<?php endif; ?>';
		return $output;
	}

	/**
	* Prepare strings for translation
	* @param array $matches Matches in HiEngineParser::process()
	* @param string The PHP replacement for the template tag
	*/
	function hi_quote( $matches )
	{
		$args = preg_split( '/\s+/u', trim( $matches[2] ) );

		preg_match_all( '/"(.+?)(?<!\\\\)"/', $matches[1], $quotes );
		$count = 0;
		$all_vars = array();
		foreach ( $quotes[1] as $index => $quote ) {
			preg_match_all( '/{hi:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*)}/', $quote, $vars, PREG_SET_ORDER );
			foreach ( $vars as $var ) {
				$count++;
				$quote = str_replace( $var[0], '%'.$count.'\$s', $quote );
				$all_vars[] = '{hi:context:' . $var[1] . '}';  //$this->hi_to_var($var[1]);
			}
			$quotes[1][$index] = $quote;
		}
		if ( count( $quotes[1] ) > 1 ) {
			$output = '<?php printf(_n("'.$quotes[1][0].'", "'.$quotes[1][1].'", {hi:context:'.$args[0].'})';
			// Add vars
			if ( count( $all_vars ) > 0 ) {
				$output .= ', ' . implode( ', ', $all_vars );
			}
			array_shift( $args );
		}
		else {
			$output = '<?php _e("'.$quotes[1][0].'"';
			// Add vars
			if ( count( $all_vars ) > 0 ) {
				$output .= ', array(' . implode( ', ', $all_vars ) . ')';
			}
		}

		// Add the domain, if any
		if ( isset( $args[0] ) ) {
			$output .= ', "'.$args[0].'"';
		}

		// Close the tag
		$output .= '); ?>';

		return $output;
	}
}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Tokenizer for HTML.
 * For use by HTMLParser.
 */
class HTMLTokenizer
{
	const NODE_TYPE_TEXT = 1;
	const NODE_TYPE_ELEMENT_OPEN = 2;
	const NODE_TYPE_ELEMENT_CLOSE = 3;
	const NODE_TYPE_PI = 4;
	const NODE_TYPE_COMMENT = 5;
	const NODE_TYPE_CDATA_SECTION = 6;
	const NODE_TYPE_STATEMENT = 7;
	const NODE_TYPE_ELEMENT_EMPTY = 8;

	/* States of the Machine ;p */
	private static $STATE_FINISHED = -1;
	private static $STATE_START = 0;
	private static $STATE_TAG = 1;
	private static $STATE_ELEMENT_OPEN = 2;
	private static $STATE_ELEMENT_CLOSE = 3;
	private static $STATE_STATEMENT = 4;
	private static $STATE_PI = 5;

	/* Character Ranges */
	private static $CHR_TAG_BEGIN = '<';
	private static $CHR_TAG_END = '>';
	private static $CHR_TAG_END_TRIM = '/';
	private static $CHR_ATTRNAME_END = '=';
	private static $CHR_WHITESPACE = " \t\r\n"; // SP, TAB, CR, LF

	private $html;
	private $pos;
	private $len;

	private $state;

	private $nodes;
	private static $empty_elements = array( 'img', 'br', 'hr', 'input', 'area', 'base', 'col', 'link', 'meta', 'param', 'command', 'keygen', 'source' );

	public function __construct( $html, $escape = true )
	{
		$this->html = $html;
		$this->len = strlen( $html );
		$this->pos = 0;
		$this->nodes = new HTMLTokenSet($escape);

		$this->state = self::$STATE_START;
	}

	public function parse()
	{
		while ( $this->has_more() && $this->state != self::$STATE_FINISHED ) {
			switch ( $this->state ) {
				case self::$STATE_START:
					$this->state = $this->parse_start();
					break;
				case self::$STATE_TAG:
					$this->state = $this->parse_tag();
					break;
				case self::$STATE_ELEMENT_OPEN:
					$this->state = $this->parse_element_open();
					break;
				case self::$STATE_ELEMENT_CLOSE:
					$this->state = $this->parse_element_close();
					break;
				case self::$STATE_STATEMENT:
					$this->state = $this->parse_statement();
					break;
				case self::$STATE_PI:
					$this->state = $this->parse_pi();
					break;
				default:
					Error::raise( _t( 'Invalid state %d in %s->parse()', array( $this->state, __CLASS__ ) ) );
					$this->state = self::$STATE_FINISHED;
					break;
			}
		}

		return $this->nodes;
	}

	public function has_more()
	{
		return ( $this->pos < $this->len );
	}

	private function node( $type, $name, $value, $attrs )
	{
		$this->nodes[] = array(
			'type' => $type,
			'name' => $name,
			'value' => $value,
			'attrs' => $attrs,
		);
	}

	private function dec( $n = 1 )
	{
		$this->pos -= $n;
	}

	private function inc( $n = 1 )
	{
		$this->pos += $n;
	}

	private function get()
	{
		if ( $this->has_more() ) {
			return $this->html{ $this->pos++ };
		}

		return null;
	}

	private function peek()
	{
		return $this->html{ $this->pos };
	}

	private function up_to_str( $str )
	{
		$pos = $this->pos;
		$this->pos = strpos( $this->html, $str, $pos );
		if ( $this->pos === false ) {
			// finish
			$this->pos = $this->len;
		}

		return substr( $this->html, $pos, $this->pos - $pos );
	}

	private function up_to_chr( $chr )
	{
		$pos = $this->pos;
		$seg_len = strcspn( $this->html, $chr, $pos );
		$this->pos += $seg_len;

		return substr( $this->html, $pos, $seg_len );
	}

	private function skip_whitespace()
	{
		$this->pos += strspn( $this->html, self::$CHR_WHITESPACE, $this->pos );
	}

	private function parse_start()
	{
		$data = $this->up_to_str( self::$CHR_TAG_BEGIN );
		$this->inc();
		if ( $data != '' ) {
			$this->node( self::NODE_TYPE_TEXT, '#text', $data, null );
		}

		return self::$STATE_TAG;
	}

	private function parse_attributes()
	{
		$attr = array();
		$name = '';

		$this->skip_whitespace();

		// read attribute name
		while ( $name = $this->up_to_chr( self::$CHR_ATTRNAME_END . self::$CHR_TAG_END . self::$CHR_WHITESPACE ) ) {
			$name = strtolower( rtrim( $name, self::$CHR_TAG_END_TRIM ) );
			// skip any whitespace
			$this->skip_whitespace();
			// first non-ws char
			$char = $this->get();
			if ( $char == '=' ) {
				// attribute value follows
				$this->skip_whitespace();
				$char = $this->get();
				if ( $char == '"' ) {
					// double-quoted
					$value = $this->up_to_str( '"' );
					$this->inc();
				}
				elseif ( $char == '\'' ) {
					// single-quoted
					$value = $this->up_to_str( '\'' );
					$this->inc();
				}
				else {
					// bad, bad, bad
					$this->dec();
					$value = $this->up_to_chr( self::$CHR_WHITESPACE . '>' );
				}
			}
			elseif ( $char !== null ) {
				// TODO HTMLParser should handle #IMPLIED attrs
				$value = null;
				$this->dec();
			}
			else {
				// default
				$value = null;
			}
			// store that attribute only if it's not empty
			if ( $name ) {
				$attr[$name] = $value;
			}
			$this->skip_whitespace();
		}

		return $attr;
	}

	private function parse_tag()
	{
		switch ( $this->get() ) {
			case '!':
				return self::$STATE_STATEMENT;
				break;
			case '?':
				// mmmh, PI
				return self::$STATE_PI;
				break;
			case '/':
				return self::$STATE_ELEMENT_CLOSE;
				break;
			default:
				// we just ate the first char of the tagName, oops
				$this->dec();
				return self::$STATE_ELEMENT_OPEN;
		}
	}

	private function parse_element_open()
	{
		$tag = rtrim( $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_WHITESPACE ), self::$CHR_TAG_END_TRIM );
		if ( $tag != '' ) {
			$attr = $this->parse_attributes();
			$char = $this->get();
			if ( ( $char == '/' && $this->peek() == '>' ) || in_array( $tag, self::$empty_elements ) ) {
				// empty element
				if ( $char == '/' && $this->peek() == '>' ) {
					// empty element in collapsed form
					$this->inc(); // skip peeked '>'
				}
				$this->node( self::NODE_TYPE_ELEMENT_EMPTY, $tag, null, $attr );
			}
			else {
				$this->node( self::NODE_TYPE_ELEMENT_OPEN, $tag, null, $attr );
			}
		}

		return self::$STATE_START;
	}

	private function parse_element_close()
	{
		$tag = $this->up_to_chr( self::$CHR_TAG_END );

		if ( $tag != '' ) {
			$char = $this->get();
			if ( $char == '/' && $this->peek() == '>' ) {
				$this->inc();
			}

			$this->node( self::NODE_TYPE_ELEMENT_CLOSE, $tag, null, null );
		}

		return self::$STATE_START;
	}

	private function parse_statement()
	{
		// everything starting with <!
		$nodeName = '#statement';
		$nodeType = self::NODE_TYPE_STATEMENT;

		$char = $this->get();
		if ( $char == '[' ) {
			// CDATA
			// <http://www.w3.org/TR/DOM-Level-2-Core/core.html>
			$nodeName = '#cdata-section';
			$nodeType = self::NODE_TYPE_CDATA_SECTION;

			$this->inc( 6 ); // strlen( 'CDATA[' )
			$data = $this->up_to_str( ']]>' );
			$this->inc( 2 ); // strlen( ']]' )
		}
		elseif ( $char == '-' && $this->peek() == '-' ) {
			// comment
			$nodeName = '#comment';
			$nodeType = self::NODE_TYPE_COMMENT;

			// skip peeked -
			$this->inc();
			// consume text
			$data = $this->up_to_str( '-->' );
			$data = $data; // should trim() upstream
			// skip over final --
			$this->inc( 2 );
		}
		else {
			// some other kind of statement
			$this->dec();
		}

		if ( $nodeType == self::NODE_TYPE_STATEMENT ) {
			$data = '';
			$nodeName = $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_TAG_END_TRIM . self::$CHR_WHITESPACE );
			if ( $this->peek() != '>' ) {
				// there be data or something
				$this->skip_whitespace();
				$data .= $this->up_to_chr( '[>' );
				if ( $this->peek() == '[' ) {
					// internal subset
					$data .= $this->get() . $this->up_to_str( ']' ) . $this->get();
				}
			}
			$data .= $this->up_to_str( '>' );
			// not like anyone uses them, eh?
		}

		// skip over final '>'
		$this->inc();

		if ( $data != '' ) {
			$this->node( $nodeType, $nodeName, $data, null );
		}

		return self::$STATE_START;
	}

	private function parse_pi()
	{
		$target = $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_WHITESPACE );
		$data = $this->up_to_chr( self::$CHR_TAG_END );
		// skip over closing tag
		$this->inc( 1 );

		$this->node( self::NODE_TYPE_PI, $target, $data, array() );

		return self::$STATE_START;
	}

}

?>

repo_name: DaSourcerer/habari


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * HTML Token Set (created by @see HTMLTokenizer)
 */
class HTMLTokenSet implements Iterator, ArrayAccess
{
	protected $tokens = array();

	protected $sliceOffsetBegin  = null;
	protected $sliceOffsetLength = null;

	public $escape;

	public function __construct( $escape = true )
	{
		$this->escape = $escape;
	}

	public function __tostring()
	{
		$out = '';
		foreach ( $this->tokens as $token ) {
			$out .= self::token_to_string( $token, $this->escape );
		}
		return $out;
	}

	/**
	 * Convert a token to a string
	 *
	 * @param array $token The token to convert
	 * @param bool $escape Whether to escape the string that is returned
	 * @return string The string representation of the token
	 */
	public static function token_to_string( array $token, $escape = true )
	{
		switch ( $token['type'] ) {
			case HTMLTokenizer::NODE_TYPE_TEXT:
				return $escape ? Utils::htmlspecialchars( html_entity_decode( $token['value'], ENT_QUOTES, 'UTF-8' ) ) : $token['value'];
				break;

			case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
			case HTMLTokenizer::NODE_TYPE_ELEMENT_EMPTY:
				$out  = '<' . $token['name'];
				if ( isset( $token['attrs'] ) && is_array( $token['attrs'] ) ) {
					foreach ( $token['attrs'] as $attr => $attrval ) {
						$out .= " {$attr}=\"";
						if ( $escape ) {
							$out .= Utils::htmlspecialchars( html_entity_decode( $attrval, ENT_QUOTES, 'UTF-8' ) );
						}
						else {
							$out .= html_entity_decode( $attrval, ENT_QUOTES, 'UTF-8' );
						}
						$out .= '"';
					}
				}
				$out .= '>';
				break;

			case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
				$out = "</{$token['name']}>";
				break;

			case HTMLTokenizer::NODE_TYPE_PI:
				$out = "<?{$token['name']}{$token['value']}>";
				break;

			case HTMLTokenizer::NODE_TYPE_COMMENT:
				$out = "<!--{$token['value']}-->";
				break;

			case HTMLTokenizer::NODE_TYPE_CDATA_SECTION:
				$out = "<![CDATA[{$token['value']}]]>";
				break;

			case HTMLTokenizer::NODE_TYPE_STATEMENT:
				$out = "<!{$token['name']}";
				if ( !empty($token['value']) ) {
					$out .= " {$token['value']}";
				}
				$out .= ">";
				break;
		}
		return $out;
	}

	/**
	 * Get the offset of the last token in this HTMLTokenSet
	 *
	 * @return integer the offset of the last token in this HTMLTokenSet
	 */
	public function get_end_offset()
	{
		return $this->sliceOffsetBegin + $this->sliceOffsetLength;
	}

	/**
	 * Fetch a section of the tokens, based on passed criteria
	 *
	 * @param mixed $names The name of an html tag, or an array of names of html tags to get the tokens of
	 * @param array $attr Any attributes for the token that you want to filter by
	 * @return array An array of the HTMLTokenSets that were found
	 */
	public function slice( $names, array $attr = null )
	{
		$names = (array)$names;
		$ret = array();
		foreach ( $names as $name ) {
			$offset = 0;
			$slices = array();
			while ( $slice = $this->find_slice( $offset, $name, $attr ) ) {
				$slices[] = $slice;
				$offset = $slice->get_end_offset();
			}
			// Need to reverse this because we need to splice the last chunks first
			// if we splice the earlier chunks first, then the offsets get all
			// messed up. Trust me.
			$ret = array_merge( $ret, array_reverse( $slices ) );
		}
		return $ret;
	}

	/**
	 * This is a support function for HTMLTokenSet::slice(). It finds a specific slice
	 * @param integer $offset The offset at which to start looking for relevant tokens
	 * @param string $name The name of the html tag being searched for
	 * @param array $attr The attributes of the html tag being searched for
	 *
	 * @return HTMLTokenSet
	 */
	protected function find_slice( $offset, $name, array $attr )
	{
		// find start:
		$foundStart = false;
		for ( ; $offset < count( $this->tokens ); $offset++ ) {
			// short circuit if possible
			if ( $this->tokens[$offset]['type'] != HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN ) {
				continue;
			}
			if ( $this->tokens[$offset]['name'] != $name ) {
				continue;
			}

			// check attributes
			if ( !count( $attr ) ) {
				$foundStart = true;
				break; // To: FOUNDSTARTBREAKPOINT
			}
			foreach ( $attr as $compareName => $compareVal ) {
				if ( isset( $this->tokens[$offset]['attrs'][$compareName] ) &&
						stripos( $this->tokens[$offset]['attrs'][$compareName], $compareVal ) !== false ) {
					$foundStart = true;
					break 2; // To: FOUNDSTARTBREAKPOINT
				}
			}
		}
		// Fake label: FOUNDSTARTBREAKPOINT

		// short circuit if possible:
		if ( !$foundStart ) {
			return false;
		}

		$startOffset = $offset;

		// find the closing tag
		// (keep a stack so we don't mistake a nested node for this closing node)
		$stackDepth = 0;
		$foundEnd = false;
		for ( ; $offset < count( $this->tokens ); $offset++ ) {
			switch ( $this->tokens[$offset]['type'] ) {
				case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
					if ( $this->tokens[$offset]['name'] == $name ) {
						++$stackDepth;
					}
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
					if ( $this->tokens[$offset]['name'] == $name ) {
						--$stackDepth;
					}
					break;
				// default: skip
			}
			if ( $stackDepth <= 0 ) {
				$foundEnd = true;
				break;
			}
		}

		// short circuit if possible:
		if ( !$foundEnd ) {
			return false;
		}

		$offsetLength = $offset - $startOffset + 1;

		// now, place the found set into a new HTMLTokenSet:
		$slice = new HTMLTokenSet($this->escape);
		$slice->sliceOffsetBegin  = $startOffset;
		$slice->sliceOffsetLength = $offsetLength;
		$slice->tokens = array_slice( $this->tokens, $slice->sliceOffsetBegin, $slice->sliceOffsetLength );
		return $slice;
	}

	/**
	 * Remove the first and last tokens from this HTMLTokenSet
	 *
	 * @return nothing
	 */
	public function trim_container()
	{
		$this->tokens = array_slice( $this->tokens, 1, -1 );
	}

	/**
	 * Replace a set of tokens in this HTMLTokenSet
	 *
	 * @param HTMLTokenSet $slice The tokens to insert in place of the old tokens
	 * @return nothing
	 */
	public function replace_slice( HTMLTokenSet $slice )
	{
		array_splice(
			$this->tokens,
			$slice->sliceOffsetBegin,
			$slice->sliceOffsetLength,
			$slice->tokens
		);
	}

	/**
	 * Replace a full set of tokens with new tokens. The tokens are replaced
	 * in place as well as being returned
	 *
	 * @param string $source The text to create the new set of tokens from
	 * @return HTMLTokenSet The new set of tokens created
	 */
	public function tokenize_replace( $source )
	{
		$ht = new HTMLTokenizer( $source, $this->escape );
		$this->tokens = $ht->parse()->tokens;
		return $this->tokens;
	}

	/**
	 * Insert an HTMLTokenset before the given position
	 *
	 * @param HTMLTokenset $set. The HTMLTokenset to insert
	 * @param integer $pos. The position to insert the HTMLTokenset before
	 * @return Nothing
	 */
	public function insert( HTMLTokenset $set, $pos = 0 )
	{
		$set->end();
		$length = $set->key() - 1;

		$pre = array_slice( $this->tokens, 0, $pos );
		$post = array_slice( $this->tokens, $pos );
		$set->rewind();
		while ( $set->valid() ) {
			$pre[] = $set->current();
			$set->next();
		}
		$this->tokens = array_merge( $pre, $post );
	}

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

	// Iterator implemetation:

	public function rewind()
	{
		reset( $this->tokens );
	}

	public function current()
	{
		return current( $this->tokens );
	}

	public function key()
	{
		return key( $this->tokens );
	}

	public function next()
	{
		return next( $this->tokens );
	}

	public function valid()
	{
		return $this->current() !== false;
	}

	public function end()
	{
		return end( $this->tokens );
	}

	// ArrayAccess implementation

	public function offsetExists( $offset )
	{
		return isset( $this->tokens[ $offset ] );
	}

	public function offsetGet( $offset )
	{
		return $this->tokens[ $offset ];
	}

	public function offsetSet( $offset, $value )
	{
		if ( $offset === null ) {
			$this->tokens[] = $value;
		}
		else {
			$this->tokens[ $offset ] = $value;
		}
	}

	public function offsetUnset( $offset )
	{
		unset( $this->tokens[ $offset ] );
	}
}
?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * This interface should be applied to plugins that implement importing
 * from other blogging tools.
 *
 */
interface Importer
{

	/**
	 * Return a list of names of things that this importer imports
	 *
	 * @return array List of importables.
	 */
	public function filter_import_names( $import_names );

	/**
	 * Return the page content for a specific stage of the import
	 *
	 * @return string Content of import stage
	 */
	public function filter_import_stage( $stageoutput, $import_name, $stage, $step );

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Object metadata
 *
 */
class InfoObject extends InfoRecords
{

	function __construct ( $params )
	{
		// Don't call the parent constructor if this is read-only
		foreach ( $params as $key => $value ) {
			$this->$key = $value;
		}
	}
}
?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Habari InfoRecords Class
 *
 * Base class for managing metadata about various Habari objects
 *
 */
abstract class InfoRecords implements URLProperties
{
	// the info array
	protected $__inforecord_array = array();
	// table which contains the info records
	protected $_table_name;
	// name of the primary key in the info record table
	protected $_key_name;
	// value of the primary key - the master record
	protected $_key_value;
	// set to true only when the inforecords have been loaded
	protected $_loaded = false;

	protected $url_args;

	/**
	 * Takes three parameters. The table of the options table, the name of the master key  and the record_id for which options are managed.
	 * posts take a slug as a record_id, comments take a comment_id and users take a user_id (user_name)
	 *
	 * <b>IMPORTANT:</b> if <tt>$primary_key_value</tt> is not set in the constructor, set_key MUST be called before inforecords can be set. Or bad things
	 * (think swarms of locusts o'er the land) will happen
	 *
	 * @param string $table_name name of the table to insert info (use the DB::o()->table_name syntax)
	 * @param string $key_name name of the primary key (for example "post_id")
	 * @param mixed $key_value (optional) the master record key value (for example, info for post_id = 1 managed by setting this param to 1). Use
	 *		set_key method if not set here.
	 **/
	public function __construct( $table_name, $key_name, $key_value = null )
	{
		$this->_table_name = $table_name;
		$this->_key_name = $key_name;
		$this->_key_value = $key_value;
		$this->_loaded = false;
	}

	/**
	 * Test if the master record value has been set (and thus, safe to set info records).
	 *
	 * @return boolean true if master record value has been set already, false otherwise
	 **/
	public function is_key_set()
	{
		return isset( $this->_key_value );
	}

	/**
	 * function set_key
	 * For use in cases where the master record key is not known at the time of object instantiation (ie: a new post)
	 *
	 * @param mixed $metadata_key the id of the master record (could be int or string, most likely int)
	 *
	 **/
	public function set_key( $metadata_key )
	{
		$this->_key_value = $metadata_key;
	}

	/**
	 * Populate the internal hashmap with the values from the DB.
	 */
	protected function _load()
	{
		if ( $this->_loaded == true ) {
			return;
		}
		if ( empty($this->_key_value) ) {
			$this->_loaded == true;
			return;
		}

		// This InfoRecord is read-only?
		if ( empty($this->_table_name) ) {
			$this->_loaded == true;
			return;
		}

		$result = DB::get_results( '
			SELECT name, value, type
			FROM ' . $this->_table_name . '
			WHERE ' . $this->_key_name . ' = ?',
			array( $this->_key_value )
		);

		foreach ( $result as $result_element ) {
			// XXX is this logic right?
			if ( $result_element->type == 1 ) {
				$this->__inforecord_array[$result_element->name] = array( 'value' => unserialize( $result_element->value ) );
			}
			else {
				$this->__inforecord_array[$result_element->name] = array( 'value' => $result_element->value );
			}
		}

		$this->_loaded = true;
	}

	/**
	 * Fetch info record value.
	 * @param string $name Name of the key to get
	 * @return mixed Stored value for specified key
	 **/
	public function __get ( $name )
	{
		$this->_load();
		if ( ! isset( $this->__inforecord_array[$name] ) ) {
			return false;
		}
		return $this->__inforecord_array[$name]['value'];
	}

	/**
	 * Update the info record.
	 * The value will not be stored in the database until calling $this->commit();
	 *
	 * @param string $name Name of the key to set
	 * @param mixed $value Value to set
	 **/
	public function __set( $name, $value )
	{
		$this->_load();
		$this->__inforecord_array[$name] = array('changed'=>true, 'value'=>$value);
	}

	/**
	 * Test for the existence of specified info value
	 *
	 * @param string $name Name of the option to set
	 * @return boolean true if the info option exists, false in all other cases
	 **/
	public function __isset ( $name )
	{
		$this->_load();
		return isset( $this->__inforecord_array[$name] );
	}

	/**
	 * Remove an info option; immediately unsets from the storage AND removes from database. Use with caution.
	 *
	 * @param string $name Name of the option to unset
	 * @return boolean true if the option is successfully unset, false otherwise
	 **/
	public function __unset( $name )
	{
		$this->_load();
		if ( isset( $this->__inforecord_array[$name] ) ) {
			DB::delete( $this->_table_name, array ( $this->_key_name => $this->_key_value, "name" => $name ) );
			unset( $this->__inforecord_array[$name] );
			return true;
		}
		return false;
	}

	/**
	 * Returns a set of properties used by URL::get to create URLs
	 * @return array Properties of this post used to build a URL
	 **/
	public function get_url_args()
	{
		if ( !$this->url_args ) {
			$this->_load();
			$this->url_args = array_map( function($element) { return $element['value']; }, $this->__inforecord_array );
		}
		return $this->url_args;
	}

	/**
	 * Remove all info options. Primarily used when deleting the parent object.
	 * I.E. when deleting a user, the delete method would call this.
	 *
	 * @return boolean true if the options were successfully unset, false otherwise
	 **/
	public function delete_all()
	{
		// This InfoRecord is read-only?
		if ( empty($this->_table_name) ) {
			return;
		}
		$result = DB::query( '
			DELETE FROM ' . $this->_table_name . '
			WHERE ' . $this->_key_name . ' = ?',
			array( $this->_key_value )
		);
		if ( Error::is_error( $result ) ) {
			$result->out();
			return false;
		}
		$this->__inforecord_array = array();
		return true;
	}

	/**
	 * Commit all of the changed info options to the database.
	 * If this function is not called, then the options will not be written.
	 *
	 * @param mixed $metadata_key (optional) Key to use when writing info data.
	 */
	public function commit( $metadata_key = null )
	{
		if ( isset( $metadata_key ) ) {
			$this->_key_value = $metadata_key;
		}
		// If the info is not already loaded, and the key value is empty,
		// then we don't have enough info to do the commit
		if ( !$this->_loaded && empty($this->_key_value) ) {
			return true;
		}

		foreach ( (array)$this->__inforecord_array as $name=>$record ) {
			if ( isset( $record['changed'] ) && $record['changed'] ) {
				$value = $record['value'];
				if ( is_array( $value ) || is_object( $value ) ) {
					$result = DB::update(
						$this->_table_name,
						array(
							$this->_key_name=>$this->_key_value,
							'name'=>$name,
							'value'=>serialize( $value ),
							'type'=>1
						),
						array( 'name'=>$name, $this->_key_name=>$this->_key_value )
					);
				}
				else {
					$result = DB::update(
						$this->_table_name,
						array(
							$this->_key_name=>$this->_key_value,
							'name'=>$name,
							'value'=>$value,
							'type'=>0
						),
						array('name'=>$name, $this->_key_name=> $this->_key_value)
					);
				}

				if ( Error::is_error( $result ) ) {
					$result->out();
				}
				$this->__inforecord_array[$name] = array('value'=>$value);
			}
		}
	}

	public function count()
	{
		$this->_load();
		return count( $this->__inforecord_array );

	}

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Input filtering functions.
 *
 */
class InputFilter
{
	/**
	 * Allowed elements.
	 */
	private static $whitelist_elements = array(
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.4
		'div', 'span',
		// http://www.w3.org/TR/html4/struct/links.html#h-12.2
		'a',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.1
		'strong', 'em', 'code', 'kbd', 'dfn', 'samp', 'var', 'cite', 'abbr', 'acronym',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.2
		'blockquote', 'q',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.3
		'sub', 'sup',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.1
		'p',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.2.1
		'br',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.4
		'pre',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.4
		'ins', 'del',
		// http://www.w3.org/TR/html4/struct/lists.html#h-10.2
		'ol', 'ul', 'li',
		// http://www.w3.org/TR/html4/struct/lists.html#h-10.3
		'dl', 'dt', 'dd',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.2.1
		'b', 'i', 'u', 's', 'tt',
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.5
		'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.6
		'address',
		// http://www.w3.org/TR/html4/struct/dirlang.html#h-8.2.4
		'bdo',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.1
		'table',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.2
		'caption',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.3
		'thead', 'tfoot', 'tbody',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.4
		'colgroup', 'col',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.5
		'tr',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.6
		'th', 'td',
		// http://www.w3.org/TR/html4/struct/objects.html#h-13.2
		'img',
		// http://www.w3.org/TR/html4/struct/objects.html#h-13.6.1
		'map', 'area',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.2.1 (the non-deprecated ones)
		'tt', 'i', 'b', 'big', 'small',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.3
		'hr',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.2.1
		'frameset',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.2.2
		'frame',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.4.1
		'noframes',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.5
		'iframe',

	);

	/**
	 * Allowed attributes and values.
	 */
	private static $whitelist_attributes = array(
		// attributes that are valid for ALL elements (a subset of coreattrs)
		// elements that only take coreattrs don't need to be listed separately
		'*' => array(
			'lang' => 'language-code',
			'xml:lang' => 'language-code', // this is our xhtml support... all of it
			'dir' => array( 'ltr', 'rtl', ),
			'title' => 'text',
		),
		// http://www.w3.org/TR/html4/struct/links.html#h-12.2
		'a' => array( 'href' => 'uri', ),
		// http://www.w3.org/TR/html4/struct/text.html#h-9.4
		'ins' => array( 'cite' => 'uri', 'datetime' => 'datetime', ),
		'del' => array( 'cite' => 'uri', 'datetime' => 'datetime', ),
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.2
		'blockquote' => array( 'cite' => 'uri', ),
		'q' => array( 'cite' => 'uri', ),
		'img' => array( 'src' => 'uri', 'alt' => 'text' ),
	);

	/**
	 * #EMPTY elements.
	 */
	private static $elements_empty = array(
		'img',
	);

	/**
	 * Protocols that are ok for use in URIs.
	 */
	private static $whitelist_protocols = array(
		'http', 'https', 'ftp', 'mailto', 'irc', 'news', 'nntp', 'callto', 'rtsp', 'mms', 'svn',
	);

	/**
	 * List of all defined named character entities in HTML 4.01 and XHTML.
	 */
	private static $character_entities = array(
		'nbsp', 'iexcl', 'cent', 'pound', 'curren', 'yen', 'brvbar', 'sect', 'uml',
		'copy', 'ordf', 'laquo', 'not', 'shy', 'reg', 'macr', 'deg', 'plusmn',
		'sup2', 'sup3', 'acute', 'micro', 'para', 'middot', 'cedil', 'sup1', 'ordm',
		'raquo', 'frac14', 'frac12', 'frac34', 'iquest', 'Agrave', 'Aacute', 'Acirc',
		'Atilde', 'Auml', 'Aring', 'AElig', 'Ccedil', 'Egrave', 'Eacute', 'Ecirc',
		'Euml', 'Igrave', 'Iacute', 'Icirc', 'Iuml', 'ETH', 'Ntilde', 'Ograve',
		'Oacute', 'Ocirc', 'Otilde', 'Ouml', 'times', 'Oslash', 'Ugrave', 'Uacute',
		'Ucirc', 'Uuml', 'Yacute', 'THORN', 'szlig', 'agrave', 'aacute', 'acirc',
		'atilde', 'auml', 'aring', 'aelig', 'ccedil', 'egrave', 'eacute', 'ecirc',
		'euml', 'igrave', 'iacute', 'icirc', 'iuml', 'eth', 'ntilde', 'ograve',
		'oacute', 'ocirc', 'otilde', 'ouml', 'divide', 'oslash', 'ugrave', 'uacute',
		'ucirc', 'uuml', 'yacute', 'thorn', 'yuml', 'fnof', 'Alpha', 'Beta', 'Gamma',
		'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu',
		'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi',
		'Psi', 'Omega', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
		'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho',
		'sigmaf', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega',
		'thetasym', 'upsih', 'piv', 'bull', 'hellip', 'prime', 'Prime', 'oline',
		'frasl', 'weierp', 'image', 'real', 'trade', 'alefsym', 'larr', 'uarr',
		'rarr', 'darr', 'harr', 'crarr', 'lArr', 'uArr', 'rArr', 'dArr', 'hArr',
		'forall', 'part', 'exist', 'empty', 'nabla', 'isin', 'notin', 'ni', 'prod',
		'sum', 'minus', 'lowast', 'radic', 'prop', 'infin', 'ang', 'and', 'or',
		'cap', 'cup', 'int', 'there4', 'sim', 'cong', 'asymp', 'ne', 'equiv', 'le',
		'ge', 'sub', 'sup', 'nsub', 'sube', 'supe', 'oplus', 'otimes', 'perp',
		'sdot', 'lceil', 'rceil', 'lfloor', 'rfloor', 'lang', 'rang', 'loz',
		'spades', 'clubs', 'hearts', 'diams', 'quot', 'amp', 'lt', 'gt', 'OElig',
		'oelig', 'Scaron', 'scaron', 'Yuml', 'circ', 'tilde', 'ensp', 'emsp',
		'thinsp', 'zwnj', 'zwj', 'lrm', 'rlm', 'ndash', 'mdash', 'lsquo', 'rsquo',
		'sbquo', 'ldquo', 'rdquo', 'bdquo', 'dagger', 'Dagger', 'permil', 'lsaquo',
		'rsaquo', 'euro',
	);

	private static $character_entities_re = '';

	public static function __static ( ) {
		self::$whitelist_elements = Plugins::filter( 'inputfilter_whitelist_elements', self::$whitelist_elements );
		self::$whitelist_attributes = Plugins::filter( 'inputfilter_whitelist_attributes', self::$whitelist_attributes );
		self::$elements_empty = Plugins::filter( 'inputfilter_elements_empty', self::$elements_empty );
		self::$whitelist_protocols = Plugins::filter( 'inputfilter_whitelist_protocols', self::$whitelist_protocols );
		self::$character_entities = Plugins::filter( 'inputfilter_character_entities', self::$character_entities );
		self::$character_entities_re = Plugins::filter( 'inputfilter_character_entities_re', self::$character_entities_re );
	}

	/**
	 * Perform all filtering, return new string.
	 * @param string $str Input string.
	 * @return string Filtered output string.
	 */
	public static function filter( $str )
	{
		if ( !MultiByte::valid_data( $str ) ) {
			return '';
		}
		else {
			do {
				$_str = $str;
				$str = self::strip_nulls( $str );
				$str = self::strip_illegal_entities( $str );
				$str = self::filter_html_elements( $str );
			} while ( $str != $_str );

			return $str;
		}
	}

	/**
	 * Remove nulls, return new string.
	 * @param string $str Input string.
	 * @return string Filtered output string.
	 */
	public static function strip_nulls( $str )
	{
		$str = str_replace( '\0', '', $str );

		return $str;
	}

	/**
	 * Callback function for strip_illegal_entities, do not use.
	 * @access private
	 * @param array $m matches
	 */
	public static function _validate_entity( $m )
	{
		$is_valid = false;

		/**
		 * valid entity references have the form
		 *   /&named([;<\n\r])/
		 * for named entities, or
		 *   /&#(\d{1,5}|[xX][0-9a-fA-F]{1,4})([;<\n\r])/
		 * for numeric character references
		 */

		$e = trim( $m[1] );
		$r = $m[2];
		if ( $r == ';' ) {
			$r = '';
		}

		if ( $e{0} == '#' ) {
			$e = strtolower( $e );
			if ( $e{1} == 'x' ) {
				$e = hexdec( substr( $e, 2 ) );
			}
			else {
				$e = substr( $e, 1 );
			}

			// numeric character references may only have values in the range 0-65535 (16 bit)
			// we strip null, though, just for kicks
			$is_valid = ( intval( $e ) > 0 && intval( $e ) <= 65535 );

			if ( $is_valid ) {
				// normalize to decimal form
				$e = '#' . intval( $e ) . ';';
			}
		}
		else {
			if ( self::$character_entities_re == '' ) {
				self::$character_entities_re = ';(' . implode( '|', self::$character_entities ) . ');';
			}

			// named entities must be known
			$is_valid = preg_match( self::$character_entities_re, $e, $matches );

			// XXX should we map named entities to their numeric equivalents?

			if ( $is_valid ) {
				// normalize to name and nothing but the name... eh.
				$e = $matches[1] . ';';
			}
		}

		return $is_valid ? '&' . $e . $r : '';
	}

	/**
	 * Remove illegal entities, return new string.
	 * @param string $str Input string.
	 * @return string Filtered output string.
	 */
	public static function strip_illegal_entities( $str )
	{
		$str = preg_replace_callback( "/&([^;<\n\r]+)([;<\n\r])/", array( __CLASS__, '_validate_entity' ), $str );

		return $str;
	}

	/**
	 * This really doesn't belong here. It should also be done much better. This is a nasty, NASTY kludge.
	 */
	public static function parse_url( $url )
	{
		// result array
		$r = array(
			'scheme' => '',
			'host' => '',
			'port' => '',
			'user' => '',
			'pass' => '',
			'path' => '',
			'query' => '',
			'fragment' => '',
			//
			'is_relative' => false,
			'is_pseudo' => false,
			'is_error' => false,
			//
			'pseudo_args' => '',
		);

		// sanitize the url
		$sanitized = html_entity_decode( $url, null, 'UTF-8' );		// make double-sure we've converted all entities
		$sanitized = filter_var( $sanitized, FILTER_SANITIZE_URL );		// strip everything but ascii, essentially

		$sanitized_scheme = parse_url( $sanitized, PHP_URL_SCHEME );

		// Use PHP's parse_url to get the basics
		$parsed = parse_url( $url );
		if ( $parsed == false ) {
			$r['is_error'] = true;
			return $r;
		}
		$r = array_merge( $r, $parsed );

		// replace the scheme with the one we got from the fully-sanitized string
		$r['scheme'] = $sanitized_scheme;

		$r['is_pseudo'] = !in_array( $r['scheme'], array( 'http', 'https', '' ) );
		$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] );

		if ( $r['is_pseudo'] ) {
			$r['pseudo_args'] = $r['path'];
			$r['path'] = '';
		}

		return $r;
	}

	/**
	 * Restore a URL separated by a parse_url() call.
	 * @param $parsed_url array An array as returned by parse_url()
	 */
	public static function glue_url( $parsed_url )
	{
		if ( ! is_array( $parsed_url ) ) {
			return false;
		}

		$res = '';
		$res .= $parsed_url['scheme'];
		if ( $parsed_url['is_pseudo'] || in_array( strtolower( $parsed_url['scheme'] ), array( 'mailto', 'callto' ) ) ) {
			$res .= ':';
		}
		else {
			if ( ! $parsed_url['is_relative'] ) {
				$res .= '://';
			}
		}
		if ( $parsed_url['is_pseudo'] ) {
			$res .= $parsed_url['pseudo_args'];
		}
		else {
			// user[:pass]@
			if ( $parsed_url['user'] ) {
				$res .= $parsed_url['user'];
				if ( $parsed_url['pass'] ) {
					$res .= ':' . $parsed_url['pass'];
				}
				$res .= '@';
			}
			$res .= $parsed_url['host'];
			if ( !empty( $parsed_url['port'] ) ) {
				if ( array_key_exists( $parsed_url['scheme'], Utils::scheme_ports() ) && Utils::scheme_ports( $parsed_url['scheme'] ) == $parsed_url['port'] ) {
					// default port for this scheme, do nothing
				}
				else {
					$res .= ':' . $parsed_url['port'];
				}
			}
			if ( !empty( $parsed_url['path'] ) ) {
				$res .= $parsed_url['path'];
			}
			else {
				$res .= '/';
			}
			if ( $parsed_url['query'] ) {
				$res .= '?' . $parsed_url['query'];
			}
			if ( $parsed_url['fragment'] ) {
				$res .= '#' . $parsed_url['fragment'];
			}
		}

		return $res;
	}

	private static function check_attr_value( $k, $v, $type )
	{
		if ( is_array( $type ) ) {
			// array of allowed values, exact matches only
			return in_array( $v, $type, true );
		}
		else {
			// data type
			switch ( $type ) {
				case 'uri':
					// RfC 2396 <http://www.ietf.org/rfc/rfc2396.txt>
					$bits = self::parse_url( $v );
					return $bits['is_relative'] || in_array( $bits['scheme'], self::$whitelist_protocols );
					break;
				case 'language-code':
					// RfC 1766 <http://www.ietf.org/rfc/rfc1766.txt>
					//    Language-Tag = Primary-tag *( "-" Subtag )
					//    Primary-tag = 1*8ALPHA
					//    Subtag = 1*8ALPHA
					return preg_match( '/^[a-zA-Z]{1,8}(?:-[a-zA-Z]{1,8})*$/i', $v );
					break;
				case 'text':
					// XXX is this sufficient?
					return is_string( $v );
					break;
				case 'datetime':
					// <http://www.w3.org/TR/1998/NOTE-datetime-19980827>
					// <http://www.w3.org/TR/html4/types.html#h-6.11>
					//    YYYY-MM-DDThh:mm:ssTZD
					return preg_match( '/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9](?:Z|[\+-][0-2][0-9]:[0-5][0-9])$/', $v );
					break;
				default:
					Error::raise( _t( 'Unknown attribute type "%s" in %s', array( $type, __CLASS__ ) ) );
					return false;
			}
		}
	}

	/**
	 * @todo TODO must build DOM to really properly remove offending elements
	 * @todo TODO properly filter URLs
	 */
	public static function filter_html_elements( $str )
	{
		$tokenizer = new HTMLTokenizer( $str );

		// tokenize, baby
		$tokens = $tokenizer->parse();

		// filter token stream
		$filtered = new HTMLTokenSet;
		$stack = array();
		foreach ( $tokens as $node ) {
			switch ( $node['type'] ) {
				case HTMLTokenizer::NODE_TYPE_TEXT:
					$node['value'] = html_entity_decode( $node['value'], ENT_QUOTES, MultiByte::hab_encoding() );
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
				case HTMLTokenizer::NODE_TYPE_ELEMENT_EMPTY:
					// is this element allowed at all?
					if ( ! in_array( strtolower( $node['name'] ), self::$whitelist_elements ) ) {
						if ( ! in_array( strtolower( $node['name'] ), self::$elements_empty ) ) {
							array_push( $stack, $node['name'] );
						}
						//$node = null; //remove the node completely
						// convert the node to text
						$node = array(
							'type' => HTMLTokenizer::NODE_TYPE_TEXT,
							'name' => '#text',
							'value' => HTMLTokenSet::token_to_string( $node ),
							'attrs' => array(),
						);
					}
					else {
						// check attributes
						foreach ( $node['attrs'] as $k => $v ) {

							$attr_ok = false;

							// if the attribute is in the global whitelist and validates
							if ( array_key_exists( strtolower( $k ), self::$whitelist_attributes['*'] ) && self::check_attr_value( strtolower( $k ), $v, self::$whitelist_attributes['*'][ strtolower( $k ) ] ) ) {
								$attr_ok = true;
							}

							// if there is a whitelist for this node and this attribute is in that list and it validates
							if ( array_key_exists( strtolower( $node['name'] ), self::$whitelist_attributes ) && array_key_exists( strtolower( $k ), self::$whitelist_attributes[ strtolower( $node['name'] ) ] ) && self::check_attr_value( strtolower( $k ), $v, self::$whitelist_attributes[ strtolower( $node['name'] ) ][ strtolower( $k ) ] ) ) {
								$attr_ok = true;
							}

							// if it wasn't in one of the whitelists or failed its check, remove it
							if ( $attr_ok != true ) {
								unset( $node['attrs'][$k] );
							}
						}
					}
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
					if ( ! in_array( strtolower( $node['name'] ), self::$whitelist_elements ) ) {
						if ( strtolower( $temp = array_pop( $stack ) ) !== strtolower( $node['name'] ) ) {
							// something weird happened (Luke, use the DOM!)
							array_push( $stack, $temp );
						}
						//$node = null;
						//convert the node to text
						$node = array(
							'type' => HTMLTokenizer::NODE_TYPE_TEXT,
							'name' => '#text',
							'value' => HTMLTokenSet::token_to_string( $node ),
							'attrs' => array(),
						);
					}
					break;
				case HTMLTokenizer::NODE_TYPE_PI:
				case HTMLTokenizer::NODE_TYPE_COMMENT:
				case HTMLTokenizer::NODE_TYPE_CDATA_SECTION:
				case HTMLTokenizer::NODE_TYPE_STATEMENT:
				default:
					$node = null;
					break;
			}

			if ( $node != null ) {
				$filtered[] = $node;
			}
		}

		// rebuild our output string
		return preg_replace( '#<([^>\s]+)(?:\s+[^>]+)?></\1>#u', '', (string) $filtered );
	}
}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * IsContent Interface for Habari
 * Defines an interface for classes to return the type of content they are
 *
 * @version $Id$
 * @copyright 2008
 */
interface IsContent
{

	/**
	 * Returns the content type of the object instance
	 *
	 * @return array An array of content types that this object represents, starting with the most specific
	 */
	function content_type();

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Habari LogEntry class
 *
 * Represents a log entry
 *
 * @todo Apply system error handling
 */
class LogEntry extends QueryRecord
{

	/**
	 * Defined event severities
	 *
	 * @final
	 */
	private static function severities($translate = true)
	{
		return array(
			0 => $translate ? _t( 'any' ) : 'any',
			1 => $translate ? _t( 'none' ) : 'none',
			2 => $translate ? _t( 'debug' ) : 'debug',
			3 => $translate ? _t( 'info' ) : 'info',
			4 => $translate ? _t( 'notice' ) : 'notice',
			5 => $translate ? _t( 'warning' ) : 'warning',
			6 => $translate ? _t( 'err' ) : 'err',
			7 => $translate ? _t( 'crit' ) : 'crit',
			8 => $translate ? _t( 'alert' ) : 'alert',
			9 => $translate ? _t( 'emerg' ) : 'emerg',
		);
	}

	/**
	 * Cache for log_types
	 */
	private static $types = array();

	/**
	 * Return the defined database columns for an Event
	 *
	 * @return array Array of columns in the LogEntry table
	 */
	public static function default_fields()
	{
		return array(
			'id' => 0,
			'user_id' => 0,
			'type_id' => null,
			'severity_id' => null,
			'message' => '',
			'data' => '',
			'timestamp' => HabariDateTime::date_create(),
			'ip' => 0,
		);
	}

	/**
	 * Constructor for the LogEntry class
	 *
	 * @param array $paramarray an associative array of initial LogEntry field values
	 */
	public function __construct( $paramarray = array() )
	{
		// Defaults
		$this->fields = array_merge(
			self::default_fields(),
			$this->fields );

		parent::__construct( $paramarray );
		if ( !isset( $this->fields['module'] ) ) {
			$this->fields['module'] = 'habari';
		}
		if ( !isset( $this->fields['type'] ) ) {
			$this->fields['type'] = 'default';
		}
		if ( !isset( $this->fields['severity'] ) ) {
			$this->fields['severity'] = 'info';
		}
		if ( isset( $this->fields['timestamp'] ) ) {
			$this->fields['timestamp'] = HabariDateTime::date_create( $this->fields['timestamp'] );
		}
		$this->exclude_fields( 'id' );
	}

	/**
	 * Returns an associative array of LogEntry types
	 *
	 * @param bool whether to force a refresh of the cached values
	 * @return array An array of log entry type names => integer values
	 */
	public static function list_logentry_types( $force = false )
	{
		if ( $force || empty( self::$types ) ) {
			self::$types = array();
			$res = DB::get_results( 'SELECT id, module, type FROM {log_types}' );
			foreach ( $res as $x ) {
				self::$types[ $x->module ][ $x->type ] = $x->id;
			}
		}
		return self::$types;
	}

	/**
	 * Return an array of Severities
	 * @return array An array of severity ID => name pairs
	**/
	public static function list_severities()
	{
		$results = self::severities();
		unset($results[1]);
		return $results;
	}

	/**
	 * Returns an array of LogEntry modules
	 * @param bool Whether to refresh the cached values
	 * @return array An array of LogEntry module id => name pairs
	**/
	public static function list_modules( $refresh = false )
	{
		$types = self::list_logentry_types( $refresh );
		foreach ( $types as $module => $types ) {
			$modules[] = $module;
		}
	}

	/**
	 * Returns an array of LogEntry types
	 * @param bool Whether to refresh the cached values
	 * @return array An array of LogEntry id => name pairs
	**/
	public static function list_types( $refresh = false )
	{
		$types = array();
		$matrix = self::list_logentry_types( $refresh );
		foreach ( $matrix as $module => $module_types ) {
			$types = array_merge( $types, $module_types );
		}
		return array_flip( $types );
	}

	/**
	 * Get the integer value for the given severity, or false.
	 *
	 * @param string $severity The severity name
	 * @return mixed numeric value for the given severity, or false
	 */
	public static function severity( $severity )
	{
		if ( is_numeric( $severity ) && array_key_exists( $severity, self::severities() ) ) {
			return $severity;
		}
		return array_search( $severity, self::severities(false) );
	}

	/**
	 * Get the string representation of the severity numeric value.
	 *
	 * @param integer $severity The severity index.
	 * @return string The string name of the severity, or 'Unknown'.
	 */
	public static function severity_name( $severity )
	{
		$severities = self::severities();
		return isset( $severities[$severity] ) ? $severities[$severity] : _t( 'Unknown' );
	}

	/**
	 * Get the integer value for the given module/type, or false.
	 *
	 * @param string $module the module
	 * @param string $type the type
	 * @return mixed numeric value for the given module/type, or false
	 */
	public static function type( $module, $type )
	{
		self::list_logentry_types();
		if ( array_key_exists( $module, self::$types ) && array_key_exists( $type, self::$types[$module] ) ) {
			return self::$types[$module][$type];
		}
		if ( $type != null && $module != null ) {
			EventLog::register_type( $type, $module );
			self::list_logentry_types( true );
			return self::$types[$module][$type];
		}
		return false;
	}

	/**
	 * Insert this LogEntry data into the database
	 */
	public function insert()
	{
		if ( isset( $this->fields['severity'] ) ) {
			$this->severity_id = LogEntry::severity( $this->fields['severity'] );
			unset( $this->fields['severity'] );
		}
		if ( isset( $this->fields['module'] ) && isset( $this->fields['type'] ) ) {
			$this->type_id = LogEntry::type( $this->fields['module'], $this->fields['type'] );
			unset( $this->fields['module'] );
			unset( $this->fields['type'] );
		}

		// if we're set to only log entries greater than a sertain level, make sure we're that level or higher
		if ( $this->fields['severity_id'] < Options::get( 'log_min_severity' ) ) {
			return;
		}

		// make sure data is a string and can be stored. lots of times it's convenient to hand in an array of data values
		if ( is_array( $this->fields['data'] ) || is_object( $this->fields['data'] ) ) {
			$this->fields['data'] = serialize( $this->fields['data'] );
		}

		Plugins::filter( 'insert_logentry', $this );
		parent::insertRecord( DB::table( 'log' ) );

		$this->id = DB::last_insert_id();

	}

	/**
	 * Return a single requested log entry.
	 *
	 *
	 * $log= LogEntry::get( array( 'id' => 5 ) );
	 *
	 *
	 * @param array $paramarray An associated array of parameters, or a querystring
	 * @return object LogEntry The first log entry that matched the given criteria
	 */
	public static function get( $paramarray = array() )
	{
		// Default parameters.
		$defaults = array (
			'fetch_fn' => 'get_row',
		);
		$user = User::identify();
		if ( $user->loggedin ) {
			$defaults['where'][] = array(
				'user_id' => $user->id,
			);
		}
		foreach ( $defaults['where'] as $index => $where ) {
			$defaults['where'][$index] = array_merge( $where, Utils::get_params( $paramarray ) );
		}
		// Make sure we fetch only a single event. (LIMIT 1)
		$defaults['limit'] = 1;

		return EventLog::get( $defaults );
	}

	/**
	 * Return the log entry's event type.
	 *
	 * $log->type
	 *
	 * @return string Human-readable event type
	 */
	public function get_event_type()
	{
		$type = DB::get_value( 'SELECT type FROM {log_types} WHERE id=' . $this->type_id );
		return $type ? $type : _t( 'Unknown' );
	}

	/**
	 * Return the log entry's event module.
	 *
	 * $log->module
	 *
	 * @return string Human-readable event module
	 */
	public function get_event_module()
	{
		$module = DB::get_value( 'SELECT module FROM {log_types} WHERE id=' . $this->type_id );
		return $module ? $module : _t( 'Unknown' );
	}

	/**
	 * Return the log entry's event severity.
	 *
	 * $log->severity
	 *
	 * @return string Human-readable event severity
	 */
	public function get_event_severity()
	{
		return self::severity_name( $this->severity_id );
	}

	/**
	 * function delete
	 * Deletes this logentry
	 */
	public function delete()
	{
		$allow = true;
		$allow = Plugins::filter( 'logentry_delete_allow', $allow, $this );
		if ( ! $allow ) {
			return;
		}
		Plugins::act( 'logentry_delete_before', $this );
		return parent::deleteRecord( DB::table( 'log' ), array( 'id'=>$this->id ) );
		Plugins::act( 'logentry_delete_after', $this );
	}

	/**
	 * Overrides QueryRecord __get to implement custom object properties
	 *
	 * @param string Name of property to return
	 * @return mixed The requested field value
	 */
	public function __get( $name )
	{
		$fieldnames = array_merge( array_keys( $this->fields ), array( 'module', 'type', 'severity' ) );
		$filter = false;
		if ( !in_array( $name, $fieldnames ) && strpos( $name, '_' ) !== false ) {
			$field_matches = implode('|', $fieldnames);
			if(preg_match( '/^(' . $field_matches . ')_(.+)$/', $name, $matches )) {
				list( $junk, $name, $filter )= $matches;
			}
		}

		switch ( $name ) {
			case 'module':
				$out = $this->get_event_module();
				break;
			case 'type':
				$out = $this->get_event_type();
				break;
			case 'severity':
				$out = $this->get_event_severity();
				break;
			default:
				$out = parent::__get( $name );
				break;
		}
		$out = Plugins::filter( "logentry_{$name}", $out, $this );
		if ( $filter ) {
			$out = Plugins::filter( "logentry_{$name}_{$filter}", $out, $this );
		}
		return $out;
	}

	/**
	 * Overrides QueryRecord __set to implement custom object properties
	 *
	 * @param string Name of property to return
	 * @return mixed The requested field value
	 */
	public function __set( $name, $value )
	{
		switch ( $name ) {
			case 'timestamp':
				if ( !( $value instanceOf HabariDateTime ) ) {
					$value = HabariDateTime::date_create( $value );
				}
				break;
		}
		return parent::__set( $name, $value );
	}

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Media access
 *
 * @version $Id$
 * @copyright 2008
 *
 * @todo Handle all error conditions using exceptions
 */
class Media
{
	static $silos = null;
	const THUMBNAIL_WIDTH = 200;
	const THUMBNAIL_HEIGHT = 100;

	/**
	 * Retrieve an array of media assets stored at a virtual path
	 *
	 * @param string $path The virtual path of the directory to retrieve
	 * @return array A list of files and directories in that path
	 */
	public static function dir( $path = '' )
	{
		if ( $path == '' ) {
			self::init_silos();
			$dirs = array();
			foreach ( self::$silos as $siloname => $silo ) {
				$info = $silo->silo_info();
				if ( isset( $info['icon'] ) ) {
					$dirs[] = new MediaAsset( $siloname, true, array(), $info['icon'] );
				}
				else {
					$dirs[] = new MediaAsset( $siloname, true, array(), null );
				}

			}
			return $dirs;
		}
		else {
			$silo = Media::get_silo( $path, true );
			return $silo->silo_dir( $path );
		}
	}

	/**
	 * Get the MediaAsset stored at a virtual path
	 *
	 * @param string $path The virtual path of the file to retrieve
	 * @param array $qualities Qualities of the image to return (such as 'thumbnail' or 'size')
	 * @return MediaAsset The requested asset
	 */
	public static function get( $path, $qualities = null )
	{
		$silo = Media::get_silo( $path, true );
		return $silo->silo_get( $path, $qualities );
	}

	/**
	 * Fetch an empty MediaAsset with the available metadata keys pre-allocated
	 * <code>
	 * $asset = Media::prepare('silotype/foo/bar');
	 * foreach($asset->get_info() as $key => $value) echo "$key : $value";
	 * </code>
	 *
	 * @param string $path The virtual path at which the asset will be stored
	 * @return MediaAsset An empty, intialized asset instance
	 */
	public static function prepare( $path )
	{
		$silo = Media::get_silo( $path, true );
		return $silo->silo_new( $path );
	}

	/**
	 * Store the asset at the specified virtual path
	 *
	 * @param MediaAsset $filedata The asset to store
	 * @param string $path The virtual path where the asset will be stored
	 * @return boolean true on success
	 */
	public static function put( $filedata, $path = null )
	{
		if ( !$path ) {
			$path = $filedata->path;
		}
		$silo = Media::get_silo( $path, true );
		if ( $path == '' ) {
			return false;
		}
		else {
			return $silo->silo_put( $path, $filedata );
		}
	}

	/**
	 * Delete the asset at the specified virtual path
	 *
	 * @param string $path The virtual path of the asset to delete
	 * @return boolean true on success
	 */
	public static function delete( $path )
	{
		$silo = Media::get_silo( $path, true );
		if ( $path == '' ) {
			return false;
		}
		else {
			return $silo->silo_delete( $path );
		}
	}

	/**
	 * Copy the asset using the specified from and to paths
	 *
	 * @param string $pathfrom The virtual path source
	 * @param string $pathto The virtual path destination
	 * @return boolean true on success
	 */
	public static function copy( $pathfrom, $pathto )
	{
		if ( $source = Media::get( $pathfrom ) ) {
			return Media::put( $pathto, $source );
		}
		else {
			return false;
		}
	}

	/**
	 * Move the asset using the specified from and to paths
	 * A shortcut for Media::copy() then Media::delete()
	 *
	 * @param string $pathfrom The virtual path source
	 * @param string $pathto The virtual path destination
	 * @return boolean true on success
	 */
	public static function move( $pathfrom, $pathto )
	{
		if ( Media::copy( $pathfrom, $pathto ) ) {
			return Media::delete( $pathfrom );
		}
		else {
			return false;
		}
	}

	/**
	 * Return an array of highlighted (featured) assets from all silos
	 *
	 * @param mixed $path The name of a silo or a silo instance.  If empty, all silos are returned.
	 * @return array An array of MediaAsset highlight assets
	 */
	public static function highlights( $path = null )
	{
		$highlights = array();
		if ( isset( $path ) ) {
			$silo = Media::get_silo( $path );
			return $silo->silo_highlights();
		}
		else {
			self::init_silos();
			foreach ( self::$silos as $silo ) {
				$highlights = $highlights + self::highlights( $silo );
			}
		}
		return $highlights;
	}

	/**
	 * Return the permissions available to the current user on the specified path
	 *
	 * @param mixed $path The name of a silo or a silo instance.
	 * @return array An array of permission constants (read, write, etc.)
	 */
	public static function permissions( $path )
	{
		$silo = Media::get_silo( $path, true );
		return $silo->silo_permissions( $path );
	}

	/**
	 * Return the instance of a silo
	 *
	 * @param mixed $silo A silo instance or the name of a silo
	 * @param boolean $parse_path If true, parse the siloname from the path and return the remainder path by reference
	 * @return MediaSilo The requested silo
	 */
	public static function get_silo( &$silo, $parse_path = false )
	{
		if ( $silo instanceof MediaSilo ) {
			return $silo;
		}
		$siloname = $silo;
		if ( $parse_path ) {
			$exp = explode( '/', $silo, 2 );
			if ( count( $exp ) > 1 ) {
				list( $siloname, $silo ) = $exp;
			}
			else {
				$siloname = $exp[0];
				$silo = '';
			}
		}
		self::init_silos();
		return self::$silos[$siloname];
	}

	/**
	 * Initialize the internal list of silo instances
	 */
	public static function init_silos()
	{
		if ( empty( self::$silos ) ) {
			$tempsilos = Plugins::get_by_interface( 'MediaSilo' );
			self::$silos = array();
			foreach ( $tempsilos as $eachsilo ) {
				$info = $eachsilo->silo_info();
				if ( isset( $info['name'] ) ) {
					self::$silos[$info['name']] = $eachsilo;
				}
			}
		}
	}
}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * MediaAsset represents a file or directory in the media system
 *
 */
class MediaAsset
{
	const MODE_NONE = 0;
	const MODE_DATA = 1;
	const MODE_FILE = 2;
	const MODE_STREAM = 3;
	const MODE_UPLOAD = 4;

	protected $path;
	protected $is_dir;
	protected $content = null;
	protected $props = array();
	public $icon = null;
	protected $filename;
	protected $mode = self::MODE_NONE;

	/**
	 * MediaAsset constructor
	 *
	 * @param string $path The path of the asset
	 * @param boolean $is_dir true if the asset is a directory
	 * @param array $properties An associative array of property values
	 */
	public function __construct( $path, $is_dir, $properties = array(), $icon = null )
	{
		$this->path = $path;
		$this->is_dir = $is_dir;
		$this->props = $properties;
		$this->icon = $icon;
	}

	/**
	 * Return the content of the asset
	 *
	 * @return mixed The asset content
	 */
	protected function _get()
	{
		if ( empty( $this->content ) ) {
			switch ( $this->mode ) {
				case self::MODE_DATA:
					$this->content = Media::get( $this->path );
					break;
				case self::MODE_FILE:
					$this->content = file_get_contents( $this->filename );
					break;
				case self::MODE_STREAM:
					$this->content = stream_get_contents( $this->stream );
					fclose( $this->stream );
					break;
			}
		}
		return $this->content;
	}

	/**
	 * Set the content of this asset
	 *
	 * @param mixed $content The asset content
	 */
	protected function _set( $content )
	{
		$this->content = $content;
	}

	public function get_props()
	{
		return array_merge(
			array(
				'path' => $this->path,
				'basename' => basename( $this->path ),
				'title' => basename( $this->path ),
			),
			$this->props
		);
	}

	/**
	 * Retrieve attributes about this asset
	 *
	 * @param string $name The name of the property to retrieve
	 * @return mixed The value requested
	 */
	public function __get( $name )
	{
		switch ( $name ) {
			case 'content':
				$output = $this->_get();
				break;
			case 'is_dir':
				$output = $this->is_dir;
				break;
			case 'path':
				$output = $this->path;
				break;
			case 'basename':
				$output = basename( $this->path );
				break;
			default:
				$output = $this->props[$name];
				break;
		}

		$output = Plugins::filter( 'mediaasset', $output, $name );

		return $output;

	}

	/**
	 * Set attributes about this asset
	 *
	 * @param string $name The name of the property to set
	 * @param mixed $value The value to set
	 */
	public function __set( $name, $value )
	{
		switch ( $name ) {
			case 'content':
				$this->mode = self::MODE_DATA;
				return $this->_set( $value );
			case 'is_dir':
			case 'path':
				break;
			default:
				$this->props[$name] = $value;
				break;
		}
	}

	/**
	 * Load the asset data from a file
	 *
	 * @param string $file The filename to load
	 */
	public function load( $file )
	{
		$this->mode = self::MODE_FILE;
		$this->filename = $file;
	}

	/**
	 * Save the asset data to a file
	 *
	 * @param string $file The destination filename
	 * @return boolean True on success
	 */
	public function save( $file )
	{
		switch ( $this->mode ) {
			case self::MODE_DATA:
				return file_put_contents( $file, $this->content ) !== false;
				break;
			case self::MODE_UPLOAD:
				return move_uploaded_file( $this->filename, $file ) !== false;
				break;
			case self::MODE_FILE:
				return copy( $this->filename, $file );
				break;
			case self::MODE_STREAM:
				$dest = fopen( $file, 'w+' );
				stream_copy_to_stream( $this->stream, $dest );
				fclose( $this->stream );
				return fclose( $dest );
				break;
		}
	}

	/**
	 * Load the asset data from an upload
	 *
	 * @param array $files The $_FILES array created when a file is uploaded
	 */
	public function upload( $files )
	{
		$this->mode = self::MODE_UPLOAD;
		$this->filename = $files['tmp_name'];
	}

	/**
	 * Shortcut for putting an asset into the correct silo based on its path
	 *
	 * @return boolean True on success
	 */
	public function put()
	{
		return Media::put( $this );
	}

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * MediaSilo interface
 *
 * @version $Id$
 * @copyright 2007
 */
interface MediaSilo
{
	const PERM_READ = 1;
	const PERM_WRITE = 2;

	/**
	 * Return basic information about this silo
	 *   name- The name of the silo, used as the root directory for media in this silo
	 **/
	public function silo_info();

	/**
	 * Return directory contents for the silo path
	 * @param string $path The path to retrieve the contents of
	 * @return array An array of MediaAssets describing the contents of the directory
	 **/
	public function silo_dir( $path );

	/**
	 * Get the file from the specified path
	 * @param string $path The path of the file to retrieve
	 * @param array $qualities Qualities that specify the version of the file to retrieve.
	 * @return MediaAsset The requested asset
	 **/
	public function silo_get( $path, $qualities = null );

	/**
	 * Store the specified media at the specified path
	 * @param string $path The path of the file to retrieve
	 * @param MediaAsset The asset to store
	 **/
	public function silo_put( $path, $filedata );

	/**
	 * Delete the file at the specified path
	 * @param string $path The path of the file to retrieve
	 **/
	public function silo_delete( $path );

	/**
	 * Retrieve a set of highlights from this silo
	 * This would include things like recently uploaded assets, or top downloads
	 * @return array An array of MediaAssets to highlihgt from this silo
	 **/
	public function silo_highlights();

	/**
	 * Retrieve the permissions for the current user to access the specified path
	 * @param string $path The path to retrieve permissions for
	 * @return array An array of permissions constants (MediaSilo::PERM_READ, MediaSilo::PERM_WRITE)
	 **/
	public function silo_permissions( $path );

}

?>


code:
<?php
/*
 * @package Habari
 *
 */

/*
 * Habari MultiByte Class
 *
 * Provides multibyte character set services,
 * a necessity since all of Habari's internal string
 * manipulations are done in UTF-8. Currently
 * this class is a wrapper around mbstring functions.
 *
 */
class MultiByte
{

	const USE_MBSTRING = 1;

	/*
	* @var $hab_enc String holding the current encoding the class is using
	*/
	static $hab_enc = 'UTF-8';
	/*
	* @var $use_library Integer denoting the current multibyte
	* library the class is using
	*/
	private static $use_library = self::USE_MBSTRING;

	/**
	* function __construct
	*
	* An empty constructor since all functions are static
	*/
	private function __construct()
	{
	}

	/*
	* function hab_encoding
	*
	* Sets and returns the internal encoding.
	*
	* @param $use_enc string. The encoding to be used
	*
	* @return string. If $enc is null, returns the current
	* encoding. If $enc is not null, returns the old encoding
	*/
	public static function hab_encoding( $use_enc = null )
	{
		if ( $use_enc === null ) {
			return self::$hab_enc;
		}
		else {
			$old_enc = self::$hab_enc;
			self::$hab_enc = $use_enc;
			return $old_enc;
		}
	}

	/*
	* function library
	*
	* Sets and returns the multibyte library being used internally
	*
	* @param $int The new library to use. One of the self::USE_* constants, null to simply return, or false to disable and use native non-multibyte-safe PHP methods.
	*
	* @return mixed  If $new_library is null, returns the current library
	* being used. If $new_library has a valid value, returns the old library,
	* else returns false.
	*/
	public static function library( $new_library = null )
	{
		if ( $new_library === null ) {
			return self::$use_library;
		}
		else if ( $new_library === self::USE_MBSTRING ) {
			$old_library = self::$use_library;
			self::$use_library = $new_library;
			return $old_library;

		}
		else if ( $new_library === false ) {
			$old_library = self::$use_library;
			self::$use_library = $new_library;
			return $old_library;
		}
		else {
			return false;
		}
	}

	/*
	* function convert_encoding
	*
	* Converts a string's encoding to a new encoding
	*
	* @param $str string. The string who's encoding is being changed.
	* @param $use_enc string. The encoding to convert to. If not set,
	* the internal encoding will be used.
	* @param $from_enc string. encoding before conversion. If not set,
 	* encoding is detected automatically.
	*
	* @return mixed  The  source string in the new encoding or boolean false.
	*/
	public static function convert_encoding( $str, $use_enc = null, $from_enc = null )
	{
		$ret = false;

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			if ( $from_enc == null ) {
				$from_enc = MultiByte::detect_encoding( $str );
			}
			$ret = mb_convert_encoding( $str, $enc, $from_enc );
		}

		return $ret;
	}

	/*
	* function detect_encoding
	*
	* Detects the encoding being used for a string
	*
	* @param $str string. The string whose encoding is being detected
	*
	* @return mixed The source string's detected encoding, or boolean false.
	*/
	public static function detect_encoding( $str )
	{
		$enc = false;

		if ( self::$use_library == self::USE_MBSTRING ) {
			// get original detection order
			$old_order = mb_detect_order();
			// make sure  ISO-8859-1 is included
			mb_detect_order( array( 'ASCII', 'JIS', 'UTF-8', 'ISO-8859-1', 'EUC-JP', 'SJIS' ) );
			//detect the encoding . the detected encoding may be wrong, but it's better than guessing
			$enc = mb_detect_encoding( $str );
			// reset detection order
			mb_detect_order( $old_order );
		}

		return $enc;
	}

	/*
	* function detect_bom_encoding
	*
	* Detects the encoding being used for a string using the existence
	* of a byte order mark
	*
	* @param $str string. The string whose encoding is being detected
	*
	* @return mixed The source string's detected encoding, or boolean false.
	*/
	public static function detect_bom_encoding( $str )
	{
		$ret = false;
		if ( "\xFE\xFF" == substr( 0, 2, $source_contents ) ) {
			$ret = 'UTF-16BE';
		}
		else if ( "\xFF\xFE" == substr( 0, 2, $source_contents ) ) {
			$ret = 'UTF-16LE';
		}
		else if ( "\xEF\xBB\xBF" == substr( 0, 3, $source_contents ) ) {
			$ret = 'UTF-8';
		}

		return $ret;
	}

	/*
	* function substr
	*
	* Get a section of a string
	*
	* @param $str string. The original string
	* @param $begin. integer. The beginning character of the string to return.
	* @param $len integer. How long the returned string should be. If $len is
	* not set, the section of the string from $begin to the end of the string is
	* returned.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return mixed The  section of the source string requested in the encoding requested or false.
	* If $len is not set, returns substring from $begin to end of string.
	*
	*/
	public static function substr( $str, $begin, $len = null, $use_enc = null )
	{
		$ret = false;

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			if ( ! isset( $len ) ) {
				$len = MultiByte::strlen( $str ) - $begin;
			}
			$ret = mb_substr( $str, $begin, $len, $enc );
		}
		else {
			$ret = substr( $str, $begin, $len );
		}
		return $ret;
	}

	/*
	* function strlen
	*
	* Gets the length of a string in characters
	*
	* @param $str string. The string who's length is being returned.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return integer. The length in characters of the string, or the length in bytes if a valid
	* multibyte library isn't loaded.
	*/
	public static function strlen( $str, $use_enc = null )
	{
		$len = 0;

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$len = mb_strlen( $str, $enc );
		}
		else {
			$len = strlen( $str );
		}

		return $len;
	}

	/*
	* function strpos
	*
	* Find position of first occurrence of string in a string
	*
	* @param $haysack string. The string being checked.
	* @param $needle. string. The position counted from the beginning of haystack .
	* @param $offset integer. The search offset. If it is not specified, 0 is used.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return mixed The  section of the source string requested in the encoding requested or false.
	* If $len is not set, returns substring from $begin to end of string.
	*
	*/
	public static function strpos( $haysack, $needle, $offset = 0, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_strpos( $haysack, $needle, $offset, $enc );
		}
		else {
			$ret = strpos( $haysack, $needle, $offset );
		}
		return $ret;
	}

	/*
	* function stripos
	*
	* Find position of first occurrence of string in a string. Case insensitive.
	*
	* @param $haysack string. The string being checked.
	* @param $needle. string. The position counted from the beginning of haystack .
	* @param $offset integer. The search offset. If it is not specified, 0 is used.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return mixed The  section of the source string requested in the encoding requested or false.
	* If $len is not set, returns substring from $begin to end of string.
	*
	*/
	public static function stripos( $haysack, $needle, $offset = 0, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_stripos( $haysack, $needle, $offset, $enc );
		}
		else {
			$ret = stripos( $haysack, $needle, $offset );
		}
		return $ret;
	}

	/*
	* function strrpos
	*
	* Find position of last occurrence of string in a string.
	*
	* @param $haysack string. The string being checked.
	* @param $needle. string. The position counted from the beginning of haystack .
	* @param $offset integer. The search offset. If it is not specified, 0 is used.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return mixed The  section of the source string requested in the encoding requested or false.
	* If $len is not set, returns substring from $begin to end of string.
	*
	*/
	public static function strrpos( $haysack, $needle, $offset = 0, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_strrpos( $haysack, $needle, $offset, $enc );
		}
		else {
			$ret = strrpos( $haysack, $needle, $offset );
		}
		return $ret;
	}

	/*
	* function strripos
	*
	* Find position of last occurrence of string in a string. Case insensitive.
	*
	* @param $haysack string. The string being checked.
	* @param $needle. string. The position counted from the beginning of haystack .
	* @param $offset integer. The search offset. If it is not specified, 0 is used.
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return mixed The  section of the source string requested in the encoding requested or false.
	* If $len is not set, returns substring from $begin to end of string.
	*
	*/
	public static function strripos( $haysack, $needle, $offset = 0, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_strripos( $haysack, $needle, $offset, $enc );
		}
		else {
			$ret = strripos( $haysack, $needle, $offset );
		}
		return $ret;
	}

	/*
	 * function strtolower
	 *
	 * Converts a multibyte string to lowercase. If a valid multibyte library
	* isn't loaded, strtolower() will be used, which can lead to unexpected results.
	 *
	 * @param $str string. The string to lowercase
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	 *
	 * @return string. The lowercased string.
	*/
	public static function strtolower( $str, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_strtolower( $str, $enc );
		}
		else {
			$ret = strtolower( $str );
		}

		return $ret;
	}

	/*
	* function strtoupper
	*
	* Converts a multibyte string to uppercase. If a valid multibyte library
	* isn't loaded, strtoupper() will be used, which can lead to unexpected results.
	*
	* @param $str string. The string to uppercase
	* @param $use_enc string. The encoding to be used. If not set,
	* the internal encoding will be used.
	*
	* @return string. The uppercased string.
	*/
	public static function strtoupper( $str, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			$ret = mb_strtoupper( $str, $enc );
		}
		else {
			$ret = strtoupper( $str );
		}

		return $ret;
	}

	/**
	 * Determines if the passed string is valid character data (according to mbstring)
	 *
	 * @param string $str the string to check
	 * @return bool
	 */
	public static function valid_data( $str, $use_enc = null )
	{
		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {
			return mb_check_encoding( $str, $enc );
		}

		return true;
	}

	/**
	 * Makes a string's first character uppercase
	 *
	 * @see http://php.net/ucfirst
	 * @param string $str The string to capitalize.
	 * @param string $use_enc The encoding to be used. If null, the internal encoding will be used.
	 * @return string The capitalized string.
	 */
	public static function ucfirst ( $str, $use_enc = null )
	{

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {

			// get the first character
			$first = self::substr( $str, 0, 1, $enc );

			// uppercase it
			$first = self::strtoupper( $first, $enc );

			// get the rest of the characters
			$last = self::substr( $str, 1, null, $enc );

			// put them back together
			$ret = $first . $last;

		}
		else {
			$ret = ucfirst( $str );
		}

		return $ret;

	}

	/**
	 * Makes a string's first character lowercase
	 *
	 * @see http://php.net/ucfirst
	 * @param string $str The string to lowercase.
	 * @param string $use_enc The encoding to be used. If null, the internal encoding will be used.
	 * @return string The lowercased string.
	 */
	public static function lcfirst ( $str, $use_enc = null )
	{

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {

			// get the first character
			$first = self::substr( $str, 0, 1, $enc );

			// lowercase it
			$first = self::strtolower( $first, $enc );

			// get the rest of the characters
			$last = self::substr( $str, 1, null, $enc );

			// put them back together
			$ret = $first . $last;

		}
		else {

			// lcfirst() is php 5.3+ so we'll emulate it
			$first = substr( $str, 0, 1 );
			$first = strtolower( $first );

			$last = substr( $str, 1 );

			$ret = $first . $last;

		}

		return $ret;

	}

	/**
	 * Replace all occurrences of the search string with the replacement string.
	 *
	 * @see http://php.net/str_replace
	 * @param mixed $search A string or an array of strings to search for.
	 * @param mixed $replace A string or an array of strings to replace search values with.
	 * @param string $subject The string to perform the search and replace on.
	 * @param int $count If passed, this value will hold the number of matched and replaced needles.
	 * @param string $use_enc The encoding to be used. If null, the internal encoding will be used.
	 * @return string The subject with replaced values.
	 */
	public static function str_replace ( $search, $replace, $subject, &$count = 0, $use_enc = null )
	{

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {

			// if search is an array and replace is not, we need to make replace an array and pad it to the same number of values as search
			if ( is_array( $search ) && !is_array( $replace ) ) {
				$replace = array_fill( 0, count( $search ), $replace );
			}

			// if search is an array and replace is as well, we need to make sure replace has the same number of values - pad it with empty strings
			if ( is_array( $search ) && is_array( $replace ) ) {
				$replace = array_pad( $replace, count( $search ), '' );
			}

			// if search is not an array, make it one
			if ( !is_array( $search ) ) {
				$search = array( $search );
			}

			// if replace is not an array, make it one
			if ( !is_array( $replace ) ) {
				$replace = array( $replace );
			}

			// if subject is an array, recursively call ourselves on each element of it
			if ( is_array( $subject ) ) {
				foreach ( $subject as $k => $v ) {
					$subject[ $k ] = self::str_replace( $search, $replace, $v, $count, $use_enc );
				}

				return $subject;
			}

			// now we've got an array of characters and arrays of search / replace characters with the same values - loop and replace them!
			$search_count = count( $search );	// we modify $search, so we can't include it in the condition next
			for ( $i = 0; $i < $search_count; $i++ ) {

				// the values we'll match
				$s = array_shift( $search );
				$r = array_shift( $replace );

				// to avoid an infinite loop if you're replacing with a value that contains the subject we get the position of each instance first
				$positions = array();

				$offset = 0;
				while ( self::strpos( $subject, $s, $offset, $enc ) !== false ) {

					// get the position
					$pos = self::strpos( $subject, $s, $offset, $enc );

					// add it to the list
					$positions[] = $pos;

					// and set the offset to skip over this value
					$offset = $pos + self::strlen( $s, $enc );

				}

				// if we pick through from the beginning, our positions will change if the replacement string is longer
				// instead, we pick through from the last place
				$positions = array_reverse( $positions );

				// now that we've got the position of each one, just loop through that and replace them
				foreach ( $positions as $pos ) {

					// pull out the part before the string
					$before = self::substr( $subject, 0, $pos, $enc );

					// pull out the part after
					$after = self::substr( $subject, $pos + self::strlen( $s, $enc ), null, $enc );

					// now we have the string in two parts without the string we're searching for
					// put it back together with the replacement
					$subject = $before . $r . $after;

					// increment our count, a replacement was made
					$count++;

				}

			}

		}
		else {

			$subject = str_replace( $search, $replace, $subject, $count );

		}

		return $subject;

	}

	/**
	 * Replace all occurrences of the search string with the replacement string.
	 *
	 * @see http://php.net/str_ireplace
	 * @param mixed $search A string or an array of strings to search for.
	 * @param mixed $replace A string or an array of strings to replace search values with.
	 * @param string $subject The string to perform the search and replace on.
	 * @param int $count If passed, this value will hold the number of matched and replaced needles.
	 * @param string $use_enc The encoding to be used. If null, the internal encoding will be used.
	 * @return string The subject with replaced values.
	 */
	public static function str_ireplace( $search, $replace, $subject, &$count = 0, $use_enc = null )
	{

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {

			// if search is an array and replace is not, we need to make replace an array and pad it to the same number of values as search
			if ( is_array( $search ) && !is_array( $replace ) ) {
				$replace = array_fill( 0, count( $search ), $replace );
			}

			// if search is an array and replace is as well, we need to make sure replace has the same number of values - pad it with empty strings
			if ( is_array( $search ) && is_array( $replace ) ) {
				$replace = array_pad( $replace, count( $search ), '' );
			}

			// if search is not an array, make it one
			if ( !is_array( $search ) ) {
				$search = array( $search );
			}

			// if replace is not an array, make it one
			if ( !is_array( $replace ) ) {
				$replace = array( $replace );
			}

			// if subject is an array, recursively call ourselves on each element of it
			if ( is_array( $subject ) ) {
				foreach ( $subject as $k => $v ) {
					$subject[ $k ] = self::str_ireplace( $search, $replace, $v, $count, $use_enc );
				}

				return $subject;
			}

			$search_count = count( $search );	// we modify $search, so we can't include it in the condition next
			for ( $i = 0; $i < $search_count; $i++ ) {

				// the values we'll match
				$s = array_shift( $search );
				$r = array_shift( $replace );

				// to avoid an infinite loop if you're replacing with a value that contains the subject we get the position of each instance first
				$positions = array();

				$offset = 0;
				while ( self::stripos( $subject, $s, $offset, $enc ) !== false ) {

					// get the position
					$pos = self::stripos( $subject, $s, $offset, $enc );

					// add it to the list
					$positions[] = $pos;

					// and set the offset to skip over this value
					$offset = $pos + self::strlen( $s, $enc );

				}

				// if we pick through from the beginning, our positions will change if the replacement string is longer
				// instead, we pick through from the last place
				$positions = array_reverse( $positions );

				// now that we've got the position of each one, just loop through that and replace them
				foreach ( $positions as $pos ) {

					// pull out the part before the string
					$before = self::substr( $subject, 0, $pos, $enc );

					// pull out the part after
					$after = self::substr( $subject, $pos + self::strlen( $s, $enc ), null, $enc );

					// now we have the string in two parts without the string we're searching for
					// put it back together with the replacement
					$subject = $before . $r . $after;

					// increment our count, a replacement was made
					$count++;

				}

			}

		}
		else {

			$subject = str_ireplace( $search, $replace, $subject, $count );

		}

		return $subject;

	}

	/**
	 * Uppercase the first character of each word in a string.
	 *
	 * From php.net/ucwords:
	 * 	The definition of a word is any string of characters that is immediately after a whitespace
	 * 	(These are: space, form-feed, newline, carriage return, horizontal tab, and vertical tab).
	 *
	 * @see http://php.net/ucwords
	 * @param string $str The input string.
	 * @param string $use_enc The encoding to be used. If null, the internal encoding will be used.
	 * @return string The modified string.
	 */
	public static function ucwords ( $str, $use_enc = null )
	{

		$enc = self::$hab_enc;
		if ( $use_enc !== null ) {
			$enc = $use_enc;
		}

		if ( self::$use_library == self::USE_MBSTRING ) {

			$delimiters = array(
				chr( 32 ),	// space
				chr( 12 ),	// form-feed
				chr( 10 ),	// newline
				chr( 13 ),	// carriage return
				chr( 9 ),	// horizontal tab
				chr( 11 ),	// vertical tab
			);

			// loop through the delimiters and explode the string by each one
			foreach ( $delimiters as $d ) {

				$pieces = explode( $d, $str );

				for ( $i = 0; $i < count( $pieces ); $i++ ) {

					// capitalize each word
					$pieces[ $i ] = self::ucfirst( $pieces[ $i ], $enc );

				}

				// put the string back together
				$str = implode( $d, $pieces );

			}

		}
		else {
			$str = ucwords( $str );
		}

		return $str;
	}

}

?>