<?php
/**
 * @version $Header: /cvsroot/bitweaver/_bit_pigeonholes/Pigeonholes.php,v 1.65 2006/06/18 07:58:13 squareing Exp $
 *
 * +----------------------------------------------------------------------+
 * | Copyright ( c ) 2004, bitweaver.org
 * +----------------------------------------------------------------------+
 * | All Rights Reserved. See copyright.txt for details and a complete list of authors.
 * | Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details
 * |
 * | For comments, please use phpdocu.sourceforge.net documentation standards!!!
 * | -> see http://phpdocu.sourceforge.net/
 * +----------------------------------------------------------------------+
 * | Authors: xing <xing@synapse.plus.com>
 * +----------------------------------------------------------------------+
 *
 * Pigeonholes class
 *
 * @author   xing <xing@synapse.plus.com>
 * @version  $Revision: 1.65 $
 * @package  pigeonholes
 */

/**
 * required setup
 */
require_once( LIBERTY_PKG_PATH.'LibertyAttachable.php' );
require_once( LIBERTY_PKG_PATH.'LibertyStructure.php' );

/**
 * Pigeonholes
 *
 * @package  pigeonholes
 */
class Pigeonholes extends LibertyContent {
	/**
	* initiate class
	* @param $pContentId content id of the pigeonhole - use either one of the ids.
	* @param $pStructureId structure id of the pigeonhole - use either one of the ids.
	* @return none
	* @access public
	**/
	function Pigeonholes( $pStructureId=NULL, $pContentId=NULL ) {
		LibertyContent::LibertyContent();
		$this->registerContentType( PIGEONHOLES_CONTENT_TYPE_GUID, array(
			'content_type_guid' => PIGEONHOLES_CONTENT_TYPE_GUID,
			'content_description' => 'Pigeonhole',
			'handler_class' => 'Pigeonholes',
			'handler_package' => 'pigeonholes',
			'handler_file' => 'Pigeonholes.php',
			'maintainer_url' => 'http://www.bitweaver.org'
		) );
		$this->mContentId = $pContentId;
		$this->mStructureId = $pStructureId;
		$this->mContentTypeGuid = PIGEONHOLES_CONTENT_TYPE_GUID;
	}

	/**
	* load the pigeonhole
	* @param $pExtras boolean - if set to true, pigeonhole content is added as well
	* @return bool TRUE on success, FALSE if it's not valid
	* @access public
	**/
	function load( $pExtras=FALSE ) {
		if( @BitBase::verifyId( $this->mContentId ) || @BitBase::verifyId( $this->mStructureId ) ) {
			global $gBitSystem;
			$lookupColumn = ( @BitBase::verifyId( $this->mContentId ) ? 'lc.`content_id`' : 'ls.`structure_id`' );
			$lookupId = ( @BitBase::verifyId( $this->mContentId ) ? $this->mContentId : $this->mStructureId );
			$query = "SELECT pig.*, ls.`root_structure_id`, ls.`parent_id`, lc.`title`, lc.`data`, lc.`user_id`, lc.`content_type_guid`,
				uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name,
				uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name
				FROM `".BIT_DB_PREFIX."pigeonholes` pig
				INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON ( lc.`content_id` = pig.`content_id` )
				LEFT JOIN `".BIT_DB_PREFIX."liberty_structures` ls ON ( ls.`structure_id` = pig.`structure_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`modifier_user_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` )
				WHERE $lookupColumn=?";
			$result = $this->mDb->query( $query, array( $lookupId ) );

			if( $result && $row = $result->fetchRow() ) {
				$this->mInfo = $row;
				$this->mContentId = $row['content_id'];
				$this->mStructureId = $row['structure_id'];
				$this->mInfo['user'] = $row['creator_user'];
				$this->mInfo['real_name'] = ( isset( $row['creator_real_name'] ) ? $row['creator_real_name'] : $row['creator_user'] );
				$this->mInfo['display_name'] = BitUser::getTitle( $this->mInfo );
				$this->mInfo['editor'] = ( isset( $row['modifier_real_name'] ) ? $row['modifier_real_name'] : $row['modifier_user'] );
				$this->mInfo['display_link'] = $this->getDisplayLink();
			}

			// if the content for the pigeonhole is requested, get it
			if( $pExtras ) {
				$this->mInfo['path'] = $this->getPigeonholePath();
				$this->mInfo['display_path'] = $this->getDisplayPath( $this->mInfo['path'] );
				$this->mInfo['members'] = $this->getMemberList();
				$this->mInfo['members_count'] = count( $this->mInfo['members'] );
			}
		}
		return( count( $this->mInfo ) );
	}

	/**
	* get all content inserted in a given pigeonhole. if no id is given, it gets all content for all pigeonholes
	* @param $pContentId content id of the pigeonhole
	* @return array of pigeonhole members with according title and content type guid
	* @access public
	**/
	function getMemberList( $pListHash=NULL ) {
		global $gBitUser, $gLibertySystem, $gBitSystem;
		$ret = FALSE;

		$where = '';
		$join = '';
		$bindVars = array();
		if( @BitBase::verifyId( $this->mContentId ) || @BitBase::verifyId( $pListHash['content_id'] ) ) {
			$where = " WHERE pig.`content_id` = ? ";
			$bindVars[] = @BitBase::verifyId( $pListHash['content_id'] ) ? $pListHash['content_id'] : $this->mContentId;
		}

		if( !empty( $pListHash['content_type_guid'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " lc.content_type_guid = ? ";
			$bindVars[] = $pListHash['content_type_guid'];
		}

		if( !empty( $pListHash['title'] ) && is_string( $pListHash['title'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " pig.`content_id` = lc2.`content_id` AND UPPER( lc2.`title` ) = ?";
			$join = ", `".BIT_DB_PREFIX."liberty_content` lc2";
			$bindVars[] = strtoupper( $pListHash['title'] );
		}

		$order = "ORDER BY lc.`content_type_guid`, lc.`title` ASC";

		$ret = array();
		$query = "SELECT pigm.*, lc.`content_id`, tct.`content_description`, lc.`last_modified`, lc.`user_id`, lc.`title`, lc.`content_type_guid`, lc.`created`, uu.`login`, uu.`real_name`
			FROM `".BIT_DB_PREFIX."pigeonhole_members` pigm
				INNER JOIN `".BIT_DB_PREFIX."pigeonholes` pig ON ( pig.`content_id` = pigm.`parent_id` )
				INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON ( lc.`content_id` = pigm.`content_id` )
				INNER JOIN `".BIT_DB_PREFIX."liberty_content_types` tct ON ( lc.`content_type_guid` = tct.`content_type_guid` )
				INNER JOIN `".BIT_DB_PREFIX."users_users` uu ON ( uu.`user_id` = lc.`user_id` )
			$join $where $order";
		$result = $this->mDb->query( $query, $bindVars, @BitBase::verifyId( $pListHash['max_records'] ) ? $pListHash['max_records'] : NULL );
		$contentTypes = $gLibertySystem->mContentTypes;
		while( $aux = $result->fetchRow() ) {
			if( !empty( $contentTypes[$aux['content_type_guid']] ) ) {
				$type = &$contentTypes[$aux['content_type_guid']];
				if( empty( $type['content_object'] ) ) {
					// create *one* object for each object *type* to  call virtual methods.
					include_once( $gBitSystem->mPackages[$type['handler_package']]['path'].$type['handler_file'] );
					$type['content_object'] = new $type['handler_class']();
				}
				$aux['display_link'] = $type['content_object']->getDisplayLink( $aux['title'], $aux );
				$aux['title'] = $type['content_object']->getTitle( $aux );
				$ret[] = $aux;
			}
		}
		return( !empty( $this->mErrors ) ? $this->mErrors : $ret );
	}

	/**
	* get all items that are not part of a pigeonhole yet
	* @return array of content not in any pigeonhole yet
	* @access public
	**/
	function getAssignableContent( &$pListHash ) {
		global $gBitUser, $gLibertySystem, $gBitSystem;

		$where = '';
		$bindVars = array();

		if( empty( $pListHash['include_members'] ) ) {
			$where .= "WHERE pigm.`content_id` IS NULL";
		}

		if( !empty( $pListHash['find'] ) && is_string( $pListHash['find'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " UPPER( lc.`title` ) LIKE ?";
			$bindVars[] = ( '%'.strtoupper( $pListHash['find'] ).'%');
		}

		if( !empty( $pListHash['content_type'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " lc.`content_type_guid`=?";
			$bindVars[] = $pListHash['content_type'];
		}

		if( !empty( $pListHash['sort_mode'] ) ) {
			$order = " ORDER BY ".$this->mDb->convert_sortmode( $pListHash['sort_mode'] )." ";
		} else {
			$order = " ORDER BY lc.`content_type_guid`, lc.`title` ASC";
		}

		$query = "SELECT pigm.`parent_id`, lc.`content_id`, lc.`user_id`, lc.`title`, lc.`content_type_guid`, uu.`login`, uu.`real_name`
			FROM `".BIT_DB_PREFIX."liberty_content` lc
			LEFT JOIN `".BIT_DB_PREFIX."pigeonhole_members` pigm ON ( pigm.`content_id` = lc.`content_id` )
			LEFT JOIN `".BIT_DB_PREFIX."users_users` uu ON ( uu.`user_id` = lc.`user_id` )
			$where $order";
		$result = $this->mDb->query( $query, $bindVars, @BitBase::verifyId( $pListHash['max_records'] ) ? $pListHash['max_records'] : NULL );

		$contentTypes = $gLibertySystem->mContentTypes;
		while( $row = $result->fetchRow() ) {
			$i = $row['content_id'];
			$ret[$i] = $row;
			if( !empty( $contentTypes[$row['content_type_guid']] ) ) {
				$type = &$contentTypes[$row['content_type_guid']];
				if( empty( $type['content_object'] ) ) {
					// create *one* object for each object *type* to  call virtual methods.
					include_once( $gBitSystem->mPackages[$type['handler_package']]['path'].$type['handler_file'] );
					$type['content_object'] = new $type['handler_class']();
				}
				$ret[$i]['display_link'] = $type['content_object']->getDisplayLink( $row['title'], $row );
				$ret[$i]['title'] = $type['content_object']->getTitle( $row );
			}

			// generate a map of what items are assigned to what pigeonholes
			if( !empty( $pListHash['include_members'] ) && @BitBase::verifyId( $row['parent_id'] ) ) {
				$map[$i][] = $row['parent_id'];
			}
		}

		// complete the output
		if( !empty( $pListHash['include_members'] ) && !empty( $ret ) ) {
			foreach( $ret as $i => $r ) {
				$ret[$i]['assigned'] = !empty( $map[$i] ) ? $map[$i] : NULL;
			}
		}

		return( !empty( $ret ) ? $ret : NULL );
	}

	/**
	* get an array of paths for all pigeonholes. used for pages where data can be inserted into pigeonholes
	* @param $pContentId content id of pigeonhole.
	* @return path in form of an array on success, FALSE ( boolean ) if content is in no pigeonhole
	* @access public
	**/
	function getPigeonholesPathList( $pContentId=NULL ) {
		$query = "SELECT pig.`content_id`, pig.`structure_id`
			FROM `".BIT_DB_PREFIX."pigeonholes` pig
			INNER JOIN `".BIT_DB_PREFIX."liberty_structures` ls ON ( ls.`structure_id` = pig.`structure_id` )
			ORDER BY ls.`root_structure_id`, ls.`structure_id` ASC";
		$result = $this->mDb->query( $query );
		$pigeonholes = $result->getRows();
		foreach( $pigeonholes as $pigeonhole ) {
			$ret[$pigeonhole['content_id']] = $this->getPigeonholePath( $pigeonhole['structure_id'] );
		}

		if( @BitBase::verifyId( $pContentId ) && $assigned = $this->getPigeonholesFromContentId( $pContentId ) ) {
			foreach( $assigned as $a ) {
				$ret[$a['content_id']][0]['selected'] = TRUE;
			}
		}

		return( !empty( $ret ) ? $ret : FALSE );
	}

	/**
	* get all pigeonholes where the contenent has been inserted
	* @param $pContentId content id of item in question
	* @return basic information about item requested
	* @access public
	**/
	function getPigeonholesFromContentId( $pContentId ) {
		if( @BitBase::verifyId( $pContentId ) ) {
			$query = "SELECT ls.*
				FROM `".BIT_DB_PREFIX."pigeonhole_members` pigm
				INNER JOIN `".BIT_DB_PREFIX."pigeonholes` pig ON ( pig.`content_id` = pigm.`parent_id` )
				INNER JOIN `".BIT_DB_PREFIX."liberty_structures` ls ON ( pig.`structure_id` = ls.`structure_id` )
				WHERE pigm.`content_id`=?";
			$ret = $this->mDb->getAll( $query, array( $pContentId ) );
		}
		return( !empty( $ret ) ? $ret : FALSE );
	}

	/**
	* get the path of a pigeonhole
	* @param $pStructureId structure id of pigeonhole, if no id is given, it gets the id from $this->mStructureId
	* @return path in form of an array
	* @access public
	**/
	function getPigeonholePath( $pStructureId=NULL ) {
		if( !@BitBase::verifyId( $pStructureId ) ) {
			$pStructureId = $this->mStructureId;
		}

		if( @BitBase::verifyId( $pStructureId ) ) {
			global $gStructure;
			// create new object if needed
			if( empty( $gStructure ) ) {
				$gStructure = new LibertyStructure();
			}
			// get the structure path
			$ret = $gStructure->getPath( $pStructureId );
		}
		return( !empty( $ret ) ? $ret : FALSE );
	}

	/**
	* Converts a structure path into valid html links
	* @param $pPath path given by getPigenholePath()
	* @return the link to display the page.
	*/
	function getDisplayPath( $pPath ) {
		$ret = '';
		if( !empty( $pPath ) && is_array( $pPath ) ) {
			foreach( $pPath as $node ) {
				$ret .= ( @BitBase::verifyId( $node['parent_id'] ) ? ' &raquo; ' : '' ).'<a title="'.htmlspecialchars( $node['title'] ).'" href="'.PIGEONHOLES_PKG_URL.'view.php?structure_id='.$node['structure_id'].'">'.htmlspecialchars( $node['title'] ).'</a>';
			}
		}
		return $ret;
	}

	/**
	* get list of all pigeonholes
	* @param $pListHash contains array of items used to limit search results
	* @param $pListHash[sort_mode] column and orientation by which search results are sorted
	* @param $pListHash[find] search for a pigeonhole title - case insensitive
	* @param $pListHash[max_records] maximum number of rows to return
	* @param $pListHash[offset] number of results data is offset by
	* @param $pListHash[title] pigeonhole name
	* @param $pListHash[parent_id] pigeonhole parent_id, optional
	* @param $pListHash[root_structure_id] 
	* @param $pListHash[load_only_root]
	* @param $pListHash[parent_content_id] all the sons of the pigeonhole parent content_id , optional
	* @param $pListHash[load_also_root] if parent_content_id is set load also the father, optionnal
	* @return array of pigeonholes in 'data' and count of pigeonholes in 'cant'
	* @access public
	**/
	function getList( &$pListHash ) {
		global $gBitSystem, $gBitUser;
		LibertyContent::prepGetList( $pListHash );

		$ret = $bindVars = array();
		$where = $order = $join = '';

		if( @BitBase::verifyId( $pListHash['root_structure_id'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " ls.`root_structure_id`=? ";
			$bindVars[] = $pListHash['root_structure_id'];
		}

		if( !empty( $pListHash['load_only_root'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " ls.`structure_id`=ls.`root_structure_id` ";
		}

		if( !empty( $pListHash['find'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= " UPPER( lc.`title` ) LIKE ? ";
			$bindVars[] = '%'.strtoupper( $pListHash['find'] ).'%';
		}

		if ( !empty( $pListHash['title'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .=  ' lc.`title` = ?';
			$bindVars[] = $pListHash['title'];
		}

		if ( isset( $pListHash['parent_id'] ) ) {
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= ' ls.`parent_id` = ? ';
			$bindVars[] = $pListHash['parent_id'];
		}

		if ( !empty( $pListHash['parent_content_id'] ) ) {
			$join .= 'INNER JOIN `'.BIT_DB_PREFIX.'liberty_structures` lsf ON (ls.`parent_id` = lsf.`structure_id`';
			if ( !empty( $pListHash['load_also_root'] ) ) {
			 	$join .= 'OR ls.`structure_id`= lsf.`structure_id`';
			}
			$join .= ')';
			$where .= empty( $where ) ? ' WHERE ' : ' AND ';
			$where .= ' lsf.`content_id` = ? ';
			$bindVars[] = $pListHash['parent_content_id'];
		}

		if( !empty( $pListHash['sort_mode'] ) ) {
			$order .= " ORDER BY ".$this->mDb->convert_sortmode( $pListHash['sort_mode'] )." ";
		} else {
			// default sort mode makes list look nice
			$order .= " ORDER BY ls.`root_structure_id`, ls.`structure_id` ASC";
		}

		$query = "SELECT pig.*, ls.`root_structure_id`, ls.`parent_id`, lc.`title`, lc.`data`, lc.`user_id`, lc.`content_type_guid`,
			uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name,
			uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name
			FROM `".BIT_DB_PREFIX."pigeonholes` pig
				INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON ( lc.`content_id` = pig.`content_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`modifier_user_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` )
				INNER JOIN `".BIT_DB_PREFIX."liberty_structures` ls ON ( ls.`structure_id` = pig.`structure_id` )
			$join $where $order";

		$result = $this->mDb->query( $query, $bindVars, $pListHash['max_records'], $pListHash['offset'] );

		while( $aux = $result->fetchRow() ) {
			$content_ids[] = $aux['content_id'];
			$aux['user'] = $aux['creator_user'];
			$aux['real_name'] = ( isset( $aux['creator_real_name'] ) ? $aux['creator_real_name'] : $aux['creator_user'] );
			$aux['display_name'] = BitUser::getTitle( $aux );
			$aux['editor'] = ( isset( $aux['modifier_real_name'] ) ? $aux['modifier_real_name'] : $aux['modifier_user'] );
			$aux['display_link'] = Pigeonholes::getDisplayLink( $aux['title'], $aux );

			if( !empty( $pListHash['force_extras'] ) || ( !empty( $pListHash['load_extras'] ) && $aux['structure_id'] == @$pListHash['structure_id'] ) ) {
				$aux['path'] = $this->getPigeonholePath( $aux['structure_id'] );
				$aux['display_path'] = Pigeonholes::getDisplayPath( $aux['path'] );
				$aux['members'] = $this->getMemberList( array( 'content_id' => $aux['content_id'] ) );
				$aux['members_count'] = count( $aux['members'] );
				if( $gBitSystem->getConfig( 'pigeonholes_list_style' ) == 'table' ) {
					$this->alphabetiseMembers( $aux['members'] );
				}
			}

			$ret[$aux['structure_id']] = $aux;
		}

		$query = "SELECT COUNT( lc.`title` )
			FROM `".BIT_DB_PREFIX."pigeonholes` pig
				INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON ( lc.`content_id` = pig.`content_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`modifier_user_id` )
				LEFT JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` )
				INNER JOIN `".BIT_DB_PREFIX."liberty_structures` ls ON ( ls.`structure_id` = pig.`structure_id` )
			$join $where";
		$pListHash['cant'] = $this->mDb->getOne( $query, $bindVars );

		LibertyContent::postGetList( $pListHash );
		return $ret;
	}

	/**
	* Check permissions of all nodes that lead to this
	* @return a nicely grouped set of pigeonhole members in a set of columns and starting letters.
	* @access public
	**/
	function checkPathPermissions( $pPath ) {
		global $gBitUser, $gBitSystem;
		if( $gBitSystem->getConfig( 'pigeonholes_permissions' ) || $gBitSystem->getConfig( 'pigeonholes_groups' ) ) {
			foreach( $pPath as $path ) {
				$contentIds[] = $path['content_id'];
			}
			if( !empty( $contentIds ) ) {
				$query = "SELECT `pref_name`, `pref_value` FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `content_id` IN( ".preg_replace( "/,$/", "", str_repeat( "?,", count( $contentIds ) ) )." ) ";
				$result = $this->mDb->query( $query, $contentIds );
				while( $aux = $result->fetchRow() ) {
					${$aux['pref_name']} = $aux['pref_value'];
					if( ( !empty( $group_id ) && !$gBitUser->isInGroup( $group_id ) ) || ( !empty( $permission ) && !$gBitUser->hasPermission( $permission ) ) ) {
						return FALSE;
					}
				}
			}
		}
		return TRUE;
	}

	/**
	* Alphabetise all member items
	* @return a nicely grouped set of pigeonhole members in a set of columns and starting letters.
	* @access public
	**/
	function alphabetiseMembers( &$pMememberHash ) {
		global $gBitSystem;
		if( !empty( $pMememberHash ) ) {
			usort( $pMememberHash, "pigeonholes_alphabetiser" );
			$per_column = ceil( count( $pMememberHash ) / $gBitSystem->getConfig( 'pigeonhole_display_columns', 3 ) );
			$i = 1;
			$j = 1;
			foreach( $pMememberHash as $member ) {
				$column = ( $i++ % $per_column == 0 ) ? $j++ : $j;
				$index = strtoupper( substr( $member['title'], 0, 1 ) );
				// check if the previous column was using the same letter as we want to use in the new column
				if(  !empty( $ret[$column - 1][$index] ) || !empty( $ret[$column - 1]["&hellip;".$index] ) ) {
					$index = "&hellip;".$index;
				}
				$ret[$column][$index][] = $member;
			}
			$pMememberHash = $ret;
			unset( $ret );
		}
	}

	/**
	* Store pigeonhole data
	* @param $pParamHash contains all data to store the pigeonholes
	* @param $pParamHash[title] title of the new pigeonhole
	* @param $pParamHash[edit] description of the pigeonhole
	* @param $pParamHash[members] array of content_ids that are associated with this pigeonhole
	* @param $pParamHash[root_structure_id] if this is set, it will add the pigeonhole to this structure. if it's not set, a new structure / top level pigeonhole is created
	* @param $pParamHash[parent_id] set the structure_id that will server as the parent in the structure
	* @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
	* @access public
	**/
	function store( &$pParamHash ) {
		if( $this->verify( $pParamHash ) && LibertyAttachable::store( $pParamHash ) ) {
			$table = BIT_DB_PREFIX."pigeonholes";
			$this->mDb->StartTrans();

			// this really confusing, strange order way of saving items is due to strange behaviour of GenID
			// probably has to do with not null default nextval('public.liberty_structures_id_seq'::text)
			if( !empty( $pParamHash['update'] ) ) {
				if( !empty( $pParamHash['pigeonhole_store'] ) ) {
					$result = $this->mDb->associateUpdate( $table, $pParamHash['pigeonhole_store'], array("content_id" => $this->mContentId ) );
				}
				$pParamHash['structure_location_id'] = $this->mStructureId;
			} else {
				// update the pigeonhole_store and structure_store content_id with the one from LibertyAttachable::store()
				$pParamHash['structure_store']['content_id'] = $pParamHash['content_id'];
				$pParamHash['pigeonhole_store']['content_id'] = $pParamHash['content_id'];

				// we need to store the new structure node now
				global $gStructure;
				// create new object if needed
				if( empty( $gStructure ) ) {
					$gStructure = new LibertyStructure();
				}
				$pParamHash['structure_location_id'] = $gStructure->storeNode( $pParamHash['structure_store'] );

				// get the corrent structure_id
				// structure_id has to be done like this since it's screwed up in the schema
				$pParamHash['pigeonhole_store']['structure_id'] =  $this->mDb->getOne( "SELECT MAX( `structure_id` ) FROM `".BIT_DB_PREFIX."liberty_structures`" );
				$result = $this->mDb->associateInsert( $table, $pParamHash['pigeonhole_store'] );
			}

			// store content items
			if( !empty( $pParamHash['pigeonhole_members_store'] ) ) {
				// remove items first
				$this->expungePigeonholeMember( array( 'parent_id' => $this->mContentId ) );
				if( !$this->insertPigeonholeMember( $pParamHash['pigeonhole_members_store'] ) ) {
					$this->mErrors['store'] = 'The content could not be inserted into the respective categories.';
				}
			}

			$this->mDb->CompleteTrans();
			$this->load();
		}
		return( count( $this->mErrors ) == 0 );
	}

	/**
	* verify, clean up and prepare data to be stored
	* @param $pParamHash all information that is being stored. will update $pParamHash by reference with fixed array of itmes
	* @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
	* @access private
	**/
	function verify( &$pParamHash ) {
		// make sure we're all loaded up if everything is valid
		if( $this->isValid() && empty( $this->mInfo ) ) {
			$this->load( TRUE );
		}

		// It is possible a derived class set this to something different
		if( empty( $pParamHash['content_type_guid'] ) ) {
			$pParamHash['content_type_guid'] = $this->mContentTypeGuid;
		}

		if( @BitBase::verifyId( $this->mContentId ) ) {
			$pParamHash['content_id'] = $this->mContentId;
			$pParamHash['update'] = TRUE;
		}

		// content store
		// check for name issues, first truncate length if too long
		if( !empty( $pParamHash['title'] ) )  {
			if( !@BitBase::verifyId( $this->mContentId ) ) {
				if( empty( $pParamHash['title'] ) ) {
					$this->mErrors['title'] = 'You must enter a name for this category.';
				} else {
					$pParamHash['content_store']['title'] = substr( $pParamHash['title'], 0, 160 );
				}
			} else {
				$pParamHash['content_store']['title'] = ( isset( $pParamHash['title'] ) ) ? substr( $pParamHash['title'], 0, 160 ) : $this->mInfo['title'];
			}
		} elseif( empty( $pParamHash['title'] ) ) {
			// no name specified
			$this->mErrors['title'] = 'You must enter a name for this category.';
		}

		// sort out the description
		if( $this->isValid() && !empty( $this->mInfo['data'] ) && empty( $pParamHash['edit'] ) ) {
			$pParamHash['edit'] = '';
		} elseif( empty( $pParamHash['edit'] ) ) {
			unset( $pParamHash['edit'] );
		} else {
			$pParamHash['edit'] = substr( $pParamHash['edit'], 0, 250 );
		}

		// pigeonhole member store
		// work out what to do with the content of the pigeonhole
		if( $this->isValid() && !empty( $this->mInfo['members'] ) && empty( $pParamHash['members'] ) ) {
			$pParamHash['pigeonhole_members_store']['members'] = '';
		} elseif( empty( $pParamHash['members'] ) ) {
			unset( $pParamHash['members'] );
		} else {
			$i = 1;
			foreach( $pParamHash['members'] as $c_id ) {
				$pParamHash['pigeonhole_members_store'][$i]['content_id'] = $c_id;
				$i++;
			}
		}

		// individual pigeonhole preference store
		$pParamHash['preferences_store'] = !empty( $pParamHash['prefs'] ) ? $pParamHash['prefs'] : NULL;

		// structure store
		if( @BitBase::verifyId( $pParamHash['root_structure_id'] ) ) {
			$pParamHash['structure_store']['root_structure_id'] = $pParamHash['root_structure_id'];
		} else {
			$pParamHash['structure_store']['root_structure_id'] = NULL;
		}

		if( @BitBase::verifyId( $pParamHash['parent_id'] ) ) {
			$pParamHash['structure_store']['parent_id'] = $pParamHash['parent_id'];
		} else {
			$pParamHash['structure_store']['parent_id'] = NULL;
		}

		return( count( $this->mErrors ) == 0 );
	}

	/**
	* Store pigeonhole member
	* @param $pParamHash an array of content to be stored.
	* @param $pParamHash[parent_id] id of pigeonhole it belongs to, default is $this->mContentId
	* @param $pParamHash[content_id] content_id of the item to be stored
	* @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
	* @access public
	**/
	function insertPigeonholeMember( &$pParamHash ) {
		if( $this->verifyPigeonholeMember( $pParamHash ) ) {
			foreach( $pParamHash['member_store'] as $item ) {
				$result = $this->mDb->associateInsert( BIT_DB_PREFIX."pigeonhole_members", $item );
			}
		} else {
			vd( $this->mErrors );
		}
		return( count( $this->mErrors ) == 0 );
	}

	/**
	* verify, clean up and prepare data to be stored
	* @param $pParamHash all information that is being stored. will update $pParamHash by reference with fixed array of itmes
	* @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
	* @access private
	**/
	function verifyPigeonholeMember( &$pParamHash ) {
		$this->mDb->StartTrans();
		foreach( $pParamHash as $key => $item ) {
			if( isset( $item['parent_id'] ) && @BitBase::verifyId( $item['parent_id'] ) ) {
				$tmp['member_store'][$key]['parent_id'] = $item['parent_id'];
			} elseif( @BitBase::verifyId( $this->mContentId ) ) {
				$tmp['member_store'][$key]['parent_id'] = $this->mContentId;
				$pParamHash[$key]['parent_id'] = $this->mContentId;
			} else {
				$this->mErrors['store_members'] = tra( 'The content could not be inserted because the parent_id was missing.' );
			}

			if( isset( $item['content_id'] ) && @BitBase::verifyId( $item['content_id'] ) ) {
				$tmp['member_store'][$key]['content_id'] = $item['content_id'];
			} else {
				$this->mErrors['store_members'] = 'The content id is not valid.';
			}
		}

		$this->mDb->CompleteTrans();
		$pParamHash = $tmp;
		return( count( $this->mErrors ) == 0 );
	}

	/**
	* Expunge pigeonhole member
	* @param $pParamHash['parent_id'] parent_id of content to be deleted
	* @param $pParamHash['member_id'] content_id of content to be deleted
	* @param $pParamHash['deletables'] array of content_ids to check against when deleting. makes sure that only members of a given structure are removed
	*	Note if only one of the 2 ids is given, all items with that id will be removed. if both are given, only that one particular entry is removed
	* @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
	* @access public
	**/
	function expungePigeonholeMember( $pParamHash ) {
		if( @BitBase::verifyId( $pParamHash['parent_id'] ) || @BitBase::verifyId( $pParamHash['member_id'] ) ) {
			$where = '';
			$bindVars = array();

			if( @BitBase::verifyId( $pParamHash['parent_id'] ) ) {
				$where .= " WHERE `parent_id`=? ";
				$bindVars[] = $pParamHash['parent_id'];
			}

			if( @BitBase::verifyId( $pParamHash['member_id'] ) ) {
				$where .= ( empty( $where ) ? " WHERE " : " AND " )." `content_id`=? ";
				$bindVars[] = $pParamHash['member_id'];
			}

			if( !empty( $pParamHash['deletables'] ) && is_array( $pParamHash['deletables'] ) ) {
				// only delete member data when it's part of the deletable structure
				$where .= ( empty( $where ) ? " WHERE " : " AND " )." `parent_id` IN( ".preg_replace( "/,$/", "", str_repeat( "?,", count( $pParamHash['deletables'] ) ) )." ) ";
				$bindVars = array_merge( $bindVars, $pParamHash['deletables'] );
			}

			// now we're ready to remove the actual members
			$query = "DELETE FROM `".BIT_DB_PREFIX."pigeonhole_members` $where";
			$result = $this->mDb->query( $query, $bindVars );
		} else {
			$this->mErrors['members_store'] = 'The category member(s) could not be removed.';
		}
		return( count( $this->mErrors ) == 0 );
	}

	/**
	* Expunge currently loaded pigeonhole
	* @return bool TRUE on success, FALSE if store could not occur.
	* @access public
	**/
	function expunge( $pStructureId = NULL ) {
		$ret = FALSE;
		// if we have a custom structure id we want to remove, load it
		if( @BitBase::verifyId( $pStructureId ) ) {
			$this->mStructureId = $pStructureId;
			$this->load();
		}

		if( $this->isValid() ) {
			$this->mDb->StartTrans();
			// get all items that are part of the sub tree
			require_once( LIBERTY_PKG_PATH.'LibertyStructure.php' );
			$struct = new LibertyStructure();

			// include the current structure id as well
			$structureIds[] = $this->mStructureId;
			$tree = $struct->getSubTree( $this->mStructureId );
			foreach( $tree as $node ) {
				$structureIds[] = $node['structure_id'];
			}

			$structureIds = array_unique( $structureIds );
			$where = '';
			foreach( $structureIds as $structureId ) {
				$where .= ( empty( $where ) ? " WHERE " : " OR ")."`structure_id`=?";
			}
			$result = $this->mDb->query( "SELECT `content_id` FROM `".BIT_DB_PREFIX."liberty_structures` $where", $structureIds );
			$contentIds = $result->getRows();

			foreach( $contentIds as $id ) {
				// now we have the content ids - let the nuking begin
				$query = "DELETE FROM `".BIT_DB_PREFIX."pigeonholes` WHERE `content_id` = ?";
				$result = $this->mDb->query( $query, array( $id['content_id'] ) );
				$query = "DELETE FROM `".BIT_DB_PREFIX."pigeonhole_members` WHERE `parent_id` = ?";
				$result = $this->mDb->query( $query, array( $id['content_id'] ) );

				// remove all entries from content tables
				$this->mContentId = $id['content_id'];
				if( LibertyAttachable::expunge() ) {
					$ret = TRUE;
					$this->mDb->CompleteTrans();
				} else {
					$this->mDb->RollbackTrans();
				}
			}

			// finally nuke the structure in liberty_structures
			$struct->s_remove_page( $this->mStructureId, FALSE );
		}
		return $ret;
	}

	/**
	* Generates the URL to this pigeonhole
	* @param $pContentId is the pigeonhole id we want to see
	* @return the link to display the page.
	*/
	function getDisplayUrl( $pContentId=NULL, $pMixed=NULL ) {
		global $gBitSystem;
		$ret = NULL;
		// try to get the correct content_id from anywhere possible
		if( !@BitBase::verifyId( $pContentId ) && !empty( $this ) ) {
			$pContentId = $this->mContentId;
		} elseif( !@BitBase::verifyId( $pContentId ) && !empty( $pMixed ) ) {
			$pContentId = $pMixed['content_id'];
		}

		if( @BitBase::verifyId( $pContentId ) ) {
			$rewrite_tag = $gBitSystem->isFeatureActive( 'pretty_urls_extended' ) ? 'view/' : '';
			if( $gBitSystem->isFeatureActive( 'pretty_urls' ) || $gBitSystem->isFeatureActive( 'pretty_urls_extended' ) ) {
				$ret = PIGEONHOLES_PKG_URL.$rewrite_tag.$pContentId;
			} else {
				$ret = PIGEONHOLES_PKG_URL.'view.php?content_id='.$pContentId;
			}
		}
		return $ret;
	}

	/**
	* Returns HTML link to display a pigeonhole
	* @param $pTitle is the pigeonhole we want to see
	* @param $pContentId content id of the pigeonhole in question
	* @return the link to display the page.
	*/
	function getDisplayLink( $pTitle=NULL, $pMixed=NULL ) {
		global $gBitSystem;
		if( empty( $pTitle ) && !empty( $this ) ) {
			$pTitle = $this->getTitle();
		}

		if( empty( $pMixed ) && !empty( $this ) ) {
			$pMixed = $this->mInfo;
		}

		$ret = $pTitle;
		if( !empty( $pTitle ) && !empty( $pMixed ) ) {
			if( $gBitSystem->isPackageActive( 'pigeonholes' ) ) {
				$ret = '<a title="'.htmlspecialchars( $pTitle ).'" href="'.Pigeonholes::getDisplayUrl( $pMixed['content_id'] ).'">'.htmlspecialchars( $pTitle ).'</a>';
			}
		}

		return $ret;
	}
}

function pigeonholes_alphabetiser($a, $b) {
	return strcasecmp( $a["title"], $b["title"] );
}


// ============= SERVICE FUNCTIONS =============

function pigeonholes_content_display( &$pObject ) {
	global $gBitSystem, $gBitSmarty, $gBitUser, $gPreviewStyle;
	if( $gBitSystem->isFeatureActive( 'pigeonholes_display_members' ) || $gBitSystem->isFeatureActive( 'pigeonholes_display_path' ) ) {
		$pigeonholes = new Pigeonholes();
		if( $gBitUser->hasPermission( 'p_pigeonholes_view' ) ) {
			if( $pigeons = $pigeonholes->getPigeonholesFromContentId( $pObject->mContentId ) ) {
				foreach( $pigeons as $pigeon ) {
					$pigeonholes->mContentId = $pigeon['content_id'];
					$pigeonholes->load( TRUE );
					//$pigeonholes->loadPreferences();
					$pigeonData[] = $pigeonholes->mInfo;
					// set the theme chosen for this page - virtually random if page is part of multiple themes
					if( $gBitSystem->isFeatureActive( 'pigeonholes_themes' ) ) {
						$pigeonholes->loadPreferences();
						if( $g = $pigeonholes->getPreference( 'style' ) ) {
							$gPreviewStyle = $g;
						}
					}
					// we need to check all pigeonholes in the path, load the prefs and work out if the user is allowed to view the page
					if( !$pigeonholes->checkPathPermissions( $pigeonholes->getField( 'path' ) ) ) {
						$msg = tra( "This content is part of a category to which you have no access to. Please log in or request the appropriate permission from the site administrator." );
						$gBitSystem->fatalPermission( NULL, $msg );
					}
				}
				$gBitSmarty->assign( 'pigeonData', !empty( $pigeonData ) ? $pigeonData : FALSE );
			}
		}
	}
}

function pigeonholes_content_edit( $pObject=NULL ) {
	global $gBitSmarty, $gBitUser;
	$pigeonPathList = array();

	if( $gBitUser->hasPermission( 'p_pigeonholes_insert_member' ) ) {
		$pigeonholes = new Pigeonholes();

		// get pigeonholes path list
		if( $pigeonPathList = $pigeonholes->getPigeonholesPathList( !empty( $pObject->mContentId ) ? $pObject->mContentId : NULL ) ) {
			$gBitSmarty->assign( 'pigeonPathList', $pigeonPathList );
		}
	}
}

function pigeonholes_content_expunge( $pObject=NULL ) {
	$pigeonholes = new Pigeonholes();
	$pigeonholes->expungePigeonholeMember( array( 'member_id' => $pObject->mContentId ) );
}

function pigeonholes_content_preview() {
	global $gBitSmarty, $gBitUser;
	$pigeonPathList = array();

	if( $gBitUser->hasPermission( 'p_pigeonholes_insert_member' ) ) {
		$pigeonholes = new Pigeonholes();

		// get pigeonholes path list
		if( $pigeonPathList = $pigeonholes->getPigeonholesPathList() ) {
			foreach( $pigeonPathList as $key => $path ) {
				if( !empty( $_REQUEST['pigeonholes']['pigeonhole'] ) && in_array( $key, $_REQUEST['pigeonholes']['pigeonhole'] ) ) {
					$pigeonPathList[$key][0]['selected'] = TRUE;
				} else {
					$pigeonPathList[$key][0]['selected'] = FALSE;
				}
			}
			$gBitSmarty->assign( 'pigeonPathList', $pigeonPathList );
		}
	}
}

function pigeonholes_content_store( $pObject, $pParamHash ) {
	global $gBitSmarty, $gBitUser;
	if( $gBitUser->hasPermission( 'p_pigeonholes_insert_member' ) ) {
		if( !empty( $pParamHash['content_id'] ) ) {
			if( is_object( $pObject ) && empty( $pParamHash['content_id'] ) ) {
				$pParamHash['content_id'] = $pObject->mContentId;
			}

			$pigeonholes = new Pigeonholes();
			$pigeonPathList = $pigeonholes->getPigeonholesPathList( $pParamHash['content_id'] );

			// here we need to work out if we need to save at all
			// get all originally selected items
			$selectedItem = array();
			if( !empty( $pigeonPathList ) ) {
				foreach( $pigeonPathList as $path ) {
					if( !empty( $path[0]['selected'] ) ) {
						$pathItem = array_pop( $path );
						$selectedItem[] = $pathItem['content_id'];
					}
				}
			}

			// quick and dirty check to start off with
			if( empty( $_REQUEST['pigeonholes'] ) || count( $_REQUEST['pigeonholes']['pigeonhole'] ) != count( $selectedItem ) ) {
				$modified = TRUE;
			} else {
				// more thorough check
				foreach( $selectedItem as $item ) {
					if( !in_array( $item, $_REQUEST['pigeonholes']['pigeonhole'] ) ) {
						$modified = TRUE;
					}
				}
			}

			if( !empty( $modified ) ) {
				// first remove all entries with this content_id
				if( $pigeonholes->expungePigeonholeMember( array( 'member_id' => $pParamHash['content_id'] ) ) && !empty( $_REQUEST['pigeonholes'] ) ) {
					// insert the content into the desired pigeonholes
					foreach( $_REQUEST['pigeonholes']['pigeonhole'] as $p_id ) {
						$memberHash[] = array(
							'parent_id' => $p_id,
							'content_id' => $pParamHash['content_id']
						);
					}

					if( !$pigeonholes->insertPigeonholeMember( $memberHash ) ) {
						$gBitSmarty->assign( 'msg', tra( "There was a problem inserting the content into the pigeonholes." ) );
						$gBitSystem->display( 'error.tpl' );
						die;
					}
				}
			}
		}
	}
}
/**
 * filter the search with pigeonholes
 * @param $pParamHash['pigeonholes'] - a pigeonhole or an array of pigeonhole content_id
 **/
function pigeonholes_content_list_sql ( &$pObject, $pParamHash = '') {
	global $gBitSystem;
	$ret = array();
	if( /*$gBitSystem->isFeatureActive( 'pigeonholes_categorize_'.$pObject->getContentType() ) && */ !empty( $pParamHash['pigeonholes'] ) ) {
		if ( is_array( $pParamHash['pigeonholes'] ) ) {
			$ret['join_sql'] = "LEFT JOIN `".BIT_DB_PREFIX."pigeonhole_members` pm ON (lc .`content_id`= pm.`content_id`)";
			$ret['where_sql'] = ' AND pm.`parent_id` in ('.implode( ',', array_fill(0, count( $pParamHash['pigeonholes']  ), '?' ) ).')';
			$ret['bind_vars'] = $pParamHash['pigeonholes'];
		} else {
			$ret['join_sql'] = "LEFT JOIN `".BIT_DB_PREFIX."pigeonhole_members` pm ON (lc .`content_id`= pm.`content_id`)";
			$ret['where_sql'] = " AND pm.`parent_id`=? ";
			$ret['bind_vars'][] = $pParamHash['pigeonholes'];
		}
	}
	return $ret;
}
?>
