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 FormComponents
{
	/**
	 * Produces a list of HTML parameters from specific values in this object
	 * @param array $map A list of attributes and the fields on this object of which to map the values to them
	 * @param array $additional A list of attributes and values to add explicitly to this output
	 * @return string A list of HTML-style parameters as produced from the input arrays
	 */
	function parameter_map($map = array(), $additional = array()) {
		$output = '';
		foreach($map as $tag_param => $tag_fields) {
			if(is_numeric($tag_param)) {
				$tag_param = $tag_fields;
			}
			$value_out = $this->get_value_out($tag_fields);
			if($value_out) {
				if(is_array($value_out)) {
					$output .= ' ' . $tag_param . '="' . implode(' ', $value_out) . '"';
				}
				else {
					$output .= ' ' . $tag_param . '="' . $value_out . '"';
				}
			}
		}
		foreach($additional as $tag_param => $value_out) {
			$output .= ' ' . $tag_param . '="' . $value_out . '"';
		}
		return $output;
	}

	/**
	 * Return the property value that is associated with the first present property from an array list
	 * @param array $tag_fields A list of potential fields to try
	 * @return bool|string False if no value found, string of the property value found
	 */
	public function get_value_out($tag_fields) {
		$value_out = false;
		foreach(Utils::single_array($tag_fields) as $tag_field) {
			if(isset($this->$tag_field)) {
				$value_out = $this->$tag_field;
				break;
			}
		}
		return $value_out;
	}
}

class FormContainer extends FormComponents
{
	public $name = '';
	public $class = '';
	public $caption = '';
	public $controls = array();
	/** @var Theme $theme_obj */
	protected $theme_obj = null;
	protected $checksum;
	public $template = 'formcontainer';
	public $properties = array();
	public $prefix = '';
	public $postfix = '';

	/**
	 * 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|FormContainer An instance of the named FormControl descendant.
	 */
	public function append()
	{
		$control = null;
		$args = func_get_args();
		$type = array_shift( $args );

		if ( $type instanceof FormControl || $type instanceof FormContainer) {
			$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|FormContainer 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;
		$theme->control = $this;

		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( ); // Create the current theme instead of: 'admin', 'RawPHPEngine', $theme_dir
			// Add the templates for the form controls tothe current theme,
			// and allow any matching templates from the current theme to override
			$formcontrol_templates = Utils::glob($theme_dir . '*.php');
			foreach($formcontrol_templates as $template) {
				$template_name = basename($template, '.php');
				$this->theme_obj->add_template($template_name, $template);
			}
			$list = array();
			$list = Plugins::filter('available_templates', $list);
			foreach($list as $template_name) {
				if($template = Plugins::filter('include_template_file', null, $template_name)) {
					$this->theme_obj->add_template($template_name, $template);
				}
			}
		}
		$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;
	}

	/**
	 * Return the property value that is associated with the first present property from an array list
	 * This version only searches the list of the class' $properties array, because __get() on this objcet returns named FormControls instances
	 * @param array $tag_fields A list of potential fields to try
	 * @return bool|string False if no value found, string of the property value found
	 */
	public function get_value_out($tag_fields) {
		$properties = array_merge($this->properties, get_object_vars($this));
		$value_out = false;
		foreach(Utils::single_array($tag_fields) as $tag_field) {
			if(isset($properties[$tag_field])) {
				$value_out = $properties[$tag_field];
				break;
			}
		}
		return $value_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 implements IsContent
{
	private $success_callback;
	private $success_callback_params = array();
	private $on_save = 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 $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();
				}
			}
		}
		else {
			$_SESSION['forms'][$this->salted_name()]['url'] = Site::get_url( 'habari', true ) . Controller::get_stub();
		}
		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;
		}

		$this->properties['id'] = isset($this->properties['id']) ? $this->properties['id'] : Utils::slugify( $this->name );
		$theme->class = Utils::single_array( $this->class );
		$this->action = $this->options['form_action'];
		$theme->salted_name = $this->salted_name();
		$theme->pre_out = $this->pre_out_controls();

		$out = $this->prefix . $theme->display_fallback( $this->options['template'], 'fetch' ) . $this->postfix;
		$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 ) {
			$out .= '<script type="text/javascript">if(controls==undefined){var controls = {init:function(fn){if(fn!=undefined){controls.inits.push(fn);}else{for(var i in controls.inits){controls.inits[i]();}}},inits:[]};}</script>';
		}
		foreach ( $this->controls as $control ) {
			$out .= $control->pre_out( );
		}
		if ( !FormUI::$outpre ) {
			FormUI::$outpre = true;
			$out .= '<script type="text/javascript">window.setTimeout(function(){controls.init();}, 500);</script>';
		}
		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;
	}

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

	/**
	 * 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();
		}
		foreach ( $this->on_save as $save ) {
			$callback = array_shift( $save );
			if ( is_callable( $callback ) ) {
				array_unshift( $save, $this );
				call_user_func_array( $callback, $save );
			}
			else {
				array_unshift( $save, $callback, $this );
				call_user_func_array( array( 'Plugins', 'act' ), $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;";
	}

	/**
	 * Redirect the user back to the stored URL value in session
	 */
	public function bounce()
	{
		$_SESSION['forms'][$this->salted_name()]['error_data'] = $_POST;
		Utils::redirect( $_SESSION['forms'][$this->salted_name()]['url'] );
	}

	/**
	 * Implementation of IsContent
	 * @return array An array of content types that this object represents, starting with the most specific
	 */
	public function content_type()
	{
		return array('form');
	}

	/**
	 * Convert this object instance to a string
	 * @return string The form as HTML
	 */
	public function __toString()
	{
		return $this->get();
	}
}

/**
 * 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, $schemes = array( 'http', 'https' ) )
	{
		if ( ! empty( $text ) ) {
			$parsed = InputFilter::parse_url( $text );
			if ( $parsed['is_relative'] ) {
				// guess if they meant to use an absolute link
				$parsed = InputFilter::parse_url( 'http://' . $text );
				if ( $parsed['is_error'] ) {
					// disallow relative URLs
					$warning = empty( $warning ) ? _t( 'Relative urls are not allowed' ) : $warning;
					return array( $warning );
				}
			}
			if ( $parsed['is_pseudo'] || ! in_array( $parsed['scheme'], $schemes ) ) {
				// allow only http(s) URLs
				$warning = empty( $warning ) ? _t( 'Only %s urls are allowed', array( Format::and_list( $schemes ) ) ) : $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 ( ! empty( $text ) ) {
			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 extends FormComponents
{
	public $caption;
	protected $default = null;
	protected $validators = array();
	protected $storage;
	protected $store_user = false;
	protected $theme_obj;
	public $container = null;
	public $id = null;
	public $class = array( 'formcontrol' );
	public $name;
	public $properties = array();
	public $template = null;
	public $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';
		}
		elseif ( is_array( $storage ) ) {
			$type = 'actionarray';
			$location = array_shift( $storage );
		}
		else {
			// Dunno what was intended here, but it wasn't a valid/known storage option, so store nothing
			$type = 'null';
		}

		switch ( $type ) {
			case 'user':
				$user = User::identify();
				$user->info->{$location} = $this->value;
				$user->info->commit();
				break;
			case 'option':
				Options::set( $location, $this->value );
				break;
			case 'filter':
				Plugins::filter( $location, $this->value, $this->name, true, $this );
				break;
			case 'action':
				Plugins::act( $location, $this->value, $this->name, true, $this );
				break;
			case 'actionarray':
				Plugins::act( $location, $this->value, $this->name, $storage );
				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 isset($this->properties['pre_out']) ? $this->properties['pre_out'] : '';
	}

	/**
	 * 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 ( property_exists( $this, $name ) ) {
			return $this->$name;
		}
		if ( isset($this->properties[$name])) {
			return $this->properties[$name];
		}
		return null;
	}

	/**
	 * Magic function __isset returns whether properties exist 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 boolean True if the property exists
	 */
	public function __isset( $name )
	{
		switch ( $name ) {
			case 'field':
			case 'value':
				return true;
		}
		if ( property_exists( $this, $name ) ) {
			return true;
		}
		if ( isset( $this->properties[$name] ) ) {
			return true;
		}
		return false;
	}

	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;

				// add the template to the list of css classes - keyed so subsequent changes overwrite it, rather than append
				$this->class['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 )
	{
		return $this->container->get_theme( $forvalidation, $this );
	}

	/**
	 * 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
{
	/**
	 * FormControlText 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 ) );
		parent::__construct($name, $storage, $caption, $template);
		$this->properties['type'] = 'text';
	}

}

/**
 * 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 FormControl
{
	/**
	 * 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 );
		$max = Tags::vocabulary()->max_count();

		$tag = $this->tag;

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

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

}

/**
 * A password control based on FormControlText for output via a FormUI.
 */
class FormControlTags extends FormControlText
{
	public static $outpre = false;

	public function pre_out()
	{
		$out = '';
		if ( !FormControlTextMulti::$outpre ) {
			FormControlTextMulti::$outpre = true;
			$out = <<< TAGS_PRE_OUT
<script type="text/javascript">
$(function(){
	$('input.tags_control').each(function(){

		for(var z in tc_tags=$(this).val().split(/\s*,\s*/)) {
			tc_tags[z]=tc_tags[z].replace(/^(["'])(.*)\1$/, '$2');
		}
		console.log(tc_tags);

		\$this = $(this);
		ajax_url = $(this).data('ajax_url');
		console.log(ajax_url);
		\$this.select2({
			tags: tc_tags,
			placeholder: "Tags",
			minimumInputLength: 1,
			ajax: {
				url: ajax_url,
				dataType: 'json',
				quietMillis: 100,
				data: function (term, page) {
					return { q: term };
				},
				results: function (data, page) {
					var results = {};
					for(var z in data.data) {
						results[parseInt(z)] = {id: parseInt(z), text: data.data[z]};
					}
					return {results: results, more: false};
				},
				formatSelection: function(item) {
					return item.text;
				},
				formatResult: function(item) {
					return item.text;
				}
			}
		});

	});
});
</script>
TAGS_PRE_OUT;
		}
		return $out;
	}
}

/**
 * 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(" <span class=\"textmulti_item\"><input type=\"text\" name=\"" + field + "[]\"> <a href=\"#\" onclick=\"return controls.textmulti.remove(this);\" title=\"'. _t( 'Remove item' ).'\" class=\"textmulti_remove opa50\">[' . _t( 'remove' ) . ']</a></span>");
						return false;
					},
					remove: function(e){
						if (confirm("' . _t( 'Remove this item?' ) . '")) {
							if ( $(e).parent().parent().find("input").length == 1) {
								field = $(e).prev().attr("name");
								$(e).parent().prev().before("<input type=\"hidden\" name=\"" + field + "\" value=\"\">");
							}
							$(e).parent().prev("input").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 $template
	 */
	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 );

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

		$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 set of checkbox controls based on FormControl for output via a FormUI.
 */
class FormControlTree extends FormControlSelect
{
	public $options = array();
	public static $outpre = false;

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

		$this->name = $name;
		$this->storage = $storage;
		$this->caption = $caption;
		$this->template = $template;
		$this->config = empty($config) ? array() : $config;
	}

		/**
	 * 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 ( !FormControlTree::$outpre ) {
			FormControlTree::$outpre = true;
			$out = <<<  CUSTOM_TREE_JS
				<script type="text/javascript">
controls.init(function(){
	$('ol.tree').nestedSortable({
		disableNesting: 'no-nest',
		forcePlaceholderSize: true,
		handle: 'div',
		items: 'li.treeitem',
		opacity: .6,
		placeholder: 'placeholder',
		tabSize: 25,
		tolerance: 'pointer',
		toleranceElement: '> div'
	});

	$('.tree_submitted').closest('form').submit(function(){
		var tree_input = $('.tree_submitted', this);
		var data = tree_input.siblings().nestedSortable('toArray', {startDepthCount: 1});
		var comma = '';
		var v = '';
		for(var i in data) {
			if(data[i].item_id != 'root') {
				v += comma + '{"id":"' + parseInt(data[i].item_id) + '","left":"' + (parseInt(data[i].left)-1) + '","right":"' + (parseInt(data[i].right)-1) + '"}';
				comma = ',';
			}
		}
		v = '[' + v + ']';
		tree_input.val(v);
	});
});
				</script>
CUSTOM_TREE_JS;
		}
		return $out;
	}

	/**
	 * 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->value;
		$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 )
	{
		static $posted = null;
		switch ( $name ) {
			case 'value':
				if ( isset( $_POST[$this->field . '_submitted'] ) ) {
					if(!isset($posted)) {
						$valuesj = $_POST->raw( $this->field . '_submitted');
						$values = json_decode($valuesj);
						$terms = array();
						foreach($this->get_default() as $term) {
							$terms[$term->id] = $term;
						}
						foreach($values as $value) {
							$terms[$value->id]->mptt_left = $value->left;
							$terms[$value->id]->mptt_right = $value->right;
						}
						$terms = new Terms($terms);
						$posted = $terms->tree_sort();
					}
					return $posted;
				}
				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 )
	{
		$output = '<input type="hidden" name="' . $this->field . '" value="' . $this->value . '"';
		if(isset($this->id)) {
			$output .= ' id="' . $this->id . '"';
		}
		$output .= '>';
		return $output;
	}

}

/**
 * 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 );
	}

}

?>


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

/**
 * HabariDateTime class to wrap dates in.
 *
 * @property-read HabariDateTime $clone Returns a clonned object.
 * @property-read string $sql Returns a unix timestamp for inserting into DB.
 * @property-read int $int Returns a unix timestamp as integer.
 * @property-read string $time Returns the time formatted according to the blog's settings.
 * @property-read string $date Returns the date formatted according to the blog's settings.
 * @property-read string $friendly Returns the time as a friendly string (ie: 4 months, 3 days ago, etc.).
 * @property-read string $fuzzy Returns the time as a short "fuzzy" string (ie: "just now", "yesterday", "2 weeks ago", etc.).
 */
class HabariDateTime extends DateTime
{
	private static $default_timezone;
	private static $default_datetime_format = 'c';
	private static $default_date_format;
	private static $default_time_format;

	// various time increments in seconds
	const YEAR		= 31556926;
	const MONTH		= 2629744;
	const WEEK		= 604800;
	const DAY		= 86400;
	const HOUR		= 3600;
	const MINUTE	= 60;

	/**
	 * Set default timezone to system default on init.
	 *
	 * @static
	 */
	public static function __static()
	{
		if ( Options::get( 'timezone' ) ) {
			self::set_default_timezone( Options::get( 'timezone' ) );
		}

		self::$default_timezone = date_default_timezone_get();

		self::$default_date_format = Options::get( 'dateformat' );
		self::$default_time_format = Options::get( 'timeformat' );

		if ( self::$default_date_format || self::$default_time_format ) {
			self::set_default_datetime_format( self::$default_date_format . ' ' . self::$default_time_format );
		}
	}

	/**
	 * Set default date/time format. The format is the same as the
	 * internal php {@link http://ca.php.net/date date() function}.
	 *
	 * @static
	 * @param string $format The date format.
	 */
	public static function set_default_datetime_format( $format )
	{
		self::$default_datetime_format = $format;
	}

	/**
	 * Get the default date/time format set.
	 *
	 * @static
	 * @see set_default_datetime_format()
	 * @return string The date format set.
	 */
	public static function get_default_datetime_format()
	{
		$user_datetime_format = User::identify()->info->locale_date_format . ' ' . User::identify()->info->locale_time_format;
		if ( $user_datetime_format != self::$default_datetime_format ) {
			self::set_default_datetime_format( $user_datetime_format );
		}
		return self::$default_datetime_format;
	}

	/**
	 * Sets the timezone for Habari and PHP.
	 *
	 * @static
	 * @param string $timezone A timezone name, not an abbreviation, for example 'America/New York'
	 */
	public static function set_default_timezone( $timezone )
	{
		self::$default_timezone = $timezone;
		date_default_timezone_set( self::$default_timezone );
	}

	/**
	 * Get the timezone for Habari and PHP.
	 * Defaults to system timezone if not set.
	 *
	 * @static
	 * @see set_default_timezone()
	 * @param string The deafult timezone.
	 */
	public static function get_default_timezone()
	{
		return self::$default_timezone;
	}

	/**
	 * Helper function to create a HabariDateTime object for the given
	 * time and timezone. If no time is given, defaults to 'now'. If no
	 * timezone given defaults to timezone set in {@link set_default_timezone()}
	 *
	 * @static
	 * @see DateTime::__construct()
	 * @param string $time String in a format accepted by
	 * {@link http://ca.php.net/strtotime strtotime()}, defaults to "now".
	 * @param string $timezone A timezone name, not an abbreviation.
	 */
	public static function date_create( $time = null, $timezone = null )
	{
		if ( $time instanceOf HabariDateTime ) {
			return $time;
		}
		elseif ( $time instanceOf DateTime ) {
			$time = $time->format( 'U' );
		}
		elseif ( $time === null ) {
			$time = 'now';
		}
		elseif ( is_numeric( $time ) ) {
			$time = '@' . $time;
		}

		if ( $timezone === null ) {
			$timezone = self::$default_timezone;
		}

		// passing the timezone to construct doesn't seem to do anything.
		$datetime = new HabariDateTime( $time );
		$datetime->set_timezone( $timezone );
		return $datetime;
	}

	/**
	 * Set the date of this object
	 *
	 * @see DateTime::setDate()
	 * @param int $year Year of the date
	 * @param int $month Month of the date
	 * @param int $day Day of the date
	 */
	public function set_date( $year, $month, $day )
	{
		parent::setDate( $year, $month, $day );
		return $this;
	}

	/**
	 * Sets the ISO date
	 *
	 * @see DateTime::setISODate()
	 * @param int $year Year of the date
	 * @param int $month Month of the date
	 * @param int $day Day of the date
	 */
	public function set_isodate( $year, $week, $day = null )
	{
		parent::setISODate( $year, $week, $day );
		return $this;
	}

	/**
	 * Set the time of this object
	 *
	 * @see DateTime::setTime()
	 * @param int $hour Hour of the time
	 * @param int $minute Minute of the time
	 * @param int $second Second of the time
	 */
	public function set_time( $hour, $minute, $second = null )
	{
		parent::setTime( $hour, $minute, $second );
		return $this;
	}

	/**
	 * Set the timezone for this datetime object. Can be either string
	 * timezone identifier, or DateTimeZone object.
	 *
	 * @see DateTime::setTimezone()
	 * @param mixed The timezone to use.
	 * @return HabariDateTime $this object.
	 */
	public function set_timezone( $timezone )
	{
		if ( ! $timezone instanceof DateTimeZone ) {
			$timezone = new DateTimeZone( $timezone );
		}
		parent::setTimezone( $timezone );
		return $this;
	}

	/**
	 * Get the timezone identifier that is set for this datetime object.
	 *
	 * @return DateTimeZone The timezone object.
	 */
	public function get_timezone()
	{
		return parent::getTimezone();
	}

	/**
	 * Returns date formatted according to given format.
	 *
	 * @see DateTime::format()
	 * @param string $format Format accepted by {@link http://php.net/date date()}.
	 * @return string The formatted date, false on failure.
	 */
	public function format( $format = null )
	{
		$day_months = array(
			'January' => _t( 'January' ),
			'February' => _t( 'February' ),
			'March' => _t( 'March' ),
			'April' => _t( 'April' ),
			'May' => _t( 'May' ),
			'June' => _t( 'June' ),
			'July' => _t( 'July' ),
			'August' => _t( 'August' ),
			'September' => _t( 'September' ),
			'October' => _t( 'October' ),
			'November' => _t( 'November' ),
			'December' => _t( 'December' ),
			'Jan' => _t( 'Jan' ),
			'Feb' => _t( 'Feb' ),
			'Mar' => _t( 'Mar' ),
			'Apr' => _t( 'Apr' ),
			'May' => _t( 'May' ),
			'Jun' => _t( 'Jun' ),
			'Jul' => _t( 'Jul' ),
			'Aug' => _t( 'Aug' ),
			'Sep' => _t( 'Sep' ),
			'Oct' => _t( 'Oct' ),
			'Nov' => _t( 'Nov' ),
			'Dec' => _t( 'Dec' ),
			'Sunday' => _t( 'Sunday' ),
			'Monday' => _t( 'Monday' ),
			'Tuesday' => _t( 'Tuesday' ),
			'Wednesday' => _t( 'Wednesday' ),
			'Thursday' => _t( 'Thursday' ),
			'Friday' => _t( 'Friday' ),
			'Saturday' => _t( 'Saturday' ),
			'Sun' => _t( 'Sun' ),
			'Mon' => _t( 'Mon' ),
			'Tue' => _t( 'Tue' ),
			'Wed' => _t( 'Wed' ),
			'Thu' => _t( 'Thu' ),
			'Fri' => _t( 'Fri' ),
			'Sat' => _t( 'Sat' ),
			'am' => _t( 'am' ),
			'pm' => _t( 'pm' ),
			'AM' => _t( 'AM' ),
			'PM' => _t( 'PM' ),
		);

		if ( $format === null ) {
			$format = self::$default_datetime_format;
		}

		$result = parent::format( $format );

		if ( ! $result ) {
			return false;
		}

		$result = Multibyte::str_replace( array_keys( $day_months ), array_values( $day_months ), $result );

		return $result;
	}

	/**
	 * Returns date components inserted into a string
	 *
	 * Example:
	 * echo HabariDateTime::date_create('2010-01-01')->text_format('The year was {Y}.');
	 * // Expected output:  The year was 2010.
	 *
	 * @param string $format A string with single-character date format codes {@link http://php.net/date date()} surrounded by braces
	 * @return string The string with date components inserted
	 */
	public function text_format( $format )
	{
		return preg_replace_callback( '%\{(\w)\}%iu', array( $this, 'text_format_callback' ), $format );
	}

	/**
	 * Callback method for supplying replacements for HabariDatTime::text_format()
	 *
	 * @param array $matches The matches found in the regular expression.
	 * @return string The date component value for the matched character.
	 */
	private function text_format_callback( $matches )
	{
		return $this->format( $matches[1] );
	}

	/**
	 * Alters the timestamp
	 *
	 * @param string $format A format accepted by {@link http://php.net/strtotime strtotime()}.
	 * @return HabariDateTime $this object.
	 */
	public function modify( $args )
	{
		parent::modify( $args );
		return $this;
	}

	/**
	 * @see format()
	 */
	public function get( $format = null )
	{
		return $this->format( $format );
	}

	/**
	 * Echos date formatted according to given format.
	 *
	 * @see format()
	 * @param string $format Format accepted by {@link http://php.net/date date()}.
	 */
	public function out( $format = null )
	{
		echo $this->format( $format );
	}

	/**
	 * Magic method called when this object is cast to string. Returns the
	 * unix timestamp of this object.
	 *
	 * @return string The unix timestamp
	 */
	public function __toString()
	{
		return $this->format( 'U' );
	}

	/**
	 * Magic method to get magic ponies... properties, I mean.
	 */
	public function __get( $property )
	{
		// if you add more cases to this list, please also add the repsective @property to the top of the class so it shows up propertly in IDEs!
		switch ( $property ) {
			case 'clone':
				return clone $this;

			case 'sql':
				return $this->format( 'U' );
				break;

			case 'int':
				return intval( $this->format( 'U' ) );
				break;

			case 'time':
				return $this->format( self::get_default_time_format() );
				break;

			case 'date':
				return $this->format( self::get_default_date_format() );
				break;

			case 'friendly':
				return $this->friendly();
				break;

			case 'fuzzy':
				return $this->fuzzy();
				break;

			default:
				$info = getdate( $this->format( 'U' ) );
				$info['mon0'] = substr( '0' . $info['mon'], -2, 2 );
				$info['mday0'] = substr( '0' . $info['mday'], -2, 2 );
				if ( isset( $info[$property] ) ) {
					return $info[$property];
				}
				return $this->$property;
		}
	}

	/**
	 * Return the default date format, as set in the Options table
	 *
	 * @return The default date format
	 **/
	public static function get_default_date_format()
	{
		if ( isset(User::identify()->info->local_date_format) && User::identify()->info->locale_date_format != Options::get( 'dateformat' ) ) {
			self::set_default_date_format( User::identify()->info->locale_date_format );
		}
		return self::$default_date_format;
	}

	/**
	 * Set default date format. The format is the same as the
	 * internal php {@link http://ca.php.net/date date() function}.
	 *
	 * @static
	 * @param string $format The date format.
	 */
	public static function set_default_date_format( $format )
	{
		self::$default_date_format = $format;
	}

	/**
	 * Return the default time format, as set in the Options table
	 *
	 * @return The default time format
	 **/
	public static function get_default_time_format()
	{
		if ( isset(User::identify()->info->local_time_format) && User::identify()->info->locale_time_format != Options::get( 'timeformat' ) ) {
			self::set_default_time_format( User::identify()->info->locale_time_format );
		}
		return self::$default_time_format;
	}

	/**
	 * Set default time format. The format is the same as the
	 * internal php {@link http://ca.php.net/date date() function}.
	 *
	 * @static
	 * @param string $format The time format.
	 */
	public static function set_default_time_format( $format )
	{
		self::$default_time_format = $format;
	}

	/**
	 * Returns an associative array containing the date information for
	 * this HabariDateTime object, as per {@link http://php.net/getdate getdate()}
	 *
	 * @return array Associative array containing the date information
	 */
	public function getdate()
	{
		$info = getdate( $this->format( 'U' ) );
		$info['mon0'] = substr( '0' . $info['mon'], -2, 2 );
		$info['mday0'] = substr( '0' . $info['mday'], -2, 2 );
		return $info;
	}

	/**
	 * Returns a friendlier string version of the time, ie: 3 days, 1 hour, and 5 minutes ago
	 *
	 * @param int $precision Only display x intervals. Note that this does not round, it only limits the display length.
	 * @param boolean $include_suffix Include the 'ago' or 'from now' suffix?
	 * @return string Time passed in the specified units.
	 */
	public function friendly ( $precision = 7, $include_suffix = true )
	{

		$difference = self::difference( self::date_create(), $this );

		$result = array();

		if ( $difference['y'] ) {
			$result[] = sprintf( '%d %s', $difference['y'], _n( 'year', 'years', $difference['y'] ) );
		}

		if ( $difference['m'] ) {
			$result[] = sprintf( '%d %s', $difference['m'], _n( 'month', 'months', $difference['m'] ) );
		}

		if ( $difference['w'] ) {
			$result[] = sprintf( '%d %s', $difference['w'], _n( 'week', 'weeks', $difference['w'] ) );
		}

		if ( $difference['d'] ) {
			$result[] = sprintf( '%d %s', $difference['d'], _n( 'day', 'days', $difference['d'] ) );
		}

		if ( $difference['h'] ) {
			$result[] = sprintf( '%d %s', $difference['h'], _n( 'hour', 'hours', $difference['h'] ) );
		}

		if ( $difference['i'] ) {
			$result[] = sprintf( '%d %s', $difference['i'], _n( 'minute', 'minutes', $difference['i'] ) );
		}

		if ( $difference['s'] ) {
			$result[] = sprintf( '%d %s', $difference['s'], _n( 'second', 'seconds', $difference['s'] ) );
		}

		// limit the precision
		$result = array_slice( $result, 0, $precision );

		$result = Format::and_list( $result );

		if ( $include_suffix ) {

			if ( $difference['invert'] == true ) {
				$result = _t( '%s from now', array( $result ) );
			}
			else {
				$result = _t( '%s ago', array( $result ) );
			}

		}

		return $result;

	}

	/**
	 * Similar to friendly(), but much more... fuzzy.
	 *
	 * Returns a very short version of the difference in time between now and the current HDT object.
	 */
	public function fuzzy ()
	{
		$difference = self::date_create()->int - $this->int;

		if ( $difference < self::MINUTE ) {
			$result = _t( 'just now' );
		}
		else if ( $difference < self::HOUR ) {
			$minutes = round( $difference / self::MINUTE );
			$result = sprintf( _n( '%d minute ago', '%d minutes ago', $minutes ), $minutes );
		}
		else if ( $difference < self::DAY ) {
			$hours = round( $difference / self::HOUR );
			$result = sprintf( _n( '%d hour ago', '%d hours ago', $hours ), $hours );
		}
		else if ( $difference < self::WEEK ) {
			$days = round( $difference / self::DAY );
			$result = sprintf( _n( 'yesterday', '%d days ago', $days ), $days );
		}
		else if ( $difference < self::MONTH ) {
			$weeks = round( $difference / self::WEEK );
			$result = sprintf( _n( 'last week', '%d weeks ago', $weeks ), $weeks );
		}
		else if ( $difference < self::YEAR ) {
			$months = round( $difference / self::MONTH );
			$result = sprintf( _n( 'last month', '%d months ago', $months ), $months );
		}
		else {
			$years = round( $difference / self::YEAR );
			$result = sprintf( _n( 'last year', '%d years ago', $years ), $years );
		}

		return $result;

	}

	/**
	 * Returns an array representing the difference between two times by interval.
	 *
	 * <code>
	 * 	print_r( HabariDateTime::difference( 'now', 'January 1, 2010' ) );
	 * 	// output (past): Array ( [invert] => [y] => 0 [m] => 9 [w] => 3 [d] => 5 [h] => 22 [i] => 33 [s] => 5 )
	 * 	print_r( HabariDateTime::difference( 'now', 'January 1, 2011' ) );
	 * 	// output (future): Array ( [invert] => 1 [y] => 0 [m] => 2 [w] => 0 [d] => 3 [h] => 5 [i] => 33 [s] => 11 )
	 * </code>
	 *
	 *  If 'invert' is true, the time is in the future (ie: x from now). If it is false, the time is in the past (ie: x ago).
	 *
	 *  For more information, see PHP's DateInterval class, which this and friendly() attempt to emulate for < PHP 5.3
	 *
	 *  @todo Add total_days, total_years, etc. values?
	 *
	 * @param mixed $start_date The start date, as a HDT object or any format accepted by HabariDateTime::date_create().
	 * @param mixed $end_date The end date, as a HDT object or any format accepted by HabariDateTime::date_create().
	 * @return array Array of each interval and whether the interval is inverted or not.
	 */
	public static function difference( $start_date, $end_date )
	{

		// if the dates aren't HDT objects, try to convert them to one. this lets you pass in just about any format
		if ( !$start_date instanceof HabariDateTime ) {
			$start_date = HabariDateTime::date_create( $start_date );
		}

		if ( !$end_date instanceof HabariDateTime ) {
			$end_date = HabariDateTime::date_create( $end_date );
		}

		$result = array();

		// calculate the difference, in seconds
		$difference = $end_date->int - $start_date->int;

		if ( $difference < 0 ) {
			// if it's negative, time AGO
			$result['invert'] = false;
		}
		else {
			// if it's positive, time UNTIL
			$result['invert'] = true;
		}

		$difference = abs( $difference );

		// we'll progressively subtract from the seconds left, so initialize it
		$seconds_left = $difference;

		$result['y'] = floor( $seconds_left / self::YEAR );
		$seconds_left = $seconds_left - ( $result['y'] * self::YEAR );

		$result['m'] = floor( $seconds_left / self::MONTH );
		$seconds_left = $seconds_left - ( $result['m'] * self::MONTH );

		$result['w'] = floor( $seconds_left / self::WEEK );
		$seconds_left = $seconds_left - ( $result['w'] * self::WEEK );

		$result['d'] = floor( $seconds_left / self::DAY );
		$seconds_left = $seconds_left - ( $result['d'] * self::DAY );

		$result['h'] = floor( $seconds_left / self::HOUR );
		$seconds_left = $seconds_left - ( $result['h'] * self::HOUR );

		$result['i'] = floor( $seconds_left / self::MINUTE );
		$seconds_left = $seconds_left - ( $result['i'] * self::MINUTE );

		$result['s'] = $seconds_left;

		return $result;

	}

}

?>

repo_name: DaSourcerer/habari


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

/**
 * Habari Locale Class
 *
 * Provides translation services.
 *
 */
class HabariLocale
{
	private static $uselocale = false;
	private static $messages = array();
	private static $plural_function;
	private static $locale;

	/**
	 * Sets the locale for Habari.
	 *
	 * @param string $locale A language code like 'en' or 'en-us' or 'x-klingon', will be lowercased
	 */
	public static function set( $locale = null )
	{
		if ( $locale == null ) {
			return;
		}

		self::$locale = strtolower( $locale );
		self::$uselocale = self::load_domain( 'habari' );
	}

	/**
	 * Return the current locale
	 * @static
	 * @return string The current locale.
	 */
	public static function get()
	{
		return self::$locale;
	}

	/**
	 * Set system locale.
	 *
	 * The problem is that every platform has its own way to designate a locale,
	 * so for German you could have 'de', 'de_DE', 'de_DE.UTF-8', 'de_DE.UTF-8@euro'
	 * (Linux) or 'DEU' (Windows), etc.
	 *
	 * @todo: This setting should probably be stored in the language files.
	 *
	 * @param string... $locale The locale(s) to set. They will be tried in order.
	 * @return string the locale that was picked, or false if an error occurred
	 */
	public static function set_system_locale()
	{
		if ( func_num_args() == 0 ) return;
		$args = func_get_args();
		array_unshift( $args, LC_ALL );

		return call_user_func_array( 'setlocale', $args );
	}

	/**
	 * Load translations for a given domain and base directory for a pluggable object.
	 * Translations are stored in gettext-style .mo files.
	 * The internal workings of the file format are not entirely meant to be understood.
	 *
	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
	 * @param string $domain the domain to load
	 * @param string $base_dir the base directory in which to find the translation files
	 * @return boolean true if data was successfully loaded, false otherwise
	 */
	public static function load_pluggable_domain( $domain, $base_dir )
	{
		$file = $base_dir . '/locale/' . self::$locale . '/LC_MESSAGES/' . $domain . '.mo';
		return self::load_file( $domain, $file );
	}

	/**
	 * Load translations for a given domain.
	 * Translations are stored in gettext-style .mo files.
	 * The internal workings of the file format are not entirely meant to be understood.
	 *
	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
	 * @param string $domain the domain to load
	 * @return boolean true if data was successfully loaded, false otherwise
	 */
	private static function load_domain( $domain )
	{
		$file_end = self::$locale . '/LC_MESSAGES/' . $domain . '.mo';

		if ( file_exists( Site::get_dir( 'config' ) . '/locale/' . $file_end ) ) {
			$file = Site::get_dir( 'config' ) . '/locale/' . $file_end;
		}
		else if ( file_exists( HABARI_PATH . '/user/locale/' . $file_end ) ) {
			$file = HABARI_PATH . '/user/locale/' . $file_end;
		}
		else if ( file_exists( HABARI_PATH . '/3rdparty/locale/' . $file_end ) ) {
			$file = HABARI_PATH . '/3rdparty/locale/' . $file_end;
		}
		else {
			$file = HABARI_PATH . '/system/locale/' . $file_end;
		}

		return self::load_file( $domain, $file );
	}

	/**
	 * function list_all
	 * Retrieves an array of the Habari locales that are installed
	 *
	 * @return array. An array of Habari locales in the installation
	 */
	public static function list_all()
	{
		$localedirs = array( HABARI_PATH . '/system/locale/', HABARI_PATH . '/3rdparty/locale/', HABARI_PATH . '/user/locale/' );
		if ( Site::CONFIG_LOCAL != Site::$config_type ) {
			// include site-specific locales
			$localedirs[] = Site::get_dir( 'config' ) . '/locale/';
		}

		$dirs = array();
		foreach ( $localedirs as $localedir ) {
			if ( file_exists( $localedir ) ) {
				$dirs = array_merge( $dirs, Utils::glob( $localedir . '*', GLOB_ONLYDIR | GLOB_MARK ) );
			}
		}
		$dirs = array_filter( $dirs, function( $a ) {return file_exists($a . "LC_MESSAGES/habari.mo"); } );

		$locales = array_map( 'basename', $dirs );
		ksort( $locales );
		return $locales;
	}

	/**
	 * Load translations from a given file.
	 *
	 * @param string $domain the domain to load the data into
	 * @param string $file the file name
	 * @return boolean true if data was successfully loaded, false otherwise
	 */
	private static function load_file( $domain, $file )
	{
		if ( ! file_exists( $file ) ) {
			Error::raise( _t( 'No translations found for locale %s, domain %s!', array( self::$locale, $domain ) ) );
			return false;
		}
		if ( filesize( $file ) < 24 ) {
			Error::raise( _t( 'Invalid .MO file for locale %s, domain %s!', array( self::$locale, $domain ) ) );
			return false;
		}

		$fp = fopen( $file, 'rb' );
		$data = fread( $fp, filesize( $file ) );
		fclose( $fp );

		// determine endianness
		$little_endian = true;

		list(,$magic) = unpack( 'V1', substr( $data, 0, 4 ) );
		switch ( $magic & 0xFFFFFFFF ) {
			case (int)0x950412de:
				$little_endian = true;
				break;
			case (int)0xde120495:
				$little_endian = false;
				break;
			default:
				Error::raise( _t( 'Invalid magic number 0x%08x in %s!', array( $magic, $file ) ) );
				return false;
		}

		$revision = substr( $data, 4, 4 );
		if ( $revision != 0 ) {
			Error::raise( _t( 'Unknown revision number %d in %s!', array( $revision, $file ) ) );
			return false;
		}

		$l = $little_endian ? 'V' : 'N';

		if ( $data && strlen( $data ) >= 20 ) {
			$header = substr( $data, 8, 12 );
			$header = unpack( "{$l}1msgcount/{$l}1msgblock/{$l}1transblock", $header );

			if ( $header['msgblock'] + ($header['msgcount'] - 1 ) * 8 > filesize( $file ) ) {
				Error::raise( _t( 'Message count (%d) out of bounds in %s!', array( $header['msgcount'], $file ) ) );
				return false;
			}

			$lo = "{$l}1length/{$l}1offset";

			for ( $msgindex = 0; $msgindex < $header['msgcount']; $msgindex++ ) {
				$msginfo = unpack( $lo, substr( $data, $header['msgblock'] + $msgindex * 8, 8 ) );
				$msgids = explode( "\0", substr( $data, $msginfo['offset'], $msginfo['length'] ) );
				$transinfo = unpack( $lo, substr( $data, $header['transblock'] + $msgindex * 8, 8 ) );
				$transids = explode( "\0", substr( $data, $transinfo['offset'], $transinfo['length'] ) );
				self::$messages[$domain][$msgids[0]] = array(
					$msgids,
					$transids,
				);
			}
		}

		// setup plural functionality
		self::$plural_function = self::get_plural_function( self::$messages[$domain][''][1][0] );

		// only use locale if we actually read something
		return ( count( self::$messages ) > 0 );
	}

	private static function get_plural_function( $header )
	{
		if ( preg_match( '/plural-forms: (.*?)\n/i', $header, $matches ) && preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $matches[1], $matches ) ) {
			// sanitize
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );

			$body = str_replace(
				array( 'plural',  'n',  '$n$plurals', ),
				array( '$plural', '$n', '$nplurals', ),
				'nplurals='. $nplurals . '; plural=' . $plural
			);

			// add parens
			// important since PHP's ternary evaluates from left to right
			$body .= ';';
			$res = '';
			$p = 0;
			for ( $i = 0; $i < strlen( $body ); $i++ ) {
				$ch = $body[$i];
				switch ( $ch ) {
					case '?':
						$res .= ' ? (';
						$p++;
						break;
					case ':':
						$res .= ') : (';
						break;
					case ';':
						$res .= str_repeat( ')', $p ) . ';';
						$p = 0;
						break;
					default:
						$res .= $ch;
				}
			}

			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
			$fn = create_function(
				'$n',
				$body
			);
		}
		else {
			// default: one plural form for all cases but n==1 (english)
			$fn = create_function(
				'$n',
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
			);
		}

		return $fn;
	}

	/**
	 * DO NOT USE THIS FUNCTION.
	 * This function is only to be used by the test case for the Locale class!
	 */
	public static function __run_plural_test( $header )
	{
		$fn = self::get_plural_function( $header );
		$res = '';
		for ( $n = 0; $n < 200; $n++ ) {
			$res .= $fn( $n );
		}

		return $res;
	}

	/**
	 * DO NOT USE THIS FUNCTION.
	 * This function is only to be used by the test case for the Locale class!
	 */
	public static function __run_loadfile_test( $filename )
	{
		return self::load_file( 'test', $filename );
	}

	/**
	 * Echo a version of the string translated into the current locale
	 * @param string $text The text to echo translated
	 * @param string $domain (optional) The domain to search for the message
	 */
	public static function _e()
	{
		$args = func_get_args();
		echo call_user_func_array( array( 'HabariLocale', '_t' ), $args );
	}

	/**
	 * Return a version of the string translated into the current locale
	 *
	 * @param string $text The text to echo translated
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The translated string
	 */
	public static function _t( $text, $args = array(), $domain = 'habari' )
	{
		if ( is_string( $args ) ) {
			$domain = $args;
		}

		if ( isset( self::$messages[$domain][$text] ) ) {
			$t = self::$messages[$domain][$text][1][0];
		}
		else {
			$t = $text;
		}

		if ( !empty( $args ) && is_array( $args ) ) {
			array_unshift( $args, $t );
			$t = call_user_func_array( 'sprintf', $args );
		}

		return $t;
	}

	/**
	 * Return the entire message catalog for a domain as an array
	 *
	 * @static
	 * @param string $domain (optional) The domain to return
	 * @return array The array of locale messages
	 */
	public static function get_messages($domain = 'habari')
	{
		return self::$messages[$domain];
	}

	/**
	 * Given a string translated into the current locale, return the untranslated string.
	 *
	 * @param string $text The translated string
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The untranslated string
	 */
	public static function _u( $text, $domain = 'habari' )
	{
		$t = $text;
		foreach ( self::$messages[$domain] as $msg ) {
			if ( $text == $msg[1][0] ) {
				$t = $msg[0][0];
				break;
			}
		}

		return $t;
	}
	/**
	 * Echo singular or plural version of the string, translated into the current locale, based on the count provided
	 *
	 * @param string $singular The singular form
	 * @param string $plural The plural form
	 * @param string $count The count
	 * @param string $domain (optional) The domain to search for the message
	 */
	public static function _ne( $singular, $plural, $count, $domain = 'habari' )
	{
		echo self::_n( $singular, $plural, $count, $domain );
	}

	/**
	 * Return a singular or plural string translated into the current locale based on the count provided
	 *
	 * @param string $singular The singular form
	 * @param string $plural The plural form
	 * @param string $count The count
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The appropriately translated string
	 */
	public static function _n( $singular, $plural, $count, $domain = 'habari' )
	{
		if ( isset( self::$messages[$domain][$singular] ) ) {
			// XXX workaround, but direct calling doesn't work
			$fn = self::$plural_function;
			$n = $fn( $count );
			if ( isset( self::$messages[$domain][$singular][1][$n] ) ) {
				return self::$messages[$domain][$singular][1][$n];
			}
		}
		// fall-through else for both cases
		return ( $count == 1 ? $singular : $plural );
	}

	/**
	 * Return a translated value of a SimpleXml object value based on the locale and namespace
	 *
	 * @param SimpleXMLElement $parent The parent node of the node we are translating
	 * @param SimpleXMLElement $child The child node we're trying to translate
	 * @param string The namespace to use for the lang attribute of the node to be translated
	 * @param string $locale The locale we want to translate into
	 * @return nothing. Translations are done in place
	 * @todo These defaults may need tweaked. It seems there should be a better way to do this whole thing
	 */
	public static function translate_xml( $parent, $child, $ns = 'http://www.w3.org/XML/1998/namespace', $locale = null )
	{
		$use = null;
		// Set the locale
		if( !isset( $locale ) ) {
			$locale = self::$locale;
		}
		// Loop through the child elements
		foreach( $child as $el ) {
			$attr = $el->attributes( $ns );
			foreach( $attr as $key => $value ) {
				// Found one with the language of the current locale
				if ( $key == 'lang' && $value == $locale ) {
					$use = (string)$el;
				}
			}
		}
		// If a translation for the element is found, use it instead of the default value
		if ( ! empty( $use ) ) {
			$name = $child->getName();
			unset( $parent->$name );
			$parent->addChild( $name, $use );
		}
	}
}

/**
 * Echo a version of the string translated into the current locale, alias for HabariLocale::_e()
 *
 * @param string $text The text to translate
 */
function _e( $text, $args = array(), $domain = 'habari' )
{
	return HabariLocale::_e( $text, $args, $domain );
}

/**
 * function _ne
 * Echo singular or plural version of the string, translated into the current locale, based on the count provided,
 * alias for HabariLocale::_ne()
 * @param string $singular The singular form
 * @param string $plural The plural form
 * @param string $count The count
 */
function _ne( $singular, $plural, $count, $domain = 'habari' )
{
	return HabariLocale::_ne( $singular, $plural, $count, $domain );
}

/**
 * Return a version of the string translated into the current locale, alias for HabariLocale::_t()
 *
 * @param string $text The text to translate
 * @return string The translated string
 */
function _t( $text, $args = array(), $domain = 'habari' )
{
	return HabariLocale::_t( $text, $args, $domain );
}

/**
 * Return a singular or plural string translated into the current locale based on the count provided
 *
 * @param string $singular The singular form
 * @param string $plural The plural form
 * @param string $count The count
 * @return string The appropriately translated string
 */
function _n( $singular, $plural, $count, $domain = 'habari' )
{
	return HabariLocale::_n( $singular, $plural, $count, $domain );
}

/**
 * Given a string translated into the current locale, return the untranslated version of the string.
 * Alias for HabariLocale::_u()
 *
 * @param string $text The translated string
 * @param string $domain (optional) The domain to search for the message
 * @return string The untranslated string
 */
function _u( $text, $domain = 'habari' )
{
	return HabariLocale::_u( $text, $domain );
}

?>