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

/**
* Habari Terms Class
* Holds multiple Term object instances in an array-like structure,
* for the purpose of acting on them en-masse, or testing against them.
*
*/
class Terms extends ArrayObject implements FormStorage
{

	/**
	 * See if a term or set of terms is in this set of terms
	 *
	 * @param mixed $tags. A string containing a string or a comma separated list of strings,
	 *  or an array of strings, Terms, or a Term subclass
	 * @return boolean. Whether the tag(s) is in the current set of tags.
	 */
	public function has( $tags )
	{
		if ( is_string( $tags ) || ( is_array( $tags ) && is_string( $tags[0] ) ) ) {
			$tags = (array)Terms::parse( $tags );
		}

		$diff = array_diff( $tags, (array)$this );
		foreach ( $tags as $tag ) {
			if ( in_array( $tag, $diff ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Turns a comma-separated string or array of terms into an array of Term objects
	 * @param mixed $terms A comma-separated string or array of string terms
	 * @param string $term_class The class of the Term object type to create from each string
	 * @param Vocabulary $vocabulary An instance of the Vocabulary that might hold the terms.
	 * 	 Use existing term object data if found in the specified vocabulary.
	 * @return Terms An instance of Terms contianing the specified Term objects
	 **/
	public static function parse( $terms, $term_class = 'Term', $vocabulary = null )
	{
		if ( is_string( $terms ) ) {
			if ( '' === $terms ) {
				return new Terms();
			}
			$terms = trim( MultiByte::str_replace( '"', '"', $terms ) );
			// dirrty ;)
			$rez = array( '\\"'=>':__unlikely_quote__:', '\\\''=>':__unlikely_apos__:' );
			$zer = array( ':__unlikely_quote__:'=>'"', ':__unlikely_apos__:'=>"'" );
			// escape
			$tagstr = str_replace( array_keys( $rez ), $rez, $terms );
			// match-o-matic
			preg_match_all( '/((("|((?<= )|^)\')\\S([^\\3]*?)\\3((?=[\\W])|$))|[^,])+/u', $tagstr, $matches );
			// cleanup
			$terms = array_map( 'trim', $matches[0] );
			$terms = preg_replace( array_fill( 0, count( $terms ), '/^(["\'])(((?!").)+)(\\1)$/' ), '$2', $terms );
			// unescape
			$terms = str_replace( array_keys( $zer ), $zer, $terms );
			// hooray
		}
		if ( is_array( $terms ) ) {
			if ( $vocabulary instanceof Vocabulary ) {
				foreach ( $terms as $k => $term ) {
					if ( $saved_term = $vocabulary->get_term( $term, $term_class ) ) {
						$terms[$k] = $saved_term;
					}
					else {
						$terms[$k] = new $term_class( $term );
					}
				}
			}
			else {
				array_walk( $terms, function( &$tag ) use ($term_class) {$tag = new $term_class($tag);} );
			}
			return new Terms( $terms );
		}
		return new Terms();
	}

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

	/**
	 * Stores a form value into the object
	 *
	 * @param string $key The name of a form component that will be stored
	 * @param mixed $value The value of the form component to store
	 */
	function field_save($key, $value)
	{
		Vocabulary::prep_update($value);
		foreach($value as $term) {
			if($term instanceof Term) {
				$term->update();
			}
		}
	}

	/**
	 * Sort the term objects by mptt_left ASC to put them in tree order
	 *
	 * @return Terms A sorted Terms instance
	 */
	function tree_sort($sort_fn = null)
	{
		if(empty($sort_fn)) {
			$sort_fn = function($a, $b) {
				return $a->mptt_left > $b->mptt_left;
			};
		}
		$terms = $this->getArrayCopy();
		usort($terms, $sort_fn);
		return new Terms($terms);
	}
}

?>


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

/**
 * Habari Theme Class
 *
 * The Theme class is the behind-the-scenes representation of
 * of a set of UI files that compose the visual theme of the blog
 *
 */
class Theme extends Pluggable
{
	public $name = null;
	public $version = null;
	public $template_engine = null;
	public $theme_dir = null;
	public $config_vars = array();
	private $var_stack = array( array() );
	private $current_var_stack = 0;
	public $context = array();
	private $added_template_vars = false;

	/**
	 * We build the Post filters by analyzing the handler_var
	 * data which is assigned to the handler ( by the Controller and
	 * also, optionally, by the Theme )
	 */
	public $valid_filters = array(
		'preset',
		'content_type',
		'not:content_type',
		'slug',
		'not:slug',
		'user_id',
		'vocabulary',
		'status',
		'page',
		'tag',
		'not:tag',
		'month',
		'year',
		'day',
		'criteria',
		'limit',
		'nolimit',
		'offset',
		'fetch_fn',
		'id',
		'info',
		'has:info',
		'all:info',
		'any:info',
		'not:info',
		'not:all:info',
		'not:any:info',
	);

	/**
	 * Constructor for theme
	 *
	 * If no parameter is supplied, then the constructor
	 * Loads the active theme from the database.
	 *
	 * If no theme option is set, a fatal error is thrown
	 *
	 * @param name            ( optional ) override the default theme lookup
	 * @param template_engine ( optional ) specify a template engine
	 * @param theme_dir       ( optional ) specify a theme directory
	 */
	public function __construct( $themedata )
	{
		$this->name = $themedata->name;
		$this->version = $themedata->version;
		$theme_dir = Utils::single_array($themedata->theme_dir);
		// Set up the corresponding engine to handle the templating
		$this->template_engine = new $themedata->template_engine();

		$this->theme_dir = $theme_dir;
		$this->template_engine->set_template_dir( $theme_dir );
		$this->plugin_id = $this->plugin_id();
		$this->load();
	}

	/**
	 * Loads a theme's metadata from an XML file in theme's
	 * directory.
	 *
	 */
	public function info()
	{

		$xml_file = end($this->theme_dir) . '/theme.xml';
		if(!file_exists($xml_file)) {
			return new SimpleXMLElement('<?xml version="1.0" encoding="utf-8" ?>
<pluggable type="theme">
	<name>Unknown Theme</name>
	<version>1.0</version>
</pluggable>
');
		}
		if ( $xml_content = file_get_contents( $xml_file ) ) {
			$theme_data = new SimpleXMLElement( $xml_content );
			return $theme_data;
		}
	}

	/**
	 * Assign the default variables that would be used in every template
	 */
	public function add_template_vars()
	{
		// set the locale and character set that habari is configured to use presently
		if ( !isset( $this->locale ) ) {
			$this->locale = Options::get('locale', 'en');	// default to 'en' just in case we somehow don't have one?
		}

		if ( !isset( $this->charset ) ) {
			$this->charset = MultiByte::hab_encoding();
		}

		if ( !$this->template_engine->assigned( 'user' ) ) {
			$this->assign( 'user', User::identify() );
		}

		if ( !$this->template_engine->assigned( 'loggedin' ) ) {
			$this->assign( 'loggedin', User::identify()->loggedin );
		}

		if ( !$this->template_engine->assigned( 'page' ) ) {
			$this->assign( 'page', isset( $this->page ) ? $this->page : 1 );
		}

		$handler = Controller::get_handler();
		if ( isset( $handler ) ) {
			Plugins::act( 'add_template_vars', $this, $handler->handler_vars );
		}
		$this->added_template_vars = true;
	}

	/**
	 * Find the first template that matches from the list provided and display it
	 * @param array $template_list The list of templates to search for
	 */
	public function display_fallback( $template_list, $display_function = 'display' )
	{
		foreach ( (array)$template_list as $template ) {
			if ( $this->template_exists( $template ) ) {
				$this->assign( '_template_list', $template_list );
				$this->assign( '_template', $template );
				return $this->$display_function( $template );
			}
		}
		return false;
	}

	/**
	 * Determine if a template exists in the current theme
	 *
	 * @param string $template_name The name of the template to detect
	 * @return boolean True if template exists
	 */
	public function template_exists( $template_name )
	{
		return $this->template_engine->template_exists( $template_name );
	}

	/**
	 * Grabs post data and inserts that data into the internal
	 * handler_vars array, which eventually gets extracted into
	 * the theme's ( and thereby the template_engine's ) local
	 * symbol table for use in the theme's templates
	 *
	 * This is the default, generic function to grab posts.  To
	 * "filter" the posts retrieved, simply pass any filters to
	 * the handler_vars variables associated with the post retrieval.
	 * For instance, to filter by tag, ensure that handler_vars['tag']
	 * contains the tag to filter by.  Simple as that.
	 */
	public function act_display( $paramarray = array( 'user_filters'=> array() ) )
	{
		Utils::check_request_method( array( 'GET', 'HEAD', 'POST' ) );

		// Get any full-query parameters
		$possible = array( 'user_filters', 'fallback', 'posts', 'post', 'content_type' );
		foreach ( $possible as $varname ) {
			if ( isset( $paramarray[$varname] ) ) {
				$$varname = $paramarray[$varname];
			}
		}

		$where_filters = array();
		$where_filters = Controller::get_handler_vars()->filter_keys( $this->valid_filters );
		$where_filters['vocabulary'] = array();

		if ( array_key_exists( 'tag', $where_filters ) ) {
			$tags = Tags::parse_url_tags( $where_filters['tag'] );
			$not_tag = $tags['exclude_tag'];
			$all_tag = $tags['include_tag'];
			if ( count( $not_tag ) > 0 ) {
				$where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':not:term' => $not_tag ) );
			}
			if ( count( $all_tag ) > 0 ) {
				$where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':all:term' => $all_tag ) );
			}
			$where_filters['tag_slug'] = Utils::slugify( $where_filters['tag'] );
			unset( $where_filters['tag'] );
		}
		if ( !isset( $_GET['preview'] ) ) {
			$where_filters['status'] = Post::status( 'published' );
		}

		if ( !isset( $posts ) ) {
			$user_filters = Plugins::filter( 'template_user_filters', $user_filters );

			// Work around the tags parameters to Posts::get() being subsumed by the vocabulary parameter
			if( isset( $user_filters['not:tag'] ) ) {
				$user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':not:term' => $user_filters['not:tag'] );
				unset( $user_filters['not:tag'] );
			}
			if( isset( $user_filters['tag'] ) ) {
				$user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':term_display' => $user_filters['tag'] );
				unset( $user_filters['tag'] );
			}

			$where_filters = $where_filters->merge( $user_filters );
			$where_filters = Plugins::filter( 'template_where_filters', $where_filters );
			$posts = Posts::get( $where_filters );
		}

		$this->assign( 'posts', $posts );

		if ( $posts !== false && count( $posts ) > 0 ) {
			if ( count( $posts ) == 1 ) {
				$post = $posts instanceof Post ? $posts : reset( $posts );
				Stack::add( 'body_class', Post::type_name( $post->content_type ) . '-' . $post->id );
			}
			else {
				$post = reset( $posts );
				Stack::add( 'body_class', 'multiple' );
			}
			$this->assign( 'post', $post );
			$type = Post::type_name( $post->content_type );
		}
		elseif ( ( $posts === false ) ||
			( isset( $where_filters['page'] ) && $where_filters['page'] > 1 && count( $posts ) == 0 ) ) {
			if ( $this->template_exists( '404' ) ) {
				$fallback = array( '404' );
				// Replace template variables with the 404 rewrite rule
				$this->request->{URL::get_matched_rule()->name} = false;
				$this->request->{URL::set_404()->name} = true;
				$this->matched_rule = URL::get_matched_rule();
				// 404 status header sent in act_display_404, but we're past
				// that, so send it now.
				header( 'HTTP/1.1 404 Not Found', true, 404 );
			}
			else {
				$this->display( 'header' );
				echo '<h2>';
				_e( "Whoops! 404. The page you were trying to access is not really there. Please try again." );
				echo '</h2>';
				header( 'HTTP/1.1 404 Not Found', true, 404 );
				$this->display( 'footer' );
				die;
			}
		}

		$extract = $where_filters->filter_keys( 'page', 'type', 'id', 'slug', 'posttag', 'year', 'month', 'day', 'tag', 'tag_slug' );
		foreach ( $extract as $key => $value ) {
			$$key = $value;
		}

		$this->assign( 'page', isset( $page )? $page:1 );

		if ( !isset( $fallback ) ) {
			// Default fallbacks based on the number of posts
			$fallback = array( '{$type}.{$id}', '{$type}.{$slug}', '{$type}.tag.{$posttag}' );
			if ( count( $posts ) > 1 ) {
				$fallback[] = '{$type}.multiple';
				$fallback[] = 'multiple';
			}
			else {
				$fallback[] = '{$type}.single';
				$fallback[] = 'single';
			}
		}

		$searches = array( '{$id}','{$slug}','{$year}','{$month}','{$day}','{$type}','{$tag}', );
		$replacements = array(
			( isset( $post ) && $post instanceof Post ) ? $post->id : '-',
			( isset( $post ) && $post instanceof Post ) ? $post->slug : '-',
			isset( $year ) ? $year : '-',
			isset( $month ) ? $month : '-',
			isset( $day ) ? $day : '-',
			isset( $type ) ? $type : '-',
			isset( $tag_slug ) ? $tag_slug : '-',
		);
		$fallback[] = 'home';
		$fallback = Plugins::filter( 'template_fallback', $fallback, $posts, isset( $post ) ? $post : null );
		$fallback = array_values( array_unique( MultiByte::str_replace( $searches, $replacements, $fallback ) ) );
		for ( $z = 0; $z < count( $fallback ); $z++ ) {
			if ( ( MultiByte::strpos( $fallback[$z], '{$posttag}' ) !== false ) && ( isset( $post ) ) && ( $post instanceof Post ) ) {
				$replacements = array();
				if ( $alltags = $post->tags ) {
					foreach ( $alltags as $current_tag ) {
						$replacements[] = MultiByte::str_replace( '{$posttag}', $current_tag->term, $fallback[$z] );
					}
					array_splice( $fallback, $z, 1, $replacements );
				}
				else {
					break;
				}
			}
		}
		return $this->display_fallback( $fallback );
	}

	/**
	 * Helper function: Displays the home page
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_display_home( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'home',
			'multiple',
		);

		// Makes sure home displays only entries
		$default_filters = array(
			'preset' => 'home',
		);

		$paramarray['user_filters'] = array_merge( $default_filters, $user_filters );

		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Displays multiple entries
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_display_entries( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'{$type}.multiple',
			'multiple',
		);

		// Makes sure home displays only entries
		$default_filters = array(
			'content_type' => Post::type( 'entry' ),
		);

		$paramarray['user_filters'] = array_merge( $default_filters, $user_filters );

		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Display a post
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_display_post( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'{$type}.{$id}',
			'{$type}.{$slug}',
			'{$type}.tag.{$posttag}',
			'{$type}.single',
			'{$type}.multiple',
			'single',
			'multiple',
		);

		// Does the same as a Post::get()
		$default_filters = array(
			'fetch_fn' => 'get_row',
			'limit' => 1,
		);

		// Remove the page from filters.
		$page_key = array_search( 'page', $this->valid_filters );
		unset( $this->valid_filters[$page_key] );

		$paramarray['user_filters'] = array_merge( $default_filters, $user_filters );

		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Display the posts for a tag
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_display_tag( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'tag.{$tag}',
			'tag',
			'multiple',
		);

		// Makes sure home displays only entries
		$default_filters = array(
			'content_type' => Post::type( 'entry' ),
		);

		$this->assign( 'tag', Controller::get_var( 'tag' ) );

		// Assign tag objects to the theme
		$tags = Tags::parse_url_tags( Controller::get_var( 'tag' ), true );
		$this->assign( 'include_tag', $tags['include_tag'] );
		$this->assign( 'exclude_tag', $tags['exclude_tag'] );
		$paramarray['user_filters'] = array_merge( $default_filters, $user_filters );

		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Display the posts for a specific date
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_display_date( $user_filters = array() )
	{
		$handler_vars = Controller::get_handler()->handler_vars;
		$y = isset( $handler_vars['year'] );
		$m = isset( $handler_vars['month'] );
		$d = isset( $handler_vars['day'] );

		if ( $y && $m && $d ) {
			$paramarray['fallback'][] = 'year.{$year}.month.{$month}.day.{$day}';
		}
		if ( $y && $m && $d ) {
			$paramarray['fallback'][] = 'year.month.day';
		}
		if ( $m && $d ) {
			$paramarray['fallback'][] = 'month.{$month}.day.{$day}';
		}
		if ( $y && $m ) {
			$paramarray['fallback'][] = 'year.{$year}.month.{$month}';
		}
		if ( $y && $d ) {
			$paramarray['fallback'][] = 'year.{$year}.day.{$day}';
		}
		if ( $m && $d ) {
			$paramarray['fallback'][] = 'month.day';
		}
		if ( $y && $d ) {
			$paramarray['fallback'][] = 'year.day';
		}
		if ( $y && $m ) {
			$paramarray['fallback'][] = 'year.month';
		}
		if ( $m ) {
			$paramarray['fallback'][] = 'month.{$month}';
		}
		if ( $d ) {
			$paramarray['fallback'][] = 'day.{$day}';
		}
		if ( $y ) {
			$paramarray['fallback'][] = 'year.{$year}';
		}
		if ( $y ) {
			$paramarray['fallback'][] = 'year';
		}
		if ( $m ) {
			$paramarray['fallback'][] = 'month';
		}
		if ( $d ) {
			$paramarray['fallback'][] = 'day';
		}
		$paramarray['fallback'][] = 'date';
		$paramarray['fallback'][] = 'multiple';
		$paramarray['fallback'][] = 'home';

		$paramarray['user_filters'] = $user_filters;
		if ( !isset( $paramarray['user_filters']['content_type'] ) ) {
			$paramarray['user_filters']['content_type'] = Post::type( 'entry' );
		}

		$this->assign( 'year', $y ? (int)Controller::get_var( 'year' ) : null );
		$this->assign( 'month', $m ? (int)Controller::get_var( 'month' ) : null );
		$this->assign( 'day', $d ? (int)Controller::get_var( 'day' ) : null );

		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Display the posts for a specific criteria
	 * @param array $user_filters Additional arguments used to get the page content
	 */
	public function act_search( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'search',
			'multiple',
		);

		$paramarray['user_filters'] = $user_filters;

		$this->assign( 'criteria', Controller::get_var( 'criteria' ) );
		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Display a 404 template
	 *
	 * @param array $user_filters Additional arguments user to get the page content
	 */
	public function act_display_404( $user_filters = array() )
	{
		$paramarray['fallback'] = array(
			'404',
		);

		header( 'HTTP/1.1 404 Not Found' );
		$paramarray['user_filters'] = $user_filters;
		return $this->act_display( $paramarray );
	}

	/**
	 * Helper function: Avoids having to call $theme->template_engine->display( 'template_name' );
	 * @param string $template_name The name of the template to display
	 */
	public function display( $template_name )
	{
		$this->play_var_stack();

		$this->template_engine->assign( 'theme', $this );
		$this->template_engine->display( $template_name );
	}

	/**
	 * Helper function: Avoids having to call $theme->template_engine->fetch( 'template_name' );
	 *
	 * @param string $template_name The name of the template to display
	 * @param boolean $unstack If true, end the current template variable buffer upon returning
	 * @return string The content of the template
	 */
	public function fetch( $template_name, $unstack = false )
	{
		$this->play_var_stack();

		$this->template_engine->assign( 'theme', $this );

		$return = $this->fetch_unassigned( $template_name );
		if ( $unstack ) {
			$this->end_buffer();
		}
		return $return;
	}

	/**
	 * Play back the full stack of template variables to assign them into the template
	 */
	protected function play_var_stack()
	{
		if(!$this->added_template_vars) {
			$this->add_template_vars();
		}
		$this->template_engine->clear();
		for ( $z = 0; $z <= $this->current_var_stack; $z++ ) {
			foreach ( $this->var_stack[$z] as $key => $value ) {
				$this->template_engine->assign( $key, $value );
			}
		}
	}

	/**
	 * Calls the template engine's fetch() method without pre-assigning template variables.
	 * Assumes that the template variables have already been set.
	 *
	 * @param string $template_name The name of the template to display
	 * @return string The content of the template
	 */
	public function fetch_unassigned( $template_name )
	{
		return $this->template_engine->fetch( $template_name );
	}

	/**
	 * Helper function: Avoids having to call $theme->template_engine->key= 'value';
	 */
	public function assign( $key, $value )
	{
		$this->var_stack[$this->current_var_stack][$key] = $value;
	}

	/**
	 * Aggregates and echos the additional header code by combining Plugins and Stack calls.
	 */
	public function theme_header( $theme )
	{

		// create a stack of the atom tags before the first action so they can be unset if desired
		Stack::add( 'template_atom', array( 'alternate', 'application/atom+xml', 'Atom 1.0', implode( '', $this->feed_alternate_return() ) ), 'atom' );
		Stack::add( 'template_atom', array( 'service', 'application/atomsvc+xml', 'Atom Publishing Protocol', URL::get( 'atompub_servicedocument' ) ), 'app' );
		Stack::add( 'template_atom', array( 'EditURI', 'application/rsd+xml', 'RSD', URL::get( 'rsd' ) ), 'rsd' );

		Plugins::act( 'template_header', $theme );

		$atom = Stack::get( 'template_atom', '<link rel="%1$s" type="%2$s" title="%3$s" href="%4$s">' );
		$styles = Stack::get( 'template_stylesheet', array( 'Stack', 'styles' ) );
		$scripts = Stack::get( 'template_header_javascript', array( 'Stack', 'scripts' ) );

		$output = implode( "\n", array( $atom, $styles, $scripts ) );

		Plugins::act( 'template_header_after', $theme );

		return $output;
	}

	/**
	 * Aggregates and echos the additional footer code by combining Plugins and Stack calls.
	 */
	public function theme_footer( $theme )
	{
		Plugins::act( 'template_footer', $theme );
		$output = Stack::get( 'template_footer_stylesheet', array( 'Stack', 'styles' ) );
		$output .= Stack::get( 'template_footer_javascript', array( 'Stack', 'scripts' ) );
		return $output;
	}

	/**
	 * Display an object using a template designed for the type of object it is
	 * The $object is assigned into the theme using the $content template variable
	 *
	 * @param Theme $theme The theme used to display the object
	 * @param object $object An object to display
	 * @param string $context The context in which the object will be displayed
	 * @return
	 */
	public function theme_content( $theme, $object, $context = null )
	{
		$fallback = array();
		$content_types = array();
		if ( $object instanceof IsContent ) {
			$content_types = Utils::single_array( $object->content_type() );
		}
		if ( is_object( $object ) ) {
			$content_types[] = strtolower( get_class( $object ) );
		}
		$content_types[] = 'content';
		$content_types = array_flip( $content_types );
		if ( isset( $context ) ) {
			foreach ( $content_types as $type => $type_id ) {
				$content_type = $context . $object->content_type();
				$fallback[] = strtolower( $context . '.' . $type );
			}
		}
		foreach ( $content_types as $type => $type_id ) {
			$fallback[] = strtolower( $type );
		}
		if ( isset( $context ) ) {
			$fallback[] = strtolower( $context );
		}
		$fallback = array_unique( $fallback );

		$this->content = $object;
		if(isset($context)) {
			$this->context[] = $context;
		}
		$result = $this->display_fallback( $fallback, 'fetch' );
		if(isset($context)) {
			array_pop($this->context);
		}
		if( $result === false && DEBUG ) {
			$fallback_list = implode( ', ', $fallback );
			$result = '<p>' . _t( 'Content could not be displayed. One of the following templates - %s - has to be present in the active theme.', array( $fallback_list ) ) . '</p>';
		}
		return $result;
	}

	/**
	 * Check to see if the theme is currently rendering a specific context
	 * @param string $context The context to check for.
	 * @return bool True if the context is active.
	 */
	public function theme_has_context($theme, $context)
	{
		if(in_array($context, $theme->context)) {
			return true;
		}
		return false;
	}

	/**
	 * Returns the appropriate alternate feed based on the currently matched rewrite rule.
	 *
	 * @param mixed $return Incoming return value from other plugins
	 * @param Theme $theme The current theme object
	 * @return string Link to the appropriate alternate Atom feed
	 */
	public function theme_feed_alternate( $theme )
	{
		$matched_rule = URL::get_matched_rule();
		if ( is_object( $matched_rule ) ) {
			// This is not a 404
			$rulename = $matched_rule->name;
		}
		else {
			// If this is a 404 and no rewrite rule matched the request
			$rulename = '';
		}
		switch ( $rulename ) {
			case 'display_entry':
			case 'display_page':
				return URL::get( 'atom_entry', array( 'slug' => Controller::get_var( 'slug' ) ) );
				break;
			case 'display_entries_by_tag':
				return URL::get( 'atom_feed_tag', array( 'tag' => Controller::get_var( 'tag' ) ) );
				break;
			case 'display_home':
			default:
				return URL::get( 'atom_feed', array( 'index' => '1' ) );
		}
		return '';
	}

	/**
	 * Returns the feedback URL to which comments should be submitted for the indicated Post
	 *
	 * @param Theme $theme The current theme
	 * @param Post $post The post object to get the feedback URL for
	 * @return string The URL to the feedback entrypoint for this comment
	 */
	public function theme_comment_form_action( $theme, $post )
	{
		return URL::get( 'submit_feedback', array( 'id' => $post->id ) );
	}

	/**
	 * Build a collection of paginated URLs to be used for pagination.
	 *
	 * @param string The RewriteRule name used to build the links.
	 * @param array Various settings used by the method and the RewriteRule.
	 * @return string Collection of paginated URLs built by the RewriteRule.
	 */
	public static function theme_page_selector( $theme, $rr_name = null, $settings = array() )
	{
		// We can't detect proper pagination if $theme->posts isn't a Posts object,
		// so if it's not, bail.
		if(!$theme->posts instanceof Posts) {
			return '';
		}
		$current = $theme->page;
		$items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ?
			$theme->posts->get_param_cache['limit'] :
			Options::get( 'pagination' );
		$total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page );

		// Make sure the current page is valid
		if ( $current > $total ) {
			$current = $total;
		}
		else if ( $current < 1 ) {
			$current = 1;
		}

		// Number of pages to display on each side of the current page.
		$leftSide = isset( $settings['leftSide'] ) ? $settings['leftSide'] : 1;
		$rightSide = isset( $settings['rightSide'] ) ? $settings['rightSide'] : 1;

		// Add the page '1'.
		$pages[] = 1;

		// Add the pages to display on each side of the current page, based on $leftSide and $rightSide.
		for ( $i = max( $current - $leftSide, 2 ); $i < $total && $i <= $current + $rightSide; $i++ ) {
			$pages[] = $i;
		}

		// Add the last page if there is more than one page.
		if ( $total > 1 ) {
			$pages[] = (int) $total;
		}

		// Sort the array by natural order.
		natsort( $pages );

		// This variable is used to know the last page processed by the foreach().
		$prevpage = 0;
		// Create the output variable.
		$out = '';

		if ( 1 === count( $pages ) && isset( $settings['hideIfSinglePage'] ) &&  $settings['hideIfSinglePage'] === true ) {
			return '';
		}

		foreach ( $pages as $page ) {
			$settings['page'] = $page;

			// Add ... if the gap between the previous page is higher than 1.
			if ( ( $page - $prevpage ) > 1 ) {
				$out .= ' <span class="sep">…</span>';
			}
			// Wrap the current page number with square brackets.
			$caption = ( $page == $current ) ?  $current  : $page;
			// Build the URL using the supplied $settings and the found RewriteRules arguments.
			$url = URL::get( $rr_name, $settings, false );
			// Build the HTML link.
			$out .= ' <a href="' . $url . '" ' . ( ( $page == $current ) ? 'class="current-page"' : '' ) . '>' . $caption . '</a>';

			$prevpage = $page;
		}

		return $out;
	}

	/**
	 *Provides a link to the previous page
	 *
	 * @param string $text text to display for link
	 */
	public function theme_prev_page_link( $theme, $text = null )
	{
		$settings = array();

		// If there's no previous page, skip and return null
		$settings['page'] = (int) ( $theme->page - 1 );
		if ( $settings['page'] < 1 ) {
			return null;
		}

		// If no text was supplied, use default text
		if ( $text == '' ) {
			$text = '← ' . _t( 'Previous' );
		}

		return '<a class="prev-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>';
	}

	/**
	 *Provides a link to the next page
	 *
	 * @param string $text text to display for link
	 */
	public function theme_next_page_link( $theme, $text = null )
	{
		$settings = array();

		// If there's no next page, skip and return null
		$settings['page'] = (int) ( $theme->page + 1 );
		$items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ?
			$theme->posts->get_param_cache['limit'] :
			Options::get( 'pagination' );
		$total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page );
		if ( $settings['page'] > $total ) {
			return null;
		}

		// If no text was supplied, use default text
		if ( $text == '' ) {
			$text = _t( 'Next' ) . ' →';
		}

		return '<a class="next-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>';
	}

	/**
	 * Returns a full qualified URL of the specified post based on the comments count, and links to the post.
 	 *
	 * Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires".
	 *
	 * Since we use sprintf() in the final concatenation, you must format passed strings accordingly.
	 *
	 * @param Theme $theme The current theme object
	 * @param Post $post Post object used to build the comments link
	 * @param string $zero String to return when there are no comments
	 * @param string $one String to return when there is one comment
	 * @param string $many String to return when there are more than one comment
	 * @param string $fragment Fragment (bookmark) portion of the URL to append to the link
	 * @param string $title Fragment (bookmark) portion of the URL to append to the link
	 * @return string Linked string to display for comment count
	 * @see Theme::theme_comments_count()
	 */
	public function theme_comments_link( $theme, $post, $zero = '', $one = '', $many = '', $fragment =  'comments' )
	{
		$count = $theme->comments_count_return( $post, $zero, $one, $many );
		return '<a href="' . $post->permalink . '#' . $fragment . '" title="' . _t( 'Read Comments' ) . '">' . end( $count ) . '</a>';
	}

	/**
	 * Returns a full qualified URL of the specified post based on the comments count.
 	 *
	 * Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires".
	 *
	 * Since we use sprintf() in the final concatenation, you must format passed strings accordingly.
	 *
	 * @param Theme $theme The current theme object
	 * @param Post $post Post object used to build the comments link
	 * @param string $zero String to return when there are no comments
	 * @param string $one String to return when there is one comment
	 * @param string $many String to return when there are more than one comment
	 * @return string String to display for comment count
	 */
	public function theme_comments_count( $theme, $post, $zero = '', $one = '', $many = '' )
	{
		$count = $post->comments->approved->count;
		if ( $count == 0 ) {
			$text = empty( $zero ) ? _t( 'No Comments' ) : $zero;
			return sprintf( $text, $count );
		}
		else {
			if ( empty( $one ) && empty( $many ) ) {
				$text = _n( '%s Comment', '%s Comments', $count );
			}
			else {
				if ( empty( $one ) ) {
					$one = $many;
				}
				if ( empty( $many ) ) {
					$many = $one;
				}
				$text = $count == 1 ? $one : $many;
			}
			return sprintf( $text, $count );
		}
	}

	/**
	 * Returns the count of queries executed
	 *
	 * @return integer The query count
	 */
	public function theme_query_count()
	{
		return count( DB::get_profiles() );
	}

	/**
	 * Returns total query execution time in seconds
	 *
	 * @return float Query execution time in seconds, with fractions.
	 */
	public function theme_query_time()
	{
		return array_sum( Utils::array_map_field(DB::get_profiles(), 'total_time') );
	}

	/**
	 * Returns a humane commenter's link for a comment if a URL is supplied, or just display the comment author's name
	 *
	 * @param Theme $theme The current theme
	 * @param Comment $comment The comment object
	 * @return string A link to the comment author or the comment author's name with no link
	 */
	public function theme_comment_author_link( $theme, $comment )
	{
		$url = $comment->url;
		if ( $url != '' ) {
			$parsed_url = InputFilter::parse_url( $url );
				if ( $parsed_url['host'] == '' ) {
					$url = '';
			}
			else {
				$url = InputFilter::glue_url( $parsed_url );
			}
		}
		if ( $url != '' ) {
			return '<a href="'.$url.'">' . $comment->name . '</a>';
		}
		else {
			return $comment->name;
		}
	}

	/**
	 * Detects if a variable is assigned to the template engine for use in
	 * constructing the template's output.
	 *
	 * @param key name of variable
	 * @returns boolean true if name is set, false if not set
	 */
	public function __isset( $key )
	{
		return isset( $this->var_stack[$this->current_var_stack][$key] );
	}

	/**
	 * Set a template variable, a property alias for assign()
	 *
	 * @param string $key The template variable to set
	 * @param mixed $value The value of the variable
	 */
	public function __set( $key, $value )
	{
		$this->assign( $key, $value );
	}

	/**
	 * Get a template variable value
	 *
	 * @param string $key The template variable name to get
	 * @return mixed The value of the variable
	 */
	public function __get( $key )
	{
		if ( isset( $this->var_stack[$this->current_var_stack][$key] ) ) {
			return $this->var_stack[$this->current_var_stack][$key];
		}
		return '';
	}

	/**
	 * Remove a template variable value
	 *
	 * @param string $key The template variable name to unset
	 */
	public function __unset( $key )
	{
		unset( $this->var_stack[$this->current_var_stack][$key] );
	}

	/**
	 * Start a new template variable buffer
	 */
	public function start_buffer()
	{
		$this->current_var_stack++;
		$this->var_stack[$this->current_var_stack] = $this->var_stack[$this->current_var_stack - 1];
	}

	/**
	 * End the current template variable buffer
	 */
	public function end_buffer()
	{
		unset( $this->var_stack[$this->current_var_stack] );
		$this->current_var_stack--;
	}

	/**
	 * Handle methods called on this class or its descendants that are not defined by this class.
	 * Allow plugins to provide additional theme actions, like a custom act_display_*()
	 *
	 * @param string $function The method that was called.
	 * @param array $params An array of parameters passed to the method
	 **/
	public function __call( $function, $params )
	{
		if ( strpos( $function, 'act_' ) === 0 ) {
			// The first parameter is an array, get it
			if ( count( $params ) > 0 ) {
				list( $user_filters )= $params;
			}
			else {
				$user_filters = array();
			}
			$action = substr( $function, 4 );
			Plugins::act( 'theme_action', $action, $this, $user_filters );
		}
		else {
			$purposed = 'output';
			if ( preg_match( '/^(.*)_(return|end|out)$/', $function, $matches ) ) {
				$purposed = $matches[2];
				$function = $matches[1];
			}
			array_unshift( $params, $function, $this );
			$result = call_user_func_array( array( 'Plugins', 'theme' ), $params );
			switch ( $purposed ) {
				case 'return':
					return $result;
				case 'end':
					return end( $result );
				case 'out':
					$output = implode( '', (array) $result );
					echo $output;
					return $output;
				default:
					$output = implode( '', (array) $result );
					return $output;
			}
		}
	}

	/**
	 * Retrieve the block objects for the current scope and specified area
	 * Incomplete!
	 *
	 * @param string $area The area to which blocks will be output
	 * @param string $scope The scope to which blocks will be output
	 * @param Theme $theme The theme that is outputting these blocks
	 * @return array An array of Block instances to render
	 * @todo Finish this function to pull data from a block_instances table
	 */
	public function get_blocks( $area, $scope, $theme )
	{
		$blocks = DB::get_results( 'SELECT b.* FROM {blocks} b INNER JOIN {blocks_areas} ba ON ba.block_id = b.id WHERE ba.area = ? AND ba.scope_id = ? ORDER BY ba.display_order ASC', array( $area, $scope ), 'Block' );
		$blocks = Plugins::filter( 'get_blocks', $blocks, $area, $scope, $theme );
		return $blocks;
	}

	/**
	 * Matches the scope criteria against the current request
	 *
	 * @param array $criteria An array of scope criteria data in RPN, where values are arrays and operators are strings
	 * @return boolean True if the criteria matches the current request
	 */
	function check_scope_criteria( $criteria )
	{
		$stack = array();
		foreach ( $criteria as $crit ) {
			if ( is_array( $crit ) ) {
				$value = false;
				switch ( $crit[0] ) {
					case 'request':
						$value = URL::get_matched_rule()->name == $crit[1];
						break;
					case 'token':
						if ( isset( $crit[2] ) ) {
							$value = User::identify()->can( $crit[1], $crit[2] );
						}
						else {
							$value = User::identify()->can( $crit[1] );
						}
						break;
					default:

						$value = Plugins::filter( 'scope_criteria_value', $value, $crit[1], $crit[2] );
						break;
				}
				$stack[] = $value;
			}
			else {
				switch ( $crit ) {
					case 'not':
						$stack[] = ! array_pop( $stack );
						break;
					case 'or':
						$value1 = array_pop( $stack );
						$value2 = array_pop( $stack );
						$stack[] = $value1 || $value2;
						break;
					case 'and':
						$value1 = array_pop( $stack );
						$value2 = array_pop( $stack );
						$stack[] = $value1 && $value2;
						break;
					default:
						Plugins::act( 'scope_criteria_operator', $stack, $crit );
						break;
				}
			}
		}
		return array_pop( $stack );
	}

	/**
	 * Retrieve current scope data from the database based on the requested area
	 *
	 * @param string $area The area for which a scope may be applied
	 * @return array An array of scope data
	 */
	public function get_scopes( $area )
	{
		$scopes = DB::get_results( 'SELECT * FROM {scopes} s INNER JOIN {blocks_areas} ba ON ba.scope_id = s.id WHERE ba.area = ? ORDER BY s.priority DESC', array( $area ) );
		foreach ( $scopes as $key => $value ) {
			$scopes[$key]->criteria = unserialize( $value->criteria );
		}
		$scopes = Plugins::filter( 'get_scopes', $scopes );

		usort( $scopes, array( $this, 'sort_scopes' ) );
		return $scopes;
	}

	/**
	 * Sort function for ordering scope object rows by priority
	 * @param StdObject $scope1 A scope to compare
	 * @param StdObject $scope2 A scope to compare
	 * @return integer A sort return value, -1 to 1
	 **/
	public function sort_scopes( $scope1, $scope2 )
	{
		if ( $scope1->priority == $scope2->priority ) {
			return 0;
		}
		return $scope1->priority < $scope2->priority ? 1 : -1;
	}

	/**
	 * Displays blocks associated to the specified area and current scope.
	 *
	 * @param Theme $theme The theme with which this area will be output
	 * @param string $area The area to which blocks will be output
	 * @param string $context The area of context within the theme that could adjust the template used
	 * @param string $scope Used to force a specific scope
	 * @return string the output of all the blocks
	 */
	public function theme_area( $theme, $area, $context = null, $scope = null )
	{

		// This array would normally come from the database via:
		$scopes = $this->get_scopes( $area );

		$active_scope = 0;
		foreach ( $scopes as $scope_id => $scope_object ) {
			if ( ( is_null($scope) && $this->check_scope_criteria( $scope_object->criteria ) ) || $scope == $scope_object->name ) {
				$scope_block_count = DB::get_value( 'SELECT count( *) FROM {blocks_areas} ba WHERE ba.scope_id = ?', array( $scope_object->id ) );
				if ( $scope_block_count > 0 ) {
					$active_scope = $scope_object->id;
				}
				break;
			}
		}

		$area_blocks = $this->get_blocks( $area, $active_scope, $theme );

		$this->area = $area;
		if(isset($context)) {
			$this->context[] = $context;
		}

		// This is the block wrapper fallback template list
		$fallback = array(
			$area . '.blockwrapper',
			'blockwrapper',
			'content',
		);
		if(!is_null($context)) {
			array_unshift($fallback, $context . '.blockwrapper');
			array_unshift($fallback, $context . '.' . $area . '.blockwrapper');
		}

		$output = '';
		$i = 0;
		foreach ( $area_blocks as $block_instance_id => $block ) {
			// Temporarily set some values into the block
			$block->_area = $area;
			$block->_instance_id = $block_instance_id;
			$block->_area_index = $i++;
			$block->_fallback = $fallback;

			$hook = 'block_content_' . $block->type;
			Plugins::act( $hook, $block, $this );
			Plugins::act( 'block_content', $block, $this );
			$block->_content = implode( '', $this->content_return( $block, $context ) );
			if ( trim( $block->_content ) == '' ) {
				unset( $area_blocks[$block_instance_id] );
			}
		}
		// Potentially render each block inside of a wrapper.
		reset( $area_blocks );
		$firstkey = key( $area_blocks );
		end( $area_blocks );
		$lastkey = key( $area_blocks );
		foreach ( $area_blocks as $block_instance_id => $block ) {
			$block->_first = $block_instance_id == $firstkey;
			$block->_last = $block_instance_id == $lastkey;

			// Set up the theme for the wrapper
			$this->block = $block;
			$this->content = $block->_content;
			// This pattern renders the block inside the wrapper template only if a matching template exists
			$newoutput = $this->display_fallback( $fallback, 'fetch' );
			if ( $newoutput === false ) {
				$output .= $block->_content;
			}
			else {
				$output .= $newoutput;
			}

			// Remove temporary values from the block so they're not saved to the database
			unset( $block->_area );
			unset( $block->_instance_id );
			unset( $block->_area_index );
			unset( $block->_first );
			unset( $block->_last );
		}

		// This is the area fallback template list
		$fallback = array(
			$context . '.area.' . $area,
			$context . '.area',
			'area.' . $area,
			'area',
		);
		$this->content = $output;
		$newoutput = $this->display_fallback( $fallback, 'fetch' );
		if ( $newoutput !== false ) {
			$output = $newoutput;
		}

		$this->area = '';
		if(isset($context)) {
			array_pop($this->context);
		}
		return $output;
	}

	/**
 	 * A theme function for outputting CSS classes based on the requested content
 	 * @param Theme $theme A Theme object instance
 	 * @param mixed $args Additional classes that should be added to the ones generated
 	 * @return string The resultant classes
 	 */
	function theme_body_class( $theme, $args = array() )
	{
		$body_class = array();
		foreach ( get_object_vars( $this->request ) as $key => $value ) {
			if ( $value ) {
				$body_class[$key] = $key;
			}
		}

		$body_class = array_unique( array_merge( $body_class, Stack::get_named_stack( 'body_class' ), Utils::single_array( $args ) ) );
		$body_class = Plugins::filter( 'body_class', $body_class, $theme );
		return implode( ' ', $body_class );
	}

	/**
	 * Add javascript to the stack to be output in the theme.
	 *
	 * @param string $where Where should it be output? Options are header and footer.
	 * @param string $value Either a URL or raw JS to be output inline.
	 * @param string $name A name to reference this script by. Used for removing or using in $requires by other scripts.
	 * @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires.
	 * @return boolean True if added successfully, false otherwise.
	 */
	public function add_script ( $where = 'header', $value, $name = null, $requires = null )
	{

		$result = false;

		switch ( $where ) {

			case 'header':
				$result = Stack::add( 'template_header_javascript', $value, $name, $requires );
				break;

			case 'footer':
				$result = Stack::add( 'template_footer_javascript', $value, $name, $requires );
				break;

		}

		return $result;

	}

	/**
	 * Add a stylesheet to the stack to be output in the theme.
	 *
	 * @param string $where Where should it be output? Options are header and footer.
	 * @param string $value Either a URL or raw CSS to be output inline.
	 * @param string $name A name to reference this script by. Used for removing or using in $after by other scripts.
	 * @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires.
	 * @return boolean True if added successfully, false otherwise.
	 */
	public function add_style ( $where = 'header', $value, $name = null, $requires = null )
	{

		$result = false;

		switch ( $where ) {

			case 'header':
				$result = Stack::add( 'template_stylesheet', $value, $name, $requires );
				break;

			case 'footer':
				$result = Stack::add( 'template_footer_stylesheet', $value, $name, $requires );
				break;

		}

		return $result;

	}

	/**
	* Provide a method to return the version number from the theme xml
	* @return string The theme version from XML
	**/
	public function get_version()
	{
		return (string)$this->info()->version;
	}

	/**
	 * Load and return a list of all assets in the current theme chain's /assets/ directory
	 * @param bool $refresh If True, clear and reload all assets
	 * @return array An array of URLs of assets in the assets directories of the active theme chain
	 */
	public function load_assets($refresh = false)
	{
		static $assets = null;

		if(is_null($assets) || $refresh) {
			$themedirs = $this->theme_dir;
			$assets = array(
				'css' => array(),
				'js' => array(),
			);

			foreach($themedirs as $dir) {
				if( file_exists(Utils::end_in_slash($dir) . 'assets')) {
					$theme_assets = Utils::glob(Utils::end_in_slash($dir) . 'assets/*.*');
					foreach($theme_assets as $asset) {
						$extension = strtolower(substr($asset, strrpos($asset, '.') + 1));
						$assets[$extension][basename($asset)] = $this->dir_to_url($asset);
					}
				}
			}
		}
		return $assets;
	}

	/**
	 * Load assets and add the CSS ones to the header on the template_stylesheet action hook.
	 */
	public function action_template_header_9()
	{
		$assets = $this->load_assets();
		foreach($assets['css'] as $css) {
			Stack::add('template_stylesheet', array($css , 'screen,projection'));
		}
	}

	/**
	 * Load assets and add the javascript ones to the footer on the template_footer_javascript action hook.
	 */
	public function action_template_footer_9()
	{
		$assets = $this->load_assets();
		foreach($assets['js'] as $js) {
			Stack::add('template_footer_javascript', $js);
		}
	}

	/**
	 * Get the URL for a resource in one of the directories used by the active theme, child theme directory first
	 * @param bool|string $resource The resource name
	 * @param bool $overrideok If false, find only the parent theme resources
	 * @return string The URL of the requested resource
	 * @todo This method needs to be aware of the class that called it so that it can find the right directory to use
	 */
	public function get_url($resource = false, $overrideok = true)
	{
		$url = false;
		$theme = '';

		$themedirs = $this->theme_dir;

		if(!$overrideok) {
			$themedirs = reset($this->theme_dir);
		}

		foreach($themedirs as $dir) {
			if(file_exists(Utils::end_in_slash($dir) . trim($resource, '/'))) {
				$url = $this->dir_to_url(Utils::end_in_slash($dir) . trim($resource, '/'));
				break;
			}
		}

		$url = Plugins::filter( 'site_url_theme', $url, $theme );
		return $url;
	}

	/**
	 * Convert a theme directory or resource into a URL
	 * @param string $dir The pathname to convert
	 * @return bool|string The URL to use, or false if none was found
	 */
	public function dir_to_url($dir)
	{
		static $tomatch = false;

		if(!$tomatch) {
			$tomatch = array(
				Site::get_dir( 'config' ) . '/themes/' => Site::get_url( 'user' ) .  '/themes/',
				HABARI_PATH . '/user/themes/' => Site::get_url( 'habari' ) . '/user/themes/',
				HABARI_PATH . '/3rdparty/themes/' => Site::get_url( 'habari' ) . '/3rdparty/themes/',
				HABARI_PATH . '/system/themes/' => Site::get_url( 'habari' ) . '/system/themes/',
			);
		}

		if(preg_match('#^(' . implode('|', array_map('preg_quote', array_keys($tomatch))) . ')(.*)$#', $dir, $matches)) {
			return $tomatch[$matches[1]] . $matches[2];
		}
		return false;
	}

	/**
	 * Add a template to the list of available templates
	 * @param string $name Name of the new template
	 * @param string $file File of the template to add
	 * @param boolean $replace If true, replace any existing template with this name
	 */
	public function add_template($name, $file, $replace = false)
	{
		$this->template_engine->add_template($name, $file, $replace);
	}

}
?>

repo_name: DaSourcerer/habari


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

/**
 * Habari Themes class
 *
 */
class Themes
{
	private static $all_themes = null;
	private static $all_data = null;

	/**
	 * Returns the theme dir and path information
	 * @return array An array of Theme data
	 **/
	public static function get_all()
	{
		if ( !isset( self::$all_themes ) ) {
			$dirs = array( HABARI_PATH . '/system/themes/*' , HABARI_PATH . '/3rdparty/themes/*', HABARI_PATH . '/user/themes/*' );
			if ( Site::is( 'multi' ) ) {
				$dirs[] = Site::get_dir( 'config' ) . '/themes/*';
			}
			$themes = array();
			foreach ( $dirs as $dir ) {
				$themes = array_merge( $themes, Utils::glob( $dir, GLOB_ONLYDIR | GLOB_MARK ) );
			}

			$themes = array_filter( $themes, function($a) {return file_exists( $a . "/theme.xml" );} );
			$themefiles = array_map( 'basename', $themes );
			self::$all_themes = array_combine( $themefiles, $themes );
		}
		return self::$all_themes;
	}

	/**
	 * Returns all theme information -- dir, path, theme.xml, screenshot url
	 * @return array An array of Theme data
	 **/
	public static function get_all_data()
	{
		if ( !isset( self::$all_data ) ) {
			foreach ( self::get_all() as $theme_dir => $theme_path ) {
				$themedata = array();
				$themedata['dir'] = $theme_dir;
				$themedata['path'] = $theme_path;
				$themedata['theme_dir'] = $theme_path;

				$themedata['info'] = simplexml_load_file( $theme_path . '/theme.xml' );
				if ( $themedata['info']->getName() != 'pluggable' || (string) $themedata['info']->attributes()->type != 'theme' ) {
					$themedata['screenshot'] = Site::get_url( 'admin_theme' ) . "/images/screenshot_default.png";
					$themedata['info']->description = '' . _t( 'This theme is a legacy theme that is not compatible with Habari ' ) . Version::get_habariversion() . '.

Please update your theme.';
					$themedata['info']->license = '';
				}
				else {
					foreach ( $themedata['info'] as $name=>$value ) {
						$themedata[$name] = (string) $value;
					}

					if ( $screenshot = Utils::glob( $theme_path . '/screenshot.{png,jpg,gif}', GLOB_BRACE ) ) {
						$themedata['screenshot'] = Site::get_url( 'habari' ) . dirname( str_replace( HABARI_PATH, '', $theme_path ) ) . '/' . basename( $theme_path ) . "/" . basename( reset( $screenshot ) );
					}
					else {
						$themedata['screenshot'] = Site::get_url( 'admin_theme' ) . "/images/screenshot_default.png";
					}
				}

				self::$all_data[$theme_dir] = $themedata;
			}
		}
		return self::$all_data;
	}

	/**
	 * Returns the name of the active or previewed theme
	 *
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return string the current theme or previewed theme's directory name
	 */
	public static function get_theme_dir( $nopreview = false )
	{
		if ( !$nopreview && isset( $_SESSION['user_theme_dir'] ) ) {
			$theme_dir = $_SESSION['user_theme_dir'];
		}
		else {
			$theme_dir = Options::get( 'theme_dir' );
		}
		$theme_dir = Plugins::filter( 'get_theme_dir', $theme_dir );
		return $theme_dir;
	}

	/**
	 * Returns the active theme's full directory path.
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return string The full path to the active theme directory
	 */
	private static function get_active_theme_dir( $nopreview = false )
	{
		$theme_dir = self::get_theme_dir( $nopreview );
		$themes = Themes::get_all();

		// If our active theme directory has gone missing, iterate through the others until we find one we can use and activate it.
		if ( !isset( $themes[$theme_dir] ) ) {
			$theme_exists = false;
			foreach ( $themes as $themedir ) {
				if ( file_exists( Utils::end_in_slash( $themedir ) . 'theme.xml' ) ) {
					$theme_dir = basename( $themedir );
					EventLog::log( _t( "Active theme directory no longer available.  Falling back to '{$theme_dir}'" ), 'err', 'theme', 'habari' );
					Options::set( 'theme_dir', basename( $themedir ) );
					$fallback_theme = Themes::create();
					Plugins::act_id( 'theme_activated', $fallback_theme->plugin_id(), $theme_dir, $fallback_theme );
					$theme_exists = true;
					// Refresh to the newly "activated" theme to ensure it takes the options that have just been set above and doesn't produce any errors.
					Utils::redirect();
					break;
				}
			}
			if ( !$theme_exists ) {
				die( _t( 'There is no valid theme currently installed.' ) );
			}
		}
		return $themes[$theme_dir];
	}

	/**
	 * Returns the active theme information from the database
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return QueryRecord An array of Theme data
	 **/
	public static function get_active( $nopreview = false )
	{
		$theme = array();
		$theme['theme_dir'] = Themes::get_active_theme_dir( $nopreview );

		$data = simplexml_load_file( Utils::end_in_slash( $theme['theme_dir'] ) . 'theme.xml' );
		foreach ( $data as $name=>$value ) {
			$theme[$name] = (string) $value;
		}
		$theme['xml'] = $data;
		return $theme;
	}

	private static function get_by_name($name) {
		$themes = self::get_all_data();
		foreach($themes as $theme) {
			if($name == $theme['info']->name) {
				return $theme;
			}
		}
		return false;
	}

	/**
	 * Returns theme information for the active theme -- dir, path, theme.xml, screenshot url
	 * @params boolean $nopreview If true, return the real active theme, not the preview
	 * @return array An array of Theme data
	 */
	public static function get_active_data( $nopreview = false )
	{
		$all_data = Themes::get_all_data();
		$active_theme_dir = basename( Themes::get_active_theme_dir( $nopreview ) );
		$active_data = $all_data[$active_theme_dir];
		return $active_data;
	}

	/**
	 * Ensure that a theme meets requirements for activation/preview
	 * @static
	 * @param string $theme_dir the directory of the theme
	 * @return bool True if the theme meets all requirements
	 */
	public static function validate_theme( $theme_dir )
	{
		$all_themes = Themes::get_all_data();
		// @todo Make this a closure in php 5.3
		$theme_names = Utils::array_map_field($all_themes, 'name');

		$theme_data = $all_themes[$theme_dir];

		$ok = true;

		if(isset($theme_data['info']->parent) && !in_array((string)$theme_data['info']->parent, $theme_names)) {
			Session::error(_t('This theme requires the parent theme named "%s" to be present prior to activation.', array($theme_data['info']->parent)));
			$ok = false;
		}

		if(isset($theme_data['info']->requires)) {
			$provided = Plugins::provided();
			foreach($theme_data['info']->requires->feature as $requirement) {
				if(!isset($provided[(string)$requirement])) {
					Session::error(_t('This theme requires the feature "%1$s" to be present prior to activation.', array((string)$requirement, $requirement['url'])));
					$ok = false;
				}
			}
		}

		return $ok;
	}
	/**
	 * function activate_theme
	 * Updates the database with the name of the new theme to use
	 * @param string the name of the theme
	**/
	public static function activate_theme( $theme_name, $theme_dir )
	{
		$ok = Themes::validate_theme($theme_dir);

		$ok = Plugins::filter( 'activate_theme', $ok, $theme_name ); // Allow plugins to reject activation
		if($ok) {
			$old_active_theme = Themes::create();
			Plugins::act_id( 'theme_deactivated', $old_active_theme->plugin_id(), $old_active_theme->name, $old_active_theme ); // For the theme itself to react to its deactivation
			Plugins::act( 'theme_deactivated_any', $old_active_theme->name, $old_active_theme ); // For any plugin to react to its deactivation
			Options::set( 'theme_name', $theme_name );
			Options::set( 'theme_dir', $theme_dir );
			$new_active_theme = Themes::create();

			// Set version of theme if it wasn't installed before
			$versions = Options::get( 'pluggable_versions' );
			if(!isset($versions[get_class($new_active_theme)])) {
				$versions[get_class($new_active_theme)] = $new_active_theme->get_version();
				Options::set( 'pluggable_versions', $versions );
			}

			// Run activation hooks for theme activation
			Plugins::act_id( 'theme_activated', $new_active_theme->plugin_id(), $theme_name, $new_active_theme ); // For the theme itself to react to its activation
			Plugins::act( 'theme_activated_any', $theme_name, $new_active_theme ); // For any plugin to react to its activation
			EventLog::log( _t( 'Activated Theme: %s', array( $theme_name ) ), 'notice', 'theme', 'habari' );
		}
		return $ok;
	}

	/**
	 * Sets a theme to be the current user's preview theme
	 *
	 * @param string $theme_name The name of the theme to preview
	 * @param string $theme_dir The directory of the theme to preview
	 */
	public static function preview_theme( $theme_name, $theme_dir )
	{
		$ok = Themes::validate_theme($theme_dir);

		if($ok) {
			$_SESSION['user_theme_name'] = $theme_name;
			$_SESSION['user_theme_dir'] = $theme_dir;
			// Execute the theme's activated action
			$preview_theme = Themes::create();
			Plugins::act_id( 'theme_activated', $preview_theme->plugin_id(), $theme_name, $preview_theme );
			EventLog::log( _t( 'Previewed Theme: %s', array( $theme_name ) ), 'notice', 'theme', 'habari' );
		}

		return $ok;
	}

	/**
	 * Cancel the viewing of any preview theme
	 */
	public static function cancel_preview()
	{
		if ( isset( $_SESSION['user_theme_name'] ) ) {
			// Execute the theme's deactivated action
			$preview_theme = Themes::create();
			Plugins::act_id( 'theme_deactivated', $preview_theme->plugin_id() );
			EventLog::log( _t( 'Canceled Theme Preview: %s', array( $_SESSION['user_theme_name'] ) ), 'notice', 'theme', 'habari' );
			unset( $_SESSION['user_theme_name'] );
			unset( $_SESSION['user_theme_dir'] );
		}
	}

	/**
	 * Returns a named Theme descendant.
	 * If no parameter is supplied, then
	 * load the active theme from the database.
	 *
	 * If no theme option is set, a fatal error is thrown
	 *
	 * @param name            ( optional ) override the default theme lookup
	 * @param template_engine ( optional ) specify a template engine
	 * @param theme_dir       ( optional ) specify a theme directory
	 **/
	public static function create( $name = null, $template_engine = null, $theme_dir = null )
	{
		static $bound = array();

		$hash = md5(serialize(array($name, $template_engine, $theme_dir)));
		if(isset($bound[$hash])) {
			return $bound[$hash];
		}

 		// If the name is not supplied, load the active theme
		if(empty($name)) {
			$themedata = self::get_active();
			if ( empty( $themedata ) ) {
				die( _t( 'Theme not installed.' ) );
			}
		}
		// Otherwise, try to load the named theme from user themes that are present
		else {
			$themedata = self::get_by_name( $name );
		}
		// If a theme wasn't found by name, create a blank object
		if(!$themedata) {
			$themedata = array();
			$themedata['name'] = $name;
			$themedata['version'] = 0;
		}
		// If a specific template engine was supplied, use it
		if(!empty($template_engine)) {
			$themedata['template_engine'] = $template_engine;
		}
		// If a theme directory was supplied, use the directory that was supplied
		if(!empty($theme_dir)) {
			$themedata['theme_dir'] = $theme_dir;
		}

		// Set the default theme file
		$themefile = 'theme.php';
		if(isset($themedata['info']->class['file']) && (string)$themedata['info']->class['file'] != '') {
			$themefile = (string)$themedata->xml->class['file'];
		}

		// Convert themedata to QueryRecord for legacy purposes
		// @todo: Potentially break themes by sending an array to the constructor instead of this QueryRecord
		$themedata = new QueryRecord($themedata);

		// Deal with parent themes
		if(isset($themedata->parent)) {
			// @todo If the parent theme doesn't exist, provide a useful error
			$parent_data = self::get_by_name( $themedata->parent );
			$parent_themefile = 'theme.php';
			if(isset($parent_data['info']->class['file']) && (string)$parent_data['info']->class['file'] != '') {
				$parent_themefile = (string)$parent_data['info']->class['file'];
			}
			include_once($parent_data['theme_dir'] . $parent_themefile);

			$themedata->parent_theme_dir = Utils::single_array($parent_data['theme_dir']);
			$themedata->theme_dir = array_merge(Utils::single_array($themedata->theme_dir), $themedata->parent_theme_dir);
		}
		$primary_theme_dir = $themedata->theme_dir;
		$primary_theme_dir = is_array($primary_theme_dir) ? reset($primary_theme_dir) : $primary_theme_dir;

		// Include the theme class file
		if ( file_exists( $primary_theme_dir . $themefile ) ) {
			include_once( $primary_theme_dir . $themefile );
		}

		if ( isset( $themedata->class ) ) {
			$classname = $themedata->class;
		}
		else {
			$classname = self::class_from_filename( $primary_theme_dir . $themefile );
		}

		// the final fallback, for the admin "theme"
		if ( $classname == '' ) {
			$classname = 'Theme';
		}

		$created_theme = new $classname( $themedata );
		$created_theme->upgrade();
		Plugins::act_id( 'init_theme', $created_theme->plugin_id(), $created_theme );
		Plugins::act( 'init_theme_any', $created_theme );
		$bound[$hash] = $created_theme;
		return $created_theme;

	}

	public static function class_from_filename( $file, $check_realpath = false )
	{
		if ( $check_realpath ) {
			$file = realpath( $file );
		}

		$theme_classes = self::get_theme_classes();
		foreach ( $theme_classes as $theme ) {
			$class = new ReflectionClass( $theme );
			$classfile = str_replace( '\\', '/', $class->getFileName() );
			if ( $classfile == $file ) {
				return $theme;
			}
		}
		// if we haven't found the plugin class, try again with realpath resolution:
		if ( $check_realpath ) {
			// really can't find it
			return false;
		}
		else {
			return self::class_from_filename( $file, true );
		}
	}

	/**
	 * Get a list of classes that extend Theme
	 * @static
	 * @return array List of string names of classes that extend Theme
	 */
	public static function get_theme_classes()
	{
		$classes = get_declared_classes();
		foreach($classes as $class) {
			$parents = class_parents( $class, false );
			if(count($parents) > 0) {
				$class_parents[$class] = $parents;
			}
		}

		$theme_classes = array();
		do {
			$delta = count($theme_classes);
			foreach($class_parents as $class => $parents) {
				if(count(array_intersect($theme_classes + array('Theme'), $parents))>0) {
					$theme_classes[$class] = $class;
				}
			}
		} while($delta != count($theme_classes));
		return $theme_classes;
	}
}

?>


code:
<?php
/**
 * Habari Update class
 *
 * Checks for updates to Habari and its libraries
 *
 * @access public
 */
class Update extends Singleton
{
	const UPDATE_URL = 'https://beacon.habariproject.org/';

	private $beacons = array();
	private $update; // SimpleXMLElement

	/**
	 * Enables singleton working properly
	 *
	 * @see singleton.php
	 */
	protected static function instance()
	{
		return self::getInstanceOf( get_class() );
	}

	/**
	 * Add a beaconid to the list of beaconids to version-check.
	 *
	 * @param string $name the name of the component that will be checked
	 * @param string $beaconid the id of the beacon to check
	 * @param string $current_version the current version of the resource represented by this beaconid
	 */
	public static function add( $name, $beaconid, $current_version )
	{
		try {
			if ( empty( $name ) || empty( $beaconid ) || empty( $current_version ) ) {
				if ( empty( $name ) ) {
					$name = "Unnamed plugin";
				}
				// @locale Signifies the plugin or theme is missing needed information to check for an update
				throw new Exception( _t( 'Invalid Beacon updater information added for %s. The plugin or theme cannot be identified. Check your plugin\'s or theme\'s XML.', array($name) ) );
			}

			self::instance()->beacons[ (string) $beaconid] = array( 'name' => (string) $name, 'version' => (string) $current_version );
		}
		catch ( Exception $e ) {
			// catch any exceptions generated by missing beacon information
			EventLog::log( _t( 'Beacon updater failed! %s', array($e->getMessage())), 'err', 'update', 'habari' );

			// tell cron the check failed
			return false;
		}
	}

	/**
	 * Return true if the beacon data contains updates from the server
	 *
	 * @param array $beacon the beacon data from the $beacons array
	 * @return boolean true if there are updates available for this beacon
	 */
	private static function filter_unchanged( $beacon )
	{
		return isset( $beacon['latest_version'] );
	}

	/**
	 * Perform a check of all beaconids.
	 * Notifies update_check plugin hooks when checking so that they can add their beaconids to the list.
	 * @return array An array of update beacon information for components that have updates
	 * @throws Exception
	 */
	public static function check()
	{

		try {

			// get a local version of the instance to save typing
			$instance = self::instance();

			// load beacons
			self::register_beacons();

			// setup the remote request
			$request = new RemoteRequest( self::UPDATE_URL, 'POST' );

			// add all the beacon versions as parameters
			$request->set_params(
				Utils::array_map_field($instance->beacons, 'version')
			);
			// we're not desperate enough to wait too long
			$request->set_timeout( 5 );

			// execute the request
			$result = $request->execute();

			// grab the body of the response, which has our xml in it
			$update_data = $request->get_response_body();

			// i don't know why we hold the XML in a class variable, but we'll keep doing that in this rewrite
			$instance->update = new SimpleXMLElement( $update_data );

			foreach ( $instance->update as $beacon ) {

				$beacon_id = (string)$beacon['id'];
				$beacon_url = (string)$beacon['url'];
				$beacon_type = isset( $beacon['type'] ) ? (string)$beacon['type'] : 'addon';

				// do we have this beacon? if not, don't process it
				// even though we POST all our beacons to the update script right now, it still hands back the whole list
				if ( empty( $instance->beacons[ $beacon_id ] ) ) {
					continue;
				}

				// add the beacon's basic info
				$instance->beacons[ $beacon_id ]['id'] = $beacon_id;
				$instance->beacons[ $beacon_id ]['url'] = $beacon_url;
				$instance->beacons[ $beacon_id ]['type'] = $beacon_type;

				foreach ( $beacon->update as $update ) {

					// pick out and cast all the values from the XML
					$u = array(
						'severity' => (string)$update['severity'],
						'version' => (string)$update['version'],
						'date' => isset( $update['date'] ) ? (string)$update['date'] : '',
						'url' => isset( $update['url'] ) ? (string)$update['url'] : '',
						'text' => (string)$update,
					);

					// if the remote update info version is newer... we want all newer versions
					if ( version_compare( $u['version'], $instance->beacons[ $beacon_id ]['version'] ) > 0 ) {

						// if this version is more recent than all the other versions
						if ( !isset( $instance->beacons[ $beacon_id ]['latest_version'] ) || version_compare( $u['version'], $instance->beacons[ $beacon_id ]['latest_version'] ) > 0 ) {

							// set this as the latest version
							$instance->beacons[ $beacon_id ]['latest_version'] = $u['version'];

						}

						// add the version to the list
						$instance->beacons[ $beacon_id ]['updates'][ $u['version'] ] = $u;

					}

				}

			}

			// return an array of beacons that have updates
			return array_filter( $instance->beacons, array( 'Update', 'filter_unchanged' ) );

		}
		catch ( Exception $e ) {
			// catches any RemoteRequest errors or XML parsing problems, etc.
			// bubble up
			throw $e;
		}

	}

	/**
	 * Loop through all the active plugins and add their information to the list of plugins to check for updates.
	 */
	private static function add_plugins()
	{

		$plugins = Plugins::get_active();

		foreach ( $plugins as $plugin ) {

			// name and version are required in the XML file, make sure GUID is set
			if ( !isset( $plugin->info->guid ) ) {
				continue;
			}

			Update::add( $plugin->info->name, $plugin->info->guid, $plugin->info->version );

		}

	}

	/**
	 * Endpoint for the update-check cronjob.
	 * Loads beacons, checks for updates from hp.o, and saves any updates to the DB.
	 *
	 * @param null $cronjob Unused. The CronJob object being executed when being run as cron.
	 * @return boolean True on successful check, false on any failure (so cron runs again).
	 */
	public static function cron( $cronjob = null )
	{

		// register the beacons
		self::register_beacons();

		// save the list of beacons we are using to check with
		Options::set( 'updates_beacons', self::instance()->beacons );

		try {
			// run the check
			$updates = Update::check();

			// save the list of updates
			Options::set( 'updates_available', $updates );

			EventLog::log( _t( 'Updates check CronJob completed successfully.' ), 'info', 'update', 'habari' );

			// return true, we succeeded
			return true;
		}
		catch ( Exception $e ) {
			// catch any exceptions generated by RemoteRequest or XML parsing

			EventLog::log( _t( 'Updates check CronJob failed!' ), 'err', 'update', 'habari', $e->getMessage() );

			// tell cron the check failed
			return false;
		}

	}

	/**
	 * Register beacons to check for updates.
	 * Includes Habari core, all active plugins, and any pluggable that implements the update_check hook.
	 */
	private static function register_beacons()
	{

		// if there are already beacons, don't run again
		if ( count( self::instance()->beacons ) > 0 ) {
			return;
		}

		Update::add( 'Habari', '7a0313be-d8e3-11db-8314-0800200c9a66', Version::get_habariversion() );

		// add the active theme
		self::add_theme();

		// add all active plugins
		self::add_plugins();

		Plugins::act( 'update_check' );

	}

	/**
	 * Add the currently active theme's information to the list of beacons to check for updates.
	 */
	private static function add_theme()
	{

		// get the active theme
		$theme = Themes::get_active_data( true );

		// name and version are required in the XML file, make sure GUID is set
		if ( isset( $theme['info']->guid ) ) {
			Update::add( $theme['info']->name, $theme['info']->guid, $theme['info']->version );
		}

	}

	/**
	 * Compare the current set of plugins with those we last checked for updates.
	 * This is run by AdminHandler on every page load to make sure we always have fresh data on the dashboard.
	 */
	public static function check_plugins()
	{

		// register the beacons
		self::register_beacons();

		// get the list we checked last time
		$checked_list = Options::get( 'updates_beacons' );

		// if the lists are different
		if ( $checked_list != self::instance()->beacons ) {

			// remove any stored updates, just to avoid showing stale data
			Options::delete( 'updates_available' );

			// schedule an update check the next time cron runs
			CronTab::add_single_cron( 'update_check_single', array( 'Update', 'cron' ), HabariDateTime::date_create()->int, _t( 'Perform a single check for plugin updates, the plugin set has changed.' ) );

		}

	}

	/**
	 * Return all available updates, or the updates available for a single GUID.
	 *
	 * @param string $guid A GUID to return available updates for.
	 * @return array Array of all available updates if no GUID is specified.
	 * @return array A single GUID's updates, if GUID is specified and they are available.
	 * @return false If a single GUID is specified and there are no updates available for it.
	 */
	public static function updates_available( $guid = null )
	{

		$updates = Options::get( 'updates_available', array() );

		if ( $guid == null ) {
			return $updates;
		}
		else {

			if ( isset( $updates[ $guid ] ) ) {
				return $updates[ $guid ];
			}
			else {
				return false;
			}

		}

	}

}

?>


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

/**
 * URL class which handles creation of URLs based on the rewrite
 * rules in the database.  Uses rules to construct pretty URLs for use
 * by the system and especially the theme's template engine
 *
 */
class URL extends Singleton
{
	// static collection of rules ( pulled from RewriteController )
	private $rules = null;
	private $matched_rule = null;
	private static $stub = null;

	/**
	 * Enables singleton working properly
	 *
	 * @see singleton.php
	 */
	protected static function instance()
	{
		return self::getInstanceOf( __CLASS__ );
	}

	/**
	 * A simple caching mechanism to avoid reloading rule array
	 */
	private function load_rules()
	{
		if ( URL::instance()->rules != null ) {
			return;
		}
		URL::instance()->rules = RewriteRules::get_active();
	}

	/**
	 * Get the matched RewriteRule that was matched in parse().
	 *
	 * @return RewriteRule matched rule, or null
	 */
	public static function get_matched_rule()
	{
		return URL::instance()->matched_rule;
	}

	/**
	 * Get the active RewriteRules that are cached in self::load_rules().
	 *
	 * @return array RewriteRules active rules, or null
	 */
	public static function get_active_rules()
	{
		return URL::instance()->rules;
	}

	/**
	 * Cause the matched rule to be unset in the case of a 404
	 *
	 * @return RewriteRule A rewrite rule that represents a 404 error - no match on the URL requested
	 */
	public static function set_404()
	{
		if ( empty( URL::instance()->matched_rule ) || ( URL::instance()->matched_rule->name != 'display_404' ) ) {
			$rule = RewriteRules::by_name( 'display_404' );
			URL::instance()->matched_rule = reset( $rule );
			URL::instance()->matched_rule->match( self::$stub );
		}
		return URL::instance()->matched_rule;
	}

	/**
	 * Match a URL/URI against the rewrite rules stored in the DB.
	 * This method is used by the Controller class for parsing
	 * requests, and by other classes, such as Pingback, which
	 * uses it to determine the post slug for a given URL.
	 *
	 * Returns the matched RewriteRule object, or false.
	 *
	 * @param string $from_url URL string to parse
	 * @return RewriteRule matched rule, or false
	 */
	public static function parse( $from_url )
	{
		$base_url = Site::get_path( 'base', true );

		/*
		 * Strip out the base URL from the requested URL
		 * but only if the base URL isn't /
		 */
		if ( strpos( $from_url, $base_url ) === 0 ) {
			$from_url = MultiByte::substr( $from_url, MultiByte::strlen( $base_url ) );
		}

		/* Trim off any leading or trailing slashes */
		$from_url = trim( $from_url, '/' );

		/* Remove the querystring from the URL */
		if ( MultiByte::strpos( $from_url, '?' ) !== false ) {
			list( $from_url, )= explode( '?', $from_url );
		}

		$url = URL::instance();
		$url->load_rules(); // Cached in singleton

		/*
		 * Run the stub through the regex matcher
		 */
		$pattern_matches = array();
		self::$stub = $from_url;
		foreach ( $url->rules as $rule ) {
			if ( $rule->match( $from_url ) ) {
				$url->matched_rule = $rule;
				/* Stop processing at first matched rule... */
				return $rule;
			}
		}

		return false;
	}

	/**
	 * Builds the required pretty URL given a supplied
	 * rule name and a set of placeholder replacement
	 * values and returns the built URL.
	 *
	 * <code>
	 * URL::get( 'display_entries_by_date', array(
	 * 	'year' => '2000',
	 * 	'month' => '05',
	 * 	'day' => '01',
	 * ) );
	 * </code>
	 *
	 * @param mixed $rule_names string name of the rule or array of rules which would build the URL
	 * @param mixed $args (optional) array or object of placeholder replacement values
	 * @param boolean $useall If true (default), then all passed parameters that are not part of the built URL are tacked onto the URL as querystring
	 * @param boolean $prepend_site If true (default), a full URL is returned, if false, only the path part of the URL is returned
	 */
	public static function get( $rule_names = '', $args = array(), $useall = true, $noamp = false, $prepend_site = true )
	{
		$args = self::extract_args( $args );

		$url = URL::instance();
		if ( $rule_names == '' ) {
			// Retrieve current matched RewriteRule
			$selectedrule = $url->get_matched_rule();

			// Retrieve arguments name the RewriteRule can use to build a URL.
			$rr_named_args = $selectedrule->named_args;

			$rr_args = array_merge( $rr_named_args['required'], $rr_named_args['optional']  );
			// For each argument, check if the handler_vars array has that argument and if it does, use it.
			$rr_args_values = array();

			foreach ( $rr_args as $rr_arg ) {
				if ( !isset( $args[$rr_arg] ) ) {
					$rr_arg_value = Controller::get_var( $rr_arg );
					if ( $rr_arg_value != '' ) {
						$args[$rr_arg] = $rr_arg_value;
					}
				}
			}

		}
		else {
			$url->load_rules();
			$selectedrule = null;

			if ( !is_array( $rule_names ) ) {
				$rule_names = array( $rule_names );
			}
			foreach ( $rule_names as $rule_name ) {
				if ( $rules = $url->rules->by_name( $rule_name ) ) {
					$rating = null;
					foreach ( $rules as $rule ) {
						$newrating = $rule->arg_match( $args );
						// Is the rating perfect?
						if ( $rating == 0 ) {
							$selectedrule = $rule;
							break;
						}
						if ( empty( $rating ) || ( $newrating < $rating ) ) {
							$rating = $newrating;
							$selectedrule = $rule;
						}
					}
					if ( isset( $selectedrule ) ) {
						break;
					}
				}
			}
		}

		if ( $selectedrule instanceOf RewriteRule ) {
			$return_url = $selectedrule->build( $args, $useall, $noamp );
			if ( $prepend_site ) {
				return Site::get_url( 'habari', true ) . $return_url;
			}
			else {
				return $return_url;
			}
		}
		else {
			$error = new Exception();
			$error_trace = $error->getTrace();
			// Since URL::out() calls this function, the index 0 is URL::get() which is not the proper failing call.
			if ( isset( $error_trace[1]['class'] ) && isset( $error_trace[1]['function'] ) && ( $error_trace[1]['class'] == 'URL' ) && ( $error_trace[1]['function'] == 'out' ) ) {
				$error_args = $error_trace[1];
			}
			// When calling URL::get() directly, the index 0 is the proper file and line of the failing call.
			else {
				$error_args = $error_trace[0];
			}
			EventLog::log( _t( 'Could not find a rule matching the following names: %s. File: %s (line %s)', array( implode( ', ', $rule_names ), $error_args['file'], $error_args['line'] ) ), 'notice', 'rewriterules', 'habari' );
		}
	}

	/**
	 * Helper wrapper function.  Outputs the URL via echo.
	 * @param string $rule_name name of the rule which would build the URL
	 * @param array $args (optional) array of placeholder replacement values
	 * @param boolean $useall If true (default), then all passed parameters that are not part of the built URL are tacked onto the URL as querystring
	 * @param boolean $prepend_site If true (default), a full URL is returned, if false, only the path part of the URL is returned
	 */
	public static function out( $rule_name = null, $args = array(), $useall = true, $noamp = true, $prepend_site = true )
	{
		echo URL::get( $rule_name, $args, $useall, $noamp, $prepend_site );
	}

	/**
	 * Get a fully-qualified URL from a filesystem path
	 *
	 * @param string $path The filesystem path
	 * @param bool whether to include a trailing slash.  Default: No
	 * @param bool whether to leave a filename on the URL.  Default: No
	 * @return string URL
	 */
	public static function get_from_filesystem( $path, $trail = false, $preserve_file = false )
	{
		if ( !$preserve_file ) {
			$path = dirname( $path );
		}
		$url = Site::get_url( 'habari' ) . MultiByte::substr( $path, MultiByte::strlen( HABARI_PATH ) );
		// Replace windows paths with forward slashes
		$url = str_replace( '\\', '/', $url );
		$url .= Utils::trail($trail);
		return $url;
	}

	/**
	 * Extract the possible arguments to use in the URL from the passed variable
	 * @param mixed $args An array of values or a URLProperties object with properties to use in the construction of a URL
	 * @return array Properties to use to construct  a URL
	 */
	public static function extract_args( $args, $prefix = '' )
	{
		if ( is_object( $args ) ) {
			if ( $args instanceof URLProperties ) {
				$args = $args->get_url_args();
			}
			else {
				$args_ob = array();
				foreach ( $args as $key => $value ) {
					$args_ob[$key] = $value;
				}
				$args = $args_ob;
			}
		}
		else {
			$args = Utils::get_params( $args );
		}
		// can this be done with array_walk?
		if ( $prefix && $args ) {
			$args_out = array();
			foreach ( $args as $key => $value ) {
				$args_out[$prefix.$key] = $value;
			}
			$args = $args_out;
		}
		return $args;
	}

}

?>


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

/**
 * Habari URLProperties interface
 *
 */
interface URLProperties
{
	// must return an associative array of
	// named args/values for URL building.
	public function get_url_args();
}

?>