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

/**
 * Habari Posts Class
 *
 * class Posts
 * This class provides two key features.
 * 1: Posts contains static method get() that returns the
 * requested posts based on the passed criteria.  Depending on the type of
 * request, different types are returned. See the function for details
 * 2: An instance of Posts functions as an array (by extending ArrayObject) and
 * is returned by Posts::get() as the results of a query.  This allows the
 * result of Posts::get() to be iterated (for example, in a foreach construct)
 * and to have properties that can be accessed that describe the results
 * (for example, $posts->onepost).
 */
class Posts extends ArrayObject implements IsContent
{
	public $get_param_cache; // Stores info about the last set of data fetched that was not a single value

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

		return false;
	}

	/**
	 * Returns a post or posts based on supplied parameters.
	 * @todo <b>THIS CLASS SHOULD CACHE QUERY RESULTS!</b>
	 *
	 * @param array $paramarray An associative array of parameters, or a querystring.
	 * The following keys are supported:
	 * - id => a post id or array of post ids
	 * - not:id => a post id or array of post ids to exclude
	 * - slug => a post slug or array of post slugs
	 * - not:slug => a post slug or array of post slugs to exclude
	 * - user_id => an author id or array of author ids
	 * - content_type => a post content type or array post content types
	 * - not:content_type => a post content type or array post content types to exclude
	 * - status => a post status, an array of post statuses, or 'any' for all statuses
	 * - year => a year of post publication
	 * - month => a month of post publication, ignored if year is not specified
	 * - day => a day of post publication, ignored if month and year are not specified
	 * - before => a timestamp to compare post publication dates
	 * - after => a timestamp to compare post publication dates
	 * - month_cts => return the number of posts published in each month
	 * - criteria => a literal search string to match post content or title
	 * - title => an exact case-insensitive match to a post title
	 * - title_search => a search string that acts only on the post title
	 * - has:info => a post info key or array of post info keys, which should be present
	 * - all:info => a post info key and value pair or array of post info key and value pairs, which should all be present and match
	 * - not:all:info => a post info key and value pair or array of post info key and value pairs, to exclude if all are present and match
	 * - any:info => a post info key and value pair or array of post info key and value pairs, any of which can match
	 * - not:any:info => a post info key and value pair or array of post info key and value pairs, to exclude if any are present and match
	 * - vocabulary => an array describing parameters related to vocabularies attached to posts. This can be one of two forms:
	 *   - object-based, in which an array of Term objects are passed
	 *     - any => posts associated with any of the terms are returned
	 *     - all => posts associated with all of the terms are returned
	 *     - not => posts associated with none of the terms are returned
	 *   - property-based, in which an array of vocabulary names and associated fields are passed
	 *     - vocabulary_name:term => a vocabulary name and term slug pair or array of vocabulary name and term slug pairs, any of which can be associated with the posts
	 *     - vocabulary_name:term_display => a vocabulary name and term display pair or array of vocabulary name and term display pairs, any of which can be associated with the posts
	 *     - vocabulary_name:not:term => a vocabulary name and term slug pair or array of vocabulary name and term slug pairs, none of which can be associated with the posts
	 *     - vocabulary_name:not:term_display => a vocabulary name and term display pair or array of vocabulary name and term display pairs, none of which can be associated with the posts
	 *     - vocabulary_name:all:term => a vocabulary name and term slug pair or array of vocabulary name and term slug pairs, all of which must be associated with the posts
	 *     - vocabulary_name:all:term_display => a vocabulary name and term display pair or array of vocabulary name and term display pairs, all of which must be associated with the posts
	 * - limit => the maximum number of posts to return, implicitly set for many queries
	 * - nolimit => do not implicitly set limit
	 * - offset => amount by which to offset returned posts, used in conjunction with limit
	 * - page => the 'page' of posts to return when paging, sets the appropriate offset
	 * - count => return the number of posts that would be returned by this request
	 * - orderby => how to order the returned posts
	 * - groupby => columns by which to group the returned posts, for aggregate functions
	 * - having => for selecting posts based on an aggregate function
	 * - where => manipulate the generated WHERE clause. Currently broken, see https://trac.habariproject.org/habari/ticket/1383
	 * - add_select => an array of clauses to be added to the generated SELECT clause.
	 * - fetch_fn => the function used to fetch data, one of 'get_results', 'get_row', 'get_value', 'get_query'
	 *
	 * Further description of parameters, including usage examples, can be found at
	 * http://wiki.habariproject.org/en/Dev:Retrieving_Posts
	 *
	 * @return Posts|Post|string An array of Post objects, or a single post object, depending on request
	 */
	public static function get( $paramarray = array() )
	{
		static $presets;

		// If $paramarray is a string, use it as a Preset
		if(is_string($paramarray)) {
			$paramarray = array('preset' => $paramarray);
		}

		// If $paramarray is a querystring, convert it to an array
		$paramarray = Utils::get_params( $paramarray );
		if($paramarray instanceof ArrayIterator) {
			$paramarray = $paramarray->getArrayCopy();
		}

		// If a preset is defined, get the named array and merge it with the provided parameters,
		// allowing the additional $paramarray settings to override the preset
		if(isset($paramarray['preset'])) {
			if(!isset($presets)) {
				$presets = Plugins::filter('posts_get_all_presets', $presets, $paramarray['preset']);
			}
			$paramarray['preset'] = Utils::single_array($paramarray['preset']);
			foreach($paramarray['preset'] as $presetname => $fallbackpreset) {
				if(isset($presets[$fallbackpreset])) {
					$preset = Plugins::filter('posts_get_update_preset', $presets[$fallbackpreset], $presetname, $paramarray);
					if(is_array( $preset ) || $preset instanceof ArrayObject || $preset instanceof ArrayIterator) {
						$paramarray = array_merge($preset, $paramarray);
						break;
					}
				}
			}
		}

		// let plugins alter the param array before we use it. could be useful for modifying search results, etc.
		$paramarray = Plugins::filter( 'posts_get_paramarray', $paramarray );

		$join_params = array();
		$params = array();
		$fns = array( 'get_results', 'get_row', 'get_value', 'get_query' );
		$select_ary = array();

		// Default fields to select, everything by default
		$default_fields = Plugins::filter('post_default_fields', Post::default_fields(), $paramarray);
		if(isset($paramarray['default_fields'])) {
			$param_defaults = Utils::single_array($paramarray['default_fields']);
			$default_fields = array_merge($default_fields, $param_defaults);
		}
		foreach ( $default_fields as $field => $value ) {
			if(preg_match('/(?:(?P<table>[\w\{\}]+)\.)?(?P<field>\w+)(?:(?:\s+as\s+)(?P<alias>\w+))?/i', $field, $fielddata)) {
				if(empty($fielddata['table'])) {
					$fielddata['table'] = '{posts}';
				}
				if(empty($fielddata['alias'])) {
					$fielddata['alias'] = $fielddata['field'];
				}
			}
			$select_ary[$fielddata['alias']] = "{$fielddata['table']}.{$fielddata['field']} AS {$fielddata['alias']}";
			$select_distinct[$fielddata['alias']] = "{$fielddata['table']}.{$fielddata['field']}";
		}

		// Default parameters
		$orderby = 'pubdate DESC';

		// Define the WHERE sets to process and OR in the final SQL statement
		if ( isset( $paramarray['where'] ) && is_array( $paramarray['where'] ) ) {
			$wheresets = $paramarray['where'];
		}
		else {
			$wheresets = array( array() );
		}

		/* Start building the WHERE clauses */

		$query = Query::create('{posts}');
		$query->select($select_ary);

		// If the request has a textual WHERE clause, add it to the query then continue the processing of the $wheresets
		if ( isset( $paramarray['where'] ) && is_string( $paramarray['where'] ) ) {
			$query->where()->add($paramarray['where']);
		}
		foreach ( $wheresets as $paramset ) {
			$where = new QueryWhere();

			$paramset = array_merge( (array) $paramarray, (array) $paramset );

			if ( isset( $paramset['id'] ) ) {
				$where->in('{posts}.id', $paramset['id'], 'posts_id', 'intval');
			}
			if ( isset( $paramset['not:id'] ) ) {
				$where->in('{posts}.id', $paramset['not:id'], 'posts_not_id', 'intval', false);
			}

			if ( isset( $paramset['status'] ) && ( $paramset['status'] != 'any' ) && ( 0 !== $paramset['status'] ) ) {
				$where->in('{posts}.status', $paramset['status'], 'posts_status', function($a) {return Post::status( $a );} );
			}

			if ( isset( $paramset['not:status'] ) && ( $paramset['not:status'] != 'any' ) && ( 0 !== $paramset['not:status'] ) ) {
				$where->in('{posts}.status', $paramset['not:status'], 'posts_not_status', function($a) {return Post::status( $a );}, null, false );
			}

			if ( isset( $paramset['content_type'] ) && ( $paramset['content_type'] != 'any' ) && ( 0 !== $paramset['content_type'] ) ) {
				$where->in('{posts}.content_type', $paramset['content_type'], 'posts_content_type', function($a) {return Post::type( $a );} );
			}
			if ( isset( $paramset['not:content_type'] ) ) {
				$where->in('{posts}.content_type', $paramset['not:content_type'], 'posts_not_content_type', function($a) {return Post::type( $a );}, false );
			}

			if ( isset( $paramset['slug'] ) ) {
				$where->in('{posts}.slug', $paramset['slug'], 'posts_slug');
			}
			if ( isset( $paramset['not:slug'] ) ) {
				$where->in('{posts}.slug', $paramset['not:slug'], 'posts_not_slug', null, false);
			}

			if ( isset( $paramset['user_id'] ) && 0 !== $paramset['user_id'] ) {
				$where->in('{posts}.user_id', $paramset['user_id'], 'posts_user_id', 'intval');
			}
			if ( isset( $paramset['not:user_id'] ) && 0 !== $paramset['not:user_id'] ) {
				$where->in('{posts}.user_id', $paramset['not:user_id'], 'posts_not_user_id', 'intval', false);
			}

			if ( isset( $paramset['vocabulary'] ) ) {

				if ( is_string( $paramset['vocabulary'] ) ) {
					$paramset['vocabulary'] = Utils::get_params( $paramset['vocabulary'] );
				}

				// parse out the different formats we accept arguments in into a single mutli-dimensional array of goodness
				$paramset['vocabulary'] = self::vocabulary_params( $paramset['vocabulary'] );
				$object_id = Vocabulary::object_type_id( 'post' );

				if ( isset( $paramset['vocabulary']['all'] ) ) {
					$all = $paramset['vocabulary']['all'];

					foreach ( $all as $vocab => $value ) {

						foreach ( $value as $field => $terms ) {

							// we only support these fields to search by
							if ( !in_array( $field, array( 'id', 'term', 'term_display' ) ) ) {
								continue;
							}

							$join_group = Query::new_param_name('join');
							$query->join( 'JOIN {object_terms} ' . $join_group . '_ot ON {posts}.id = ' . $join_group . '_ot.object_id', array(), 'term2post_posts_' . $join_group );
							$query->join( 'JOIN {terms} ' . $join_group . '_t ON ' . $join_group . '_ot.term_id = ' . $join_group . '_t.id', array(), 'terms_term2post_' . $join_group );
							$query->join( 'JOIN {vocabularies} ' . $join_group . '_v ON ' . $join_group . '_t.vocabulary_id = ' . $join_group . '_v.id', array(), 'terms_vocabulary_' . $join_group );

							$where->in( $join_group . '_v.name', $vocab );
							$where->in( $join_group . "_t.{$field}", $terms );
							$where->in( $join_group . '_ot.object_type_id', $object_id );
						}

						// this causes no posts to match if combined with 'any' below and should be re-thought... somehow
						$groupby = implode( ',', $select_distinct );
						$having = 'count(*) = ' . count( $terms );

					}
				}

				if ( isset( $paramset['vocabulary']['any'] ) ) {
					$any = $paramset['vocabulary']['any'];

					$orwhere = new QueryWhere( 'OR' );

					foreach ( $any as $vocab => $value ) {

						foreach ( $value as $field => $terms ) {

							$andwhere = new QueryWhere();

							// we only support these fields to search by
							if ( !in_array( $field, array( 'id', 'term', 'term_display' ) ) ) {
								continue;
							}

							$join_group = Query::new_param_name('join');
							$query->join( 'JOIN {object_terms} ' . $join_group . '_ot ON {posts}.id = ' . $join_group . '_ot.object_id', array(), 'term2post_posts_' . $join_group );
							$query->join( 'JOIN {terms} ' . $join_group . '_t ON ' . $join_group . '_ot.term_id = ' . $join_group . '_t.id', array(), 'terms_term2post_' . $join_group );
							$query->join( 'JOIN {vocabularies} ' . $join_group . '_v ON ' . $join_group . '_t.vocabulary_id = ' . $join_group . '_v.id', array(), 'terms_vocabulary_' . $join_group );

							$andwhere->in( $join_group . '_v.name', $vocab );
							$andwhere->in( $join_group . "_t.{$field}", $terms );
							$andwhere->in( $join_group . '_ot.object_type_id', $object_id );
						}
						$orwhere->add( $andwhere );

					}
					$where->add( $orwhere );
				}

				if ( isset( $paramset['vocabulary']['not'] ) ) {
					$not = $paramset['vocabulary']['not'];

					foreach ( $not as $vocab => $value ) {

						foreach ( $value as $field => $terms ) {

							// we only support these fields to search by
							if ( !in_array( $field, array( 'id', 'term', 'term_display' ) ) ) {
								continue;
							}

							$subquery_alias = Query::new_param_name('subquery');
							$subquery = Query::create( '{object_terms}' )->select('object_id');
							$subquery->join( 'JOIN {terms} ON {terms}.id = {object_terms}.term_id' );
							$subquery->join( 'JOIN {vocabularies} ON {terms}.vocabulary_id = {vocabularies}.id' );

							$subquery->where()->in( "{terms}.{$field}", $terms );
							$subquery->where()->in( '{object_terms}.object_type_id', $object_id );
							$subquery->where()->in( '{vocabularies}.name', $vocab );

							$query->join( 'LEFT JOIN (' . $subquery->get() . ') ' . $subquery_alias . ' ON ' . $subquery_alias . '.object_id = {posts}.id', $subquery->params(), $subquery_alias );

							$where->add( 'COALESCE(' . $subquery_alias . '.object_id, 0) = 0' );
						}

					}
				}
			}

			if ( isset( $paramset['criteria'] ) ) {
				// this regex matches any unicode letters (\p{L}) or numbers (\p{N}) inside a set of quotes (but strips the quotes) OR not in a set of quotes
				preg_match_all( '/(?<=")([\p{L}\p{N}]+[^"]*)(?=")|([\p{L}\p{N}]+)/u', $paramset['criteria'], $matches );
				foreach ( $matches[0] as $word ) {
					$crit_placeholder = $query->new_param_name('criteria');
					$where->add("( LOWER( {posts}.title ) LIKE :{$crit_placeholder} OR LOWER( {posts}.content ) LIKE :{$crit_placeholder})", array($crit_placeholder => '%' . MultiByte::strtolower( $word ) . '%'));
				}
			}

			if ( isset( $paramset['title'] ) ) {
				$where->add("LOWER( {posts}.title ) LIKE :title_match", array('title_match' => MultiByte::strtolower( $paramset['title'] )));
			}

			if ( isset( $paramset['title_search'] ) ) {
				// this regex matches any unicode letters (\p{L}) or numbers (\p{N}) inside a set of quotes (but strips the quotes) OR not in a set of quotes
				preg_match_all( '/(?<=")([\p{L}\p{N}]+[^"]*)(?=")|([\p{L}\p{N}]+)/u', $paramset['title_search'], $matches );
				foreach ( $matches[0] as $word ) {
					$crit_placeholder = $query->new_param_name('title_search');
					$where->add("LOWER( {posts}.title ) LIKE :{$crit_placeholder}", array($crit_placeholder => '%' . MultiByte::strtolower( $word ) . '%'));
				}
			}

			// Handle field queries on posts and joined tables
			foreach($select_ary as $field => $aliasing) {
				if(in_array($field, array('id', 'title', 'slug', 'status', 'content_type', 'user_id')) ) {
					// skip fields that we're handling a different way
					continue;
				}
				if(isset($paramset[$field])) {
					if(is_callable($paramset[$field])) {
						$paramset[$field]($where, $paramset);
					}
					else {
						$where->in($field, $paramset[$field], 'posts_field_' . $field);
					}
				}
			}

			//Done
			if ( isset( $paramset['all:info'] ) || isset( $paramset['info'] ) ) {

				// merge the two possibile calls together
				$infos = array_merge( isset( $paramset['all:info'] ) ? $paramset['all:info'] : array(), isset( $paramset['info'] ) ? $paramset['info'] : array() );

				if ( Utils::is_traversable( $infos ) ) {
					$pi_count = 0;
					foreach ( $infos as $info_key => $info_value ) {
						$pi_count++;

						$infokey_field = Query::new_param_name('info_key' );
						$infovalue_field = Query::new_param_name( 'info_value');
						$query->join( "LEFT JOIN {postinfo} ipi{$pi_count} ON {posts}.id = ipi{$pi_count}.post_id AND ipi{$pi_count}.name = :{$infokey_field} AND ipi{$pi_count}.value = :{$infovalue_field}", array( $infokey_field => $info_key, $infovalue_field => $info_value ), 'all_info_' . $info_key );
						$where->add( "ipi{$pi_count}.name <> ''" );
						$query->select( array( "info_{$info_key}_value" => "ipi{$pi_count}.value AS info_{$info_key}_value" ) );
						$select_distinct["info_{$info_key}_value"] = "info_{$info_key}_value";
					}
				}

			}

			//Done
			if ( isset( $paramset['any:info'] ) ) {
				if ( Utils::is_traversable( $paramset['any:info'] ) ) {
					$pi_count = 0;
					$orwhere = new QueryWhere( 'OR' );
					foreach ( $paramset['any:info'] as $info_key => $info_value ) {
						$pi_count++;

						if ( is_array( $info_value ) ) {
							$infokey_field = Query::new_param_name( 'info_key' );
							$inwhere = new QueryWhere( '' );
							$inwhere->in( "aipi{$pi_count}.value", $info_value );
							$query->join( "LEFT JOIN {postinfo} aipi{$pi_count} ON {posts}.id = aipi{$pi_count}.post_id AND aipi{$pi_count}.name = :{$infokey_field} AND " . $inwhere->get(), array_merge( array( $info_key ), $inwhere->params() ), 'any_info_' . $info_key );
						}
						else {
							$infokey_field = Query::new_param_name( 'info_key' );
							$infovalue_field = Query::new_param_name( 'info_value' );
							$query->join( "LEFT JOIN {postinfo} aipi{$pi_count} ON {posts}.id = aipi{$pi_count}.post_id AND aipi{$pi_count}.name = :{$infokey_field} AND aipi{$pi_count}.value = :{$infovalue_field}", array( $infokey_field => $info_key, $infovalue_field => $info_value ), 'any_info_' . $info_key );
						}

						$orwhere->add( "aipi{$pi_count}.name <> ''" );

						$query->select( array( "info_{$info_key}_value" => "aipi{$pi_count}.value AS info_{$info_key}_value" ) );
						$select_distinct["info_{$info_key}_value"] = "info_{$info_key}_value";
					}
					$where->add( '(' . $orwhere->get() . ')' );
				}
			}

			// Done
			if ( isset( $paramset['has:info'] ) ) {
				$has_info = Utils::single_array( $paramset['has:info'] );
				$pi_count = 0;
				$orwhere = new QueryWhere( 'OR' );
				foreach( $has_info as $info_name ) {
					$infoname_field = Query::new_param_name( 'info_name' );
					$pi_count++;
					$query->join("LEFT JOIN {postinfo} hipi{$pi_count} ON {posts}.id = hipi{$pi_count}.post_id AND hipi{$pi_count}.name = :{$infoname_field}", array( $infoname_field => $info_name ), 'has_info_' . $info_name );
					$orwhere->add( "hipi{$pi_count}.name <> ''" );

					$query->select( array( "info_{$info_name}_value" => "hipi{$pi_count}.value AS info_{$info_name}_value" ) );
					$select_distinct["info_{$info_name}_value"] = "info_{$info_name}_value";
				}
				$where->add( '(' . $orwhere->get() . ')' );
			}

			//Done
			if ( isset( $paramset['not:all:info'] ) || isset( $paramset['not:info'] ) ) {

				// merge the two possible calls together
				$infos = array_merge( isset( $paramset['not:all:info'] ) ? $paramset['not:all:info'] : array(), isset( $paramset['not:info'] ) ? $paramset['not:info'] : array() );

				if ( Utils::is_traversable( $infos ) ) {
					$orwhere = new QueryWhere( 'OR' );
					foreach ( $infos as $info_key => $info_value ) {
						$andwhere = new QueryWhere();
						$andwhere->in( '{postinfo}.name', $info_key );
						$andwhere->in( '{postinfo}.value', $info_value );
						$orwhere->add( $andwhere );

					}
					// see that hard-coded number in having()? sqlite wets itself if we use a bound parameter... don't change that
					$subquery = Query::create( '{postinfo}' )->select( '{postinfo}.post_id' )->groupby( 'post_id' )->having( 'COUNT(*) = ' . count( $infos ) );
					$subquery->where()->add( $orwhere );

					$where->in( '{posts}.id', $subquery, 'posts_not_all_info_query', null, false );
				}

			}

			//Tested. Test fails with original code
			if ( isset( $paramset['not:any:info'] ) ) {
				if ( Utils::is_traversable( $paramset['not:any:info'] ) ) {
					$subquery = Query::create('{postinfo}')->select('post_id');

					foreach ( $paramset['not:any:info'] as $info_key => $info_value ) {
						$infokey_field = $query->new_param_name('info_key');
						$infovalue_field = $query->new_param_name('info_value');
//							$subquery->where()->add(" ({postinfo}.name = :{$infokey_field} AND {postinfo}.value = :{$infovalue_field} ) ", array($infokey_field => $info_key, $infovalue_field => $info_value));
						$subquery->where( 'OR' )->add(" ({postinfo}.name = :{$infokey_field} AND {postinfo}.value = :{$infovalue_field} ) ", array($infokey_field => $info_key, $infovalue_field => $info_value));
					}

					$where->in('{posts}.id', $subquery, 'posts_not_any_info', null, false);
				}
			}

			/**
			 * Build the statement needed to filter by pubdate:
			 * If we've got the day, then get the date;
			 * If we've got the month, but no date, get the month;
			 * If we've only got the year, get the whole year.
			 */
			if ( isset( $paramset['day'] ) && isset( $paramset['month'] ) && isset( $paramset['year'] ) ) {
				$start_date = sprintf( '%d-%02d-%02d', $paramset['year'], $paramset['month'], $paramset['day'] );
				$start_date = HabariDateTime::date_create( $start_date );
				$where->add('pubdate BETWEEN :start_date AND :end_date', array('start_date' => $start_date->sql, 'end_date' => $start_date->modify( '+1 day -1 second' )->sql));
			}
			elseif ( isset( $paramset['month'] ) && isset( $paramset['year'] ) ) {
				$start_date = sprintf( '%d-%02d-%02d', $paramset['year'], $paramset['month'], 1 );
				$start_date = HabariDateTime::date_create( $start_date );
				$where->add('pubdate BETWEEN :start_date AND :end_date', array('start_date' => $start_date->sql, 'end_date' => $start_date->modify( '+1 month -1 second' )->sql));
			}
			elseif ( isset( $paramset['year'] ) ) {
				$start_date = sprintf( '%d-%02d-%02d', $paramset['year'], 1, 1 );
				$start_date = HabariDateTime::date_create( $start_date );
				$where->add('pubdate BETWEEN :start_date AND :end_date', array('start_date' => $start_date->sql, 'end_date' => $start_date->modify( '+1 year -1 second' )->sql));
			}

			if ( isset( $paramset['after'] ) ) {
				$where->add('pubdate > :after_date', array('after_date' => HabariDateTime::date_create( $paramset['after'] )->sql));
			}

			if ( isset( $paramset['before'] ) ) {
				$where->add('pubdate < :before_date', array('before_date' => HabariDateTime::date_create( $paramset['before'] )->sql));
			}

			// Concatenate the WHERE clauses
			$query->where()->add($where);
		}

		if(isset($paramset['post_join'])) {
			$post_joins = Utils::single_array($paramset['post_join']);
			foreach($post_joins as $post_join) {
				if(preg_match('#^(\S+)(?:\s+as)?\s+(\S+)$#i', $post_join, $matches)) {
					$query->join("LEFT JOIN {$matches[1]} {$matches[2]} ON {$matches[2]}.post_id = {posts}.id ");
				}
				else {
					$query->join("LEFT JOIN {$post_join} ON {$post_join}.post_id = {posts}.id ");
				}
			}
		}

		// Only show posts to which the current user has permission
		if ( isset( $paramset['ignore_permissions'] ) ) {
			$master_perm_where = new QueryWhere();
			// Set up the merge params
			$merge_params = array( $join_params, $params );
			$params = call_user_func_array( 'array_merge', $merge_params );
		}
		else {
			$master_perm_where = new QueryWhere();
			// This set of wheres will be used to generate a list of post_ids that this user can read
			$perm_where = new QueryWhere('OR');
			$perm_where_denied = new QueryWhere('AND');

			// Get the tokens that this user is granted or denied access to read
			$read_tokens = isset( $paramset['read_tokens'] ) ? $paramset['read_tokens'] : ACL::user_tokens( User::identify(), 'read', true );
			$deny_tokens = isset( $paramset['deny_tokens'] ) ? $paramset['deny_tokens'] : ACL::user_tokens( User::identify(), 'deny', true );

			// If a user can read any post type, let him
			if ( User::identify()->can( 'post_any', 'read' ) ) {
				$perm_where->add( '(1=1)' );
			}
			else {
				// If a user can read his own posts, let him
				if ( User::identify()->can( 'own_posts', 'read' ) ) {
					$perm_where->add('{posts}.user_id = :current_user_id', array('current_user_id' => User::identify()->id));
				}

				// If a user can read specific post types, let him
				$permitted_post_types = array();
				foreach ( Post::list_active_post_types() as $name => $posttype ) {
					if ( User::identify()->can( 'post_' . Utils::slugify( $name ), 'read' ) ) {
						$permitted_post_types[] = $posttype;
					}
				}
				if ( count( $permitted_post_types ) > 0 ) {
					$perm_where->in('{posts}.content_type', $permitted_post_types, 'posts_permitted_types', 'intval');
				}

				// If a user can read posts with specific tokens, let him
				if ( count( $read_tokens ) > 0 ) {
					$query->join('LEFT JOIN {post_tokens} pt_allowed ON {posts}.id= pt_allowed.post_id AND pt_allowed.token_id IN ('.implode( ',', $read_tokens ).')', array(), 'post_tokens__allowed');
					$perm_where->add('pt_allowed.post_id IS NOT NULL', array(), 'perms_join_not_null');
				}

				// If a user has access to read other users' unpublished posts, let him
				if ( User::identify()->can( 'post_unpublished', 'read' ) ) {
					$perm_where->add('({posts}.status <> :status_published AND {posts}.user_id <> :current_user_id)', array('current_user_id' => User::identify()->id, 'status_published' => Post::status('published')));
				}

			}

			// If a user is denied access to all posts, do so
			if ( User::identify()->cannot( 'post_any' ) ) {
				$perm_where_denied->add('(1=0)');
			}
			else {
				// If a user is denied read access to specific post types, deny him
				$denied_post_types = array();
				foreach ( Post::list_active_post_types() as $name => $posttype ) {
					if ( User::identify()->cannot( 'post_' . Utils::slugify( $name ) ) ) {
						$denied_post_types[] = $posttype;
					}
				}
				if ( count( $denied_post_types ) > 0 ) {
					$perm_where_denied->in('{posts}.content_type', $denied_post_types, 'posts_denied_types', 'intval', false);
				}

				// If a user is denied read access to posts with specific tokens, deny it
				if ( count( $deny_tokens ) > 0 ) {
					$query->join('LEFT JOIN {post_tokens} pt_denied ON {posts}.id= pt_denied.post_id AND pt_denied.token_id IN ('.implode( ',', $deny_tokens ).')', array(), 'post_tokens__denied');
					$perm_where_denied->add('pt_denied.post_id IS NULL', array(), 'perms_join_null');
				}

				// If a user is denied access to read other users' unpublished posts, deny it
				if ( User::identify()->cannot( 'post_unpublished' ) ) {
					$perm_where_denied->add('({posts}.status = :status_published OR {posts}.user_id = :current_user_id)', array('current_user_id' => User::identify()->id, 'status_published' => Post::status('published')));
				}

			}

			Plugins::act( 'post_get_perm_where', $perm_where, $paramarray );
			Plugins::act( 'post_get_perm_where_denied', $perm_where_denied, $paramarray );

			// If there are granted permissions to check, add them to the where clause
			if($perm_where->count() == 0 && !$query->joined('post_tokens__allowed')) {
				$master_perm_where->add('(1=0)', array(), 'perms_granted');
			}
			else {
				$master_perm_where->add($perm_where, array(), 'perms_granted');
			}

			// If there are denied permissions to check, add them to the where clause
			if($perm_where_denied->count() > 0 || $query->joined('post_tokens__denied')) {
				$master_perm_where->add($perm_where_denied, array(), 'perms_denied');
			}

		}
		$query->where()->add($master_perm_where, array(), 'master_perm_where');

		// Extract the remaining parameters which will be used onwards
		// For example: page number, fetch function, limit
		$paramarray = new SuperGlobal( $paramarray );
		$extract = $paramarray->filter_keys( 'page', 'fetch_fn', 'count', 'orderby', 'groupby', 'limit', 'offset', 'nolimit', 'having', 'add_select' );
		foreach ( $extract as $key => $value ) {
			$$key = $value;
		}

		// Define the LIMIT if it does not exist, unless specific posts are requested or we're getting the monthly counts
		if ( !isset( $limit ) && !isset( $paramset['id'] ) && !isset( $paramset['slug'] ) && !isset( $paramset['month_cts'] ) ) {
			$limit = Options::get( 'pagination' ) ? (int) Options::get( 'pagination' ) : 5;
		}
		elseif ( !isset( $limit ) ) {
			$selected_posts = 0;
			if ( isset( $paramset['id'] ) ) {
				$selected_posts += count( Utils::single_array( $paramset['id'] ) );
			}
			if ( isset( $paramset['slug'] ) ) {
				$selected_posts += count( Utils::single_array( $paramset['slug'] ) );
			}
			$limit = $selected_posts > 0 ? $selected_posts : '';
		}

		// Calculate the OFFSET based on the page number
		if ( isset( $page ) && is_numeric( $page ) && !isset( $paramset['offset'] ) ) {
			$offset = ( intval( $page ) - 1 ) * intval( $limit );
		}

		/**
		 * Determine which fetch function to use:
		 * If it is specified, make sure it is valid (based on the $fns array defined at the beginning of this function);
		 * Else, use 'get_results' which will return a Posts array of Post objects.
		 */
		if ( isset( $fetch_fn ) ) {
			if ( ! in_array( $fetch_fn, $fns ) ) {
				$fetch_fn = $fns[0];
			}
		}
		else {
			$fetch_fn = $fns[0];
		}

		// Add arbitrary fields to the select clause for sorting and output
		if ( isset( $add_select ) ) {
			$query->select($add_select);
		}

		/**
		 * If a count is requested:
		 * Replace the current fields to select with a COUNT();
		 * Change the fetch function to 'get_value';
		 * Remove the ORDER BY since it's useless.
		 * Remove the GROUP BY (tag search added it)
		 */
		if ( isset( $count ) ) {
			$query->set_select( "COUNT({$count})" );
			$fetch_fn = isset($paramarray['fetch_fn']) ? $fetch_fn : 'get_value';
			$orderby = null;
			$groupby = null;
			$having = null;
		}

		// If the month counts are requested, replaced the select clause
		if ( isset( $paramset['month_cts'] ) ) {
			if ( isset( $paramset['vocabulary'] ) ) {
				$query->set_select('MONTH(FROM_UNIXTIME(pubdate)) AS month, YEAR(FROM_UNIXTIME(pubdate)) AS year, COUNT(DISTINCT {posts}.id) AS ct');
			}
			else {
				$query->set_select('MONTH(FROM_UNIXTIME(pubdate)) AS month, YEAR(FROM_UNIXTIME(pubdate)) AS year, COUNT(*) AS ct');
			}
			$groupby = 'year, month';
			if ( !isset( $paramarray['orderby'] ) ) {
				$orderby = 'year, month';
			}
		}

		// Remove the LIMIT if 'nolimit'
		// Doing this first should allow OFFSET to work
		if ( isset( $nolimit ) ) {
			$limit = null;
		}

		// Define the LIMIT, OFFSET, ORDER BY, GROUP BY if they exist
		if(isset($limit)) {
			$query->limit($limit);
		}
		if(isset($offset)) {
			$query->offset($offset);
		}
		if(isset($orderby)) {
			$query->orderby($orderby);
		}
		if(isset($groupby)) {
			$query->groupby($groupby);
		}
		if(isset($having)) {
			$query->having($having);
		}

		if(isset($paramarray['on_query_built'])) {
			foreach(Utils::single_array($paramarray['on_query_built']) as $built) {
				$built($query);
			}
		}

		Plugins::act('posts_get_query', $query, $paramarray);
		/* All SQL parts are constructed, on to real business! */

		/**
		 * DEBUG: Uncomment the following line to display everything that happens in this function
		 */
		//print_R('<pre>'.$query.'</pre>');
		//Utils::debug( $paramarray, $fetch_fn, $query, $params );
		//Session::notice($query);

		if ( 'get_query' == $fetch_fn ) {
			return array($query->get(), $query->params());
		}

		/**
		 * Execute the SQL statement using the PDO extension
		 */
		DB::set_fetch_mode( PDO::FETCH_CLASS );
		$fetch_class = 'Post';
		if(isset($paramarray['fetch_class'])) {
			$fetch_class = $paramarray['fetch_class'];
		}
		DB::set_fetch_class( $fetch_class );
		$results = DB::$fetch_fn( $query->get(), $query->params(), $fetch_class );

		//Utils::debug($results, $query->get(), $query->params());
		//Utils::debug( $paramarray, $fetch_fn, $query->get(), $query->params(), $results );
		//var_dump( $query );

		/**
		 * Return the results
		 */
		if ( 'get_results' != $fetch_fn ) {
			// Since a single result was requested, return a single Post object.
			return $results;
		}
		elseif ( is_array( $results ) ) {
			// With multiple results, return a Posts array of Post objects.
			$c = __CLASS__;
			$return_value = new $c( $results );
			$return_value->get_param_cache = $paramarray;
			return $return_value;
		}
	}

	/**
	 * Extract parameters from a Posts::get()-style param array, even from within where's
	 * @static
	 * @param array $paramarray An array of Posts::get()-style parameters
	 * @param string $param The parameters to extract
	 * @return array|bool The parameters in the $paramarray that match $param or false
	 */
	public static function extract_param($paramarray, $param) {
		$result = array();
		if(isset($paramarray[$param])) {
			$result = array_merge($result, Utils::single_array($paramarray[$param]));
		}
		if(isset($paramarray['where'])) {
			if(is_array($paramarray['where'])) {
				foreach($paramarray['where'] as $where) {
					if(isset($where[$param])) {
						$result = array_merge($result, Utils::single_array($where[$param]));
					}
				}
			}
		}
		return count($result) ? $result : false;
	}

	/**
	 * function by_status
	 * select all posts of a given status
	 * @param int a status value
	 * @return array an array of Comment objects with the same status
	 */
	public static function by_status ( $status )
	{
		return self::get( array( 'status' => $status ) );
	}

	/**
	 * function by_slug
	 * select all post content by slug
	 * @param string a post slug
	 * @return array an array of post content
	 */
	public static function by_slug ( $slug = '' )
	{
		return self::get( array( 'slug' => $slug ) );
	}

	/**
	 * static count_total
	 * return a count for the total number of posts
	 * @param mixed a status value to filter posts by; if false, then no filtering will be performed
	 * @return int the number of posts of specified type ( published or draft )
	 */
	public static function count_total( $status = false )
	{
		$params = array( 'count' => 1 );
		if ( $status !== false ) {
			$params['status'] = $status;
		}
		return self::get( $params );
	}

	/**
	 * return a count for the number of posts last queried
	 * @return int the number of posts of specified type ( published or draft )
	 */
	public function count_all()
	{
		$params = array_merge( ( array ) $this->get_param_cache, array( 'count' => '*', 'nolimit' => 1 ) );
		return Posts::get( $params );
	}

	/**
	 * return the query that generated this set of posts
	 * @return string The SQL and paramters used to generate this set of posts
	 */
	public function get_query()
	{
		$params = array_merge( ( array ) $this->get_param_cache, array( 'fetch_fn' => 'get_query') );
		return Posts::get( $params );
	}

	/**
	 * static count_by_author
	 * return a count of the number of posts by the specified author
	 * @param int an author ID
	 * @param mixed a status value to filter posts by; if false, then no filtering will be performed
	 * @return int the number of posts by the specified author
	 */
	public static function count_by_author( $user_id, $status = false )
	{
		$params = array( 'user_id' => $user_id, 'count' => 1 );
		if ( false !== $status ) {
			$params['status'] = $status;
		}
		return self::get( $params );
	}

	/**
	 * static count_by_tag
	 * return a count of the number of posts with the assigned tag
	 * @param string A tag
	 * @param mixed a status value to filter posts by; if false, then no filtering will be performed
	 * @return int the number of posts with the specified tag
	 */
	public static function count_by_tag( $tag, $status = false )
	{
		$params = array( 'vocabulary' => array( Tags::vocabulary()->name . ':term_display' => $tag ), 'count' => 1 );
		if ( false !== $status ) {
			$params['status'] = $status;
		}
		return self::get( $params );
	}

	/**
	 * Reassigns the author of a specified set of posts
	 * @param mixed a user ID or name
	 * @param mixed an array of post IDs, an array of Post objects, or an instance of Posts
	 * @return bool Whether the rename operation succeeded or not
	 */
	public static function reassign( $user, $posts )
	{

		if ( ! is_int( $user ) ) {
			$u = User::get( $user );
			$user = $u->id;
		}
		// safety checks
		if ( ( $user == 0 ) || empty( $posts ) ) {
			return false;
		}
		switch ( true ) {
			case is_integer( reset( $posts ) ):
				break;
			case reset( $posts ) instanceof Post:
				$ids = array();
				foreach ( $posts as $post ) {
					$ids[] = $post->id;
				}
				$posts = $ids;
				break;
			default:
				return false;
		}
		$ids = implode( ',', $posts );

		// allow plugins the opportunity to prevent the reassignment now that we've verified the user and posts
		$allow = true;
		$allow = Plugins::filter( 'posts_reassign_allow', $allow, $user, $posts );

		if ( !$allow ) {
			return false;
		}

		// actually perform the reassignment
		Plugins::act( 'posts_reassign_before', array( $user, $posts ) );
		$results = DB::query( "UPDATE {posts} SET user_id=? WHERE id IN ({$ids})", array( $user ) );
		Plugins::act( 'posts_reassign_after', array( $user, $posts ) );

		return $results;
	}

	/**
	 * function publish_scheduled_posts
	 *
	 * Callback function to publish scheduled posts
	 */
	public static function publish_scheduled_posts( $params )
	{
		$select = array();
		// Default fields to select, everything by default
		foreach ( Post::default_fields() as $field => $value ) {
			$select[$field] = "{posts}.$field AS $field";
		}
		$select = implode( ',', $select );
		$posts = DB::get_results( 'SELECT ' . $select . ' FROM {posts} WHERE {posts}.status = ? AND {posts}.pubdate <= ? ORDER BY {posts}.pubdate DESC', array( Post::status( 'scheduled' ), HabariDateTime::date_create() ), 'Post' );
		foreach ( $posts as $post ) {
			$post->publish();
		}
	}

	/**
	 * function update_scheduled_posts_cronjob
	 *
	 * Creates or recreates the cronjob to publish
	 * scheduled posts. It is called whenever a post
	 * is updated or created
	 *
	 */
	public static function update_scheduled_posts_cronjob()
	{
		$min_time = DB::get_value( 'SELECT MIN(pubdate) FROM {posts} WHERE status = ?', array( Post::status( 'scheduled' ) ) );

		CronTab::delete_cronjob( 'publish_scheduled_posts' );
		if ( $min_time ) {
			CronTab::add_single_cron( 'publish_scheduled_posts', array( 'Posts', 'publish_scheduled_posts' ), $min_time, 'Next run: ' . HabariDateTime::date_create( $min_time )->get( 'c' ) );
		}
	}

	/**
	 * Returns an ascending post
	 *
	 * @param The Post from which to start
	 * @param The params by which to work out what is the next ascending post
	 * @return Post The ascending post
	 */
	public static function ascend( $post, $params = null )
	{
		$posts = null;
		$ascend = false;
		if ( !$params ) {
			$params = array( 'where' => "pubdate >= '{$post->pubdate->sql}' AND content_type = {$post->content_type} AND status = {$post->status}", 'limit' => 2, 'orderby' => 'pubdate ASC' );
			$posts = Posts::get( $params );
		}
		elseif ( $params instanceof Posts ) {
			$posts = $params;
		}
		else {
			if ( !array_key_exists( 'orderby', $params ) ) {
				$params['orderby'] = 'pubdate ASC';
			}
			$posts = Posts::get( $params );
		}
		// find $post and return the next one.
		$index = $posts->search( $post );
		$target = $index + 1;
		if ( array_key_exists( $target, $posts ) ) {
			$ascend = $posts[$target];
		}
		return $ascend;
	}

	/**
	 * Returns a descending post
	 *
	 * @param The Post from which to start
	 * @param The params by which to work out what is the next descending post
	 * @return Post The descending post
	 */
	public static function descend( $post, $params = null )
	{
		$posts = null;
		$descend = false;
		if ( !$params ) {
			$params = array( 'where' => "pubdate <= '{$post->pubdate->sql}' AND content_type = {$post->content_type} AND status = {$post->status}", 'limit' => 2, 'orderby' => 'pubdate DESC' );
			$posts = Posts::get( $params );
		}
		elseif ( $params instanceof Posts ) {
			$posts = array_reverse( $params );
		}
		else {
			if ( !array_key_exists( 'orderby', $params ) ) {
				$params['orderby'] = 'pubdate DESC';
			}
			$posts = Posts::get( $params );
		}
		// find $post and return the next one.
		$index = $posts->search( $post );
		$target = $index + 1;
		if ( array_key_exists( $target, $posts ) ) {
			$descend = $posts[$target];
		}
		return $descend;
	}

	/**
	 * Search this Posts object for the needle, returns its key if found
	 *
	 * @param Post $needle Post object to find within this Posts object
	 * @return mixed Returns the index of the needle, on failure, null is returned
	 */
	public function search( $needle )
	{
		return array_search( $needle, $this->getArrayCopy() );
	}

	/**
	 * Parses a search string for status, type, author, and tag keywords. Returns
	 * an associative array which can be passed to Posts::get(). If multiple
	 * authors, statuses, tags, or types are specified, we assume an implicit OR
	 * such that (e.g.) any author that matches would be returned.
	 *
	 * @param string $search_string The search string
	 * @return array An associative array which can be passed to Posts::get()
	 */
	public static function search_to_get( $search_string )
	{
		// if adding to this array, make sure you update the consequences of a search on this below in the switch.
		$keywords = array( 'author' => 1, 'status' => 1, 'type' => 1, 'tag' => 1, 'info' => 1 );
		$statuses = Post::list_post_statuses();
		$types = Post::list_active_post_types();
		$arguments = array(
			'user_id' => array(),
			'status' => array(),
			'content_type' => array(),
			'vocabulary' => array(),
			'info' => array(),
		);
		$criteria = '';

		// this says, find stuff that has the keyword at the start, and then some term straight after.
		// the terms should have no whitespace, or if it does, be ' delimited.
		// ie tag:foo or tag:'foo bar'
		$flag_regex = '/(?P<flag>\w+):(?P<value>[^\'"][^\s]*|(?P<quote>[\'"])[^\3]+(?<!\\\\)\3)/i';

		// now do some matching.
		preg_match_all( $flag_regex, $search_string, $matches, PREG_SET_ORDER );

		// now we remove those terms from the search string, otherwise the keyword search below has issues. It will pick up things like
		// from tag:'pair of' -> matches of'
		$criteria = trim( preg_replace( $flag_regex, '', $search_string ) );

		// Add special criteria based on the flag parameters.
		foreach ( $matches as $match ) {
			// trim out any quote marks that have been matched.
			$quote = isset( $match['quote'] ) ? $match['quote'] : ' ';
			$value = trim( stripslashes( $match['value'] ), $quote );

			$flag = $match['flag'];
			$arguments = Plugins::filter( 'posts_search_to_get', $arguments, $flag, $value, $match, $search_string );
			switch ( $flag )  {
				case 'author':
					if ( $u = User::get( $value ) ) {
						$arguments['user_id'][] = (int) $u->id;
					}
					break;
				case 'tag':
					$arguments['vocabulary'][Tags::vocabulary()->name . ':term_display'][] = $value;
					break;
				case 'status':
					if ( isset( $statuses[$value] ) ) {
						$arguments['status'][] = (int) $statuses[$value];
					}
					break;
				case 'type':
					if ( isset( $types[$value] ) ) {
						$arguments['content_type'][] = (int) $types[$value];
					}
					break;
				case 'info':
					if ( strpos( $value, ':' ) !== false ) {
						list( $infokey, $infovalue ) = explode( ':', $value, 2 );
						$arguments['info'][$infokey] = $infovalue;
					}
					break;
			}
		}

		// flatten keys that have single-element or no-element arrays
		foreach ( $arguments as $key => $arg ) {
			switch ( count( $arg ) ) {
				case 0:
					unset( $arguments[$key] );
					break;
				case 1:
					if ( is_array( $arg ) ) {
						$arguments[$key] = $arg;
					}
					else {
						$arguments[$key] = $arg[0];
					}
					break;
			}
		}

		if ( $criteria != '' ) {
			$arguments['criteria'] = $criteria;
		}

		return $arguments;
	}

	/**
	 * Check if the requested post is of the type specified, to see if a rewrite rule matches.
	 *
	 * @return Boolean Whether the requested post matches the content type of the rule.
	 */
	public static function rewrite_match_type( $rule, $slug, $parameters )
	{
		$args = $rule->named_arg_values;
		$args['count'] = true;
		$postcount = Posts::get( $args );
		return $postcount > 0;
	}

	/**
	 * Return the type of the content represented by this object
	 *
	 * @return array The names of the possible content represented by this object
	 */
	function content_type ()
	{
		$content_type = array_map(
			function($a){
				return 'posts.' . $a;
			},
			$this->preset
		);
		$content_type[] = 'posts';
		return $content_type;
	}

	/**
	 * Accepts a set of term query qualifiers and converts it into a multi-dimensional array
	 * of vocabulary (ie: tags), matching method (any, all, not), matching field (id, term, term_display), and list of terms
	 *
	 * @return array An array of parsed term-matching conditions
	 */
	private static function vocabulary_params( $params )
	{
		$return = array();

		foreach ( $params as $key => $value ) {
			// split vocab off the beginning of the key
			if ( strpos( $key, ':' ) !== false ) {
				list( $newkey, $subkey ) = explode( ':', $key, 2 );
				$params[$newkey][$subkey] = $value;
				unset( $params[$key] );
			}

		}

		foreach ( $params as $vocab => $values ) {

			foreach ( $values as $key => $value ) {

				$value = Utils::single_array( $value );

				// if there's a colon we've got a mode and a field
				if ( strpos( $key, ':' ) !== false ) {
					list( $mode, $by_field ) = explode( ':', $key, 2 );
					foreach ( $value as $v ) {
						$return[$mode][$vocab][$by_field][] = $v;
					}
				}
				else {

					// if there's no colon we've got a single field name
					foreach ( $value as $v ) {

						if ( $v instanceof Term ) {
							// $vocab is not a vocab, but the mode - always match by its ID for the best performance
							$return[$vocab][$v->vocabulary->name]['id'][] = $v->id;
						}
						else {
							$return['any'][$vocab][$key][] = $v;
						}

					}

				}

			}

		}

		return $return;

	}

	/**
	 * Register plugin hooks
	 * @static
	 */
	public static function __static()
	{
		Pluggable::load_hooks('Posts');
	}

	/**
	 * Provide some default presets
	 * @static
	 * @param array $presets List of presets that other classes might provide
	 * @return array List of presets this class provides
	 */
	public static function filter_posts_get_all_presets($presets)
	{
		$presets['page_list'] = array( 'content_type' => 'page', 'status' => 'published', 'nolimit' => true );
		$presets['asides'] = array( 'vocabulary' => array( 'tags:term' => 'aside' ), 'limit' => 5 );
		$presets['home'] = array( 'content_type' => Post::type( 'entry' ), 'status' => Post::status( 'published' ), 'limit' => Options::get('pagination', 5) );

		return $presets;
	}

	/**
	 * function delete
	 * Delete all Posts in a Posts object
	 */
	public function delete()
	{
		foreach( $this as $post ) {
			$post->delete();
		}
	}

	/**
	 * Serialize these posts as JSON
	 * @return string Posts as JSON
	 */
	public function to_json()
	{
		$posts = array_map(function($e){return $e->to_json();}, $this->getArrayCopy());
		return '[' . implode(',', $posts) . ']';
	}
}

?>


code:
<?php

/**
 * Base Query class for building new queries
 * @see Posts::get
 */
class Query {

	/** @var QueryWhere $where Internal QueryWhere object */
	private $where = null;
	public $primary_table = null;
	protected $fields = array();
	protected $joins = array();
	protected $join_params = array();
	protected $limit = null;
	protected $offset = null;
	protected $orderby = null;
	protected $groupby = null;
	protected $having = null;

	/**
	 * Construct a Query
	 * @example
	 * $q = new Query('{posts}');
	 * @param $primary_table Name of the primary table (use {table} syntax to expand)
	 */
	public function __construct($primary_table = null)
	{
		$this->primary_table = $primary_table;
	}

	/**
	 * Static helper method to create a new query instance
	 * @example
	 * $q = Query::create('{posts}');;
	 * @static
	 * @param string $primary_table Name of the primary table (use {table} syntax to expand)
	 * @return Query A new instance of the Query class
	 */
	public static function create($primary_table = null)
	{
		return new Query($primary_table);
	}

	/**
	 * Adds fields to the SELECT statement
	 * @param array|string $fields A field or list of fields to add to existing select'ed fields
	 * @return Query Returns $this for fluid interface
	 */
	public function select($fields)
	{
		$args = func_get_args();
		foreach($args as $fields) {
			$this->fields = array_merge($this->fields, Utils::single_array($fields));
		}
		return $this;
	}

	/**
	 * Sets fields for the SELECT statement
	 * @param array|string $fields A field or list of fields to set as the fields to select, replaces existing selected fields
	 * @return Query Returns $this for fluid interface
	 */
	public function set_select($fields)
	{
		$this->fields = Utils::single_array($fields);
		return $this;
	}

	/**
	 * Set whether only distinct results should be returned
	 * @param bool $set True if the result set should be distinct
	 * @return Query Returns $this for fluid interface
	 */
	public function distinct($set = true)
	{
		$this->distinct = $set;
		return $this;
	}

	/**
	 * Sets the primary table for the FROM statement
	 * @param string $primary_table The primary table from which to select
	 * @return Query Returns $this for fluid interface
	 */
	public function from($primary_table)
	{
		$this->primary_table = $primary_table;
		return $this;
	}

	/**
	 * Adds a JOIN table
	 * @param string $join The table to create a join with
	 * @param array $parameters An array of parameters on which the JOIN is built
	 * @param string $alias An optional alias for the joined table
	 * @return Query Returns $this for fluid interface
	 */
	public function join($join, $parameters = array(), $alias = null)
	{
		if(empty($alias)) {
			$alias = md5($join);
		}
		$this->joins[$alias] = $join;
		$this->join_params = array_merge($this->join_params, $parameters);
		return $this;
	}

	/**
	 * Discover if a table alias is already JOINed to this query
	 * @param string $alias The name of a JOIN table alias
	 * @return bool true if the alias was used for a join
	 */
	public function joined($alias)
	{
		return array_key_exists($alias, $this->joins);
	}

	/**
	 * Create and/or return a QueryWhere object representing the where clause of this query
	 * @param string $operator The operator (AND/OR) to use between expressions in this clause
	 * @return QueryWhere An instance of the where clause for this query
	 */
	public function where($operator = 'AND')
	{
		if(!isset($this->where)) {
			$this->where = new QueryWhere($operator);
		}
		return $this->where;
	}

	/**
	 * Set the GROUP BY clause
	 * @param string $value The GROUP BY clause
	 * @return Query Returns $this for fluid interface
	 */
	public function groupby($value)
	{
		$this->groupby = empty($value) ? null : $value;
		return $this;
	}

	/**
	 * Set the ORDER BY clause
	 * @param string $value The ORDER BY clause
	 * @return Query Returns $this for fluid interface
	 */
	public function orderby($value)
	{
		$this->orderby = empty($value) ? null : $value;
		return $this;
	}

	public function having($value)
	{
		$this->having = empty($value) ? null : $value;
		return $this;
	}

	/**
	 * Sets the LIMIT
	 * @param integer $value The LIMIT
	 * @return Query Returns $this for fluid interface
	 */
	public function limit($value)
	{
		$this->limit = is_numeric($value) ? intval($value) : null;
		return $this;
	}

	/**
	 * Sets the OFFSET
	 * @param integer $value The OFFSET
	 * @return Query Returns $this for fluid interface
	 */
	public function offset($value)
	{
		$this->offset = is_numeric($value) ? intval($value) : null;
		return $this;
	}

	/**
	 * Obtain the SQL used to execute this query
	 * @return string The SQL to execute
	 */
	public function get()
	{
		$fields = $this->fields;

		// If the orderby has a function in it, try to create a select field for it with an alias
		$orderby = null;
		if(isset($this->orderby)) {
			$orderby = $this->orderby;
			if ( strpos( $orderby, '(' ) !== false ) {
				$orders = explode( ',', $orderby );
				$ob_index = 0;
				foreach ( $orders as $key => $order ) {
					if ( !preg_match( '%(?P<field>.+)\s+(?P<direction>DESC|ASC)%i', $order, $order_matches ) ) {
						$order_matches = array(
							'field' => $order,
							'direction' => '',
						);
					}

					if ( strpos( $order_matches['field'], '(' ) !== false ) {
						$ob_index++;
						$field = 'orderby' . $ob_index;
						$fields[$field] = "{$order_matches['field']} AS $field";
						$orders[$key] = $field . ' ' . $order_matches['direction'];
					}
				}
				$orderby = implode( ', ', $orders );
			}
		}

		$sql = "SELECT \n\t";
		if($this->distinct()) {
			$sql .= "DISTINCT \n\t";
		}
		if(count($fields) > 0) {
			$sql .= implode(",\n\t", $fields);
		}
		else {
			$sql .= "*";
		}
		$sql .= "\nFROM\n\t" . $this->primary_table;
		foreach($this->joins as $join) {
			$sql .= "\n" . $join;
		}
		$where = $this->where()->get();
		if(!empty($where)) {
			$sql .= "\nWHERE\n" . $this->where()->get();
		}

		if(isset($this->groupby)) {
			$sql .= "\nGROUP BY " . $this->groupby;
		}

		if(isset($this->having)) {
			$sql .= "\nHAVING " . $this->having;
		}
		if(isset($orderby)) {
			$sql .= "\nORDER BY " . $orderby;
		}

		if(isset($this->limit)) {
			$sql .= "\nLIMIT " . $this->limit;
			if(isset($this->offset)) {
				$sql .= "\nOFFSET " . $this->offset;
			}
		}

		return $sql;
	}

	/**
	 * Obtain the parameter values needed for the query
	 * @return array An associative array containing the parameters of the query
	 */
	public function params()
	{
		return array_merge($this->where()->params(), $this->join_params);
	}

	/**
	 * Obtain a parameter name with an optionally specified prefix that has not yet been used
	 * @static
	 * @param string $prefix An optional prefix to use for a new parameter
	 * @return string the new parameter
	 */
	public static function new_param_name($prefix = 'param')
	{
		static $param_names = array();

		if(!isset($param_names[$prefix])) {
			$param_names[$prefix] = 0;
		}
		$param_names[$prefix]++;
		return $prefix . '_' . $param_names[$prefix];
	}

	/**
	 * Execute and return the first row of this query
	 * @param string $class The optional class to return results as
	 * @return object The result object, a QueryRecord or the specified class
	 */
	public function row($class = null)
	{
		return DB::get_row($this->get(), $this->params(), $class);
	}

	/**
	 * Execute and return the returns of this query
	 * @param string $class The optional class to return results as
	 * @return array The results array of QueryRecords or the specified class
	 */
	public function results($class = null)
	{
		return DB::get_results($this->get(), $this->params(), $class);
	}

	/**
	 * Execute and return key-value pairs from this query
	 * @return array The results array of QueryRecords or the specified class
	 */
	public function keyvalue()
	{
		return DB::get_keyvalue($this->get(), $this->params());
	}

	/**
	 * Execute and return the first first field from each row of this query
	 * @return array The result array of values
	 */
	public function column()
	{
		return DB::get_column($this->get(), $this->params());
	}

	/**
	 * Execute and return the first row of this query
	 * @return object The result object, a QueryRecord or the specified class
	 */
	public function value()
	{
		return DB::get_value($this->get(), $this->params());
	}
}

/**
 * QueryWhere
 * Represents a where clause (or subclause) of a Query
 * @see Query
 */
class QueryWhere {
	protected $operator = 'AND';
	protected $expressions = array();
	protected $parameters = array();

	/**
	 * Constructor for the QueryWhere
	 * @param string $operator The operator (AND/OR) to user between expressions in this clause
	 */
	public function __construct($operator = 'AND')
	{
		$this->operator = $operator;
	}

	/**
	 * Convenience function for fluid interface
	 * @param string $operator The operator (AND/OR) to user between expressions in this clause
	 * @return QueryWhere Configured instance of the QueryWhere
	 */
	public function create($operator = 'AND')
	{
		return new QueryWhere($operator);
	}

	/**
	 * @param string|QueryWhere $expression A string expression to use as part of the query's where clause or
	 *                                      a compound expression represented by an additional QueryWhere instance
	 * @param array $parameters An associative array of values to use as named parameters in the added expression
	 * @param string $name Name of the expression
	 * @return QueryWhere Returns $this, for fluid interface.
	 */
	public function add($expression, $parameters = array(), $name = null)
	{
		if(empty($name)) {
			$name = count($this->expressions) + 1;
		}
		$this->expressions[$name] = $expression;
		$this->parameters = array_merge($this->parameters, $parameters);
		return $this;
	}

	/**
	 * Shortcut to implementing an IN or equality test for one or more values as a new expression
	 * @param $field
	 * @param $values
	 * @param string $paramname
	 * @param callback $validator
	 * @param boolean $positive
	 * @return QueryWhere Returns $this, for fluid interface
	 */
	public function in($field, $values, $paramname = null, $validator = null, $positive = true)
	{
		$expression = $field . ' ';
		if($values instanceof Query) {
			if( !$positive ) {
				$expression .= 'NOT ';
			}
			$expression .= 'IN (' . $values->get() . ')';
			$this->parameters = array_merge( $this->parameters, $values->params() );
		}
		elseif(is_array($values) && count($values) > 1) {
			$in_elements = array();
			if(is_callable($validator)) {
				foreach($values as $value) {
					$in_elements[] = $validator($value);
				}
			}
			else {
				foreach($values as $value) {
					$value_name = Query::new_param_name($paramname);
					$in_elements[] = ':' . $value_name;
					$this->parameters[$value_name] = $value;
				}
			}
			if(!$positive) {
				$expression .= 'NOT ';
			}
			$expression .= 'IN (' . implode(',', $in_elements) . ')';
		}
		else {
			if(is_array($values)) {
				$values = reset($values);
			}
			if(!$positive) {
				$expression .= ' <> ';
			}
			else {
				$expression .= ' = ';
			}

			if(empty($paramname)) {
				$paramname = Query::new_param_name();
			}

			if(is_callable($validator)) {
				$expression .= $validator($values);
			}
			else {
				$expression .= ':' . $paramname;
				$this->parameters[$paramname] = $values;
			}
		}

		if(empty($paramname)) {
			$paramname = count($this->expressions) + 1;
		}

		$this->expressions[$paramname] = $expression;
		return $this;
	}

	/**
	 * Shortcut to implementing an EXISTS test for one or more values as a new expression
	 * @param Query $values
	 * @param string $paramname
	 * @param boolean $positive
	 * @return QueryWhere Returns $this, for fluid interface
	 */
	public function exists( Query $values, $paramname = null, $positive = true )
	{
		$expression = '';

		if( !$positive ) {
			$expression .= 'NOT ';
		}

		$expression .= 'EXISTS (' . $values->get() . ')';

		$this->parameters = array_merge( $this->parameters, $values->params() );

		if( empty( $paramname ) ) {
			$paramname = count( $this->expressions ) + 1;
		}

		$this->expressions[$paramname] = $expression;
		return $this;
	}

	/**
	 * Obtain the parameters supplied for the where clause
	 * @return array An associative array of parameters added to this where clause
	 */
	public function params()
	{
		$parameters = $this->parameters;
		foreach($this->expressions as $expression) {
			if($expression instanceof Query) {
				$parameters = array_merge($parameters, $expression->params());
			}
			if($expression instanceof QueryWhere) {
				$parameters = array_merge($parameters, $expression->params());
			}
		}
		return $parameters;
	}

	/**
	 * Set a parameter value, class magic __set method
	 * @param string $name The name of the parameter to set
	 * @param mixed $value The value to set the parameter to
	 * @return mixed The supplied value
	 */
	public function __set($name, $value)
	{
		$this->parameters[$name] = $value;
		return $this->parameters[$name];
	}

	/**
	 * Get a parameter value, class magic __get method
	 * @param string $name The name of the parameter to get
	 * @return mixed The value of the parameter requested
	 */
	public function __get($name)
	{
		return $this->parameters[$name];
	}

	/**
	 * Obtain the where clause as a string to use in a query
	 * @param int $level Used internally to retain indenting
	 * @return string The where clause represented by this object
	 */
	public function get($level = 0)
	{
		$outputs = array();
		$indents = str_repeat("\t", $level);
		if(count($this->expressions) == 0) {
			return null;
		}
		foreach($this->expressions as $expression) {
			if($expression instanceof Query) {
				$outputs[] = $expression->get();
			}
			if($expression instanceof QueryWhere) {
				$outputs[] = $expression->get($level + 1);
			}
			else {
				$outputs[] = $indents . "\t" .  $expression;
			}
		}
		$outputs = array_filter($outputs);
		$output = implode("\n" . $indents . $this->operator . "\n", $outputs);
		if($level == 0) {
			return $output;
		}
		return $indents . "(\n" . $output . "\n" . $indents . ")";
	}

	/**
	 * Get the number of expressions contained in this QueryWhere
	 * @return int Number of expressions.
	 */
	public function count()
	{
		return count($this->expressions);
	}

}

?>

repo_name: DaSourcerer/habari


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

/**
 * Class to assist in profiling queries
 *
 */
class QueryProfile
{
	public $start_time;     // time that query started execution
	public $end_time;       // time that query ended execution
	public $query_text;     // SQL text
	public $backtrace = '';  // stack backtrace for debugging.

	/**
	 * Constructor for the query profile.  Automatically sets the
	 * start time for the query
	 *
	 * @param query SQL being executed
	 */
	public function __construct( $query )
	{
		$this->query_text = $query;
		/* Backtracing is very verbose. Enable only if set via query string */
		if ( isset( $_GET['db_profile'] )
			&& $_GET['db_profile'] == 'verbose' )
			$this->backtrace = debug_backtrace();
	}

	public function start()
	{
		$this->start_time = $this->get_time_in_microseconds();
	}

	public function stop()
	{
		$this->end_time = $this->get_time_in_microseconds();
	}

	public function __get( $name )
	{
		switch ( $name ) {
			case 'total_time':
				return $this->end_time - $this->start_time;
			default:
				return $this->$name;
		}
	}

	/**
	 * Returns an integer representing the current time
	 * in microseconds from Epoch
	 *
	 * @return  int the number of microseconds since epoch.
	 */
	public static function get_time_in_microseconds()
	{
		list( $usec, $sec ) = explode( ' ', microtime() );
		return ( (float) ( 1000*$sec )  + $usec );
	}

}


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

/**
 * Habari QueryRecord Class
 *
 */
class QueryRecord implements URLProperties
{
	protected $fields = array();  // Holds field values from db
	protected $newfields = array(); // Holds updated field values to commit to db
	protected $unsetfields = array(); // Holds field names to remove when committing to the db
	private $properties_loaded = array(); // Set [$name] to true after first load of properties

	/**
	 * constructor __construct
	 * Constructor for the QueryRecord class.
	 * @param array an associative array of initial field values.
	 */
	public function __construct( $paramarray = array() )
	{
		$params = Utils::get_params( $paramarray );
		if ( count( $params ) ) {
			// Defaults
			$this->fields = array_merge(
				$this->fields,
				$params
			);
			// mark any passed params as loaded when creating this object
			$this->properties_loaded = array_merge(
				$this->properties_loaded,
				array_combine( array_keys( $params ), array_fill( 0, count( $params ), true ) )
			);
		}
	}

	/**
	 * function __get
	 * Handles getting virtual properties for this class
	 * @param string Name of the property
	 * @return mixed The set value or null if none exists
	 **/
	public function __get( $name )
	{
		$return = null;
		if ( isset( $this->newfields[$name] ) ) {
			$return = $this->newfields[$name];
		}
		else if ( isset( $this->fields[$name] ) ) {
			$return = $this->fields[$name];
		}
		$classname = strtolower(get_class($this));
		return Plugins::filter('get_' . $classname . '_' . $name, $return, $this);
	}

	/**
	 * function __set
	 * Handles setting virtual properties for this class
	 * @param string Name of the property
	 * @param mixed Value to set it to
	 * @return mixed The set value
	 **/
	public function __set( $name, $value )
	{
		$classname = strtolower(get_class($this));
		$hook = 'set_' . $classname . '_' . $name;
		if(Plugins::implemented($hook, 'action')) {
			return Plugins::act('set_' . $classname . '_' . $name, $value, $this);
		}
		if ( isset( $this->properties_loaded[$name] ) ) {
			$this->newfields[$name] = $value;
		}
		else {
			$this->fields[$name] = $value;
			$this->properties_loaded[$name] = true;
		}
		return $value;
	}

	/**
	* Magic isset for QueryRecord, returns whether a property value is set.
	* @param string $name The name of the parameter
	* @return boolean True if the value is set, false if not
	*/
	public function __isset( $name )
	{
		return ( isset( $this->newfields[$name] ) || isset( $this->fields[$name] ) );
	}

	/**
	* Registers a (list of) fields(s) as being managed exclusively by the database.
	* @param mixed A database field name (string) or an array of field names
	*/
	public function exclude_fields( $fields )
	{
		if ( is_array( $fields ) ) {
			$this->unsetfields = array_flip( $fields );
		}
		else {
			$this->unsetfields[$fields] = $fields;
		}
	}

	/**
	* returns an array of fields that should not be included in any database insert operation
	* @return array an array of field names
	*/
	public function list_excluded_fields()
	{
		return $this->unsetfields;
	}

	/**
	 * This is the public interface that inserts a record
	 */
	public function insert()
	{
		return null;
	}

	/**
	 * function insertRecord(
	 * Inserts this record's fields as a new row
	 * @param string Table to update, use table name without prefix and without braces
	 * @return integer The inserted record id on success, false if not
	 *
	 * Again, the parent class's method's signature must match that of the
	 * child class's signature
	 */
	protected function insertRecord( $table, $schema = null )
	{
		$merge = array_merge( $this->fields, $this->newfields );
		$ptable = DB::table($table);
		if(empty($schema)) {
			$result = DB::insert( $ptable, array_diff_key( $merge, $this->unsetfields ) );
			if($result) {
				$result = DB::last_insert_id();
			}
		}
		else {
			$result = false;
			if(DB::insert( $ptable, array_intersect_key(array_diff_key( $merge, $this->unsetfields ), $schema[$table]) )) {
				$result = true;
				$record_id = DB::last_insert_id();
				$merge['*id'] = $record_id;
				foreach($schema as $schema_table => $fields) {
					if($schema_table == '*' || $table == $schema_table) {
						continue;
					}
					$data = array();
					foreach($fields as $field => $value) {
						$data[$field] = $merge[$value];
					}
					$pschema_table = DB::table($schema_table);
					$result = $result && DB::insert( $pschema_table, array_intersect_key(array_diff_key($data, $this->unsetfields), $fields));
				}
			}
			if($result) {
				$result = $record_id;
			}
		}
		return $result;
	}

	/**
	 * function to_array
	 * Returns an array with the current field settings
	 * @return array The field settings as they would be saved
	 */
	public function to_array()
	{
		return array_merge( $this->fields, $this->newfields );
	}

	/**
	 * Convert record data to json
	 * Returns a string with the current field values in JSON format
	 * @return string The field settings as they would be saved in JSON
	 */
	public function to_json()
	{
		return $this->jsonSerialize();
	}

	/**
	 * Implements JsonSerializable, only available in PHP 5.4  :(
	 * @return string
	 */
	public function jsonSerialize()
	{
		$array = array_merge( $this->fields, $this->newfields );
		$array = Plugins::filter('queryrecord_to_json', $array);
		return json_encode($array);
	}

	/**
	 * Returns an array with the current field settings
	 * @return array The field settings as they would be saved
	 */
	public function get_url_args()
	{
		return $this->to_array();
	}

	/**
	 * This is the public interface that updates a record
	 */
	public function update()
	{
		return null;
	}

	/**
	 * function updateRecord
	 * Updates this record's fields using the new data
	 * @param string Table to update, use table name without prefix and without braces
	 * @param array An associative array of field data to match
	 * @return boolean True on success, false if not
	 */
	protected function updateRecord( $table, $updatekeyfields = array(), $schema = null )
	{
		$merge = array_merge( $this->fields, $this->newfields );
		$ptable = DB::table($table);
		if(empty($schema)) {
			$result = DB::update( $ptable, array_diff_key( $merge, $this->unsetfields ), $updatekeyfields );
		}
		else {
			$result = false;
			if($result = DB::update( $ptable, array_intersect_key(array_diff_key( $merge, $this->unsetfields ), $schema[$table]), $updatekeyfields )) {
				foreach($updatekeyfields as $kf => $kd) {
					$merge['*'.$kf] = $kd;
				}
				foreach($schema as $schema_table => $fields) {
					if($schema_table == '*' || $table == $schema_table) {
						continue;
					}
					$data = array();
					$updatedata = array();
					foreach($fields as $field => $value) {
						if($value[0] == '*') {
							$updatedata[$field] = $merge[$value];
						}
						else {
							$data[$field] = $merge[$value];
						}
					}
					$pschema_table = DB::table($schema_table);
					$result = $result && DB::update( $pschema_table, array_intersect_key(array_diff_key($data, $this->unsetfields), $fields), $updatedata);
				}
			}
		}
		return $result;
	}

	/**
	 * This is the public interface that deletes a record
	 */
	public function delete()
	{
		return null;
	}

	/**
	 * function deleteRecord
	 * Deletes a record based on the match array
	 * @param string Table to delete from
	 * @param array An associative array of field data to match
	 * @return boolean True on success, false if not
	 */
	protected function deleteRecord( $table, $updatekeyfields )
	{
		return DB::delete( DB::table($table), $updatekeyfields );
	}

	/**
	 * This is the public interface to update a record with an array
	 */
	public function modify( $paramarray = array() )
	{
		$this->newfields = array_merge( $this->newfields, $paramarray );
	}
}

?>


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

/**
 * Habari RawPHPEngine class
 *
 *
 * The RawPHPEngine is a subclass of the abstract TemplateEngine class
 * which is intended for those theme designers who choose to use raw PHP
 * to design theme templates.
 */
class RawPHPEngine extends TemplateEngine
{
	// Internal data to be extracted into template symbol table
	protected $engine_vars = array();
	protected $available_templates = array();
	protected $template_map =array();
	protected $var_stack = array();
	protected $loaded_templates = false;

	/**
	 * Constructor for RawPHPEngine
	 *
	 * Sets up default values for required settings.
	 */
	public function __construct()
	{
		// Nothing to do here...
	}

	/**
	 * Tries to retrieve a variable from the internal array engine_vars.
	 * Method returns the value if succesful, returns false otherwise.
	 *
	 * @param key name of variable
	 */
	public function __get( $key )
	{
		return isset( $this->engine_vars[$key] ) ? $this->engine_vars[$key] : null;
	}

	/**
	 * Assigns a variable to the template engine for use in
	 * constructing the template's output.
	 *
	 * @param key name of variable
	 * @param value value of variable
	 */
	public function __set( $key, $value )
	{
		$this->engine_vars[$key] = $value;
	}

	/**
	 * Unassigns a variable to the template engine.
	 *
	 * @param key name of variable
	 */
	public function __unset( $key )
	{
		unset( $this->engine_vars[$key] );
	}

	/**
	 * 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->engine_vars[$key] );
	}

	/**
	 * A function which outputs the result of a transposed
	 * template to the output stream
	 *
	 * @param template  Name of template to display
	 */
	public function display( $template )
	{
		/**
		 * @todo  Here would be a good place to notify observers of output.
		 *        For instance, having sessions/headers output before
		 *        the template content...
		 */
		extract( $this->engine_vars );
		if ( $this->template_exists( $template ) ) {
			//$template_file= Plugins::filter( 'include_template_file', $this->template_dir . $template . '.php', $template, __CLASS__ );
			$template_file = isset( $this->template_map[$template] ) ? $this->template_map[$template] : null;
			$template_file = Plugins::filter( 'include_template_file', $template_file, $template, __CLASS__ );
			include ( $template_file );
		}
	}

	/**
	 * Returns the existance of the specified template name
	 *
	 * @param string $template Name of template to detect
	 * @returns boolean True if the template exists, false if not
	 */
	public function template_exists( $template )
	{
		if ( !$this->loaded_templates ) {
			if ( !is_array( $this->template_dir ) ) {
				$this->template_dir = array( $this->template_dir );
			}
			$alltemplates = array();
			$dirs = array_reverse( $this->template_dir );
			foreach ( $dirs as $dir ) {
				$templates = Utils::glob( $dir . '*.*' );
				$alltemplates = array_merge( $alltemplates, $templates );
			}
			$available_templates = array_map( 'basename', $alltemplates, array_fill( 1, count( $alltemplates ), '.php' ) );
			$template_map = array_combine( $available_templates, $alltemplates );
			$this->template_map = array_merge($this->template_map, $template_map);
			// Workaround for the 404 template key being merged into the 0 integer index
			unset($this->template_map[0]);
			if(isset($template_map[404])) {
				$this->template_map['404'] = $template_map[404];
			}
			array_unique( $available_templates );
			$available_templates = Plugins::filter( 'available_templates', $available_templates, __CLASS__ );
			$this->available_templates = array_merge($this->available_templates, $available_templates);
			$this->loaded_templates = true;
		}
		return in_array( $template, $this->available_templates );
	}

	/**
	 * A function which returns the content of the transposed
	 * template as a string
	 *
	 * @param string $template Name of template to fetch
	 */
	public function fetch( $template )
	{
		ob_start();
		$this->display( $template );
		$contents = ob_get_clean();
		return $contents;
	}

	/**
	 * Assigns a variable to the template engine for use in
	 * constructing the template's output.
	 *
	 * @param key name( s ) of variable
	 * @param value value of variable
	 */
	public function assign( $key, $value = '' )
	{
		$this->$key = $value;
	}

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

	/**
	 * Clear all of the assigned template variables
	 */
	public function clear()
	{
		$this->engine_vars = array();
	}

	/**
	 * Appends to an existing variable more values
	 *
	 * @param key name of variable
	 * @param value value of variable
	 */
	public function append( $key, $value = '' )
	{
		if ( ! isset( $this->engine_vars[$key] ) ) {
			$this->engine_vars[$key][] = $value;
		}
		else {
			$this->engine_vars[$key] = $value;
		}
	}

	public function add_template($name, $file, $replace = false)
	{
		if($replace || empty($this->available_templates[$name])) {
			$this->available_templates[] = $name;
			$this->template_map[$name] = $file;
		}
	}
}
?>


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

/**
 * Holds the basic RemoteRequest functionality.
 *
 * Interface for Request Processors. RemoteRequest uses a RequestProcessor to
 * do the actual work.
 *
 */
abstract class RequestProcessor
{

	protected $response_body = '';
	protected $response_headers = array();

	protected $executed = false;

	protected $can_followlocation = true;

	abstract public function execute( $method, $url, $headers, $body, $config );

	public function get_response_body ( ) {

		if ( !$this->executed ) {
			throw new Exception( _t( 'Unable to get response body. Request did not yet execute.' ) );
		}

		return $this->response_body;

	}
	public function get_response_headers ( ) {

		if ( !$this->executed ) {
			throw new Exception( _t( 'Unable to get response headers. Request did not yet execute.' ) );
		}

		return $this->response_headers;

	}
}

/**
 * Generic class to make outgoing HTTP requests.
 *
 */
class RemoteRequest
{
	private $method = 'GET';
	private $url;
	private $params = array();
	private $headers = array(
		'Accept' => '*/*',
	);
	private $postdata = array();
	private $files = array();
	private $body = '';
	private $processor = null;
	private $executed = false;

	private $response_body = '';
	private $response_headers = '';

	private $user_agent = 'Habari';

	// Array of adapter configuration options
	private $config = array(
		'connect_timeout'   => 30,
		'timeout'           => 180,
		'buffer_size'       => 16384,
		'follow_redirects'  => true,
		'max_redirects'     => 5,

		// These are configured in the main config file
		'proxy' => array(
			'server' => null,
			'port' => null,
			'username' => null,
			'password' => null,
			'auth_type' => 'basic',
			'exceptions' => array(),
			'type' => 'http',		// http is the default, 'socks' for a SOCKS5 proxy
		),

		// TODO: These don't apply to SocketRequestProcessor yet
		'ssl' => array(
			'verify_peer' => true,
			'verify_host' => 2,		// 1 = check CN of ssl cert, 2 = check and verify @see http://php.net/curl_setopt
			'cafile' => null,
			'capath' => null,
			'local_cert' => null,
			'passphrase' => null,

		),
	);

	/**
	 * @param string $url URL to request
	 * @param string $method Request method to use (default 'GET')
	 * @param int $timeuot Timeout in seconds (default 180)
	 */
	public function __construct( $url, $method = 'GET', $timeout = 180 )
	{
		$this->method = strtoupper( $method );
		$this->url = $url;
		$this->set_timeout( $timeout );

		// load the proxy configuration, if it exists
		$default = new stdClass();
		$proxy = Config::get( 'proxy', $default );
		if ( isset( $proxy->server ) ) {
			$this->set_config( array( 'proxy' => (array)$proxy ) );
		}

		// populate the default proxy exceptions list, since we can't up there
		$this->config['proxy']['exceptions'] = array_merge( $this->config['proxy']['exceptions'], array(
			'localhost',
			'127.0.0.1',
			'::1',		// ipv6 localhost
		) );

		// these next two could be duplicates of 'localhost' and 127.0.0.1 / ::1 if you're on localhost - that's ok
		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
			$this->config['proxy']['exceptions'][] = $_SERVER['SERVER_NAME'];
		}

		if ( isset( $_SERVER['SERVER_ADDR'] ) ) {
			$this->config['proxy']['exceptions'][] = $_SERVER['SERVER_ADDR'];
		}

		$this->user_agent .= '/' . Version::HABARI_VERSION;
		$this->add_header( array( 'User-Agent' => $this->user_agent ) );

		// if they've manually specified that we should not use curl, use sockets instead
		if ( Config::get( 'remote_request_processor' ) == 'socket' ) {
			$this->processor = new SocketRequestProcessor();
		}
		else {
			// otherwise, see if we can use curl and fall back to sockets if not
			if ( function_exists( 'curl_init' )
				&& ! ( ini_get( 'safe_mode' ) || ini_get( 'open_basedir' ) ) ) {
				$this->processor = new CURLRequestProcessor;
			}
			else {
				$this->processor = new SocketRequestProcessor;
			}

		}
	}

	/**
	 * DO NOT USE THIS FUNCTION.
	 * This function is only to be used by the test case for RemoteRequest!
	 */
	public function __set_processor( $processor )
	{
		$this->processor = $processor;
	}

	/**
	 * Set adapter configuration options
	 *
	 * @param mixed			$config An array of options or a string name with a	corresponding $value
	 * @param mixed			$value
	 */
	public function set_config( $config, $value = null )
	{
		if ( is_array( $config ) ) {
			foreach ( $config as $name => $value ) {
				$this->set_config( $name, $value );
			}
		}
		else {
			if ( is_array( $value ) ) {
				$this->config[ $config ] = array_merge( $this->config[ $config ], $value );
			}
			else {
				$this->config[ $config ] = $value;
			}
		}
	}

	/**
	 * Add a request header.
	 * @param mixed $header The header to add, either as a string 'Name: Value' or an associative array 'name'=>'value'
	 */
	public function add_header( $header )
	{
		if ( is_array( $header ) ) {
			$this->headers = array_merge( $this->headers, $header );
		}
		else {
			list( $k, $v )= explode( ': ', $header );
			$this->headers[$k] = $v;
		}
	}

	/**
	 * Add a list of headers.
	 * @param array $headers List of headers to add.
	 */
	public function add_headers( $headers )
	{
		foreach ( $headers as $header ) {
			$this->add_header( $header );
		}
	}

	/**
	 * Set the request body.
	 * Only used with POST requests, will raise a warning if used with GET.
	 * @param string $body The request body.
	 */
	public function set_body( $body )
	{
		if ( $this->method == 'GET' ) {
			throw new Exception( _t( 'Trying to add a request body to a GET request.' ) );
		}

		$this->body = $body;
	}

	/**
	 * Set the request query parameters (i.e., the URI's query string).
	 * Will be merged with existing query info from the URL.
	 * @param array $params
	 */
	public function set_params( $params )
	{
		if ( ! is_array( $params ) )
			$params = parse_str( $params );

		$this->params = $params;
	}

	/**
	 * Set the timeout.
	 * @param int $timeout Timeout in seconds
	 */
	public function set_timeout( $timeout )
	{
		$this->config['timeout'] = $timeout;
		return $this->config['timeout'];
	}

	/**
	 * set postdata
	 *
	 * @access public
	 * @param mixed $name
	 * @param string $value
	 */
	public function set_postdata( $name, $value = null )
	{
		if ( is_array( $name ) ) {
			$this->postdata = array_merge( $this->postdata, $name );
		}
		else {
			$this->postdata[$name] = $value;
		}
	}

	/**
	 * set file
	 *
	 * @access public
	 * @param string $name
	 * @param string $filename
	 * @param string $content_type
	 */
	public function set_file( $name, $filename, $content_type = null, $override_filename = null )
	{
		if ( !file_exists( $filename ) ) {
			throw new Exception( _t( 'File %s not found.', array( $filename ) ) );
		}
		if ( empty( $content_type ) ) $content_type = 'application/octet-stream';
		$this->files[$name] = array( 'filename' => $filename, 'content_type' => $content_type, 'override_filename' => $override_filename );
		$this->headers['Content-Type'] = 'multipart/form-data';
	}

	/**
	 * A little housekeeping.
	 */
	private function prepare()
	{
		// remove anchors (#foo) from the URL
		$this->url = $this->strip_anchors( $this->url );
		// merge query params from the URL with params given
		$this->url = $this->merge_query_params( $this->url, $this->params );

		if ( $this->method === 'POST' ) {
			if ( !isset( $this->headers['Content-Type'] ) || ( $this->headers['Content-Type'] == 'application/x-www-form-urlencoded' ) ) {
				// TODO should raise a warning
				$this->add_header( array( 'Content-Type' => 'application/x-www-form-urlencoded' ) );

				if ( $this->body != '' && count( $this->postdata ) > 0 ) {
					$this->body .= '&';
				}
				$this->body .= http_build_query( $this->postdata, '', '&' );
			}
			elseif ( $this->headers['Content-Type'] == 'multipart/form-data' ) {
				$boundary = md5( Utils::nonce() );
				$this->headers['Content-Type'] .= '; boundary=' . $boundary;

				$parts = array();
				if ( $this->postdata && is_array( $this->postdata ) ) {
					reset( $this->postdata );
					while ( list( $name, $value ) = each( $this->postdata ) ) {
						$parts[] = "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n{$value}\r\n";
					}
				}

				if ( $this->files && is_array( $this->files ) ) {
					reset( $this->files );
					while ( list( $name, $fileinfo ) = each( $this->files ) ) {
						$filename = basename( $fileinfo['filename'] );
						if ( !empty( $fileinfo['override_filename'] ) ) {
							$filename = $fileinfo['override_filename'];
						}
						$part = "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$filename}\"\r\n";
						$part .= "Content-Type: {$fileinfo['content_type']}\r\n\r\n";
						$part .= file_get_contents( $fileinfo['filename'] ) . "\r\n";
						$parts[] = $part;
					}
				}

				if ( !empty( $parts ) ) {
					$this->body = "--{$boundary}\r\n" . join( "--{$boundary}\r\n", $parts ) . "--{$boundary}--\r\n";
				}
			}
			$this->add_header( array( 'Content-Length' => strlen( $this->body ) ) );
		}
	}

	/**
	 * Actually execute the request.
	 * On success, returns true and populates the response_body and response_headers fields.
	 * On failure, throws Exception.
	 *
	 * @throws Exception
	 */
	public function execute()
	{
		$this->prepare();
		$result = $this->processor->execute( $this->method, $this->url, $this->headers, $this->body, $this->config );

		if ( $result ) { // XXX exceptions?
			$this->response_headers = $this->processor->get_response_headers();
			$this->response_body = $this->processor->get_response_body();
			$this->executed = true;

			return true;
		}
		else {
			// processor->execute should throw an Exception which would bubble up
			$this->executed = false;

			return $result;
		}
	}

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

	/**
	 * Return the response headers. Raises a warning and returns '' if the request wasn't executed yet.
	 * @todo This should probably just call the selected processor's method, which throws its own error.
	 */
	public function get_response_headers()
	{
		if ( !$this->executed ) {
			throw new Exception( _t( 'Unable to fetch response headers for a pending request.' ) );
		}

		return $this->response_headers;
	}

	/**
	 * Return the response body. Raises a warning and returns '' if the request wasn't executed yet.
	 * @todo This should probably just call the selected processor's method, which throws its own error.
	 */
	public function get_response_body()
	{
		if ( !$this->executed ) {
			throw new Exception( _t( 'Unable to fetch response body for a pending request.' ) );
		}

		return $this->response_body;
	}

	/**
	 * Remove anchors (#foo) from given URL.
	 */
	private function strip_anchors( $url )
	{
		return preg_replace( '/(#.*?)?$/', '', $url );
	}

	/**
	 * Call the filter hook.
	 */
	private function __filter( $data, $url )
	{
		return Plugins::filter( 'remoterequest', $data, $url );
	}

	/**
	 * Merge query params from the URL with given params.
	 * @param string $url The URL
	 * @param string $params An associative array of parameters.
	 */
	private function merge_query_params( $url, $params )
	{
		$urlparts = InputFilter::parse_url( $url );

		if ( ! isset( $urlparts['query'] ) ) {
			$urlparts['query'] = '';
		}

		if ( ! is_array( $params ) ) {
			parse_str( $params, $params );
		}

		$urlparts['query'] = http_build_query( array_merge( Utils::get_params( $urlparts['query'] ), $params ), '', '&' );

		return InputFilter::glue_url( $urlparts );
	}

	/**
	 * Static helper function to quickly fetch an URL, with semantics similar to
	 * PHP's file_get_contents. Does not support
	 *
	 * Returns the content on success or false if an error occurred.
	 *
	 * @param string $url The URL to fetch
	 * @param bool $use_include_path whether to search the PHP include path first (unsupported)
	 * @param resource $context a stream context to use (unsupported)
	 * @param int $offset how many bytes to skip from the beginning of the result
	 * @param int $maxlen how many bytes to return
	 * @return string description
	 */
	public static function get_contents( $url, $use_include_path = false, $context = null, $offset = 0, $maxlen = -1 )
	{
		try {
			$rr = new RemoteRequest( $url );
			if ( $rr->execute() === true ) {
				return ( $maxlen != -1
					? MultiByte::substr( $rr->get_response_body(), $offset, $maxlen )
					: MultiByte::substr( $rr->get_response_body(), $offset ) );
			}
			else {
				return false;
			}
		}
		catch ( Exception $e ) {
			// catch any exceptions to try and emulate file_get_contents() as closely as possible.
			// if you want more control over the errors, instantiate RemoteRequest manually
			return false;
		}
	}

}

class RemoteRequest_Timeout extends Exception { }

?>


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

/**
 * Habari RewriteRule Class
 *
 * Helper class to encapsulate rewrite rule data
 *
 */
class RewriteRule extends QueryRecord
{

	const RULE_SYSTEM = 0;
	const RULE_THEME = 1;
	const RULE_PLUGIN = 2;
	const RULE_CUSTOM = 5;

	public $entire_match = null; // Entire matched string from the URL
	public $named_arg_values = array(); // Values of named arguments filled during URL::parse()
	private $m_named_args = null; // Named arguments matches

	/**
	 * Returns the defined database columns for a rewrite rule.
	 * @return array Array of columns in the rewrite_rules table
	 */
	public static function default_fields()
	{
		return array(
			'rule_id' => 0,
			'name' => '',
			'parse_regex' => '/.^/',
			'build_str' => '',
			'handler' => '',
			'action' => '',
			'priority' => 0,
			'is_active' => 0,
			'rule_class' => RewriteRule::RULE_CUSTOM,
			'description' => '',
			'parameters' => '',
		);
	}

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

		parent::__construct( $paramarray );
		$this->exclude_fields( 'rule_id' );
	}

	/**
	 * Match the stub against this rule
	 * Also sets internal structures based on a successful match
	 * @param string The URL stub to match against
	 * @return boolean True if this rule matches the stub, false if not
	 */
	public function match( $stub )
	{
		if ( preg_match( $this->parse_regex, $stub, $pattern_matches ) > 0 ) {
			$this->entire_match = array_shift( $pattern_matches ); // The entire matched string is returned at index 0
			$named_args = $this->named_args; // Direct call shows a PHP notice

			if ( (is_string($this->parameters) && $parameters = unserialize( $this->parameters )) || (is_array($this->parameters) && $parameters = $this->parameters )) {
				$this->named_arg_values = array_merge( $this->named_arg_values, $parameters );
			}

			foreach ( $named_args as $keys ) {
				foreach ( $keys as $key ) {
					if ( !empty( $pattern_matches[$key] ) ) {
						$this->named_arg_values[$key] = urldecode( str_replace( '%252F', '%2F', $pattern_matches[$key] ) );
					}
				}
			}

			if ( preg_match( '/^\\{\\$(\\w+)\\}$/u', $this->action, $matches ) > 0 ) {
				$this->action = $this->named_arg_values[$matches[1]];
			}

			if ( isset( $parameters['require_match'] ) ) {
				return call_user_func( $parameters['require_match'], $this, $stub, $parameters );
			}

			return true;
		}
		return false;
	}

	/**
	 * Builds a URL using this rule based on the passed in data
	 * @param array $args An associative array of arguments to use for replacement in the rule
	 * @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
	 * @return string The URL created from the substituted arguments
	 */
	public function build( $args, $useall = true, $noamp = false )
	{
		$named_args = $this->named_args; // Direct call prints a PHP notice
		$named_args_combined = array_flip( array_merge( $named_args['required'], $named_args['optional'] ) );

		$args_defined = array_intersect_key( $args, $named_args_combined );
		$args = Plugins::filter( 'rewrite_args', $args, $this->name );
		// Replace defined arguments with their value
		$searches = array();
		$replacements = array();
		foreach ( $named_args as $keys ) {
			foreach ( $keys as $key ) {
				if ( !empty( $args[$key] ) ) {
					$searches[] = '/{\$'.$key.'}/';
					$replacements[] = str_replace( '%2F', '%252F', urlencode( $args[$key] ) );
				}
			}
		}

		// Remove undefined arguments
		$searches[] = '/\([^\(\)]*\$+[^\(\)]*\)/';
		$replacements[] = '';
		// Remove parens left from defined optional arguments
		$searches[] = '/\(|\)/';
		$replacements[] = '';

		$return_url = preg_replace( $searches, $replacements, $this->build_str );

		// Append any remaining args as query string arguments
		if ( $useall ) {
			$args = array_diff_key( $args, $named_args_combined );
			$query_seperator = ( $noamp ) ? '&' : '&';
			$return_url.= ( count( $args ) == 0 ) ? '' : '?' . http_build_query( $args, '', $query_seperator );
		}

		return $return_url;
	}

	/**
	 * Returns a distance from 0 indicating the appropriateness of the rule
	 * based on the passed-in arguments.
	 * @param array $args An array of arguments
	 * @return integer Returns 0 for an exact match, a higher number for less of a match
	 * @todo Enable this logic
	 */
	public function arg_match( $args )
	{
		return 0; // Let's let this logic linger for a little while

		/* This needs further testing once that logic is established */
		$named_args = $this->named_args; // Direct call prints a PHP notice
		$named_args_combined = array_flip( array_merge( $named_args['required'], $named_args['optional'] ) );

		$args = Plugins::filter( 'rewrite_args', $args, $this->name );

		$diffargs = array_diff_key( $args, $named_args_combined );
		$sameargs = array_intersect_key( $args, $named_args_combined );
		$rating = count( $named_args_combined ) - count( $sameargs ) + count( $diffargs );

		return $rating;
	}

	/**
	 * Magic property getter for this class
	 * @param string $name The name of the class property to return
	 * @returns mixed The value of that field in this object
	 */
	public function __get( $name )
	{
		switch ( $name ) {
			case 'named_args':
				if ( empty( $this->m_named_args ) ) {
					preg_match_all( '/(?<!\()\{\$(\w+?)\}(?!\))/u', $this->build_str, $required );
					preg_match_all( '/(?<=\()[^\(\)]*\{\$(\\w+?)\}[^\(\)]*(?=\))/u', $this->build_str, $optional );
					$this->m_named_args['required'] = $required[1];
					$this->m_named_args['optional'] = $optional[1];
				}
				return $this->m_named_args;
			default:
				return parent::__get( $name );
		}
	}

	/**
	 * Saves a new rewrite rule to the rewrite_rules table
	 */
	public function insert()
	{
		return parent::insertRecord( DB::table( 'rewrite_rules' ) );
	}

	/**
	 * Updates an existing rule in the rewrite_rules table
	 */
	public function update()
	{
		return parent::updateRecord( DB::table( 'rewrite_rules' ), array( 'rule_id' => $this->rule_id ) );
	}

	/**
	 * Deletes an existing rule
	 */
	public function delete()
	{
		return parent::deleteRecord( DB::table( 'rewrite_rules' ), array( 'rule_id' => $this->rule_id ) );
	}

	/**
	 * Create an old-style rewrite rule
	 * @param string $build_str
	 * @param string $handler
	 * @param string $action
	 * @return RewriteRule The created rule
	 */
	public static function create_url_rule( $build_str, $handler, $action )
	{
		$arr = explode( '/', $build_str );

		$searches = array( '/^([^"\']+)$/', '/^["\'](.+)["\']$/' );
		$replacements = array( '(?P<\1>.+)', '\1' );
		$re_arr = preg_replace( $searches, $replacements, $arr );

		$searches = array( '/^([^"\']+)$/', '/^["\'](.+)["\']$/' );
		$replacements = array( '{$\1}', '\1' );
		$str_arr = preg_replace( $searches, $replacements, $arr );

		$regex = '/^' . implode( '\/', $re_arr ) . '\/?$/i';
		$build_str = implode( '/', $str_arr );

		return new RewriteRule( array(
			'name' => $action,
			'parse_regex' => $regex,
			'build_str' => $build_str,
			'handler' => $handler,
			'action' => $action,
			'priority' => 1,
			'is_active' => 1,
			'rule_class' => RewriteRule::RULE_CUSTOM,
			'description' => 'Custom old-style rule.',
		) );
	}

}
?>