code:
<?php

/**
 * Vocabulary Class
 *
 * Vocabulary is part of the taxonomy system. A vocabulary holds terms and has features.
 *
 */

class Vocabulary extends QueryRecord
{
	/**
	 *
	 * @var Array of strings $features. An array of the features that limit the behaviour of the vocabulary.
	 * Default values can include:
	 *  hierarchical: The vocabulary's terms exist in a parent child hierarchy
	 *  required:
	 *  multiple: More than one term in the vocabulary can be associated with an object
	 *  free: Terms within the vocabulary can have any value
	 *  unique: A term may be associated with exactly 0 or 1 object
	 */
	public static $features = array( 'hierarchical', 'required', 'multiple', 'free', 'unique' );

	/**
	 * Return the defined database columns for a Vocabulary.
	 * @return array Array of columns in the Vocabulary table
	 **/
	public static function default_fields()
	{
		return array(
			'id' => 0,
			'name' => '',
			'description' => '',
			'features' => array(),
		);
	}

	/**
	 * Vocabulary constructor
	 * Creates a Vocabulary instance
	 *
	 * @param array $paramarray an associative array of initial vocabulary values
	 **/
	public function __construct( $paramarray = array() )
	{
		// Defaults
		$this->fields = array_merge(
			self::default_fields(),
			$this->fields
		);

		parent::__construct( $paramarray );

		$this->exclude_fields( 'id' );
		if ( is_string( $this->features ) ) {
			$this->features = unserialize( $this->features );
		}
	}

	/**
	 * Create a vocabulary and save it.
	 *
	 * @param array $paramarray An associative array of vocabulary fields
	 * @return Vocabulary The new vocabulary object
	 */
	static function create( $paramarray )
	{
		$vocabulary = new Vocabulary( $paramarray );
		$vocabulary->insert();
		return $vocabulary;
	}

	/**
	 * function __get
	 * Overrides QueryRecord __get to implement custom object properties
	 * @param string Name of property to return
	 * @return mixed The requested field value
	 **/
	public function __get( $name )
	{
		$out = parent::__get( $name );
		switch ( $name ) {
			case 'features':
				if ( ! is_array( $out ) ) {
					$out = unserialize( $out );
				}
				break;
		}
		if ( is_null( $out ) ) {
			$out = in_array( $name, $this->features );
		}
		return $out;
	}

	/**
	 * Return a Vocabulary by name.
	 * @return Vocabulary The requested vocabulary
	 **/
	public static function get( $name )
	{
		$query = Query::create('{vocabularies}')->select('*');
		$query->where()->add('name = :name', array('name' => $name));
		return $query->row('Vocabulary');
	}

	/**
	 * Return a Vocabulary by id
	 *
	 * @param integer $id The id of the vocabulary
	 * @return Vocabulary The object requested
	 */
	public static function get_by_id( $id )
	{
		$query = Query::create('{vocabularies}')->select('*');
		$query->where()->add('id = :id', array('id' => $id));
		return $query->row('Vocabulary');
	}

	/**
	 * Return all vocabularies as Vocabulary objects
	 *
	 * @return array An array of Vocabulary objects
	 */
	public static function get_all()
	{
		$query = Query::create('{vocabularies}')->select('*');
		return $query->results('Vocabulary');
	}

	/**
	 * Rename a Vocabulary.
	 * @return boolean true if the Vocabulary was renamed, false otherwise
	 **/
	public function rename( $newname )
	{
		$this->name = $newname;
		$result = $this->update();

		return $result;
	}

	/**
	 * Return the names of all vocabularies
	 * @return array Array of Vocabulary names
	 **/
	public static function names()
	{
		$query = Query::create('{vocabularies}')->select('name');
		return $query->column();
	}

	/**
	 * Return the Term objects associated to that type of object with that id in any vocabulary.
	 * For example, return all terms associated with a particular post, from all vocabularies.
	 *
	 * @return array Array of Vocabulary names
	 **/
	public static function get_all_object_terms( $object_type, $id )
	{
		$query = Query::create('{terms}')->select('id', 'term', 'term_display', 'vocabulary_id', 'mptt_left', 'mptt_right');
		$query->join('JOIN {object_terms} ON {terms}.id = {object_terms}.term_id', array(), 'object_terms');
		$query->where()->in('{object_terms}.object_type_id', self::object_type_id( $object_type ));
		$query->where()->in('{object_terms}.object_id', $id);
		return new Terms($query->results('Term'));
	}

	/**
	 * Determine whether a vocabulary exists
	 * @param string $name a vocabulary name
	 * @return bool whether the vocabulary exists or not
	**/
	public static function exists( $name )
	{
		$query = Query::create('{vocabularies}')->select('count(id)');
		$query->where()->add('name = :name', array('name' => $name));
		return ((int) $query->value()) > 0;
	}

	/**
	 * function insert
	 * Saves a new vocabulary to the vocabularies table
	 */
	public function insert()
	{
		// Don't allow duplicate vocabularies
		if ( Vocabulary::exists( $this->fields['name'] ) ) {
			return false;
		}

		// Let plugins disallow and act before we write to the database
		$allow = true;
		$allow = Plugins::filter( 'vocabulary_insert_allow', $allow, $this );
		if ( !$allow ) {
			return false;
		}
		Plugins::act( 'vocabulary_insert_before', $this );

		// Serialize features before they are stored
		if ( isset( $this->newfields['features'] ) ) {
			$this->newfields['features'] = serialize( $this->newfields['features'] );
		}
		if ( isset( $this->fields['features'] ) ) {
			$this->fields['features'] = serialize( $this->fields['features'] );
		}
		$result = parent::insertRecord( DB::table( 'vocabularies' ) );

		// Make sure the id is set in the vocabulary object to match the row id
		$this->newfields['id'] = DB::last_insert_id();

		// Update the vocabulary's fields with anything that changed
		$this->fields = array_merge( $this->fields, $this->newfields );
		// And unserialize the features
		if ( isset( $this->fields['features'] ) ) {
			$this->fields['features'] = unserialize( $this->fields['features'] );
		}

		// We've inserted the vocabulary, reset newfields
		$this->newfields = array();

		EventLog::log( _t( 'New vocabulary %1$s (%2$s)', array( $this->id, $this->name ) ), 'info', 'content', 'habari' );

		// Let plugins act after we write to the database
		Plugins::act( 'vocabulary_insert_after', $this );

		return $result;
	}

	/**
	 * function update
	 * Updates an existing vocabulary in the vocabularies table
	 */
	public function update()
	{
		// Don't allow duplicate vocabularies
		if ( isset( $this->newfields['name'] ) && Vocabulary::exists( $this->newfields['name'] ) ) {
			return false;
		}

		// Let plugins disallow and act before we write to the database
		$allow = true;
		$allow = Plugins::filter( 'vocabulary_update_allow', $allow, $this );
		if ( !$allow ) {
			return;
		}
		Plugins::act( 'vocabulary_update_before', $this );

		if ( isset( $this->newfields['features'] ) ) {
			$this->newfields['features'] = serialize( $this->newfields['features'] );
		}
		if ( isset( $this->fields['features'] ) ) {
			$this->fields['features'] = serialize( $this->fields['features'] );
		}
		$result = parent::updateRecord( '{vocabularies}', array( 'id' => $this->id ) );

		$this->fields = array_merge( $this->fields, $this->newfields );
		$this->newfields = array();

		if ( isset( $this->fields['features'] ) ) {
			$this->fields['features'] = unserialize( $this->fields['features'] );
		}

		// Let plugins act after we write to the database
		Plugins::act( 'vocabulary_update_after', $this );

		return $result;
	}

	/**
	 * Delete an existing vocabulary
	 */
	public function delete()
	{
		// Let plugins disallow and act before we write to the database
		$allow = true;
		$allow = Plugins::filter( 'vocabulary_delete_allow', $allow, $this );
		if ( !$allow ) {
			return;
		}
		Plugins::act( 'vocabulary_delete_before', $this );

		// Get the ids for all this vocabulary's terms
		$ids = DB::get_column( 'SELECT id FROM {terms} WHERE vocabulary_id = ?', array( $this->id ) );

		// Delete the records from object_terms for those ids (if there were any)
		if ( count( $ids ) ) {
			$placeholder = Utils::placeholder_string( count( $ids ) );
			DB::query( "DELETE FROM {object_terms} WHERE term_id IN ($placeholder)", $ids );
		}

		// Delete this vocabulary's terms
		DB::delete( '{terms}', array( 'vocabulary_id' => $this->id ) );

		// Finally, delete the vocabulary
		$result = parent::deleteRecord( '{vocabularies}', array( 'id'=>$this->id ) );
		EventLog::log( _t( 'Vocabulary %1$s (%2$s) deleted.', array( $this->id, $this->name ) ), 'info', 'content', 'habari' );

		// Let plugins act after we write to the database
		Plugins::act( 'vocabulary_delete_after', $this );
		return $result;
	}

	/**
	 * Adds a term to the vocabulary. Returns a Term object. null parameters append the term to the end of any hierarchies.
	 * @return Term The Term object added
	 **/
	public function add_term( $term, $target_term = null, $before = false )
	{
		$new_term = $term;
		if ( is_string( $term ) ) {
			$new_term = new Term( array( 'term_display' => $term ) );
		}

		$new_term->vocabulary_id = $this->id;

		$ref = 0;
		DB::begin_transaction();

		// If there are terms in the vocabulary, work out the reference point
		if ( !$this->is_empty() ) {

			if ( $this->hierarchical ) {
				// If no parent is specified, put the new term after the last term
				if ( null == $target_term ) {
					$ref = DB::get_value( 'SELECT mptt_right FROM {terms} WHERE vocabulary_id=? ORDER BY mptt_right DESC LIMIT 1', array( $this->id ) );
				}
				else {
					if ( ! $before ) {
						$ref = $target_term->mptt_right - 1;
					}
					else {
						$ref = $target_term->mptt_left - 1;
					}
				}
			}
			else {
				// If no before_term is specified, put the new term after the last term
				if ( ! $before ) {
					$ref = DB::get_value( 'SELECT mptt_right FROM {terms} WHERE vocabulary_id=? ORDER BY mptt_right DESC LIMIT 1', array( $this->id ) );
				}
				else {
					$ref = $target_term->mptt_left - 1;
				}
			}

			// Make space for the new node
			$params = array( 'vocab_id' => $this->id, 'ref' => $ref );
			$res = DB::query( 'UPDATE {terms} SET mptt_right=mptt_right+2 WHERE vocabulary_id=:vocab_id AND mptt_right>:ref', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}
			$res = DB::query( 'UPDATE {terms} SET mptt_left=mptt_left+2 WHERE vocabulary_id=:vocab_id AND mptt_left>:ref', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}

		}

		// Set the right and left appropriately
		$new_term->mptt_left = $ref + 1;
		$new_term->mptt_right = $ref + 2;

		// Insert the new node
		$result = $new_term->insert();
		if ( $result ) {
			DB::commit();
			return $new_term;
		}
		else {
			DB::rollback();
			return false;
		}

	}

	/**
	 * Gets the term object by id. No parameter returns the root Term object.
	 * @param mixed $term A Term object, null (for the first node in the tree), a string (for a term slug or display), or an integer (for a Term ID).
	 * @param string $term_class The class of the returned term object.
	 * @return Term The Term object requested
	 * @todo improve selective fetching by term slug vs term_display
	 **/
	public function get_term( $term = null, $term_class = 'Term' )
	{
		$params = array( 'vocab_id' => $this->id );
		$query = '';
		if ( $term instanceof Term ) {
			$params[ 'term_id' ] = $term->id;
			$query = 'SELECT * FROM {terms} WHERE vocabulary_id = :vocab_id AND id = ABS(:term_id)';
		}
		elseif ( is_null( $term )  ) {
			// The root node has an mptt_left value of 1
			$params[ 'left' ] = 1;
			$query = 'SELECT * FROM {terms} WHERE vocabulary_id = :vocab_id AND mptt_left = :left';
		}
		elseif ( is_string( $term ) ) {
			$params[ 'term' ] = $term;
			$query = 'SELECT * FROM {terms} WHERE vocabulary_id = :vocab_id AND (term = :term OR term_display = :term)';
		}
		elseif ( is_int( $term ) ) {
			$params[ 'term_id' ] = $term;
			$query = 'SELECT * FROM {terms} WHERE vocabulary_id = :vocab_id AND id = ABS(:term_id)';
		}
		return DB::get_row( $query, $params, $term_class );
	}

	/**
	 * Gets the Term objects associated to that type of object with that id in this vocabulary
	 * For example, return all terms in this vocabulary that are associated with a particular post
	 *
	 * @param String the name of the object type
	 * @param integer The id of the object for which you want the terms
	 * @return Array The Term objects requested
	 **/
	public function get_object_terms( $object_type, $id )
	{
		$query = Query::create('{terms}')->select('id', 'term', 'term_display', 'vocabulary_id', 'mptt_left', 'mptt_right');
		$query->join('JOIN {object_terms} ON {terms}.id = {object_terms}.term_id', array(), 'object_terms');
		$query->where()->in('{terms}.vocabulary_id', $this->id);
		$query->where()->in('{object_terms}.object_type_id', self::object_type_id( $object_type ));
		$query->where()->in('{object_terms}.object_id', $id);
		return new Terms($query->results('Term'));
	}

	/**
	 * Sets the Term objects associated to that type of object with that id in this vocabulary
	 *
	 * @param String $object_type the name of the object type
	 * @param Integer $id The id of the object for which you want the terms
	 * @param Array $terms The names of the terms to associate
	 *
	 * @return boolean. Whether the associations were successful or not
	 **/
	public function set_object_terms( $object_type, $id, $terms )
	{
		if ( ! isset( $object_type ) || ! isset( $id ) ) {
			return false;
		}

		if ( ! isset( $terms ) || empty( $terms ) ) {
			$terms = array();
//			DB::query( "DELETE FROM {object_terms} WHERE object_id = :object_id AND object_type_id = :type_id", array( 'object_id' => $id, 'type_id' => Vocabulary::get_object_id( $object_type ) ) );
//			return true;
		}

		// Make sure we have an array
		$terms = new Terms( $terms );
		$new_terms = array();

		// Make sure we have terms and they're in the database.
		// Key the terms to their id while we're at it.
		foreach ( $terms as $term ) {
			$new_term = $this->get_term( $term );
			if ( ! $new_term instanceof Term ) {
				$new_term = $this->add_term( $term );
			}
			if ( ! array_key_exists( $new_term->id, $new_terms ) ) {
				$new_terms[$new_term->id] = $new_term;
			}
		}

		// Get the current terms
		$old_terms = $this->get_object_terms( $object_type, $id );
		$keys = array_keys( $new_terms );
		foreach ( $old_terms as $term ) {
			// If the old term isn't in the new terms, dissociate it from the object
			if ( ! in_array( $term->id, $keys ) ) {
				$term->dissociate( $object_type, $id );
			}
		}

		// Associate the new terms
		foreach ( $new_terms as $term ) {
			$term->associate( $object_type, $id );
		}

		return true;
	}

	/**
	 * Remove the term from the vocabulary.  Convenience method to ->get_term('foo')->delete().
	 *
	 **/
	public function delete_term( $term )
	{
		if ( ! is_object( $term ) ) {
			$term = $this->get_term( $term );
		}

		// TODO How should we handle deletion of a term with descendants?
		// Perhaps a $keep_children flag to move descendants to be descendants of
		// the deleted term's parent? Terms should not change the left and right
		// values of other terms, and thus their deletion should only occur through
		// the vocabulary to which they belong. Is it feasible to restrict this?
		// For the moment, just delete the descendants
		$params = array( $this->id, $term->mptt_left, $term->mptt_right );
		DB::query( 'DELETE from {terms} WHERE vocabulary_id=? AND mptt_left>? AND mptt_right<?', $params );

		// Fix mptt_left and mptt_right values for other nodes in the vocabulary
		$offset = $term->mptt_right - $term->mptt_left + 1;
		$ref = $this->mptt_left;
		$params = array( $offset, $this->id, $term->mptt_left );

		// Delete the term
		$term->delete();

		// Renumber left and right values of other nodes appropriately
		DB::query( 'UPDATE {terms} SET mptt_right=mptt_right-? WHERE vocabulary_id=? AND mptt_right>?', $params );
		DB::query( 'UPDATE {terms} SET mptt_left=mptt_left-? WHERE vocabulary_id=? AND mptt_left>?', $params );

	}

	/**
	 * Check if this vocabulary is empty.
	 *
	 **/
	public function is_empty()
	{
		$query = Query::create('{terms}')->select('count(id)');
		$query->where()->in('vocabulary_id', $this->id);
		return ((int) $query->value()) == 0;
	}

	/**
	 * Retrieve the vocabulary
	 * @return Terms The Term objects in the vocabulary, in tree order
	 **/
	public function get_tree( $orderby = 'mptt_left ASC' )
	{
		$query = Query::create('{terms}')->select('{terms}.*');
		$query->where()->in('vocabulary_id', $this->id);
		// If the vocabulary is unique, save the extra mess of queries and fetch the object data, too
		if(in_array('unique', $this->features)) {
			$query->join('LEFT JOIN {object_terms} on {object_terms}.term_id = {terms}.id', array(), 'object_terms');
			$query->join('LEFT JOIN {object_types} on {object_types}.id = {object_terms}.object_type_id', array(), 'object_types');
			$query->select('{object_terms}.object_id');
			$query->select('{object_types}.name AS type');
		}
		$query->orderby($orderby);
		return new Terms($query->results('Term'));
	}

	/**
	 * Retrieve the terms in the vocabulary that match the specified criteria
	 * @param string $search The string to search for
	 * @return Terms The Term objects that match the search term
	 */
	public function get_search( $search, $orderby = 'mptt_left ASC' )
	{
		$search = '%' . $search . '%';
		return new Terms( DB::get_results( "SELECT * FROM {terms} WHERE vocabulary_id = :vid and LOWER(term_display) LIKE LOWER(:crit) ORDER BY {$orderby}", array( 'vid' => $this->id, 'crit' => $search ), 'Term' ) );
	}

	/**
	 * Retrieve the vocabulary as an associative array suitable for FormUI select controls
	 * @return Array The Term objects in the vocabulary, in tree order
	 **/
	public function get_options()
	{
		$tree = $this->get_tree( 'mptt_left ASC' );
		$output = array();
		if ( $firstnode = reset( $tree ) ) {
			$lastright = $lastleft = reset( $tree )->mptt_left;
			$indent = 0;
			$stack = array();
			foreach ( $tree as $term ) {
				while ( count( $stack ) > 0 && end( $stack )->mptt_right < $term->mptt_left ) {
					array_pop( $stack );
				}
				$output[$term->id] = str_repeat( '- ', count( $stack ) ) . $term->term_display;
				$stack[] = $term;
			}
		}

		return $output;
	}

	/**
	 * Get all root elements in this vocabulary
	 * @return Array The root Term objects in the vocabulary
	 */
	public function get_root_terms()
	{
		/**
		 * If we INNER JOIN the terms table with itself on ALL the descendants,
		 * then descendants one level down are listed once, two levels down are listed twice,
		 * etc. If we return only those terms which appear once, we get root elements.
		 * ORDER BY NULL to avoid the MySQL filesort.
		 */
		$query = <<<SQL
SELECT child.term as term,
	child.term_display as term_display,
	child.mptt_left as mptt_left,
	child.mptt_right as mptt_right,
	child.vocabulary_id as vocabulary_id,
	child.id as id
FROM {terms} as parent
INNER JOIN {terms} as child
	ON child.mptt_left BETWEEN parent.mptt_left AND parent.mptt_right
	AND child.vocabulary_id = parent.vocabulary_id
WHERE parent.vocabulary_id = ?
GROUP BY child.term
HAVING COUNT(child.term)=1
ORDER BY mptt_left ASC
SQL;
		return new Terms(DB::get_results( $query, array( $this->id ), 'Term' ));
	}

	/**
	 * inserts a new object type into the database, if it doesn't exist
	 * @param string The name of the new post type
	 * @param bool Whether the new post type is active or not
	 * @return none
	**/
	public static function add_object_type( $type )
	{
		$params = array( 'name' => $type );
		if ( ! DB::exists( "{object_types}", $params ) ) {
			DB::insert( "{object_types}", $params );
		}
	}

	/**
	 * Return the object type id for a named object, such as a post
	 *
	 * @param string $name The type of object
	 * @return integer The id of the object type
	*/
	public static function object_type_id( $type )
	{
		$id = (int)DB::get_value( "SELECT id FROM {object_types} WHERE name = ?", array( $type ) );
		return $id;

	}

	/**
	 * Moves a term within the vocabulary. Returns a Term object. null parameters append the term to the end of any hierarchies.
	 *
	 * The MPTT operations can seem complex, but they're actually pretty simple:
	 * 		1: Find our insertion point:
	 * 			Either at the very end of the vocabulary, or before / after the given term
	 * 		2: Create a gap at that point:
	 * 			We'll bump everything at that point or after up enough to fit in the term we're moving
	 * 		3: Move the term:
	 * 			We know the offset between the old point and the new point, so move the range up that number of spaces.
	 * 		4: Close the original gap:
	 * 			Now we've got all our terms moved, but we need to bump everything back down to close the gap it left, similar to #2.
	 *
	 * @param Term $term The term to move.
	 * @param Term|null $target_term The term to move $term before or after, or null to move it to the very end of the vocabulary.
	 * @param bool $before True to move $term BEFORE $target_term, false (the default) to move $term AFTER $target_term.
	 * @return Term The Term object moved
	 **/
	public function move_term( $term, $target_term = null, $before = false )
	{
		// We assume that the arguments passed are valid terms. Check them before calling this.

		// If there are terms in the vocabulary, work out the reference point
		if ( !$this->is_empty() ) {

			$source_left = $term->mptt_left;
			$source_right = $term->mptt_right;
			$range = $source_right - $source_left + 1;

			DB::begin_transaction();

			// Determine the insertion point mptt_target
			if ( $target_term == null ) {
				// if no target is specified, put the new term after the last term
				$mptt_target = DB::get_value( 'SELECT MAX(mptt_right) FROM {terms} WHERE vocabulary_id = :id', array( ':id' => $this->id ) );
				$mptt_target = $mptt_target + 1;	// the left is one greater than the highest right
			}
			else {

				// if we're putting it before
				if ( $before ) {
					$mptt_target = $target_term->mptt_left;		// we're actually taking the place of the target term's left
				}
				else {
					$mptt_target = $target_term->mptt_right + 1;	// we just need to start at the next number
				}

			}

			// Create space in the tree for the insertion
			$params = array( 'vocab_id' => $this->id, 'range' => $range, 'mptt_target' => $mptt_target );
			$res = DB::query( 'UPDATE {terms} SET mptt_left = mptt_left + :range WHERE vocabulary_id = :vocab_id AND mptt_left >= :mptt_target', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}

			$res = DB::query( 'UPDATE {terms} SET mptt_right = mptt_right + :range WHERE vocabulary_id = :vocab_id AND mptt_right >= :mptt_target', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}

			// if we're moving it "down" ("forward"?) in the vocabulary, we just created a gap that changed our term's left and right values
			if ( $mptt_target < $source_left ) {
				$source_left = $source_left + $range;
				$source_right = $source_right + $range;
			}

			// figure out how far our nodes are moving
			$offset = $mptt_target - $source_left;

			// move our lucky nodes into the space we just created
			$params = array( ':offset' => $offset, ':vocab_id' => $this->id, ':source_left' => $source_left, ':source_right' => $source_right );
			$res = DB::query( '
				UPDATE {terms}
				SET
					mptt_left = mptt_left + :offset,
					mptt_right = mptt_right + :offset
				WHERE
					vocabulary_id = :vocab_id AND
					mptt_left >= :source_left AND
					mptt_right <= :source_right
				',
				$params
			);

			// Close the gap in the tree created by moving those nodes out
			$params = array( 'range' => $range, 'vocab_id' => $this->id, 'source_left' => $source_left );
			$res = DB::query( 'UPDATE {terms} SET mptt_left = mptt_left - :range WHERE vocabulary_id = :vocab_id AND mptt_left > :source_left', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}

			$params = array( 'range' => $range, 'vocab_id' => $this->id, 'source_right' => $source_right );
			$res = DB::query( 'UPDATE {terms} SET mptt_right = mptt_right - :range WHERE vocabulary_id = :vocab_id AND mptt_right > :source_right', $params );
			if ( ! $res ) {
				DB::rollback();
				return false;
			}

			// Success!
			DB::commit();

			return $this->get_term( $term->id );
		}
		return false;
	}

	/**
	 * Returns the number of tags in the database.
	 *
	 * @return int The number of tags in the database.
	 **/
	public  function count_total()
	{
		return count( $this->get_tree() );
	}

	/**
	 * Returns the number of times the most used tag is used.
	 *
	 * @return int The number of times the most used tag is used.
	 **/
	public function max_count()
	{
		return DB::get_value( 'SELECT count( t2.object_id ) AS max FROM {terms} t, {object_terms} t2 WHERE t2.term_id = t.id AND t.vocabulary_id = ? GROUP BY t.id ORDER BY max DESC LIMIT 1', array( $this->id ) );
	}

	/**
	 * Renames terms.
	 * If the master term exists, the terms will be merged with it.
	 * If not, it will be created first.
	 *
	 * @param mixed $master The Term to which they should be renamed, or the slug, text or id of it
	 * @param Array $tags The tag text, slugs or ids to be renamed
	 **/
	public function merge( $master, $tags, $object_type = 'post' )
	{
		$type_id = Vocabulary::object_type_id( $object_type );

		$post_ids = array();
		$tag_names = array();

		// get array of existing tags first to make sure we don't conflict with a new master tag
		foreach ( $tags as $tag ) {

			$posts = array();
			$term = $this->get_term( $tag );

			// get all the post ID's tagged with this tag
			$posts = $term->objects( $object_type );

			if ( count( $posts ) > 0 ) {
				// merge the current post ids into the list of all the post_ids we need for the new tag
				$post_ids = array_merge( $post_ids, $posts );
			}

			$tag_names[] = $tag;
			if ( $tag != $master ) {
				$this->delete_term( $term->id );
			}
		}

		// get the master term
		$master_term = $this->get_term( $master );

		if ( !isset( $master_term->term ) ) {
			// it didn't exist, so we assume it's tag text and create it
			$master_term = $this->add_term( $master );

			$master_ids = array();
		}
		else {
			// get the posts the tag is already on so we don't duplicate them
			$master_ids = $master_term->objects( $object_type );

		}

		if ( count( $post_ids ) > 0 ) {
			// only try and add the master tag to posts it's not already on
			$post_ids = array_diff( $post_ids, $master_ids );
		}
		else {
			$post_ids = $master_ids;
		}
		// link the master tag to each distinct post we removed tags from
		foreach ( $post_ids as $post_id ) {
			$master_term->associate( $object_type, $post_id );
		}

		EventLog::log( sprintf(
			_n( 'Term %1$s in the %2$s vocabulary has been renamed to %3$s.',
				'Terms %1$s in the %2$s vocabulary have been renamed to %3$s.',
				count( $tags )
			), implode( $tag_names, ', ' ), $this->name, $master ), 'info', 'vocabulary', 'habari'
		);

	}

	/**
	 * Get the tags associated with this object
	 *
	 * @param Integer $object_id. The id of the tagged object
	 * @param String $object_type. The name of the type of the object being tagged. Defaults to post
	 *
	 * @return Terms The terms associated with this object
	 */
	public function get_associations( $object_id, $object_type = 'post' )
	{
		$terms = $this->get_object_terms( $object_type, $object_id );
		if ( $terms ) {
			$terms = new Terms( $terms );
		}

		return $terms;
	}

	/**
	 * Returns the count of times a tag is used.
	 *
	 * @param mixed $term The tag to count usage.
	 * @return int The number of times a tag is used.
	 **/
	public function post_count( $term, $object_type = 'post' )
	{
		$term = $this->get_term( $term );
		return $term->count( $object_type );
	}

	/**
	 * Moves all of the terms into a temporary area so that they can be moved
	 *
	 * @static
	 * @param  Terms $terms An array of Term objects
	 */
	public static function prep_update( $terms )
	{
		$index = -1;
		foreach($terms as $term) {
			DB::query( 'UPDATE {terms} SET mptt_left=:left, mptt_right=:right WHERE id=:id', array('id' => $term->id, 'left' => $index, 'right' => $index -1 ) );
			$index -=2;
		}
	}

}

?>


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

/**
 * XMLRPC Binary type
 * Used to hold binary data for transmission in XMLRPC calls.
 *
 */
class XMLRPCBinary
{
	public $data;

	public function load_from_file( $filename )
	{
		$this->data = file_get_contents( $filename );
	}

	public function __construct( $data = null )
	{
		if ( isset( $data ) ) {
			$this->data = $data;
		}
	}
}

?>

repo_name: DaSourcerer/habari


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

/**
 * Create an XMLRPC client connection
 *
 * To Use:
 *
 * // Create the object using the XMLRPC entrypoint and scope.
 * $rpc= new XMLRPCClient( 'http://rpc.pingomatic.com', 'weblogUpdates' );
 * // Make a weblogUpdates.ping call on the client.
 * $result= $rpc->ping( 'Blog name', 'http://example.com' );
 *
 * // Change the scope on the existing client object.
 * $rpc->set_scope( 'system' );
 * // Make a system.listMethods call on the client.
 * $methods= $rpc->listMethods();
 *
 *
 * @todo Handle XMLRPC fault results by returning some error value.
 */
class XMLRPCClient
{
	private $entrypoint;
	private $scope = 'system';

	/**
	 * Create the XMLRPCClient
	 *
	 * @param string $xmlrpc_entrypoint The entrypoint of the remote server
	 */
	public function __construct( $xmlrpc_entrypoint, $scope = null )
	{
		$this->entrypoint = $xmlrpc_entrypoint;
		if ( isset( $scope ) ) {
			$this->scope = $scope;
		}
	}

	/**
	 * Set the scope of any subsequent function calls
	 * The default scope is 'system'.
	 *
	 * @param string $scope The scope to use
	 */
	public function set_scope( $scope )
	{
		$this->scope = $scope;
	}

	/**
	 * Allow method overloading for this class.
	 * This method allows any method name to be called on this object.  The method
	 * called is the method called via RPC, within the scope defined in $this->scope.
	 *
	 * @param string $fname The function name to call
	 * @param array $args An array of arguments that were called with the function
	 * @return array The result array
	 */
	public function __call( $fname, $args )
	{
		if ( $this->scope != '' ) {
			$rpc_method = "{$this->scope}.{$fname}";
		}
		else {
			$rpc_method = $fname;
		}

		$rpx = new SimpleXMLElement( '<methodCall/>' );
		$rpx->addChild( 'methodName', $rpc_method );
		if ( count( $args ) > 0 ) {
			$params = $rpx->addchild( 'params' );
			foreach ( $args as $arg ) {
				$param = $params->addchild( 'param' );
				XMLRPCUtils::encode_arg( $param, $arg );
			}
		}

		$request = new RemoteRequest( $this->entrypoint, 'POST' );
		$request->add_header( 'Content-Type: text/xml;charset=utf-8' );
		$request->set_body( $rpx->asXML() );

		$request->execute();

		if ( $request->executed() ) {
			$response = $request->get_response_body();
			// @todo this should use the MultiByte class, not directly call mb_string functions
			$enc = mb_detect_encoding( $response );
			$responseutf8 = mb_convert_encoding( $response, 'UTF-8', $enc );
			try {
				// @todo this should use libxml_use_internal_errors() instead of trying to hide the PHP warning see the plugin info parsing code for an example
				$bit = ini_get( 'error_reporting' );
				error_reporting( $bit && !E_WARNING );
				$responsexml = new SimpleXMLElement( $responseutf8 );
				error_reporting( $bit );
				$tmp = $responsexml->xpath( '//params/param/value' );
				if ( !$responsestruct = reset( $tmp ) ) {
					$tmp = $responsexml->xpath( '//fault/value' );
					if ( !$responsestruct = reset( $tmp ) ) {
						throw new Exception( _t( 'Invalid XML response.' ) );
					}
				}
				return XMLRPCUtils::decode_args( $responsestruct );
			}
			catch ( Exception $e ){
				//Utils::debug( $response, $e );
				error_reporting( $bit );
				return false;
			}
		}
	}

	/**
	 * Allow scoped functions to be called in shorthand
	 * Example:
	 *
	 * // Create the XMLRPC object
	 * $rpc= new XMLRPCClient( 'http://rpc.pingomatic.com' );
	 * // Call weblogUpdates.ping RPC call
	 * $rpc->weblogUpdates->ping( 'Blog name', 'http://example.com' );
	 *
	 *
	 * @param string $scope The scope to set this object to.
	 * @return XMLRPCClient This object instance
	 **/
	public function __get( $scope )
	{
		$this->set_scope( $scope );
		return $this;
	}

	/**
	 * Convenience method to create a new XMLRPCClient object
	 * Example:
	 *
	 * XMLRPCClient::open( 'http://rpc.pingomatic.com' )->weblogUpdates->ping( 'Blog name', 'http://example.com' );
	 *
	 * @param string $xmlrpc_entrypoint The entrypoint of the remote server
	 * @return XMLRPCClient The created client object
	 **/
	public static function open( $xmlrpc_entrypoint )
	{
		return new XMLRPCClient( $xmlrpc_entrypoint );
	}

}

?>


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

/**
 * XMLRPC Date type
 * Used to hold dates for transmission in XMLRPC calls.
 *
 */
class XMLRPCDate
{
	private $rpcdate;

	public function __set( $name, $value )
	{
		switch ( $name ) {
			case 'date':
				if ( is_numeric( $value ) ) {
					$this->rpcdate = $value;
				}
				else {
					$this->rpcdate = strtotime( $value );
				}
		}
	}

	public function __get( $name )
	{
		switch ( $name ) {
			case 'date':
				return $this->rpcdate;
		}
	}

	public function __construct( $date = null )
	{
		if ( isset( $date ) ) {
			$this->date = $date;
		}
	}
}

?>


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

/**
 * A custom exception for XMLRPC failure results
 */
class XMLRPCException extends Exception
{
	/**
	 * The exception constructor, called to create this excpetion
	 * @param integer $code The error code to produce
	 * @param string $message Optional The message to display with the error
	 **/
	public function __construct( $code, $message = null )
	{
		// make sure everything is assigned properly
		if ( empty( $message ) ) {
			$message = $this->get_message( $code );
		}
		parent::__construct( $message, $code );
	}

	/**
	 * Return a test-based error description for a numeric error code
	 * @param integer $code The error code to search for
	 * @return string A localized text-based error message.
	 **/
	private function get_message( $code )
	{
		switch ( $code ) {

		//Generic XMLRPC errors
			case -32700:
				return _t( 'parse error. not well formed' );
			case -32701:
				return _t( 'parse error. unsupported encoding' );
			case -32702:
				return _t( 'parse error. invalid character for encoding' );
			case -32600:
				return _t( 'server error. invalid xml-rpc. not conforming to spec.' );
			case -32601:
				return _t( 'server error. requested method not found' );
			case -32602:
				return _t( 'server error. invalid method parameters' );
			case -32603:
				return _t( 'server error. internal xml-rpc error' );
			case -32500:
				return _t( 'application error' );
			case -32400:
				return _t( 'system error' );
			case -32300:
				return _t( 'transport error' );

			// Pingback errors
			case 16:
				return _t( 'The source URI does not exist.' );
			case 17:
				return _t( 'The source URI does not contain a link to the target URI, and so cannot be used as a source.' );
			case 32:
				return _t( 'The specified target URI does not exist.' );
			case 33:
				return _t( 'The specified target URI cannot be used as a target.' );
			case 48:
				return _t( 'The pingback has already been registered.' );
			case 49:
				return _t( 'Access denied.' );
			case 50:
				return _t( 'The server could not communicate with an upstream server, or received an error from an upstream server, and therefore could not complete the request.' );

			// Additional standard errors
			case 1:
				return _t( 'This XMLRPC server only accepts POST requests.' );

			default:
				return Plugins::filter( 'xmlrpcexception_get_message', _t( 'Unknown XMLRPC Exception' ), $code );
		}
	}

	/**
	 * Represent this exception as a string
	 **/
	public function __toString()
	{
		return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
	}

	/**
	 * Send an XML-RPC fault output and quit.
	 **/
	public function output_fault_xml()
	{
		$xmltext = '<?xml version="1.0"?'.'><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>' . $this->code . '</int></value></member><member><name>faultString</name><value><string>' . $this->message . '</string></value></member></struct></value></fault></methodResponse>';
		ob_end_clean();
		header( 'Content-Type: text/xml;charset=utf-8' );
		echo trim( $xmltext );
		exit;
	}
}
?>


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

/**
 * A handler for incoming XMLRPC calls - an XMLRPC server
 *
 */
class XMLRPCServer extends ActionHandler
{

	/**
	 * Handle all incoming XMLRPC requests.
	 */
	public function act_xmlrpc_call()
	{
		if ( $_SERVER['REQUEST_METHOD'] != 'POST' ) {
			$exception = new XMLRPCException( 1 );
			$exception->output_fault_xml(); // dies here
		}
		$input = file_get_contents( 'php://input' );

		$xml = new SimpleXMLElement( $input );

		$function = $xml->methodName;
		$params = array();
		$found_params = $xml->xpath( '//params/param/value' );
		if ( is_array( $found_params ) ) {
			foreach ( $found_params as $param ) {
				$params[] = XMLRPCUtils::decode_args( $param );
			}
		}

		$returnvalue = false;

		Plugins::register( array( $this, 'system_listMethods' ), 'xmlrpc', 'system.listMethods' );
		$returnvalue = Plugins::xmlrpc( "{$function}", $returnvalue, $params, $this );

		$response = new SimpleXMLElement( '<?xml version="1.0"?'.'><methodResponse><params><param></param></params></methodResponse>' );
		XMLRPCUtils::encode_arg( $response->params->param, $returnvalue );

		ob_end_clean();
		header( 'Content-Type: text/xml;charset=utf-8' );
		echo trim( $response->asXML() );
		exit;
	}

	/**
	 * A plugin sink to return a list of XML-RPC methods on a call to system.listMethods
	 * Allows plugins to add their own methods to the list.
	 * @param mixed $returnvalue The value that will be returned to the remote caller.
	 * @param mixed $params The parameters that were called with the remote call.
	 * @return array An array of supported XML-RPC methods.
	 **/
	public function system_listMethods( $returnvalue, $params )
	{
		$res = array(
			'system.listMethods',
		);
		$res = Plugins::filter( 'xmlrpc_methods', $res );
		return $res;
	}

}

?>


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

/**
 * XMLRPC Struct type
 * Used to hold struct types (objects) that are returned in XMLRPC requests.
 *
 */
class XMLRPCStruct
{
	private $fields = array();

	/**
	 * Property setter for the XMLRPCStruct class.
	 * This allows the following:
	 * <code>
	 * $struct = new XMLRPCStruct();
	 * $struct->foo = 'bar'; // This is done by __set() and assigns 'bar' into $this->fields['foo']
	 * </code>
	 *
	 * @param string $name The name of the property on this object to set
	 * @param mixed $value The value to set in the property
	 * @return
	 */
	public function __set( $name, $value )
	{
		$this->fields[$name] = $value;
	}

	/**
	 * Property getter for the XMLRPCStruct class.
	 * Returns the value of $this->fields for the specified porperty name.
	 *
	 * @param string $name The name of the property
	 * @return mixed The value stored for the requested property
	 */
	public function __get( $name )
	{
		return $this->fields[$name];
	}

	/**
	 * Magic isset for XMLRPCStruct, 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->fields[$name] );
	}

	/**
	 * Get the list of properties that this object contains.
	 *
	 * @return array List of object properties, as stored in $this->fields.
	 */
	public function get_fields()
	{
		return array_keys( $this->fields );
	}

	/**
	 * Constructor for XMLRPCStruct
	 *
	 * @param array $fields Default field values to set into properties.
	 */
	public function __construct( $fields = array() )
	{
		$this->fields = $fields;
	}
}

?>


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

/**
 * XMLRPC Utility class
 *
 */
class XMLRPCUtils
{

	/**
	 * Encode a variable value into the parameters of the XML tree
	 *
	 * @param SimpleXMLElement $params The parameter to add the value elements to.
	 * @param mixed $arg The value to encode
	 */
	public static function encode_arg( $params, $arg )
	{
		switch ( true ) {
			case is_array( $arg ):
				$data = $params->addchild( 'value' )->addchild( 'array' )->addchild( 'data' );
				foreach ( $arg as $element ) {
					self::encode_arg( $data, $element );
				}
				break;
			case ( $arg instanceof XMLRPCDate ):
				$params->addchild( 'value' )->addchild( 'dateTime.iso8601', date( 'c', $arg->date ) );
				break;
			case ( $arg instanceof XMLRPCBinary ):
				$params->addchild( 'value' )->addchild( 'base64', base64_encode( $arg->data ) );
				break;
			case ( $arg instanceof XMLRPCStruct ):
				$struct = $params->addchild( 'value' )->addchild( 'struct' );
				$object_vars = $arg->get_fields();
				foreach ( $object_vars as $field ) {
					$member = $struct->addchild( 'member' );
					$member->addchild( 'name', $field );
					self::encode_arg( $member, $arg->$field );
				}
				break;
			case is_object( $arg ):
				$struct = $params->addchild( 'value' )->addchild( 'struct' );
				$object_vars = get_object_vars( $arg );
				foreach ( $object_vars as $key=>$value ) {
					$member = $struct->addchild( 'member' );
					$member->addchild( 'name', $key );
					self::encode_arg( $member, $value );
				}
				break;
			case is_integer( $arg ):
				$params->addchild( 'value' )->addchild( 'i4', $arg );
				break;
			case is_bool( $arg ):
				$params->addchild( 'value' )->addchild( 'boolean', $arg ? '1' : '0' );
				break;
			case is_string( $arg ):
				$params->addchild( 'value' )->addchild( 'string', $arg );
				break;
			case is_float( $arg ):
				$params->addchild( 'value' )->addchild( 'double', $arg );
				break;
		}
	}

	/**
	 * Decode the value of a response parameter using the datatype specified in the XML element.
	 *
	 * @param SimpleXMLElement $value A "value" element from the XMLRPC response
	 * @return mixed The value of the element, decoded from the datatype specified in the xml element
	 */
	public static function decode_args( $value )
	{
		if ( count( $value->children() ) ) {
			$value = $value->xpath( '*' );
			$value = $value[0];
		}
		switch ( $value->getName() ) {
			case 'data':
			case 'array':
				$result_array = array();
				foreach ( $value->xpath( '//data/value' ) as $array_value ) {
					$result_array[] = self::decode_args( $array_value );
				}
				return $result_array;
				break;
			case 'struct':
				$result_struct = new XMLRPCStruct();
				foreach ( $value->xpath( '//member' ) as $struct_value ) {
					$property_name = (string) $struct_value->name;
					$children = $struct_value->value->children();
					if ( count( $children ) > 0 ) {
						$result_struct->$property_name = self::decode_args( $children[0] );
					}
					else {
						$result_struct->$property_name = (string) $struct_value->value;
					}
				}
				return $result_struct;
				break;
			case 'string':
				return (string) $value;
			case 'i4':
			case 'integer':
				return (int) $value;
			case 'double':
				return (double) $value;
			case 'boolean':
				return ( (int) $value == 1 ) ? true : false;
			case 'dateTime.iso8601':
				return strtotime( (string) $value );
			default:
				return (string) $value;
		}
	}

}

?>


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

/**
 * A base class handler for URL-based actions. All ActionHandlers must
 * extend this class for the Controller to call their actions.
 *
 */
class ActionHandler
{
	/**
	 * Name of action to trigger
	 *
	 * @var string
	 * @see act()
	 */
	public $action = '';

	/**
	 * Internal array of handler variables (state info)
	 *
	 * @var SuperGlobal
	 */
	public $handler_vars = null;

	/**
	 * Storage for the theme used, if created
	 *
	 * @var Theme $theme
	 */
	public $theme = null;

	/**
	 * All handlers must implement act() to conform to handler API.
	 * This is the default implementation of act(), which attempts
	 * to call a class member method of $this->act_$action().  Any
	 * subclass is welcome to override this default implementation.
	 *
	 * @param string $action the action that was in the URL rule
	 */
	public function act( $action )
	{
		if ( null === $this->handler_vars ) {
			$this->handler_vars = new SuperGlobal( array() );
		}
		$this->action = $action;

		$action_method = 'act_' . $action;
		$before_action_method = 'before_' . $action_method;
		$after_action_method = 'after_' . $action_method;

		if ( method_exists( $this, $action_method ) ) {
			if ( method_exists( $this, $before_action_method ) ) {
				$this->$before_action_method();
			}
			/**
			 * Plugin action to allow plugins to execute before a certain
			 * action is triggered
			 *
			 * @see ActionHandler::$action
			 * @action before_act_{$action}
			 */
			Plugins::act( $before_action_method, $this );

			$this->$action_method();

			/**
			 * Plugin action to allow plugins to execute after a certain
			 * action is triggered
			 *
			 * @see ActionHandler::$action
			 * @action before_act_{$action}
			 */
			Plugins::act( $after_action_method );
			if ( method_exists( $this, $after_action_method ) ) {
				$this->$after_action_method();
			}
		}
	}

	/**
	 * Helper method to convert calls to $handler->my_action()
	 * to $handler->act('my_action');
	 *
	 * @param string $function function name
	 * @param array $args function arguments
	 */
	public function __call( $function, $args )
	{
		return $this->act( $function );
	}

	/**
	 * Helper method to allow RewriteRules to send a redirect. The method will
	 * redirect to the build_str of the RewriteRule if matched.
	 */
	public function act_redirect()
	{
		$vars = isset( $_SERVER['QUERY_STRING'] ) ? Utils::get_params( $_SERVER['QUERY_STRING'] ) : array();
		Utils::redirect( URL::get( null, $vars ) );
	}

	/**
	 * Load the active theme and create a new Theme instance.
	 * Also, assign the request variables.
	 */
	public function setup_theme()
	{
		$this->theme = Themes::create();
		$this->theme->assign( 'matched_rule', URL::get_matched_rule() );
		$request = new StdClass();
		foreach ( URL::get_active_rules() as $rule ) {
			$request->{$rule->name} = false;
		}
		$request->{$this->theme->matched_rule->name} = true;
		$this->theme->assign( 'request', $request );
	}
}

?>


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

/**
 * Habari AdminCommentsHandler Class
 * Handles comment-related actions in the admin
 *
 */
class AdminCommentsHandler extends AdminHandler
{
	/**
	 * Construct a form for a comment.
	 * @return FormUI The comment's form.
	 */
	public function form_comment( $comment, $actions )
	{
		$form = new FormUI( 'comment' );

		$user = User::identify();

		// Create the top description
		$top = $form->append( 'wrapper', 'buttons_1' );
		$top->class = 'container buttons comment overview';

		$top->append( 'static', 'overview', $this->theme->fetch( 'comment.overview' ) );

		$buttons_1 = $top->append( 'wrapper', 'buttons_1' );
		$buttons_1->class = 'item buttons';

		foreach ( $actions as $status => $action ) {
			$id = $action . '_1';
			$buttons_1->append( 'submit', $id, _t( ucfirst( $action ) ) );
			$buttons_1->$id->class = 'button ' . $action;
			if ( Comment::status_name( $comment->status ) == $status ) {
				$buttons_1->$id->class = 'button active ' . $action;
				$buttons_1->$id->disabled = true;
			}
			else {
				$buttons_1->$id->disabled = false;
			}
		}

		// Content
		$form->append( 'wrapper', 'content_wrapper' );
		$content = $form->content_wrapper->append( 'textarea', 'content', 'null:null', _t( 'Comment' ), 'admincontrol_textarea' );
		$content->class = 'resizable';
		$content->value = $comment->content;

		// Create the splitter
		$comment_controls = $form->append( 'tabs', 'comment_controls' );

		// Create the author info
		$author = $comment_controls->append( 'fieldset', 'authorinfo', _t( 'Author' ) );

		$author->append( 'text', 'author_name', 'null:null', _t( 'Author Name' ), 'tabcontrol_text' );
		$author->author_name->value = $comment->name;

		$author->append( 'text', 'author_email', 'null:null', _t( 'Author Email' ), 'tabcontrol_text' );
		$author->author_email->value = $comment->email;

		$author->append( 'text', 'author_url', 'null:null', _t( 'Author URL' ), 'tabcontrol_text' );
		$author->author_url->value = $comment->url;

		$author->append( 'text', 'author_ip', 'null:null', _t( 'IP Address:' ), 'tabcontrol_text' );
		$author->author_ip->value = $comment->ip;

		// Create the advanced settings
		$settings = $comment_controls->append( 'fieldset', 'settings', _t( 'Settings' ) );

		$settings->append( 'text', 'comment_date', 'null:null', _t( 'Date:' ), 'tabcontrol_text' );
		$settings->comment_date->value = $comment->date->get( 'Y-m-d H:i:s' );

		$settings->append( 'text', 'comment_post', 'null:null', _t( 'Post ID:' ), 'tabcontrol_text' );
		$settings->comment_post->value = $comment->post->id;

		$statuses = Comment::list_comment_statuses( false );
		$statuses = Plugins::filter( 'admin_publish_list_comment_statuses', $statuses );
		$settings->append( 'select', 'comment_status', 'null:null', _t( 'Status' ), $statuses, 'tabcontrol_select' );
		$settings->comment_status->value = $comment->status;

		// // Create the stats
		// $comment_controls->append('fieldset', 'stats_tab', _t('Stats'));
		// $stats = $form->stats_tab->append('wrapper', 'tags_buttons');
		// $stats->class = 'container';
		//
		// $stats->append('static', 'post_count', '

'._t('Comments on this post:').'

' . Comments::count_by_id($comment->post->id) . '

');
		// $stats->append('static', 'ip_count', '

'._t('Comments from this IP:').'

' . Comments::count_by_ip($comment->ip) . '

');
		// $stats->append('static', 'email_count', '

'._t('Comments by this author:').'

' . Comments::count_by_email($comment->email) . '

');
		// $stats->append('static', 'url_count', '

'._t('Comments with this URL:').'

' . Comments::count_by_url($comment->url) . '

');

		// Create the second set of action buttons
		$buttons_2 = $form->append( 'wrapper', 'buttons_2' );
		$buttons_2->class = 'container buttons comment';

		foreach ( $actions as $status => $action ) {
			$id = $action . '_2';
			$buttons_2->append( 'submit', $id, _t( ucfirst( $action ) ) );
			$buttons_2->$id->class = 'button ' . $action;
			if ( Comment::status_name( $comment->status ) == $status ) {
				$buttons_2->$id->class = 'button active ' . $action;
				$buttons_2->$id->disabled = true;
			}
			else {
				$buttons_2->$id->disabled = false;
			}
		}

		// Allow plugins to alter form
		Plugins::act( 'form_comment_edit', $form, $comment );

		return $form;
	}

	/**
	 * Handles GET requests for an individual comment.
	 */
	public function get_comment( $update = false )
	{
		if ( isset( $this->handler_vars['id'] ) && $comment = Comment::get( $this->handler_vars['id'] ) ) {
			$this->theme->comment = $comment;

			// Convenience array to output actions twice
			$actions = array(
				'deleted' => 'delete',
				'spam' => 'spam',
				'unapproved' => 'unapprove',
				'approved' => 'approve',
				'saved' => 'save'
				);

			$form = $this->form_comment( $comment, $actions );

			if ( $update ) {
				foreach ( $actions as $key => $action ) {
					$id_one = $action . '_1';
					$id_two = $action . '_2';
					if ( $form->$id_one->value != null || $form->$id_two->value != null ) {
						if ( $action == 'delete' ) {
							$comment->delete();
							Utils::redirect( URL::get( 'admin', 'page=comments' ) );
						}
						if ( $action != 'save' ) {
							foreach ( Comment::list_comment_statuses() as $status ) {
								if ( $status == $key ) {
									$comment->status = Comment::status_name( $status );
									$set_status = true;
								}
							}
						}
					}
				}

				$comment->content = $form->content;
				$comment->name = $form->author_name;
				$comment->url = $form->author_url;
				$comment->email = $form->author_email;
				$comment->ip = $form->author_ip;

				$comment->date = HabariDateTime::date_create( $form->comment_date );
				$comment->post_id = $form->comment_post;

				if ( ! isset( $set_status ) ) {
					$comment->status = $form->comment_status->value;
				}

				$comment->update();

				Plugins::act( 'comment_edit', $comment, $form );

				Utils::redirect();
			}

			$comment->content = $form;
			$this->theme->form = $form;

			$this->display( 'comment' );
		}
		else {
			Utils::redirect( URL::get( 'admin', 'page=comments' ) );
		}
	}

	/**
	 * Handles POST requests for an individual comment.
	 */
	public function post_comment()
	{
		$this->get_comment( true );
	}

	/**
	 * Handles GET requests for the comments  page.
	 */
	public function get_comments()
	{
		$this->post_comments();
	}

	/**
	 * Handles the submission of the comment moderation form.
	 * @todo Separate delete from "delete until purge"
	 */
	public function post_comments()
	{
		// Get special search statuses
		$statuses = Comment::list_comment_statuses();
		$labels = array_map(
			function($a) {return MultiByte::ucfirst(Plugins::filter("comment_status_display", $a));},
			$statuses
		);
		$terms = array_map(
			function($a) {return "status:{$a}";},
			$statuses
		);
		$statuses = array_combine( $terms, $labels );

		// Get special search types
		$types = Comment::list_comment_types();
		$labels = array_map(
			function($a) {return MultiByte::ucfirst(Plugins::filter("comment_type_display", $a, "singular")) ;},
			$types
		);
		$terms = array_map(
			function($a) {return "type:{$a}";},
			$types
		);
		$types = array_combine( $terms, $labels );

		$this->theme->special_searches = array_merge( $statuses, $types );

		$this->fetch_comments();
		$this->display( 'comments' );
	}

	/**
	 * Retrieve comments.
	 */
	public function fetch_comments( $params = array() )
	{
		// Make certain handler_vars local with defaults, and add them to the theme output
		$locals = array(
			'do_delete' => false,
			'do_spam' => false,
			'do_approve' => false,
			'do_unapprove' => false,
			'comment_ids' => null,
			'nonce' => '',
			'timestamp' => '',
			'password_digest' => '',
			'mass_spam_delete' => null,
			'mass_delete' => null,
			'type' => 'All',
			'limit' => 20,
			'offset' => 0,
			'search' => '',
			'status' => 'All',
			'orderby' => 'date DESC',
		);
		foreach ( $locals as $varname => $default ) {
			$$varname = isset( $this->handler_vars[$varname] ) ? $this->handler_vars[$varname] : ( isset( $params[$varname] ) ? $params[$varname] : $default );
			$this->theme->{$varname} = $$varname;
		}

		// Setting these mass_delete options prevents any other processing.  Desired?
		if ( isset( $mass_spam_delete ) && $status == Comment::STATUS_SPAM ) {
			// Delete all comments that have the spam status.
			Comments::delete_by_status( Comment::STATUS_SPAM );
			// let's optimize the table
			$result = DB::query( 'OPTIMIZE TABLE {comments}' );
			Session::notice( _t( 'Deleted all spam comments' ) );
			EventLog::log( _t( 'Deleted all spam comments' ), 'info' );
			Utils::redirect();
		}
		elseif ( isset( $mass_delete ) && $status == Comment::STATUS_UNAPPROVED ) {
			// Delete all comments that are unapproved.
			Comments::delete_by_status( Comment::STATUS_UNAPPROVED );
			Session::notice( _t( 'Deleted all unapproved comments' ) );
			EventLog::log( _t( 'Deleted all unapproved comments' ), 'info' );
			Utils::redirect();
		}
		// if we're updating posts, let's do so:
		elseif ( ( $do_delete || $do_spam || $do_approve || $do_unapprove ) && isset( $comment_ids ) ) {
			$okay = true;
			if ( empty( $nonce ) || empty( $timestamp ) ||  empty( $password_digest ) ) {
				$okay = false;
			}
			$wsse = Utils::WSSE( $nonce, $timestamp );
			if ( $password_digest != $wsse['digest'] ) {
				$okay = false;
			}
			if ( $okay ) {
				if ( $do_delete ) {
					$action = 'delete';
				}
				elseif ( $do_spam ) {
					$action = 'spam';
				}
				elseif ( $do_approve ) {
					$action = 'approve';
				}
				elseif ( $do_unapprove ) {
					$action = 'unapprove';
				}
				$ids = array();
				foreach ( $comment_ids as $id => $id_value ) {
					if ( ! isset( ${'$comment_ids['.$id.']'} ) ) { // Skip unmoderated submitted comment_ids
						$ids[] = $id;
					}
				}
				$to_update = Comments::get( array( 'id' => $ids ) );
				$modstatus = array(
					_t( 'Deleted %d comments' ) => 0,
					_t( 'Marked %d comments as spam' ) => 0,
					_t( 'Approved %d comments' ) => 0,
					_t( 'Unapproved %d comments' ) => 0,
					_t( 'Edited %d comments' ) => 0
				);
				Plugins::act( 'admin_moderate_comments', $action, $to_update, $this );

				switch ( $action ) {

					case 'delete':
						// This comment was marked for deletion
						$to_update = $this->comment_access_filter( $to_update, 'delete' );
						Comments::delete_these( $to_update );
						$modstatus[_t( 'Deleted %d comments' )] = count( $to_update );
						break;

					case 'spam':
						// This comment was marked as spam
						$to_update = $this->comment_access_filter( $to_update, 'edit' );
						Comments::moderate_these( $to_update, Comment::STATUS_SPAM );
						$modstatus[_t( 'Marked %d comments as spam' )] = count( $to_update );
						break;

					case 'approve':
					case 'approved':
						// Comments marked for approval
						$to_update = $this->comment_access_filter( $to_update, 'edit' );
						Comments::moderate_these( $to_update, Comment::STATUS_APPROVED );
						$modstatus[_t( 'Approved %d comments' )] = count( $to_update );
						foreach ( $to_update as $comment ) {
									$modstatus[_t( 'Approved comments on these posts: %s' )] = ( isset( $modstatus[_t( 'Approved comments on these posts: %s' )] )? $modstatus[_t( 'Approved comments on these posts: %s' )] . ' · ' : '' ) . 'post->permalink . '">' . $comment->post->title . ' ';
						}
						break;

					case 'unapprove':
					case 'unapproved':
						// This comment was marked for unapproval
						$to_update = $this->comment_access_filter( $to_update, 'edit' );
						Comments::moderate_these( $to_update, Comment::STATUS_UNAPPROVED );
						$modstatus[_t( 'Unapproved %d comments' )] = count( $to_update );
						break;

					case 'edit':
						$to_update = $this->comment_access_filter( $to_update, 'edit' );
						foreach ( $to_update as $comment ) {
							// This comment was edited
							if ( $_POST['name_' . $comment->id] != null ) {
								$comment->name = $_POST['name_' . $comment->id];
							}
							if ( $_POST['email_' . $comment->id] != null ) {
								$comment->email = $_POST['email_' . $comment->id];
							}

							if ( $_POST['url_' . $comment->id] != null ) {
								$comment->url = $_POST['url_' . $comment->id];
							}
							if ( $_POST['content_' . $comment->id] != null ) {
								$comment->content = $_POST['content_' . $comment->id];
							}

							$comment->update();
						}
						$modstatus[_t( 'Edited %d comments' )] = count( $to_update );
						break;

				}

				foreach ( $modstatus as $key => $value ) {
					if ( $value ) {
						Session::notice( sprintf( $key, $value ) );
					}
				}

			}

			Utils::redirect();

		}

		// we load the WSSE tokens
		// for use in the delete button
		$this->theme->wsse = Utils::WSSE();

		$arguments = array(
			'type' => $type,
			'status' => $status,
			'limit' => $limit,
			'offset' => $offset,
			'orderby' => $orderby,
		);

		// only get comments the user is allowed to manage
		if ( !User::identify()->can( 'manage_all_comments' ) ) {
			$arguments['post_author'] = User::identify()->id;
		}

		// there is no explicit 'all' type/status for comments, so we need to unset these arguments
		// if that's what we want. At the same time we can set up the search field
		$this->theme->search_args = '';
		if ( $type == 'All' ) {
			unset( $arguments['type'] );
		}
		else {
			$this->theme->search_args = 'type:' . Comment::type_name( $type ) . ' ';
		}

		if ( $status == 'All' ) {
			unset ( $arguments['status'] );
		}
		else {
			$this->theme->search_args .= 'status:' . Comment::status_name( $status );
		}

		if ( '' != $search ) {
			$arguments = array_merge( $arguments, Comments::search_to_get( $search ) );
		}

		$this->theme->comments = Comments::get( $arguments );
		$monthcts = Comments::get( array_merge( $arguments, array( 'month_cts' => 1 ) ) );
		$years = array();
		foreach ( $monthcts as $month ) {
			if ( isset( $years[$month->year] ) ) {
				$years[$month->year][] = $month;
			}
			else {
				$years[$month->year] = array( $month );
			}
		}
		$this->theme->years = $years;

		$baseactions = array();
		$statuses = Comment::list_comment_statuses();
		foreach ( $statuses as $statusid => $statusname ) {
			$baseactions[$statusname] = array( 'url' => 'javascript:itemManage.update(\'' . $statusname . '\',__commentid__);', 'title' => _t( 'Change this comment\'s status to %s', array( $statusname ) ), 'label' => Comment::status_action( $statusid ), 'access' => 'edit' );
		}

		/* Standard actions */
		$baseactions['delete'] = array( 'url' => 'javascript:itemManage.update(\'delete\',__commentid__);', 'title' => _t( 'Delete this comment' ), 'label' => _t( 'Delete' ), 'access' => 'delete' );
		$baseactions['edit'] = array( 'url' => URL::get( 'admin', 'page=comment&id=__commentid__' ), 'title' => _t( 'Edit this comment' ), 'label' => _t( 'Edit' ), 'access' => 'edit' );

		/* Allow plugins to apply actions */
		$actions = Plugins::filter( 'comments_actions', $baseactions, $this->theme->comments );

		foreach ( $this->theme->comments as $comment ) {
			// filter the actions based on the user's permissions
			$comment_access = $comment->get_access();
			$menu = array();
			foreach ( $actions as $name => $action ) {
				if ( !isset( $action['access'] ) || ACL::access_check( $comment_access, $action['access'] ) ) {
					$menu[$name] = $action;
				}
			}
			// remove the current status from the dropmenu
			unset( $menu[Comment::status_name( $comment->status )] );
			$comment->menu = Plugins::filter( 'comment_actions', $menu, $comment );
		}
	}

	/**
	 * A helper function for fetch_comments()
	 * Filters a list of comments by ACL access
	 * @param object $comments an array of Comment objects
	 * @param string $access the access type to check for
	 * @return a filtered array of Comment objects.
	 */
	public function comment_access_filter( $comments, $access )
	{
		$result = array();
		foreach ( $comments as $comment ) {
			if ( ACL::access_check( $comment->get_access(), $access ) ) {
				$result[] = $comment;
			}
		}
		return $result;
	}

	/**
	 * Handles AJAX requests from the manage comments page.
	 */
	public function ajax_comments()
	{
		Utils::check_request_method( array( 'GET', 'HEAD' ) );

		$this->create_theme();
		$this->theme->theme = $this->theme;

		$params = $_GET;

		$this->fetch_comments( $params );
		$items = $this->theme->fetch( 'comments_items' );
		$timeline = $this->theme->fetch( 'timeline_items' );

		$item_ids = array();

		foreach ( $this->theme->comments as $comment ) {
			$item_ids['p' . $comment->id] = 1;
		}

		$ar = new AjaxResponse();
		$ar->data = array(
			'items' => $items,
			'item_ids' => $item_ids,
			'timeline' => $timeline,
		);
		$ar->out();
	}

	/**
	 * Handles AJAX requests to update comments, comment moderation
	 */
	public function ajax_update_comment( $handler_vars )
	{

		Utils::check_request_method( array( 'POST' ) );

		// check WSSE authentication
		$wsse = Utils::WSSE( $handler_vars['nonce'], $handler_vars['timestamp'] );
		if ( $handler_vars['digest'] != $wsse['digest'] ) {
			Session::error( _t( 'WSSE authentication failed.' ) );
			echo Session::messages_get( true, array( 'Format', 'json_messages' ) );
			return;
		}

		$ids = array();

		foreach ( $_POST as $id => $update ) {
			// skip POST elements which are not comment ids
			if ( preg_match( '/^p\d+$/', $id ) && $update ) {
				$ids[] = (int) substr( $id, 1 );
			}
		}

		if ( ( ! isset( $ids ) || empty( $ids ) ) && $handler_vars['action'] == 'delete' ) {
			Session::notice( _t( 'No comments selected.' ) );
			echo Session::messages_get( true, array( 'Format', 'json_messages' ) );
			return;
		}

		$comments = Comments::get( array( 'id' => $ids, 'nolimit' => true ) );
		Plugins::act( 'admin_moderate_comments', $handler_vars['action'], $comments, $this );
		$status_msg = _t( 'Unknown action "%s"', array( $handler_vars['action'] ) );

		switch ( $handler_vars['action'] ) {
			case 'delete_spam':
				Comments::delete_by_status( Comment::STATUS_SPAM );
				$status_msg = _t( 'Deleted all spam comments' );
				break;
			case 'delete_unapproved':
				Comments::delete_by_status( Comment::STATUS_UNAPPROVED );
				$status_msg = _t( 'Deleted all unapproved comments' );
				break;
			case 'delete':
				// Comments marked for deletion
				Comments::delete_these( $comments );
				$status_msg = sprintf( _n( 'Deleted %d comment', 'Deleted %d comments', count( $ids ) ), count( $ids ) );
				break;
			case 'spam':
				// Comments marked as spam
				Comments::moderate_these( $comments, Comment::STATUS_SPAM );
				$status_msg = sprintf( _n( 'Marked %d comment as spam', 'Marked %d comments as spam', count( $ids ) ), count( $ids ) );
				break;
			case 'approve':
			case 'approved':
				// Comments marked for approval
				Comments::moderate_these( $comments, Comment::STATUS_APPROVED );
				$status_msg = sprintf( _n( 'Approved %d comment', 'Approved %d comments', count( $ids ) ), count( $ids ) );
				break;
			case 'unapprove':
			case 'unapproved':
				// Comments marked for unapproval
				Comments::moderate_these( $comments, Comment::STATUS_UNAPPROVED );
				$status_msg = sprintf( _n( 'Unapproved %d comment', 'Unapproved %d comments', count( $ids ) ), count( $ids ) );
				break;
			default:
				// Specific plugin-supplied action
				$status_msg = Plugins::filter( 'admin_comments_action', $status_msg, $handler_vars['action'], $comments );
				break;
		}

		Session::notice( $status_msg );
		echo Session::messages_get( true, array( 'Format', 'json_messages' ) );
	}

}
?>


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

/**
 * Habari AdminDashboardHandler Class
 * Handles dashboard-related actions in the admin
 *
 */
class AdminDashboardHandler extends AdminHandler
{
	/**
	 * Handles get requests for the dashboard
	 * @todo update check should probably be cron'd and cached, not re-checked every load
	 */
	public function get_dashboard()
	{
		// Not sure how best to determine this yet, maybe set an option on install, maybe do this:
		$firstpostdate = DB::get_value( 'SELECT min(pubdate) FROM {posts} WHERE status = ?', array( Post::status( 'published' ) ) );
		if ( $firstpostdate ) {
			$this->theme->active_time = HabariDateTime::date_create( $firstpostdate );
		}

		// check to see if we have updates to display
		$this->theme->updates = Options::get( 'updates_available', array() );

		// collect all the stats we display on the dashboard
		$user = User::identify();
		$this->theme->stats = array(
			'author_count' => Users::get( array( 'count' => 1 ) ),
			'post_count' => Posts::get( array( 'count' => 1, 'content_type' => Post::type( 'any' ), 'status' => Post::status( 'published' ) ) ),
			'comment_count' => Comments::count_total( Comment::STATUS_APPROVED, false ),
			'tag_count' => Tags::vocabulary()->count_total(),
			'user_draft_count' => Posts::get( array( 'count' => 1, 'content_type' => Post::type( 'any' ), 'status' => Post::status( 'draft' ), 'user_id' => $user->id ) ),
			'unapproved_comment_count' => User::identify()->can( 'manage_all_comments' ) ? Comments::count_total( Comment::STATUS_UNAPPROVED, false ) : Comments::count_by_author( User::identify()->id, Comment::STATUS_UNAPPROVED ),
			'spam_comment_count' => $user->can( 'manage_all_comments' ) ? Comments::count_total( Comment::STATUS_SPAM, false ) : Comments::count_by_author( $user->id, Comment::STATUS_SPAM ),
			'user_scheduled_count' => Posts::get( array( 'count' => 1, 'content_type' => Post::type( 'any' ), 'status' => Post::status( 'scheduled' ), 'user_id' => $user->id ) ),
		);

		// check for first run
		$u = User::identify();
		if ( ! isset( $u->info->experience_level ) ) {
			$this->theme->first_run = true;
			$u->info->experience_level = 'user';
			$u->info->commit();
		}
		else {
			$this->theme->first_run = false;
		}

		$this->get_additem_form();

		$this->display( 'dashboard' );
	}

	/**
	 * Handles POST requests from the dashboard.
	 */
	public function post_dashboard()
	{
		$this->get_dashboard();
	}

	/**
	 * Add the Add Item form to the theme for display
	 */
	public function get_additem_form()
	{
		$additem_form = new FormUI( 'dash_additem' );
		$additem_form->append( 'select', 'module', 'null:unused' );
		$additem_form->module->options = Plugins::filter( 'dashboard_block_list', array() );
		$additem_form->append( 'submit', 'submit', _t( '+' ) );
		//$form->on_success( array( $this, 'dash_additem' ) );
		$additem_form->properties['onsubmit'] = "dashboard.add(); return false;";
		$this->theme->additem_form = $additem_form->get();
	}

	/**
	 * Handles AJAX requests from the dashboard
	 */
	public function ajax_dashboard( $handler_vars )
	{
		Utils::check_request_method( array( 'POST' ) );

		$this->create_theme();
		$this->get_additem_form();
		$available_modules = Plugins::filter('dashboard_block_list', array());

		switch ( $handler_vars['action'] ) {
			case 'updateModules':
				$modules = $_POST['moduleOrder'];
				$order = 0;
				foreach ( $modules as $module ) {
					$order++;
					DB::query('UPDATE {blocks_areas} SET display_order = :display_order WHERE block_id = :id AND area = "dashboard"', array('display_order' => $order, 'id' => $module));
				}
				$ar = new AjaxResponse( 200, _t( 'Modules updated.' ) );
				break;
			case 'addModule':
				$type = $handler_vars['module_name'];
				$title = $available_modules[$type];
				$block = new Block( array( 'title' => $title, 'type' => $type ) );
				$block->insert();
				$max_display_order = DB::get_value('SELECT max(display_order) FROM {blocks_areas} WHERE area = "dashboard" and scope_id = 0;');
				$max_display_order++;
				DB::query( 'INSERT INTO {blocks_areas} (block_id, area, scope_id, display_order) VALUES (:block_id, "dashboard", 0, :display_order)', array( 'block_id'=>$block->id, 'display_order'=>$max_display_order ) );

				$ar = new AjaxResponse( 200, _t( 'Added module %s.', array( $title ) ) );
				$ar->html( 'modules', $this->theme->fetch( 'dashboard_modules' ) );
				break;
			case 'removeModule':
				$block_id = $handler_vars['moduleid'];
				DB::delete('{blocks}', array('id' => $block_id));
				DB::delete('{blocks_areas}', array('block_id' => $block_id));
				$ar = new AjaxResponse( 200, _t( 'Removed module.' ) );
				$ar->html( 'modules', $this->theme->fetch( 'dashboard_modules' ) );
				break;
			case 'configModule':
				$block_id = $handler_vars['moduleid'];

				$block = DB::get_row('SELECT * FROM {blocks} b WHERE b.id = :id', array('id' => $block_id), 'Block');

				/** Block $block */
				$form = $block->get_form();
				$form->_ajax = true;
				$form->set_option( 'success_message', _t('Module Configuration Saved.')
					. '<script type="text/javascript">window.setTimeout(function(){$(".form_message").fadeOut();}, 2000);</script>'
				);
				$control_id = new FormControlHidden('moduleid', 'null:null');
				$control_id->value = $block->id;
				$control_id->id = 'moduleid';
				$form->append($control_id);
				$control_action = new FormControlHidden('action', 'null:null');
				$control_action->value = 'configModule';
				$control_action->id = 'action';
				$form->append($control_action);
				$form->out();
				$form_id = $form->name;
				exit;
				break;
		}

		$ar->out();
	}

}


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

/**
 * Habari AdminGroupsHandler Class
 * Handles group-related actions in the admin
 *
 */
class AdminGroupsHandler extends AdminHandler
{
	/**
	 * Handles GET requests for the groups page.
	 */
	public function get_groups()
	{
		// prepare the WSSE tokens
		$this->theme->wsse = Utils::WSSE();
		$groups = UserGroups::get_all();
		$this->theme->groups = Plugins::filter('admin_groups_visible', $groups);
		$this->display( 'groups' );
	}

	/**
	 * Handles POST requests for the groups page.
	 */
	public function post_groups()
	{
		// prepare the WSSE tokens
		$this->theme->wsse = Utils::WSSE();
		$this->theme->groups = UserGroups::get_all();
		$this->update_groups( $this->handler_vars, false );
		Utils::redirect( URL::get( 'admin', 'page=groups' ) );
	}

	/**
	 * Handles GET requests for a group's page.
	 */
	public function get_group()
	{
		$group = UserGroup::get_by_id( $this->handler_vars['id'] );
		if ( null == $group ) {
			Utils::redirect( URL::get( 'admin', 'page=groups' ) );
		}
		else {

			$tokens = ACL::all_tokens( 'id' );
			$access_names = ACL::$access_names;
			$access_names[] = 'deny';

			// attach access bitmasks to the tokens
			foreach ( $tokens as $token ) {
				$token->access = ACL::get_group_token_access( $group->id, $token->id );
			}

			// separate tokens into groups
			$grouped_tokens = array();
			foreach ( $tokens as $token ) {
				$grouped_tokens[$token->token_group][( $token->token_type ) ? 'crud' : 'bool'][] = $token;
			}

			$potentials = array();

			$users = Users::get_all();

			$users[] = User::anonymous();

			$members = $group->members;
			$jsusers = array();
			foreach ( $users as $user ) {
				$jsuser = new StdClass();
				$jsuser->id = $user->id;
				$jsuser->username = $user->username;
				$jsuser->member = in_array( $user->id, $members );

				$jsusers[$user->id] = $jsuser;
			}

			$this->theme->potentials = $potentials;
			$this->theme->users = $users;
			$this->theme->members = $members;

			$js = '$(function(){groupManage.init(' . json_encode( $jsusers ) . ');});';

			Stack::add( 'admin_header_javascript', $js, 'groupmanage', 'admin' );

			$this->theme->access_names = $access_names;
			$this->theme->grouped_tokens = $grouped_tokens;

			$this->theme->groups = UserGroups::get_all();
			$this->theme->group = $group;
			$this->theme->id = $group->id;

			$this->theme->wsse = Utils::WSSE();

			$this->display( 'group' );
		}

	}

	/**
	 * Handles POST requests to a group's page.
	 */
	public function post_group()
	{
		$group = UserGroup::get_by_id( $this->handler_vars['id'] );
		$tokens = ACL::all_tokens();

		if ( isset( $this->handler_vars['nonce'] ) ) {
			$wsse = Utils::WSSE( $this->handler_vars['nonce'], $this->handler_vars['timestamp'] );

			if ( isset( $this->handler_vars['digest'] ) && $this->handler_vars['digest'] != $wsse['digest'] ) {
				Session::error( _t( 'WSSE authentication failed.' ) );
			}

			if ( isset( $this->handler_vars['delete'] ) ) {
				$group->delete();
				Utils::redirect( URL::get( 'admin', 'page=groups' ) );
			}

			if ( isset( $this->handler_vars['user'] ) ) {
				$users = $this->handler_vars['user'];
				foreach ( $users as $user => $status ) {
					if ( $status == 1 ) {
						$group->add( $user );
					}
					else {
						$group->remove( $user );
					}
				}

				foreach ( $tokens as $token ) {
					$bitmask = new Bitmask( ACL::$access_names );
					if ( isset( $this->handler_vars['tokens'][$token->id]['deny'] ) ) {
						$bitmask->value = 0;
						$group->deny( $token->id );
					}
					else {
						foreach ( ACL::$access_names as $name ) {
							if ( isset( $this->handler_vars['tokens'][$token->id][$name] ) ) {
								$bitmask->$name = true;
							}
						}
						if ( isset( $this->handler_vars['tokens'][$token->id]['full'] ) ) {
							$bitmask->value = $bitmask->full;
						}
						if ( $bitmask->value != 0 ) {
							$group->grant( $token->id, $bitmask );
						}
						else {
							$group->revoke( $token->id );
						}
					}
				}
			}

		}

		Session::notice( _t( 'Updated permissions.' ), 'permissions' );

		Utils::redirect( URL::get( 'admin', 'page=group' ) . '?id=' . $group->id );

	}

	/**
	 * Handles AJAX requests to update groups.
	 */
	public function ajax_update_groups( $handler_vars )
	{
		Utils::check_request_method( array( 'POST' ) );

		echo json_encode( $this->update_groups( $handler_vars ) );
	}

	/**
	 * Handles AJAX requests from the groups page.
	 */
	public function ajax_groups( $handler_vars )
	{
		Utils::check_request_method( array( 'GET', 'HEAD' ) );

		$this->create_theme();

		$output = '';

		foreach ( UserGroups::get_all() as $group ) {
			$this->theme->group = $group;

			$group = UserGroup::get_by_id( $group->id );
			$users = array();
			foreach ( $group->members as $id ) {
				$user = $id == 0 ? User::anonymous() : User::get_by_id( $id );
				if ( $user->id == 0 ) {
					$users[] = '' . $user->displayname . '';
				}
				else {
					$users[] = 'id ) . '">' . $user->displayname . '';
				}
			}

			$this->theme->users = $users;

			$output .= $this->theme->fetch( 'groups_item' );
		}

		$ar = new AjaxResponse();
		$ar->data = array(
			'items' => $output
		);
		$ar->out();
	}

	/**
	 * Add or delete groups.
	 */
	public function update_groups( $handler_vars, $ajax = true )
	{
		$wsse = Utils::WSSE( $handler_vars['nonce'], $handler_vars['timestamp'] );
		if ( ( isset( $handler_vars['digest'] ) && $handler_vars['digest'] != $wsse['digest'] ) || ( isset( $handler_vars['password_digest'] ) && $handler_vars['password_digest'] != $wsse['digest'] ) ) {
			Session::error( _t( 'WSSE authentication failed.' ) );
			return Session::messages_get( true, 'array' );
		}

		if ( isset( $handler_vars['password_digest'] ) || isset( $handler_vars['digest'] ) ) {

			if ( ( isset( $handler_vars['action'] ) && $handler_vars['action'] == 'add' ) || isset( $handler_vars['newgroup'] ) ) {
				if ( isset( $handler_vars['newgroup'] ) ) {
					$name = trim( $handler_vars['new_groupname'] );
				}
				else {
					$name = trim( $handler_vars['name'] );
				}

				$settings = array( 'name' => $name );

				$this->theme->addform = $settings;

				if ( UserGroup::exists( $name ) ) {
					Session::notice( _t( 'The group %s already exists', array( $name ) ) );
					if ( $ajax ) {
						return Session::messages_get( true, 'array' );
					}
					else {
						return;
					}
				}
				elseif ( empty( $name ) ) {
					Session::notice( _t( 'The group must have a name' ) );
					if ( $ajax ) {
						return Session::message_get( true, 'array' );
					}
					else {
						return;
					}
				}
				else {
					$groupdata = array(
						'name' => $name
					);
					$group = UserGroup::create( $groupdata );
					Session::notice( _t( 'Added group %s', array( $name ) ) );
					// reload the groups
					$this->theme->groups = UserGroups::get_all();

					$this->theme->addform = array();
				}

				if ( $ajax ) {
					return Session::messages_get( true, 'array' );
				}
				else {
					if ( !$ajax ) {
						Utils::redirect( URL::get( 'admin', 'page=groups' ) );
					}
				}

			}

			if ( isset( $handler_vars['action'] ) && $handler_vars['action'] == 'delete' && $ajax == true ) {

				$ids = array();

				foreach ( $_POST as $id => $delete ) {

					// skip POST elements which are not group ids
					if ( preg_match( '/^p\d+$/', $id ) && $delete ) {
						$id = (int) substr( $id, 1 );

						$ids[] = array( 'id' => $id );

					}

				}

				$count = 0;

				if ( !isset( $ids ) ) {
					Session::notice( _t( 'No groups deleted.' ) );
					return Session::messages_get( true, 'array' );
				}

				foreach ( $ids as $id ) {
					$id = $id['id'];
					$group = UserGroup::get_by_id( $id );

					$group->delete();

					$count++;
				}

				if ( !isset( $msg_status ) ) {
					$msg_status = _t( 'Deleted %d groups.', array( $count ) );
				}

				Session::notice( $msg_status );

				return Session::messages_get( true, 'array' );
			}
		}

	}

}
?>