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

/**
 * Habari EventLog class
 *
 * @todo Apply system error handling
 */
class EventLog extends ArrayObject
{
	protected $get_param_cache; // Stores info about the last set of data fetched that was not a single value

	/**
	 * Returns properties of a EventLog object.
	 * This is the function that returns information about the set of log entries that
	 * was requested.  This function should offer property names that are identical
	 * to properties of instances of the URL class.  A call to EventLog::get()
	 * without parameters should return mostly the same property values as the
	 * global $url object for the request.  The difference would occur when
	 * the data returned doesn't necessarily match the request, such as when
	 * several log entries are requested, but only one is available to return.
	 *
	 * @param string The name of the property to return.
	 */
	public function __get( $name )
	{
		switch( $name ) {
			case 'onelogentry':
				return ( count( $this ) == 1 );
		}

		return false;
	}

	/**
	 * Adds a logging type to the lookup table
	 *
	 * @param string $type The type of the error
	 * @param string $module The module of the error
	 */
	public static function register_type( $type = 'default', $module = null )
	{
		try {
			DB::query( 'INSERT INTO {log_types} (module, type) VALUES (?,?)', array( self::get_module($module), $type ) );
		}
		catch( Exception $e ) {
			// Don't really care if there's a duplicate.
		}
	}

	/**
	 * Removes a logging type from the lookup table
	 *
	 * @param string $type The type of the error
	 * @param string $module The module of the error
	 */
	public static function unregister_type( $type = 'default', $module = null )
	{
		$id = DB::get_value( "SELECT id FROM {log_types} WHERE module = ? and type = ?", array( self::get_module( $module ), $type ) );
		if( $id ) {
			if( !DB::exists( '{log}', array( 'type_id' => $id ) ) ) {
				DB::delete( '{log_types}', array( 'id' => $id ) );
			}
		}
	}

	/**
	 * Write an entry to the event log.
	 *
	 * @param string $message The message
	 * @param string $severity The severity
	 * @param string $type The type
	 * @param string $module The module
	 * @param mixed $data The data
	 * @return object LogEntry The inserted LogEntry object
	 */
	public static function log( $message, $severity = 'info', $type = 'default', $module = null, $data = null )
	{
		$module = self::get_module( $module );
		$log = new LogEntry( array(
			'message' => $message,
			'severity' => $severity,
			'module' => $module,
			'type' => $type,
			'data' => $data,
			'ip' => sprintf("%u", ip2long( $_SERVER['REMOTE_ADDR'] ) ),
		) );
		$user = User::identify();
		if ( $user->loggedin ) {
			$log->user_id = $user->id;
		}
		$log->insert();
		return $log;
	}

	/**
	 * Get the module in which the logged code was executed
	 *
	 * @param integer $level How many backtrace calls to go back through the trace
	 * @return string The classname or .php module in which the log code was called.
	 */
	public static function get_module( $module = null, $level = 2 )
	{
		if ( is_null( $module ) ) {
			$bt = debug_backtrace();
			$last = $bt[$level];
			$module = isset( $last['class'] ) ? $last['class'] : basename( $last['file'], '.php' );
		}
		return $module;
	}

	/**
	 * Returns a LogEntry or EventLog array based on supplied parameters.
	 * By default,fetch as many entries as pagination allows and order them in a descending fashion based on timestamp.
	 *
	 * @todo Cache query results.
	 * @param array $paramarry An associated array of parameters, or a querystring
	 * @return array An array of LogEntry objects, or a single LogEntry object, depending on request
	 */
	public static function get( $paramarray = array() )
	{
		$params = array();
		$fns = array( 'get_results', 'get_row', 'get_value' );
		$select = '';

		// Put incoming parameters into the local scope
		$paramarray = Utils::get_params( $paramarray );

		$select_fields = LogEntry::default_fields();
		if(!isset($paramarray['return_data'])) {
			unset($select_fields['data']);
		}
		foreach ( $select_fields as $field => $value ) {
			$select .= ( '' == $select )
				? "{log}.$field"
				: ", {log}.$field";
		}
		// Default parameters.
		$orderby = 'ORDER BY timestamp DESC, id DESC';
		$limit = Options::get( 'pagination' );

		// Get any full-query parameters
		$possible = array( 'orderby', 'fetch_fn', 'count', 'month_cts', 'nolimit', 'index', 'limit', 'offset' );
		foreach ( $possible as $varname ) {
			if ( isset( $paramarray[$varname] ) ) {
				$$varname = $paramarray[$varname];
			}
		}

		foreach ( $paramarray as $key => $value ) {
			if ( 'orderby' == $key ) {
				$orderby = ' ORDER BY ' . $value;
				continue;
			}
		}

		// Transact on possible multiple sets of where information that is to be OR'ed
		if ( isset( $paramarray['where'] ) && is_array( $paramarray['where'] ) ) {
			$wheresets = $paramarray['where'];
		}
		else {
			$wheresets = array( array() );
		}

		$wheres = array();
		$join = '';
		if ( isset( $paramarray['where'] ) && is_string( $paramarray['where'] ) ) {
			$wheres[] = $paramarray['where'];
		}
		else {
			foreach ( $wheresets as $paramset ) {
				// Safety mechanism to prevent empty queries
				$where = array( '1=1' );
				$paramset = array_merge( ( array ) $paramarray, ( array ) $paramset );

				if ( isset( $paramset['id'] ) && is_numeric( $paramset['id'] ) ) {
					$where[] = "id= ?";
					$params[] = $paramset['id'];
				}
				if ( isset( $paramset['user_id'] ) ) {
					$where[] = "user_id= ?";
					$params[] = $paramset['user_id'];
				}
				if ( isset( $paramset['severity'] ) && ( 'any' != LogEntry::severity_name( $paramset['severity'] ) ) ) {
					$where[] = "severity_id= ?";
					$params[] = LogEntry::severity( $paramset['severity'] );
				}
				if ( isset( $paramset['type_id'] ) ) {
					if ( is_array( $paramset['type_id'] ) ) {
						$types = array_filter( $paramset['type_id'], 'is_numeric' );
						if ( count( $types ) ) {
							$where[] = 'type_id IN (' . implode( ',', $types ) . ')';
						}
					}
					else {
						$where[] = 'type_id = ?';
						$params[] = $paramset['type_id'];
					}
				}
				if ( isset( $paramset['ip'] ) ) {
					$where[] = 'ip = ?';
					$params[] = $paramset['ip'];
				}

				/* do searching */
				if ( isset( $paramset['criteria'] ) ) {
					preg_match_all( '/(?<=")(\w[^"]*)(?=")|([:\w]+)/u', $paramset['criteria'], $matches );
					foreach ( $matches[0] as $word ) {
						if(preg_match('%^id:(\d+)$%i', $word, $special_crit)) {
							$where[] .= '(id = ?)';
							$params[] = $special_crit[1];
						}
						else {
							$where[] .= "(message LIKE CONCAT('%',?,'%'))";
							$params[] = $word;
						}
					}
				}

				/**
				 * Build the pubdate
				 * If we've got the day, then get the date.
				 * If we've got the month, but no date, get the month.
				 * If we've only got the year, get the whole year.
				 *
				 * @todo Ensure that we've actually got all the needed parts when we query on them
				 * @todo Ensure that the value passed in is valid to insert into a SQL date (ie '04' and not '4')
				 */
				if ( isset( $paramset['day'] ) ) {
					$where[] = 'timestamp BETWEEN ? AND ?';
					$startDate = sprintf( '%d-%02d-%02d', $paramset['year'], $paramset['month'], $paramset['day'] );
					$startDate = HabariDateTime::date_create( $startDate );
					$params[] = $startDate->sql;
					$params[] = $startDate->modify( '+1 day' )->sql;
					//$params[] = date( 'Y-m-d H:i:s', mktime( 0, 0, 0, $paramset['month'], $paramset['day'], $paramset['year'] ) );
					//$params[] = date( 'Y-m-d H:i:s', mktime( 23, 59, 59, $paramset['month'], $paramset['day'], $paramset['year'] ) );
				}
				elseif ( isset( $paramset['month'] ) ) {
					$where[] = 'timestamp BETWEEN ? AND ?';
					$startDate = sprintf( '%d-%02d-%02d', $paramset['year'], $paramset['month'], 1 );
					$startDate = HabariDateTime::date_create( $startDate );
					$params[] = $startDate->sql;
					$params[] = $startDate->modify( '+1 month' )->sql;
					//$params[] = date( 'Y-m-d H:i:s', mktime( 0, 0, 0, $paramset['month'], 1, $paramset['year'] ) );
					//$params[] = date( 'Y-m-d H:i:s', mktime( 23, 59, 59, $paramset['month'] + 1, 0, $paramset['year'] ) );
				}
				elseif ( isset( $paramset['year'] ) ) {
					$where[] = 'timestamp BETWEEN ? AND ?';
					$startDate = sprintf( '%d-%02d-%02d', $paramset['year'], 1, 1 );
					$startDate = HabariDateTime::date_create( $startDate );
					$params[] = $startDate->sql;
					$params[] = $startDate->modify( '+1 year' )->sql;
					//$params[] = date( 'Y-m-d H:i:s', mktime( 0, 0, 0, 1, 1, $paramset['year'] ) );
					//$params[] = date( 'Y-m-d H:i:s', mktime( 0, 0, -1, 1, 1, $paramset['year'] + 1 ) );
				}

				$wheres[] = ' (' . implode( ' AND ', $where ) . ') ';
			}
		}

		if ( isset( $index ) && is_numeric( $index ) ) {
			$offset = ( intval( $index ) - 1 ) * intval( $limit );
		}

		if ( isset( $fetch_fn ) ) {
			if ( ! in_array( $fetch_fn, $fns ) ) {
				$fetch_fn = $fns[0];
			}
		}
		else {
			$fetch_fn = $fns[0];
		}

		if ( isset( $count ) ) {
			$select = "COUNT($count)";
			$fetch_fn = 'get_value';
			$orderby = '';
		}
		if ( isset( $limit ) ) {
			$limit = " LIMIT $limit";
			if ( isset( $offset ) ) {
				$limit .= " OFFSET $offset";
			}
		}
		// If the month counts are requested, replace the select clause
		if( isset( $paramset['month_cts'] ) ) {
			$select = 'MONTH(FROM_UNIXTIME(timestamp)) AS month, YEAR(FROM_UNIXTIME(timestamp)) AS year, COUNT(*) AS ct';
			$groupby = 'year, month';
			$orderby = ' ORDER BY year, month';
		}
		if ( isset( $nolimit ) || isset( $month_cts ) ) {
			$limit = '';
		}

		$query = '
			SELECT ' . $select . '
			FROM {log} ' . $join;

		if ( count( $wheres ) > 0 ) {
			$query .= ' WHERE ' . implode( " \nOR\n ", $wheres );
		}
		$query .= ( ! isset($groupby) || $groupby == '' ) ? '' : ' GROUP BY ' . $groupby;
		$query .= $orderby . $limit;
		// Utils::debug( $paramarray, $fetch_fn, $query, $params );

		DB::set_fetch_mode( PDO::FETCH_CLASS );
		DB::set_fetch_class( 'LogEntry' );
		$results = DB::$fetch_fn( $query, $params, 'LogEntry' );

			// If the fetch callback function is not get_results,
			// return an EventLog ArrayObject filled with the results as LogEntry objects.
		if ( 'get_results' != $fetch_fn ) {
			return $results;
		}
		elseif ( is_array( $results ) ) {
			$c = __CLASS__;
			$return_value = new $c( $results );
			$return_value->get_param_cache = $paramarray;
			return $return_value;
		}
	}

}

?>


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

/**
 * Habari FeedbackHandler Class
 * Deals with feedback mechnisms: Commenting, Pingbacking, and the like.
 *
 */
class FeedbackHandler extends ActionHandler
{
	/**
	 * function add_comment
	 * adds a comment to a post, if the comment content is not NULL
	 * @param array An associative array of content found in the $_POST array
	 */
	public function act_add_comment()
	{
		Utils::check_request_method( array( 'POST' ) );

		// We need to get the post anyway to redirect back to the post page.
		$post = Post::get( array( 'id' => $this->handler_vars['id'] ) );
		if ( ! $post ) {
			// trying to comment on a non-existent post?  Weirdo.
			header( 'HTTP/1.1 403 Forbidden', true, 403 );
			die();
		}

		// Allow theme action hooks to work
		Themes::create();
		$form = $post->comment_form();
		$form->get( null, false );

		// Disallow non-FormUI comments
		if ( !$form->submitted ) {
			// Trying to submit a non-FormUI comment
			header( 'HTTP/1.1 403 Forbidden', true, 403 );
			die();
		}
		else {

			// To be eventually incorporated more fully into FormUI.
			Plugins::act( 'comment_form_submit', $form );

			if ( $form->success ) {
				$this->add_comment(
					$post->id,
					$form->cf_commenter->value,
					$form->cf_email->value,
					$form->cf_url->value,
					$form->cf_content->value,
					$form->get_values()
				);
			}
			else {
				Session::error( _t( 'There was a problem submitting your comment.' ) );
				foreach( $form->validate() as $error ) {
					Session::error( $error );
				}
				$form->bounce();
			}
		}
	}

	/**
	 * Add a comment to the site
	 *
	 * @param mixed $post A Post object instance or Post object id
	 * @param string $name The commenter's name
	 * @param string $email The commenter's email address
	 * @param string $url The commenter's website URL
	 * @param string $content The comment content
	 * @param array $extra An associative array of extra values that should be considered
	 */
	function add_comment( $post, $name = null, $email = null, $url = null, $content = null, $extra = null )
	{
		if ( is_numeric( $post ) ) {
			$post = Post::get( array( 'id' => $post ) );
		}

		if ( !$post instanceof Post ) {
			// Not sure what you're trying to pull here, but that's no good
			header( 'HTTP/1.1 403 Forbidden', true, 403 );
			die();
		}

		// let's do some basic sanity checking on the submission
		if ( ( 1 == Options::get( 'comments_require_id' ) ) && ( empty( $name ) || empty( $email ) ) ) {
			Session::error( _t( 'Both name and e-mail address must be provided.' ) );
		}

		if ( empty( $content ) ) {
			Session::error( _t('You did not provide any content for your comment!' ) );
		}

		if ( Session::has_errors() ) {
			// save whatever was provided in session data
			Session::add_to_set( 'comment', $name, 'name' );
			Session::add_to_set( 'comment', $email, 'email' );
			Session::add_to_set( 'comment', $url, 'url' );
			Session::add_to_set( 'comment', $content, 'content' );
			// now send them back to the form
			Utils::redirect( $post->permalink . '#respond' );
		}

		if ( $post->info->comments_disabled ) {
			// comments are disabled, so let's just send
			// them back to the post's permalink
			Session::error( _t( 'Comments on this post are disabled!' ) );
			Utils::redirect( $post->permalink );
		}

		/* Sanitize data */
		foreach ( array( 'name', 'url', 'email', 'content' ) as $k ) {
			$$k = InputFilter::filter( $$k );
		}

		/* Sanitize the URL */
		if ( !empty( $url ) ) {
			$parsed = InputFilter::parse_url( $url );
			if ( $parsed['is_relative'] ) {
				// guess if they meant to use an absolute link
				$parsed = InputFilter::parse_url( 'http://' . $url );
				if ( ! $parsed['is_error'] ) {
					$url = InputFilter::glue_url( $parsed );
				}
				else {
					// disallow relative URLs
					$url = '';
				}
			}
			if ( $parsed['is_pseudo'] || ( $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https' ) ) {
				// allow only http(s) URLs
				$url = '';
			}
			else {
				// reconstruct the URL from the error-tolerant parsing
				// http:moeffju.net/blog/ -> http://moeffju.net/blog/
				$url = InputFilter::glue_url( $parsed );
			}
		}
		if ( preg_match( '/^\p{Z}*$/u', $content ) ) {
			Session::error( _t( 'Comment contains only whitespace/empty comment' ) );
			Utils::redirect( $post->permalink );
		}

		/* Create comment object*/
		$comment = new Comment( array(
			'post_id' => $post->id,
			'name' => $name,
			'email' => $email,
			'url' => $url,
			'ip' => sprintf( "%u", ip2long( $_SERVER['REMOTE_ADDR'] ) ),
			'content' => $content,
			'status' => Comment::STATUS_UNAPPROVED,
			'date' => HabariDateTime::date_create(),
			'type' => Comment::COMMENT,
		) );

		// Should this really be here or in a default filter?
		// In any case, we should let plugins modify the status after we set it here.
		$user = User::identify();
		if ( ( $user->loggedin ) && ( $comment->email == $user->email ) ) {
			$comment->status = Comment::STATUS_APPROVED;
		}

		// Users need to have permission to add comments
		if(!$user->can('comment')) {
			Session::error( _t( 'You do not have permission to create comments.' ) );
			Utils::redirect( $post->permalink );
		}

		// Allow themes to work with comment hooks
		Themes::create();

		// Allow plugins to change comment data and add commentinfo based on plugin-added form fields
		Plugins::act( 'comment_accepted', $comment, $this->handler_vars, $extra );

		$spam_rating = 0;
		$spam_rating = Plugins::filter( 'spam_filter', $spam_rating, $comment, $this->handler_vars, $extra );

		$comment->insert();
		$anchor = '';

		// If the comment was saved
		if ( $comment->id && $comment->status != Comment::STATUS_SPAM ) {
			$anchor = '#comment-' . $comment->id;

			// store in the user's session that this comment is pending moderation
			if ( $comment->status == Comment::STATUS_UNAPPROVED ) {
				Session::notice( _t( 'Your comment is pending moderation.' ), 'comment_' . $comment->id );
			}

			// if no cookie exists, we should set one
			// but only if the user provided some details
			$cookie = 'comment_' . Options::get('GUID');
			if ( ( ! User::identify()->loggedin )
				&& ( ! isset( $_COOKIE[$cookie] ) )
				&& ( ! empty( $name )
					|| ! empty( $email )
					|| ! empty( $url )
				)
			)
			{
				$cookie_content = $comment->name . '#' . $comment->email . '#' . $comment->url;
				$site_url = Site::get_path( 'base',true );
				setcookie( $cookie, $cookie_content, time() + 31536000, $site_url );
			}
		}

		// Return the commenter to the original page.
		Utils::redirect( $post->permalink . $anchor );
	}

}
?>

repo_name: DaSourcerer/habari


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

/**
 * Contains the FileCache class
 *
 * Stores cache data in local files
 */
class FileCache extends Cache
{
	protected $cache_location;
	protected $enabled = false;
	protected $cache_files = array();
	protected $cache_data = array();
	protected $index_file;

	/**
	 * Constructor for FileCache
	 *
	 * Sets up paths etc. and reads cache index, if it exists.
	 */
	public function __construct()
	{
		if ( !defined( 'FILE_CACHE_LOCATION' ) ) {
			define( 'FILE_CACHE_LOCATION', HABARI_PATH . '/user/cache/' );
		}
		$this->cache_location = FILE_CACHE_LOCATION;
		$this->index_file = $this->cache_location . md5( 'index' . Options::get( 'GUID' ) ) . '.data';
		$this->enabled = is_writeable( $this->cache_location );
		if ( $this->enabled ) {
			if ( file_exists( $this->index_file ) ) {
				$this->cache_files = unserialize( file_get_contents( $this->index_file ) );
			}
		}
		else {
			Session::error( sprintf( _t("The cache directory '%s' is not writable - the cache is disabled. The user, or group, which your web server is running as, needs to have read, write, and execute permissions on this directory."), $this->cache_location ), 'filecache' );
			EventLog::log( sprintf( _t("The cache directory '%s' is not writable - the cache is disabled."), $this->cache_location ), 'notice', 'cache', 'habari' );
		}
	}

	/**
	 * Is record with $name in the cache?
	 *
	 * @param string $name name of the cached item
	 * @return boolean TRUE if item is cached, FALSE if not
	 */
	protected function _has( $name, $group )
	{
		if ( !$this->enabled ) {
			return false;
		}
		$hash = $this->get_name_hash( $name );
		$ghash = $this->get_group_hash( $group );

		return isset( $this->cache_files[$ghash][$hash] ) && ( $this->cache_files[$ghash][$hash]['keep'] || $this->cache_files[$ghash][$hash]['expires'] > time() ) && file_exists( $this->cache_files[$ghash][$hash]['file'] );
	}

	/**
	 * Is group in the cache?
	 *
	 * @param string $name name of the cached item
	 * @return boolean TRUE if item is cached, FALSE if not
	 */
	protected function _has_group( $group )
	{
		if ( !$this->enabled ) {
			return false;
		}
		$ghash = $this->get_group_hash( $group );

		$valid = true;
		$now = time();
		foreach ( $this->cache_files[$ghash] as $hash => $record ) {
			if ( ! file_exists( $record['file'] ) || $record['expires'] <= $now ) {
				$valid = false;
				break;
			}
		}

		return ( isset( $this->cache_files[$ghash] ) && count( $this->cache_files[$ghash] ) > 1 ) && $valid;
	}

	/**
	 * Returns the group from the cache.
	 *
	 * @param string $name The name of the cached item
	 * @return mixed The group or array() if it doesn't exist in cache
	 */
	protected function _get_group( $group )
	{
		if ( !$this->enabled ) {
			return null;
		}
		$ghash = $this->get_group_hash( $group );

		if ( !isset( $this->cache_data[$group] ) ) {
			$this->cache_data[$group] = array();
			if ( isset( $this->cache_files[$ghash] ) ) {
				foreach ( $this->cache_files[$ghash] as $hash => $record ) {
					$this->cache_data[$group][$record['name']] = unserialize(
						file_get_contents( $record['file'] )
					);
				}
			}
		}
		return $this->cache_data[$group];
	}

	/**
	 * Returns the named value from the cache.
	 *
	 * @param string $name The name of the cached item
	 * @return mixed The item value or NULL if it doesn't exist in cache
	 */
	protected function _get( $name, $group )
	{
		if ( !$this->enabled ) {
			return null;
		}
		$hash = $this->get_name_hash( $name );
		$ghash = $this->get_group_hash( $group );

		if ( !isset( $this->cache_data[$group][$name] ) ) {
			$this->cache_data[$group][$name] = null;
			if ( isset( $this->cache_files[$ghash][$hash] ) && ($this->cache_files[$ghash][$hash]['keep'] || $this->cache_files[$ghash][$hash]['expires'] > time()) && file_exists( $this->cache_files[$ghash][$hash]['file'] ) ) {
				$this->cache_data[$group][$name] = unserialize( file_get_contents( $this->cache_files[$ghash][$hash]['file'] ) );
			}
		}
		return $this->cache_data[$group][$name];
	}

	protected function _set( $name, $value, $expiry, $group, $keep )
	{
		if ( !$this->enabled ) {
			return null;
		}

		Plugins::act( 'cache_set_before', $name, $group, $value, $expiry );

		$hash = $this->get_name_hash( $name );
		$ghash = $this->get_group_hash( $group );

		if ( !isset( $this->cache_data[$group] ) ) {
			// prime our cache so the local version is up-to-date and complete
			$this->_get_group( $group );
		}
		$this->cache_data[$group][$name] = $value;

		file_put_contents( $this->cache_location . $ghash . $hash, serialize( $value ) );
		$this->cache_files[$ghash][$hash] = array( 'file' => $this->cache_location . $ghash . $hash, 'expires' => time() + $expiry, 'name' => $name, 'keep' => $keep );
		$this->clear_expired();
		file_put_contents( $this->index_file, serialize( $this->cache_files ) );

		Plugins::act( 'cache_set_after', $name, $group, $value, $expiry );
	}

	/**
	 * Expires the named value from the cache.
	 *
	 * @param string $name The name of the cached item
	 * @param string $group The name of the cache group
	 * @param string $match_mode (optional) how to match bucket names ('strict', 'regex', 'glob') (default 'strict')
	 */
	protected function _expire( $name, $group, $match_mode = 'strict' )
	{
		if ( !$this->enabled ) {
			return null;
		}
		$keys = array();
		switch ( strtolower($match_mode) ) {
			case 'glob':
				if ( array_key_exists( $group, $this->cache_data ) ) {
					$keys = preg_grep( Utils::glob_to_regex( $name ), array_keys( $this->cache_data[$group] ) );
				}
				break;
			case 'regex':
				if ( array_key_exists( $group, $this->cache_data ) ) {
					$keys = preg_grep( $name, array_keys( $this->cache_data[$group] ) );
				}
				break;
			case 'strict':
			default:
				$keys = array( $name );
				break;
		}

		$ghash = $this->get_group_hash( $group );
		foreach ( $keys as $key ) {
			Plugins::act( 'cache_expire_before', $name, $group );

			$hash = $this->get_name_hash( $key );

			if ( isset( $this->cache_files[$ghash][$hash] ) && file_exists( $this->cache_files[$ghash][$hash]['file'] ) ) {
				unlink( $this->cache_files[$ghash][$hash]['file'] );
				unset( $this->cache_files[$ghash][$hash] );
			}

			Plugins::act( 'cache_expire_after', $name, $group );
		}

		$this->clear_expired();
		file_put_contents( $this->index_file, serialize( $this->cache_files ) );
	}

	/**
	 * Return whether a named cache value has expired
	 *
	 * @param string $name The name of the cached item
	 * @param string $group The group of the cached item
	 * @return boolean true if the stored value has expired
	 */
	protected function _expired( $name, $group )
	{
		if ( !$this->enabled ) {
			return null;
		}
		$hash = $this->get_name_hash( $name );
		$ghash = $this->get_group_hash( $group );

		// Do not check cached data, since we can return (and cache in this object) data if the cache is set to 'keep'
		if ( isset( $this->cache_files[$ghash][$hash] ) && $this->cache_files[$ghash][$hash]['expires'] > time() && file_exists( $this->cache_files[$ghash][$hash]['file'] ) ) {
			return false;
		}
		else {
			return true;
		}
	}

	/**
	 * Extend the expiration of the named cached value.
	 *
	 * @param string $name The name of the cached item
	 * @param integer $expiry The duration in seconds to extend the cache expiration by
	 */
	protected function _extend( $name, $expiry, $group )
	{
		if ( !$this->enabled ) {
			return null;
		}

		Plugins::act( 'cache_extend_before', $name, $group, $expiry );

		$hash = $this->get_name_hash( $name );
		$ghash = $this->get_group_hash( $group );

		if ( isset( $this->cache_files[$ghash][$hash] ) ) {
			$this->cache_files[$ghash][$hash]['expires'] = time() + $expiry;
			$this->clear_expired();
			file_put_contents( $this->index_file, serialize( $this->cache_files ) );
		}

		Plugins::act( 'cache_extend_after', $name, $group, $expiry );
	}

	/**
	 * Remove all cache files
	 */
	protected function _purge()
	{
		Plugins::act( 'cache_purge_before' );

		$glob = Utils::glob( FILE_CACHE_LOCATION . '*.data' );
		foreach( $glob as $file ) {
			unlink( $file );
		}
		$glob = Utils::glob( FILE_CACHE_LOCATION . '*.cache' );
		foreach( $glob as $file ) {
			unlink( $file );
		}

		Plugins::act( 'cache_purge_after' );
	}

	/**
	 * Get the unique hash for a given key.
	 *
	 * @param string $name The name of the cached item.
	 */
	private function get_name_hash( $name )
	{
		return md5( $name . Options::get( 'GUID' ) ) . '.cache';
	}

	/**
	 * Get the unique hash for a given key.
	 *
	 * @param string $name The name of the cached group.
	 */
	private function get_group_hash( $group )
	{
		return md5( $group . Options::get( 'GUID' ) ) . '.';
	}

	/**
	 * Check whether a given record is still fresh (e.g. has not expired).
	 */
	private function record_fresh( $record )
	{
		if ( $record['expires'] > time() || $record['keep'] ) {
			return true;
		}
		elseif ( file_exists( $record['file'] ) ) {
			unlink( $record['file'] );
		}
		return false;
	}

	/**
	 * Purge expired items from the cache.
	 */
	private function clear_expired()
	{
		foreach ( $this->cache_files as $ghash => $records ) {
			$this->cache_files[$ghash] = array_filter( $records, array( $this, 'record_fresh' ) );
		}
	}

}

?>


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

/**
 * Habari Format Class
 *
 * Provides formatting functions for use in themes.  Extendable.
 *
 */
class Format
{
	private static $formatters = null;

	/**
	 * Called to register a format function to a plugin hook, only passing the hook's first parameter to the Format function.
	 * @param string $format A function name that exists in a Format class
	 * @param string $onwhat A plugin hook to apply that Format function to as a filter
	 **/
	public static function apply($format, $onwhat)
	{
		if ( self::$formatters == null ) {
			self::load_all();
		}

		foreach ( self::$formatters as $formatobj ) {
			if ( method_exists($formatobj, $format) ) {
				$index = array_search($formatobj, self::$formatters);
				$func = '$o = Format::by_index(' . $index . ');return $o->' . $format . '($a';
				$args = func_get_args();
				if ( count($args) > 2) {
					$func.= ', ';
					$args = array_map(create_function('$a', 'return "\'{$a}\'";'), array_slice($args, 2));
					$func .= implode(', ', $args);
				}
				$func .= ');';
				$lambda = create_function('$a', $func);
				Plugins::register( $lambda, 'filter', $onwhat);
				break;  // We only look for one matching format function to apply.
			}
		}
	}

	/**
	 *
	 *
	 **/
	public static function apply_with_hook_serialize( $arg )
	{
		$arg = serialize( $arg );
		return "'{$arg}'";
	}

	public static function apply_with_hook_unserialize( $arg )
	{
		$arg = unserialize( $arg );
		return $arg;
	}

	/**
	 * Called to register a format function to a plugin hook, and passes all of the hook's parameters to the Format function.
	 * @param string $format A function name that exists in a Format class
	 * @param string $onwhat A plugin hook to apply that Format function to as a filter
	 **/
	public static function apply_with_hook_params($format, $onwhat)
	{
		if ( self::$formatters == null ) {
			self::load_all();
		}

		foreach ( self::$formatters as $formatobj ) {
			if ( method_exists($formatobj, $format) ) {
				$index = array_search($formatobj, self::$formatters);
				$func = '$o = Format::by_index(' . $index . ');';
				$func .= '$args = func_get_args();';
				$func .= '$args = array_merge( $args';
				$args = func_get_args();
				if ( count($args) > 2 ) {

					$func .= ', array_map( array( "Format", "apply_with_hook_unserialize" ),';
					$args = array_map( array( "Format", "apply_with_hook_serialize" ), array_slice($args, 2));
					$func .= 'array( ' . implode(', ', $args) . ' ))';
				}
				$func .= ');';
				$func .= 'return call_user_func_array(array($o, "' . $format . '"), $args);';
				$lambda = create_function('$a', $func);
				Plugins::register( $lambda, 'filter', $onwhat);
				break;  // We only look for one matching format function to apply.
			}
		}
	}

	/**
	 * function by_index
	 * Returns an indexed formatter object, for use by lambda functions created
	 * to supply additional parameters to plugin filters.
	 * @param integer $index The index of the formatter object to return.
	 * @return Format The formatter object requested
	 **/
	public static function by_index($index)
	{
		return self::$formatters[$index];
	}

	/**
	 * function load_all
	 * Loads and stores an instance of all declared Format classes for future use
	 **/
	public static function load_all()
	{
		self::$formatters = array();
		$classes = get_declared_classes();
		foreach ( $classes as $class ) {
			if ( ( get_parent_class($class) == 'Format' ) || ( $class == 'Format' ) ) {
				self::$formatters[] = new $class();
			}
		}
		self::$formatters = array_merge( self::$formatters, Plugins::get_by_interface( 'FormatPlugin' ) );
		self::$formatters = array_reverse( self::$formatters, true );
	}

	/** DEFAULT FORMAT FUNCTIONS **/

	/**
	 * function autop
	 * Converts non-HTML paragraphs separated with 2 or more new lines into HTML paragraphs
	 * while preserving any internal HTML.
	 * New lines within the text of block elements are converted to linebreaks.
	 * New lines before and after tags are stripped.
	 *
	 * If you make changes to this, PLEASE add test cases here:
	 *   http://svn.habariproject.org/habari/trunk/tests/data/autop/
	 *
	 * @param string $value The string to apply the formatting
	 * @returns string The formatted string
	 **/
	public static function autop( $value )
	{
		$value = str_replace( "\r\n", "\n", $value );
		$value = trim( $value );
		$ht = new HtmlTokenizer( $value, false );
		$set = $ht->parse();
		$value = '';

		// should never autop ANY content in these items
		$noAutoP = array(
			'pre','code','ul','h1','h2','h3','h4','h5','h6',
			'table','ul','ol','li','i','b','em','strong'
		);

		$blockElements = array(
			'address','blockquote','center','dir','div','dl','fieldset','form',
			'h1','h2','h3','h4','h5','h6','hr','isindex','menu','noframes',
			'noscript','ol','p','pre','table','ul'
		);

		$token = $set->current();

		// There are no tokens in the text being formatted
		if ( $token === FALSE ) {
			return $value;
		}

		$openP = false;
		do {

			if ( $openP ) {
				if ( ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN || $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE ) && in_array( strtolower( $token['name'] ), $blockElements ) ) {
					if ( strtolower( $token['name'] ) != 'p' || $token['type'] != HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE ) {
						$value .= '
';
					}
					$openP = false;
				}
			}

			if ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN && !in_array( strtolower( $token['name'] ), $blockElements ) && $value == '' ) {
				// first element, is not a block element
				$value = '
';
				$openP = true;
			}

			// no-autop, pass them through verbatim
			if ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN && in_array( strtolower( $token['name'] ), $noAutoP ) ) {
				$nestedToken = $token;
				do {
					$value .= HtmlTokenSet::token_to_string( $nestedToken, false );
					if (
						( $nestedToken['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE
						 && strtolower( $nestedToken['name'] ) == strtolower( $token['name'] ) ) // found closing element
					) {
						break;
					}
				} while ( $nestedToken = $set->next() );
				continue;
			}

			// anything that's not a text node should get passed through
			if ( $token['type'] != HTMLTokenizer::NODE_TYPE_TEXT ) {
				$value .= HtmlTokenSet::token_to_string( $token, true );
				// If the token itself is p, we need to set $openP
				if ( strtolower( $token['name'] ) == 'p' && $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN ) {
					$openP = true;
				}
				continue;
			}

			// if we get this far, token type is text
			$localValue = $token['value'];
			if ( strlen( $localValue ) ) {
				if ( !$openP ) {
					$localValue = '
' . ltrim( $localValue );
					$openP = true;
				}

				$localValue = preg_replace( '/\s*(\n\s*){2,}/u', "

", $localValue ); // at least two \n in a row (allow whitespace in between)
				$localValue = str_replace( "\n", "
", $localValue ); // nl2br
			}
			$value .= $localValue;
		} while ( $token = $set->next() );

		$value = preg_replace( '#\s*

\s*#u', '', $value ); // replace

		if ( $openP ) {
			$value .= '
';
		}

		return $value;
	}

	/**
	 * function and_list
	 * Turns an array of strings into a friendly delimited string separated by commas and an "and"
	 * @param array $array An array of strings
	 * @param string $between Text to put between each element
	 * @param string $between_last Text to put between the next-to-last element and the last element
	 * @reutrn string The constructed string
	**/
	public static function and_list( $array, $between = ', ', $between_last = NULL )
	{
		if ( ! is_array( $array ) ) {
			$array = array( $array );
		}

		if ( $between_last === NULL ) {
			$between_last = _t( ' and ' );
		}

		$last = array_pop( $array );
		$out = implode(', ', $array );
		$out .= ($out == '') ? $last : $between_last . $last;
		return $out;
	}

	/**
	 * function tag_and_list
	 * Formatting function (should be in Format class?)
	 * Turns an array of tag names into an HTML-linked list with commas and an "and".
	 * @param array $array An array of tag names
	 * @param string $between Text to put between each element
	 * @param string $between_last Text to put between the next to last element and the last element
	 * @return string HTML links with specified separators.
	 **/
	public static function tag_and_list($array, $between = ', ', $between_last = NULL )
	{
		if ( ! is_array( $array ) ) {
			$array = array ( $array );
		}

		if ( $between_last === NULL ) {
			$between_last = _t(' and ');
		}

		$fn = create_function('$a,$b', 'return " $b) ) . "\\" rel=\\"tag\\">" . $a . "";');
		$array = array_map($fn, $array, array_keys($array));
		$last = array_pop($array);
		$out = implode($between, $array);
		$out .= ($out == '') ? $last : $between_last . $last;
		return $out;
	}

	/**
	 * Format a date using a specially formatted string
	 * Useful for using a single string to format multiple date components.
	 * Example:
	 *  If $dt is a HabariDateTime for December 10, 2008...
	 *  echo $dt->format_date('
{F} {j}, {Y}
');
	 *  // Output:
December 10, 2008

	 *
	 * @param HabariDateTime $date The date to format
	 * @param string $format A string with date()-like letters within braces to replace with date components
	 * @return string The formatted string
	 */
	public static function format_date($date, $format)
	{
		if ( !( $date instanceOf HabariDateTime ) ) {
			$date = HabariDateTime::date_create( $date );
		}
		preg_match_all('%\{(\w)\}%iu', $format, $matches);

		$components = array();
		foreach ( $matches[1] as $format_component ) {
			$components['{'.$format_component.'}'] = $date->format($format_component);
		}
		return strtr($format, $components);
	}

	/**
	 * function nice_date
	 * Formats a date using a date format string
	 * @param HabariDateTime A date as a HabariDateTime object
	 * @param string A date format string
	 * @returns string The date formatted as a string
	 **/
	public static function nice_date($date, $dateformat = 'F j, Y')
	{
		if ( !( $date instanceOf HabariDateTime ) ) {
			$date = HabariDateTime::date_create( $date );
		}
		return $date->format( $dateformat );
	}

	/**
	 * function nice_time
	 * Formats a time using a date format string
	 * @param HabariDateTime A date as a HabariDateTime object
	 * @param string A date format string
	 * @returns string The time formatted as a string
	 **/
	public static function nice_time($date, $dateformat = 'H:i:s')
	{
		if ( !( $date instanceOf HabariDateTime ) ) {
			$date = HabariDateTime::date_create( $date );
		}
		return $date->format( $dateformat );
	}

	/**
	 * Returns a shortened version of whatever is passed in.
	 * @param string $value A string to shorten
	 * @param integer $count Maximum words to display [100]
	 * @param integer $max_paragraphs Maximum paragraphs to display [1]
	 * @return string The string, shortened
	 **/
	public static function summarize( $text, $count = 100, $max_paragraphs = 1 )
	{
		$ellipsis = '...';

		$showmore = false;

		$ht = new HtmlTokenizer($text, false);
		$set = $ht->parse();

		$stack = array();
		$para = 0;
		$token = $set->current();
		$summary = new HTMLTokenSet();
		$set->rewind();
		$remaining_words = $count;
		// $bail lets the loop end naturally and close all open elements without adding new ones.
		$bail = false;
		for($token = $set->current(); $set->valid(); $token = $set->next() ) {
			if(!$bail && $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN) {
				$stack[] = $token;
			}
			if(!$bail) {
				switch($token['type']) {
					case HTMLTokenizer::NODE_TYPE_TEXT:
						$words = preg_split('/(\\s+)/u', $token['value'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
						// word count is doubled because spaces between words are captured as their own array elements via PREG_SPLIT_DELIM_CAPTURE
						$words = array_slice($words, 0, $remaining_words * 2);
						$remaining_words -= count($words) / 2;
						$token['value'] = implode('', $words);
						if($remaining_words <= 0) {
							$token['value'] .= $ellipsis;
							$summary[] = $token;
							$bail = true;
						}
						else {
							$summary[] = $token;
						}
						break;
					case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE;
						// don't handle this case here
						break;
					default:
						$summary[] = $token;
						break;
				}
			}
			if($token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE) {
				do{
					$end = array_pop($stack);
					$end['type'] = HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE;
					$end['attrs'] = null;
					$end['value'] = null;
					$summary[] = $end;
				} while( ($bail || $end['name'] != $token['name']) && count($stack) > 0 );
				if(count($stack) == 0) {
					$para++;
				}
				if($bail || $para >= $max_paragraphs) {
					break;
				}
			}
		}

		return (string) $summary;
	}

	/**
	 * Returns a truncated version of post content when the post isn't being displayed on its own.
	 * Posts are split either at the comment  or at the specified maximums.
	 * Use only after applying autop or other paragrpah styling methods.
	 * Apply to posts using:
	 * Format::apply_with_hook_params( 'more', 'post_content_out' );
	 * @param string $content The post content
	 * @param Post $post The Post object of the post
	 * @param string $more_text The text to use in the "read more" link.
	 * @param integer $max_words null or the maximum number of words to use before showing the more link
	 * @param integer $max_paragraphs null or the maximum number of paragraphs to use before showing the more link
	 * @return string The post content, suitable for display
	 **/
	public static function more($content, $post, $properties = array())
	{
		// If the post requested is the post under consideration, always return the full post
		if ( $post->slug == Controller::get_var('slug') ) {
			return $content;
		}
		else if( is_string( $properties ) ) {
			$args = func_get_args();
			$more_text = $properties;
			$max_words = ( isset( $args[3] ) ? $args[3] : NULL );
			$max_paragraphs = ( isset( $args[4] ) ? $args[4] : NULL );
			$paramstring = "";
		}
		else {
			$paramstring = "";
			$paramarray = Utils::get_params( $properties );

			$more_text = ( isset( $paramarray['more_text'] ) ? $paramarray['more_text'] : 'Read More' );
			$max_words = ( isset( $paramarray['max_words'] ) ? $paramarray['max_words'] : NULL );
			$max_paragraphs = ( isset( $paramarray['max_paragraphs'] ) ? $paramarray['max_paragraphs'] : NULL );

			if( isset( $paramarray['title:before'] ) ||
				isset( $paramarray['title'] ) ||
				isset( $paramarray['title:after'] ) ) {
				$paramstring .= 'title="';

				if( isset( $paramarray['title:before'] ) ) 	$paramstring .= $paramarray['title:before'];
				if( isset( $paramarray['title'] ) ) 		$paramstring .= $post->title;
				if( isset( $paramarray['title:after'] ) ) 	$paramstring .= $paramarray['title:after'];
				$paramstring .= '" ';
			}
			if( isset( $paramarray['class'] ) ) $paramstring .= 'class="' . $paramarray['class'] . '" ';

		}
		$matches = preg_split( '//isu', $content, 2, PREG_SPLIT_NO_EMPTY );
		if ( count($matches) > 1 ) {
			return ( $more_text != '' ) ? reset($matches) . ' permalink . '">' . $more_text . '' : reset($matches);
		}
		elseif ( isset($max_words) || isset($max_paragraphs) ) {
			$max_words = empty($max_words) ? 9999999 : intval($max_words);
			$max_paragraphs = empty($max_paragraphs) ? 9999999 : intval($max_paragraphs);
			$summary = Format::summarize($content, $max_words, $max_paragraphs);
			if ( strlen($summary) >= strlen($content) ) {
				return $content;
			}
			else {
				return ( $more_text != '' ) ? $summary . ' permalink . '">' . $more_text . '' : $summary;
			}
		}

    return $content;
	}

	/**
	 * html_messages
	 * Creates an HTML unordered list of an array of messages
	 * @param array $notices a list of success messages
	 * @param array $errors a list of error messages
	 * @return string HTML output
	 **/
	public static function html_messages( $notices, $errors )
	{
		$output = '';
		if ( count( $errors ) ) {
			$output.= '
';
			foreach ( $errors as $error ) {
				$output.= '
' . $error . '
';
			}
			$output.= '
';
		}
		if ( count( $notices ) ) {
			$output.= '
';
			foreach ( $notices as $notice ) {
				$output.= '
' . $notice . '
';
			}
			$output.= '
';
		}

		return $output;
	}

	/**
	 * humane_messages
	 * Creates JS calls to display session messages
	 * @param array $notices a list of success messages
	 * @param array $errors a list of error messages
	 * @return string JS output
	 **/
	public static function humane_messages( $notices, $errors )
	{
		$output = '';
		if ( count( $errors ) ) {
			foreach ( $errors as $error ) {
				$error = addslashes($error);
				$output .= "humanMsg.displayMsg(\"{$error}\");";
			}
		}
		if ( count( $notices ) ) {
			foreach ( $notices as $notice ) {
				$notice = addslashes($notice);
				$output .= "humanMsg.displayMsg(\"{$notice}\");";
			}
		}

		return $output;
	}

	/**
	 * json_messages
	 * Creates a JSON list of session messages
	 * @param array $notices a list of success messages
	 * @param array $errors a list of error messages
	 * @return string JS output
	 **/
	public static function json_messages( $notices, $errors )
	{
		$messages = array_merge( $errors, $notices );
		return json_encode( $messages );
	}
}
?>


code:
<?php

/**
 * interface FormStorage
 *
 * Describes functions that are required to store form data into an object
 */

interface FormStorage
{
	/**
	 * Stores a form value into the object
	 *
	 * @param string $key The name of a form component that will be stored
	 * @param mixed $value The value of the form component to store
	 */
	function field_save($key, $value);

	/**
	 * Loads form values from an object
	 *
	 * @param string $key The name of a form component that will be loaded
	 * @return mixed The stored value returned
	 */
	function field_load($key);
}

?>


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

/**
 * FormUI Library - Create interfaces for plugins
 *
 * FormUI			This is the main class, it generates the form itself;
 * FormContainer	A form-related class that can contain form elements, derived by FormUI and FormControlFieldset;
 * FormValidators	Catalog of validation functions, it can be extended if needed;
 * FormControl		Parent class to controls, it contains basic functionalities overrode in each control's class;
 * FormControl*		Every control needs a FormControl* class, FormUI literally looks for example, FormControlCheckbox.
 *
 */
class FormContainer
{
	public $name = '';
	public $class = '';
	public $caption = '';
	public $controls = array();
	protected $theme_obj = null;
	protected $checksum;
	public $template = 'formcontainer';
	public $properties = array();

	/**
	 * Constructor for FormContainer prevents construction of this class directly
	 */
	private function __construct() {}

	/**
	 * Append a control to the end of this container
	 *
	 * @param string $name The name of the control
	 * @param string $type A classname, or the postfix of a class starting 'FormControl' that will be used to create the control
	 * @return FormControl An instance of the named FormControl descendant.
	 */
	public function append()
	{
		$control = null;
		$args = func_get_args();
		$type = array_shift($args);

		if ( $type instanceof FormControl ) {
			$control = $type;
			$name = $control->name;
		}
		elseif ( is_string($type) && class_exists('FormControl' . ucwords($type)) ) {
			$name = reset($args);
			$type = 'FormControl' . ucwords($type);

			if ( class_exists($type) ) {
				// Instanciate a new object from $type
				$controlreflect = new ReflectionClass($type);
				$control = $controlreflect->newInstanceArgs($args);
			}
		}
		if ( $control ) {
			$control->container = $this;
			$this->controls[$name] = $control;
		}
		return $control;
	}

	/**
	 * Insert a control into the container
	 *
	 * @param string The name of the control to insert the new control in front of
	 * @param string The type of the new control
	 * @param string The name of the new control
	 * @return FormControl The new control instance
	 */
	public function insert()
	{
		$args = func_get_args();
		$before = array_shift($args);

		$control = call_user_func_array(array($this, 'append'), $args);
		if ( is_string($before) ) {
			$before = $this->$before;
		}
		$this->move_before($control, $before);
		return $control;
	}

	/**
	 * Generate a hash for this container
	 *
	 * @return string An md5 hash built using the controls contained within this container
	 */
	public function checksum()
	{
		if ( !isset($this->checksum) ) {
			$checksum = '';
			foreach($this->controls as $control) {
				if ( method_exists($control, 'checksum') ) {
					$checksum .= get_class($control) . ':' . $control->checksum();
				}
				else {
					$checksum .= get_class($control) . ':' . $control->name;
				}
				$checksum .= '::';
			}
			$this->checksum = md5($checksum .= $this->name);
		}
		return $this->checksum;
	}

	/**
	 * Returns an associative array of the controls' values
	 *
	 * @return array Associative array where key is control's name and value is the control's value
	 */
	public function get_values()
	{
		$values = array();
		foreach ($this->controls as $control) {
			if ( $control instanceOf FormContainer ) {
				$values = array_merge($values, $control->get_values());
			}
			else {
				$values[$control->name] = $control->value;
			}
		}
		return $values;
	}

	/**
	 * Returns an associative array of controls
	 *
	 * @return array An array of FormControls
	 */
	public function get_controls()
	{
		$controls = array();
		foreach ($this->controls as $control) {
			if ( $control instanceOf FormContainer ) {
				$controls = array_merge($controls, $control->get_controls());
			}
			else {
				$controls[$control->name] = $control;
			}
		}
		return $controls;
	}

	/**
	 * Produce HTML output for all this fieldset and all contained controls
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation, $this);
		$contents = '';
		foreach ( $this->controls as $control ) {
			$contents .= $control->get($forvalidation);
		}
		$theme->contents = $contents;
		// Do not move before $contents
		// Else, these variables will contain the last control's values
		$theme->class = $this->class;
		$theme->id = $this->name;
		$theme->caption = $this->caption;

		return $theme->fetch( $this->template, true );
	}

	/**
	 * Retreive the Theme used to display the form component
	 *
	 * @param boolean $forvalidation If true, perform validation on control and add error messages to output
	 * @param FormControl $control The control to output using a template
	 * @return Theme The theme object to display the template for the control
	 */
	function get_theme($forvalidation = false, $control = null)
	{
		if ( !isset($this->theme_obj) ) {
			$theme_dir = Plugins::filter( 'control_theme_dir', Plugins::filter( 'admin_theme_dir', Site::get_dir( 'admin_theme', TRUE ) ) . 'formcontrols/', $control );
			$this->theme_obj = Themes::create( 'admin', 'RawPHPEngine', $theme_dir );
		}
		$this->theme_obj->start_buffer();
		if ( $control instanceof FormControl ) {
			// PHP doesn't allow __get() to return pointers, and passing this array to foreach directly generates an error.
			$properties = $control->properties;
			foreach ( $properties as $name => $value ) {
				$this->theme_obj->$name = $value;
			}
			$this->theme_obj->field = $control->field;
			$this->theme_obj->value = $control->value;
			$this->theme_obj->caption = $control->caption;
			$this->theme_obj->id = (string) $control->id;
			$class = (array) $control->class;

			$message = '';
			if ( $forvalidation ) {
				$validate = $control->validate();
				if ( count($validate) != 0 ) {
					$class[] = 'invalid';
					$message = implode('<br>', (array) $validate);
				}
			}
			$this->theme_obj->class = implode( ' ', (array) $class );
			$this->theme_obj->message = $message;
		}
		return $this->theme_obj;
	}

	/**
	 * Moves a control to target's position to which we add $int if specified
	 * That integer is useful to move before or move after the target
	 *
	 * @param FormControl $control FormControl object to move
	 * @param FormControl $target FormControl object acting as destination
	 * @param int $int Integer added to $target's position (index)
	 */
	function move($source, $target, $offset = 0)
	{
		// Remove the source control from its container's list of controls
		$controls = array();
		foreach ( $source->container->controls as $name => $ctrl ) {
			if ( $ctrl === $source ) {
				$source_name = $name;
				continue;
			}
			$controls[$name] = $ctrl;
		}
		$source->container->controls = $controls;

		// Insert the source control into the destination control's container's list of controls in the correct location
		$target_index = array_search($target, array_values($target->container->controls), true);
		$left_slice = array_slice($target->container->controls, 0, ($target_index + $offset), true);
		$right_slice = array_slice($target->container->controls, ($target_index + $offset), count($target->container->controls), true);

		$target->container->controls = $left_slice + array($source_name => $source) + $right_slice;
	}

	/**
	 * Moves a control before the target control
	 *
	 * @param FormControl $control FormControl object to move
	 * @param FormControl $target FormControl object acting as destination
	 */
	function move_before($control, $target)
	{
		$this->move($control, $target);
	}

	/**
	 * Moves a control after the target control
	 *
	 * @param FormControl $control FormControl object to move
	 * @param FormControl $target FormControl object acting as destination
	 */
	function move_after($control, $target)
	{
		$this->move($control, $target, 1); // Increase left slice's size by one.
	}

	/**
	 * Move a control into the container
	 *
	 * @param FormControl $control FormControl object to move
	 * @param FormControl $target FormControl object acting as destination
	 */
	public function move_into( $control, $target )
	{
		// Remove the source control from its container's list of controls
		$controls = array();
		foreach ( $control->container->controls as $name => $ctrl ) {
			if ( $ctrl === $control ) {
				$source_name = $name;
				continue;
			}
			$controls[$name] = $ctrl;
		}
		$control->container->controls = $controls;

		$target->controls[$control->name] = $control;
	}

	/**
	 * Replaces a target control by the supplied control
	 *
	 * @param FormControl $target FormControl object to replace
	 * @param FormControl $control FormControl object to replace $target with
	 */
	function replace($target, $control)
	{
		$this->move_after($control, $target);
		$this->remove($target);
	}

	/**
	 * Removes a target control from this group (can be the form or a fieldset)
	 *
	 * @param FormControl $target FormControl to remove
	 */
	function remove( $target )
	{
		// Strictness will skip recursiveness, else you get an exception (recursive dependency)
		unset( $this->controls[array_search($target, $this->controls, TRUE)] );
	}

	/**
	 * Returns true if any of the controls this container contains should be stored in userinfo
	 *
	 * @return boolean True if control data should be sotred in userinfo
	 */
	function has_user_options()
	{
		$has_user_options = false;
		foreach ( $this->controls as $control ) {
			$has_user_options |= $control->has_user_options();
		}
		return $has_user_options;
	}

	/**
	 * Magic property getter, returns the specified control
	 *
	 * @param string $name The name of the control
	 * @return FormControl The control object requested
	 */
	function __get($name)
	{
		if ( isset($this->controls[$name]) ) {
			return $this->controls[$name];
		}
		foreach ( $this->controls as $control ) {
			if ( $control instanceof FormContainer ) {
				// Assignment is needed to avoid an indirect modification notice
				if ( $ctrl = $control->$name ) {
					return $ctrl;
				}
			}
		}
	}

	/**
	 * Magic property isset, returns if the specified control exists
	 *
	 * @param string $name The name of the control
	 * @return bool If the control object is set
	 */
	function __isset($name)
	{
		if ( isset($this->controls[$name]) ) {
			return true;
		}
		foreach ( $this->controls as $control ) {
			if ( $control instanceof FormContainer ) {
				// Assignment is needed to avoid an indirect modification notice
				if ( $ctrl = $control->$name ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Return the HTML/script required for all contained controls.  Do it only once.
	 *
	 * @return string The HTML/javascript required for all contained controls.
	 */
	function pre_out()
	{
		$preout = '';
		foreach ( $this->controls as $control ) {
			$preout .= $control->pre_out();
		}
		return $preout;
	}

	/**
	 * Runs any attached validation functions to check validation of each control contained in this fieldset.
	 *
	 * @return array An array of string validation error descriptions or an empty array if no errors were found.
	 */
	function validate()
	{
		$results = array();
		foreach ( $this->controls as $control ) {
			if ( $result = $control->validate() ) {
				$results[] = $result;
			}
		}
		return $results;
	}

	/**
	 * Store each contained control's value under the control's specified key.
	 *
	 * @param string $key (optional) The Options table key to store this option in
	 */
	function save()
	{
		foreach ( $this->controls as $control ) {
			$control->save();
		}
	}

	/**
	 * Explicitly assign the theme object to be used with this container
	 *
	 * @param Theme $theme The theme object to use to output this container
	 */
	function set_theme($theme)
	{
		$this->theme_obj = $theme;
	}

	/**
	 * Output any validation errors on any controls in this container using the supplied format
	 * $this->validate must be called first!
	 *
	 * @params string $format A sprintf()-style format string to format the validation error
	 * @params string $format A sprintf()-style format string to wrap the returned error, only if at least one error exists
	 */
	public function errors_out($format, $wrap = '%s')
	{
		echo $this->errors_get($format, $wrap);
	}

	/**
	 * Return any validation errors on any controls in this container using the supplied format
	 * $this->validate must be called first!
	 *
	 * @params string $format A sprintf()-style format string to format the validation error
	 * @params string $format A sprintf()-style format string to wrap the returned error, only if at least one error exists
	 * @return string The errors in the supplied format
	 */
	public function errors_get($format, $wrap = '%s')
	{
		$out = '';
		foreach ( $this->get_controls() as $control ) {
			foreach ( $control->errors as $error ) {
				$out .= sprintf($format, $error);
			}
		}
		if ( $out != '' ) {
			$out = sprintf($wrap, $out);
		}
		return $out;
	}

}

/**
 * FormUI Class
 * This will generate the <form> structure and call subsequent controls
 *
 * For a list of options to customize its output or behavior see FormUI::set_option()
 */
class FormUI extends FormContainer
{
	private $success_callback;
	private $success_callback_params = array();
	public $success = false;
	public $submitted = false;
	private static $outpre = false;
	private $options = array(
		'save_button' => true,
		'ajax' => false,
		'form_action' => '',
		'template' => 'formcontrol_form',
		'theme' => '',
		'success_message' => '',
	);
	public $class = array( 'formui' );
	public $id = null;
	public $formtype = '';

	public $properties = array(
		'action' => '',
		'onsubmit' => '',
		'enctype' => 'application/x-www-form-urlencoded',
		'accept_charset' => 'UTF-8',
	);

	/**
	 * FormUI's constructor, called on instantiation.
	 *
	 * @param string $name The name of the form, used to differentiate multiple forms.
	 * @param string $formtype The type of the form, used to classify form types for plugin modification
	 */
	public function __construct( $name, $formtype = null )
	{
		$this->name = $name;
		if ( isset($formtype) ) {
			$this->formtype = $formtype;
		}
		else {
			$this->formtype = $name;
		}
	}

	/**
	 * Generate a unique MD5 hash based on the form's name or the control's name.
	 *
	 * @return string Unique string composed of 35 hexadecimal digits representing the victim.
	 */
	public function salted_name()
	{
		return md5(Options::get('secret') . 'added salt, for taste' . $this->checksum());
	}

	/**
	 * Produce a form with the contained fields.
	 *
	 * @param boolean $process_for_success Set to true to display the form as it would look if the submission succeeded, but do not execute success methods.
	 * @return string HTML form generated from all controls assigned to this form
	 */
	public function get($use_theme = null, $process_for_success = true)
	{
		$forvalidation = false;

		Plugins::act('modify_form_' . Utils::slugify($this->formtype, '_'), $this);
		Plugins::act('modify_form', $this);

		if ( isset($use_theme) ) {
			$theme = $use_theme;
		}
		else {
			$theme = $this->get_theme($forvalidation, $this);
		}
		$theme->start_buffer();
		$theme->success = false;
		$this->success = false;
		$this->submitted = false;

		// Should we be validating?
		if ( isset($_POST['FormUI']) && $_POST['FormUI'] == $this->salted_name() ) {
			$this->submitted = true;
			$validate = $this->validate();
			if ( count($validate) == 0 ) {
				if ( $process_for_success ) {
					$result = $this->success();
					if ( $result ) {
						return $result;
					}
				}
				$theme->success = true;
				$this->success = true;
				$theme->message = $this->options['success_message'];
			}
			else {
				$forvalidation = true;
				if ( !isset( $_SESSION['forms'][$this->salted_name()]['url'] ) ) {
					$_SESSION['forms'][$this->salted_name()]['url'] = Site::get_url( 'habari', true ) . Controller::get_stub() . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
				}
			}
		}
		else {
			$_SESSION['forms'][$this->salted_name()]['url'] = Site::get_url( 'habari', true ) . Controller::get_stub() . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
		}
		if ( isset($_SESSION['forms'][$this->salted_name()]['error_data']) ) {
			foreach ( $_SESSION['forms'][$this->salted_name()]['error_data'] as $key => $value ) {
				$_POST[$key] = $value;
			}
			unset($_SESSION['forms'][$this->salted_name()]['error_data']);
			$forvalidation = true;
		}

		$out = '';

		$theme->controls = $this->output_controls($forvalidation);
		$theme->form = $this;

		foreach ( $this->properties as $prop => $value ) {
			$theme->$prop = $value;
		}

		$theme->id = Utils::slugify($this->name);
		$theme->class = implode( " ", (array) $this->class );
		$theme->action = $this->options['form_action'];
		$theme->onsubmit = ($this->properties['onsubmit'] == '') ? '' : "onsubmit=\"{$this->properties['onsubmit']}\"";
		$theme->salted_name = $this->salted_name();
		$theme->pre_out = $this->pre_out_controls();

		$out = $theme->display_fallback($this->options['template'], 'fetch');
		$theme->end_buffer();

		return $out;
	}

	/**
	 * Output a form with the contained fields.
	 * Calls $this->get() and echoes.
	 */
	public function out()
	{
		$args = func_get_args();
		echo call_user_func_array(array($this, 'get'), $args);
	}

	/**
	 * Return the form control HTML.
	 *
	 * @param boolean $forvalidation True if the controls should output additional information based on validation.
	 * @return string The output of controls' HTML.
	 */
	public function output_controls( $forvalidation = false )
	{
		$out = '';
		$theme = $this->get_theme( $forvalidation );
		foreach ( $this->controls as $control ) {
			$out .= $control->get( $forvalidation );
		}
		$theme->end_buffer();
		return $out;
	}

	/**
	 * Return pre-output control configuration scripts for any controls that require them.
	 *
	 * @return string The output of controls' pre-output HTML.
	 */
	public function pre_out_controls( )
	{
		$out = '';
		if ( !FormUI::$outpre ) {
			FormUI::$outpre = true;
			$out .= '<script type="text/javascript">var controls = Object();</script>';
		}
		foreach ( $this->controls as $control ) {
			$out .= $control->pre_out( );
		}
		return $out;
	}

	/**
	 * Process validation on all controls of this form.
	 *
	 * @return array An array of strings describing validation issues, or an empty array if no issues.
	 */
	public function validate()
	{
		$validate = array();
		foreach ( $this->controls as $control ) {
			$validate = array_merge($validate, $control->validate());
		}
		return $validate;
	}

	/**
	 * Set a function to call on form submission success
	 *
	 * @param mixed $callback A callback function or a plugin filter name.
	 */
	public function on_success( $callback )
	{
		$params = func_get_args();
		$callback = array_shift($params);
		$this->success_callback = $callback;
		$this->success_callback_params = $params;
	}

	/**
	 * Calls the success callback for the form, and optionally saves the form values
	 * to the options table.
	 */
	public function success()
	{
		$result = true;
		if ( isset($this->success_callback) ) {
			$params = $this->success_callback_params;
			array_unshift($params, $this);
			if ( is_callable($this->success_callback) ) {
				$result = call_user_func_array($this->success_callback, $params);
			}
			else {
				array_unshift($params, $this->success_callback, false);
				$result = call_user_func_array(array('Plugins', 'filter'), $params);
			}
			if ( $result ) {
				return $result;
			}
		}
		else {
			$this->save();
			return false;
		}
	}

	/**
	 * Save all controls to their storage locations
	 */
	public function save()
	{
		foreach ( $this->controls as $control ) {
			$control->save();
		}
		if ( $this->has_user_options() ) {
			User::identify()->info->commit();
		}
	}

	/**
	 * Set a form option
	 * Defaults for options are stored in the $this->options array
	 *
	 * @param string $option The name of the option to set
	 * @param mixed $value The value of the option
	 */
	public function set_option( $option, $value )
	{
		$this->options[$option] = $value;
	}

	/**
	 * Get a form option
	 *
	 * @param string $option The name of the option to get
	 * @return mixed The value of the named option if set, null if not set
	 */
	public function get_option( $option )
	{
		return isset($this->options[$option]) ? $this->options[$option] : null;
	}

	/**
	 * Configure all the options necessary to make this form work inside a media bar panel
	 * @param string $path Identifies the silo
	 * @param string $panel The panel in the silo to submit to
	 * @param string $callback Javascript function to call on form submission
	 */
	public function media_panel($path, $panel, $callback)
	{
		$this->options['ajax'] = true;
		$this->options['form_action'] = URL::get('admin_ajax', array('context' => 'media_panel'));
		$this->properties['onsubmit'] = "habari.media.submitPanel('$path', '$panel', this, '{$callback}');return false;";
	}

	public function bounce()
	{
		$_SESSION['forms'][$this->salted_name()]['error_data'] = $_POST;
		Utils::redirect($_SESSION['forms'][$this->salted_name()]['url']);
	}

}

/**
 * FormValidators Class
 *
 * Extend this class to supply your own validators, by default we supply most common
 */
class FormValidators
{

	/**
	 * A validation function that returns an error if the value passed in is not a valid URL.
	 *
	 * @param string $text A string to test if it is a valid URL
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $form The container that holds the control
	 * @param string $warning An optional error message
	 * @return array An empty array if the string is a valid URL, or an array with strings describing the errors
	 */
	public static function validate_url( $text, $control, $form, $warning = null )
	{
		if ( !empty( $text ) ) {
			if ( !preg_match('/^(?P<protocol>https?):\/\/(?P<domain>[-A-Z0-9.]+)(?P<file>\/[-A-Z0-9+&@#\/%=~_|!:,.;]*)?(?P<parameters>\\?[-A-Z0-9+&@#\/%=~_|!:,.;]*)?/i', $text) ) {
				$warning = empty($warning) ? _t('Value must be a valid URL.') : $warning;
				return array($warning);
			}
		}
		return array();
	}

	/**
	 * A validation function that returns an error if the value passed in is not a valid Email Address,
	 * as per RFC2822 and RFC2821.
	 *
	 * @param string $text A string to test if it is a valid Email Address
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $form The container that holds the control
	 * @param string $warning An optional error message
	 * @return array An empty array if the string is a valid Email Address, or an array with strings describing the errors
	 */
	public static function validate_email( $text, $control, $form, $warning = null )
	{
		if ( !preg_match("@^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*\@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$@i", $text ) ) {
			$warning = empty($warning) ? _t('Value must be a valid Email Address.') : $warning;
			return array($warning);
		}
		return array();
	}

	/**
	 * A validation function that returns an error if the value passed in is not set.
	 *
	 * @param string $text A value to test if it is empty
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $form The container that holds the control
	 * @param string $warning An optional error message
	 * @return array An empty array if the value exists, or an array with strings describing the errors
	 */
	public static function validate_required( $value, $control, $form, $warning = null )
	{
		if ( empty($value) || $value == '' ) {
			$warning = empty($warning) ? _t('A value for this field is required.') : $warning;
			return array($warning);
		}
		return array();
	}

	/**
	 * A validation function that returns an error if the the passed username is unavailable
	 *
	 * @param string $text A value to test as username
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $form The container that holds the control
	 * @param string $allowed_name An optional name which overrides the check and is always allowed
	 * @param string $warning An optional error message
	 * @return array An empty array if the value exists, or an array with strings describing the errors
	 */
	public static function validate_username( $value, $control, $form, $allowed_name = null, $warning = null )
	{
		if ( isset($allowed_name) && ($value == $allowed_name) ) {
			return array();
		}
		if ( User::get_by_name( $value ) ) {
			$warning = empty($warning) ? _t('That username is already in use!') : $warning;
			return array($warning);
		}
		return array();
	}

	/**
	 * A validation function that returns an error if the passed control values do not match
	 *
	 * @param string $text A value to test for similarity
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $form The container that holds the control
	 * @param FormControl $matcher The control which should have a matching value
	 * @param string $warning An optional error message
	 * @return array An empty array if the value exists, or an array with strings describing the errors
	 */
	public static function validate_same( $value, $control, $form, $matcher, $warning = null )
	{
		if ( $value != $matcher->value ) {
			$warning = empty($warning) ? _t('The value of this field must match the value of %s.', array($matcher->caption)) : $warning;
			return array($warning);
		}
		return array();
	}

	/**
	 * A validation function that returns an error if the value passed does not match the regex specified.
	 *
	 * @param string $value A value to test if it is empty
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $container The container that holds the control
	 * @param string $regex The regular expression to test against
	 * @param string $warning An optional error message
	 * @return array An empty array if the value exists, or an array with strings describing the errors
	 */
	public static function validate_regex( $value, $control, $container, $regex, $warning = NULL )
	{
		if ( preg_match($regex, $value) ) {
			return array();
		}
		else {
			if ( $warning == NULL ) {
				$warning = _t('The value does not meet submission requirements');
			}
			return array($warning);
		}
	}

	/**
	 * A validation function that returns an error if the value passed is not within a specified range
	 *
	 * @param string $value A value to test if it is empty
	 * @param FormControl $control The control that defines the value
	 * @param FormContainer $container The container that holds the control
	 * @param float $min The minimum value, inclusive
	 * @param float $max The maximum value, inclusive
	 * @param string $warning An optional error message
	 * @return array An empty array if the value is value, or an array with strings describing the errors
	 */
	public static function validate_range( $value, $control, $container, $min, $max, $warning = NULL )
	{
		if ( $value < $min ) {
			if ( $warning == NULL ) {
				$warning = _t('The value entered is lesser than the minimum of %d.', array($min));
			}
			return array($warning);
		}
		elseif ( $value > $max ) {
			if ( $warning == NULL ) {
				$warning = _t('The value entered is greater than the maximum of %d.', array($max));
			}
			return array($warning);
		}
		else {
			return array();
		}
	}
}

/**
 * A base class from which form controls to be used with FormUI can descend
 */
class FormControl
{
	protected $caption;
	protected $default = null;
	protected $validators = array();
	protected $storage;
	protected $store_user = false;
	protected $theme_obj;
	protected $container = null;
	public $id = null;
	public $class = array( 'formcontrol' );
	public $name;
	protected $properties = array();
	protected $template = null;
	protected $raw = false;
	public $errors = array();

	/**
	 * FormControl constructor - set initial settings of the control
	 *
	 * @param string $storage The storage location for this control
	 * @param string $default The default value of the control
	 * @param string $caption The caption used as the label when displaying a control
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $storage, $caption, $template) = array_merge($args, array_fill(0, 4, null));

		$this->name = $name;
		$this->storage = $storage;
		$this->caption = $caption;
		$this->template = $template;

		$this->default = null;
	}

	/**
	 * Retrieve the FormUI object that contains this control
	 *
	 * @return FormUI The containing form
	 */
	public function get_form()
	{
		$container = $this->container;
		while ( !$container instanceof FormUI ) {
			$container = $container->container;
		}
		return $container;
	}

	/**
	 * Return a checksum representing this control
	 *
	 * @return string A checksum
	 */
	public function checksum()
	{
		$storage = is_object($this->storage) ? gettype($this->storage) : $this->storage;
		return md5($this->name . $storage . $this->caption );
	}

	/**
	 * Set the default value of this control from options or userinfo if the default value isn't explicitly set on creation
	 */
	protected function get_default()
	{
		// Get the default value from Options/UserInfo if it's not set explicitly
		if ( empty($this->default) ) {
			if ( $this->storage instanceof FormStorage ) {
				$type = 'formstorage';
			}
			else {
				$storage = explode(':', $this->storage, 2);
				switch ( count($storage) ) {
					case 2:
						list($type, $location) = $storage;
						break;
					case 1:
						list($location) = $storage;
						$type = 'option';
						break;
					default:
						return $this->default;
				}
			}

			switch ( $type ) {
				case 'user':
					$this->default = User::identify()->info->{$location};
					break;
				case 'option':
					$this->default = Options::get( $location );
					break;
				case 'action':
					$this->default = Plugins::filter($location, '', $this->name, false);
					break;
				case 'session';
					$session_set = Session::get_set($location , false);
					if ( isset( $session_set[$this->name] ) ) {
						$this->default = $session_set[$this->name];
					}
					break;
				case 'formstorage':
					$this->default = $this->storage->field_load($this->name);
					break;
				case 'null':
					break;
			}

		}
		return $this->default;
	}

	/**
	 * Store this control's value under the control's specified key.
	 *
	 * @param string $storage (optional) A storage location to store the control data
	 */
	public function save($storage = null)
	{
		if ( $storage == null ) {
			$storage = $this->storage;
		}

		if ( is_string($storage) ) {
			$storage = explode(':', $storage, 2);
			switch ( count($storage) ) {
				case 2:
					list($type, $location) = $storage;
					break;
				case 1:
					list($location) = $storage;
					$type = 'option';
					break;
				default:
					return;
			}
		}
		elseif ( $storage instanceof FormStorage ) {
			$type = 'formstorage';
		}

		switch ( $type ) {
			case 'user':
				User::identify()->info->{$location} = $this->value;
				break;
			case 'option':
				Options::set( $location, $this->value );
				break;
			case 'action':
				Plugins::filter($location, $this->value, $this->name, true);
				break;
			case 'session';
				Session::add_to_set( $location, $this->value, $this->name );
				break;
			case 'formstorage':
				$storage->field_save( $this->name, $this->value );
				break;
			case 'null':
				break;
		}
	}

	/**
	 * Return the HTML construction of the control.
	 * Abstract function.
	 *
	 * @param boolean $forvalidation True if the control should output validation information with the control.
	 */
	public function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation);

		foreach ( $this->properties as $prop => $value ) {
			$theme->$prop = $value;
		}

		$theme->caption = $this->caption;
		$theme->id = $this->name;
		$theme->value = $this->value;
		$theme->control = $this;

		return $theme->fetch( $this->get_template(), true );
	}

	/**
	 * Return the template name associated to this control, whether set explicitly or by class
	 *
	 * @return string The template used to display this control.
	 */
	public function get_template()
	{
		if ( isset( $this->template ) ) {
			$template = $this->template;
		}
		else {
			$classname = get_class( $this );
			$type = '';
			if ( preg_match('%FormControl(.+)%i', $classname, $controltype) ) {
				$type = strtolower($controltype[1]);
			}
			else {
				$type = strtolower($classname);
			}
			$template = 'formcontrol_' . $type;
		}

		return $template;
	}

	/**
	 * Return the HTML/script required for this type of control.
	 * Abstract function.
	 *
	 */
	public function pre_out()
	{
		return '';
	}

	/**
	 * Runs any attached validation functions to check validation of this control.
	 *
	 * @return array An array of string validation error descriptions or an empty array if no errors were found.
	 */
	public function validate()
	{
		$valid = array();
		foreach ( $this->validators as $validator ) {
			$validator_fn = array_shift($validator);
			if ( is_callable($validator_fn) ) {
				$params = array_merge(array($this->value, $this, $this->container), $validator);
				$valid = array_merge($valid, call_user_func_array( $validator_fn, $params ) );
			}
			elseif ( method_exists('FormValidators', $validator_fn) ) {
				$validator_fn = array('FormValidators', $validator_fn);
				$params = array_merge(array($this->value, $this, $this->container), $validator);
				$valid = array_merge($valid, call_user_func_array( $validator_fn, $params ) );
			}
			else {
				$params = array_merge(array($validator_fn, $valid, $this->value, $this, $this->container), $validator);
				$valid = array_merge($valid, call_user_func_array( array('Plugins', 'filter'), $params ) );
			}
		}
		$this->errors = $valid;
		return $valid;
	}

	/**
	 * Output any validation errors on this control using the supplied format
	 * $this->validate must be called first!
	 *
	 * @params string $format A sprintf()-style format string to format the validation error
	 * @params string $format A sprintf()-style format string to wrap the returned error, only if at least one error exists
	 * @return boolean true if the control has errors
	 */
	public function errors_out($format, $wrap = '%s')
	{
		echo $this->errors_get($format, $wrap);
	}

	/**
	* Return any validation errors on this control using the supplied format
	 * $this->validate must be called first!
	 *
	 * @params string $format A sprintf()-style format string to format the validation error
	 * @params string $format A sprintf()-style format string to wrap the returned error, only if at least one error exists
	 * @return boolean true if the control has errors
	 */
	public function errors_get($format, $wrap = '%s')
	{
		$out = '';
		foreach ( $this->errors as $error ) {
			$out .= sprintf($format, $error);
		}
		if ( $out != '' ) {
			$out = sprintf($wrap, $out);
		}
		return $out;
	}

	/**
	 * Magic function __get returns properties for this object.
	 * Potential valid properties:
	 * field: A valid unique name for this control in HTML.
	 * value: The value of the control, whether the default or submitted in the form
	 *
	 * @param string $name The parameter to retrieve
	 * @return mixed The value of the parameter
	 */
	public function __get($name)
	{
		switch ( $name ) {
			case 'field':
				// must be same every time, no spaces
				return isset($this->id) ? $this->id : sprintf('%x', crc32($this->name));
			case 'value':
				if ( isset($_POST[$this->field]) ) {
					return $this->raw ? $_POST->raw($this->field) : $_POST[$this->field];
				}
				else {
					return $this->get_default();
				}
		}
		if ( isset($this->$name) ) {
			return $this->$name;
		}
		if ( isset($this->properties[$name]) ) {
			return $this->properties[$name];
		}
		return null;
	}

	public function __toString()
	{
		return $this->value;
	}

	/**
	 * Returns true if this control should be stored as userinfo
	 *
	 * @return boolean True if this control should be stored as userinfo
	 */
	public function has_user_options()
	{
		if ( is_string($this->storage) ) {
			$storage = explode(':', $this->storage, 2);
			switch ( count($storage) ) {
				case 2:
					list($type, $location) = $storage;
					break;
				default:
					return false;
			}

			if ( $type == 'user' ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Magic property setter for FormControl and its descendants
	 *
	 * @param string $name The name of the property
	 * @param mixed $value The value to set the property to
	 */
	public function __set($name, $value)
	{
		switch ( $name ) {
			case 'value':
				$this->default = $value;
				break;
			case 'container':
				if ( $this->container != $value && isset($this->container) ) {
					$this->container->remove($this);
				}
				$this->container = $value;
				break;
			case 'name':
				$this->name = (string) $value;
				break;
			case 'caption':
				$this->caption = $value;
				break;
			case 'storage':
				$this->storage = $value;
				$this->default = null;
				break;
			case 'template':
				$this->template = $value;
				break;
			case 'raw':
				$this->raw = $value;
				break;
			default:
				$this->properties[$name] = $value;
				break;
		}
	}

	/**
	 * Return the theme used to output this control and perform validation if required.
	 *
	 * @param boolean $forvalidation If true, process this control for validation (adds validation failure messages to the theme)
	 * @return Theme The theme that will display this control
	 */
	protected function get_theme($forvalidation)
	{
		$theme = $this->container->get_theme($forvalidation, $this);
		foreach ( $this->properties as $name => $value ) {
			$theme->name = $value;
		}
		return $theme;
	}

	/**
	 * Add a validation function to this control
	 * Multiple parameters are passed as parameters to the validation function
	 * @param mixed $validator A callback function
	 * @param mixed $option... Multiple parameters added to those used to call the validator callback
	 * @return FormControl Returns the control for chained execution
	 */
	public function add_validator()
	{
		$args = func_get_args();
		$validator = reset($args);
		if ( is_array($validator) ) {
			$index = (is_object($validator[0]) ? get_class($validator[0]) : $validator[0]) . ':' . $validator[1];
		}
		else {
			$index = $validator;
		}
		$this->validators[$index] = $args;
		return $this;
	}

	/**
	 * Removes a validation function from this control
	 *
	 * @param string $name The name of the validator to remove
	 */
	public function remove_validator($name)
	{
		if ( is_array($name) ) {
			$index = (is_object($name[0]) ? get_class($name[0]) : $name[0]) . ':' . $name[1];
		}
		else {
			$index = $name;
		}
		unset($this->validators[$index]);
	}

	/**
	 * Move this control inside of the target
	 * In the end, this will use FormUI::move()
	 *
	 * @param object $target The target control to move this control before
	 */
	function move_into( $target )
	{
		$this->container->move_into( $this, $target );
	}

	/**
	 * Move this control before the target
	 * In the end, this will use FormUI::move()
	 *
	 * @param object $target The target control to move this control before
	 */
	function move_before( $target )
	{
		$this->container->move_before( $this, $target );
	}

	/**
	 * Move this control after the target
	 * In the end, this will use FormUI::move()
	 *
	 * @param object $target The target control to move this control after
	 */
	function move_after( $target )
	{
		$this->container->move_after( $this, $target );
	}

	/**
	 * Remove this controls from the form
	 */
	function remove()
	{
		$this->container->remove($this);
	}

}

/**
* A control prototype that does not save its data
*/
class FormControlNoSave extends FormControl
{

	/**
	 * The FormControlNoSave constructor initializes the control without a save location
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $caption, $template) = array_merge($args, array_fill(0, 3, null));

		$this->name = $name;
		$this->caption = $caption;
		$this->template = $template;
	}

	/**
	 * Do not store this static control anywhere
	 *
	 * @param mixed $key Unused
	 * @param mixed $store_user Unused
	 */
	public function save($key = null, $store_user = null)
	{
		// This function should do nothing.
	}
}

/**
 * A text control based on FormControl for output via a FormUI.
 */
class FormControlText extends FormControl
{
// Placeholder class
}

/**
 * A submit control based on FormControl for output via FormUI
 */
class FormControlSubmit extends FormControlNoSave
{
// Placeholder class
}

/**
 * A button control based on FormControl for output via FormUI
 */
class FormControlButton extends FormControlNoSave
{
	// Placeholder class
}

/**
 * A text control based on FormControl for output via a FormUI.
 */
class FormControlStatic extends FormControlNoSave
{
	/**
	 * Produce HTML output for this static text control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	public function get($forvalidation = true)
	{
		return $this->caption;
	}
}

/**
 * A control to display a single tag for output via FormUI
 */
class FormControlTag extends FormContainer
{
	/**
	 * Override the FormControl constructor to support more parameters
	 *
	 * @param string $name Name of this control
	 * @param string $tag A tag object
	 * @param string $template A template to use for display
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $tag, $template) = array_merge($args, array_fill(0, 3, null));

		$this->name = $name;
		$this->tag = $tag;
		$this->template = isset($template) ? $template : 'tabcontrol_tag';
	}

	/**
	 * Produce HTML output for all this fieldset and all contained controls
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation, $this);
		$max = Tags::max_count();

		$tag = $this->tag;

		$theme->class = 'tag_'.$tag->slug;
		$theme->id = $tag->id;
		$theme->weight = $max > 0 ? round(($tag->count * 10)/$max) : 0;
		$theme->caption = $tag->tag;
		$theme->count = $tag->count;

		return $theme->fetch( 'tabcontrol_tag', true );
	}

}

/**
 * A password control based on FormControlText for output via a FormUI.
 */
class FormControlPassword extends FormControlText
{

	/**
	 * Produce HTML output for this password control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	public function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation);
		$theme->outvalue = $this->value == '' ? '' : substr(md5($this->value), 0, 8);

		return $theme->fetch( $this->get_template(), true );
	}

	/**
	 * Magic function __get returns properties for this object, or passes it on to the parent class
	 * Potential valid properties:
	 * value: The value of the control, whether the default or submitted in the form
	 *
	 * @param string $name The paramter to retrieve
	 * @return mixed The value of the parameter
	 */
	public function __get($name)
	{
		$default = $this->get_default();
		switch ( $name ) {
			case 'value':
				if ( isset($_POST[$this->field]) ) {
					if ( $_POST[$this->field] == substr(md5($default), 0, 8) ) {
						return $default;
					}
					else {
						return $_POST[$this->field];
					}
				}
				else {
					return $default;
				}
			default:
				return parent::__get($name);
		}
	}
}

/**
 * A multiple-slot text control based on FormControl for output via a FormUI.
 * @todo Make DHTML fallback for non-js browsers
 */
class FormControlTextMulti extends FormControl
{
	public static $outpre = false;

	/**
	 * Return the HTML/script required for this control.  Do it only once.
	 * @return string The HTML/javascript required for this control.
	 */
	public function pre_out()
	{
		$out = '';
		if ( !FormControlTextMulti::$outpre ) {
			FormControlTextMulti::$outpre = true;
			$out .= '
				<script type="text/javascript">
				controls.textmulti = {
					add: function(e, field){
						$(e).before("<label><input type=\"text\" name=\"" + field + "[]\"> <a href=\"#\" onclick=\"return controls.textmulti.remove(this);\">[' . _t('remove') . ']</a></label>");
						return false;
					},
					remove: function(e){
						if(confirm("' . _t('Remove this item?') . '")) {
							$(e).parent().prev().remove();
							$(e).parent().remove();
						}
						return false;
					}
				}
				</script>
			';
		}
		return $out;
	}

}

/**
 * A select control based on FormControl for output via a FormUI.
 */
class FormControlSelect extends FormControl
{
	public $options = array();
	public $multiple = false;
	public $size = 5;

	/**
	 * Override the FormControl constructor to support more parameters
	 *
	 * @param string $name
	 * @param string $caption
	 * @param array $options
	 * @param string $selected
	 */
	public function __construct( )
	{
		$args = func_get_args();
		list($name, $storage, $caption, $options, $template) = array_merge($args, array_fill(0, 5, null));

		$this->name = $name;
		$this->storage = $storage;
		$this->caption = $caption;
		$this->options = $options;
		$this->template = $template;

		$this->default = null;
	}

	/**
	 * Produce HTML output for this text control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	public function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation);
		$theme->options = $this->options;
		$theme->multiple = $this->multiple;
		$theme->size = $this->size;
		$theme->id = $this->name;
		$theme->control = $this;

		return $theme->fetch( $this->get_template(), true );
	}
}

/**
 * A set of checkbox controls based on FormControl for output via a FormUI.
 */
class FormControlCheckboxes extends FormControlSelect
{
	/**
	 * Produce HTML output for this text control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	public function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation);
		$theme->options = $this->options;
		$theme->id = $this->name;
		$theme->control = $this;

		return $theme->fetch( $this->get_template(), true );
	}

	/**
	 * Magic __get method for returning property values
	 * Override the handling of the value property to properly return the setting of the checkbox.
	 *
	 * @param string $name The name of the property
	 * @return mixed The value of the requested property
	 */
	public function __get($name)
	{
		switch ( $name ) {
		case 'value':
			if ( isset($_POST[$this->field . '_submitted']) ) {
				if ( isset($_POST[$this->field]) ) {
					return $_POST[$this->field];
				}
				else {
					return array();
				}
			}
			else {
				return $this->get_default();
			}
		}
		return parent::__get($name);
	}

}

/**
 * A textarea control based on FormControl for output via a FormUI.
 */
class FormControlTextArea extends FormControl
{
	// Placeholder class
}

/**
 * A checkbox control based on FormControl for output via a FormUI.
 */
class FormControlCheckbox extends FormControl
{
	/**
	 * Magic __get method for returning property values
	 * Override the handling of the value property to properly return the setting of the checkbox.
	 *
	 * @param string $name The name of the property
	 * @return mixed The value of the requested property
	 */
	public function __get($name)
	{
		switch ( $name ) {
		case 'value':
			if ( isset($_POST[$this->field . '_submitted']) ) {
				if ( isset($_POST[$this->field]) ) {
					return true;
				}
				else {
					return false;
				}
			}
			else {
				return $this->get_default();
			}
		}
		return parent::__get($name);
	}
}

/**
 * A hidden field control based on FormControl for output via a FormUI.
 */
class FormControlHidden extends FormControl
{

	/**
	 * Produce HTML output for this hidden control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	public function get($forvalidation = true)
	{
		return '<input type="hidden" name="' . $this->field . '" value="' . $this->value . '">';
	}

}

/**
 * A fieldset control based on FormControl for output via a FormUI.
 */
class FormControlFieldset extends FormContainer
{

	/**
	 * Override the FormControl constructor to support more parameters
	 *
	 * @param string $name Name of this control
	 * @param string $caption The legend to display in the fieldset markup
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $caption, $template) = array_merge($args, array_fill(0, 3, null));

		$this->name = $name;
		$this->caption = $caption;
		$this->template = isset($template) ? $template : 'formcontrol_fieldset';
	}
}

/**
 * A div wrapper control based on FormContainer for output via FormUI
 */
class FormControlWrapper extends FormContainer
{
	/**
	 * Override the FormControl constructor to support more parameters
	 *
	 * @param string $name Name of this control
	 * @param string $classes The classes to use in the div wrapper markup
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $class, $template) = array_merge($args, array_fill(0, 3, null));

		$this->name = $name;
		$this->class = $class;
		$this->caption = '';
		$this->template = isset($template) ? $template : 'formcontrol_wrapper';
	}
}

/**
 * A label control based on FormControl for output via a FormUI.
 */
class FormControlLabel extends FormControlNoSave
{

	/**
	 * Produce HTML output for this label control.
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	function get( $forvalidation = true )
	{
		$out = '<div' . (($this->class) ? ' class="' . implode( " ", (array) $this->class ) . '"' : '') . (($this->id) ? ' id="' . $this->id . '"' : '') .'><label for="' . $this->name . '">' . $this->caption . '</label></div>';
		return $out;
	}

}

/**
 * A radio control based on FormControl for output via a FormUI.
 */
class FormControlRadio extends FormControlSelect
{
// Placeholder class
}

/**
 * A file upload control based on FormControl for output via a FormUI.
 */
class FormControlFile extends FormControl
{
	/**
	 * Magic function __get returns properties for this object, or passes it on to the parent class
	 * Potential valid properties:
	 * tmp_file: The uploaded file
	 *
	 * @param string $name The parameter to retrieve
	 * @return mixed The value of the parameter
	 *
	 */
	public function __get($name)
	{
		switch ( $name ) {
			case 'tmp_file':
				return $_FILES[$this->field]['tmp_name'];

			default:
				return parent::__get($name);
		}
	}

	/**
	 * Return the HTML construction of the control, after changing the encoding of the parent form to allow for file uploads.
	 *
	 * @param boolean $forvalidation True if the control should output validation information with the control.
	 */
	public function get($forvalidation = true)
	{
		$form = $this->get_form();
		$form->properties['enctype'] = 'multipart/form-data';

		return parent::get($forvalidation);
	}

	/**
	 * Store this control's value under the control's specified key.
	 *
	 * @param string $storage (optional) A storage location to store the control data
	 */
	public function save($storage = null)
	{
		if ( $storage == null ) {
			$storage = $this->storage;
		}

		if ( is_string($storage) ) {
			$storage = explode(':', $storage, 2);
			switch ( count($storage) ) {
				case 2:
					list($type, $location) = $storage;
					break;
				default:
					return;
			}
		}

		switch ( $type ) {
			case 'silo':
				// TODO
				// Get silo by path $location
				// Create a MediaAsset based on $_FILES[$this->name]['tmp_name']
				break;
			case 'path':
				move_uploaded_file($_FILES[$this->field]['tmp_name'], $location . '/' . basename($_FILES[$this->field]['name']) );
				break;
				/* TODO is there any use for any of these ?
			case 'user':
				User::identify()->info->{$location} = $this->value;
				break;
			case 'option':
				Options::set( $location, $this->value );
				break;
			case 'action':
				Plugins::filter($location, $this->value, $this->name, true);
				break;
			case 'formstorage':
				$storage->field_save( $this->name, $this->value );
				break;
				*/
			case 'null':
				break;
		}
	}
}

/**
 * A control to display media silo contents based on FormControl for output via a FormUI.
 */
class FormControlSilos extends FormControlNoSave
{
// Placeholder class
}

/**
 * A control to display a tab splitter based on FormControl for output via a FormUI.
 */
class FormControlTabs extends FormContainer
{
	/**
	 * Override the FormControl constructor to support more parameters
	 *
	 * @param string $name Name of this control
	 * @param string $caption The legend to display in the fieldset markup
	 */
	public function __construct()
	{
		$args = func_get_args();
		list($name, $caption, $template) = array_merge($args, array_fill(0, 3, null));

		$this->name = $name;
		$this->caption = $caption;
		$this->template = isset($template) ? $template : 'formcontrol_tabs';
	}

	/**
	 * Produce HTML output for all this fieldset and all contained controls
	 *
	 * @param boolean $forvalidation True if this control should render error information based on validation.
	 * @return string HTML that will render this control in the form
	 */
	function get($forvalidation = true)
	{
		$theme = $this->get_theme($forvalidation, $this);

		foreach ( $this->controls as $control ) {
			if ( $control instanceof FormContainer ) {
				$content = '';
				foreach ( $control->controls as $subcontrol ) {
					// There should be a better way to know if a control will produce actual output,
					// but this instanceof is ok for now:
					if ( $content != '' && !( $subcontrol instanceof FormControlHidden ) ) {
						$content .= '<hr>';
					}
					$content .= $subcontrol->get($forvalidation);
				}
				$controls[$control->caption] = $content;
			}
		}
		$theme->controls = $controls;
		// Do not move before $contents
		// Else, these variables will contain the last control's values
		$theme->class = $this->class;
		$theme->id = $this->name;
		$theme->caption = $this->caption;

		return $theme->fetch( $this->template, true );
	}

}

?>