<?php if (!defined('VB_ENTRY')) die('Access denied.');
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 5.1.9 Patch Level 4 - Licence Number LD125EAAF9
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2016 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
{

	protected $nodeFields = array();
	protected $contentLibs = array();

	protected $albumChannel = false;
	protected $VMChannel = false;
	protected $PMChannel = false;
	protected $SGChannel = false;
	protected $ReportChannel = false;
	protected $forumChannel = false;
	protected $infractionChannel = false;
	protected $articleChannel = false;

	protected $channelTypeId;

	protected function __construct()
	{

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

	/**
	 * 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()->allCacheEvent($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)
	{
		$loginfo = array();
		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)
		{
			$node = $this->getNode($nodeid);
			$result = vB::getDbAssertor()->assertQuery('vBForum:openNode', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'nodeid' => $nodeid));

			$loginfo[] = array(
				'nodeid'		=> $node['nodeid'],
				'nodetitle'		=> $node['title'],
				'nodeusername'	=> $node['authorname'],
				'nodeuserid'	=> $node['userid']
			);

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

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

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

		vB_Library_Admin::logModeratorAction($loginfo, 'node_opened_by_x');

		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)
	{
		$loginfo = array();
		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)
		{
			$node = $this->getNode($nodeid);
			$result = vB::getDbAssertor()->assertQuery('vBForum:closeNode', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'nodeid' => $nodeid));

			$loginfo[] = array(
				'nodeid'		=> $node['nodeid'],
				'nodetitle'		=> $node['title'],
				'nodeusername'	=> $node['authorname'],
				'nodeuserid'	=> $node['userid']
			);

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

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

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

		vB_Library_Admin::logModeratorAction($loginfo, 'node_closed_by_x');

		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');
			}
		}

		if ($existing['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Infraction'))
		{
			throw new vB_Exception_Api('cannot_delete_infraction_nodes');
		}

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

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

		if(!empty($result))
		{
			vB_Api::instanceInternal('Search')->delete($nodeid);
			vB_Api::instanceInternal('Search')->purgeCacheForCurrentUser();
			$this->resetAncestorCounts($existing, $ancestors, true);
		}

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

		vB_Library_Admin::logModeratorAction($loginfo, 'node_hard_deleted_by_x');

		return($result);
	}

	/**
	 * Updates the ancestors counts and last data from a given node being deleted.
	 * Counts and last data are info from the node table records:
	 * totalcount, totalunpubcount, textcount, textunpubcount
	 * lastcontentid, lastcontent, lastauthor, lastauthorid.
	 * Is critical that the ancestors are in DESC order so we can properly update.
	 *
	 * @param	array		The node being deleted information.
	 * @param	array		Information from the node's ancestors needed to update last (nodeid, contenttypeid needed).
	 * @param	bool		Flag indicating if we are soft/hard-deleting
	 *
	 */
	public function resetAncestorCounts($existing, $ancestorsData, $hard)
	{
		if (empty($ancestorsData))
		{
			//This can happen with a defective node while it is being deleted.
			return;
		}

		// now update last content and counts for parents
		$ancestorsId = array();
		$toUpdate = array();
		foreach ($ancestorsData AS $ancestor)
		{
			$ancestorsId[] = $ancestor['nodeid'];
			$toUpdate[$ancestor['nodeid']] = array('nodeid' => $ancestor['nodeid'], 'contenttypeid' => $ancestor['contenttypeid']);
		}

		// make sure we have unique ancestors and they're in the right order
		krsort($toUpdate);
		$ancestorsId = array_unique($ancestorsId);

		// reset last content for all parents that have the deleted node
		if ($existing['showpublished'] AND $hard)
		{
			$totalChange = -1 - $existing['totalcount'];
			$totalUnpubChange = 0;
			$textChange = -1;
			$textUnpubChange = 0;
		}
		else if (!$existing['showpublished'] AND $hard)
		{
			$totalChange = 0;
			$totalUnpubChange = -1 - $existing['totalunpubcount'];
			$textChange = 0;
			$textUnpubChange = -1;
		}
		else if ($existing['showpublished'] AND !$hard)
		{
			$totalChange = -1 - $existing['totalcount'];
			$totalUnpubChange = 1 + $existing['totalcount'];
			$textChange = -1;
			$textUnpubChange = 1;
		}
		else
		{
			$totalChange = 0;
			$totalUnpubChange = 0;
			$textChange = 0;
			$textUnpubChange = 0;

		}
		//Update total counts.
		vB::getDbAssertor()->assertQuery('vBForum:UpdateAncestorCount',
			array(
				'totalChange' => $totalChange,
				'totalUnpubChange' => $totalUnpubChange,
				'nodeid' => $ancestorsId)
		);
		//text counts for parent only.
		vB::getDbAssertor()->assertQuery('vBForum:UpdateParentTextCount',
			array(
				'textChange' => $textChange,
				'textUnpubChange' => $textUnpubChange,
				'nodeid' => $existing['parentid'])
		);

		//And the "last" data. We have to work bottom-to-top.
		$searchAPI = vB_Api::instanceInternal('search');
		foreach ($toUpdate AS $ancestor)
		{
			if ($ancestor['contenttypeid'] == $this->channelTypeId)
			{
				$this->fixNodeLast($ancestor['nodeid']);
			}
			else
			{
				vB::getDbAssertor()->assertQuery('vBForum:updateLastData', array('parentid' => $ancestor['nodeid'], 'timenow' => vB::getRequest()->getTimeNow()));
			}

			vB_Cache::instance()->allCacheEvent("nodeChg_" . $ancestor['nodeid']);
			$searchAPI->attributeChanged($ancestor['nodeid']);
		}

	}


	/** 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)
	 *	@param	bool	Log the deletes in moderator log
	 *  @param	bool	Report node content to spam service
	 *
	 *	@return	array nodeids that were deleted
	 **/
	public function deleteNodes($deleteNodeIds, $hard, $reason, $ancestorsId, $starters, $modlog = true, $reportspam = false)
	{
		$loginfo = array();
		$starters = array();
		$needRebuild = false;

		$vboptions = vB::getDatastore()->getValue('options');
		if ((!$vboptions['vb_antispam_type'] OR !$vboptions['vb_antispam_key']) AND !$vboptions['vb_antispam_sfs_key'])
		{
			$reportspam = false;
		}

		//if we are doing a hard delete we need to first delete the type-specific data.
		if ($reportspam)
		{
			$nodes = $this->getContentforNodes($deleteNodeIds);
			$akismet = vB_Akismet::instance();
			$stopForumSpam = vB_StopForumSpam::instance();
			foreach ($nodes AS $node)
			{
				if ($node['content']['rawtext'])
				{
					$text = vB_String::stripBbcode($node['content']['rawtext'], true);
					$akismet->markAsSpam(
						array(
							'comment_type'    => 'comment',
							'comment_author'  => $node['content']['authorname'],
							'comment_content' => $text,
							'user_ip'         => $node['content']['ipaddress']
					));

					if ($vboptions['vb_antispam_sfs_key'])
					{
						$userinfo = vB_User::fetchUserinfo($node['content']['userid']);	// must have email address
						$stopForumSpam->markAsSpam($userinfo['username'], $node['content']['ipaddress'], $text, $userinfo['email']);
					}
				}
			}
		}
		else
		{
			$nodes = $this->getNodes($deleteNodeIds, false);
		}

		if ($hard)
		{
			$infractionTypeid = vB_Types::instance()->getContentTypeId('vBForum_Infraction');

			foreach ($nodes AS $node)
			{
				if ($node['contenttypeid'] == $infractionTypeid)
				{
					throw new vB_Exception_Api('cannot_delete_infraction_nodes');
				}

				//see if we need a rebuild
				if ($node['contenttypeid'] == vB_Types::instance()->getContentTypeID('vBForum_Channel'))
				{
					$needRebuild = true;
				}

				try
				{
					$starters[] = $node['starter'];

					// content delete method handle counts updating
					vB_Api_Content::getContentApi($node['contenttypeid'])->delete($node['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.

					// Actually, it's possible that the content API will throw a no_permission exception if it fails the
					// validate() check for "action delete." In that case, we're just silently continuing, which could
					// result in a node not being deleted with 0 indication that something went wrong.
					// We should think of a better way to output this situation to the moderator.

					// If the node did not get deleted. Possibly due to lack of permission to delete.
					// 1. check if node still exists (which means there was actually a problem deleting, not that it was a non-existent node)
					// 2. if "yes", remove the node from $deleteNodeIds so the return value doesn't include it and so no further processing happens
					// 3. skip to the next iteration of this loop, so this nodeid is not included in the mod log.
					$check = vB::getDbAssertor()->getRows('vBForum:node', array('nodeid' => $node['nodeid']));
					if (count($check) == 1)
					{
						// $deleteNodeIds can be an array or a single nodeid
						if (is_array($deleteNodeIds))
						{
							$key = array_search($node['nodeid'], (array)$deleteNodeIds, true);
							if ($key !== false)
							{
								unset($deleteNodeIds[$key]);
							}
						}
						else if ($deleteNodeIds == $node['nodeid'])
						{
							$deleteNodeIds = '';
						}

						continue;
					}
				}

				// Note: Do not decrement user post count here. That is done
				// in the content library for hard-deletes.

				if($modlog)
				{
					$loginfo[] = array(
						'nodeid'       => $node['nodeid'],
						'nodetitle'    => $node['title'],
						'nodeusername' => $node['authorname'],
						'nodeuserid'   => $node['userid'],
					);
				}
			}

			vB_Library_Admin::logModeratorAction($loginfo, 'node_hard_deleted_by_x');
		}
		else
		{
			$fields = array (
				'unpublishdate' => vB::getRequest()->getTimeNow(),
				'deletereason' => $reason,
				'deleteuserid' => vB::getCurrentSession()->get('userid'),
				'approved'     => 1,
				'showapproved' => 1,
			);
			$result = vB::getDbAssertor()->update('vBForum:node', $fields, array('nodeid' => $deleteNodeIds));

			$errors = array();
			foreach ($nodes as $node)
			{
				//see if we need a rebuild
				if ($node['contenttypeid'] == vB_Types::instance()->getContentTypeID('vBForum_Channel'))
				{
					$needRebuild = true;
				}
				$starters[] = $node['starter'];

				$nodeUpdates = $this->unpublishChildren($node['nodeid']);
				$this->updateChangedNodeParentCounts($node, $nodeUpdates);

				// Update user post count (soft delete)
				vB_Library_Content::getContentLib($node['contenttypeid'])->decrementUserPostCount($node, 'unpublish');

				if($modlog)
				{
					$loginfo[] = array(
						'nodeid'		=> $node['nodeid'],
						'nodetitle'		=> $node['title'],
						'nodeusername'	=> $node['authorname'],
						'nodeuserid'	=> $node['userid']
					);
				}

				if (!empty($node['setfor']))
				{
					vB_Cache::instance()->allCacheEvent('fUserContentChg_' . $node['setfor']);
				}
			}

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

			vB_Library_Admin::logModeratorAction($loginfo, 'node_soft_deleted_by_x');
		}

		$starters = array_unique($starters);
		// reset last content for all parents that have the deleted nodes
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$fastCache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$events = array();

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

		foreach ($deleteNodeIds as $nodeid)
		{
			$events[] = "nodeChg_" . $nodeid;
		}
		$events = array_unique($events);
		$cache->allCacheEvent($events);
		vB_Api::instance('Search')->purgeCacheForCurrentUser();

		if ($needRebuild)
		{
			vB::getUserContext()->rebuildGroupAccess();
			vB_Channel::rebuildChannelTypes();
		}

		return $deleteNodeIds;
	}

	/** 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)
	 *	@param	bool	Include joinable content
	 *
	 * 	@return	mixed	array of id's
	 **/
	public function listNodes($parentid, $page, $perpage, $depth, $contenttypeid, $options, $withJoinableContent = false)
	{
		//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 = $this->getNodes($srchResults['nodeIds'], $withJoinableContent);

				$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;
		}

		//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)
		{
			if (!empty($options['depth_exact']))
			{
				$criteria['depth_exact'] = 1;
			}
			$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'];
		}

		if (isset($options['includeProtected']) AND !($options['includeProtected']))
		{
			$criteria['ignore_protected'] = 1;
		}

		$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['authorid'] = $options['userid'];
		}

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

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

		// we don't want to store a cached value into another cache
		$criteria['ignore_cache'] = true;
		$results = $searchApi->getInitialNodes($criteria, $perpage, $page);

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

		if (!empty($results['nodeIds']))
		{
			//Track Visits
			vB::getDbAssertor()->assertQuery('vBForum:trackNodeVisits', array(
				'userid' => vB::getCurrentSession()->get('userid'),
				'dateline' => $dayStart,
				'nodeid' => $parentid
			));
			$nodes = $this->getNodes($results['nodeIds'], $withJoinableContent);

			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  int   The Node ID
	 *
	 * @return mixed Array of node records
	 */
	public function getNodeBare($nodeid)
	{
		if (empty($nodeid))
		{
			return false;
		}
		$node = vB_Library_Content::fetchFromCache($nodeid, vB_Library_Content::CACHELEVEL_NODE);

		if ($node AND !empty($node['found']) AND !empty($node['found'][$nodeid]))
		{
			$node = $node['found'][$nodeid];
		}
		else
		{
			$node = vB::getDbAssertor()->getRow('vBForum:node', array('nodeid' => $nodeid));

			if (empty($node) OR !empty($node['errors']))
			{
				throw new vB_Exception_Api('invalid_node_id', array('nodeid' => $nodeid));
			}

			// censor textual node items
			vB_Library_Node::censorNode($node);

			vB_Library_Content::writeToCache(array($node), vB_Library_Content::CACHELEVEL_NODE);
		}

		return $node;
	}

	/**
	 * Censors the different text items that need censoring
	 *
	 * @param array (reference) The array of node information
	 */
	public static function censorNode(&$node)
	{
		$nodes = array();
		$nodes[] =& $node;
		vB_Library_Node::censorNodes($nodes);
	}

	/**
	 * Censors the different text items in each node that need censoring
	 *
	 * @param array (reference) The array of node information
	 */
	public static function censorNodes(&$nodes)
	{
		$items = array('title', 'htmltitle');

		foreach ($nodes as $key => $node)
		{
			foreach ($items AS $item)
			{
				if (!empty($node[$item]))
				{
					$nodes[$key][$item] = vB_String::fetchCensoredText($node[$item]);
				}
			}
		}
	}

	/**
	 * Gets one node.
	 * @param	integer The node id
	 * @param	boolean Whether to include list of parents
	 * @param	boolean Whether to include attachments
	 * @param	boolean	Include joinable content
	 *
	 * @return Array.  A node record, optionally including attachment and ancestry.
	 **/
	public function getNode($nodeid, $withParents = false, $withJoinableContent = false)
	{
		if ($withJoinableContent)
		{
			$node = $this->getNodeFullContent($nodeid);
			$node = reset($node);
		}
		else
		{
			$node = $this->getNodeBare($nodeid);
		}

		if ($withParents)
		{
			$node['parents'] = $this->getNodeParents($nodeid);
			$node['parents_reversed'] = array_reverse($node['parents']);
		}

		foreach (vB_Api::instanceInternal('node')->getOptions() as $key => $bitmask)
		{
			$node[$key] = ($bitmask & $node['nodeoptions']) ? 1 : 0;
		}

		return $node;
	}

	/**
	 * get the ancestors of a node
	 * @param int $nodeid
	 * @return array
	 */
	public function getNodeParents($nodeid)
	{
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$hashKey = "nodeParents_$nodeid";
		$parents = $cache->read($hashKey);
		if (empty($parents))
		{
			$parents = array();
			$ancestors = $this->fetchClosureParent($nodeid);

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

		}

		return $parents;
	}

	/** Gets the node info for a list of nodes
	 *	@param	array of node ids
	 *	@param	bool	Include joinable content
	 *
	 * 	@return	mixed	array of node records
	 **/
	public function getNodes($nodeList, $withJoinableContent = false)
	{
		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'];
		}

		if ($withJoinableContent)
		{
			$indexed = vB::getDbAssertor()->getRows(
				'vBForum:fetchNodeWithContent', array(
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD,
					'nodeid' => $cached['notfound']
				),
				false,
				'nodeid'
			);
		}
		else
		{
			$indexed = vB::getDbAssertor()->getRows(
				'vBForum:node',
				array('nodeid' => $cached['notfound']),
				false,
				'nodeid'
			);
		}

		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($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
	 */
	public 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'] = 0;
		}
		$timeNow = vB::getRequest()->getTimeNow();

		if (($datefrom <= $timeNow) AND (!intval($dateto) OR ($dateto > $timeNow)))
		{
			$data['showpublished'] = 1;
		}
		else
		{
			$data['showpublished'] = 0;
		}
		//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->contentLibs[$node['nodeid']]))
		{
			$this->contentLibs[$node['nodeid']] =
				vB_Library::instance('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		$ret = $this->contentLibs[$node['nodeid']]->update($nodeid, $data);
		vB_Api::instanceInternal('Search')->attributeChanged($nodeid);
		return $ret;
	}

	/** 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->contentLibs[$node['nodeid']]))
		{
			$this->contentLibs[$node['nodeid']] =
				vB_Library::instance('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

		$ret = $this->contentLibs[$node['nodeid']]->update($nodeid, $data);
		vB_Api::instanceInternal('Search')->attributeChanged($nodeid);
		return $ret;
	}

	/** 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('forumpermissions2', 'canpublish', $nodeid))
		{
			throw new Exception('no_publish_permissions');
		}

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

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

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

		$this->clearCacheEvents(array_unique(array($nodeid, $node['parentid'], $node['starter'])));
		$ret = $this->contentLibs[$node['nodeid']]->update($nodeid, $data);
		vB_Api::instanceInternal('Search')->attributeChanged($nodeid);
		return $ret;
	}

	/*** 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)
	{
		if (!$nodeids)
		{
			return array();
		}
		else if (!is_array($nodeids))
		{
			$nodeids = array($nodeids);
		}

		$loginfo = array();
		foreach ($nodeids as $nodeid)
		{
			$node = $this->getNode($nodeid);

			$loginfo[] = array(
				'nodeid'		=> $node['nodeid'],
				'nodetitle'		=> $node['title'],
				'nodeusername'	=> $node['authorname'],
				'nodeuserid'	=> $node['userid'],
			);
		}

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

		$this->clearCacheEvents($nodeids);

		vB_Library_Admin::logModeratorAction($loginfo, ($set ? 'node_featured_by_x' : 'node_unfeatured_by_x'));
		foreach ($nodeids as $nodeid)
		{
			vB_Api::instanceInternal('Search')->attributeChanged($nodeid);
		}
		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->contentLibs[$node['nodeid']]))
		{
			$this->contentLibs[$node['nodeid']] =
				vB_Library::instance('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));
		}

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

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

	}

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

		$nodeList = array_unique($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']))
		{
			if ($withJoinableContent)
			{
				$nodes = vB::getDbAssertor()->assertQuery(
					'vBForum:fetchNodeWithContent', array(
						vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD,
						'nodeid' => $cached['notfound']
					)
				);
			}
			else
			{
				$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 (empty($nodeList))
		{
			return 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, true);
		return $this->addContentInfo($nodeList, $options);
	}

	/**
	 * 	Gets the content info for a list of nodes
	 *
	 *	@param array $nodeList Ids of the nodes to be fetched
	 *
	 * 	@return array array of content records -- preserves the original keys
	 **/
	public function getFullContentforNodes($nodeList, $options = array())
	{
		//we can short cut a fair amount of processing if we are requesting no nodes.
		//this is an odd request but not exactly an error...
		if (empty($nodeList))
		{
			return 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, true);
		return $this->addFullContentInfo($nodeList, $options);
	}

	/**
	 * Cache a list of node votes
	 *
	 * @param array $nodeIds A list of Nodes to be cached
	 *
	 * @see vB_Api_Reputation::cacheNodeVotes()
	 */
	protected function cacheNodeVotes(array $nodeIds)
	{
		vB_Api::instanceInternal('reputation')->cacheNodeVotes($nodeIds);
	}

	/**	Gets the channel title and routeid
	 *	@param	int		The node id.
	 *	@return	mixed	Array of channel info
	 */
	public function getChannelInfoForNode($channelId)
	{
		$channelInfo = $this->getNodeBare($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 int[] Node IDs that the user has voted. Keyed by nodeid
	 * @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();
		$userinfo = 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']) AND !isset($nodeList[$key]['content']['deleteusername']))
			{
				$nodeList[$key]['content']['deleteusername'] = $userApi->fetchUserName($node['deleteuserid']);
			}

			if (isset($node['content']['userinfo']['hascustomavatar']))
			{
				$userinfo[$node['content']['userinfo']['userid']] = $node['content']['userinfo'];
			}
		}

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

		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;

				// comments/replies don't have a set for so we might want to get that from parent...
				if (!empty($nodeList[$key]['content']['setfor']))
				{
					$setfor = $nodeList[$key]['content']['setfor'];
				}
				else
				{
					$parentInfo = $this->getNode($nodeList[$key]['parentid']);
					$setfor = $parentInfo['setfor'];
				}

				$vm_userInfo = vB_User::fetchUserinfo($setfor);
				$vmAvatar = $userApi->fetchAvatar($setfor, true, $vm_userInfo);
				$vm_userInfo = array(
					'userid' => $vm_userInfo['userid'],
					'username' => $vm_userInfo['username'],
					'rank' => $vm_userInfo['rank'],
					'usertitle' => $vm_userInfo['usertitle'],
					'joindate' => $vm_userInfo['joindate'],
					'posts' => $vm_userInfo['posts'],
					'customtitle' => $vm_userInfo['customtitle'],
					'userfield' => array(),
				);
				$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();
			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'] != $this->channelTypeId))
				{
					$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']) AND !isset($node['deleteusername']))
		{
			$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']);
						$node['vm_userInfo'] = array(
							'userid' => $vm_userInfo['userid'],
							'username' => $vm_userInfo['username'],
							'rank' => $vm_userInfo['rank'],
							'usertitle' => $vm_userInfo['usertitle'],
							'joindate' => $vm_userInfo['joindate'],
							'posts' => $vm_userInfo['posts'],
							'customtitle' => $vm_userInfo['customtitle'],
							'userfield' => array(),
						);
						$vmAvatar = $userApi->fetchAvatar($node['setfor'], true, $vm_userInfo);
						if (is_array($vmAvatar))
						{
							$node['vm_userInfo'] = array_merge($node['vm_userInfo'], $vmAvatar);
						}
					}
				}
			}
			else
			{
				$node['isVisitorMessage'] = 0;
				$node['vm_userInfo'] = array();
			}
		}

		if (!empty($options['withParent']) AND !empty($node))
		{
			$parentid = 0;

			if (($node['nodeid'] != $node['starter']) AND ($node['parentid'] != $node['starter']) AND ($node['contenttypeid'] != $this->channelTypeId))
			{
				$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)
	{

		return $this->fetchNodeAttachments($parentIds);

	}


	/** This gets the attachment information for a node. Which may be empty.
	 *
	 *	@param		mixed	int or array of ints- the nodeid we are checking
	 *
	 *	@return		mixed	either false or an array of attachments with the following fields:
	 *						** attach fields **
	 *						- filedataid
	 *						- nodeid
	 *						- parentid
	 *						- visible
	 *						- counter
	 *						- posthash
	 *						- filename
	 *						- caption
	 *						- reportthreadid
	 *						- settings
	 *						- hasthumbnail
	 *
	 *						** filedata fields **
	 *						- userid
	 *						- extension
	 *						- filesize
	 *						- thumbnail_filesize
	 *						- dateline
	 *						- thumbnail_dateline
	 *
	 *						** link info **
	 *						- url
	 *						- urltitle
	 *						- meta
	 *
	 *						** photo info **
	 *						- caption
	 *						- height
	 *						- width
	 *						- style
	 *
	 * **
	 **/
	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_LARGE);
		$cacheids = array();
		foreach ($parentids AS $parentid)
		{
			$cacheids[$parentid] = "vBAtchmnts_$parentid";
		}

		$attachments = $cache->read($cacheids);

		foreach ($parentids AS $parentid)
		{
			$cacheid = $cacheids[$parentid];

			if (!$attachments OR !isset($attachments[$cacheid]) OR $attachments[$cacheid] === false)
			{
				$notfound[$parentid] = array() ;
			}
			else if (!empty($attachments[$cacheid]))
			{
				foreach($attachments[$cacheid] AS $attach)
				{
					$found[] = $attach;
				}
			}

		}

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

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

				if ($attachment['filedataid'] > 0)
				{
					$updateFiledataInfo[$attachment['filedataid']][] =& $attachment;
				}
			}

			//now fetch missing filedata info
			if(!empty($updateFiledataInfo))
			{
				$filedataInfo = vB::getDbAssertor()->assertQuery('vBForum:getFiledataWithThumb', array('filedataid' => array_keys($updateFiledataInfo)));
				if ($filedataInfo)
				{
					foreach($filedataInfo AS $filedata)
					{
						foreach($updateFiledataInfo[$filedata['filedataid']] AS &$attachment)
						{
							// these fields are required for text parsing
							$keys = array('userid', 'extension', 'filesize', 'dateline', 'resize_filesize', 'resize_dateline');
							foreach($keys AS $key)
							{
								$attachment[$key] = $filedata[$key];
							}

							// todo: is there a reason to not return the filename?
							// $attachment['filename'] = $filedata['filehash'] . '.' . $filedata['extension'];
							$attachment['counter'] = $filedata['refcount'];
							$attachment['hasthumbnail'] = ($filedata['resize_filesize'] > 0);
						}
					}
				}
			}

			//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 addFullContentInfo($nodeList, $options = array())
	{
		//Now separate by content type	$contenttypes = array();
		if (empty($nodeList))
		{
			return array();
		}

		$nodeIds = array();
		$needVote = array();
		$cacheVote = array();
		$needRead = array();
		$parentids = array();
		$attachCounts = array();
		$photoCounts = array();
		$channels = array();
		$grabAttachCounts = array();

		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
		$userids = array();
		foreach ($nodeList AS $key => $node)
		{
			if (empty($node['nodeid']))
			{
				continue;
			}

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

			if ($this->channelTypeId != $node['contenttypeid'])
			{
				// only fetch attachments for non channels.
				$grabAttachCounts[] = $node['nodeid'];
			}

			if (!isset($node['nodeVoted']))
			{
				$needVote[] = $node['nodeid'];
			}
			else
			{
				$cacheVote[$node['nodeid']] = $node['nodeVoted'];
			}

			if (!isset($node['readtime']))
			{
				$needRead[$node['nodeid']] = $node['nodeid'];
				$nodeList[$key]['readtime'] = 0;
			}
			$parentids[$node['parentid']] = $node['parentid'];
			$needRead = array_merge($parentids, $needRead);

			if (!isset($userids[$node['userid']]))
			{
				$userids[$node['userid']] = $node['userid'];
			}
		}

		vB_Library::instance('user')->preloadUserInfo($userids);

		// pre-cache parents
		$parents = $this->getNodes($parentids);
		$parentrouteids = array();
		foreach ($parents as $parent)
		{
			$parentrouteids[] = $parent['routeid'];
		}

		//pre-load parent routes
		vB5_Route::preloadRoutes($parentrouteids);
		// get votes
		$nodeVotes = (empty($needVote)) ? array() : $this->getNodeVotes($needVote);

		if (!empty($cacheVote))
		{
			$this->cacheNodeVotes($cacheVote);
		}

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

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

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

			$parentsreads = array();
			foreach ($reads AS $read)
			{
				if (!empty($nodeList[$read['nodeid']]))
				{
					$nodeList[$read['nodeid']]['readtime'] = $read['readtime'];
				}
				else
				{
					$parentsreads[$read['nodeid']] = $read['readtime'];
				}
			}

			foreach ($nodeList as $nodeid => $node)
			{
				if (empty($parentsreads[$node['parentid']])) {
					$parentsreads[$node['parentid']] = 0;
				}
				$nodeList[$nodeid]['parentreadtime'] = $parentsreads[$node['parentid']];
			}

		}

		//For each type, get the content detail.

		if (!empty($grabAttachCounts))
		{
			$attachCountQry = vB::getDbAssertor()->getRows('vBForum:getDescendantAttachCount', array('nodeid' => $grabAttachCounts));
			foreach ($attachCountQry as $count)
			{
				$attachCounts[$count['nodeid']] = $count['count'];
			}

			$photoCountQry = vB::getDbAssertor()->getRows('vBForum:getDescendantPhotoCount', array('nodeid' => $grabAttachCounts,	// this is .1 seconds
				'photoTypeid' => vB_Types::instance()->getContentTypeID('vBForum_Photo')));
			foreach ($photoCountQry as $count)
			{
				$photoCounts[$count['nodeid']] = $count['count'];
			}
		}
		// precache closure
		$this->fetchClosureParent($nodeIds);
		$optionMask = vB_Api::instanceInternal('node')->getOptions();

		foreach ($contenttypes as $contenttypeid => $nodes)
		{
			if (!empty($nodes))
			{
				$contentLib = vB_Library_Content::getContentLib($contenttypeid);

				$contentList = $contentLib->getFullContent($nodes);

				foreach ($nodes as $key => $nodeid)
				{
					if (isset($contentList[$nodeid]))
					{
						if (!empty($contentList[$nodeid]['node_no_permission']))
						{
							unset($nodeList[$nodeid]);
							continue;
						}
						if (isset($nodeList[$key]['nodeVoted']))
						{	// node came into the function with nodeVoted already set
							$contentList[$nodeid]['nodeVoted'] = $nodeList[$key]['nodeVoted'];
						}
						else
						{	// node came into this function w/o nodeVoted set so getNodeVotes retrieved it up there^
							$contentList[$nodeid]['nodeVoted'] = in_array($nodeid, $nodeVotes) ? 1 : 0;
						}
						$nodeList[$key]['content'] = $contentList[$nodeid];

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

						if ($contentList[$nodeid]['contenttypeid'] == $this->channelTypeId)
						{
							$channels[$contentList[$nodeid]['nodeid']] = $contentList[$nodeid]['nodeid'];
						}
						else if (!empty($contentList[$nodeid]['channelid']) AND !isset($channels[$contentList[$nodeid]['channelid']]))
						{
							$channels[$contentList[$nodeid]['channelid']] = $contentList[$nodeid]['channelid'];
						}
					}


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


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


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

			}
		}

		// censor textual node items
		vB_Library_Node::censorNodes($nodeList);

		$this->addOptionalContentInfo($nodeList, $options);
		//Note- it is essential that the parentids be passed along with the nodeList. This allows all the permissions to
		// be pulled in one function call, and saves a lot of processing in the usercontext object.
		$this->markSubscribed($nodeList);
		$this->markJoined($nodeList);

		return $nodeList;
	}

	/**
	 *	Cleans the node list according the to permissions set in the node record
	 *	for the current user.  For use by various APIs that return node information
	 *	to consolidate the cleaning in one place.
	 *
	 *	Currently removes the ipaddress fields when the user should not be able to
	 *	view them.
	 *
	 *	The passed node array is cleaned in place to avoid unnecesary copies of large
	 *	objects instead of returned.
	 *
	 *	@param array $nodelist
	 *	@return none
	 */
	public function removePrivateDataFromNodeList(&$nodelist)
	{
		foreach ($nodelist AS $key => $node)
		{
			$contentLib = vB_Library_Content::getContentLib($node['contenttypeid']);
			$contentLib->removePrivateDataFromNode($nodelist[$key]);
		}
	}

	/**
	 * 	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
	 * 	@deprecated Use addFullContentInfo instead
	 ***/
	public function addContentInfo($nodeList, $options = array())
	{
		return $this->addFullContentInfo($nodeList, $options);
	}

	/**
	* 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
	* @deprecated Use getNodeFullContent instead
	***/
	public function getNodeContent($nodeid, $contenttypeid = false, $options = array())
	{
		return $this->getNodeFullContent($nodeid, $contenttypeid, $options);
	}


	/** returns id of the Albums Channel
	 *
	 *	@return	integer		array including
	 ***/
	public function fetchAlbumChannel()
	{
		if ($this->albumChannel)
		{
			return $this->albumChannel;
		}
		$albumChannel = vB_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(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_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(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_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(vB_Channel::VISITORMESSAGE_CHANNEL);
		$this->VMChannel = $VMChannel['nodeid'];
		return $this->VMChannel;

	}

	public function getSGChannel()
	{
		if (!empty($this->sgChannel))
		{
			return $this->sgChannel;
		}
		// use default pagetemplate for social groups
		$sgChannel = vB_Library::instance('content_channel')->fetchChannelByGUID(vB_Channel::DEFAULT_SOCIALGROUP_PARENT);
		$this->sgChannel = $sgChannel['nodeid'];
		return $this->sgChannel;
	}

	/** returns id of the Report Channel
	 *
	 *	@return	integer		array including
	 ***/
	public function fetchReportChannel()
	{
		if ($this->ReportChannel)
		{
			return $this->ReportChannel;
		}
		$ReportChannel = vB_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(vB_Channel::REPORT_CHANNEL);
		$this->ReportChannel = $ReportChannel['nodeid'];
		return $this->ReportChannel;

	}

	/**
	 * Returns the nodeid of the root forums channel
	 *
	 * @return	integer	The nodeid for the root forums channel
	 */
	public function fetchForumChannel()
	{
		if ($this->forumChannel)
		{
			return $this->forumChannel;
		}

		$forumChannel = vB_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(vB_Channel::DEFAULT_FORUM_PARENT);
		$this->forumChannel = $forumChannel['nodeid'];

		return $this->forumChannel;
	}

	/**
	 * Returns the nodeid of the infraction channel
	 *
	 * @return	integer	The nodeid for the infraction channel
	 */
	public function fetchInfractionChannel()
	{
		if ($this->infractionChannel)
		{
			return $this->infractionChannel;
		}

		$infractionChannel = vB_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(vB_Channel::INFRACTION_CHANNEL);
		if (!empty($infractionChannel))
		{
			$this->infractionChannel = $infractionChannel['nodeid'];
		}
		return $this->infractionChannel;
	}

	/**
	 * Returns the nodeid of the CMS/Articles channel
	 *
	 * @return	integer	The nodeid for the CMS channel
	 */
	public function fetchArticleChannel()
	{
		if ($this->articleChannel)
		{
			return $this->articleChannel;
		}

		$articleChannel = vB_Api::instanceInternal('Content_Channel')->fetchChannelByGUID(vB_Channel::DEFAULT_ARTICLE_PARENT);
		if (!empty($articleChannel))
		{
			$this->articleChannel = $articleChannel['nodeid'];
		}
		return $this->articleChannel;
	}

	/**
	 * Returns a content record based on nodeid including channel and starter information
	 *
	 * @param  int  Node ID
	 * @param  int  (optional) Content type ID
	 * @param  bool (optional) Array of options
	 *
	 * @return array Content record
	 */
	public function getNodeFullContent($nodeid, $contenttypeid = false, $options = array())
	{
		$db = vB::getDbAssertor();

		if ($contenttypeid)
		{
			$contentLib = vB_Library_Content::getContentLib($contenttypeid);
		}
		else
		{
			$node = $this->getNodeBare($nodeid);
			if (empty($node) OR !empty($node['errors']))
			{
				throw new vB_Exception_Api('invalid_data');
			}

			$contentLib = vB_Library_Content::getContentLib($node['contenttypeid']);
		}
		$result = $contentLib->getFullContent($nodeid);

		$totalphotocount = isset($result[$nodeid]['photo']) ? count($result[$nodeid]['photo']) : 0;

		if (!empty($options['attach_options']['perpage']) AND $result[$nodeid]['photocount'] > $options['attach_options']['perpage'])
		{
			$page = empty($options['attach_options']['page']) ? 1 : $options['attach_options']['page'];
			$from = ($page -1) * $options['attach_options']['perpage'];
			$result[$nodeid]['photo'] = array_slice($result[$nodeid]['photo'], $from, $options['attach_options']['perpage']);
			$result[$nodeid]['pagenav'] = array(
				'startcount' => $from,
				'totalcount' => $totalphotocount,
				'currentpage' => $page,
				'totalpages' => ceil($totalphotocount / $options['attach_options']['perpage']),
				'perpage' => $options['attach_options']['perpage']
			);

		}

		$attachments = $this->fetchNodeAttachments($nodeid);
		$result[$nodeid]['attachments'] = array();
		foreach ($attachments AS $attachment)
		{
			if (!empty($attachment))
			{
				//$result[$nodeid]['attachments'][$attachment['filedataid']] = $attachment;
				// Above was commented out & replaced by below @ 5.1.1 Alpha 8. If this doesn't cause problems, let's remove
				// it in a couple versions.
				// filedataid shouldn't be the key, because we could have multiple attachments (which should have different
				// nodeids) that use the same filedataid. Instead, use nodeid as the key.
				$result[$nodeid]['attachments'][$attachment['nodeid']] = $attachment;
			}
		}
		$totalattachcount = $result[$nodeid]['attachcount'] = count($result[$nodeid]['attachments']);
		if (!empty($options['attach_options']['perpage']) AND !empty($result[$nodeid]['attachcount']) AND
			$result[$nodeid]['attachcount'] > $options['attach_options']['perpage'])
		{
			$page = empty($options['attach_options']['page']) ? 1 : $options['attach_options']['page'];
			$from = ($page -1) * $options['attach_options']['perpage'];
			$result[$nodeid]['attachments'] = array_slice($result[$nodeid]['attachments'], $from, $options['attach_options']['perpage']);

			$result[$nodeid]['attachpagenav'] = array(
				'startcount' => $from,
				'totalcount' => $totalattachcount,
				'currentpage' => $page,
				'totalpages' => ceil($totalattachcount / $options['attach_options']['perpage']),
				'perpage' => $options['attach_options']['perpage']
			);
		}
		$this->addOptionalNodeContentInfo($result[$nodeid], $options);

		if ($result[$nodeid]['contenttypeid'] == $this->channelTypeId)
		{
			$channelid = $nodeid;
		}
		else
		{
			$channelid = $result[$nodeid]['channelid'];
		}
		$perms = vB::getUserContext()->fetchPermsForChannels(array($channelid));
		$thisPerms = $perms[$channelid];
		foreach ($perms['global'] AS $key => $perm)
		{
			$thisPerms[$key] = $perm;
		}
		$this->markSubscribed($result);
		$this->markJoined($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->fetchNodeAttachments($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();
		$result = array();

		if (empty($moderateInfo['can']) AND empty($moderateInfo['canpublish']))
		{
			$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;
			$params['canModerate'] = $moderateInfo['can'];
			$params['canPublish'] = $moderateInfo['canpublish'];

			if (!empty($options['time']))
			{
				$now = vB::getRequest()->getTimeNow();
				switch ($options['time'])
				{
					case 'today':
						$params['cutofftime'] = $now - 86400; //24 hours
					break;
					case 'thisweek':
						$params['cutofftime'] = $now - 604800; //1 week
					break;
					case 'thisweek':
						$params['cutofftime'] = $now - 2592000; //30 days
					break;
				}
			}

			if (!empty($options['type']))
			{
				$params['type'] = $options['type'];
			}

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

			$pending = array();
			foreach ($pendingPosts AS $post)
			{
				$pending[intval($post['nodeid'])] = array('nodeid' => intval($post['nodeid']), 'contenttypeid' => intval($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;
	}

	/**
	 * This was a function wrapper for listPendingPosts but used for current user.
	 * Now returns different information due to post processing steps.
	 *
	 */
	public function listPendingPostsForCurrentUser($options = array())
	{
		$result = $this->listPendingPosts(vB::getCurrentSession()->get('userid'), $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'];
			}

			$nodes[$nodeid]['isVisitorMessage'] = $nodes[$nodeid]['content']['isVisitorMessage'] = !empty($node['content']['setfor']);

			$nodes[$nodeid]['userinfo'] = array(
				'avatar'	=> $userApi->fetchAvatar($node['content']['userid'], array('avatar'), $node['content']['userinfo']),
				'userid'	=> $node['content']['userid'],
				'username'	=> $node['content']['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
	 */
	public function setApproved($approveNodeIds, $approved = true)
	{
		if (empty($approveNodeIds))
		{
			return false;
		}

		$loginfo = array();
		$nodeIds = array();

		foreach ($approveNodeIds AS $idx => $id)
		{
			$nodeInfo = $this->getNode($id);
			if ($nodeInfo['deleteuserid'])
			{
				// Do not do approve/unapprove actions on deleted posts
				continue;
			}

			if (!empty($nodeInfo['errors']))
			{
				continue;
			}

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

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

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

			$loginfo[] = array(
				'nodeid'       => $nodeInfo['nodeid'],
				'nodetitle'    => $nodeInfo['title'],
				'nodeusername' => $nodeInfo['authorname'],
				'nodeuserid'   => $nodeInfo['userid']
			);
		}

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

		$errors = array();
		$assertor = vB::getDbAssertor();

		$result = $assertor->update('vBForum:node', array('approved' => $approved), array('nodeid' => $nodeIds));

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

		$method = empty($approved) ? 'unapproveNode' : 'approveNode';
		$result = $assertor->assertQuery('vBForum:' . $method, array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'nodeid' => $nodeIds));

		// Report as ham if this node was spam..
		if ($method == 'approveNode')
		{
			$vboptions = vB::getDatastore()->getValue('options');
			if ($vboptions['vb_antispam_type'] AND $vboptions['vb_antispam_key'])
			{
				$spamids = array();
				$spamcheck = $assertor->getRows('spamlog', array(
					'nodeid' => $nodeIds,
				));
				foreach ($spamcheck AS $spam)
				{
					$spamids[] = $spam['nodeid'];
				}

				if ($spamids)
				{
					$nodes = $this->getContentforNodes($spamids);
					$akismet = vB_Akismet::instance();
					foreach ($nodes AS $node)
					{
						if ($node['content']['rawtext'])
						{
							$text = vB_String::stripBbcode($node['content']['rawtext'], true);
							$akismet->markAsHam(
								array(
									'comment_type'    => 'comment',
									'comment_author'  => $node['content']['authorname'],
									'comment_content' => $text,
									'user_ip'         => $node['content']['ipaddress']
							));
						}
					}
					$assertor->delete('spamlog', array(
						'nodeid' => $spamids
					));
				}
			}
		}

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

		$nodeIds = array_unique($nodeIds);
		$searchAPI = vB_Api::instanceInternal('Search');
		foreach ($nodeIds AS $nodeid)
		{
			$node = $this->getNode($nodeid);
			$parent = $this->getNodeBare($node['parentid']);

			if ($node['showpublished'])
			{
				$nodeUpdates = $this->publishChildren($node['nodeid']);
			}
			else
			{
				$nodeUpdates = $this->unpublishChildren($node['nodeid']);
			}

			//we must update the last nodes for the subtree before handling the parents, otherwise it won't work.
			$this->updateLastForSubtree($nodeid);
			$this->updateChangedNodeParentCounts($node, $nodeUpdates);
			//$assertor->assertQuery('vBForum:updateLastData', array('parentid' => $nodeid, 'timenow' => vB::getRequest()->getTimeNow()));


			// Update the user post count (approve / unapprove)
			vB_Cache::allCacheEvent('nodeChg_' . $nodeid);
			if ($approved)
			{
				vB_Library_Content::getContentLib($node['contenttypeid'])->incrementUserPostCount($node);
			}
			else
			{
				vB_Library_Content::getContentLib($node['contenttypeid'])->decrementUserPostCount($node, 'unapprove');
			}
			$searchAPI->attributeChanged($node['nodeid']);
		}

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

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

		vB_Library_Admin::logModeratorAction($loginfo, ($approved ? 'node_approved_by_x' : 'node_unapproved_by_x'));

		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
	 *
	 * @deprecated
	 * @return	bool	Flag to indicate if approving went succesfully done (true/false).
	 */
	public function setApprovedPost($nodeid = false, $approved = false)
	{
		return $this->setApproved($nodeid, $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)
		{
			$cachedNodes['nodeChg_' . $nodeid] = '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)
				{
					$cachedNodes['nodeChg_' . $cached[$field]] = 'nodeChg_' . $cached[$field];
				}
			}
			else
			{
				$notCached[] = $nodeid;
			}
		}

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

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

		return true;
	}

	/**
	 * Mark multiple nodes read
	 *
	 * @param $nodeids Node Ids
	 *
	 * @return	array	Returns an array of nodes that were marked as read
	 */
	public function markReadMultiple($nodeids)
	{
		$nodes_marked = array();
		foreach ($nodeids as $nodeid)
		{
			$nodeid = intval($nodeid);

			if ($nodeid)
			{
				$nodes_marked = array_merge($this->markRead($nodeid), $nodes_marked);
			}
		}

		return $nodes_marked;
	}

	/**
	 * Mark multiple nodes unread
	 *
	 * @param $nodeids Node Ids
	 *
	 * @return	array	Returns an array of nodes that were marked as unread
	 */
	public function markUnreadMultiple($nodeids)
	{
		$nodes_marked = array();
		foreach ($nodeids as $nodeid)
		{
			$nodeid = intval($nodeid);

			if ($nodeid)
			{
				$nodes_marked = array_merge($this->markUnread($nodeid), $nodes_marked);
			}
		}

		return $nodes_marked;
	}

	/**
	* Marks a node as read using the appropriate method.
	*
	* @param int $nodeid The ID of node being marked. The node should be a channel or starter.
	*
	* @return	array	Returns an array of nodes that were marked as read
	*/
	public function markRead($nodeid)
	{
		$userid = vB::getCurrentSession()->get('userid');
		$threadMarking = vB::getDatastore()->getOption('threadmarking');

		if (!empty($nodeid) AND !empty($userid))
		{
			$node = $this->getNodeBare($nodeid);
			// read notifications regardless of threadmarking
			vB_Library::instance("notification")->triggerNotificationEvent('read_topic', array('nodeid' => $node['starter']));
		}
		else
		{
			return array();
		}

		if (!$threadMarking)
		{
			//!$threadMarking  means Cookie based read marking. Handled by presentation.
			return array();
		}

		//node marking only makes sense for channels and starters
		if ($node['contenttypeid'] == $this->channelTypeId)
		{
			return $this->markChannelsRead($nodeid);
		}
		else if ($node['starter'] != $node['nodeid'])
		{
			return array();
		}
		$timenow = vB::getRequest()->getTimeNow();
		$nodes_marked = array($nodeid);

		/*
		 * automatic threadmarking
		 */
		if ($threadMarking == 2)
		{
			// channel can only be marked as read if all the children are read as well

			// any threads older than the markinglimit (days) are considered "read" even without
			// a noderead record
			$cutoff = $timenow - (vB::getDatastore()->getOption('markinglimit') * 86400);
			$channelPerms = vB::getUserContext()->getAllChannelAccess();
			$parentData = vB::getDbAssertor()->assertQuery('vBForum:getParentLastRead',
				array(
					'nodeid' => $nodeid,
					'userid' => $userid,
					'canview' =>  array_merge($channelPerms['canview'], $channelPerms['canalwaysview'], $channelPerms['canmoderate']),
					'cutoff' => $cutoff
				));
			foreach ($parentData as $parent)
			{
				$all_children = explode(',', $parent['all_children']);
				$read_children = explode(',', $parent['read_children']);
				$unread_children = array_diff($all_children, $read_children);
				// we may be reading a channel that's a child of the current "parent"
				// take into account the nodes that will be read after this loop.
				$unread_children = array_diff($unread_children, $nodes_marked);

				// if parent channel's last content is older than cutoff
				// OR there is no unread child
				if (($parent['lastcontent'] < $cutoff) OR empty($unread_children))
				{
					$nodes_marked[] = intval($parent['parent']);
				}
				else
				{
					// if any parent is not read, we break out. The query result is sorted by depth, so if a parent not gonna be marked read,
					// the grandparent(s) wouldn't be mark read.
					break;
				}
			}
		}


		// regardless of the automatic threadmarking setting, we need to at least mark the thread itself as read. So we can't have the query
		// in the if-block above.
		vB::getDbAssertor()->assertQuery('nodeMarkread', array('nodeid' => $nodes_marked, 'userid' => $userid, 'readtime' => $timenow));
		return $nodes_marked;
	}

	/**
	 * Marks a node as unread using the appropriate method.
	 *
	 * @param int $nodeid The ID of node being marked
	 *
	 * @return	array	Returns an array of nodes that were marked as unread
	 */
	public function markUnread($nodeid)
	{
		$userid = vB::getCurrentSession()->get('userid');
		$threadMarking = vB::getDatastore()->getOption('threadmarking');

		if (empty($userid) OR !$threadMarking OR empty($nodeid))
		{
			//!$threadMarking  means Cookie based read marking. Handled by presentation.
			return array();
		}

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

		//node marking only makes sense for channels and starters
		if ($node['contenttypeid'] == $this->channelTypeId)
		{
			return $this->markChannelsUnread($nodeid);
		}
		else if ($node['starter'] != $node['nodeid'])
		{
			return array();
		}

		$nodes_marked = array($nodeid);

		if ($threadMarking == 2)
		{
			vB::getDbAssertor()->delete('noderead', array('nodeid' => $nodes_marked, 'userid' => $userid));
		}
		return $nodes_marked;
	}

	/**
	* 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)
	{
		$userid = vB::getCurrentSession()->get('userid');

		if (!$userid)
		{
			// Guest call
			return array();
		}

		// read notifications regardless of threadmarking
		if ($nodeid)
		{
			vB_Library::instance("notification")->triggerNotificationEvent('read_channel', array('nodeid' => $nodeid));
		}
		else
		{
			vB_Library::instance("notification")->triggerNotificationEvent('read_channel', array('nodeid' => 1));
		}

		$threadmarking = vB::getDatastore()->getOption('threadmarking');

		if (!$threadmarking)
		{
			return array();
		}
		$timenow = vB::getRequest()->getTimeNow();
		//Here's a trick. If the oldest content in a topic is older than the age limit, don't bother marking it.
		$cutoff = $timenow - (vB::getDatastore()->getOption('markinglimit') * 86400);
		$return_channels = array();

		if ($nodeid)
		{
			$node = $this->getNode($nodeid);
			if ($node['contenttypeid'] != $this->channelTypeId)
			{
				return array();
			}
		}
		else
		{
			$nodeid = 1;
		}
		$assertor = vB::getDbAssertor();
		$channelPerms = vB::getUserContext()->getAllChannelAccess();
		$canview = array_merge($channelPerms['canview'], $channelPerms['canalwaysview'], $channelPerms['canmoderate']);
		$toMarkQry = $assertor->assertQuery('getChannelsToMark', array('nodeid' => $nodeid, 'canview' => $canview,
			'cutoff' => $cutoff));

		foreach ($toMarkQry AS $nodes)
		{
			$return_channels[] = $nodes['nodeid'];
		}
		// mark the channel and all child channels read
		$assertor->assertQuery('channelsMarkRead', array(
				'nodeid' => $nodeid, 'userid' => $userid, 'readtime' => $timenow, 'canview' => $canview, 'cutoff' => $cutoff));

		if ($threadmarking == 2)
		{
			$assertor->assertQuery('startersMarkRead', array(
				'nodeid' => $nodeid, 'userid' => $userid, 'readtime' => $timenow, 'canview' => $canview, 'cutoff' => $cutoff));
		}

		return $return_channels;
	}

	/**
	 * Marks a channel as unread
	 *
	 * @param int $nodeid The node ID of channel being marked. If 0, all channels will be marked as unread
	 *
	 * @return	array	Returns an array of channel ids that were marked as unread
	 */
	public function markChannelsUnread($nodeid = 0)
	{
		$userid = vB::getCurrentSession()->get('userid');

		if (!$userid)
		{
			// Guest call
			return array();
		}

		$threadmarking = vB::getDatastore()->getOption('threadmarking');

		if (!$threadmarking)
		{
			return array();
		}
		$timenow = vB::getRequest()->getTimeNow();
		$return_channels = array();

		if ($nodeid)
		{
			$node = $this->getNode($nodeid);
			if ($node['contenttypeid'] != $this->channelTypeId)
			{
				return array();
			}
		}
		else
		{
			$nodeid = 1;
		}
		$assertor = vB::getDbAssertor();
		$channelPerms = vB::getUserContext()->getAllChannelAccess();
		$canview = array_merge($channelPerms['canview'], $channelPerms['canalwaysview'], $channelPerms['canmoderate']);
		$toMarkQry = $assertor->assertQuery('getChannelsToMark', array('nodeid' => $nodeid, 'canview' => $canview, 'cutoff' => 0));

		foreach ($toMarkQry AS $nodes)
		{
			$return_channels[] = $nodes['nodeid'];
		}

		// mark the channel unread
		vB::getDbAssertor()->delete('noderead', array('nodeid' => $return_channels, 'userid' => $userid));

		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]);

					if (empty($parentid) OR ($closure['parent'] == $parentid))
					{
						foreach ($cacheValue as $value)
						{
							$found[] = $value;
						}
					}
				}

				$cacheValue = array();
				$cacheEvents = array();
				$thisChild = $closure['child'];
			}

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

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

			if (empty($parentid) OR ($closure['parent'] == $parentid))
			{
				foreach ($cacheValue as $value)
				{
					$found[] = $value;
				}
			}

			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		$channelid		the nodeid of the channel to which access is requested.
	 *	@param		string	$requestType	the type of request. See vB_Api_Node::REQUEST_<> constants
	 *	@param		int		$recipient		the userid of the member who will get the request
	 *	@param		string	$recipientname	(Optional) the username of the member who will get the request
	 *	@param		boolean	$skipFloodCheck	(Optional) whether request private message should skip flood check or not
	 *
	 *	@return		mixed 	If it is 1 or true, then it means that the follow request was successful.
	 *							If it is integer and greater than 1, then the request is pending.
	 */
	public function requestChannel($channelid, $requestType, $recipient = 0, $recipientname = null, $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 ($recipient <= 0)
		{
			if(!empty($recipientname))
			{
				// fetch by username
				$recipient = vB::getDbAssertor()->getField('user', array('username' => $recipientname));

				if (!$recipient)
				{
					throw new vB_Exception_API('invalid_data');
				}
			}
			else
			{
				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))
			{
				$isBlog = vB_Api::instanceInternal('blog')->isBlogNode($channelid);
				$group = vB::getDbAssertor()->getRow('usergroup', array('systemgroupid' => vB_Api_UserGroup::CHANNEL_MEMBER_SYSGROUPID));
				if ($isBlog)
				{
					// clear follow cache
					vB_Api::instanceInternal('follow')->clearFollowCache(array($userid));
				}

				// return boolean true if successful
				$result = vB_User::setGroupInTopic($userid, $channelid, $group['usergroupid']);

				// for join requests, check for auto-subscribe & add subscription for user
				if ( ($result === true) AND ($node['nodeoptions'] & vB_Api_Node::OPTION_AUTOSUBSCRIBE_ON_JOIN) )
				{
					vB_Api::instanceInternal('follow')->add($channelid, vB_Api_Follow::FOLLOWTYPE_CHANNELS, $userid, true);
				}

				return $result;
			}

			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
				{
					//	@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 (!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 int  1 - Following for auto-approved subscriptions
						return $response = vB_Api::instanceInternal('follow')->add($channelid, vB_Api_Follow::FOLLOWTYPE_CONTENT);
					}

					//see if this is set to invite only
					if (($node['nodeoptions'] & vB_Api_Node::OPTION_NODE_INVITEONLY) > 0  )
					{
						throw new vB_Exception_Api('invalid_special_channel_subscribe_request');
					}

					$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');
		$userInfo = vB::getCurrentSession()->fetch_userinfo();

		$data = array('msgtype' => 'request',
			'about' => $requestType,
			'sentto' => $recipient,
			'aboutid' => $channelid,
			'sender' => $userInfo['userid']);

		// return int nodeid of created pending request
		if ($skipFloodCheck)
		{
			$result = $messageLib->addMessageNoFlood($data);
			return $result;
		}
		else
		{
			$result = $messageLib->add($data);
			return $result['nodeid'];
		}
	}

	/**
	 * Adds the joined flag if the current user is member of content's parent.
	 *
	 * @params	array	Array of the content node list.
	 *
	 */
	protected function markJoined(&$nodes)
	{
		$userid = vB::getCurrentSession()->get('userid');

		foreach ($nodes AS $key => $node)
		{
			$nodes[$key]['joined'] = $nodes[$key]['content']['joined'] = false;
		}

		$parents = array();
		foreach ($nodes AS $key => $node)
		{
			if (empty($node['parents']))
			{
				$nodes[$key]['parents'] = $this->getNodeParents($node['nodeid']);
			}
		}

		// guests can't be members
		if ($userid < 1)
		{
			return false;
		}

		$joinedInfo = vB::getUserContext()->fetchGroupInTopic();
		foreach ($nodes AS $key => $node)
		{
			if (isset($joined[$node['nodeid']]))
			{
				$nodes[$key]['joined'] = $nodes[$key]['content']['joined'] = 1;
			}
			else if (!empty($node['parents']))
			{

				foreach($node['parents'] AS $parent)
				{
						//We get this information in two ways- parent can be an array or an integer.
					if (is_array($parent))
					{
						$parentid = $parent['parent'];
					}
					else if (is_numeric($parent))
					{
						$parentid = $parent;
					}
					else
					{
						continue;
					}

					if (isset($joinedInfo[$parentid]))
					{
						$nodes[$key]['joined'] = $nodes[$key]['content']['joined'] = 1;
						break;
					}
				}
			}
		}
	}


	/***Returns the ancestry
	 *
	 * 	@param	int		nodeid
	 * 	@return	mixed	array of partial node records -- in ascending order of depth
	 */
	public function getParents($nodeid)
	{
		return vB::getDbAssertor()->getRows('vBForum:getParents', array('nodeid' => $nodeid));
	}

	private function getParentIds($nodeid)
	{
		$parents = $this->getParents($nodeid);
		$parentids = array();
		//The first record will be the node itself.
		foreach ($parents AS $parent)
		{
			if ($parent['nodeid'] == $nodeid)
			{
				continue;
			}
			$parentids[] = $parent['nodeid'];
		}

		return $parentids;
	}

	/***Returns node children
	 *
	 * 	@param	int		nodeid
	 * 	@return	mixed	array of partial node records
	 */
	public function getChildren($nodeid)
	{
		$cacheKey = "vB_Childs_$nodeid";
		$childs = vB_Cache::instance(vB_Cache::CACHE_FAST)->read($cacheKey);
		if ($childs !== false)
		{
			return $childs;
		}

		$childs = vB::getDbAssertor()->getRows('vBForum:getChildren', array('nodeid' => $nodeid));
		vB_Cache::instance(vB_Cache::CACHE_FAST)->write($cacheKey, $childs, 1440, "nodeChg_$nodeid");
		return $childs;
	}

	/**
	 * Check if the user has permission for edit the thread title also check the option editthreadtitlelimit
	 * if we pass the time and we are not moderators we can edit the thread title
	 * @param integer $nodeid
	 *
	 */
	public function canEditThreadTitle($nodeid, $node = false)
	{
		static $threadLimit = false;
		$userid = vB::getCurrentSession()->get('userid');

		if ($userid == 0)
		{
			return false;
		}

		$userContext = vB::getUserContext();

		if ($userContext->isSuperAdmin())
		{
			return true;
		}

		if ($threadLimit === false)
		{
			$threadLimit = vB::getDatastore()->getOption('editthreadtitlelimit');
		}

		//grab the options and the info of the node
		if (empty($node))
		{
			$node = $this->getNode($nodeid);
		}

		//check if user have moderator permissions or pass the time limit
		//The original creator can change for some period
		//A user with caneditothers should be able to bypass the time limit, VBV-12182
		if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid))
		{
			return true;
		}
		else if (($node['userid'] == $userid)
			AND (($threadLimit == 0 ) OR ($node['publishdate'] + ($threadLimit * 60) > vB::getRequest()->getTimeNow()))
			)
		{
			return true;
		}
		else if (
			($node['userid'] != $userid) AND
			vB::getUserContext()->getChannelPermission('forumpermissions2', 'caneditothers', $nodeid)
		)
		{
			return true;
		}

		return false;
	}

	/** Gets a list of the content types that change text type

	 	@return 	mixed	array of integers
	 */
	public function getTextChangeTypes()
	{
		static $changeTypes = false;

		if ($changeTypes)
		{
			return $changeTypes;
		}
		$hashKey = 'vb_textchangetypes';
		$changeTypes = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read($hashKey);

		if (empty($changeTypes))
		{
			$changeTypes = array();
			$types = vB_Types::instance()->getContentTypes();
			foreach ($types AS $type)
			{
				try
				{
					$contentLib = vB_Library_Content::getContentLib($type['id']);

					if (!empty($contentLib))
					{
						$textCountChange = $contentLib->getTextCountChange();

						if ($textCountChange > 0)
						{
							$changeTypes[$type['id']] = $type['id'];
						}
					}
				}
				catch (exception $e) //This is a normal occurence- just keep going.
				{}
			}
		}
		return $changeTypes;
	}

	/** Set the node options
	*
	* 	@param	mixed	options- can be an integer or an array
	*
	* 	@return	either 1 or an error message.
	**/
	public function setNodeOptions($nodeid, $options = false)
	{
		if (empty($nodeid) OR !intval($nodeid) OR ($options === false))
		{
			return;
		}

		// TODO: move getOptions to this library
		$optionsInfo = vB_Api::instanceInternal('node')->getOptions();
		if (is_numeric($options))
		{
			$newOptions = 0;
			//Still check each bitfield
			foreach ($optionsInfo as $key => $value)
			{
				if ($options & $value)
				{
					$newOptions += $value;
				}
			}
		}
		else
		{
			$current = $this->getNode($nodeid);
			$newOptions = $current['nodeoptions'];
			foreach ($optionsInfo as $key => $value)
			{
				if (isset($options[$key]))
				{
					if (intval($options[$key]))
					{
						$newOptions = $newOptions | $value;
					}
					else
					{
						$newOptions = $newOptions & ~intval($value);
					}
				}
			}
		}
		//And we set the value.
		$result = vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
				'nodeid' => $nodeid, 'nodeoptions' => $newOptions));
		$this->clearCacheEvents($nodeid);
		return $result;
	}

	/** gets the node option as an array of values
	 *
	 * 	@param	int		nodeid of the desired record
	 *
	 * 	@return	array 	associative array of bitfield name => 0 or 1
	 **/
	public function getNodeOptions($nodeid)
	{
		if (empty($nodeid) OR !intval($nodeid))
		{
			return;
		}

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

		$nodeOptionsBitfields = vB_Api::instanceInternal('node')->getOptions();
		foreach ($nodeOptionsBitfields  as $key => $value)
		{
			if ($node['nodeoptions'] & $value)
			{
				$options[$key] = 1;
			}
			else
			{
				$options[$key] = 0;
			}
		}

		return $options;
	}

	/**
	 * Gets the starter's parent (channel) node
	 * @param array/int $node the node or nodeid
	 */
	public function getChannelId($node)
	{
		if (is_numeric($node))
		{
			$node = $this->getNodeBare($node);
		}

		// this is the channel
		if ($node['starter'] == 0)
		{
			return $node['nodeid'];
		}

		// this is the starter, so the channel is the parent
		if ($node['starter'] == $node['nodeid'])
		{
			return $node['parentid'];
		}

		// this must be a reply
		return $this->getChannelId($node['starter']);
	}

	/**
	 * Undelete a set of nodes
	 * @param array $nodeids
	 * @param boolean is rebuild needed
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have been deleted
	 */
	public function undeleteNodes($nodeids, $needRebuild = false)
	{
		if (empty($nodeids))
		{
			return false;
		}

		$errors = array();
		$events = array();
		$loginfo = array();

		$counts = $updates = array();
		$nodeids = array_unique($nodeids);

		$assertor = vB::getDbAssertor();

		$result = $assertor->assertQuery('vBForum:node', array(
			'nodeid' => $nodeids,
			'deleteuserid' => 0,
			'deletereason' => '',
			'unpublishdate' => 0,
			'showpublished' => 1,
			vB_db_Query::TYPE_KEY => vB_db_Query::QUERY_UPDATE,
		));

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

		$searchAPI = vB_Api::instanceInternal('Search');

		foreach ($nodeids AS $nodeid)
		{
			$events[] = $nodeid;

			$result = $this->publishChildren($nodeid);

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

			// Clear cache for this node or user post count won't update
			vB_Cache::allCacheEvent('nodeChg_' . $nodeid);
			$node = $this->getNode($nodeid);

			// Update user post count (un(soft)delete)
			vB_Library_Content::getContentLib($node['contenttypeid'])->incrementUserPostCount($node);

			$loginfo[] = array(
				'nodeid'       => $node['nodeid'],
				'nodetitle'    => $node['title'],
				'nodeusername' => $node['authorname'],
				'nodeuserid'   => $node['userid'],
			);

			$parents = $this->fetchClosureParent($nodeid);

			foreach ($parents as $parent)
			{
				$nodeInfo = $this->getNodeBare($parent['parent']);

				if ($nodeInfo['contenttypeid'] == $this->channelTypeId)
				{
				 	$result = $this->fixNodeLast($parent['parent']);
				}
				else
				{
					$result = $assertor->assertQuery('vBForum:updateLastData', array('parentid' => $parent['parent'], 'timenow' => vB::getRequest()->getTimeNow()));
				}

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

				switch($parent['depth'])
				{
					case 0: // Actual node.
						vB_Node::fixNodeCount($parent['parent']);
					break;

					case 1: // Immediate parent.
						$parentinfo = $this->getNodeBare($parent['parent']);

						$counts = array(
							'totalcount' => $parentinfo['totalcount'],
							'totalunpubcount' => $parentinfo['totalunpubcount'],
						);

						vB_Node::fixNodeCount($parent['parent']);
						$parentinfo = $this->getNodeBare($parent['parent']);

						$counts = array(
							'totalcount' => $parentinfo['totalcount'] - $counts['totalcount'],
							'totalunpubcount' => $parentinfo['totalunpubcount'] - $counts['totalunpubcount'],
						);
					break;

					default: // Higher parents.
						$updates['totalcount'][$parent['parent']] = $counts['totalcount'];
						$updates['totalunpubcount'][$parent['parent']] = $counts['totalunpubcount'];
					break;
				}
			}

			$assertor->assertQuery('vBForum:updateNodeTotals', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD, 'updates' => $updates));
			$searchAPI->attributeChanged($nodeid);
		}

		$searchAPI->purgeCacheForCurrentUser();
		if ($needRebuild)
		{
			vB::getUserContext()->rebuildGroupAccess();
			vB_Channel::rebuildChannelTypes();
		}

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

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

		vB_Library_Admin::logModeratorAction($loginfo, 'node_restored_by_x');

		return $nodeids;
	}

	/**
	 * Returns the userids of the moderators of that node
	 *
	 * @param int $nodeid
	 * @return array
	 */
	public function getNodeModerators($nodeid)
	{
		$nodeid = intval($nodeid);
		$moderators = vB::getDbAssertor()->getRows('vBForum:getNodeModerators', array(
			vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'nodeid' => $nodeid
		));

		return $moderators;
	}

	/**
	 * Returns forum super moderators and admins
	 * @param int $nodeid
	 * @return array - the userid, username, email of the moderators of the node
	 */
	public function getForumSupermoderatorsAdmins($userids = array())
	{
		// To prevent SQL error if empty array is passed
		$userids = (empty($userids) ? array(-1) : $userids);
		$admins = vB::getDbAssertor()->getRows('vBForum:getSuperModeratorsAdmins', array(
			vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'userids' => $userids
		));

		return $admins;
	}


	/**
	 * Clone Nodes and their children deeply into a new parent Node.
	 *
	 * @param array $nodeids Source nodeIDs
	 * @param string|int $to_parent Parent node id. If to_parent is a string, it should be a route path to the node
	 * @param string $newtitle If parent is a channel, the oldest post will be promoted to a Thread with the new title.
	 * @return mixed array of origional nodeids as keys, cloned nodeids as values
	 */
	public function cloneNodes($nodeids, $to_parent, $newtitle = '')
	{
		$parentid = $this->assertNodeidStr($to_parent);

		$loginfo = array();
		$parent = $this->getNodeFullContent($parentid);
		$parent = $parent[$parentid];
		$channelTypeid = vB_Types::instance()->getContentTypeId('vBForum_Channel');

		$nodes = vB::getDbAssertor()->getRows(
			'vBForum:node',
			array (
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'nodeid' => $nodeids,
			),
			array (
				'field' => array('publishdate'),
				'direction' => array(vB_dB_Query::SORT_ASC)
			)
		);

		/*
		 *	Set up information for moderator log
		 */
		foreach ($nodes as $node)
		{
			//Only channels can be moved to categories, UI shouldn't allow this
			if (($node['contenttypeid'] != $channelTypeid) AND (!empty($parent['category'])))
			{
				throw new vB_Exception_Api('invalid_request');
			}

			$oldparent = $this->getNode($node['parentid']);

			$extra = array(
				'fromnodeid'	=> $oldparent['nodeid'],
				'fromtitle'		=> $oldparent['title'],
				'tonodeid'		=> $parent['nodeid'],
				'totitle'		=> $parent['title'],
			);

			$loginfo[] = array(
				'nodeid'		=> $node['nodeid'],
				'nodetitle'		=> $node['title'],
				'nodeusername'	=> $node['authorname'],
				'nodeuserid'	=> $node['userid'],
				'action'		=> $extra,
			);
		}

		/*
		 *	Actually clone the node.
		 */
		$retval = $this->cloneNodesInternal($nodes, $parent, $newtitle);

		vB_Library_Admin::logModeratorAction($loginfo, 'node_copied_by_x');
		return $retval;
	}

	public function cloneNodesInternal($nodes, $parent, $newtitle)
	{
		$db = vB::getDbAssertor();
		$parentid = $parent['nodeid'];

		// A var that stores the relationship 'oldnodeid' => 'newcopiednodeid'
		$oldnewnodes = array();

		reset($nodes);
		$firstNode = current($nodes);

		$newtitleset = false;
		foreach ($nodes as $node)
		{
			$children = $db->assertQuery(
				'vBForum:closure',
				array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,	'parent' => intval($node['nodeid'])),
				array('field' => array('depth'), 'direction' => array(vB_dB_Query::SORT_ASC))
			);

			foreach ($children as $k => $closure)
			{
				$child = $this->getNode($closure['child'], false, false);
				// Clone node record
				$newnodeid = $db->assertQuery('vBForum:cloneNodeRecord', array('table' => 'node', 'oldnodeid' => $child['nodeid']));
				if (!$newnodeid)
				{
					continue;
				}

				$oldnewnodes[$child['nodeid']] = $newnodeid;

				$fields = array();

				//if this is the first node, set it to the new parent
				if ($child['nodeid'] == $firstNode['nodeid'])
				{
					$fields['parentid'] = $parentid;
				}

				//other top level nodes should be moved under the first node
				else if ($closure['depth'] == 0)
				{
					$fields['parentid'] = $oldnewnodes[$firstNode['nodeid']];
				}

				//otherwise its the clone of the original node's parent
				else
				{
					$fields['parentid'] = $oldnewnodes[$child['parentid']];
				}

				if (!empty($fields))
				{
					$db->update('vBForum:node', $fields, array('nodeid' => $newnodeid));
				}

				//Now create the new closure records -- we'll call moved node logic below that
				//will recreate the closure records, but it assumes that there are valid closure records in place
				//so we need to create them.
				// Make sure that level 0 closure of this node exists.
				$db->insert('vBForum:closure', array('parent' => $newnodeid, 'child' => $newnodeid,
					'depth' => 0, 'publishdate' =>  (int) $closure['publishdate']));
				$db->assertQuery('vBForum:insertMovedNodeClosureRecords', array('nodeid' => $newnodeid, 'parentid' => $fields['parentid']));

				//this is bad.  The node class should not be doing type specific logic -- it makes
				//it harder to create new types.p
				$tables = array();
				switch ($child['contenttypeid'])
				{
					case vB_Types::instance()->getContentTypeID('vBForum_Attach'):
						$tables = array('attach');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Text'):
						$tables = array('text');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Channel'):
						$tables = array('channel');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Gallery'):
						$tables = array('text', 'gallery');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Link'):
						$tables = array('text', 'link');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Photo'):
						$tables = array('photo');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Poll'):
						$tables = array('text', 'poll');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_PrivateMessage'):
						$tables = array('text', 'link', 'privatemessage');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Report'):
						$tables = array('text', 'report');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Infraction'):
						$tables = array('text', 'infraction');
						break;

					case vB_Types::instance()->getContentTypeID('vBForum_Video'):
						$tables = array('text', 'video');
						break;
				}

				$data = array('table' => '', 'oldnodeid' => $child['nodeid'], 'newnodeid' => $newnodeid);
				foreach($tables as $table)
				{
					$data['table'] = $table;
					$db->assertQuery('vBForum:cloneNodeRecord', $data);
				}

				vB_Cache::instance()->allCacheEvent('nodeChg_' . $newnodeid);
			}

			if (!$newtitleset AND !empty($newtitle) AND intval($node['inlist']) AND !intval($node['protected']))
			{
			// Update the title of the oldest inlist node
				$db->assertQuery('vBForum:node', array(
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
					'title' => $newtitle,
					'htmltitle' => vB_String::htmlSpecialCharsUni(vB_String::stripTags($newtitle), false),
					'urlident' => vB_String::getUrlIdent($newtitle),
					vB_dB_Query::CONDITIONS_KEY => array(
						'nodeid' => $oldnewnodes[$node['nodeid']],
					)
				));
				$newtitleset = true;
			}
		}

		//We've already created the parent structure and the closure structure so in same ways this is
		//overkill.  However moveNodesInternal cleans up a bunch of stuff related to having a new
		//parent that we need to take into account.  A little redundant work is better than not
		//getting all of the details right.  It may be worth refactoring moveNodesInternal
		//to pull out just the logic needed here, but this works and its not a common operation.
		$newparent = $this->getNode($oldnewnodes[$firstNode['nodeid']], false, false);
		$this->moveNodesInternal(array($newparent['nodeid']), $parent);

		$nodeids = array();
		foreach($nodes AS $node)
		{
			if($node['nodeid'] != $firstNode['nodeid'])
			{
				$nodeids[] = $oldnewnodes[$node['nodeid']];
			}
		}

		if ($nodeids)
		{
			//need to grab the new values for this node.  We could blow out the cache and
			//call get node, but that's overkill for what we need.
			$newparent = $db->getRow('vBForum:node',
				array (
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
					'nodeid' => $newparent['nodeid']
				)
			);
			$this->moveNodesInternal($nodeids, $newparent);
		}

		$this->updateLastForSubtree($newparent['nodeid']);

		//fix the published status of the new tree based on its new location.
		//this includes updating the parents
		$this->updateSubTreePublishStatus($newparent, $parent['showpublished']);

		foreach($oldnewnodes as $newnodeid)
		{
			vB_Cache::instance()->allCacheEvent('nodeChg_' . $newnodeid);
		}

		return $oldnewnodes;
	}

	/**
	 * Moves nodes to a new parent
	 *
	 * @param	array	Node ids
	 * @param	int	New parent node id
	 * @param	bool	Make topic
	 * @param	bool	New title
	 * @param	bool	Mod log
	 * @param	array	Information to leave a thread redirect. If empty, no redirect is created.
	 *			If not empty, should contain these items:
	 *				redirect (string) - perm|expires	Permanent or expiring redirect
	 *				frame (string) - h|d|w|m|y	Hours, days, weeks, months, years (valid only for expiring redirects)
	 *				period (int) - 1-10	How many hours, days, weeks etc, for the expiring redirect
	 *
	 * @return
	 */
	public function moveNodes($nodeids, $to_parent, $makeTopic = false, $newtitle = false, $modlog = true, array $leaveRedirectData = array())
	{
		$movedNodes = array();
		$oldnodeInfo = array();
		$to_parent = $this->assertNodeidStr($to_parent);
		$userContext = vB::getUserContext();

		$currentUserid = vB::getCurrentSession()->get('userid');

		$channelAPI = vB_Api::instanceInternal('content_channel');

		$newparent = $this->getNode($to_parent);


		// If all the nodes to be moved are content nodes and are currently in the same
		// channel, then we are only merging posts/topics and need to check different permissions.
		// To detemine this, we iterate through all the nodes to be moved and get all the "starter"
		// nodeids. We then iterate through all the starters and check if they all have the same
		// parentid, which is the channel they are in.
		$isTopicMerge = false;
		$checkNodes = $this->getNodes(array_merge((array)$nodeids, (array)$to_parent));
		$skipCheck = false;
		$checkStarterIds = array();
		foreach ($checkNodes AS $checkNode)
		{
			if (empty($checkNode['starter']))
			{
				// we can skip this check because one of the nodes is a channel
				$skipCheck = true;
				break;
			}
			else
			{
				$checkStarterIds[] = $checkNode['starter'];
			}
		}

		if (!$skipCheck)
		{
			$checkStarters = $this->getNodes($checkStarterIds);
			$starterParents = array();
			foreach ($checkStarters AS $checkStarter)
			{
				$starterParents[$checkStarter['parentid']] = $checkStarter['parentid'];
			}
			if (count($starterParents) === 1)
			{
				// the parent node of all the starters is the same, so we are
				// only moving topics / posts around inside the same channel,
				// this is likely a topic merge request and we can check a separate
				// permission below
				$isTopicMerge = true;
			}
			unset($checkStarters, $checkStarter, $starterParents);
		}
		unset($checkNode, $checkStarterIds, $skipCheck);


		//If the current user has can moderator canmove on the current nodes, or if the user can create in
		//the new channel and is the owner of the moved nodes and has forum canmove, then they can move
		if (
			!$userContext->getChannelPermission('forumpermissions', 'canmove', $to_parent)
			AND
			!$userContext->getChannelPermission('moderatorpermissions', 'canmassmove', $to_parent)
			AND
			(!$isTopicMerge OR !$userContext->getChannelPermission('moderatorpermissions', 'canmanagethreads', $to_parent))
		)
		{
			throw new vB_Exception_Api('no_permission');
		}

		$nodes = vB::getDbAssertor()->getRows('vBForum:node',
			array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'nodeid' => $nodeids,
			),
			array(
				'field' => array('publishdate'),
				'direction' => array(vB_dB_Query::SORT_ASC)
			),
			'nodeid'
		);

		$needRebuild = false;
		$firstTitle = false;

		$loginfo = array();
		$parent = $this->getNodeFullContent($to_parent);
		$parent = $parent[$to_parent];
		$cacheEvents = array($to_parent);
		$oldparents = array();
		$channelTypeid = vB_Types::instance()->getContentTypeId('vBForum_Channel');
		$infractionTypeid = vB_Types::instance()->getContentTypeId('vBForum_Infraction');

		foreach ($nodes as $node)
		{
			if ($node['contenttypeid'] == $infractionTypeid)
			{
				throw new vB_Exception_Api('cannot_move_infraction_nodes');
			}

			if ($node['contenttypeid'] == $channelTypeid)
			{
				// If any of the moved nodes are channels, the target must be a channel.
				if ($newparent['contenttypeid'] != $channelTypeid)
				{
					throw new vB_Exception_Api('invalid_request');
				}
				// We should not allow the moving of channels from one root channel to another.
				if ($channelAPI->getTopLevelChannel($newparent['nodeid']) != $channelAPI->getTopLevelChannel($node['nodeid']))
				{
					throw new vB_Exception_Api('cant_change_top_level');
				}
			}

			//Only channels can be moved to categories, UI shouldn't allow this
			if ($parent['contenttypeid'] == $channelTypeid)
			{
				$newrouteid = vB_Api::instanceInternal('route')->getChannelConversationRoute($to_parent);
				if (($node['contenttypeid'] != $channelTypeid) AND (empty($newrouteid) OR !empty($parent['category'])))
				{
					// The node we want to move is not a channel and the parent cannot have conversations
					// (e.g. categories, the root blog channel, the root forum channel)
					throw new vB_Exception_Api('invalid_request');
				}
			}

			if ($node['contenttypeid'] == vB_Types::instance()->getContentTypeID('vBForum_Channel'))
			{
				$needRebuild = true;
			}

			if (
				$userContext->getChannelPermission('moderatorpermissions', 'canmassmove', $node['nodeid'])
				OR
				(($currentUserid == $node['userid']) AND $userContext->getChannelPermission('forumpermissions', 'canmove', $node['nodeid'], false, $node['parentid']))
				OR
				($isTopicMerge AND $userContext->getChannelPermission('moderatorpermissions', 'canmanagethreads', $to_parent))
			)
			{
				if (empty($movedNodes))
				{
					if (empty($node['title']) AND !empty($node['starter']))
					{
						$starter = vB_Library::instance('node')->getNodeBare($node['starter']);
						$firstTitle = $starter['title'];
					}
					else
					{
						$firstTitle = $node['title'];
					}
				}

				$movedNodes[] = $node['nodeid'];
				$oldnodeInfo[$node['nodeid']] = $node;
				$oldparents[$node['nodeid']] = $node['parentid'];
				$this->contentLibs[$node['nodeid']] = vB_Library::instance('Content_' . vB_Types::instance()->getContentTypeClass($node['contenttypeid']));

				if ($modlog)
				{
					$oldparent = $this->getNode($node['parentid']);

					$extra = array(
						'fromnodeid'	=> $oldparent['nodeid'],
						'fromtitle'		=> $oldparent['title'],
						'tonodeid'		=> $newparent['nodeid'],
						'totitle'		=> $newparent['title'],
					);

					$loginfo[] = array(
						'nodeid'		=> $node['nodeid'],
						'nodetitle'		=> $node['title'],
						'nodeusername'	=> $node['authorname'],
						'nodeuserid'	=> $node['userid'],
						'action'		=> $extra,
					);
				}

				if (!in_array($node['parentid'], $cacheEvents) )
				{
					$cacheEvents[] = $node['parentid'];
				}

				if (!in_array($node['starter'], $cacheEvents) AND intval($node['starter']))
				{
					$cacheEvents[] = $node['starter'];
				}
			}
			else
			{
				throw new vB_Exception_Api('no_permission');
			}
		}

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

		//can't move a node to its decendant, we like proper trees.
		$db = vB::getDbAssertor();
		$row = $db->getRow('vBForum:closure', array (
			vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			'parent' => $movedNodes,
			'child' => $to_parent,
			vB_Db_Query::PARAM_LIMIT => 1)
		);

		if(!empty($row))
		{
			throw new vB_Exception_Api('move_node_to_child') ;
		}

		//back out counts for the nodes about to be moved.
		//keep track of the parentids to so we can update the last content after
		//we've moved things around.
		$lastContentParents = array();
		foreach($movedNodes as $nodeid)
		{
			$node = $this->getNode($nodeid);
			$movedNodeParents = $this->getParents($nodeid);

			$parentids = array();
			foreach($movedNodeParents AS $movedNodeParent)
			{
				if ($movedNodeParent['nodeid'] != $nodeid)
				{
					$parentids[] = $movedNodeParent['nodeid'];
					$lastContentParents[] = $movedNodeParent['nodeid'];
				}
			}

			$this->updateAddRemovedNodeParentCounts($node, $node, $node['showpublished'], false, $parentids);
		}

		if (($parent['contenttypeid'] == $channelTypeid) AND $makeTopic)
		{

			if (empty($newtitle))
			{
				if (!empty($firstTitle))
				{
					$newtitle = $firstTitle;
				}
				else
				{
					throw new vB_Exception_Api('notitle');
				}
			}

			$newchildid = $movedNodes[0];
			$this->moveNodesInternal(array($newchildid), $newparent);

			// We need to promote give the new node the correct title
			vB::getDbAssertor()->assertQuery('vBForum:node', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
				'routeid' => vB_Api::instanceInternal('route')->getChannelConversationRoute($to_parent),
				'title' => $newtitle,
				'htmltitle' => vB_String::htmlSpecialCharsUni(vB_String::stripTags($newtitle), false),
				'urlident' => vB_String::getUrlIdent($newtitle),
				'description' => $newtitle,
				vB_dB_Query::CONDITIONS_KEY => array(
					'nodeid' => $movedNodes[0],
				)
			));

			if (count($movedNodes) > 1)
			{
				$grandchildren = array_slice($movedNodes, 1);
				$this->moveNodesInternal($grandchildren, $checkNodes[$newchildid]);
			}

			//moving the grandchildren under the first node may have changed it's last child info
			$db->assertQuery('vBForum:updateLastData', array('parentid' => $newchildid, 'timenow' => vB::getRequest()->getTimeNow()));
			$node = $this->getNode($newchildid);
			$this->updateSubTreePublishStatus($node, $newparent['showpublished']);
		}
		else
		{
			$this->moveNodesInternal($movedNodes, $newparent);
			foreach($movedNodes as $nodeid)
			{
				$node = $this->getNode($nodeid);
				$this->updateSubTreePublishStatus($node, $newparent['showpublished']);
			}
		}

		//dedup the array (so we don't update a node more than once) but we need to
		//do so in a particular way.  We always want to keep the *last* occurance
		//of any id in the array.  This ensures that a node is not updated before
		//any of its decendants (which could cause bad results).
		$seen = array();
		foreach($lastContentParents AS $key => $parentid)
		{
			if (isset($seen[$parentid]))
			{
				unset($lastContentParents[$seen[$parentid]]);
			}
			$seen[$parentid] = $key;
		}

		//we can't do this before we move the nodes because the parent/child relationships haven't
		//changed yet.
		foreach($lastContentParents AS $parentid)
		{
			$this->fixNodeLast($parentid);
		}

		$userid = vB::getCurrentSession()->get('userid');

		// afterMove requires some ancestors info which we just changed above, let's clear cache before updating
		$searchAPI = vB_Api::instanceInternal('search');
		foreach ($movedNodes as $nodeid)
		{
			vB_Cache::instance()->allCacheEvent('nodeChg_' . $nodeid);

			//some search information may have changed, let's check
			$searchAPI->attributeChanged($nodeid);
		}

		// Leave a thread redirect if required
		// Note: UI only allows leaving a redirect when moving a thread which is one node
		if (!empty($leaveRedirectData) AND count($movedNodes) == 1 AND count($nodes) == 1)
		{
			$node = reset($nodes);

			$redirectData = array(
				'title' => $node['title'],
				'urlident' => $node['urlident'],
				'parentid' => $node['parentid'],
				'tonodeid' => $node['nodeid'],
				'userid' => $node['userid'],
				'publishdate' => $node['publishdate'],
				'created' => $node['created'],
			);

			// handle expiring redirects
			if (isset($leaveRedirectData['redirect']) AND $leaveRedirectData['redirect'] == 'expires')
			{
				$period = (int) isset($leaveRedirectData['period']) ? $leaveRedirectData['period'] : 1;
				$frame = (string) isset($leaveRedirectData['frame']) ? $leaveRedirectData['frame'] : 'm';

				$period = max(min($period, 10), 1);
				$frame = in_array($frame, array('h', 'd', 'w', 'm', 'y'), true) ? $frame : 'm';

				$frames = array(
					'h' => 3600,
					'd' => 86400,
					'w' => 86400 * 7,
					'm' => 86400 * 30,
					'y' => 86400 * 365,
				);

				$redirectData['unpublishdate'] = vB::getRequest()->getTimeNow() + ($period * $frames[$frame]);
			}

			// skip any text spam checks, because a redirect has no text to check.
			vB_Library::instance('content_redirect')->add($redirectData, array('skipSpamCheck' => true));
		}

		vB_Api::instance('Search')->purgeCacheForCurrentUser();
		vB_Library_Admin::logModeratorAction($loginfo, 'node_moved_by_x');

		$cacheEvents = array_unique(array_merge($movedNodes, $cacheEvents, array($to_parent)));
		$this->clearChildCache($cacheEvents);
		$this->clearCacheEvents($cacheEvents);

		if ($needRebuild)
		{
			vB::getUserContext()->rebuildGroupAccess();
			vB_Channel::rebuildChannelTypes();
		}

		return $movedNodes;
	}

	/*
	 *	This assumes that the subtree values have been backed out of any counts
	 *	and, at this point, only need to be added to the new location.  The move
	 *	function specifically removes counts prior to moving nodes while clone
	 *	is adding new nodes and doesn't need to worry about it.
	 */
	protected function updateSubTreePublishStatus($node, $isParentPublished)
	{
		//if the parent is not published then we *always* want to unpublish nodes
		$publish = ($isParentPublished AND $this->isPublished($node));

		if ($publish)
		{
			$nodeUpdates = $this->publishChildren($node['nodeid']);
		}
		else
		{
			$nodeUpdates = $this->unpublishChildren($node['nodeid']);
		}

		$this->updateAddRemovedNodeParentCounts($node, $nodeUpdates, $publish, true);
	}

	protected function updateChangedNodeParentCounts($node, $counts)
	{
		$textChangeTypes = $this->getTextChangeTypes();

		$textChange = 0;
		$textUnPubChange = 0;

		//if the node isn't a node type that we track for counts then we
		//shouldn't be worried about any change to its "count" status.
		if (isset($textChangeTypes[$node['contenttypeid']]))
		{
			//a node counts as published if its published and approved.  Otherwise it counts as upublished.
			//this is only for the count rollups, the actual published and approved statuses have different meanings.
			$origIsPub =  ($node['showpublished'] AND $node['showapproved']);
			$newIsPub = ($counts['showpublished'] AND $counts['showapproved']);

			//if the original is published but the new one isn't then we need to add one to the
			//unpub and subtract one from the pubcount for the parent.
			if ($origIsPub AND !$newIsPub)
			{
				$textChange = -1;
				$textUnPubChange = 1;
			}

			//if the situation is reversed, then do the reverse
			else if (!$origIsPub AND $newIsPub)
			{
				$textChange = 1;
				$textUnPubChange = -1;
			}

			//if they're both published or unpublished then we didn't change anything.
		}

		$totalChange = ($counts['totalcount'] - $node['totalcount']) + $textChange;
		$totalUnPubChange = ($counts['totalunpubcount'] - $node['totalunpubcount']) + $textUnPubChange;

		$this->updateParentCounts(
			$node['nodeid'],
			$textChange,
			$textUnPubChange,
			$totalChange,
			$totalUnPubChange,
			$counts['showpublished'],
			true
		);
	}

	/**
	 *	Add or remove node counts for a given node its parents
	 *
	 *	Note this should be called before removing a node entire from the tree (or moving it to a new location) and after adding it to the tree
	 *	(or moving it to a new location).  It corrects the counts of parents based on the change made, but it affects the parents of the node
	 *	at the time the function is called -- we always want to remove the count of the nodes old parents (prior to a move or delete) and add
	 *	to the new parents (after a more or an add).
	 *
	 *	@param array $node -- the node array
	 *	@param arrray $counts -- the "count" array ('showpublished', 'showapproved', 'textcount', 'textunpubcount', 'totalcount', 'totalunpubcount') either from
	 *		the node record or the publishChildren function (it is permissable to pass the node array here if the counts are accurate, but the function
	 *		guarentees it will only access the listed fields so updated information form publishChildren can be passed instead).
	 *	@param bool $publish -- controls if the parent lastcontent is also updated.
	 *	@param bool $add -- true to add the counts, false to remove
	 *	@param array $parendids -- array of parent ids for the node. If empty we will look it up from the node record.
	 */
	public function updateAddRemovedNodeParentCounts($node, $counts, $publish, $add, $parentids = array())
	{
		$textChangeTypes = $this->getTextChangeTypes();
		$mult = ($add ? 1 : -1);

		$textChange = 0;
		$textUnPubChange = 0;
		if (isset($textChangeTypes[$node['contenttypeid']]))
		{
			//if this node is published and approved then add to the parent text
			//otherwise add to text unpublished.
			if ($publish AND $counts['showapproved'])
			{
				$textChange = 1;
			}
			else
			{
				$textUnPubChange = 1;
			}
		}

		$totalChange = ($counts['totalcount'] + $textChange);
		$totalUnPubChange = ($counts['totalunpubcount'] + $textUnPubChange);

		if (!$parentids)
		{
			$parentids = $this->getParentIds($node['nodeid']);
		}

		$this->updateParentCountsList(
			$parentids,
			$mult * $textChange,
			$mult * $textUnPubChange,
			$mult * $totalChange,
			$mult * $totalUnPubChange,
			$publish
		);
	}


	/**
	 * 	Handles the actual move of nodes to the new parent.
	 *
	 * 	Makes all nodes ids direct children of new parent.  Also cleans up some, but not all,
	 * 	of the various denormalized data surrounding moving a node.
	 *
	 * 	This was refactored from the "moveNodes" method query and should continue to be refactored
	 * 	so it only handles updating the parent field and the closure table records directly involved
	 * 	in moving a node.  The public function shoudl handle clearning up the denormalized data.
	 *
	 *	@param array $nodeids nodes to move
	 * 	@param array $newparent the standard node array for the parent node
	 **/
	protected function moveNodesInternal($nodeids, $newparent)
	{
		$db = vB::getDbAssertor();
		$to_parent = $newparent['nodeid'];

		//First delete the closure records from this to the top;
		$db->assertQuery('vBForum:deleteMovedNodeClosureRecords', array('nodeids' => $nodeids));
		$db->delete('vBForum:closure', array(
			'child' => $nodeids,
		 	array('field' => 'depth', 'value' => 0, 'operator' =>  vB_dB_Query::OPERATOR_GT),
		));

		//Now set the parentid for the node being moved.
		$db->update('vBForum:node', array('parentid' => $to_parent), array('nodeid' => $nodeids));

		foreach ($nodeids as $nodeid)
		{
			//Now create the new closure records- the moved node itself
			$db->assertQuery('vBForum:insertMovedNodeClosureRecords', array('nodeid' => $nodeid, 'parentid' => $to_parent));

			//Next the children of the moved node;
			$db->assertQuery('vBForum:insertMovedNodeChildrenClosureRecords', array('nodeid' => $nodeid));
		}

		//We need to set starter and routeids.
		$channelTypeid = vB_Types::instance()->getContentTypeId('vBForum_Channel');

		if ($newparent['contenttypeid'] == $channelTypeid)
		{
			//this is a channel
			//each node is a starter, and their children are responses.
			// We shouldn't update the starter node, so we update them separately

			//Do non-channel nodes
			//Due to the changes in VBV-4806, this query is no longer needed, except that the next query isn't run if newrouteid is blank
			$db->assertQuery('vBForum:updateMovedNodeStarter', array('nodeids' => $nodeids, 'channelTypeid' => $channelTypeid));

			$newrouteid = vB_Api::instanceInternal('route')->getChannelConversationRoute($to_parent);

			// some channels don't have a route, like the Main Forum -- all channels should have a route, otherwise we have serious
			// problems when we move nodes to them
			if (!empty($newrouteid))
			{
				// We also need to update starter's routeid. See VBV-4806.
				// Note that we don't update the child if the route is null.  That's really not the correct behavior.
				$db->assertQuery('vBForum:updateMovedNodeChildrenStarter',
					array('nodeids' => $nodeids, 'channelTypeid' => $channelTypeid, 'routeid' => $newrouteid));
			}
			//Note that we don't need to change anything about children of channels. They are already starters.
		}
		else
		{
			//this is not a channel, so it has a starter. Each node should inherit this starter.
			$db->assertQuery('vBForum:updateMovedNodeChildrenStarterNonChannel',
				array('nodeids' => $nodeids, 'starter' => $newparent['starter'], 'routeid' => $newparent['routeid']));
		}

		$timenow =  vB::getRequest()->getTimeNow();

		// update show fields
		$db->assertQuery('vBForum:updateMovedNodeShowFields',
			array('nodeids' => $nodeids, 'parentid' => $to_parent, 'timenow' => $timenow));

		//showopen
		$db->assertQuery('vBForum:updateMovedNodeShowOpen', array('nodeids' => $nodeids));

		//showapproved
		$db->assertQuery('vBForum:updateMovedNodeShowApproved', array('nodeids' => $nodeids));
	}



	/**
	 * Checks for any content that needs to be published or unpublished.
	 *
	 ***/
	public function timedCountUpdates($maxrows = 25)
	{
		$timeNow = vB::getRequest()->getTimeNow();
		//First get a list of expired items.
		$needUpdate = vB::getDbAssertor()->assertQuery('vBForum:getNeedUpdate',
			array('maxrows' => $maxrows, 'timenow' => $timeNow));

		if (!$needUpdate->valid())
		{
			return array('success' => true);
		}

		$textChangeTypes = $this->getTextChangeTypes();
		$processed = array();
		$assertor = vB::getDbAssertor();
		$pmtype = vB_Types::instance()->getContentTypeID('vBForum_PrivateMessage');
		//Only if a channel changes we need to rebuild the group access

		$needRebuild = false;
		foreach ($needUpdate as $node)
		{
			$processed[] = $node['nodeid'];
			//never do private messages.
			if ($node['contenttypeid'] == $pmtype)
			{
				continue;
			}
			//Check to see if we actually need an update.
			if ($node['parentpublished'] > 0)
			{
				$newpublished = (($node['publishdate'] <= $timeNow) AND (empty($node['unpublishdate']) OR ($node['unpublishdate'] > $timeNow)));
			}
			else
			{
				$newpublished = FALSE;
			}
			$parentTextChange = false;

			if ($newpublished != (bool)$node['showpublished'])
			{
				if ($node['contenttypeid'] == $this->channelTypeId)
				{
					$needRebuild = true;
				}
				//First set all the children to the correct value.
				//then update the counts.
				if ($newpublished)
				{
					$nodeUpdates = $this->publishChildren($node['nodeid']);

					if (!empty($nodeUpdates) AND isset($nodeUpdates['totalcount']))
					{
						if (isset($textChangeTypes[$node['contenttypeid']]))
						{
							$textChange = 1;
						}
						else
						{
							$textChange = 0;
						}
						$textUnPubChange = -1 * $textChange;
						$totalPubChange = $nodeUpdates['totalcount'] - $node['totalcount'] + $textChange;
						$totalUnPubChange = -1 * $totalPubChange;
						$parentTextChange = true; //update counts for the parent
					}
				}
				else // from published to unpublished
				{
					$nodeUpdates = $this->unpublishChildren($node['nodeid']);

					if (!empty($nodeUpdates) AND isset($nodeUpdates['totalunpubcount']))
					{
						if (isset($textChangeTypes[$node['contenttypeid']]))
						{
							$textUnPubChange = 1;
						}
						else
						{
							$textUnPubChange = 0;
						}
						$textChange = -1 * $textUnPubChange;
						$totalUnPubChange = $nodeUpdates['totalunpubcount'] - $node['totalunpubcount'] + $textUnPubChange;
						$totalPubChange = -1 * $totalUnPubChange;
						$parentTextChange = true; //update counts for the parent
					}
				}

				// clear cache for node & children. Events for parents are taken care of in updateParentCounts()
				$this->clearCacheEvents($node['nodeid']);
				$this->clearChildCache($node['nodeid']);

				$this->updateLastForSubtree($node['nodeid']);
				//update counts for the parent
				if (!empty($parentTextChange))
				{
						$this->updateParentCounts($node['nodeid'], $textChange, $textUnPubChange, $totalPubChange, $totalUnPubChange, $newpublished);
				}
			}
		}

		//Now below we're going to reset the nextupdate date. So let's clear it for the nodes we're going to process now.
		$assertor->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
			'nodeid' => $processed, 'nextupdate' => 0));

		if ($needRebuild)
		{
			vB::getUserContext()->rebuildGroupAccess();
		}
		return array('success' => true);
	}

	/**
	 *	Updates the showpublished & count fields of $parentid & its descendents for publishing
	 *
	 *	Caller MUST ensure that $parentid is actually published (i.e. a valid publishdate is set
	 *	and a positive unpublishdate is removed or in the future).  This claimed but not entirely
	 *	true of the previous version of the function (which would set the parent node showpublished
	 *	to true regardless of the dates which meant that it would work if the dates were corrected
	 *	after the call to publish children).  This version of the function requires that the
	 *	dates are set correctly before it is called.
	 *
	 *	Caller MUST also ensure that approved and showapproved flags are correctly set in the entire substree headed by
	 *	$parentid, otherwise the counts may end up being off.
	 *
	 * 	@param	int		$parentid	nodeid of node being published
	 * 	@return	mixed	An array that contains the updated
	 *								count fields of $parentid, or an array with key 'errors' if the
	 *								assertor hit an error while updating
	 *
	 */
	public function publishChildren($parentid)
	{
		$db = vB::getDbAssertor();
		$maxDepth = $db->getRow('vBForum:selectMaxDepth', array('rootnodeid' => $parentid));
		$maxDepth = $maxDepth['maxDepth'];

		$timeNow = vB::getRequest()->getTimeNow();
		$excluded = vB_Node::getExcludedTypes();

		//from the top down, update the showpublished fields
		for ($depth = 0; $depth <= $maxDepth; $depth++)
		{
			$db->assertQuery('vBForum:updatePublishedForDepth',
				array('rootnodeid' => $parentid, 'depth' => $depth, 'timenow' => $timeNow));
		}

		//from the bottom up, recompute totals for nodes with children -- nodes without children
		//should have 0's for the count fields and should already be correct.  Note that we skip the
		//last depth because we no longer need it
		for ($depth = ($maxDepth-1); $depth >= 0; $depth--)
		{
			$db->assertQuery('vBForum:updateCountsForDepth',
				array('rootnodeid' => $parentid, 'depth' => $depth, 'timenow' => $timeNow, 'excluded' => $excluded ));
		}

		//grab the new parent node values to make this work like the old function
		$node = $db->getRow('vBForum:node', array('nodeid' => $parentid,
			vB_dB_Query::COLUMNS_KEY => array('showpublished', 'showapproved', 'textcount', 'textunpubcount', 'totalcount', 'totalunpubcount')));
		return $node;
	}

	/**
	 *	Updates the showpublished & count fields of $parentid & its descendents for unpublishing
	 *	Caller MUST ensure that $parentid is actually unpublished (i.e. a valid unpublishdate is set)
	 *
	 * 	@param	int		$parentid	nodeid of node being unpublished
	 * 	@return	mixed				Can be false (for no updates), an array that contains the updated
	 *								count fields of $parentid, or an array with key 'errors' if the
	 *								assertor hit an error while updating
	 */
	public function unpublishChildren($parentid)
	{
		/*
		 * The new publishChildren actually sets the values properly based on the dates regardless either way.
		 * It's possible that we could use the fact that unpublish is simpler to shave a little time off
		 * setting the showpublish flags, but right now using battle hardened code is more critical.
		 */
		return $this->publishChildren($parentid);
	}



	protected function updateLastForSubtree($rootid)
	{
		// these ones don't affect count fields or lastcontent.
		$excluded = vB_Node::getExcludedTypes();
		$db = vB::getDbAssertor();

		$db->assertQuery('vbForum:updateLastContentDateForTree', array('rootid' => $rootid, 'excluded' => $excluded));
		$db->assertQuery('vbForum:updateLastContentNodeForTree', array('rootid' => $rootid, 'excluded' => $excluded));
		$db->assertQuery('vbForum:updateLastContentBlankNodeForTree', array('rootid' => $rootid));
	}


	/**
	 *	Determines if a node record is published or not.  Note that this only checks if the record is published, not if
	 *	it should be shown as published.
	 */
	public function isPublished($node)
	{
		/*
		 *	The logic in the this function is duplicated in at least one DB query.  If we change it here we
		 *	*must* hunt down the other locations and make them consistant.  Otherwise we will upset customers
		 *	and drive ourselves mad.
		 */
		$timeNow = vB::getRequest()->getTimeNow();
		$ispublished = (										// publish this node if
			($node['publishdate'] > 0) 							// it has been published ...
				AND ($node['publishdate'] <= $timeNow) 				// ... and isn't waiting future publishing
				AND (empty($node['unpublishdate']) OR ($node['unpublishdate'] > $timeNow)) // and node wasn't soft-deleted or unpublished
		);	// don't forget the parentheses! "=" assignment op has higher precedence than "AND" logical op

		return $ispublished;
	}

	/**
	 * Updates the parent counts and data when saving a node.
	 *
	 *	@param int $nodeid -- ID of the node who's parents should be updated
	 *	@param int $textChange -- change in the "text" count.  This is the badly named field for published direct children.
	 *	@param int $textUnPubChange -- Change in the direct unpublished children
	 *	@param int $totalPubChange -- Change in the count of the published decendants
	 *	@param int $totalUnPubChange -- Change in the count of the unpublished decendants
	 *	@param bool $published
	 *	@param bool $updatelastcontent
	 ***/
	public function updateParentCounts
	(
		$nodeid,
		$textChange,
		$textUnPubChange,
		$totalPubChange,
		$totalUnPubChange,
		$published,
		$updatelastcontent = true
	)
	{
		$parentids = $this->getParentIds($nodeid);
		return $this->updateParentCountsList(
			$parentids,
			$textChange,
			$textUnPubChange,
			$totalPubChange,
			$totalUnPubChange,
			$published,
			$updatelastcontent
		);
	}

	/**
	 * Updates the parent counts and data when saving a node.
	 *
	 *	@param array $parentids -- $ids of the node to updates parents.  Should not include the node itself
	 *	@param int $textChange -- change in the "text" count.  This is the badly named field for published direct children.
	 *	@param int $textUnPubChange -- Change in the direct unpublished children
	 *	@param int $totalPubChange -- Change in the count of the published decendants
	 *	@param int $totalUnPubChange -- Change in the count of the unpublished decendants
	 *	@param bool $published
	 *	@param bool $updatelastcontent
	 ***/
	private function updateParentCountsList
	(
		$parentids,
		$textChange,
		$textUnPubChange,
		$totalPubChange,
		$totalUnPubChange,
		$published,
		$updatelastcontent = true
	)
	{
		$parentids = array_unique($parentids);

		if (!empty($parentids))
		{
			$assertor = vB::getDbAssertor();
			$assertor->assertQuery('vBForum:UpdateParentTextCount',
			array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
			'nodeid' => $parentids[0],
			'textChange' => $textChange, 'textUnpubChange' => $textUnPubChange));

			$assertor->assertQuery('vBForum:UpdateAncestorCount',
			array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
			'nodeid' => $parentids,
			'totalChange' => $totalPubChange, 'totalUnpubChange' => $totalUnPubChange));

			$this->clearCacheEvents($parentids);
			if ($updatelastcontent)
			{
				$searchAPI = vB_Api::instanceInternal('search');
				foreach ($parentids AS $parentid)
				{
				 	$this->fixNodeLast($parentid);
					$searchAPI->attributeChanged($parentid);
				}
			}
		}
	}

	public function fixNodeLast($nodeid)
	{
		$db = vB::getDbAssertor();
		$result = $db->assertQuery('vBForum:node',
			array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				vB_dB_Query::CONDITIONS_KEY => array(
					'parentid' => $nodeid,
					'showpublished' => 1,
					'showapproved' => 1
				),
				vB_dB_Query::PARAM_LIMIT => 1,
				vB_dB_Query::COLUMNS_KEY => array('lastcontent', 'lastcontentid', 'lastcontentauthor', 'lastauthorid')
			),
			array(
				'field' => array('lastcontent', 'lastcontentid'),
				'direction' => array(vB_dB_Query::SORT_DESC, vB_dB_Query::SORT_DESC)
			)
		);

		if ($result AND $result->valid())
		{
			$row = $result->current();
			return $db->update('vBForum:node', $row, array('nodeid' => $nodeid));
		}
		else
		{
			return $db->assertQuery('vBForum:updateLastContentSelf', array('nodeid' => $nodeid, 'excluded' => vB_Node::getExcludedTypes()));
		}
	}

	/**
	 * Merges "posted" info into the topics array, used by the display_Topics template.
	 * This adds the "content.posted" element to the node record. The value is the number of
	 * times the passed userid has posted in the topic (replies and comments); zero if none.
	 *
	 * @param  array  Nodes
	 * @param  int    User ID
	 *
	 * @return array  Same array of nodes, with the "posted" element added to the "content" sub-array
	 */
	public function mergePostedStatusForTopics($nodes, $userid)
	{
		$userid = (int) $userid;

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

		$posts = vB::getDbAssertor()->getRows('vBForum:getUserPostsInTopic', array(
			'nodeids' => $nodeids,
			'userid' => $userid,
		));

		$info = array();
		foreach ($posts AS $post)
		{
			$parent = $post['parent'];

			if (!isset($info[$parent]))
			{
				$info[$parent] = array(
					'count' => 0,
					'lastpost' => 0,
				);
			}

			++$info[$parent]['count'];
			$info[$parent]['lastpost'] = $post['publishdate'];
		}

		foreach ($nodes AS $k => $node)
		{
			$nodeid = $node['nodeid'];

			if (isset($info[$nodeid]))
			{
				$nodes[$k]['content']['dot_postcount'] = $info[$nodeid]['count'];
				$nodes[$k]['content']['dot_lastpostdate'] = $info[$nodeid]['lastpost'];
			}
			else
			{
				$nodes[$k]['content']['dot_postcount'] = 0;
				$nodes[$k]['content']['dot_lastpostdate'] = 0;
			}
		}

		return $nodes;
	}
}

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 05:43, Thu May 26th 2016
|| # CVS: $RCSfile$ - $Revision: 84856 $
|| #######################################################################
\*=========================================================================*/
