<?php
if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 5.0.0 Beta 10
|| # ---------------------------------------------------------------- # ||
|| # Copyright �2000-2012 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/


/**
 * vB_Library_Node
 *
 * @package vBApi
 * @access public
 */
class vB_Library_Node extends vB_Library
{

	/** @TODO: this is duplicated info which is already declared in querydef. We should read it from there.*/
	protected $nodeFields = array();

	protected $contentAPIs = array();

	protected $albumChannel = false;
	protected $VMChannel = false;
	protected $PMChannel = false;
	protected $ReportChannel = false;

	protected function __construct()
	{

		$this->pmContenttypeid = vB_Types::instance()->getContentTypeId('vBForum_PrivateMessage');
		$structure = vB::getDbAssertor()->fetchTableStructure('vBForum:node');
		$this->nodeFields = $structure['structure'];
	}

	/** Return the list of fields in the node table
	*
	*
	**/
	public function getNodeFields()
	{
		return $this->nodeFields;
	}


	/** This clear cache for all children of a node list
	*
	* 	@param	mixed	array of nodes
	**/
	public function clearChildCache($nodeids)
	{
		$childrenArray = $this->fetchClosurechildren($nodeids);
		$events = array();
		foreach ($childrenArray as $children)
		{
			foreach ($children as $child)
			$events[] = 'nodeChg_' . $child['child'];
		}
		vB_Cache::instance()->event($events);
		vB_Cache::instance(vB_Cache::CACHE_FAST)->event($events);
	}


	/** opens a node for posting
	 *
	 * 	@param	mixed	integer or array of integers
	 *
	 *	@return	mixed	Either array 'errors' => error string or array of id's.
	 **/
	public function openNode($nodeids)
	{
		vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
			'open' => 1, 'showopen' => 1, 'nodeid' => $nodeids));

		foreach ($nodeids as $nodeid)
		{
			$result = vB::getDbAssertor()->assertQuery('vBForum:openNode', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
				'nodeid' => $nodeid));

			if (!empty($result['errors']))
			{
				break;
			}
		}


		$this->clearCacheEvents($nodeids);
		$this->clearChildCache($nodeids);

		if (!empty($result['errors']))
		{
				return array('errors' => $result['errors']);
		}
		return $nodeids;

	}

	/** Closes a node for posting. Closed nodes can still be viewed but nobody can reply to one.
	 *
	 * 	@param	mixed	integer or array of integers
	 *
	 *	@return	mixed	Either array 'errors' => error string or array of id's.
	 **/
	public function closeNode($nodeids)
	{

		vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
			'open' => 0, 'showopen' => 0, 'nodeid' => $nodeids));


		foreach ($nodeids as $nodeid)
		{
			$result = vB::getDbAssertor()->assertQuery('vBForum:closeNode', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
				'nodeid' => $nodeid));
			if (!empty($result['errors']))
			{
				breaki;
			}
		}
		$this->clearCacheEvents($nodeids);
		$this->clearChildCache($nodeids);

		if (!empty($result['errors']))
		{
			return array('errors' => $result['errors']);
		}
		return $nodeids;
	}

	/** Adds a new node. The record must already exist as an individual content item.
	 *
	 *	@param	integer	The id in the primary table
	 *	@param	integer	The content type id of the record to be added
	 *	@param	mixed		Array of field => value pairs which define the record.
	 *
	 * 	@return	boolean
	 **/
	public function addNode($contenttypeid, $data)
	{
		$params = array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_METHOD, 'contenttypeid' => $contenttypeid);

		foreach ($this->nodeFields as $fieldname)
		{
			if (isset($data[$fieldname]))
			{
				$params[$fieldname] = $data[$fieldname];
			}
		}


		$result = vB::getDbAssertor()->assertQuery('vBForum:addNode', $params);

		//If this is not a channel, we should set the lastcontentid to this nodeid,
		// and lastcontent to now.
		if ( ($data['contenttypeid'] <> vB_Types::instance()->getContentTypeID('vBForum_Channel')) AND
			empty($data['lastcontentid']))
		{
			vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_UPDATE,
				'nodeid' => $result, 'lastcontent' => vB::getRequest()->getTimeNow(), 'lastcontentid' => $result));
		}

		if (!empty($result))
		{
			vB_Api::instanceInternal('Search')->index($result);

		}
		return($result);
	}

	/** Permanently deletes a node
	 *	@param	integer	The nodeid of the record to be deleted
	 *
	 *	@return	boolean
	 **/
	public function deleteNode($nodeid)
	{
		if (!intval($nodeid))
		{
			return false;
		}

		//If it's a protected channel, don't allow removal.
		$existing = $this->getNode($nodeid);

		if (empty($existing))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		if ($existing['protected'])
		{
			//O.K. if it's not a channel.
			if ($existing['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Channel'))
			{
				throw new vB_Exception_Api('no_delete_permissions');
			}
		}

		$ancestorsId = array();
		$ancestors = $this->fetchClosureParent($nodeid);

		foreach ($ancestors as $parent)
		{
			$ancestorsId[] = $parent['parent'];
		}

		$result = vB::getDbAssertor()->assertQuery('vBForum:deleteNode', array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_METHOD, 'nodeid' => $nodeid, 'delete_subnodes' => true));

		if(!empty($result))
		{
			vB_Api::instanceInternal('Search')->delete($nodeid);
			vB_Api::instanceInternal('Search')->purgeCacheForCurrentUser();

			// now update last content and counts for parents
			$ancestorsId = array_unique($ancestorsId);
			// reset last content for all parents that have the deleted node
			vB::getDbAssertor()->update('node',
					array(
						'lastcontent' => 0,
						'lastcontentid' => 0,
						'lastcontentauthor' => '',
						'lastauthorid' => 0),
					array(
						array('field' => 'nodeid', 'value' => $ancestorsId)
					)
			);
			foreach ($ancestorsId AS $ancestor)
			{
				vB_Node::fixNodeCount($ancestor);
				vB::getDbAssertor()->assertQuery('vBForum:updateLastData', array('parentid' => $ancestor, 'timenow' => vB::getRequest()->getTimeNow()));
				vB_Cache::instance(vB_Cache::CACHE_FAST)->event("nodeChg_" . $ancestor);
				vB_Cache::instance()->event("nodeChg_" . $ancestor);
			}
		}

		$loginfo[] = array(
				'nodeid' => $nodeid,
				'nodetitle' => $existing['title']
		);

		// logging the action
		require_once(DIR . '/includes/functions_log_error.php');
		$userinfo = vB::getCurrentSession()->fetch_userinfo();
		$route = 'profile';
		if (vB5_Cookie::isEnabled())
		{
			// session is stored in cookies, so do not append it to url
			$route .= '|nosession';
		}
		$baseurl = class_exists('vB5_Config') ? vB5_Config::instance()->baseurl : vB::getDatastore()->getOption('bburl');
		$url = '<a href="' . $baseurl . vB5_Route::buildUrl($route, $userinfo) . '">' . $userinfo['username'] . '</a>';

		log_moderator_action($loginfo, 'node_x_by_y_removed', array($existing['title'], url));

		return($result);

	}
	/** Permanently/Temporarily deletes a set of nodes
	 *	@param	array	The nodeids of the records to be deleted
	 *	@param	bool	hard/soft delete
	 *	@param	string	the reason for soft delete (not used for hard delete)
	 *
	 *	@return	array nodeids that were deleted
	 **/
	public function deleteNodes($deleteNodeIds, $hard, $reason, $ancestorsId, $starters)
	{
		//if we are doing a hard delete we need to first delete the nodeids.
		if ($hard)
		{
			foreach ($deleteNodeIds AS $nodeid)
			{
				try
				{
					$node = $this->getNode($nodeid);
					$api = vB_Api_Content::getContentApi($node['contenttypeid'])->delete($nodeid);
				}
				//Note that if one of the nodes to be deleted is a child of a node we've already deleted, we'll get an exception here.
				catch(vB_Exception_Api $e)
				{
					//nothing to do.
				}
			}
		}
		else
		{
			$fields = array (
				'unpublishdate' => vB::getRequest()->getTimeNow(),
				'deletereason' => $reason,
				'deleteuserid' => vB::getCurrentSession()->get('userid')
			);
			$result = vB::getDbAssertor()->update('node', $fields, array(array('field' => 'nodeid', 'value' => $deleteNodeIds, 'operator' => vB_dB_Query::OPERATOR_EQ)));

			$errors = array();
			foreach ($deleteNodeIds as $nodeid)
			{
				$result = vB::getDbAssertor()->assertQuery('vBForum:unPublishNode', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
						'nodeid' => $nodeid));
				if (!empty($result['errors']))
				{
					$errors[] = $result['errors'];
				}
			}

			if (!empty($errors))
			{
				return array('errors' => $errors);
			}
			// now update last content and counts for parents
		}
		$ancestorsId = array_unique($ancestorsId);
		$starters = array_unique($starters);
		// reset last content for all parents that have the deleted nodes
		vB::getDbAssertor()->update('node',
		array(
			'lastcontent' => 0,
			'lastcontentid' => 0,
			'lastcontentauthor' => '',
			'lastauthorid' => 0),
		array(
			array('field' => 'nodeid', 'value' => $ancestorsId)
		)
		);
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$fastCache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		$events = array();
		foreach ($ancestorsId AS $ancestor)
		{
			vB_Node::fixNodeCount($ancestor);
			vB::getDbAssertor()->assertQuery('vBForum:updateLastData', array('parentid' => $ancestor, 'timenow' => vB::getRequest()->getTimeNow()));
			$events[] = "nodeChg_" . $ancestor;
		}

		foreach ($starters AS $starter)
		{
			$events[] = "nodeChg_" . $starter;
		}

		foreach ($deleteNodeIds as $nodeid)
		{
			$events[] = "nodeChg_" . $nodeid;
		}
		$events = array_unique($events);
		$cache->event($events);
		$fastCache->event($events);

		vB_Api::instance('Search')->purgeCacheForCurrentUser();

		return $deleteNodeIds;

	}
	/** sets the hasowner field for a parent node
	*
	*	@param	$nodeid
	*
	*	@return	mixed	either true for success or error information
	***/
	public function setHasOwner($nodeid, $hasowner = 1)
	{
		//Get the node data. We should get an array with two nodes
		$node = vB::getDbAssertor()->getRows('vBForum:node', array('nodeid' => $nodeid));

		if (empty($node) OR !empty($node['errors']))
		{
			throw new vB_Exception_Api('invalid_node_parameters');
		}

		//if this node is protected we fail.
		if ($node[$nodeid]['protected'])
		{
			throw new vB_Exception_Api('no_permission');
		}

		//Set the hasowner field
		vB::getDbAssertor()->assertQuery('vBForum:setHasOwner', array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_STORED,
			'nodeid' => $nodeid, 'hasowner' => min(1, intval($hasowner))));
		$this->clearCacheEvents($nodeid);
		$this->clearChildCache($nodeid);
		return true;
	}


	/** lists the nodes that should be displayed on a specific page.
	 *	@param	integer	The node id of the parent where we are listing
	 *	@param	integer	page number to return
	 *	@param	integer	items per page
	 *	@param	integer	depth- 0 means no stopping, otherwise 1= direct child, 2= grandchild, etc
	 *	@param	mixed	if desired, will only return specific content types.
	 *	@param	mixed	recognizes 'sort', 'exclude', 'userid', 'featured'
	 *	@param	mixed	array of filters (date - last day, last week, last month, all time or/and following - members, channels or both), showchannel (include channel title)
	 *
	 * 	@return	mixed	array of id's
	 **/
	public function listNodes($parentid, $page, $perpage, $depth, $contenttypeid, $options)
	{
		//Let's see if we have a cached record.
		$options['parentid'] = $parentid;
		$options['depth'] = $depth;

		if ($contenttypeid)
		{
			$options['contenttypeid'] = $contenttypeid;
		}

		$searchApi = vB_Api::instanceInternal('search');
		$timestamp = vB::getRequest()->getTimeNow();
		$dayStart = $timestamp - ($timestamp % 86400);

		//now see if we have a cached value.
		$hashkey = 'SrchResults' . vB::getUserContext()->fetchUserId() . crc32(serialize($options)) ;

		if (($srchResultId = vB_Cache::instance()->read($hashkey)))
		{
			//Track Visits
			vB::getDbAssertor()->assertQuery('vBForum:trackNodeVisits', array(
				'userid' => vB::getCurrentSession()->get('userid'),
				'dateline' => $dayStart,
				'nodeid' => $parentid
			));

			$srchResults = $searchApi->getMoreNodes($srchResultId, $perpage, $page);
			if (!empty($srchResults['nodeIds']))
			{
				$resultNodes = vB::getDbAssertor()->getRows('vBForum:node',
					array('nodeid' => $srchResults['nodeIds']),
					false,
					'nodeid'
				);

				$result = array();
				if (is_array($resultNodes) AND !isset($resultNodes['errors']))
				{
					foreach ($srchResults['nodeIds'] AS $nodeid)
					{
						if (empty($resultNodes[$nodeid]))
						{
							continue;
						}
						$result[$nodeid] = $resultNodes[$nodeid];
					}
				}
				return $result;
			}
			else
			{
				return array();
			}
		}

		//We need to do a new search. We need a criteria object.
		$criteria = array();
		//Let's set the values.

		//contenttype
		if (intval($contenttypeid) > 0)
		{
			$criteria['contenttypeid'] = $contenttypeid;
		}

		//includeProtected
		if (empty($options['includeProtected']) AND empty($options['include_protected']))
		{
			$criteria['ignore_protected'] = 1;
		}

		//channel
		if ($parentid)
		{
			$criteria['channel'] = $parentid;
		}
		else if (!empty($options['channel']))
		{
			$criteria['channel'] = $parentid;
		}

		//exclude
		if (!empty($options['exclude']))
		{
			$criteria['exclude'] = $options['exclude'];
		}

		//depth
		if ($depth)
		{
			$criteria['depth'] = $depth;
		}
		else if (!empty($options['channel']))
		{
			$criteria['channel'] = $parentid;
		}

		//featured
		if (!empty($options['featured']) AND (bool)$options['featured'])
		{
			$criteria['featured'] = 1;
		}

		//time filter
		if (!empty($options[vB_Api_Node::FILTER_TIME]))
		{
			//only allow a subset of the values that the search will accept for a from date. If it not one of
			//values we accept assume "all values" aka no date filter.
			switch ($options[vB_Api_Node::FILTER_TIME])
			{
				case vB_Api_Search::FILTER_LASTDAY:
				case vB_Api_Search::FILTER_LASTWEEK:
				case vB_Api_Search::FILTER_LASTMONTH:
				case vB_Api_Search::FILTER_LASTYEAR:

					$criteria['date'] = array('from' => $options[vB_Api_Node::FILTER_TIME]);
					break;
				;
			} // switch
		}

		if (isset($options['include_starter']))
		{
			$criteria['include_starter'] = $options['include_starter'];
		}

		$userdata = vB::getUserContext()->getReadChannels();

		$exclude = $userdata['cantRead'];

		if (isset($options[vB_Api_Search::FILTER_FOLLOW]))
		{
			if (empty($options['followerid']))
			{
				throw new vB_Exception_Api('invalid_request');
			}
			switch($options[vB_Api_Search::FILTER_FOLLOW])
			{
				case vB_Api_Search::FILTER_FOLLOWING_USERS:
					$criteria[vB_Api_Search::FILTER_FOLLOW] = array(vB_Api_Search::FILTER_FOLLOWING_USERS => $options['followerid']);
					break;
				case vB_Api_Search::FILTER_FOLLOWING_CHANNEL:
					$criteria[vB_Api_Search::FILTER_FOLLOW] = array(vB_Api_Search::FILTER_FOLLOWING_CHANNEL => $options['followerid']);
					break;
				case vB_Api_Search::FILTER_FOLLOWING_CONTENT:
					$criteria[vB_Api_Search::FILTER_FOLLOW] = array(vB_Api_Search::FILTER_FOLLOWING_CONTENT => $options['followerid']);
					break;
				case vB_Api_Search::FILTER_FOLLOWING_BOTH:
					$criteria[vB_Api_Search::FILTER_FOLLOW] = array(vB_Api_Search::FILTER_FOLLOWING_BOTH => $options['followerid']);
					break;
				default:
					throw new vB_Exception_Api('invalid_request');

			}
			$criteria['include_starter'] = 1;
		}

		//userid
		if (!empty($options['userid']))
		{
			$criteria['author'] = $options['userid'];
		}

		//sort order
		if (!empty($options['sort']))
		{
			$criteria['sort'] = $options['sort'];
// 			//We could have sort four ways- as a single value, as an array of fields,
// 			// as field => direction,
// 			// or as an array of field=>direction.
// 			//now we want to default to sort ascending, so we need to set that way.
// 			if (is_array($options['sort']))
// 			{
// 				$test = current($options['sort']);

// 				if (is_array($test))
// 				{
// 					//sort is an array of arrays. Pass it on.
// 					$criteria['sort'] = $options['sort'];
// 				}
// 				else
// 				{
// 					//look like an array of fields.
// 					$criteria['sort'] = array();
// 					list ($key, $sort) = each($options['sort']);
// 					$criteria_sort = $key;
// 					if (!empty($sort))
// 					{
// 						$criteria_sort = array($key=>$sort);
// 					}
// 					$criteria['sort'] = $criteria_sort;

// 				}

// 			}
// 			else
// 			{
// 				$criteria['sort'] = array($options['sort'] => 'ASC');
// 			}
		}

		// we don't want to store a cached value into another cache
		$criteria['ignore_cache'] = true;
		$srcResult = $searchApi->getSearchResult($criteria);

		//cache the result id.
		vB_Cache::instance()->write($hashkey, $srcResult['resultId'], 5, 'nodeChg_' . $parentid);

		$results = $searchApi->getMoreNodes($srcResult['resultId'], $perpage, $page);
		if (!empty($results['nodeIds']))
		{
			//Track Visits
			vB::getDbAssertor()->assertQuery('vBForum:trackNodeVisits', array(
				'userid' => vB::getCurrentSession()->get('userid'),
				'dateline' => $dayStart,
				'nodeid' => $parentid
			));
			$nodes = vB::getDbAssertor()->getRows('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'nodeid' => $results['nodeIds']),false, 'nodeid');
			foreach ($results['nodeIds'] as $nodeid)
			{
				if (empty($nodes[$nodeid]))
				{
					unset($results['nodeIds'][$nodeid]);
				}
				$results['nodeIds'][$nodeid] = $nodes[$nodeid];
			}
			return $results['nodeIds'];
		}
		else
		{
			return array();
		}

	}


	/** gets one node.
	 *	@param	integer The node id
	 * @param	boolean Whether to include list of parents
	 * @param	boolean Whether to include attachments
	 *
	 *	@return	mixed	array of node records, optionally including attachment and ancestry.
	 **/
	public function getNode($nodeid, $withParents = false)
	{
		$node = vB_Library_Content::fetchFromCache($nodeid, vB_Library_Content::CACHELEVEL_NODE);

		if ($node AND $node['found'] AND $node['found'][$nodeid])
		{
			$node = $node['found'][$nodeid];
		}
		else
		{
			$queryinfo = vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'nodeid' => $nodeid));

			if (!$queryinfo OR !$queryinfo->valid())
			{
				throw new vB_Exception_Api('invalid_node_id' );
			}
			$node = $queryinfo->current();
			vB_Library_Content::writeToCache(array($node), vB_Library_Content::CACHELEVEL_NODE);
		}

		if ($node['contenttypeid'] == $this->pmContenttypeid)
		{
			$userid = vB::getCurrentSession()->get('userid');

			if ($userid != $node['userid'])
			{
				$sentQry = vB::getDbAssertor()->getRow('vBForum:sentto', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
					'nodeid' => $nodeid, 'userid' => $userid));

				if (empty($sentQry) OR !empty($sentQry['errors']))
				{
					if ((defined('VB_AREA') AND VB_AREA != 'Upgrade') AND !vB::getUserContext()->isSuperAdmin())
					{
						throw new vB_Exception_Api('no_view_permissions');
					}
				}
			}
		}
		else
		{
			//check permissions
			if ((defined('VB_AREA') AND VB_AREA != 'Upgrade') AND !vB::getUserContext()->isSuperAdmin())
			{
				$userid = vB::getCurrentSession()->get('userid');

				if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $nodeid) AND ($node['userid'] != $userid))
				{
					throw new vB_Exception_Api('no_view_permissions');
				}

				if (($node['viewperms'] == 0) AND
					!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid'], false, $node['parentid']))
				{
					//This can only be viewed by members or moderators.
					$groupintopic = vB_Api::instanceInternal('user')->getGroupInTopic($userid, $nodeid);

					if (empty($groupintopic))
					{
						throw new vB_Exception_Api('no_permissions');
					}

				}
				else
				{
					if (($node['viewperms'] == 1) AND ($userid < 1))
					{
						throw new vB_Exception_Api('no_permissions');
					}
				}
			}

		}
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		if ($withParents)
		{
			$hashKey = "nodeParents_$nodeid";
			$parents = $cache->read($hashKey);

			if ($parents)
			{
				$node['parents'] = $parents;
			}
			else
			{
				$node['parents'] = array();
				$ancestors = $this->fetchClosureParent($nodeid);

				foreach ($ancestors AS $closure)
				{
					$node['parents'][$closure['depth']] = $closure['parent'];
				}
				$cache->write($hashKey, $node['parents'], 1440, 'nodeChg_' . $nodeid);

			}
			$node['parents_reversed'] = array_reverse($node['parents']);
		}

		foreach (vB_Api::instanceInternal('node')->getOptions() as $key => $bitmask)
		{
			$node[$key] = ($bitmask & $node['nodeoptions']) ? 1 : 0;
		}
		$node['attachments'] = $this->fetchAttachInfo($nodeid);
		$node['attachcount'] = count($node['attachments']);
		return $node;
	}

	/** Gets the node info for a list of nodes
	 *	@param	array of node ids
	 *
	 * 	@return	mixed	array of node records
	 **/
	public function getNodes($nodeList)
	{
		static $cachedNodeList = array();

		if (empty($nodeList))
		{
			return array();
		}

		if (!is_array($nodeList))
		{
			$nodeList = array($nodeList);
		}
		//if we are passed options we can't precache.
		$cachedNodeList = array_unique(array_merge($cachedNodeList, $nodeList));
		vB_Api::instanceInternal('page')->registerPrecacheInfo('node', 'getNodes', $cachedNodeList);

		$cached = vB_Library_Content::fetchFromCache($nodeList, vB_Library_Content::CACHELEVEL_NODE);

		if (empty($cached['notfound']))
		{
			//We found everything, so we're done.
			return $cached['found'];
		}
		$nodes = vB::getDbAssertor()->getRows('vBForum:node', array('nodeid' => $cached['notfound']), false, 'nodeid');
		$indexed = array();
		foreach ($nodes AS $node)
		{
			$indexed[$node['nodeid']] = $node;
		}
		vB_Library_Content::WriteToCache($indexed, vB_Library_Content::CACHELEVEL_NODE);
		//now we need to merge and sort them.
		$merged = array();
		foreach ($nodeList AS $nodeid)
		{
			if (array_key_exists($nodeid, $cached['found']))
			{
				$merged[$nodeid] = $cached['found'][$nodeid];
			}
			else if (array_key_exists($nodeid, $indexed))
			{
				$merged[$nodeid] = $indexed[$nodeid];
			}
		}
		unset($nodes,$cached, $indexed);
		return $merged;
	}

	/**
	 * Convert node path or id string to node id.
	 *
	 * @param string|int $nodestring Node String. If $nodestring is a string, it should be a route path to the node
	 * @return int Node ID
	 */
	protected function assertNodeidStr($nodestring)
	{
		if (!is_numeric($nodestring))
		{
			// $to_parent is a string. So we think it's a path to the node.
			// We need to convert it back to nodeid.
			$route = vB_Api::instanceInternal('route')->getRoute($nodestring, '');
			if (!empty($route['arguments']['nodeid']))
			{
				$nodestring = $route['arguments']['nodeid'];
			}
			elseif (!empty($route['redirect']))
			{
				$route2 = vB_Api::instanceInternal('route')->getRoute(substr($route['redirect'], 1), '');
				if (!empty($route2['arguments']['nodeid']))
				{
					$nodestring = $route2['arguments']['nodeid'];
				}else if (!empty($route2['arguments']['contentid']))
				{
					$nodestring = $route2['arguments']['contentid'];
				}
			}

			unset($route, $route2);
		}
		else
		{
			$nodestring = intval($nodestring);
		}

		return $nodestring;
	}
	/** Sets the publishdate and (optionally) the unpublish date of a node
	 *	@param	integer	The node id
	 *	@param	integer	The timestamp for publish date
	 *	@param	integer	The timestamp for unpublish date if applicable
	 *
	 *	@return	boolean
	 **/
	public function setPublishDate($nodeid, $datefrom, $dateto = null)
	{
		$data = array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_UPDATE,
			'nodeid' => $nodeid, 'publishdate' => $datefrom);

		if (intval($dateto))
		{
			$data['unpublishdate'] = $dateto;
		}
		else
		{
			$data['unpublishdate'] = -1;
		}

		//We need to use the content object to set this because there may be
		// type-specific data needed.

		$node = $this->getNode($nodeid);
		if (empty($this->contentAPIs[$node['nodeid']]))
		{
			$this->contentAPIs[$node['nodeid']] =
				vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		return $this->contentAPIs[$node['nodeid']]->update($nodeid, $data);
	}

	/** Sets the unpublish date
	 *	@param	integer	The node id
	 *	@param	integer	The timestamp for unpublish
	 *
	 *	@return	boolean
	 **/
	public function setUnPublishDate($nodeid, $dateto = false)
	{
		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
			'nodeid' => $nodeid);

		if (intval($dateto))
		{
			$data['unpublishdate'] = $dateto;
		}
		else
		{
			$data['unpublishdate'] = vB::getRequest()->getTimeNow();
		}
		//We need to use the content object to set this because there may be
		// type-specific data needed.
		$node = $this->getNode($nodeid);
		if (empty($this->contentAPIs[$node['nodeid']]))
		{
			$this->contentAPIs[$node['nodeid']] =
				vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		return $this->contentAPIs[$node['nodeid']]->update($nodeid, $data);
	}

	/** sets a node to not published
	 *	@param	integer	The node id
	 *
	 *	@return	boolean
	 **/
	public function setUnPublished($nodeid)
	{
		if (!intval($nodeid))
		{
			throw new Exception('invalid_node_id');
		}

		if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canpublish', $nodeid))
		{
			throw new Exception('no_publish_permissions');
		}

		$node = $this->getNode($nodeid);

		if (empty($this->contentAPIs[$node['nodeid']]))
		{
			$this->contentAPIs[$node['nodeid']] =
				vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		$data = array('publishdate' => -1, 'showpublished' => 0);

		return $this->contentAPIs[$node['nodeid']]->update($nodeid, $data);
		$this->clearCacheEvents(array_unique($nodeid, $node['parentid'], $node['starter']));
	}

	/*** sets a list of nodes to be featured
	 *	@param	array	The node ids
	 *	@param	boot	set or unset the featured flag
	 *
	 *	@return	array nodeids that have permission to be featured
	 **/
	public function setFeatured($nodeids, $set = true)
	{
		vB::getDbAssertor()->assertQuery('vBForum:node',
			array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE, 'nodeid' => $nodeids, 'featured' => $set));
		$this->clearCacheEvents($nodeids);
		return $nodeids;
	}



	/** clears the unpublishdate flag.
	 *	@param	integer	The node id
	 *
	 *	@return	boolean
	 **/
	public function clearUnpublishDate($nodeid)
	{
		$result = vB::getDbAssertor()->assertQuery('vBForum:node',
			array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE, 'nodeid' => $nodeid, 'unpublishdate' => -1));

		$node = $this->getNode($nodeid);
		if (empty($this->contentAPIs[$node['nodeid']]))
		{
			$this->contentAPIs[$node['nodeid']] =
				vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		$data = array('unpublishdate' => 0);

		return $this->contentAPIs[$node['nodeid']]->update($nodeid, $data);

	}
	/**This takes a list of nodes, and returns node records for all that are valid nodeids.
	*
	* 	@param		mixed		array of ints
	*
	*	@return		mixed		array of node records
	**/
	protected function cleanNodeList(&$nodeList)
	{
		if (!is_array($nodeList))
		{
			$nodeList = array($nodeList);
		}

		//many of them may be in cache.
		$cached = vB_Library_Content::fetchFromCache($nodeList, vB_Library_Content::CACHELEVEL_NODE);
		$listIndex = array_flip($nodeList);

		if (!empty($cached['found']))
		{
			foreach ($cached['found'] as $node)
			{
				$nodeid = $node['nodeid'];
				if (isset($listIndex[$nodeid]))
				{
					$nodeList[$listIndex[$nodeid]] = $node;
					unset($listIndex[$nodeid]);
				}
			}
		}

		if (!empty($cached['notfound']))
		{
			$nodes = vB::getDbAssertor()->assertQuery('vBForum:node', array('nodeid' => $cached['notfound']));
			foreach ($nodes as $node)
			{
				$nodeid = $node['nodeid'];
				if (isset($listIndex[$nodeid]))
				{
					$nodeList[$listIndex[$nodeid]] = $node;
					unset($listIndex[$nodeid]);
				}
			}
		}

		//if we filtered out a node in the query, remove it from the list.
		foreach($listIndex as $key => $value)
		{
			unset($nodeList[$value]);
		}

	}


	/** Gets the content info for a list of nodes
	 *	@param	mixed	array of node ids
	 *
	 * 	@return	mixed	array of content records
	 **/
	public function getContentforNodes($nodeList, $options = array())
	{
		static $cachedNodeList = array();

		//if we are passed options we can't precache.
		if (empty($options))
		{
			if (!is_array($nodeList))
			{
				$nodeList = array($nodeList);
			}
			//if we are passed options we can't precache.
			$cachedNodeList = array_unique(array_merge($cachedNodeList, $nodeList));
			vB_Api::instanceInternal('page')->registerPrecacheInfo('node', 'getContentforNodes', $cachedNodeList);
		}

		$this->cleanNodeList($nodeList);
		return $this->addContentInfo($nodeList, $options);
	}

	/** Gets the content info for a list of nodes
	 *	@param array array of node ids
	 *
	 * 	@return array array of content records -- preserves the original keys
	 **/
	public function getFullContentforNodes($nodeList, $options = array())
	{
		static $cachedNodeList = array();

		//if we are passed options we can't precache.
		if (empty($options))
		{
			if (!is_array($nodeList))
			{
				$nodeList = array($nodeList);
			}
			//if we are passed options we can't precache.
			$cachedNodeList = array_unique(array_merge($cachedNodeList, $nodeList));
			vB_Api::instanceInternal('page')->registerPrecacheInfo('node', 'getFullContentforNodes', $cachedNodeList);
		}

		$this->cleanNodeList($nodeList);
		return $this->addFullContentInfo($nodeList, $options);
	}

	/**	Gets the channel title and routeid
	 *	@param	int		The node id.
	 *	@return	mixed	Array of channel info
	 */
	public function getChannelInfoForNode($channelId)
	{
		$channelInfo = vB::getDbAssertor()->getRow('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'nodeid' => $channelId));

		return array('title' => $channelInfo['title'], 'routeid' => $channelInfo['routeid']);
	}

	/**
	 * Check a list of nodes and see whether the user has voted them
	 *
	 * @param array $nodeIds A list of Nodes to be checked
	 * @param int $userid User ID to be checked. If not there, currently logged-in user will be checked.
	 * @return array Node IDs that the user has voted.
	 * @see vB_Api_Reputation::fetchNodeVotes()
	 */
	protected function getNodeVotes(array $nodeIds, $userid = 0)
	{
		return vB_Api::instanceInternal('reputation')->fetchNodeVotes($nodeIds, $userid);
	}


	/** Adds optional content information. At the time of this writing it understands showVM and withParent
	 *
	 *	@param	mixed	the assembled array of node info
	 * 	@param	mixed	optional array of optional information
	 *
	 ***/
	protected function addOptionalContentInfo(&$nodeList, $options = false)
	{
		//We always need to add avatar information,
		$userApi = vB_Api::instanceInternal('user');
		$useridAvatarsToFetch = array();
		foreach($nodeList AS $key => $node)
		{
			if (empty($node['content']))
			{
				$nodeList[$key]['content'] = array();
			}

			if (!empty($node['userid']))
			{
				$useridAvatarsToFetch[] = $node['userid'];
			}

			if (!empty($node['lastauthorid']) AND $node['lastauthorid'] > 0)
			{
				$useridAvatarsToFetch[] = $node['lastauthorid'];
			}

			if (!empty($node['deleteuserid']))
			{
				$nodeList[$key]['content']['deleteusername'] = $userApi->fetchUserName($node['deleteuserid']);
			}
		}

		$avatarsurl = $userApi->fetchAvatars($useridAvatarsToFetch, true);

		foreach ($nodeList AS $nodeKey => $nodeInfo)
		{

			if (!empty($nodeInfo['userid']))
			{
				$nodeList[$nodeKey]['content']['avatar'] = $avatarsurl[$nodeInfo['userid']];
			}

			if (!empty($nodeInfo['lastauthorid']) AND $nodeInfo['lastauthorid'] > 0 AND !empty($avatarsurl[$nodeInfo['lastauthorid']]))
			{
				$nodeList[$nodeKey]['content']['avatar_last_poster'] = $avatarsurl[$nodeInfo['lastauthorid']];
			}
		}

		if (!empty($options['showVM']) AND !empty($nodeList))
		{
			$nodeids = array();
			//We need to flag which are visitor messages
			foreach($nodeList AS $key => $node)
			{
				$nodeids[$node['nodeid']] = $node['nodeid'];
				$nodeList[$key]['content']['isVisitorMessage'] = 0;
			}
			//We have all the nodes. Now query for which are VM's
			$vMs = $this->fetchClosureParent(array_keys($nodeids), vB_Api::instanceInternal('node')->fetchVMChannel());

			foreach ($vMs AS $closureRecord)
			{

				//Remember the nodes are keys into nodeids, which are keys into nodeList.
				$key = $nodeids[$closureRecord['child']];
				$nodeList[$key]['content']['isVisitorMessage'] = 1;
				$vm_userInfo = vB_User::fetchUserinfo($nodeList[$key]['content']['setfor']);
				$vmAvatar = $userApi->fetchAvatar($nodeList[$key]['content']['setfor'], true, $vm_userInfo);
				$vm_userInfo = array_merge($vm_userInfo, $vmAvatar);
				$nodeList[$key]['content']['vm_userInfo'] = $vm_userInfo;
			}
		}

		if (!empty($options['withParent']) AND !empty($nodeList))
		{
			//We need to pull parent node information, but only for comments- not starters or replies:
			$parentids = array();
			$indexes = array();
			$channelType = vB_Types::instance()->getContentTypeId('vBForum_Channel');
			foreach($nodeList AS $key => $node)
			{
				//Note that we can't use an indexed array to lookup the same way as for showVM, because we
				// often will have multiple records with the same parent.
				if (($node['nodeid'] != $node['starter']) AND ($node['parentid'] != $node['starter']) AND ($node['contenttypeid'] != $channelType))
				{
					$parentids[] = $node['parentid'];
				}
			}

			//If we had no comments in the list, we're done.
			if (!empty($parentids))
			{
				$parents = $this->getFullContentforNodes(array_unique($parentids));

				foreach ($parents AS $key => $parent)
				{
					$indexes[$parent['nodeid']] = $key;
				}
				foreach($nodeList AS $key => $node)
				{
					if (array_key_exists($node['parentid'], $indexes))
					{
						$parentKey = $indexes[$node['parentid']];
						$nodeList[$key]['content']['parentConversation'] = $parents[$parentKey];
					}
				}
			}
		}
	}
	
	/** Adds optional content information for a single node. 
	 * 	At the time of this writing it understands showVM and withParent
	 *
	 *	@param	mixed	the assembled array of node info
	 * 	@param	mixed	optional array of optional information
	 *
	 ***/
	protected function addOptionalNodeContentInfo(&$node, $options = false)
	{
		//We always need to add avatar information
		$userApi = vB_Api::instanceInternal('user');
		$useridAvatarsToFetch = array();
		if (!empty($node['userid']))
		{
			$useridAvatarsToFetch[] = $node['userid'];
		}
		if (!empty($node['lastauthorid']) AND $node['lastauthorid'] > 0)
		{
			$useridAvatarsToFetch[] = $node['lastauthorid'];
		}
		
		if (!empty($node['deleteuserid']))
		{
			$node['deleteusername'] = $userApi->fetchUserName($node['deleteuserid']);
		}
		
		if (!empty($useridAvatarsToFetch))
		{
			$avatarsurl = $userApi->fetchAvatars($useridAvatarsToFetch, true);
			
			if (!empty($node['userid']))
			{
				$node['avatar'] = $avatarsurl[$node['userid']];
			}

			if (!empty($node['lastauthorid']) AND $node['lastauthorid'] > 0 AND !empty($avatarsurl[$node['lastauthorid']]))
			{
				$node['avatar_last_poster'] = $avatarsurl[$node['lastauthorid']];
			}
		}

		if (!empty($options['showVM']) AND !empty($node))
		{
			//We have the node. Now query for which are VM's
			$vMs = $this->fetchClosureParent($node['nodeid'], vB_Api::instanceInternal('node')->fetchVMChannel());
			if (!empty($vMs))
			{
				foreach ($vMs AS $closureRecord)
				{
					$key = $closureRecord['child'];
					if ($key == $node['nodeid'])
					{
						$node['isVisitorMessage'] = 1;
						$vm_userInfo = vB_User::fetchUserinfo($node['setfor']);
						$vmAvatar = $userApi->fetchAvatar($node['setfor'], true, $vm_userInfo);
						$vm_userInfo = array_merge($vm_userInfo, $vmAvatar);
						$node['vm_userInfo'] = $vm_userInfo;
					}
				}
			}
			else
			{
				$node['isVisitorMessage'] = 0;
				$node['vm_userInfo'] = array();
			}
		}

		if (!empty($options['withParent']) AND !empty($node))
		{
			$parentid = 0;
			$channelType = vB_Types::instance()->getContentTypeId('vBForum_Channel');
			if (($node['nodeid'] != $node['starter']) AND ($node['parentid'] != $node['starter']) AND ($node['contenttypeid'] != $channelType))
			{
				$parentid = $node['parentid'];
			}

			//If we had no comments in the list, we're done.
			if (!empty($parentid))
			{
				$parent = $this->getNodeFullContent($parentid);
				$node['parentConversation'] = $parent;
			}
		}
	}

	/** This gets the attachment information filedata for a node. Which may be empty.
	*
	*	@param		int		the nodeid we are checking
	*
	*	@return		mixed	either false or an array of filedata.
	**/
	public function fetchAttachInfo($parentIds)
	{
		if (!is_array($parentIds))
		{
			$parentIds = array($parentIds);
		}

		//First let's see what we have in cache.
		$found = array();
		$notfound = array();
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		foreach ($parentIds AS $parentId)
		{
			$attachments = $cache->read("vBAtchInf_$parentId");
			if ($attachments === false)
			{
				$notfound[$parentId] = array() ;
			}
			else
			{
				$found = array_merge($found, $attachments);
			}
		}

		if (!empty($notfound))
		{
			try
			{
				$attachments = vB::getDbAssertor()->assertQuery('vBForum:fetchAttachInfo', array(
					'parentId' => array_keys($notfound)
				));

				foreach($attachments AS $attachment)
				{
					$found[] = $attachment;
					$notfound[$attachment['parentid']][] = $attachment;
				}

				//cache what we've found- but not false. Use empty array so we can distinguish
				// cached data from uncached.
				foreach ($notfound AS $parentId => $attachments)
				{
					$hashKey = "vBAtchInf_$parentId";

					if (empty($attachments))
					{
						$attachments = array();
					}
					$cache->write($hashKey, $attachments, 1440, "nodeChg_$parentId");
				}
			}
			catch(exception $e)
			{
				//this happens during upgrade from a vb3x site. Just continue;
			}
		}

		return $found;

	}


	/** This gets the attachment information for a node. Which may be empty. Does not include filedata
	 *
	 *	@param		int		the nodeid we are checking
	 *
	 *	@return		mixed	either false or an array of filedata.
	 **/
	public function fetchNodeAttachments($parentids)
	{
		if (!is_array($parentids))
		{
			$parentids = array($parentids);
		}

		//First let's see what we have in cache.
		$found = array();
		$notfound = array();
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$parents = array();
		foreach ($parentids AS $parentid)
		{
			$attachments = $cache->read("vBAtchmnts_$parentid");

			if ($attachments === false)
			{
				$notfound[$parentid] = array() ;
			}
			else
			{
				$found = array_merge($found, $attachments);
			}
		}

		if (!empty($notfound))
		{
			try
			{
				$attachments = vB::getDbAssertor()->assertQuery('vBForum:fetchNodeAttachments',
					array('nodeid' => array_keys($notfound)), 'displayorder');
			}
			catch(exception $e)
			{
				//this can happen during a preload. Just continue;
				$attachments = array();
			}


			foreach($attachments AS $attachment)
			{
				$found[] = $attachment;
				$notfound[$attachment['parentid']][] = $attachment;
			}

			//cache what we've found- but not false. Use empty array so we can distinguish
			// cached data from uncached.
			foreach ($notfound AS $parentid => $attachments)
			{
				$hashKey = "vBAtchmnts_$parentid";

				if (empty($attachments))
				{
					$attachments = array();
				}

				$cache->write($hashKey, $attachments, 1440, "nodeChg_$parentid");
			}
		}
		return $found;
	}

	/** Takes an array of node information and adds contentInfo
	 *	@param	integer	The node id of the parent where we are listing
	 *	@param	integer	page number to return
	 *	@param	integer	items per page
	 *	@param	integer	depth- 0 means no stopping, otherwise 1= direct child, 2= grandchild, etc
	 *	@param	mixed	if desired, will only return specific content types.
	 *	@param	mixed	'sort', or 'exclude' recognized.
	 *
	 * 	@return	mixed	array of id's
	 ***/
	public function addContentInfo($nodeList, $options = array(), $fullContent = false)
	{
		//Now separate by content type	$contenttypes = array();
		if (empty($nodeList))
		{
			return array();
		}
		$nodeIds = array();
		foreach ($nodeList as $key => $node)
		{
			if (!isset($contenttypes[$node['contenttypeid']]))
			{
				$contenttypes[$node['contenttypeid']] = array();
			}
			$contenttypes[$node['contenttypeid']][$key] = $node['nodeid'];
			$nodeIds[] = $node['nodeid'];
		}

		// get votes
		$nodeVotes = empty($nodeIds) ? array() : $this->getNodeVotes($nodeIds);

		if (!empty($nodeIds))
		{
			$attachments = $this->fetchAttachInfo($nodeIds);

			$nodeAttachments = array();
			foreach ($attachments as $key => $attach)
			{
				$nodeAttachments[$attach['parentid']][$attach['filedataid']] = & $attachments[$key];
			}
		}

		// Fetch read marking data
		$threadmarking = vB::getDatastore()->getOption('threadmarking');
		$user = vB::getCurrentSession()->fetch_userinfo();
		if ($threadmarking AND $user['userid'] AND !empty($nodeIds))
		{
			$reads = vB::getDbAssertor()->getRows('noderead', array(
				'userid' => $user['userid'],
				'nodeid' => $nodeIds,
			));

			$nodeReads = array();
			foreach ($reads as $read)
			{
				$nodeReads[$read['nodeid']] = $read['readtime'];
			}
		}
		$userid = vB::getCurrentSession()->get('userid');
		//For each type, get the content detail.
		$attachCountQry = vB::getDbAssertor()->getRows('vBForum:getDescendantAttachCount', array('nodeid' => $nodeIds));
		$attachCounts = array();
		foreach ($attachCountQry as $count)
		{
			$attachCounts[$count['nodeid']] = $count['count'];
		}
		$photoCountQry = vB::getDbAssertor()->getRows('vBForum:getDescendantPhotoCount', array('nodeid' => $nodeIds,
			'photoTypeid' => vB_Types::instance()->getContentTypeID('vBForum_Photo')));
		$photoCounts = array();
		foreach ($photoCountQry as $count)
		{
			$photoCounts[$count['nodeid']] = $count['count'];
		}

		$contentParents = array();
		foreach ($contenttypes as $contenttypeid => $nodes)
		{
			if (!empty($nodes))
			{
				$contentApi = vB_Api_Content::getContentApi($contenttypeid);

				if ($fullContent)
				{
					$contentList = $contentApi->getFullContent($nodes, array('forumpermissions' => array('canview'),
						'moderatorpermissions' => array('canmoderateposts')));
				}
				else
				{
					$contentList = $contentApi->getContent($nodes, array('forumpermissions' => array('canview'),
						'moderatorpermissions' => array('canmoderateposts')));
				}
				$optionMask = vB_Api::instanceInternal('node')->getOptions();
				foreach ($nodes as $key => $nodeid)
				{
					foreach ($contentList as $key2 => $content)
					{
						if ($content['nodeid'] == $nodeid)
						{
							$content['nodeVoted'] = in_array($nodeid, $nodeVotes) ? 1 : 0;
							$nodeList[$key]['content'] = $content;

							if (!empty($content['contenttypeclass']))
							{
								$nodeList[$key]['contenttypeclass'] = $content['contenttypeclass'];
							}
							break;
						}
					}

					foreach ($optionMask as $bitname => $bitmask)
					{
						$nodeList[$key][$bitname] = ($bitmask & $node['nodeoptions']) ? 1 : 0;
					}							//unset ($contentList[$key2]);

					if (isset($nodeAttachments[$nodeid]))
					{
						$nodeList[$key]['content']['attachments'] = & $nodeAttachments[$nodeid];
					}
					else
					{
						$nodeList[$key]['content']['attachments'] = array();
					}

					if ($threadmarking AND $user['userid'])
					{
						if (isset($nodeReads[$nodeid]))
						{
							$nodeList[$key]['readtime'] = $nodeReads[$nodeid];
						}
						else
						{
							$nodeList[$key]['readtime'] = 0;
						}
					}

					if (($content['hasowner'] > 0) AND ($content['userid'] == $userid))
					{
						$nodeList[$key]['content']['isowner'] = 1;
					}
					else
					{
						$nodeList[$key]['content']['isowner'] = 0;
					}

					if (empty($attachCounts[$nodeid]))
					{
						$nodeList[$key]['attachcount'] = 0;
					}
					else
					{
						$nodeList[$key]['attachcount'] = $attachCounts[$nodeid];
					}

					if (!empty($photoCounts[$nodeid]))
					{
						$nodeList[$key]['attachcount'] += $photoCounts[$nodeid];
					}
				}
			}
		}

		$contentParents = array_unique($contentParents);

		$this->addOptionalContentInfo($nodeList, $options);
		$this->addPermissions($nodeList);
		$this->markSubscribed($nodeList);
		$this->markJoined($nodeList);
		return $nodeList;
	}

	/** Takes an array of node information and adds contentInfo
	 *	@param	integer	The node id of the parent where we are listing
	 *	@param	integer	page number to return
	 *	@param	integer	items per page
	 *	@param	integer	depth- 0 means no stopping, otherwise 1= direct child, 2= grandchild, etc
	 *	@param	mixed	if desired, will only return specific content types.
	 *	@param	mixed	'sort', or 'exclude' recognized.
	 *
	 * 	@return	mixed	array of id's
	 ***/
	public function addFullContentInfo($nodeList, $options = array())
	{
		return $this->addContentInfo($nodeList, $options, true);
	}


	/** This adds permissions information to a list of node items.
	*
	*	@param	mixed	array of content nodes
	*
	*	@return	mixed	array which includes permission array: canmoderate, canedit and canvote. Probably more permissions will be added in the future.
	***/
	protected function addPermissions(&$contentList)
	{

		$userid = vB::getCurrentSession()->get('userid');
		$userContext = vB::getUserContext();
		foreach ($contentList as $key => $contentItem)
		{

			$canmoderate = $userContext->getChannelPermission('moderatorpermissions', 'canmoderateposts', $contentItem['nodeid'], false, $contentItem['parentid']);
			$canvote = $userContext->getChannelPermission('forumpermissions', 'canvote', $contentItem['nodeid'], false, $contentItem['parentid']);
			$contentList[$key]['content']['permissions'] = array('canedit' => ($userid == $contentItem['userid']) OR $canmoderate,
			'canmoderate' => $canmoderate, 'canvote' => $canvote);
			$contentList[$key]['content']['createpermissions'] = $userContext->getCanCreate($contentItem['nodeid']);
		}
	}

	/**
	* This gets a content record based on nodeid. Useful from ajax.
	*
	*	@param	int
	*	@param	int	optional
	*	@param	mixed options	Options to get optional info (showVM, withParent)
	*
	*	@return array. An array of node record arrays as $nodeid => $node
	*
	***/
	public function getNodeContent($nodeid, $contenttypeid = false, $options = array())
	{
		if ($contenttypeid)
		{
			$contentApi = vB_Api_Content::getContentApi($contenttypeid);
		}
		else
		{
			$node = $this->getNode($nodeid);

			if (empty($node) OR !empty($node['errors']))
			{
				throw new vB_Exception_Api('invalid_data');
			}
			$contentApi = vB_Api_Content::getContentApi($node['contenttypeid']);
		}

		$result = $contentApi->getContent($nodeid);
		$nodeVotes = $this->getNodeVotes(array($nodeid));
		$result[$nodeid]['nodeVoted'] = in_array($nodeid, $nodeVotes) ? 1 : 0;
		$attachments = $this->fetchAttachInfo($nodeid);
		$result[$nodeid]['attachments'] = array();
		foreach ($attachments AS $attachment)
		{
			$result[$nodeid]['attachments'][$attachment['filedataid']] = $attachment;
		}

		if (empty($result[$nodeid]['attachments']))
		{
			$result[$nodeid]['attachcount'] = 0;
		}
		else
		{
			$result[$nodeid]['attachcount'] = count($result[$nodeid]['attachments']);
		}

		$this->addOptionalNodeContentInfo($result[$nodeid], $options);
		$this->markSubscribed($result);
		$this->markJoined($result);
		return $result;
	}


	/** returns id of the Albums Channel
	 *
	 *	@return	integer		array including
	 ***/
	public function fetchAlbumChannel()
	{
		if ($this->albumChannel)
		{
			return $this->albumChannel;
		}

		$albumChannel = vB::getDbAssertor()->getRow('channel',
			 	array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'guid' => vB_Channel::ALBUM_CHANNEL));
		$this->albumChannel = $albumChannel['nodeid'];
		return $this->albumChannel;

	}

	/** returns id of the Private Message Channel
	 *
	 *	@return	integer		array including
	 ***/
	public function fetchPMChannel()
	{
		if ($this->PMChannel)
		{
			return $this->PMChannel;
		}

		$PMChannel = vB::getDbAssertor()->getRow('channel',
			 	array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'guid' => vB_Channel::PRIVATEMESSAGE_CHANNEL));
		$this->PMChannel = $PMChannel['nodeid'];
		return $this->PMChannel;

	}

	/** returns id of the Vistor Message Channel
	 *
	 *	@return	integer		array including
	 **/
	public function fetchVMChannel()
	{
		if ($this->VMChannel)
		{
			return $this->VMChannel;
		}

		$VMChannel = vB::getDbAssertor()->getRow('channel',
			 	array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'guid' => vB_Channel::VISITORMESSAGE_CHANNEL));
		$this->VMChannel = $VMChannel['nodeid'];
		return $this->VMChannel;

	}

	/** returns id of the Report Channel
	 *
	 *	@return	integer		array including
	 ***/
	public function fetchReportChannel()
	{
		if ($this->ReportChannel)
		{
			return $this->ReportChannel;
		}

		$ReportChannel = vB::getDbAssertor()->getRow('channel',
			 	array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'guid' => vB_Channel::REPORT_CHANNEL));
		$this->ReportChannel = $ReportChannel['nodeid'];
		return $this->ReportChannel;

	}

	/** This gets a content record based on nodeid including channel and starter information.
	 *
	 * 	@param	int
	 *	@param	int	optional
	 *	@param	bool optional
	 *
	 *	@return mixed
	 *
	 ***/
	public function getNodeFullContent($nodeid, $contenttypeid = false, $options = array())
	{
		$db = vB::getDbAssertor();

		if ($contenttypeid)
		{
			$contentApi = vB_Api_Content::getContentApi($contenttypeid);
		}
		else
		{
			$node = $db->getRow('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			 'nodeid' => $nodeid));
			if (empty($node) OR !empty($node['errors']))
			{

				throw new vB_Exception_Api('invalid_data');
			}

			$contentApi = vB_Api_Content::getContentApi($node['contenttypeid']);
		}
		$result = $contentApi->getFullContent($nodeid);
		$attachments = $this->fetchAttachInfo($nodeid);

		$result[$nodeid]['attachments'] = array();
		foreach ($attachments AS $attachment)
		{
			$result[$nodeid]['attachments'][$attachment['filedataid']] = $attachment;
		}
		$result[$nodeid]['attachcount'] = count($result[$nodeid]['attachments']);
		
		$this->addOptionalNodeContentInfo($result[$nodeid], $options);
		$this->markSubscribed($result);
		return $result;
	}

	/** This returns all the albums in a channel. Those can be photogalleries or text with attachments.
	 *
	 *	@param		int
	 *
	 *	@return		mixed		array of node records. Each node includes the node content and userinfo, and attachment records.
	 **/
	public function getAlbums($nodeid)
	{
		//first query to get the id's.
		$nodeids = array();
		$nodeQry = vB::getDbAssertor()->assertQuery('vBForum:fetchNodesWithAttachments',
			array('channel' => $nodeid, 'contenttypeid' => array(vB_Types::instance()->getContentTypeId('vBForum_Attach'), vB_Types::instance()->getContentTypeId('vBForum_Photo'))) );

		foreach($nodeQry AS $node)
		{
			$nodeids[] = $node['nodeid'];
		}

		if (empty($nodeids))
		{
			return array();
		}

		$content = $this->getFullContentforNodes($nodeids, true);
		//let's set everything with key nodeid.
		//We want to know the difference between attachments and photos in the template.
		//The array of photos in the gallery is "photo". For text let's call it "album".
		$sortable = array();
		foreach ($content as $key => $node)
		{
			$sortable[$node['nodeid']] = $node;
			$sortable[$node['nodeid']]['album'] = array();
		}

		if (!empty($nodeids))
		{
			$attachments = $this->fetchAttachInfo($nodeids);

			foreach ($attachments as $key => $attach)
			{
				$sortable[$attach['parentid']]['album'][$attach['nodeid']] = $attach;
			}
		}

		foreach ($content as $key => &$node)
		{
			if (empty($node['attachments']))
			{
				$node['attachcount'] = 0;
			}
			else
			{
				$node['attachcount'] = count($node['attachments']);
			}
		}

		//now we need photocount
		foreach ($sortable as $key => $node)
		{
			$sortable[$key]['photocount'] = count($node['album']);
			$sortable[$key]['starteruserid'] = $node['content']['starteruserid'];
			$sortable[$key]['starterauthorname'] = $node['content']['starterauthorname'];
			$sortable[$key]['starterroute'] = $node['content']['starterroute'];
		}
		//Now galleries.
		$nodeQry = vB::getDbAssertor()->assertQuery('vBForum:fetchGalleriesInChannel',
			array('channel' => $nodeid, 'contenttypeid' => vB_Types::instance()->getContentTypeId('vBForum_Gallery')) );

		$nodeids = array();

		foreach($nodeQry AS $node)
		{
			$nodeids[] = $node['nodeid'];
		}

		if (!empty($nodeids))
		{
			$galleries = vB_Api::instanceInternal('content_gallery')->getFullContent($nodeids);

			//now let's merge and sort them.
			foreach ($galleries as $gallery)
			{
				//we only have text objects that we know have attachments, but there
				//could be a gallery with no photos.
				if (!empty($gallery['photo']))
				{
					$sortable[$gallery['nodeid']] = $gallery;
				}
			}
		}
		if (empty($sortable))
		{
			throw new vB_Exception_Api('invalid_data');
		}
		ksort($sortable);
		//Now we have a non-associative array of $content and an associative array of albums. We need to merge.
		return $sortable;
	}

	/**
	 * Sets the approved field
	 * @param array $nodeids
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have the permission to be changed
	 */
	function approve($nodeids)
	{
		return $this->setApproved($nodeids, true);
	}

	/**
	 * Unsets the approved field
	 * @param array $nodeids
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have the permission to be changed
	 */
	function unapprove($nodeids)
	{
		return $this->setApproved($nodeids, false);
	}

	/**
	 * Gets the list of unapproved posts for the current user
	 *
	 * @param	int		User id. If not specified will take current User
	 * @param	mixed	Options used for pagination:
	 * 						page 		int		number of the current page
	 * 						perpage		int		number of the results expected per page.
	 * 						totalcount	bool	flag to indicate if we need to get the pending posts totalcount
	 *
	 * @return	mixed	Array containing the pending posts nodeIds with contenttypeid associated.
	 */
	public function listPendingPosts($userId = false, $options = array())
	{
		$userId = intval($userId);
		if (!$userId)
		{
			$userId = vB::getCurrentSession()->get('userid');
		}

		if (!$userId)
		{
			throw new vB_Exception_Api('not_logged_no_permission');
		}

		// let's get mod permissions
		$params = array();
		$moderateInfo = vB::getUserContext()->getCanModerate();

		// canmoderate
		foreach ($moderateInfo['can'] AS $channel)
		{
			$params['canModerate'][] = $channel;
		}

		//cantmoderate
		foreach ($moderateInfo['cannot'] AS $channel)
		{
			// we don't want root channel here
			if ($channel != 1)
			{
				$params['cantModerate'][] = $channel;
			}
		}

		$result = array();
		if (empty($moderateInfo['can']))
		{
			$result = array('nodes' => array());
		}
		else
		{
			// let's take pagination info first...
			$params[vB_dB_Query::PARAM_LIMITPAGE] = (isset($options['page']) AND intval($options['page'])) ? $options['page'] : 1;

			$params[vB_dB_Query::PARAM_LIMIT] = (isset($options['perpage']) AND intval($options['perpage'])) ? $options['perpage'] : 20;

			$pendingPosts = vB::getDbAssertor()->assertQuery('vBForum:fetchPendingPosts',
					$params
			);

			$pending = array();
			foreach ($pendingPosts AS $post)
			{
				$pending[$post['nodeid']]['nodeid'] = $post['nodeid'];
				$pending[$post['nodeid']]['contenttypeid'] = $post['contenttypeid'];
			}

			$result = array('nodes' => $pending);

			// if totalcount flag is set...
			if ($options['totalcount'])
			{
				$page = $params[vB_dB_Query::PARAM_LIMITPAGE];
				$perpage = $params[vB_dB_Query::PARAM_LIMIT];
				unset($params[vB_dB_Query::PARAM_LIMITPAGE]);
				unset($params[vB_dB_Query::PARAM_LIMIT]);
				$countInfo = vB::getDbAssertor()->getRow('vBForum:fetchPendingPostsCount',
					$params
				);

				$result['totalcount'] = intval($countInfo['ppCount']);
				$pagecount = ceil($result['totalcount']/$perpage);
				if ($page > 1)
				{
					$prevpage = $page - 1;
				}
				else
				{
					$prevpage = false;
				}

				if ($page < $pagecount)
				{
					$nextpage = $page + 1;
				}
				else
				{
					$nextpage =false;
				}

				$pageInfo = array('totalcount' => $result['totalcount'], 'pages' => $pagecount, 'nextpage' => $nextpage, 'prevpage' => $prevpage, 'perpage' => $perpage, 'currentpage' => $page);
				$result['pageInfo'] = $pageInfo;
			}
		}

		return $result;
	}

	/**
	 * Function wrapper for listPendingPosts but used for current user.
	 *
	 */
	public function listPendingPostsForCurrentUser($options = array())
	{
		$result = $this->listPendingPosts(vB::getUserContext()->fetchUserId(), $options);

		if (isset($result['totalcount']))
		{
			$totalCount = intval($result['totalcount']);
		}

		if (isset($result['pageInfo']))
		{
			$pageInfo = $result['pageInfo'];
		}


		$contenttypes = array();
		$nodes = array();

		foreach ($result['nodes'] AS $node)
		{
			$contenttypeid = $node['contenttypeid'];
			$nodeid = $node['nodeid'];

			if (!isset($contenttypes[$contenttypeid]))
			{
				$contenttypes[$contenttypeid] = array();
			}
			$contenttypes[$contenttypeid][] = $nodeid;
			$nodes[$nodeid] = $node;
		}

		//For each type, get the content detail.
		foreach ($contenttypes as $contenttypeid => $nodeList)
		{
			if (!empty($nodes))
			{
				$contentApi = vB_Api_Content::getContentApi($contenttypeid);
				$contentList = $contentApi->getFullContent($nodeList);
				foreach ($nodes as $nodeid => $node)
				{
					foreach ($contentList as $key => $content)
					{
						if ($content['nodeid'] == $nodeid)
						{
							$nodes[$nodeid]['content'] = $content;
							break;
						}
					}
				}
			}
		}

		$userApi = vB_Api::instanceInternal('user');
		$pmContentType = vB_Types::instance()->getContentTypeId('vBForum_PrivateMessage');
		//We need a list of parents for nodes that are neither starters nor replies.
		$parents = array();
		//add parent, visitormessage, and author information
		foreach ($nodes as $nodeid => $node)
		{
			if (($node['content']['starter'] != $node['content']['nodeid']) AND ($node['content']['starter'] != $node['content']['parentid']))
			{
				$parents[$nodeid] = $node['content']['parentid'];
			}

			if ($node['content']['contenttypeid'] == $pmContentType)
			{
				$nodes[$nodeid]['isVisitorMessage'] = 1;
			}
			else
			{
				$nodes[$nodeid]['isVisitorMessage'] = 0;
			}
			$userinfo = vB_User::fetchUserInfo($node['content']['userid']);

			$nodes[$nodeid]['userinfo'] = array('avatar' => $userApi->fetchAvatar($node['content']['userid'], array('avatar'), $userinfo ),
			'userid' => $node['content']['userid'], 'username' => $userinfo['username']);
		}

		//See if we need to add some parent information
		if (!empty($parents))
		{
			$parentInfo = vB_Api::instanceInternal('node')->getNodes($parents);

			foreach ($parents AS $nodeid => $parentid)
			{
				foreach ($parentInfo AS $info)
				{
					if ($info['nodeid'] == $parentid)
					{
						$nodes[$nodeid]['parent'] = $info;
					}
				}
			}
		}

		$this->addOptionalContentInfo($nodes, $options);
		$this->markSubscribed($nodes);
		$return = array('nodes' => $nodes);
		if (isset($totalCount))
		{
			$return['totalcount'] = $totalCount;
		}
		else
		{
			$return['totalcount'] = count($nodes);
		}

		if (isset($pageInfo) AND !empty($pageInfo))
		{
			$return['pageInfo'] = $pageInfo;
		}
		return $return;
	}

	/**
	 * Sets or unsets the approved field
	 * @param array $nodeids
	 * @param boolean $approved - set or unset the approved field
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have the permission to be changed
	 */
	function setApproved($approveNodeIds, $approved = true)
	{
		if (empty($approveNodeIds))
		{
			return false;
		}

		$nodeIds = array();
		foreach ($approveNodeIds AS $idx => $id)
		{
			$nodeInfo = $this->getNode($id);
			if (!empty($nodeInfo['errors']))
			{
				continue;
			}

			if (!$nodeInfo['approved'] AND !$approved)
			{
				continue;
			}

			if ($nodeInfo['approved'] AND $approved)
			{
				continue;
			}

			$nodeIds[] = $nodeInfo['nodeid'];
		}

		if (empty($nodeIds))
		{
			return false;
		}

		$errors = array();
		$assertor = vB::getDbAssertor();
		$result = $assertor->update('vBForum:node', array('approved' => $approved), array(array('field' => 'nodeid', 'value' => $nodeIds, 'operator' => vB_dB_Query::OPERATOR_EQ)));
		$method = empty($approved) ? 'unapproveNode' : 'approveNode';
		$result = $assertor->assertQuery('vBForum:' . $method, array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'nodeid' => $nodeIds));

		if (!empty($result['errors']))
		{
			$errors[] = $result['errors'];
		}
		$parents = $this->fetchClosureParent($nodeIds);
		$processed = array();
		foreach ($parents as $parent)
		{
			if (array_key_exists($parent['parent'], $processed))
			{
				continue;
			}
			vB_Node::fixNodeCount($parent['parent']);
			$result = $assertor->assertQuery('vBForum:updateLastData', array('parentid' => $parent['parent'], 'timenow' => vB::getRequest()->getTimeNow()));
			$processed[$parent['parent']] = $parent['parent'];

			if (!empty($result['errors']))
			{
				$errors[] = $result['errors'];
			}
		}
		$this->clearCacheEvents($nodeIds);
		$this->clearChildCache($nodeIds);

		if (!empty($errors))
		{
			return array('errors' => $errors);
		}

		// clear cache for those...

		return $nodeIds;
	}

	/**
	 * Approves a post. Since the publish date might be affected user will need moderate and
	 * publish posts permissions.
	 *
	 * @param	int		Id from the node we are approving.
	 * @param	int		Boolean used to set or unset the approved value
	 *
	 * @return	bool	Flag to indicate if approving went succesfully done (true/false).
	 */
	public function setApprovedPost($nodeid = false, $approved = false)
	{
		// @TODO remove and uncomment this when implementing VBV-585
		return $this->setApproved($nodeid, $approved);
		/*if (!intval($nodeid))
		{
			throw new vB_Exception_Api('invalid_node_id');
		}

		// let's check that node really exists
		$node = $this->getNode($nodeid);

		// and call the content api class to set approve
		if (empty($this->contentAPIs[$node['nodeid']]))
		{
			$this->contentAPIs[$node['nodeid']] =
				vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}
		return $this->contentAPIs[$node['nodeid']]->setApproved($node, $approved);*/
	}

	/**
	 * Clears the cache events from a given list of nodes.
	 * Useful to keep search results updated due node changes.
	 *
	 * @param	array		List of node ids to clear cached results.
	 *
	 * @return
	 */
	public function clearCacheEvents($nodeIds)
	{
		if (empty($nodeIds))
		{
			return false;
		}

		if (!is_array($nodeIds))
		{
			$nodeIds = array($nodeIds);
		}

		$cachedNodes = array();
		$notCached = array();
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		foreach($nodeIds AS $nodeid)
		{
			if ((!in_array('nodeChg_' . $nodeid, $cachedNodes)))
			{
				$cachedNodes[] = 'nodeChg_' . $nodeid;
			}

			$hashKey = 'node_' . $nodeid . "_lvl3data";
			$cached = $cache->read($hashKey);

			//We need the parent, starter, and channel. Let's see if we have those in fast cache.
			if ($cached)
			{
				foreach(array('starter', 'parentid', 'channelid') AS $field)
				{
					if ((!in_array('nodeChg_' . $cached[$field], $cachedNodes)))
					{
						$cachedNodes[] = 'nodeChg_' . $cached[$field];
					}
				}
			}
			else
			{
				$notCached[] = $nodeid;
			}

		}

		if (!empty($notCached))
			{
			$parents = vB::getDbAssertor()->assertQuery('vBForum:getParents', array('nodeid' => $notCached));
			foreach ($parents AS $parent)
			{
				if ((!in_array('nodeChg_' . $parent['nodeid'], $cachedNodes)))
				{
					$cachedNodes[] = 'nodeChg_' . $parent['nodeid'];
				}
			}
		}

		try
		{
			vB_Cache::instance()->event($cachedNodes);
			$cache->event($cachedNodes);
		}
		catch (Exception $ex)
		{
			throw new vB_Exception_Api($ex->getMessage());
		}

		return true;
	}


	/**
	* Marks a node as read using the appropriate method.
	*
	* @param int $nodeid The ID of node being marked
	*
	* @return	array	Returns an array of nodes that were marked as read
	*/
	public function markRead($nodeid)
	{
		$user = vB::getCurrentSession()->fetch_userinfo();

		if (!$user['userid'])
		{
			// Guest call
			return array();
		}

		$vboptions = vB::getDatastore()->getValue('options');

		if (!$vboptions['threadmarking'])
		{
			// Cookie based read marking. Handled by presentation.
			return array();
		}

		$node = $this->getNode($nodeid);

		$timenow = vB::getRequest()->getTimeNow();
		$channelcontenttypeid = vB_Types::instance()->getContentTypeId('vBForum_Channel');

		$nodes_marked = array($node['nodeid']);

		if ($node['contenttypeid'] == $channelcontenttypeid)
		{
			// Mark channel read
			vB::getDbAssertor()->assertQuery('node_markread', array(
				'nodeid' => $node['nodeid'],
				'userid' => $user['userid'],
				'readtime' => $timenow,
			));

			// check to see if any parent channels should be marked as read as well
			$parentchannels = vB::getDbAssertor()->getRows('closure', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'child' => $node['nodeid']
			), array(
				'field' => array('depth'),
				'direction' => array(vB_dB_Query::SORT_ASC),
			));

			$parentarray = array();
			foreach ($parentchannels as $parent)
			{
				if ($parent['parent'] != $node['nodeid'] AND $parent['parent'] != 1)
				{
					$parentarray[] = $parent['parent'];
				}
			}

			if (!empty($parentarray))
			{
				// find the top most entry in the parent list -- we need its child list
				$top_parentid = end($parentarray);
				$top_parentchildren = $this->listNodes($top_parentid, 1, 100, 0, $channelcontenttypeid, array());
				$top_parentchildrenids = array($top_parentid);
				foreach ($top_parentchildren as $child)
				{
					$top_parentchildrenids[] = $child['nodeid'];
				}

				if (!$top_parentchildrenids)
				{
					return $nodes_marked;
				}

				// determine the read time for all forums that we need to consider
				$readtimes = array();
				$readtimedata = vB::getDbAssertor()->getRows('noderead', array(
					'userid' => $user['userid'],
					'nodeid' => $top_parentchildrenids
				));
				foreach ($readtimedata as $readtime)
				{
					$readtimes["$readtime[forumid]"] = $readtime['readtime'];
				}

				$cutoff = ($timenow - ($vboptions['markinglimit'] * 86400));

				$parentscache = vB_Api::instanceInternal('node')->getNodes($parentarray);

				// now work through the parent, grandparent, etc of the forum we just marked
				// and mark it read only if all direct children are marked read
				$parentsquery = array();
				foreach ($parentarray AS $parentid)
				{
					if (empty($parentscache["$parentid"]))
					{
						continue;
					}

					$markread = true;
					$thischildren = vB_Api::instanceInternal('node')->listNodeContent($parentid, 1, 100, 0, $channelcontenttypeid);

					// now look through all the children and confirm they are all read
					foreach ($thischildren AS $thischild)
					{
						$check = max($cutoff, $readtimes[$thischild['nodeid']]);
						if (
							($thischild['lastcontent'] AND $check < $thischild['lastcontent'])
							OR
							(!$thischild['lastcontent'] AND $check < $thischild['created'])
						)
						{
							$markread = false;
							break;
						}
					}

					// if all children are read, make sure all the threads in this forum are read too
					if ($markread)
					{
						$noderead = intval(max($readtimes["$parentid"], $cutoff));

						$unread = vB::getDbAssertor()->getRow('node_checktopicread', array(
							'userid' => $user['userid'],
							'parentid' => $parentid,
							'noderead' => $noderead,
						));
						if ($unread['count'] > 0)
						{
							$markread = false;
						}
					}

					if ($markread)
					{
						// can mark as read
						$readtimes["$parentid"] = $timenow;
						$parentsquery[] = array($parentid, $user['userid']);
						$nodes_marked[] = $parentid;
					}
					else
					{
						// can't mark this as read, so we have no need to continue with further generations
						break;
					}
				}

				if ($parentsquery)
				{
					foreach ($parentsquery as $parent)
					{
						vB::getDbAssertor()->assertQuery('node_markread', array(
							'nodeid' => $parent[0],
							'userid' => $parent[1],
							'readtime' => $timenow,
						));
					}
				}
			}

			return $nodes_marked;
		}
		elseif ($node['nodeid'] == $node['starter'])
		{
			// Topic
			vB::getDbAssertor()->assertQuery('node_markread', array(
				'nodeid' => $node['nodeid'],
				'userid' => $user['userid'],
				'readtime' => $timenow,
			));

			// now if applicable search to see if this was the last topic requiring marking in this channel
			if ($vboptions['threadmarking'] == 2)
			{
				// channel can only be marked as read if all the children are read as well,
				// so determine which children "count"
				$channelchildren = $this->listNodes($node['parentid'], 1, 100, 0, $channelcontenttypeid, array());
				$channelchildrenids = array($node['parentid']);
				foreach ($channelchildren as $child)
				{
					$channelchildrenids[] = $child['nodeid'];
				}
				$children = array();
				$usercontext = vB::getUserContext($user['userid']);
				foreach ($channelchildrenids as $child_forum)
				{
					if (
						!$usercontext->getChannelPermission('forumpermissions', 'canview', $child_forum)
						OR
						!$usercontext->getChannelPermission('forumpermissions', 'canviewthreads', $child_forum)
						OR
						!$usercontext->getChannelPermission('forumpermissions', 'canviewothers', $child_forum)
					)
					{
						// invalid channel, can't be viewed, can't view topics, can't view others topics
						// means we can't include this when trying to mark a thread as read
						continue;
					}

					$children[] = $child_forum;
				}

				if ($children)
				{
					$cutoff = $timenow - ($vboptions['markinglimit'] * 86400);
					$unread = vB::getDbAssertor()->getRow('node_checktopicreadinchannels', array(
						'userid' => $user['userid'],
						'children' => $children,
						'cutoff' => $cutoff,
					));
					if ($unread['count'] == 0)
					{
						$this->markRead($node['parentid']);
					}
				}
			}

			return $nodes_marked;
		}
		else
		{
			return array();
		}
	}


	/**
	* Marks a channel, its child channels and all contained topics as read
	*
	* @param int $nodeid The node ID of channel being marked. If 0, all channels will be marked as read
	*
	* @return	array	Returns an array of channel ids that were marked as read
	*/
	public function markChannelsRead($nodeid = 0)
	{
		$user = vB::getCurrentSession()->fetch_userinfo();

		if (!$user['userid'])
		{
			// Guest call
			return array();
		}

		$vboptions = vB::getDatastore()->getValue('options');
		$timenow = vB::getRequest()->getTimeNow();
		$return_channels = array();

		if ($nodeid)
		{
			$node = $this->getNode($nodeid);

			$channelcontenttypeid = vB_Types::instance()->getContentTypeId('vBForum_Channel');

			if ($node['contenttypeid'] != $channelcontenttypeid)
			{
				return array();
			}

			$children = $this->listNodes($node['nodeid'], 1, 100, 0, $channelcontenttypeid, array());
			$childrenids = array($node['nodeid']);
			foreach ($children as $child)
			{
				$childrenids[] = $child['nodeid'];
			}
			$return_channels = $childrenids;

			if ($vboptions['threadmarking'] AND $user['userid'])
			{
				foreach ($return_channels as $channelid)
				{
					// mark the channel and all child channels read
					vB::getDbAssertor()->assertQuery('node_markread', array(
						'nodeid' => $channelid,
						'userid' => $user['userid'],
						'readtime' => $timenow,
					));
				}

				$parent_marks = $this->markRead($nodeid);
				if (is_array($parent_marks))
				{
					$return_channels = array_unique(array_merge($return_channels, $parent_marks));
				}
			}
		}
		else
		{
			// Mark all channels read
			if ($user['userid'])
			{
				// init user data manager
				$userdata = new vB_Datamanager_User(null, vB_DataManager_Constants::ERRTYPE_STANDARD);
				$userdata->set_existing($user);
				$userdata->set('lastactivity', $timenow);
				$userdata->set('lastvisit', $timenow - 1);
				$userdata->save();

				if ($vboptions['threadmarking'])
				{
					$channels = vB_Api::instanceInternal('search')->getChannels(true);
					foreach ($channels AS $nodeid => $channelinfo)
					{
						// mark the channel and all child channels read
						vB::getDbAssertor()->assertQuery('node_markread', array(
							'nodeid' => $nodeid,
							'userid' => $user['userid'],
							'readtime' => $timenow,
						));
						$return_channels[] = $nodeid;
					}
				}
			}
		}
		return $return_channels;
	}


	/** marks nodes with "subscribed" true/false
	*
	*	@param	array	list of nodes, normally with a content array.
	**/
	public function markSubscribed(&$nodes)
	{
		$following = vB_Api::instanceInternal('follow')->getFollowingParameters();
		$check = array();
		foreach ($nodes AS $key => &$node)
		{
			if (array_key_exists('content', $node))
			{
				$node['content']['subscribed'] = 0;
			}
			else
			{
				$node['subscribed'] = 0;
			}
			$check[$node['nodeid']]	= $key;
		}

		//if this user isn't following anyone, we don't need to do this check.
		if (!empty($following['user']) )
		{
			foreach ($nodes AS $key => &$node)
			{
				if (in_array($node['userid'], $following['user']))
				{
					$node['content']['subscribed'] = 1;
				}
			}
		}

		//if there's nothing to check and no followed content, we're done.
		if (empty($check) or (empty($following['content']) AND empty($following['member'])))
		{
			return;
		}
		//We have both followed nodes and content, so we need to run a query and check.
		$followNodes = array_merge($following['content'], $following['member']);
		$clParents = $this->fetchClosureParent(array_keys($check));
		$parents = array();
		foreach ($clParents AS $parent)
		{
			//We have a child value in $closureRec['child'] is an index into $check, which gives an index into $nodes;
			if (in_array($parent['parent'], $followNodes))
			{
				$nodeKey = $check[$parent['child']];
				//This node is followed.
				if (array_key_exists('content', $nodes[$nodeKey]))
				{
					$nodes[$nodeKey]['content']['subscribed'] = 1;
				}
				else
				{
					$nodes[$nodeKey]['subscribed'] = 0;
				}
			}
		}
	}

	/** Returns closure table information given a child id
	 *
	 *	@param	mixed	child nodeid or array of nodeids
	 * 	@param	int		optional parent nodeid
	 *
	 * 	@return	mixed	array of closure table records
	 **/
	public function fetchClosureParent($childids, $parentid = false)
	{
		static $cachedChildIds = array();
		//find what we have in fastcache.
		if (!is_array($childids))
		{
			$childids = array($childids);
		}

		$cachedChildIds = array_unique(array_merge($cachedChildIds, $childids));
		vB_Api::instanceInternal('page')->registerPrecacheInfo('node', 'fetchClosureParent', $cachedChildIds);
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		$found = array();
		$notfound = array();
		foreach ($childids AS $childid)
		{
			$data = $cache->read("vBClParents_$childid");
			if ($data)
			{
				//we marked any id's that don't have data with zero. In that case we return no results.
				if ($data === 0 )
				{
					continue;
				}

				if (($parentid === false) OR !is_numeric($parentid))
				{
					$found = array_merge($found, $data);
				}
				else
				{
					foreach ($data as $closure)
					{
						if ($closure['parent'] == $parentid)
						{
							$found[] = $closure;
							break;
						}
					}
				}
			}
			else
			{
				$notfound[$childid] = $childid;
			}
		}

		//if we got everything, we're done.
		if (empty($notfound))
		{
			return $found;
		}

		//Search for what's left
		//Note that even if we were passed a parentid we still get the complete ancestry and cache it.
		$closureRecs = vB::getDbAssertor()->assertQuery('vBForum:closure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			'child' => $notfound), array('field' => (array('child', 'depth')), 'direction' => array(vB_dB_Query::SORT_ASC, vB_dB_Query::SORT_ASC)));
		//Now we build the results and cache the values;

		$thisChild = false;
		foreach ($closureRecs AS $closure)
		{
			//If we changed child id values and this isn't the first value, write to cache
			// and start building the ancestry for the new child.
			if ($thisChild != $closure['child'])
			{
				if ($thisChild)
				{
					$cache->write("vBClParents_$thisChild", $cacheValue, 1440, $cacheEvents);
					unset($notfound[$thisChild]);
				}
				$cacheValue = array();
				$cacheEvents = array();
				$thisChild = $closure['child'];

			}

			if (empty($parentid) OR ($closure['parent'] == $parentid))
			{
				$found[] = $closure;
			}
			$cacheValue[] = $closure;
			$cacheEvents[] = 'nodeChg_' . $closure['parent'];
		}

		if ($thisChild)
		{

			$cache->write("vBClParents_$thisChild", $cacheValue, 1440, $cacheEvents);

			if (empty($parentid))
			{
				$found[] = $closure;
			}
			unset($notfound[$thisChild]);
		}

		//Any remaining records in $notfound are for nodes that aren't in the closure table.
		// we'll cache those with value zero so we don't query again.
		foreach ($notfound AS $childid)
		{
			$cache->write("vBClParents_$childid", 0, 1440, "nodeChg_$childid");
		}

		return $found;
	}


	/** Returns closure table information given a child id
	 *
	 *	@param	mixed	parent nodeid or array of nodeids
	 *
	 * 	@return	mixed	array of closure table records
	 **/
	public function fetchClosurechildren($parentids)
	{
		//find what we have in fastcache.
		if (!is_array($parentids))
		{
			$parentids = array($parentids);
		}
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$found = array();
		$notfound = array();
		foreach ($parentids AS $parentid)
		{
			$data = $cache->read("vBClChildren_$parentid");
			//we marked any id's that don't have data with zero. In that case we return no results.
			if ($data)
			{
				$found[$parentid] = $data;
			}
			else if ($data !== 0)
			{
				$notfound[$parentid] = $parentid;
			}
		}

		//if we got everything, we're done.
		if (empty($notfound))
		{
			return $found;
		}

		//Search for what's left
		//Note that even if we were passed a parentid we still get the complete ancestry and cache it.
		$closureRecs = vB::getDbAssertor()->assertQuery('vBForum:closure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			'parent' => $notfound), array('field' => array('parent', 'depth'),
			'direction' => array(vB_dB_Query::SORT_ASC, vB_dB_Query::SORT_DESC)));
		//Now we build the results and cache the values;

		$thisParent = false;
		foreach ($closureRecs AS $closure)
		{
			//If we changed child id values and this isn't the first value, write to cache
			// and start building the ancestry for the new child.
			if ($thisParent != $closure['parent'])
			{
				if ($thisParent)
				{
					$cache->write("vBClChildren_$thisParent", $cacheValue, 1440, $cacheEvents);
					unset($notfound[$thisParent]);
					$found[$thisParent] = $cacheValue;
				}
				$cacheValue = array();
				$cacheEvents = array();
				$thisParent = $closure['parent'];

			}

			$cacheValue[] = $closure;
			$cacheEvents[] = 'nodeChg_' . $closure['child'];
		}

		if ($thisParent)
		{
			$found[$thisParent] = $cacheValue;
			$cache->write("vBClChildren_$thisParent", $cacheValue, 1440, $cacheEvents);
			unset($notfound[$thisParent]);
		}


		//Any remaining records in $notfound are for nodes that aren't in the closure table.
		// we'll cache those with value zero so we don't query again.
		foreach ($notfound AS $parentid)
		{
			$cache->write("vBClChildren_$parentid", 0, 1440, "nodeChg_$parentid");
		}

		return $found;
	}

	/** This creates a request for access to a channel
	 *
	 *	@param	int		the nodeid of the channel to which access is requested.
	 *	@param	mixed	the userid/username of the member who will get the request
	 *	@param	string	the type of request-
	 *
	 **/
	public function requestChannel($channelid, $recipient, $requestType, $skipFloodCheck = false)
	{
		$userInfo = vB::getCurrentSession()->fetch_userinfo();
		$userid = $userInfo['userid'];

		if (!($userid > 0))
		{
			throw new vB_Exception_API('not_logged_no_permission');
		}

		//make sure the parameters are valid
		if (!intval($channelid) OR !intval($channelid) OR
			!in_array($requestType,
				array(vB_Api_Node::REQUEST_TAKE_OWNER, vB_Api_Node::REQUEST_TAKE_MODERATOR, vB_Api_Node::REQUEST_TAKE_MEMBER,
					vB_Api_Node::REQUEST_GRANT_OWNER, vB_Api_Node::REQUEST_GRANT_MODERATOR, vB_Api_Node::REQUEST_GRANT_MEMBER,
					vB_Api_Node::REQUEST_SG_TAKE_OWNER, vB_Api_Node::REQUEST_SG_TAKE_MODERATOR, vB_Api_Node::REQUEST_SG_TAKE_MEMBER,
					vB_Api_Node::REQUEST_SG_GRANT_OWNER, vB_Api_Node::REQUEST_SG_GRANT_MODERATOR, vB_Api_Node::REQUEST_SG_GRANT_MEMBER,
					vB_Api_Node::REQUEST_TAKE_SUBSCRIBER, vB_Api_Node::REQUEST_GRANT_SUBSCRIBER, vB_Api_Node::REQUEST_SG_TAKE_SUBSCRIBER, vB_Api_Node::REQUEST_SG_GRANT_SUBSCRIBER
					))
		)
		{
			throw new vB_Exception_API('invalid_data');
		}

		if(!intval($recipient))
		{
			// fetch by username
			$recipient = vB::getDbAssertor()->getField('user', array('username' => $recipient));

			if (!$recipient)
			{
				throw new vB_Exception_API('invalid_data');
			}
		}

		$node = $this->getNode($channelid);

		if ($node['contenttypeid'] != vB_Types::instance()->getContentTypeId('vBForum_Channel'))
		{
			throw new vB_Exception_API('invalid_request');
		}

		//Let's make sure the user can grant this request.
		if (in_array($requestType, array(
				vB_Api_Node::REQUEST_TAKE_OWNER,
				vB_Api_Node::REQUEST_TAKE_MODERATOR,
				vB_Api_Node::REQUEST_TAKE_MEMBER,
				vB_Api_Node::REQUEST_SG_TAKE_OWNER,
				vB_Api_Node::REQUEST_SG_TAKE_MODERATOR,
				vB_Api_Node::REQUEST_SG_TAKE_MEMBER,
			)))
		{
			//Can we grant the transfer?
			$userContext = vB::getUserContext();
			if (!$userContext->getChannelPermission('moderatorpermissions', 'canaddowners', $channelid ))
			{
				throw new vB_Exception_API('no_permission');
			}
		}
		else
		{
			// join is not valid when invite only...
			if (in_array($requestType, array(vB_Api_Node::REQUEST_GRANT_MEMBER, vB_Api_Node::REQUEST_SG_GRANT_MEMBER)) AND
				(($node['nodeoptions'] & vB_Api_Node::OPTION_NODE_INVITEONLY) > 0))
			{
				throw new vB_Exception_Api('invalid_invite_only_request');
			}

			//if this is set to auto-approve we don't need to send a request.
			if (in_array($requestType, array(vB_Api_Node::REQUEST_GRANT_MEMBER, vB_Api_Node::REQUEST_SG_GRANT_MEMBER)) AND
				(($node['nodeoptions'] & vB_Api_Node::OPTION_AUTOAPPROVE_MEMBERSHIP) > 0))
			{
				$group = vB::getDbAssertor() -> getRow('usergroup', array('systemgroupid' => vB_Api_UserGroup::CHANNEL_MEMBER_SYSGROUPID));
				return vB_User::setGroupInTopic($userid, $channelid, $group['usergroupid']);
			}

			if (in_array($requestType, array(vB_Api_Node::REQUEST_GRANT_SUBSCRIBER, vB_Api_Node::REQUEST_SG_GRANT_SUBSCRIBER)))
			{
				// subscribe means join in blog's context
				try
				{
					$isBlog = vB_Api::instanceInternal('blog')->isBlogNode($channelid);
					if ($isBlog)
					{
						return true;
					}

					$sgApi = vB_Api::instanceInternal('socialgroup');
					//	@TODO check if using only the canview perms is fair enough... there might be cases where sg owner set canview perms for everyone that includes no joined members, even not logged users...
					if (($sgApi->isSGNode($channelid) AND $sgApi->isChannelMember($channelid) != 1) OR !vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $channelid))
					{
						throw new vB_Exception_Api('invalid_special_channel_subscribe_request');
					}

					// check the auto accept first
					if (($node['nodeoptions'] & vB_Api_Node::OPTION_AUTOAPPROVE_SUBSCRIPTION) > 0)
					{
						return $response = vB_Api::instanceInternal('follow')->add($channelid, vB_Api_Follow::FOLLOWTYPE_CONTENT);
					}
					else
					{
						$owner = vB_Api::instanceInternal('blog')->fetchOwner($channelid);
						if (!$owner)
						{
							$recipient = $node['userid'];
						}
						else
						{
							$recipient = $owner;
						}
					}
				}
				catch (vB_Exception_Api $ex)
				{
					throw $ex;
				}
			}

			//Can the recipient grant the transfer?
			$userContext = vB::getUserContext($recipient);
			if (!$userContext->getChannelPermission('moderatorpermissions', 'canaddowners', $channelid ))
			{
				throw new vB_Exception_API('no_permission');
			}
		}

		$messageLib = vB_Library::instance('content_privatemessage');
		$data = array('msgtype' => 'request',
			'about' => $requestType,
			'sentto' => $recipient,
			'aboutid' => $channelid);

		if ($skipFloodCheck)
		{
			return $messageLib->addMessageNoFlood($data);
		}
		else
		{
			return $messageLib->add($data);
		}
	}

	/**
	 * Adds the joined flag if the current user is member of content's parent.
	 *
	 * @params	array	Array of the content node list.
	 *
	 */
	public function markJoined(&$nodes)
	{
		$parents = array();
		foreach ($nodes AS $key => $node)
		{
			if (!empty($node['parentid']))
			{
				$parents[] = $node['parentid'];
			}
			else if (array_key_exists('content', $node) AND !empty($node['content']['parentid']))
			{
				$parents[] = $node['content']['parentid'];
			}
		}

		$parents = array_unique($parents);
		if (empty($parents))
		{
			return false;
		}

		$userid = vB::getCurrentSession()->get('userid');
		$hashKey = 'joinedFlagParents_' . $userid . crc32(serialize($parents));
		$joined = vB_Cache::instance(vB_Cache::CACHE_FAST)->read($hashKey);
		if ($joined === false)
		{
			$joinedInfo = vB::getDbAssertor()->assertQuery('vBForum:groupintopic', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'userid' => $userid,
				'nodeid' => $parents
			));

			$joined = array();
			while ($joinedInfo AND $joinedInfo->valid())
			{
				$node = $joinedInfo->current();
				$joined[$node['nodeid']] = $node;

				$joinedInfo->next();
			}

			vB_Cache::instance(vB_Cache::CACHE_FAST)->write($hashKey, $joined, 60, array('sgMemberChg_' . $userid));
		}

		foreach ($nodes AS $key => $node)
		{
			if (array_key_exists('content', $node))
			{
				$nodes[$key]['content']['joined'] = (!empty($nodes[$key]['content']['parentid']) AND !empty($joined[$nodes[$key]['content']['parentid']])) ? 1 : 0;
			}
			else
			{
				$nodes[$key]['joined'] = (!empty($nodes[$key]['parentid']) AND !empty($joined[$nodes[$key]['parentid']])) ? 1 : 0;
			}
		}
	}
}
