<?php
if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
   || #################################################################### ||
   || # vBulletin 5.1.1 - Licence Number VBCUP51ZOY
   || # ---------------------------------------------------------------- # ||
   || # Copyright 2000-2014 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_Api_Node
 *
 * @package vBApi
 * @access public
 */
class vB_Api_Node extends vB_Api
{

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

	/**
	 * @var 	array 	 Collection of nodeids with content library related.
	 */
	protected $contentLibraries = array();

	/**
	 *
	 * @var vB_Library_Node
	 */
	protected $library;

	const FILTER_SOURCEALL = 'source_all';
	const FILTER_SOURCEUSER = 'source_user';
	const FILTER_SOURCEVM = 'source_vm';

	const FILTER_SORTMOSTRECENT = 'sort_recent';
	const FILTER_SORTPOPULAR = 'sort_popular';
	const FILTER_SORTFEATURED = 'sort_featured';
	const FILTER_SORTOLDEST = 'sort_oldest';

	const FILTER_SHOWALL = 'show_all';
	const FILTER_TIME = 'date';
	const FILTER_SOURCE = 'filter_source';
	const FILTER_SORT = 'sort';
	const FILTER_ORDER = 'order';
	const FILTER_SHOW = 'filter_show';
	const FILTER_FOLLOW = 'filter_follow';
	const FILTER_DEPTH= 'filter_depth';

	// When adding new channel requests, be sure to add it to the $channelRequests array in vB_Library_Content_Privatemessage

	// requests for blogs
	const REQUEST_TAKE_OWNER = 'owner_to';// ask the recipient to assume ownership;
	const REQUEST_TAKE_MODERATOR = 'moderator_to';// ask the recipient to assume moderation;
	const REQUEST_TAKE_MEMBER = 'member_to';// ask the recipient to become a member;
	// @TODO for inviting subscriber functionality
	const REQUEST_TAKE_SUBSCRIBER = 'subscriber_to';// ask the recipient to become a subscriber;
	const REQUEST_GRANT_OWNER = 'owner_from';// ask the recipient to grant ownership;
	const REQUEST_GRANT_MODERATOR = 'moderator';// ask the recipient to grant moderation;
	const REQUEST_GRANT_MEMBER = 'member';// ask the recipient to grant membership;
	const REQUEST_GRANT_SUBSCRIBER = 'subscriber';// ask the recipient to grant subscribription, used for forum-channels;

	// requests for social groups
	const REQUEST_SG_TAKE_OWNER = 'sg_owner_to';// ask the recipient to assume ownership;
	const REQUEST_SG_TAKE_MODERATOR = 'sg_moderator_to';// ask the recipient to assume moderation;
	const REQUEST_SG_TAKE_MEMBER = 'sg_member_to';// ask the recipient to become a member;
	// @TODO for inviting subscriber functionality
	const REQUEST_SG_TAKE_SUBSCRIBER = 'sg_subscriber_to';// ask the recipient to become a subscriber;
	const REQUEST_SG_GRANT_OWNER = 'sg_owner_from';// ask the recipient to grant ownership;
	const REQUEST_SG_GRANT_MODERATOR = 'sg_moderator';// ask the recipient to grant moderation;
	const REQUEST_SG_GRANT_MEMBER = 'sg_member';// ask the recipient to grant membership;
	const REQUEST_SG_GRANT_SUBSCRIBER = 'sg_subscriber';// ask the recipient to grant subscription;

	const OPTION_ALLOW_POST = 2;
	const OPTION_MODERATE_COMMENTS = 4;
	const OPTION_AUTOAPPROVE_MEMBERSHIP = 8;
	const OPTION_NODE_INVITEONLY = 16;
	const OPTION_NODE_PARSELINKS = 32;
	const OPTION_NODE_DISABLE_SMILIES = 64;
	const OPTION_AUTOAPPROVE_SUBSCRIPTION = 128;
	const OPTION_MODERATE_TOPICS = 256;
	const OPTION_AUTOSUBSCRIBE_ON_JOIN = 512;		// mainly used for blogs, but binds membership & subscription together (membership approval/removal also adds/removes subscription)
	const OPTION_NODE_DISABLE_BBCODE = 1024;		// disable bbcode parsing. Used for static pages (CMS)
	const OPTION_NODE_HIDE_TITLE = 2048;			// CMS
	const OPTION_NODE_HIDE_AUTHOR = 4096;			// CMS
	const OPTION_NODE_HIDE_PUBLISHDATE = 8192;		// CMS
	const OPTION_NODE_DISPLAY_FULL_IN_CATEGORY = 16384;	// CMS
	const OPTION_NODE_DISPLAY_PAGEVIEWS = 32768;		// CMS
	const OPTION_NODE_HIDE_COMMENT_COUNT = 65536;		// CMS

	const DATE_RANGE_DAILY = 'daily';
	const DATE_RANGE_MONTHLY = 'monthly';

	protected $options = array(
		'allow_post'             => self::OPTION_ALLOW_POST,
		'moderate_comments'      => self::OPTION_MODERATE_COMMENTS,
		'approve_membership'     => self::OPTION_AUTOAPPROVE_MEMBERSHIP,
		'invite_only'            => self::OPTION_NODE_INVITEONLY,
		'autoparselinks'         => self::OPTION_NODE_PARSELINKS,
		'disablesmilies'         => self::OPTION_NODE_DISABLE_SMILIES,
		'approve_subscription'   => self::OPTION_AUTOAPPROVE_SUBSCRIPTION,
		'moderate_topics'        => self::OPTION_MODERATE_TOPICS,
		'auto_subscribe_on_join' => self::OPTION_AUTOSUBSCRIBE_ON_JOIN,
		'disable_bbcode'         => self::OPTION_NODE_DISABLE_BBCODE,
		'hide_title'             => self::OPTION_NODE_HIDE_TITLE,
		'hide_author'            => self::OPTION_NODE_HIDE_AUTHOR,
		'hide_publishdate'       => self::OPTION_NODE_HIDE_PUBLISHDATE,
		'display_fullincategory' => self::OPTION_NODE_DISPLAY_FULL_IN_CATEGORY,
		'display_pageviews'      => self::OPTION_NODE_DISPLAY_PAGEVIEWS,
		'hide_comment_count'     => self::OPTION_NODE_HIDE_COMMENT_COUNT,
	);

	//This should be kept consistent with content library $allCanview. Anything there should be here,
	// although not necessarily the reverse.
	protected $previewFields = array('nodeid' => 'nodeid', 'title' => 'title', 'publishdate' => 'publishdate',
		'showpublished' => 'showpublished', 'showapproved' => 'showapproved', 'routeid' => 'routeid',
		'contenttypeid' => 'contenttypeid', 'publishdate' => 'publishdate', 'unpublishdate' => 'unpublishdate',
		'description' => 'description',	'htmltitle' => 'htmltitle', 'parentid' => 'parentid',
		'urlident' => 'urlident', 'displayorder' => 'displayorder', 'created' => 'created',
		'taglist' => 'taglist', 'showopen' => 'showopen', 'nodeoptions' => 'nodeoptions',
		'public_preview' => 'public_preview', 'channelid' => 'channelid', 'channelroute' => 'channelroute',
		'channeltitle' => 'channeltitle', 'starterroute' => 'starterroute', 'startertitle' => 'startertitle',
		'views' => 'views',	'disable_bbcode' => 'disable_bbcode', 'hide_title' => 'hide_title',
		'hide_author' => 'hide_author', 'hide_publishdate' => 'hide_publishdate',
		'display_fullincategory' => 'display_fullincategory', 'display_pageviews' => 'display_pageviews',
		'channeltype' => 'channeltype', 'edit_reason' =>  'edit_reason', 'edit_userid' =>  'edit_userid', 'edit_username' =>  'edit_username',
		'edit_dateline' =>  'edit_dateline', 'hashistory' =>  'hashistory',  'starternodeoptions' =>  'starternodeoptions',
		'channelnodeoptions' =>  'channelnodeoptions', 'contenttypeclass' => 'contenttypeclass',
		);

	protected function __construct()
	{
		parent::__construct();
		$this->library = vB_Library::instance('node');
		$this->pmContenttypeid = vB_Types::instance()->getContentTypeId('vBForum_PrivateMessage');
	}

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

	/** 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($nodeid)
	{
		$userContext = vB::getUserContext();
		if($userContext->isModerator())
		{
			$this->inlinemodAuthCheck();
		}


		//we need to handle a single nodeid or an array of nodeids
		if (!is_array($nodeid))
		{
			$nodeids = array($nodeid);
		}
		else
		{
			$nodeids = $nodeid;
		}
		//First check permissions of course.
		foreach ($nodeids as $nodeid)
		{
			//this can be approved through moderator permissions, or because the node is the current user's and they have forumpermission canopenclose
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canopenclose', $nodeid))
			{
				$node = $this->library->getNode($nodeid);

				if(($node['userid'] != vB::getCurrentSession()->get('userid')) OR
					!vB::getUserContext()->getChannelPermission('forumpermissions', 'canopenclose', $nodeid))
				{
					throw new vB_Exception_Api('no_permission');
				}
			}
		}

		return $this->library->openNode($nodeids);
	}


	/** Returns a list of the fields we can show for a node with public preview but which the current user cannot see.
	*
	*	@return		mixed	array of strings- the fieldnames the user can view.
	*/
	public function fetchPreviewFields()
	{
		return $this->previewFields;
	}

	/** 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($nodeid)
	{
		$userContext = vB::getUserContext();
		if($userContext->isModerator())
		{
			$this->inlinemodAuthCheck();
		}

		//we need to handle a single nodeid or an array of nodeids
		if (!is_array($nodeid))
		{
			$nodeids = array($nodeid);
		}
		else
		{
			$nodeids = $nodeid;
		}

		//First check permissions of course.
		foreach ($nodeids as $nodeid)
		{
			//this can be approved through moderator permissions, or because the node is the current user's and they have forumpermission canopenclose
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canopenclose', $nodeid))
			{
				$node = $this->library->getNode($nodeid);

				if(($node['userid'] != vB::getCurrentSession()->get('userid')) OR
					!vB::getUserContext()->getChannelPermission('forumpermissions', 'canopenclose', $nodeid))
				{
					throw new vB_Exception_Api('no_permission');
				}
			}
		}

		return $this->library->closeNode($nodeids);
	}

	/** 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($nodeids, $hard = true, $reason = '', $modlog = true, $reportspam = false)
	{
		if (empty($nodeids))
		{
			return false;
		}
		//If it's a protected channel, don't allow removal.
		$existing = vB_Library::instance('node')->getNodes($nodeids);
		// need to see if we require authentication
		$currentUserId = vB::getCurrentSession()->get('userid');
		$need_auth = false;
		$moderateInfo = vB::getUserContext()->getCanModerate();
		$allowToDelete = array();
		foreach ($existing as $node)
		{
			// this is a Visitor Message
			if (!empty($node['setfor']) AND ($node['setfor'] == $currentUserId))
			{
				$canModerateOwn = vB::getUserContext()->hasPermission('visitormessagepermissions', 'canmanageownprofile');
				if ($canModerateOwn)
				{
					$allowToDelete[$node['nodeid']] = $node['nodeid'];
					continue;
				}
			}
			else
			{
				$canModerateOwn = vB::getUserContext()->getChannelPermission('forumpermissions2', 'canmanageownchannels', $node['nodeid']);
			}
			// check if this is the owner of a blog that needs to moderate the comments
			if (!empty($moderateInfo['can']) OR ($canModerateOwn))
			{
				// let's get the channel node
				$channelid = vB_Library::instance('node')->getChannelId($node);
				if ($channelid == $node['nodeid'])
				{
					$channel = $node;
				}
				else
				{
					$channel = vB_Library::instance('node')->getNodeBare($channelid);
				}

				// this channel was created by the current user so we don't need the auth check
				if ( (in_array($channelid, $moderateInfo['can']) OR $canModerateOwn) AND ($channel['userid'] == $currentUserId))
				{
					$allowToDelete[$node['nodeid']] = $node['nodeid'];
					continue;
				}
			}

			if ($node['userid'] != $currentUserId)
			{
				$need_auth = true;
				break;
			}
		}

		$userContext = vB::getUserContext();
		// VBV-12184 Only moderators should get the inline mod auth prompt
		if (($need_auth OR $reportspam) AND $userContext->isModerator())
		{
			$this->inlinemodAuthCheck();
		}

		$userinfo = vB::getCurrentSession()->fetch_userinfo();
		$deleteNodeIds = array();
		$ancestorsId = $starters = array();
		$vmChannel = $this->fetchVMChannel();
		$contenttype_Channel = vB_Types::instance()->getContentTypeId('vBForum_Channel');
		foreach ($existing as $node)
		{
			//Check for protected- O.K. if it's not a channel.
			if ($node['protected'] AND ($node['contenttypeid'] == $contenttype_Channel))
			{
				throw new vB_Exception_Api('invalid_request');
			}


			// note that canremoveposts gives them ONLY physical-delete permissions, not soft delete.
			$canDeleteAsMod =
				(
					($userContext->getChannelPermission('moderatorpermissions', 'canremoveposts', $node['nodeid']) AND $hard)
					OR
					($userContext->getChannelPermission('moderatorpermissions', 'candeleteposts', $node['nodeid']) AND !$hard)
				);
			$canSoftDeleteOwn =
				(
					($node['userid'] == $currentUserId)
					AND !$hard
					AND (
							(($node['starter'] == $node['nodeid']) AND
								$userContext->getChannelPermission('forumpermissions', 'candeletethread', $node['nodeid']))
							OR
							(($node['starter'] != $node['nodeid']) AND
								$userContext->getChannelPermission('forumpermissions', 'candeletepost', $node['nodeid']))
					)
				);
			$canSoftDeleteOthers =
				(
					($node['userid'] != $currentUserId)
					AND !$hard
					AND $userContext->getChannelPermission('forumpermissions2', 'candeleteothers', $node['nodeid'])

				);

			// if they're not allowed to delete this node let's throw an exception in their face
			if (
				!(
					array_key_exists($node['nodeid'], $allowToDelete)
					OR $canDeleteAsMod OR $canSoftDeleteOwn	OR $canSoftDeleteOthers
				)
			)
			{
				throw new vB_Exception_Api('no_permission');
			}

			if ($node['parentid'] == $vmChannel AND $node['setfor'] == $currentUserId)
			{
				$vm_user = vB_User::fetchUserinfo($node['setfor']);
				if (!vB::getUserContext($vm_user['userid'])->hasPermission('genericpermissions', 'canviewmembers'))
				{
					throw new vB_Exception_Api('no_permission');
				}
			}

			array_push($deleteNodeIds, $node['nodeid']);

			$route = 'profile';
			if (class_exists('vB5_Cookie') AND vB5_Cookie::isEnabled())
			{
				// session is stored in cookies, so do not append it to url
				$route .= '|nosession';
			}

			if (!empty($node['starter']))
			{
				$starters[] = $node['starter'];
			}

			$parents = $this->library->fetchClosureParent($node['nodeid']);
			foreach ($parents as $parent)
			{
				if ($parent['depth'] > 0)
				{
					$ancestorsId[] = $parent['parent'];
				}
			}
		}
		$ancestorsId = array_unique($ancestorsId);

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

		return $this->library->deleteNodes($deleteNodeIds, $hard, $reason, $ancestorsId, $starters, $modlog, $reportspam);
	}


	/**
	 * Delete nodes as spam
	 *
	 * @param array	$nodeids The nodeids of the records to be deleted
	 * @param array $userids Selected userids who are being applied punitive action to
	 * @param bool $hard hard/soft delete
	 * @param string $reason
	 * @param bool $deleteother Whether to delete other posts and threads started by the affected users
	 * @param bool $banusers Whether to ban the affected users
	 * @param int $banusergroupid Which banned usergroup to move the users to
	 * @param string $period Ban period
	 * @param string $banreason Ban reason
	 */
	public function deleteNodesAsSpam($nodeids, $userids = array(), $hard = true, $reason = "",
		$deleteother = false, $banusers = false, $banusergroupid = 0, $period = 'PERMANENT', $banreason = '', $report = false
	)
	{
		$this->inlinemodAuthCheck();
		// Permission check
		$loginuser = &vB::getCurrentSession()->fetch_userinfo();
		$usercontext = &vB::getUserContext();

		if ($banusers AND !$usercontext->hasPermission('moderatorpermissions', 'canbanusers'))
		{
			$forumHome = vB_Library::instance('content_channel')->getForumHomeChannel();
			throw new vB_Exception_Api('nopermission_loggedin',
				$loginuser['username'],
				vB_Template_Runtime::fetchStyleVar('right'),
				vB::getCurrentSession()->get('sessionurl'),
				$loginuser['securitytoken'],
				vB5_Route::buildUrl($forumHome['routeid'] . '|nosession|fullurl')
			);
		}

		foreach ((array)$nodeids as $k => $nodeid)
		{
			if (!$usercontext->getChannelPermission('moderatorpermissions', 'canremoveposts', $nodeid))
			{
				unset($nodeids[$k]);
			}
		}

		if (empty($nodeids))
		{
			throw new vB_Exception_Api('no_permission');
		}

		$checkuserids = $this->fetchUsersFromNodeids($nodeids);
		$userids = array_intersect((array)$userids, array_keys($checkuserids));

		if ($deleteother AND !empty($userids))
		{
			$search_api = vB_Api::instanceInternal('search');
			$search_json = json_encode(array(
				'authorid'		=> $userids,
				'ignore_cache'	=> true,
				'exclude_type' => array(
					'vBForum_Channel',
					'vBForum_PrivateMessage',
					'vBForum_Report',
					'vBForum_Infraction',
				)
			));

			$result = $search_api->getSearchResult($search_json);

			$othernodeids = array();
			do
			{
				$othernodes = $search_api->getMoreNodes($result['resultId']);

				if ($othernodeids == array_values($othernodes['nodeIds']))
				{
					break;
				}
				$othernodeids = array_values($othernodes['nodeIds']);

				if (!empty($othernodeids))
				{
					$this->deleteNodes($othernodeids, $hard, $reason, true, $report);
				}
			} while (!empty($othernodeids));
		}
		else
		{
			$this->deleteNodes($nodeids, $hard, $reason, true, $report);
		}

		if ($banusers AND !empty($userids))
		{
			$user_api = vB_Api::instanceInternal('user');
			//get a $banusergroupid if none is provided
			if (empty($banusergroupid))
			{
				$bannedusergroups = vB_Api::instanceInternal('usergroup')->fetchBannedUsergroups();
				$banusergroupid = key($bannedusergroups);
			}
			$user_api->banUsers($userids, $banusergroupid, $period, $banreason);
		}

		return $nodeids;
	}

	/** undeletes a set of nodes
	 *	@param	array	The nodeids of the records to be deleted
	*
	*	@return	array nodeids that were deleted
	**/
	public function undeleteNodes($nodeids)
	{
		if (empty($nodeids))
		{
			return false;
		}

		$assertor = vB::getDbAssertor();
		//If it's a protected channel, don't allow removal.
		$existing = vB_Library::instance('node')->getNodes($nodeids);
		// need to see if we require authentication
		$currentUserid = vB::getCurrentSession()->get('userid');
		$userContext = vB::getUserContext();
		//If any of the nodes are channels we need to rebuild
		$needRebuild = false;
		foreach ($existing as $node)
		{
			if ($node['userid'] != $currentUserid)
			{
				$this->inlinemodAuthCheck();
			}

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

			if (!$userContext->getChannelPermission('moderatorpermissions', 'candeleteposts', $node['nodeid']))
			{
				//one check for channels
				if (($node['starter'] == 0) AND $userContext->getChannelPermission('forumpermissions2', 'canmanageownchannels', $node['nodeid']))
				{

					$starter = vB_Api::instanceInternal('node')->getNode($node['starter']);
					$channel = vB_Api::instanceInternal('node')->getNode($starter['parentid']);

					if ($channel['userid'] != $currentUserid)
					{
						throw new vB_Exception_Api('no_delete_permissions');
					}
				}
				else
				{
					throw new vB_Exception_Api('no_delete_permissions');
				}
			}
		}

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

		return $this->library->undeleteNodes($nodeids, $needRebuild);
	}

	/**
	 * 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())
	{
		if(vB::getUserContext()->isModerator())
		{
			$this->inlinemodAuthCheck();
		}

		return $this->library->moveNodes($nodeids, $to_parent, $makeTopic, $newtitle, $modlog, $leaveRedirectData);
	}

	/**
	 * DEPRECATED Move multiple posts to a new topic or to a new channel.  Use moveNodes function.
	 *
	 * @param $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 array Moved node ids
	 * @deprecated
	 */
	public function movePosts($nodeids, $to_parent, $newtitle = '')
	{
		//You should just call the moveNodes method
		return $this->moveNodes($nodeids, $to_parent, true, $newtitle);
	}

	/**
	 * 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 = '')
	{
		$userContext = vB::getUserContext();
		if($userContext->isModerator())
		{
			$this->inlinemodAuthCheck();
		}

		return $this->library->cloneNodes($nodeids, $to_parent, $newtitle);
	}

	/**
	 * Convert node path or id string to node id.
	 *
	 * @param string|int $nodestring Node String. If $nodestring is a string, it should be a route path to the node
	 * @return int Node ID
	 */
	protected function assertNodeidStr($nodestring)
	{
		return $this->library->assertNodeidStr($nodestring);
	}

	/**
	 * Merge several topics into a target topic
	 *
	 * @param array $nodeids Source topic node IDs
	 * @param int $targetnodeid Target topic node ID
	 *
	 * @return array
	 */
	public function mergeTopics($nodeids, $targetnodeid, array $leaveRedirectData = array())
	{
		$this->inlinemodAuthCheck();

		// check that the user has permission
		$nodesToCheck = $nodeids;
		$nodesToCheck[] = $targetnodeid;
		foreach ($nodesToCheck AS $key => $nodeid)
		{
			// this is here just in case for some reason, a nodeid is 0. Shouldn't happen, but
			// I don't want getChannelPermission to go bonkers from it.
			if (empty($nodeid))
			{
				unset($nodesToCheck[$key]);
				continue;
			}

			if ( !vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmanagethreads', $nodeid) )
			{
				// perhaps we could generate a list of unmergeable nodes and return a warning instead, but
				// I don't think there's a real use case where a moderator can manage only *some* of the
				// nodes they're trying to merge. I think that would require multiple channels being involved, and
				// we don't have a UI for that so I can't test it. As such I'm just going to throw an exception if
				// *any* of the nodes fail the check.
				throw new vB_Exception_Api('no_permission');
			}
		}

		$mergedNodes = array();
		$sourceNodes = array();

		if (count($nodeids) < 2)
		{
			throw new vB_Exception_Api('not_much_would_be_accomplished_by_merging');
		}

		if (!in_array($targetnodeid, $nodeids))
		{
			throw new vB_Exception_Api('invalid_target');
		}

		$userContext = vB::getUserContext();

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

		$loginfo = array();
		$targetnode = $this->getNode($targetnodeid);

		foreach ($nodes as $node)
		{
			if (intval($node['inlist']) AND !intval($node['protected'])
				AND
				(
					$userContext->getChannelPermission('forumpermissions', 'canview', $node['nodeid'], false, $node['parentid'])
				)
				AND $node['nodeid'] == $node['starter'] // Node must be a topic
			)
			{
				$mergedNodes[] = $node['nodeid'];

				if ($node['nodeid'] != $targetnodeid)
				{
					$sourceNodes[] = $node['nodeid'];

					$extra = array(
						'targetnodeid'	=> $targetnode['nodeid'],
						'targettitle'	=> $targetnode['title'],
					);

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

		if (count($mergedNodes) < 2)
		{
			throw new vB_Exception_Api('not_much_would_be_accomplished_by_merging');
		}

		if ($mergedNodes == $sourceNodes)
		{
			// Something wrong with target node
			throw new vB_Exception_Api('invalid_target');
		}

		$this->moveNodes($sourceNodes, $targetnodeid, false, false, false, $leaveRedirectData); // Dont log the individual moves

		// We need to promote the replies of the sourcenodes to the replies of the targetnode instead of being comments of the sourcenodes.
		foreach ($sourceNodes as $sourcenodeid)
		{
			$sourcereplies = vB::getDbAssertor()->getRows('vBForum:node',
				array(
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
					'parentid' => $sourcenodeid,
				),
				array(
					'field' => array('publishdate'),
					'direction' => array(vB_dB_Query::SORT_ASC)
				)
			);

			$replyNodes = array();
			foreach ($sourcereplies as $replies)
			{
				$replyNodes[] = $replies['nodeid'];
			}

			if ($replyNodes)
			{
				// Move the replies to the targetnode
				$this->moveNodes($replyNodes, $targetnodeid, false, false, false);
			}
		}

		// If source node is a poll, we need to delete it
		// Here all source nodes' children have been promoted so they won't be deleted
		foreach ($nodes as $node)
		{
			if ($node['nodeid'] != $targetnodeid AND $node['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Poll'))
			{
				$this->library->deleteNode($node['nodeid']);
			}
		}

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

		$clearCacheNodes = array_unique(array_merge($mergedNodes, $sourceNodes, array($targetnodeid)));
		$this->library->clearChildCache($clearCacheNodes);
		$this->clearCacheEvents($clearCacheNodes);

		return array($mergedNodes, $sourceNodes, $targetnodeid);
	}

	/**  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)
	{
		if (!intval($nodeid) OR !intval($datefrom))
		{
			throw new Exception('invalid_node_id');
		}

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

		return $this->library->setPublishDate($nodeid, $datefrom, $dateto);
	}

	/** Sets the unpublish date
	 *	@param	integer	The node id
	 *	@param	integer	The timestamp for unpublish
	 *
	 *	@return	boolean
	 **/
	public function setUnPublishDate($nodeid, $dateto = false)
	{
		if (!intval($nodeid))
		{
			throw new Exception('invalid_node_id');
		}

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

		return $this->library->setUnPublishDate($nodeid, $dateto);
	}

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

		return $this->library->setUnPublished($nodeid);
	}

	/*** 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)
	{
		$this->inlinemodAuthCheck();

		if (!is_array($nodeids))
		{
			$nodeids = array($nodeids);
		}
		$featureIds = array();
		foreach ($nodeids as $nodeid)
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'cansetfeatured', $nodeid))
			{
				continue;
			}
			$featureIds[] = $nodeid;
		}
		//If this user doesn't have the featured permission and they are trying to set it,
		//throw an exception
		if (empty($featureIds))
		{
			throw new Exception('no_featured_permissions');
		}

		return $this->library->setFeatured($featureIds, $set);
	}

	/*** sets a node list to be not featured
	 *	@param	array	The node ids
	 *
	 *	@return	array nodeids that have permission to be featured
	 **/
	public function  setUnFeatured($nodeids)
	{
		return $this->setFeatured($nodeids, false);
	}


	/** clears the unpublishdate flag.
	 *	@param	integer	The node id
	 *
	 *	@return	boolean
	 **/
	public function  clearUnpublishDate($nodeid)
	{
		if (!intval($nodeid))
		{
			throw new Exception('invalid_node_id');
		}

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

		return $this->library->clearUnpublishDate($nodeid);
	}

	/** gets one node.
	 *	@param	integer The node id
	 *  @param	boolean Whether to include list of parents
	 *  @param	boolean Whether to include joinable content
	 *
	 *	@return	mixed	array of node records, optionally including attachment and ancestry.
	 **/
	public function getNode($nodeid, $withParents = false, $withJoinableContent = false)
	{
		$node = $this->library->getNode($nodeid, $withParents, $withJoinableContent);

		//check permissions.
		$approved = $this->validateNodeList(array($nodeid => $node));

		if (!empty($approved['errors']))
		{
			throw new vB_Exception_Api('invalid_request');
		}
		else if (empty($approved) OR empty($approved[$nodeid]))
		{
			throw new vB_Exception_Api('no_permission');
		}
		else
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermission', 'canviewips', $nodeid))
			{
				$approved[$nodeid]['ipaddress'] = '';
			}
			return $approved[$nodeid];
		}
	}

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

		$nodes = $this->library->getNodes($nodeList);
		$nodes = $this->validateNodeList($nodes);

		foreach ($nodes AS $key => $node)
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermission', 'canviewips', $node['nodeid'], false, $node['parentid']))
			{
				$nodes[$key]['ipaddress'] = '';
			}
		}
		return $nodes;
	}

	/**
	 * Gets the attachment information for a node. Which may be empty.
	 * @param mixed  	integer or array of integers
	 * @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
	 */
	function getNodeAttachments($nodeids)
	{
		if (empty($nodeids))
		{
			return array();
		}

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

		$getnodeids = array();
		$perms = array();
		foreach ($nodeids AS $nodeid)
		{
			$perms[$nodeid] = array(
				'cangetattachment' => vB::getUserContext()->getChannelPermission('forumpermissions', 'cangetattachment', $nodeid),
				'cangetimgattachment' => vB::getUserContext()->getChannelPermission('forumpermissions2', 'cangetimgattachment', $nodeid),
			);

			if ($perms[$nodeid]['cangetattachment'] OR $perms[$nodeid]['cangetimgattachment'])
			{
				$getnodeids[] = $nodeid;
			}
		}

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

		$attachments = array();
		$rawAttach = $this->library->fetchNodeAttachments($getnodeids);

		foreach ($rawAttach AS $attach)
		{
			$isImg = in_array(strtolower($attach['extension']), array('png', 'bmp', 'jpeg', 'jpg', 'jpe', 'gif'));
			if (
				($isImg AND $perms[$attach['parentid']]['cangetimgattachment'])
				OR
				(!$isImg AND $perms[$attach['parentid']]['cangetattachment'])
			)
			{
				$attachments[$attach['nodeid']] = $attach;
			}
		}

		return $attachments;
	}


	/** lists the nodes that should be displayed on a specific page including content detail.
	 *	@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..
	 *							Options flags:
	 *							showVm => appends visitor message node info.
	 *							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	 *  						withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	 *
	 * 	@return	mixed	array of id's
	 **/
	public function listNodeContent($parentid, $page = 1, $perpage = 20, $depth = 0, $contenttypeid = null, $options = false)
	{
		return $this->listNodeFullContent($parentid, $page, $perpage, $depth, $contenttypeid, $options);
	}

	/** lists the nodes that should be displayed on a specific page including content detail.
	 *	@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.
	 *							Options flags:
	 *							showVm => appends visitor message node info.
	 *							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	 *  						withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	 *
	 * 	@return	mixed	array of id's
	 **/
	public function listNodeFullContent($parentid, $page = 1, $perpage = 20, $depth = 0, $contenttypeid = null, $options = false)
	{
		//First get the node list
		$nodeList = $this->library->listNodes($parentid, $page, $perpage, $depth, $contenttypeid, $options);
		$nodeList = $this->library->addFullContentInfo($nodeList, $options);
		foreach ( $nodeList AS $key => $node)
		{
			if (!$node['content']['moderatorperms']['canviewips'])
			{
				$nodeList[$key]['ipaddress'] = '';
				$nodeList[$key]['content']['ipaddress'] = '';
			}
		}
		return $this->validateNodeList($nodeList);
	}

	/** Gets the content info for a list of nodes
	 *	@param	mixed	array of node ids
	 *	@param 	mixed	array of options.
	 *						Options flags:
	 *							showVm => appends visitor message node info.
	 *							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	 *  						withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	 *
	 * 	@return	mixed	array of content records
	 **/
	public function getContentforNodes($nodeList, $options = false)
	{
		if (empty($nodeList))
		{
			return array();
		}
		$content = $this->library->getContentforNodes($nodeList, $options);
		$content = $this->validateNodeList($content);
		foreach ($content AS $key => $node)
		{
			if (!$node['content']['moderatorperms']['canviewips'])
			{
				$content[$key]['ipaddress'] = '';
				$content[$key]['content']['ipaddress'] = '';
			}
		}
		return $content;
	}

	/** Gets the content info for a list of nodes
	 *	@param array array of node ids
	 *	@param 	mixed	array of options.
	 *						Options flags:
	 *							showVm => appends visitor message node info.
	 *							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	 *  						withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	 *
	 * 	@return array array of content records -- preserves the original keys
	 **/
	public function getFullContentforNodes($nodeList)
	{
		if (empty($nodeList))
		{
			return array();
		}

		$content = $this->library->getFullContentforNodes($nodeList);
		$content = $this->validateNodeList($content);
		foreach ($content AS $key => $node)
		{
			if (!$node['content']['moderatorperms']['canviewips'])
			{
				$content[$key]['ipaddress'] = '';
				$content[$key]['content']['ipaddress'] = '';
			}
		}
		return $content;
		}

	/** Given a list of nodes, removes those the user can't see.
	*
	* 	@param	mixed	array of integers
	*
	* 	@return	mixed	array of integers
	**/
	protected function validateNodeList($nodes)
	{
		//@todo.  Let's look into how this works.
		$userid = vB::getCurrentSession()->get('userid');
		$pmIds = array();
		$nodeMap = array();
		$canViewIps = vB::getUserContext()->hasPermission('moderatorpermissions', 'canviewips');
		//First check permissions
		foreach ($nodes as $key => $node)
		{
			//If they are the author they can see it.
			if ($node['userid'] == $userid)
			{
				continue;
			}
			//if it's a private message, the user can only see if they have a record in sentto or are the originator.
			else if ($node['contenttypeid'] == $this->pmContenttypeid)
			{

				$pmIds[] = $node['nodeid'];
				$nodeMap[$node['nodeid']] = $key;
			}
			else if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $node['nodeid'], false, $node['parentid']))
			{
				if ($node['public_preview'])
				{
					$this->getPreviewOnly($nodes[$key]);
				}
				else
				{
					unset($nodes[$key]);
				}
			}
			else if (!vb::getUserContext()->getChannelPermission('forumpermissions', 'canviewthreads', $node['nodeid']))
			{
				if ($node['public_preview'])
				{
					$this->getPreviewOnly($nodes[$key]);
				}
				else
				{
					//The user can't see content. We hide the content and the "last" data
					unset($nodes[$key]['content']);
					$nodes[$key]['content'] = $node;
					$nodes[$key]['lastcontent'] = $node['publishdate'];
					$nodes[$key]['lastcontentid'] = $node['nodeid'];
					$nodes[$key]['lastcontentauthor'] = $node['authorname'];
					$nodes[$key]['lastauthorid'] = $node['userid'];
					unset($nodes[$key]['photo']);
					unset($nodes[$key]['attach']);
				}
			}
			if (!$canViewIps)
			{
				unset($nodes[$key]['ipaddress']);
			}

		}

		if (!empty($pmIds))
		{
			//The user can see it if there's a record in sentto
			$sentQry = vB::getDbAssertor()->assertQuery('vBForum:sentto', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			 'nodeid' => $pmIds, 'userid' => $userid));
			$cansee = array();
			foreach ($sentQry as $sentto)
			{
				$cansee[] = $sentto['nodeid'];
			}

			$pmIds = array_diff($pmIds, $cansee);

			foreach ($pmIds as $nodeid)
			{
				$index = $nodeMap[$nodeid];

				if ($index !== false)
				{
					unset($nodes[$index]);
				}
			}
		}

		return $nodes;
	}

	/**	Gets the channel title and routeid
	 *	@param	int		The node id.
	 *	@return	mixed	Array of channel info
	 */
	public function getChannelInfoForNode($channelId)
	{
		$channelInfo = vB_Library::instance('node')->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 array Node IDs that the user has voted.
	 * @see vB_Api_Reputation::fetchNodeVotes()
	 */
	protected function getNodeVotes(array $nodeIds, $userid = 0)
	{
		return vB_Api::instanceInternal('reputation')->fetchNodeVotes($nodeIds, $userid);
	}

	/**
	* This gets a content record based on nodeid. Useful from ajax.
	*
	*	@param	int
	*	@param	int	optional
	*	@param	bool optional. 	Options flags:
	*							showVm => appends visitor message node info.
	*							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	*  							withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	*
	*	@return array.  An array of node record arrays as $nodeid => $node
	*
	***/
	public function getNodeContent($nodeid, $contenttypeid = false, $options = array())
	{
		return $this->getNodeFullContent($nodeid, $contenttypeid, $options);
	}


	/** This gets a content record based on nodeid including channel and starter information.
	 *
	 * 	@param	int
	 *	@param	int	optional
	 *	@param	bool optional	Options flags:
	*							showVm => appends visitor message node info.
	*							Such as isVisitorMessage flag indicating if node is visitor message and vm_userInfo from the user the visitor message was posted for.
	*  							withParent => appends information from the parent. This info will append the 'parentConversation' info if the node is a comment.
	 *
	 *	@return mixed
	 *
	 ***/
	public function getNodeFullContent($nodeid, $contenttypeid = false, $options = array())
	{
		if (!is_numeric($nodeid))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		$nodeid = intval($nodeid);
		$result = $this->library->getNodeFullContent($nodeid, $contenttypeid, $options);

		//Can happen with a damaged node
		if (empty($result) OR empty($result[$nodeid]) OR empty($result[$nodeid]['nodeid']))
		{
			return array();
		}

		foreach ($result AS $key => $node)
		{
			$contentApi = vB_Api_Content::getContentApi($node['contenttypeid']);

			if (!$contentApi->validate($node, vB_Api_Content::ACTION_VIEW, $node['nodeid'], array($node['nodeid'] => $node)))
			{
				throw new vB_Exception_Api('no_permission');
			}

			if (!$node['moderatorperms']['canviewips'])
			{
				$result[$key]['ipaddress'] = '';
			}
		}
		return $result;
	}

	/** This gets a content record based on nodeid including channel and starter information.
	 *
	 * 	@param	int
	 *	@return mixed
	 *
	 ***/
	public function getQuoteFullContent($quoteId)
	{
		if (!is_array($quoteId))
		{
			$quoteId = array($quoteId);
		}

		$nodeIds = $postIds = $translatedIds = array();
		foreach ($quoteId AS $id)
		{
			if (preg_match('#^n(\d+)$#', $id, $matches))
			{
				// it's a node id
				$nodeIds[] = $matches[1];
			}
			else
			{
				// it's a postid from vB4, we need to translate
				$postIds[] = $id;
			}
		}

		if (!empty($postIds))
		{
			// check cache first
			$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);
			$cacheKey = 'vB4_QuoteIds';
			$cached = $cache->read($cacheKey);

			if ($cached)
			{
				foreach($postIds AS $key => $oldId)
				{
					if (isset($cached[$oldId]))
					{
						$translatedIds[$cached[$oldId]] = $oldId;
						unset($postIds[$key]);
					}
				}
			}
			else
			{
				$cached = array();
			}

			// do we still need to translate?
			if (!empty($postIds))
			{
				$nodes = vB::getDbAssertor()->assertQuery('vBForum:fetchLegacyPostIds', array(
					'oldids' => $postIds,
					'postContentTypeId' => vB_Types::instance()->getContentTypeID('vBForum_Post'),
				));

				if ($nodes)
				{
					foreach($nodes as $node)
					{
						$translatedIds[$node['nodeid']] = $node['oldid'];
						$cached[$node['oldid']] = $node['nodeid'];
					}

					$cache->write($cacheKey, $cached, 14400);
				}
			}
		}

		// use the the ids originally requested as keys
		$result = array();
		foreach (array_unique($nodeIds + array_keys($translatedIds)) as $nodeId)
		{
			$info = $this->getNodeFullContent($nodeId);
			$info = $info[$nodeId];

			if (in_array($nodeId, $nodeIds))
			{
				$result["n$nodeId"] =& $info;
			}

			if (isset($translatedIds[$nodeId]))
			{
				$result[$translatedIds[$nodeId]] =& $info;
			}
		}

		return $result;
	}

	/** Validates permission and sets a node to published status
	 *
	 *	@param	mixed	nodeid- integer or array of integers
	 *
	 **/
	public function publish($nodes)
	{
		if (!is_array($nodes))
		{
			$nodes = array($nodes);
		}

		$timeNow = vB::getRequest()->getTimeNow();
		foreach ($nodes as $nodeid)
		{
			if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid) OR
			vB::getUserContext()->getChannelPermission('forumpermissions2', 'canpublish', $nodeid))
			{
				$this->setPublishDate($nodeid, $timeNow);
			}
		}
	}

	/** Validates permission and sets a node to unpublished status
	 *
	 *	@param	mixed	nodeid- integer or array of integers
	 *
	 **/
	public function unPublish($nodes, $permanent = false)
	{
		if (!is_array($nodes))
		{
			$nodes = array($nodes);
		}

		$approved = array();

		foreach ($nodes as $nodeid)
		{
			if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid, false) OR
				vB::getUserContext()->getChannelPermission('forumpermissions2', 'canpublish', $nodeid))
			{
				$approved[] = $nodeid;
			}

		}


		if ($permanent)
		{
			$publishdate = -1;
		}
		else
		{
			$publishdate = 0;
		}
		$approvedNodes = vB_Library::instance('node')->getNodes($approved);
		$parents = $starters = array();
		$events = array();
		foreach ($approvedNodes as $approvedNode)
		{
			if (empty($approvedNode['starter']) OR !empty($starters[$approvedNode['starter']]))
			{
				continue;
			}
			$starters[$approvedNode['starter']] = 1;
			$events[] = "nodeChg_" . $approvedNode['starter'];
		}

		if (!empty($starters))
		{
			$starterNodes = vB_Library::instance('node')->getNodes(array_keys($starters));
			foreach ($starterNodes as $starterNode)
			{
				if (empty($starterNode['parentid']) OR !empty($parents[$starterNode['parentid']]))
				{
					continue;
				}
				$parents[$starterNode['parentid']] = 1;
				$events[] = "nodeChg_" . $starterNode['parentid'];
			}
		}
		vB_Cache::instance()->allCacheEvent($events);
		foreach ($approved as $nodeid)
		{
			$this->library->setUnPublished($nodeid);
		}
		vB_Api::instance('Search')->purgeCacheForCurrentUser();
	}

	/**
	 *	@param	mixed	array, which can include a channelid, contenttypeid, userid, sort, vB_dB_Query::PARAM_LIMIT and 'fetchall'=>true to get posts which were already flagged
	 *
	 *	@return	mixed	array of nodeid, title, created
	 **/
	public function getModeration($options = array())
	{
		/** defaults are :
		 *	all channels
		 * 	all content types
		 *	maxCount = 100
		 *	fetchall = false
		 *  userid	= all
		 *	sort	created asc (oldest first)
		 *			can be username, created, contenttypeid, or title. If you want other than asc,
		 *			pass array('sortby'=>'username', 'direction' => 'asc' or 'desc'
		 *
		 */

		//If we got passed a channel- first make sure they have permission. If not, we're done.
		if (!empty($options['channelid']))
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $options['channelid']))
			{
				return false;
			}
			$getModerate = vB::getUserContext()->getCanModerate();

			$parameters = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD, 'canModerate' => $options['channelid'],
				'noModerate' => $getModerate['cannot']);
		}
		else
		{
			$getModerate = vB::getUserContext()->getCanModerate();
			if (empty($getModerate['can']))
			{
				throw new Exception('no_moderate_permissions');
			}
			$parameters = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD, 'canModerate' => $getModerate['can'],
				'noModerate' => $getModerate['cannot']);
		}

		foreach (array('channelid','contenttypeid','sort', vB_dB_Query::PARAM_LIMIT,'fetchall', 'userid') as $optionName)
		{
			if (isset($options[$optionName]))
			{
				$parameters[$optionName] = $options[$optionName];
			}
		}


		$query = vB::getDbAssertor()->assertQuery('vBForum:getModeration', $parameters);

		if (!$query OR !$query->valid())
		{
			return false;
		}

		$results = array();
		$node = $query->current();
		while ($query->valid())
		{
			$results[$node['nodeid']] = $node;
			$node = $query->next();
		}

		return $results;
	}

	/** Adds one or more attachments
	 *
	 * 	@param 	int
	 *	@param mixed	array of attachment info
	 *
	 *	@return	int		an attachid
	 *
	 **/
	public function addAttachment($nodeid, $data)
	{
		/* data must include a filedataid, and possibly
		   any combination of visible,counter,posthash,filename,caption, reportthreadid, settings
		*/
		if (empty($nodeid) OR empty($data['filedataid']))
		{
			throw new Exception('incorrect_attach_data');
		}
		//we need the current node data.
		$node = $this->getNode($nodeid);

		if (empty($node))
		{
			throw new Exception('incorrect_attach_data');
		}

		$maxattachments = vB::getUserContext()->getChannelLimitPermission('forumpermissions', 'maxattachments', $nodeid);

		//check the permission.
		$canAdd = false;
		if (($node['userid'] == vB::getCurrentSession()->get('userid')) AND vB::getUserContext()->getChannelPermission('forumpermissions', 'canpostattachment', $nodeid))
		{
			$canAdd = true;
		}

		if (!$canAdd AND vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateattachments', $nodeid))
		{
			$canAdd = true;
		}

		if (!$canAdd)
		{
			throw new Exception('no_permission');
		}

		if (isset($node['attachments'])AND ($maxattachments > 0) AND count($node['attachments']) >= $maxattachments)
		{
			throw new Exception('max_attachments_reached');
		}

		$attachApi = vB_Api::InstanceInternal('content_attach');
		$data['parentid'] = $nodeid;
		$attachid = $attachApi->add($data);

		//update the refcount in filedata
		if ($attachid)
		{
			 vB::getDbAssertor()->assertQuery('updateFiledataRefCount',
				array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'countChange' => 1, 'filedataid' => $data['filedataid']));
		}
		return is_array($attachid) ? $attachid[0] : $attachid;
	}

	/** delete one or more attachments
	 *
	 * 	@param 	int
	 *	@param 	mixed	an attachment id, or an array of either attach id's, or an array of filedataids
	 *	@param	bool	whether to delete all attachments for this node
	 *
	 *
	 ***/
	public function removeAttachment($nodeid, $data, $all = false)
	{
		/* data must include a filedataid, and possibly
		   any combination of visible,counter,posthash,filename,caption, reportthreadid, settings
		*/
		if (empty($nodeid))
		{
			throw new Exception('incorrect_attach_data');
		}

		//check the permissions.
		$node = vB_Library::instance('node')->getNodeBare($nodeid);
		if (empty($node))
		{
			throw new Exception('incorrect_attach_data');
		}

		if (!($node['userid'] == vB::getCurrentSession()->get('userid')) OR
			!vB::getUserContext()->getChannelPermission('forumpermissions', 'canpostattachment', $nodeid))
		{
			if (! vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateattachments', $nodeid))
			{
				throw new Exception('no_permission');
			}
		}

		//If we got here we have permission.
		//If all is, we delete all attachments for this node.
		if ($all)
		{
			$params = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
				vB_dB_Query::CONDITIONS_KEY => array('nodeid' => $nodeid));

			return  vB::getDbAssertor()->assertQuery('vBForum:attach', $params);
		}

		if (isset($data['attachnodeid']))
		{
			return  vB::getDbAssertor()->assertQuery('vBForum:attach', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
				'nodeid' => $data['attachnodeid'],
			));
		}

		if (isset($data['filedataid']))
		{
			$params = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
				vB_dB_Query::CONDITIONS_KEY => array());
			if (is_array($data['filedataid']))
			{
				foreach($data['filedataid'] AS $id)
				{
					$params[vB_dB_Query::CONDITIONS_KEY][] = array('filedataid' => $id,
						'nodeid' => $nodeid);
				}
			}
			else
			{
				$params[vB_dB_Query::CONDITIONS_KEY] = array('nodeid' => $nodeid,
				'filedataid' => $data['filedataid']);
			}

			vB::getDbAssertor()->assertQuery('updateFiledataRefCount',
				array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'countChange' => -1, 'filedataid' => $data['filedataid']));

			return  vB::getDbAssertor()->assertQuery('vBForum:attach', $params);
		}

		//if we got here we don't have enough data
		throw new Exception('incorrect_attach_data');
	}

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

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

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

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

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

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

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

	/** This returns a list of a user's albums
	*
	*
	*
	**/
	public function listAlbums($userid = false, $page = 1, $perpage = 100, $options= array())
	{
		if (!$userid)
		{
			if (empty($_REQUEST['userid']))
			{
				$userid = vB::getUserContext()->fetchUserId();
	}
			else
			{
				$userid = $_REQUEST['userid'];
			}
		}
		$albumChannel = $this->fetchAlbumChannel();
		$contenttypeid = vB_Types::instance()->getContentTypeId('vBForum_Gallery');
		$nodeList = $this->library->listNodes($albumChannel, $page, $perpage, 0, $contenttypeid, array('includeProtected' => 1,
			'userid' => $userid));
		return $this->library->addContentInfo($nodeList, $options);
	}


	/** returns array of all node content for a user's activity
	 *
	 *	@param	mixed	array- can include userid, sento, date flag, count, page, and  content type.
	 *
	 *	@return	integer		array including
	 **/
	public function fetchActivity($params)
	{
		$userdata = vB::getUserContext()->getReadChannels();
		//It's possible we have a permission record for the current node.
		if (empty($userdata['canRead']) OR !in_array(1, $userdata['canRead']))
		{
			throw new Exception('no_read_permission');
		}
		$exclude = $userdata['cantRead'];

		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD);

		/** Time filter */
		if (!empty($params[self::FILTER_TIME]))
		{
			$data[self::FILTER_TIME] = $params[self::FILTER_TIME];
		}

		if (!empty($params['contenttypeid']))
		{
			$data['contenttypeid'] = intval($params['contenttypeid']);
		}

		if (!empty($params['contenttypeid']))
		{
			$data['contenttypeid'] = intval($params['contenttypeid']);
		}

		if (!empty($params[self::FILTER_SOURCE]))
		{
			$data[self::FILTER_SOURCE] = $params[self::FILTER_SOURCE];
		}

		//We must have userid
		if (!empty($params['userid']))
		{
			$data['userid'] = intval($params['userid']);
		}
		else
		{
			$userinfo = vB::getCurrentSession()->fetch_userinfo();
			$data['userid'] = $userinfo['userid'];
		}

		if (!empty($params['perpage']))
		{
			$data[vB_dB_Query::PARAM_LIMIT] = intval($params['perpage']);
		}

		if (!empty($params['page']))
		{
			$data[vB_dB_Query::PARAM_LIMITPAGE] = intval($params['page']);
		}

		$results = vB::getDbAssertor()->assertQuery('vBForum:getActivity',
			$data);

		if (!$results->valid())
		{
			return array();
		}

		$nodeids = array();
		foreach ($results as $result)
		{
			$nodeids[] = $result['nodeid'];
		}
		$results = $this->getFullContentforNodes($nodeids);
		return $results;
	}

	/** This returns all the albums in a channel. Those can be photogalleries or text with attachments.
	 *
	 *	@param		int
	 *
	 *	@mixed		array of node records. Each node includes the node content and userinfo, and attachment records.
	 **/
	public function getAlbums($nodeid)
	{

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

		if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $nodeid))
		{
			throw new vB_Exception_Api('no_permission');
		}

		return $this->library->getAlbums($nodeid);
	}

	/**
	 * Sets or unsets the sticky field
	 * @param array $nodeids
	 * @param boolean $stick - set or unset the sticky field
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have the permission to be changed
	 */
	function setSticky($nodeids, $stick = true)
	{
		$this->inlinemodAuthCheck();

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

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

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

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

		foreach ($nodeids as $nodeid)
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmanagethreads', $nodeid))
			{
				continue;
			}

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

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

			array_push($stickyNodeIds, $nodeid);
			array_push($events, 'nodeChg_' . $nodeid);
		}

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


		$result = vB::getDbAssertor()->update('vBForum:node', array('sticky' => $stick), array(array('field' => 'nodeid', 'value' => $stickyNodeIds, 'operator' => vB_dB_Query::OPERATOR_EQ)));

		vB_Cache::instance()->allCacheEvent($events);
		// we need to purge the cache so it is immediately shown
		vB_Api::instance('Search')->purgeCacheForCurrentUser();

		vB_Library_Admin::logModeratorAction($loginfo, ($stick ? 'node_stuck_by_x' : 'node_unstuck_by_x'));

		return $stickyNodeIds;
	}

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

	/**
	 * Sets or unsets the approved field
	 * @param array $nodeids
	 * @param boolean $approved - set or unset the approved field
	 * @throws vB_Exception_Api
	 * @return array - the nodeids that have the permission to be changed
	 */
	function setApproved($nodeids, $approved = true)
	{
		$nodeids = vB::getCleaner()->clean($nodeids,  is_array($nodeids) ? vB_Cleaner::TYPE_ARRAY_UINT : vB_Cleaner::TYPE_UINT);

		if (empty($nodeids))
		{
			return false;
		}
		$currentUserid = vB::getCurrentSession()->get('userid');

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

		if (!is_array($nodeids))
		{
			$nodeids = array($nodeids);
		}
		$existing = vB_Library::instance('node')->getNodes($nodeids);

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

		// need to see if we require authentication
		$userContext = vB::getUserContext();
		$approveNodeIds = array();
		//allow unapproving of VMs by the recipient that has canmanageownprofile
		$need_auth = false;
		$moderateInfo = vB::getUserContext()->getCanModerate();
		$timeNow = vB::getRequest()->getTimeNow();
		$result = false;
		foreach ($existing as $node)
		{
			//Two possibilities. It might be unapproved, in which case we need moderate permissions. Or it might be unpublished,
			// in which case we need canpublish.

			//if (($node['publishdate'] < $timeNow) OR ($node['unpublishdate'] > 0))
			if (!$this->library->isPublished($node))
			{
				$this->inlinemodAuthCheck();

				if ($userContext->getChannelPermission('forumpermissions2', 'canpublish', $node['nodeid']))
				{
					$this->setPublishDate($node['nodeid'], $timeNow);
					$result = true;
				}
			}
			$currentApproved = (intval($node['approved']) > 0);

			if ($approved != $currentApproved)
			{
				//do we need to call setApproved? Not if we just have
				$approve = 1;
				$canModerateOwn = $userContext->getChannelPermission('forumpermissions2', 'canmanageownchannels', $node['nodeid']);

				// check if this is the owner of a blog that needs to moderate the comments
				if (!empty($moderateInfo['can']) OR ($canModerateOwn))
				{
					// let's get the channel node
					$channelid = vB_Library::instance('node')->getChannelId($node);

					if ($channelid == $node['nodeid'])
					{
						$channel = $node;
					}
					else
					{
						$channel = vB_Library::instance('node')->getNodeBare($channelid);
					}

					// this channel was created by the current user so we don't need the auth check
					if ((in_array($channelid, $moderateInfo['can'])) OR ($canModerateOwn AND ($channel['userid'] == $currentUserid)))
					{
						$approveNodeIds[] = $node['nodeid'];
						continue;
					}
				}

				// don't check permissions if the user is the recipient of the VM
				if (!empty($node['setfor']) AND ($node['setfor'] == $currentUserid) AND $userContext->hasPermission('visitormessagepermissions', 'canmanageownprofile'))
				{
					$approveNodeIds[] = $node['nodeid'];
				}
				else
				{
					$this->inlinemodAuthCheck();

					if ($userContext->getChannelPermission('moderatorpermissions', 'canmanagethreads', $node['nodeid']))
					{
						$approveNodeIds[] = $node['nodeid'];
					}
				}
			}
		}

		if (empty($approveNodeIds))
		{
			return $result;
		}

		return $this->library->setApproved($approveNodeIds, $approved);
	}


	/**
	 * Approves a post. Since the publish date might be affected user will need moderate and
	 * publish posts permissions.
	 *
	 * @param	int|array	Int Nodeid (or an array of int nodeids) for the node(s) we are approving.
	 * @param	int		Boolean used to set or unset the approved value
	 * @param	array	Optional array to update the node fields. Accepted fields are: title, text, reason
	 *
	 * @return	bool	Flag to indicate if approving went succesfully done (true/false).
	 */
	public function setApprovedPost($nodeid = false, $approved = false, $data = array())
	{
		$nodeid = vB::getCleaner()->clean($nodeid,  is_array($nodeid) ? vB_Cleaner::TYPE_ARRAY_UINT : vB_Cleaner::TYPE_UINT);

		$ret = $this->setApproved($nodeid, $approved);
		if (empty($data) OR (empty($data['title']) AND empty($data['text'])))
		{
			return $ret;
		}

		if (!intval($nodeid))
		{
			throw new vB_Exception_Api('invalid_node_id');
		}

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

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

		$data = array_intersect_key($data, array_flip('title', 'text', 'reason'));

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


	/**
	 * 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);
	}

	/** 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		mixed	$recipient		the userid/username of the member who will get the request
	 *	@param		string	$recipientname	(Optional) the username of the member who will get the request
	 *
	 *	@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)
	{
		if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canjoin', $channelid)
			OR !vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $channelid))
		{
			throw new vB_Exception_Api('no_permission');
		}

		return $this->library->requestChannel($channelid, $requestType, $recipient, $recipientname, vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canaddowners', $channelid));

	}
	/**
	 * @uses Fetch statistical indicators for Nodes by given interval
	 * @param int $nodeId The node we are fetching info of
	 * @param int $startdate Start range value, unix timestamp; DEFAULT to 2 months ago
	 * @param int $enddate End range value, unix timestamp; DEFAULT to now
	 * @param string $interval Representation of interval; {daily, monthly}
	 * @return array $response Array containing result statistics needed, values inside 'stats' key
	 */
	public function fetchNodeStats($nodeid, $interval = self::DATE_RANGE_DAILY, $page = 1)
	{
		$assertor = vB::getDbAssertor();
		$response = array();

		// channel mods can stats
		$perms = vB_Api::instanceInternal('blog')->getChannelAdminPerms($nodeid);
		if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid) AND !$perms['canstats'])
		{
			throw new vB_Exception_Api('no_permission');
		}
		$timestamp = vB::getRequest()->getTimeNow();
		$perpage = vB::getDatastore()->getOption('maxposts');
		if (empty($perpage))
		{
			$perpage = 20;
		}
		$cutdate = mktime(0,0,0,date('n') - 2, 1, date("Y"));
		if ($interval == self::DATE_RANGE_DAILY)
		{
			$total = vB::getDbAssertor()->getRow('vBForum:nodestats', array(
					vB_dB_Query::CONDITIONS_KEY=> array(
						array('field'=>'nodeid', 'value' => $nodeid, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_EQ),
						array('field'=>'dateline', 'value' => $cutdate, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_GTE)
					),
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_COUNT
			));
			if (($page - 1) * $perpage > $total['count'])
			{
				$page = ceil($total['count'] / $perpage);
			}
			$stats = array(
				'pagingInfo' => array(
						'page' => $page,
						'records' => $total['count'],
						'perpage' => $perpage,
						'totalpages' => ceil($total['count'] / $perpage)
				),
				'stats' => array()
			);
			$qry = vB::getDbAssertor()->assertQuery('vBForum:nodestats', array(
					vB_dB_Query::CONDITIONS_KEY=> array(
						array('field'=>'nodeid', 'value' => $nodeid, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_EQ),
						array('field'=>'dateline', 'value' => $cutdate, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_GTE)
					),
					vB_dB_Query::PARAM_LIMITSTART => ($page - 1) * $perpage, vB_dB_Query::PARAM_LIMIT => $perpage,
					//vB_dB_Query::DEBUG_QUERY => true
				),
				array('field' => 'dateline', 'direction' => vB_dB_Query::SORT_DESC)
			);
			foreach ($qry as $stat)
			{
				$day = date('m-d', $stat['dateline']);
				if (!empty($stats['stats'][$day]))
				{
					$stats['stats'][$day]['replies'] += $stat['replies'];
					$stats['stats'][$day]['visitors'] += $stat['visitors'];
				}
				else
				{
					$stats['stats'][$day] = $stat;
				}
			}
			return $stats;
		}
		else
		{
			$qry = vB::getDbAssertor()->assertQuery('vBForum:nodestats', array(
					vB_dB_Query::CONDITIONS_KEY=> array(
							array('field'=>'nodeid', 'value' => $nodeid, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_EQ),
							array('field'=>'dateline', 'value' => $cutdate, vB_Db_Query::OPERATOR_KEY => vB_Db_Query::OPERATOR_GTE)
					),
				),
				array('field' => 'dateline', 'direction' => vB_dB_Query::SORT_DESC)
			);
			$thismonth = date('Y-m');
			foreach ($qry as $stat)
			{
				$month = date('Y-m', $stat['dateline']);
				if (!empty($stats['stats'][$month]))
				{
					$stats['stats'][$month]['replies'] += $stat['replies'];
					$stats['stats'][$month]['visitors'] += $stat['visitors'];
				}
				else
				{
					$stats['stats'][$month] = $stat;
				}

				if ($month == $thismonth)
				{
					$stats['stats'][$thismonth]['upto'] = empty($stats['stats'][$month]['upto']) ? $stat['dateline'] : max($stat['dateline'], $stats['stats'][$month]['upto']);
				}
			}
			return $stats;
		}
	}

	/** Approves a channel request.
	*
	*  	@param	int		id of a request private message.
	*
	**/
	public function approveChannelRequest($messageId)
	{
		$userInfo =  vB::getCurrentSession()->fetch_userinfo();
		$userid = $userInfo['userid'];
		$assertor = vB::getDbAssertor();

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

		$sentto = $assertor->getRow('vBForum:sentto', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
					'nodeid' => $messageId, 'userid' => $userid));

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

		$messageApi = vB_Api::instanceInternal('content_privatemessage');
		$message = $messageApi->getContent($messageId);
		$message = $message[$messageId];

		if (($message['msgtype'] != 'request' ) OR empty($message['aboutid']))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		// if a user joins a channel as owner/moderator/member, check if they should automatically be subscribed
		$autoSubscribeMember = false;
		$node = $this->getNode($message['aboutid']);
		if ( ($node['nodeoptions'] & vB_Api_Node::OPTION_AUTOSUBSCRIBE_ON_JOIN) > 0)
		{
			$autoSubscribeMember = true;
		}

		switch ($message['about'])
		{
			case self::REQUEST_TAKE_MODERATOR:
			case self::REQUEST_SG_TAKE_MODERATOR:
				//Can we grant the transfer?
				$userContext = vB::getUserContext($message['userid']);
				if (!$userContext->getChannelPermission('moderatorpermissions', 'canaddowners', $message['aboutid'] ))
				{
					throw new vB_Exception_API('no_permission');
				}
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_MODERATOR_SYSGROUPID);
				$usergroupid = $usergroupInfo['usergroupid'];
				$permUserid = $userid;
				break;

			case self::REQUEST_TAKE_MEMBER:
			case self::REQUEST_SG_TAKE_MEMBER:
				$userContext = vB::getUserContext($message['userid']);
				if (!$userContext->getChannelPermission('moderatorpermissions', 'canaddowners', $message['aboutid'] ))
				{
					throw new vB_Exception_API('no_permission');
				}
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_MEMBER_SYSGROUPID);
				$usergroupid = $usergroupInfo['usergroupid'];
				$permUserid = $userid;
				break;

			case self::REQUEST_TAKE_OWNER:
			case self::REQUEST_SG_TAKE_OWNER:
				//Can we grant the transfer?
				$userContext = vB::getUserContext($message['userid']);
				if (!$userContext->getChannelPermission('moderatorpermissions', 'canaddowners', $message['aboutid'] ))
				{
					throw new vB_Exception_API('no_permission');
				}
				//We can't use the user api, because that checks the permissions.

				//let's get the current channels in which the user already is set for that group.
				//Then remove any for which they already are set.
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_OWNER_SYSGROUPID);

				$groupInTopic = vB_Api::instanceInternal('user')->getGroupInTopic($userid, $message['aboutid']);

				if ($groupInTopic AND in_array($usergroupInfo, $groupInTopic))
				{
					//This user already has this right
					$result = true;
					break;
				}
				//There is only one owner at a time, so we delete the current user;
				$result = $assertor->assertQuery('vBForum:groupintopic', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
					'nodeid' => $message['aboutid'], 'groupid' => $usergroupInfo['usergroupid']));

				//and do the inserts
				$result = $assertor->assertQuery('vBForum:groupintopic', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT,
					'userid' => $userid, 'nodeid' => $message['aboutid'], 'groupid' => $usergroupInfo['usergroupid']));

				// add subscription for new owner if autosubcribe is enabled
				if ( $autoSubscribeMember )
				{
					vB_Api::instanceInternal('follow')->add($message['aboutid'], vB_Api_Follow::FOLLOWTYPE_CHANNELS, $userid, true);
				}

				// Add membership for the old owner. Note the lack of auto-subscription check because an old owner
				// should already have a subscription if channel is auto-subscribe.
				$oldOwner = $node['userid'];
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_MEMBER_SYSGROUPID);
				vB_User::setGroupInTopic($oldOwner, $message['aboutid'], $usergroupInfo['usergroupid']);

				//replace the old owner in the node table as well
				$assertor->update('vBForum:node', array('userid' => $userid), array('nodeid' => $message['aboutid']));
				$myUserContext = vB::getUserContext();
				vB_Cache::instance()->allCacheEvent("userPerms_$userid" );
				// TODO: add node change event & check cache for blogs tab. Currently blogs tab doesn't update w/ the new owner.
				$myUserContext->clearChannelPermissions();
				$myUserContext->reloadGroupInTopic();
				$senderUserContext =  vB::getUserContext($message['userid']);
				$senderUserContext->clearChannelPermissions();
				$senderUserContext->reloadGroupInTopic();
				return true;
				break;

			case self::REQUEST_GRANT_OWNER:
			case self::REQUEST_SG_GRANT_OWNER:
				// these requests don't exist yet.
				throw new vB_Exception_API('invalid_action');
				// when these requests are implemented, be careful about what $permUserid below should be.
				// I think this request goes to the owner, so $permUserid has to be the sender of the message,
				// meaning we have to grab the node where nodeid = $message['nodeid'], & grab its author
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_OWNER_SYSGROUPID);
				$usergroupid = $usergroupInfo['usergroupid'];
				$permUserid = $message['userid'];
				break;
			case self::REQUEST_GRANT_MODERATOR:
			case self::REQUEST_SG_GRANT_MODERATOR:
				// these requests don't exist yet.
				throw new vB_Exception_API('invalid_action');
				// when these requests are implemented, be careful about what $permUserid below should be.
				// I think this request goes to the owner, so $permUserid has to be the author of the message,
				// meaning we have to grab the node where nodeid = $message['nodeid'], & grab its author
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_MODERATOR_SYSGROUPID);
				$usergroupid = $usergroupInfo['usergroupid'];
				$permUserid = $message['userid'];
				break;
			case self::REQUEST_GRANT_MEMBER:
			case self::REQUEST_SG_GRANT_MEMBER:
				$usergroupInfo = vB_Api::instanceInternal('usergroup')->fetchUsergroupBySystemID(vB_Api_UserGroup::CHANNEL_MEMBER_SYSGROUPID);
				$usergroupid = $usergroupInfo['usergroupid'];
				$permUserid = $message['userid'];
				break;
			default:
				throw new vB_Exception_API('invalid_data');
		} // switch

		$result = vB_User::setGroupInTopic($permUserid, $message['aboutid'], $usergroupid);

		// If automatic subscription is enabled for the channel (e.g. blogs by default) and if groupintopic is set
		// make the user follow the channel.
		if ( ($result === true) AND $autoSubscribeMember )
		{
			// The 4th param, $auto_subscribe = true, is required since an auto-subscribe probably doesn't come
			// with an existing subscriber request, only a join request.
			vB_Api::instanceInternal('follow')->add($message['aboutid'], vB_Api_Follow::FOLLOWTYPE_CHANNELS, $permUserid, true);
		}

		// I'm fairly certain that the below is incorrect. The (old) OWNER would receive the request, so $userid = $message['userid']..
		// It should probably compaer $userid with $permUserid, given that $permUserid is corrected (see above case statement)
		//last item- if we just granted owner to a new member we should remove anyone else & add the old owner as a regular member
		if (($message['userid'] != $userid) AND in_array($message['about'], array(self::REQUEST_GRANT_OWNER, self::REQUEST_SG_GRANT_OWNER)))
		{
			$result = $assertor->assertQuery('vBForum:groupintopic', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
				vB_dB_Query::CONDITIONS_KEY => array(
					array('field' => 'nodeid', 'value'=> $message['aboutid'], 'operator' => vB_dB_Query::OPERATOR_EQ),
					array('field' => 'groupid', 'value'=> $usergroupid, 'operator' => vB_dB_Query::OPERATOR_EQ),
					array('field' => 'userid', 'value'=> $message['userid'], 'operator' => vB_dB_Query::OPERATOR_NE))
				));

			// TODO: if this is ever corrected/implemented, be sure to add the old owner as a member of the channel

			//reset the recipient's permissions.
			$myUserContext = vB::getUserContext();
			vB_Cache::instance()->allCacheEvent("userPerms_$userid" );
			$myUserContext->clearChannelPermissions();
			$myUserContext->reloadGroupInTopic();
		}
		vB_Api::instanceInternal('user')->clearChannelPerms($userid);
		return true;
	}



	/** 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))
		{
			throw new vB_Exception_Api('invalid_request');
		}

		if (!vB::getUserContext()->getChannelPermission('forumpermissions2', 'canconfigchannel', $nodeid))
		{
			throw new vB_Exception_Api('no_permission');
		}

		return $this->library->setNodeOptions($nodeid, $options);
	}


	/** Set the node special permissions
	*
	 * 	@param	mixed	array with 'viewperms' and/or 'commentperms'
	*
	 * 	@return	either 1 or an error message.
	**/
	public function setNodePerms($nodeid, $perms = array())
	{
		if (empty($nodeid) OR !intval($nodeid) OR
			(!isset($perms['viewperms']) AND !isset($perms['commentperms'])))
		{
			throw new vB_Exception_Api('invalid_request');
		}

		if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canaddowners', $nodeid))
		{
			throw new vB_Exception_Api('no_permission');
		}

		$updates = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'nodeid' => $nodeid);
		$node = $this->getNode($nodeid);

		if (isset($perms['viewperms']) AND is_numeric($perms['viewperms']) AND in_array($perms['viewperms'], array(0,1,2)))
		{
			$updates['viewperms'] = $perms['viewperms'];
		}
		else
		{
			$updates['viewperms'] = $node['viewperms'];
		}


		if (isset($perms['commentperms']) AND is_numeric($perms['commentperms']) AND in_array($perms['commentperms'], array(0,1,2)))
		{
			$updates['commentperms'] = $perms['commentperms'];
		}
		else
		{
			$updates['commentperms'] = $node['commentperms'];
		}
		$result = vB::getDbAssertor()->assertQuery('vBForum:updateNodePerms', $updates);
		$this->clearCacheEvents($nodeid);
		$this->library->clearChildCache($nodeid);
		return $result;
	}

	/**
	 * Validates whether nodes can be merged and returns the merging info. If nodes cannot be merged, an error is returned.
	 * @param array $nodeIds
	 * @return array
	 */
	protected function validateMergePosts($nodeIds)
	{
		$nodes = $this->getContentForNodes($nodeIds);

		$response = array();
		$response['contenttypeclass'] = 'Text';

		$contentTypes = array();

		// check content type constraints
		foreach ($nodes AS $node)
		{
			$contentTypeClass = $node['contenttypeclass'];
			$contentTypes[$contentTypeClass] = isset($contentTypes[$contentTypeClass]) ? ($contentTypes[$contentTypeClass] + 1) : 1;

			// currently we cannot merge multiple links or videos
			if ($contentTypes[$contentTypeClass] > 1 AND !in_array($contentTypeClass, array('Text', 'Gallery')))
			{
				return array('error' => 'merge_invalid_contenttypes_multiple');
			}

			// meanwhile, gather the info from it...
			if ($contentTypeClass != 'Text')
			{
				$response['contenttypeclass'] = $contentTypeClass;
			}

			$response['destnodes'][$node['nodeid']] = $node;
			$response['destauthors'][$node['content']['userid']] = $node['content']['authorname'];
			// The mergeContentInfo method is checking view permissions to the $response
			$response['nodeid'] = $node['nodeid'];

			vB_Library::instance('content_' . $contentTypeClass)->mergeContentInfo($response, $node['content']);
		}

		if (count($contentTypes) > 1)
		{
			// we are merging different contenttypes.
			// If there are two, one must be Text.
			if (count($contentTypes) > 2 || !array_key_exists('Text', $contentTypes))
			{
				return array('error' => 'merge_invalid_contenttypes');
			}
		}

		// we are good to continue...
		asort($response['destauthors']);

		return $response;
	}

	/**
	 * Validates whether nodes can be merged and returns merging info.
	 * @param mixed $nodeIds
	 * @return array
	 */
	public function getMergePostsInfo($nodeIds)
	{
		if (empty($nodeIds))
		{
			throw new vB_Exception_Api('please_select_at_least_one_post');
		}
		else if (is_string($nodeIds))
		{
			$nodeIds = explode(',', $nodeIds);
		}

		$response = $this->validateMergePosts($nodeIds);

		if (isset($response['error']))
		{
			throw new vB_Exception_Api($response['error']);
		}
		else
		{
			return $response;
		}
	}

	/**
	 * Performs the actual merging, using edited input from UI.
	 * @param type $data - Contains pairs (value, name) from edit form in addition to the following fields:
	 *						* mergePosts - posts to be merged
	 *						* destnodeid - target post
	 *						* destauthorid - author to be used
	 *						* contenttype - target contenttype
	 */
	public function mergePosts($input)
	{
		$this->inlinemodAuthCheck();
		$cleaner = vB::getCleaner();
		$data = array();

		foreach ($input as $i)
		{
			$name = $cleaner->clean($i['name'], vB_Cleaner::TYPE_NOHTML);
			//mostly the data is either integer or string, although the possibility exists of a
			//'url_image'
			switch($name)
			{
				case 'nodeid':
				case 'url_nopreview':
				case 'nodeuserid':
				case 'filedataid':
				case 'destauthorid':
				case 'destnodeid':
					$value = $cleaner->clean($i['value'],vB_Cleaner::TYPE_UINT );
					break;
				case 'title':
				case 'text':
				case 'reason':
					$value = $cleaner->clean($i['value'],vB_Cleaner::TYPE_STR);
					break;
				case 'url_title':
				case 'authorname':
				case 'url':
				case 'url_image':
				case 'url_meta':
					$value = $cleaner->clean($i['value'],vB_Cleaner::TYPE_NOHTML );
					break;
				case 'mergePosts':
					if (!is_array($i['value']))
					{
						$i['value'] = explode(',', $i['value']) ;
					}
					$value = $cleaner->clean($i['value'],vB_Cleaner::TYPE_ARRAY_UINT);
					break;
				case 'filedataid[]' :
					//The filedata records are passed as
					//input[xx][name]	filedataid[]
					//input[xx][value]	<integer>
					$value = $cleaner->clean($i['value'],vB_Cleaner::TYPE_UINT );

					if (!isset($data['filedataid']))
					{
						$data['filedataid'] = array();
					}

					if (!isset($data['filedataid'][$value]))
					{
						$data['filedataid'][$value] = '';
					}
					continue;

				default:
					//The title records are passed as
					//input[xx][name]	title_<filedataid>
					//input[xx][value]	<title>
					if (empty($name))
					{
						continue;
					}

					if (substr($name, 0, 6) == 'title_')
					{
						$filedataid = substr($name, 6);
						$filedataid = $cleaner->clean($filedataid,vB_Cleaner::TYPE_UINT );

						if ($filedataid)
						{
							if (!isset($data['filedataid']))
							{
								$data['filedataid'] = array();
							}
							$data['filedataid'][$filedataid] = $cleaner->clean($i['value'],vB_Cleaner::TYPE_NOHTML );
						}
						continue;
					}
					else if (preg_match('#^videoitems\[([\d]+)#', $name, $matches))
					{
						if (!isset($data['videoitems']))
						{
							$data['videoitems'] = array();
						}
						$videoitems[] = array(
							'videoitemid' => intval($matches[1]),
							'url' => $i['value'],
						);
					}
					else if (preg_match('^videoitems\[new^', $name, $matches))
					{
						if (!isset($data['videoitems']))
						{
							$data['videoitems'] = array();
						}
						foreach ($matches as $video)
						{
							$data['videoitems'][] = array('url' => $video['url']);
						}
					}

					continue;
			}
			if (isset($data[$name]))
			{
				if (!is_array($data[$name]))
				{
					$data[$name] = array($data[$name]);
				}

				$data[$name][] = $value;
			}
			else
			{
				$data[$name] = $value;
			}
		}

		if (empty($data['mergePosts']))
		{
			throw new vB_Exception_Api('please_select_at_least_one_post');
		}

		// check that the user has permission
		$nodesToCheck = $data['mergePosts'];
		$nodesToCheck[] = $data['destnodeid'];
		foreach ($nodesToCheck AS $key => $nodeid)
		{
			// this is here just in case for some reason, a nodeid is 0. Shouldn't happen, but
			// I don't want getChannelPermission to go bonkers from it.
			if (empty($nodeid))
			{
				unset($nodesToCheck[$key]);
				continue;
			}

			if ( !vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmanagethreads', $nodeid) )
			{
				// perhaps we could generate a list of unmergeable nodes and return a warning instead, but
				// I don't think there's a real use case where a moderator can manage only *some* of the
				// nodes they're trying to merge. I think that would require multiple channels being involved, and
				// we don't have a UI for that so I can't test it. As such I'm just going to throw an exception if
				// *any* of the nodes fail the check.
				throw new vB_Exception_Api('no_permission');
			}
		}

		// validate that selected nodes can be merged
		$mergeInfo = $this->validateMergePosts($data['mergePosts']);

		if (isset($mergeInfo['error']))
		{
			throw new vB_Exception_Api($mergeInfo['error']);
		}

		// validate form fields
		if (empty($data['destnodeid']) || !array_key_exists($data['destnodeid'], $mergeInfo['destnodes']))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		if (empty($data['destauthorid']) || !array_key_exists($data['destauthorid'], $mergeInfo['destauthors']))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		$destnode = $this->library->getNodeFullContent($data['destnodeid']);
		$destnode = array_pop($destnode);

		if ($destnode['starter'] != $destnode['nodeid'] AND $destnode['starter'] != $destnode['parentid'])
		{
			if (isset($data['tags']))
			{
				unset($data['tags']);
			}
		}

		$type = vB_Types::instance()->getContentTypeClass($destnode['contenttypeid']);
		$response = vB_Library::instance("content_{$type}")->mergeContent($data);

		if ($response)
		{
			$sources = array_diff($data['mergePosts'], array($data['destnodeid']));

			$origDestnode = $destnode;

			if (!empty($destnode['rawtext']))
			{
				$origRawText = $destnode['rawtext'];
			}
			else if (!empty($destnode['content']['rawtext']))
			{
				$origRawText = $destnode['content']['rawtext'];
			}
			else
			{
				$origRawText = '';
			}
			$destnode = $this->getNode($data['destnodeid']);

			if (!empty($destnode['rawtext']))
			{
				$rawText = $destnode['rawtext'];
			}
			else if (!empty($destnode['content']['rawtext']))
			{
				$rawText = $destnode['content']['rawtext'];
			}
			else
			{
				$rawText = '';
			}
			$destnode = $this->getNode($data['destnodeid']);

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

			// move children to target node
			$children = vB::getDbAssertor()->assertQuery('vBForum:closure', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'parent' => $sources,
				'depth' => 1,
			));

			$childrenIds = array();
			foreach ($children AS $child)
			{
				$childrenIds[] = $child['child'];
			}

			if (!empty($childrenIds))
			{
				$this->moveNodes($childrenIds, $data['destnodeid'], false, false, false);
			}

			// remove merged nodes
			$this->deleteNodes($sources, true, null, false); //  Dont log the deletes

			$loginfo['action'] = array('merged_nodes' => implode(',' , $sources));
			$vboptions = vB::getDatastore()->getValue('options');
			if (
				(
					vB_Api::instanceInternal('user')->hasPermissions('genericoptions', 'showeditedby')
						AND
					$destnode['publishdate'] > 0
						AND
					$destnode['publishdate'] < (vB::getRequest()->getTimeNow() - ($vboptions['noeditedbytime'] * 60))
				)
					OR
				!empty($data['reason'])
			)
			{

				$userinfo = vB::getCurrentSession()->fetch_userinfo();
				if ($vboptions['postedithistory'])
				{
					$record = vB::getDbAssertor()->getRow('vBForum:postedithistory',
						array(
							vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
							'original' => 1,
							'nodeid'   => $destnode['nodeid']
					));
					// insert original post on first edit
					if (empty($record))
					{
						vB::getDbAssertor()->assertQuery('vBForum:postedithistory', array(
							vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT,
							'nodeid'   => $origDestnode['nodeid'],
							'userid'   => $origDestnode['userid'],
							'username' => $origDestnode['authorname'],
							'dateline' => $origDestnode['publishdate'],
							'pagetext' => $origRawText,
							'original' => 1,
						));
					}
					// insert the new version
					vB::getDbAssertor()->assertQuery('vBForum:postedithistory', array(
						vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT,
						'nodeid'   => $destnode['nodeid'],
						'userid'   => $userinfo['userid'],
						'username' => $userinfo['username'],
						'dateline' => vB::getRequest()->getTimeNow(),
						'reason'   => isset($data['reason']) ? vB5_String::htmlSpecialCharsUni($data['reason']) : '',
						'pagetext' => $rawText
					));
				}

				vB::getDbAssertor()->assertQuery('editlog_replacerecord', array(
					'nodeid'     => $destnode['nodeid'],
					'userid'     => $userinfo['userid'],
					'username'   => $userinfo['username'],
					'timenow'    => vB::getRequest()->getTimeNow(),
					'reason'     => isset($data['reason']) ? vB5_String::htmlSpecialCharsUni($data['reason']) : '',
					'hashistory' => intval($vboptions['postedithistory'])
				));
			}

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

			return true;
		}

	}

	/** gets the node option bitfields
	 *
	 * 	@return	array 	associative array of bitfield name => value
	 **/
	public function getOptions()
	{
		return $this->options;
	}

	/**
	 * 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($options = array())
	{

		return $this->library->listPendingPosts(vB::getCurrentSession()->get('userid'), $options);
	}

	/**
	 * Function wrapper for listPendingPosts but used for current user.
	 *
	 */
	public function listPendingPostsForCurrentUser($options = array())
	{
		$userId = vB::getCurrentSession()->get('userid');

		if (!$userId)
		{
			return array();
		}
		return $this->library->listPendingPostsForCurrentUser($options);
	}


	/**
	 * 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)
	{
		return $this->library->clearCacheEvents($nodeIds);
	}
	/**
	* Marks a node as read using the appropriate method.
	*
	* @param int $nodeid The ID of node being marked
	*
	* @return	array	Returns an array of nodes that were marked as read
	*/
	public function markRead($nodeid)
	{
		return $this->library->markRead($nodeid);
	}

	/**
	 * Mark multiple nodes read
	 *
	 * @param $nodeids Node Ids
	 *
	 * @return	array	Returns an array of nodes that were marked as read
	 */
	public function markReadMultiple($nodeids)
	{
		return $this->library->markReadMultiple($nodeids);
	}

	/**
	 * Mark multiple nodes unread
	 *
	 * @param $nodeids Node Ids
	 *
	 * @return	array	Returns an array of nodes that were marked as unread
	 */
	public function markUnreadMultiple($nodeids)
	{
		return $this->library->markUnreadMultiple($nodeids);
	}

	/**
	* 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)
	{
		return $this->library->markChannelsRead($nodeid);
	}

	/**
	 * Fetches the moderator logs for a node
	 * @param int $nodeid
	 * @return array $logs list of log records
	 */
	public function fetchModLogs($nodeid)
	{
		if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid))
		{
			$node = $this->getNode($nodeid);
			if ($node['userid'] != vB::getCurrentSession()->get('userid'))
			{
				throw new vB_Exception_Api('no_permission');
			}
			// do not throw an error if the author requests logs, just don't show them to it.
			else
			{
				return array();
			}
		}

		$logs = array();
		$log_res = vB::getDbAssertor()->assertQuery('getModLogs', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'nodeid' => $nodeid));

		foreach ($log_res as $log)
		{
			$phrase_name = vB_Library_Admin::GetModlogAction($log['type']);
			$phrase = vB_Api::instanceInternal('phrase')->fetch($phrase_name);
			if (!isset($phrase[$phrase_name]))
			{
				continue;
			}
			$phrase = $phrase[$phrase_name];
			$log['action'] = vsprintf($phrase, $log['username']);
			$logs[] = $log;
		}
		return $logs;
	}

	/**
	 * Manages a deleted node regarding options being passed.
	 *
	 * @param 	int 	Nodeid.
	 * @param 	array 	Options for the node managing.
	 * 					keys -- deletetype, reason, iconid, prefixid, topictitle, option_open, option_sticky, option_visible
	 *
	 * @return 	array 	Whether manage action succeeded or not.
	 *					keys -- success
	 *
	 */
	public function manageDeletedNode($nodeid, $params)
	{
		$currentnode = vB_Api::instanceInternal('node')->getNode($nodeid);
		if (empty($currentnode))
		{
			throw new vB_Exception_Api('invalid_target');
		}

		if ($currentnode['userid'] != vB::getCurrentSession()->get('userid'))
		{
			$this->inlinemodAuthCheck();
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $nodeid))
			{
				throw new vB_Exception_Api('no_permission');
			}
		}

		// hard deleting the node. There is no point doing anything else beyond this
		if (!empty($params['deletetype']) AND $params['deletetype'] == 3)
		{
			$this->deleteNodes($nodeid, true, empty($params['reason']) ? '' : $params['reason']);
			return array('success' => true);
		}


		$is_topic = ($currentnode['starter'] == $currentnode['nodeid']);
		$is_reply = ($currentnode['starter'] == $currentnode['parentid']);

		$nodeFields = array();

		if (!empty($params['reason']))
		{
			$nodeFields['deletereason'] = $params['reason'];
		}

		if ($is_topic AND $params['post_icon'] != $currentnode['iconid'])
		{
			$nodeFields['iconid'] = $params['post_icon'];
		}

		if($is_topic AND $params['prefixid'] != $currentnode['prefixid'])
		{
			$nodeFields['prefixid'] = $params['prefixid'];
		}

		if ($is_topic AND !empty($params['topictitle']) AND $params['topictitle'] != $currentnode['title'])
		{
			$nodeFields['title'] = $params['topictitle'];
		}
		//updating the node if needed
		if (!empty($nodeFields))
		{
			$nodeFields['parentid'] = $currentnode['parentid'];
			vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($currentnode['contenttypeid']))->update($nodeid, $nodeFields);
		}

		// add moderation note
		if ($is_topic AND !empty($params['moderator_notes']))
		{
			$userinfo = vB::getCurrentSession()->fetch_userinfo();
			$route = 'profile';
			if (class_exists('vB5_Cookie') AND vB5_Cookie::isEnabled())
			{
				// session is stored in cookies, so do not append it to url
				$route .= '|nosession';
			}
		}

		// undelete
		if (!empty($params['deletetype']) AND $params['deletetype'] == 2)
		{
			$this->undeleteNodes($nodeid);
		}

		if (isset($params['deletetype']) AND ($params['deletetype'] == 0 OR $params['deletetype'] == 2))
		{
			if ($is_topic)
			{
				if (!empty($params['option_open']) AND !$currentnode['open'])
				{
					$this->openNode($nodeid);
				}
				elseif (empty($params['option_open']) AND $currentnode['open'])
				{
					$this->closeNode($nodeid);
				}

				if (!empty($params['option_sticky']) AND !$currentnode['sticky'])
				{
					$this->setSticky($nodeid, true);
				}
				elseif (empty($params['option_sticky']) AND $currentnode['sticky'])
				{
					$this->setSticky($nodeid, false);
				}

				if (!empty($params['option_visible']) AND !$currentnode['approved'])
				{
					$this->setApproved($nodeid, true);
				}
				elseif (empty($params['option_visible']) AND $currentnode['approved'])
				{
					$this->setApproved($nodeid, false);
				}
			}

			return array('success' => true);
		}

		//delete attachments
		if (!empty($params['deletetype']) AND $params['deletetype'] == 1 AND empty($params['keep_attachments']))
		{
			$attachments = $this->getNodeAttachments($nodeid);
			if (!empty($attachments))
			{
				$this->deleteNodes(array_keys($attachments), true, empty($params['reason']) ? '' : $params['reason']);
			}
		}

		return array('success' => true);
	}

	/**
	 * Check whether current logged-in user is "authenticated" for moderation actions.
	 * If the user is a moderator but not "authenticated", this will throw an exception.
	 * If the user is not a moderator, this won't throw the exception!
	 *
	 * @throws vB_Exception_Api inlinemodauth_required if we need user to login again
	 */
	protected function inlinemodAuthCheck()
	{
		$session = vB::getCurrentSession();

		if (!$session->validateCpsession())
		{
			throw new vB_Exception_Api('inlinemodauth_required');
		}
	}

	/**
	 * Checks if ip should be shown
	 *
	 * @return	bool	If the ip of the poster should be posted or not
	 */
	public function showIp($nodeid)
	{
		$logip = vB::getDatastore()->getOption('logip');
		if ($logip == 2)
		{
			return true;
		}
		else if ($logip == 1 AND vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canviewips', $nodeid))
		{
			return true;
		}

		return false;
	}

	public function getHostName($ip)
	{
		return @gethostbyaddr($ip);
	}

	/**
	 * Fetch userids from given nodeids
	 * @deprecated since version 5.0.3
	 * @param array $nodeids Node IDs
	 * @return array User IDs.
	 */
	public function fetchUseridsFromNodeids($nodeids)
	{
		return array_keys($this->fetchUsersFromNodeids($nodeids));
	}

	/**
	 * Fetch users from given nodeids
	 * @param array $nodeids Node IDs
	 * @return array User IDs and names.
	 */
	public function fetchUsersFromNodeids($nodeids)
	{
		foreach ($nodeids as &$nodeid)
		{
			$nodeid = intval($nodeid);
		}
		$nodes = vB_Library::instance('node')->getNodes($nodeids);

		$userids = array();
		foreach ($nodes as $node)
		{
			if ($node['inlist'] == 1 AND !in_array($node['userid'], $userids))
			{
				$userids[$node['userid']] = $node['authorname'];
			}
		}

		return $userids;
	}


	/**
	 * Fetch node tree structure of the specified parent id as the root. Root is excluded from the tree structure.
	 * If parentid is not specified or less than 1, it is automatically set to the top-most level special category "Forum"
	 *
	 * @param integer $parentid
	 * @param integer $depth
	 * @param integer $pagenum
	 * @param integer $perpage
	 * @param integer $options
	 */
	public function fetchChannelNodeTree($parentid = 0, $depth = 3, $pagenum = 1, $perpage = 20)
	{
		/**This is a very expensive function. Therefore we should cache at three levels.
		 * First- if asked at root, we cache a copy of the for this user without the "last" data.
		 Then we have a copy of the structure for this user but with no "last".
		 Then we have a copy of the entire structure.
		 These all have a lifetime of one day, with appropriate events.
		 */
		$userid = vB::getCurrentSession()->get('userid');

		//If we are not passed a parent, we default to main forum.
		if ($parentid < 1)
		{
			$topChannels = vB_Api::instanceInternal('content_channel')->fetchTopLevelChannelIds();
 			$parentid = $topChannels['forum'];
		}
		//See if we have a cached version
		$userTree = $this->getUserChannels($userid);
		$complete = $this->addChannelLastData();
		$tree =  $this->assembleTree($userTree, $complete, $parentid, $depth, $userid);
		$tree['root'] = $parentid;
		return $tree;
	}

	/**
	 * Fetches a list of channels that guests can access. Basically a public access for getUserChannels()
	 *	but only for userid 0.
	 */
	public function getGuestChannels()
	{
		return $this->getUserChannels(0);
	}

	protected function getUserChannels($userid)
	{
		$rawTree = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read("vB_ChannelTreeRaw_$userid");

		if ($rawTree === FALSE)
		{
			$rawTree = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read("vB_ChannelTree_Total");
			if (empty($rawTree))
			{
				$rawTree = $this->getChannelTree($userid);
			}
			//We need to do three things here- filter out nodes this user can't see at all, and flag those where they can only see their own.
			$channelPerms = vB::getUserContext($userid)->getAllChannelAccess();
			//this needs to be filtered recursively.
			foreach($rawTree AS $index => $channel)
			{
				if (isset($channelPerms['selfonly'][$channel['nodeid']]) OR isset($channelPerms['starteronly'][$channel['nodeid']]))
				{
					$rawTree[$index]['noDetail'] = true;
					$rawTree[$index]['readtime'] = 0;
				}
				else if (!isset($channelPerms['canview'][$channel['nodeid']]) AND !isset($channelPerms['canalwaysview'][$channel['nodeid']])
					AND !isset($channelPerms['canmoderate'][$channel['nodeid']]))
				{
					unset($rawTree[$index]);
				}
				else
				{
					$rawTree[$index]['readtime'] = 0;
				}
			}
		}
		vB_Cache::instance(vB_Cache::CACHE_LARGE)->write("vB_ChannelTreeRaw_$userid", $rawTree, 1440,
			array('vB_ChannelStructure_chg', 'perms_changed', "userPerms_$userid"));
		return $rawTree;
	}

	/** This takes the total tree and adds 'last' data to it */
	protected function addChannelLastData()
	{
		$cacheKey = 'vB_ChTreeWithLast';
		$cacheLife = vB::getDatastore()->getOption('channeltreelife');

		if (!isset($cacheLife))
		{
			$cacheLife = 1;
		}

		if ( $cacheLife > 0 )
		{
			$channels = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read($cacheKey);
			if (!empty($channels))
			{
				return $channels;
			}
		}
		$channels = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read("vB_ChannelTree_Total");

		if (empty($channels))
		{
			$channels = $this->getChannelTree();
		}
		$nodes = $this->library->getNodes(array_keys($channels));
		$lastNodes = array();
		//Let's put avatars in the array so the template doesn't need to fetch them.
		$authors = array();
		foreach ($nodes AS $node)
		{
			if (!empty($channels[$node['nodeid']]))
			{
				$channels[$node['nodeid']]['textcount'] = $node['textcount'];
				$channels[$node['nodeid']]['totalcount'] = $node['totalcount'];

				if (empty($channels[$node['nodeid']]['noDetail']))
				{
					$channels[$node['nodeid']]['viewing'] = 0; //@TODO: is the number of 'viewing' users implemented in api?

					$channels[$node['nodeid']]['lastcontent'] = array ('nodeid' => $node['lastcontentid'],
						'authorname' => $node['lastcontentauthor'], 'userid' => $node['lastauthorid'], 'starter' => array(), 'created' => $node['lastcontent']);
					$authors[$node['lastauthorid']] = $node['lastauthorid'];
					$lastNodes[] = $node['lastcontentid'];
				}
				$channels[$node['nodeid']]['topics'] = $node['textcount'];
			}

		}

		//we preload the user information. That way the
		$avatars = vB_Api::instanceInternal('user')->fetchAvatars($authors, true);

		//Now we need to build the 'starter' array. since the node has the same route as its starter.
		$lastNodes =  $this->library->getNodes($lastNodes);
		//getNodes returns the array with nodeid as the key, which is useful here.
		$starters = array();
		foreach($lastNodes AS $lastNode)
		{
			if ($lastNode['starter'] != $lastNode['nodeid'])
			{
				$starters[] = $lastNode['starter'];
			}
		}

		if (!empty($starters))
		{
			$starters = $this->library->getNodes($starters);
			foreach($lastNodes AS $index => $lastNode)
			{
				if (isset($starters[$lastNode['starter']]))
				{
					$lastNodes[$index] = $starters[$lastNode['starter']];
				}
			}
		}
		//now we can populate the starters sub-array, add avatars,
		//and do the totals. We couldn't until we know we have the table fully populated.
		foreach ($channels AS $nodeid => $channel)
		{
			if (empty($channel['noDetail']))
			{
				if (!empty($channel['lastcontent']['nodeid']) AND isset($lastNodes[$channel['lastcontent']['nodeid']])
					AND ($lastNodes[$channel['lastcontent']['nodeid']]['parentid'] == $nodeid))
				{
					$lastnode = $lastNodes[$channel['lastcontent']['nodeid']];
					$channels[$nodeid]['lastcontent']['starter'] = array('nodeid' => $lastnode['nodeid'],
						'routeid' => $lastnode['routeid'],
						'title' => $lastnode['title'], 'channelid' => $lastnode['parentid']);
					$channels[$nodeid]['lastcontent']['title'] = $lastnode['title'];

					if ($channel['lastcontent']['userid'] AND !empty($avatars[$channel['lastcontent']['userid']]))
					{
						$channels[$nodeid]['avatar'] = $avatars[$channel['lastcontent']['userid']];
					}
					else
					{
						$channels[$nodeid]['avatar'] = array();
					}
				}
			}
		}

		if ($cacheLife > 0 )
		{
			vB_Cache::instance(vB_Cache::CACHE_LARGE)->write($cacheKey, $channels, $cacheLife, array('vB_ChannelStructure_chg'));
		}
		return $channels;
	}

	protected function assembleTree(&$userTree, &$channels, $rootid, $depth, $userid)
	{
		if (empty($userTree) OR !is_array($userTree) OR !isset($userTree[$rootid]))
		{
			return false;
		}

		if (intval($userid))
		{
			$readData = vB::getDbAssertor()->assertQuery('noderead', array( vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				'userid' => $userid, 'nodeid'  => array_keys($userTree)));

			if (!empty($readData))
			{
				foreach($readData AS $noderead)
				{
					$userTree[$noderead['nodeid']]['readtime'] = $noderead['readtime'];
				}
			}
		}

		foreach ($userTree as $key => $node)
		{
			if (!empty($node['noDetail']))
			{
				$channels[$node['nodeid']]['lastcontent'] = array ();
			}
		}

		//Now the final assembly
		$result = array('channels' => array());
		$root = $userTree[$rootid];
		$startsAt = $root['depth'];

		if (empty($root['children']))
		{
			return;
		}

		//add the correct totals
		foreach($root['children'] AS $childid)
		{
			if (!empty($userTree[$childid]) AND !empty($channels[$childid]['displayorder']))
			{
				$this->addChannelTotals($channels, $userTree, $childid);
			}
		}

		foreach($root['children'] AS $childid)
		{
			if (!empty($userTree[$childid]) AND !empty($channels[$childid]['displayorder']))
			{
				$result['channels'][$childid] = $channels[$childid];

				if (!empty($result['channels'][$childid]) AND !empty($result['channels'][$childid]['starter']) AND
					($result['channels'][$childid]['starter']['channelid'] != $childid ))
				{
					$result['channels'][$childid]['lastcontent']['posts'] =  $channels[$childid]['textcount'];
				}
				$this->addFinalBranches($result['channels'][$childid], $channels, $userTree, $startsAt + $depth, $userid);
			}
		}

		return $result;
	}

	protected function addFinalBranches(&$trunk, &$tree, &$userTree, $maxDepth, $userid)
	{
		static $oldestNew;

		if (!isset($oldestNew))
		{
			$oldestNew = vB::getRequest()->getTimeNow() - (vB::getDatastore()->getOption('markinglimit') * 86400);
		}

		if (isset($userTree[$trunk['nodeid']]) )
		{
			if (isset($userTree[$trunk['nodeid']]['readtime']))
			{
				$trunk['readtime'] = $userTree[$trunk['nodeid']]['readtime'];
			}
		}
		$trunk['topics'] = $userTree[$trunk['nodeid']]['topics'];
		$trunk['posts'] = $userTree[$trunk['nodeid']]['posts'];

		if (!empty($trunk['children']) AND ($trunk['depth'] <= $maxDepth))
		{
			$lastContent = array();
			foreach ($trunk['children'] AS $subchannel)
			{
				if (isset($userTree[$subchannel]) AND ($trunk['nodeid'] !== $subchannel) AND ($tree[$subchannel]['displayorder'] > 0))
				{
					if (!isset($trunk['subchannels']))
					{
						$trunk['subchannels'] = array();
					}
					$trunk['subchannels'][$subchannel] = $tree[$subchannel];
					$this->addFinalBranches($trunk['subchannels'][$subchannel], $tree, $userTree, $maxDepth, $userid);

					if (!empty($trunk['subchannels'][$subchannel]['lastcontent'])  AND
						!empty($trunk['subchannels'][$subchannel]['lastcontent']['created']) AND
							(empty($lastContent) OR ($lastContent['created'] < $tree[$subchannel]['lastcontent']['created'])) )
					{
						$lastContent = $trunk['subchannels'][$subchannel]['lastcontent'];
					}
				}
			}

			if (!empty($lastContent) AND
				(empty($trunk['lastcontent']) OR empty($trunk['lastcontent']['created']) OR empty($trunk['lastcontent']['starter'])
					OR ($lastContent['created'] > $trunk['lastcontent']['created'])))
			{
				$trunk['lastcontent'] = $lastContent;
			}
		}

		if (!empty($trunk['lastcontent']) AND ($trunk['lastcontent']['created'] < $oldestNew))
		{
			$trunk['is_new'] = 0;
		}
		else if (!intval($userid) OR (empty($trunk['readtime']) OR (!empty($trunk['lastcontent']) AND ($trunk['lastcontent']['created'] > $trunk['readtime']))))
		{
			$trunk['is_new'] = 1;
		}
		else
		{
			$trunk['is_new'] = 0;
		}

		// rss info
		$type = vB_Api_External::TYPE_RSS2;
		$rssinfo = vB_Library::instance('external')->getExternalDataForChannels(array($trunk['nodeid']), $type);
		$trunk['rss_enabled'] = $rssinfo[$trunk['nodeid']][$type . '_enabled'];
		$trunk['rss_route'] = $rssinfo[$trunk['nodeid']][$type . '_route'];

		//This isn't needed any more.
		unset($trunk['children']);
	}

	/** This function builds the basic channel tree. This is global, without reference to depth or user permissions.

	 	@return		mixed	array of with nodeid => array of node information including ancestry, etc.
	 */
	protected function getChannelTree()
	{
		$channelList = array();
		//First let's get all the channels
		$rawChannels = vB::getDbAssertor()->getRows('vBForum:getChannelTree', array());
		/* We are sorted by parentid, and the normal simple case will be that the node tree is clean and (since children
		can't be created until their parents exist, the child nodes come after their parents. So in that case we simply scan the
		table once.

		 BUT...
		there are two possible problem cases. First, somehow we got an orphan record. We have to recover from that case.
		Second, somebody moved a channel to a node created after it, therefore a higher nodeid. In that case we will have to
		scan the array twice. So.. we'll keep a "rescan" flag- if we skipped anything, we rescan. */

		$rescan = true;
		//First we need to insert the root node.
		foreach($rawChannels AS $index => $channel)
		{
			if ($channel['parentid'] == 0)
			{
				$channel['children'] = array();
				$channel['depth'] = 0;
				$channel['parents'] = array($channel['nodeid']);
				$channelList[$channel['nodeid']] = $channel;
				unset($rawChannels[$index]);
				break;
			}
		}

		while ($rescan AND !empty($rawChannels))
		{
			$rescan = false;
			foreach($rawChannels AS $index => $rawChannel)
			{
				if (isset($channelList[$rawChannel['parentid']]))
				{
					$channel = $rawChannel;
					$rescan = true;
					$channel['children'] = array();
					$channel['parents'] = array($channel['nodeid']);
					foreach($channelList[$channel['parentid']]['parents'] AS $parentid)
					{
						$channel['parents'][] = $parentid;
					}
					$channel['depth'] = $channelList[$channel['parentid']]['depth'] + 1;
					$channelList[$channel['parentid']]['children'][$channel['nodeid']] = $channel['nodeid'];
					$channel['parent'] = $channel['parentid'];
					$channelList[$channel['nodeid']] = $channel;
					unset($rawChannels[$index]);
				}
			}
		}
		vB_Cache::instance(vB_Cache::CACHE_LARGE)->write('vB_ChannelTree_Total', $channelList, 1440, array('vB_ChannelStructure_chg'));
		return $channelList;
	}

	/** This starts at the bottom and sums the totalcount and textcount fields to topics and posts in the userChannels.
	 *
	 * @param mixed		array of all channel records
	 * @param mixed		array of channel records for the current user only
	 * @param mixed		a reference to the channel we are currently processing.
	 */
	protected function addChannelTotals(&$channels, &$userChannels, $channelid)
	{
		/** This is a complex task. The user should see only totals that they could view. A
		 * parent channel may have both subchannels and direct totals.  So we keep a total four totals starting from bottom- textcount, totalcount,
		 * viewtotal. viewtext. The difference between textcount for the children and textcount for the parent is the number of posts directly on the parent.
		 *
		 * We call this recursively to get the totals, which we add to $userChannels.
		 */
		$textcount = 0;
		$totalcount = 0;
		$viewTotal = 0;
		$viewtext = 0;

		if (!empty($channels[$channelid]['children']))
		{
			foreach($channels[$channelid]['children'] AS $childid)
			{
				$textcount += $channels[$childid]['textcount'];
				$totalcount += $channels[$childid]['totalcount'];

				//We only add if the current user can see this.
				if (!empty($userChannels[$childid]))
				{
					$this->addChannelTotals($channels, $userChannels, $childid);
					$viewTotal += $userChannels[$childid]['posts'];
					$viewtext += $userChannels[$childid]['topics'];
				}
			}
		}
		$userChannels[$channelid]['topics'] = $channels[$channelid]['textcount'] + $viewtext;
		$userChannels[$channelid]['posts'] = max($channels[$channelid]['totalcount'] + $viewTotal - $totalcount, 0);
	}


	public function fetchChannelDetails($nodes, $nodesOnly = array())
	{
		$userContext = vB::getUserContext();
		//We want some detail on the lastcontent record
		$lastIds = array();
		foreach ($nodes AS $node)
		{
			//There might be some channels for which this user can't see content.
			$channels[$node['nodeid']] = array(
				'nodeid' 		=> $node['nodeid'],
				'routeid' 		=> $node['routeid'],
				'title'			=> $node['title'],
				'description'	=> $node['description'],
				'parentid' 		=> $node['parentid'],
				'textcount'		=> $node['textcount'],
				'totalcount'	=> $node['totalcount'],
				'viewing'		=> 0, //@TODO: is the number of 'viewing' users implemented in api?
				'readtime' 		=> isset($node['readtime']) ? $node['readtime'] : null,
				'is_new'		=> empty($node['readtime']) OR (!empty($node['lastcontent']) AND $node['lastcontent'] > $node['readtime']),
				'category' 	    => $node['content']['category'],
				'displayorder'  => $node['displayorder'],
				'parents'  		=> $node['parents'],
				'subchannels' 	=> array(),
			);

			if (array_key_exists($node['nodeid'], $nodesOnly)
				OR (
					($node['lastcontentid'] > 0) AND
					(!$userContext->getChannelPermission('forumpermissions', 'canview', $node['lastcontentid']) OR
					 !$userContext->getChannelPermission('forumpermissions', 'canviewthreads', $node['lastcontentid']) OR
					 !$userContext->getChannelPermission('forumpermissions', 'canviewothers', $node['lastcontentid']))
				)
			)
			{
				$channels[$node['nodeid']]['lastcontent'] = array(
				'nodeid'	=> 0,
				'title'		=> '',
				'authorname'=> '',
				'userid'	=> '',
				'starter'	=> array());

			}
			else
			{
				if (($node['lastcontentid'] > 0) AND !array_key_exists($node['lastcontentid'], $lastIds))
				{
					$lastIds[$node['lastcontentid']] = $node['lastcontentid'];
				}

				$channels[$node['nodeid']]['lastcontent'] = array(
				'nodeid'	=> $node['lastcontentid'],
				'title'		=> '',
				'authorname'=> $node['lastcontentauthor'],
				'userid'	=> $node['lastauthorid'],
				'starter'	=> array());
			}
			if (!empty($node['readtime']))
			{
				$channels[$node['nodeid']]['readtime'] = $node['readtime'];
			}
		}

		unset($nodes);
		//first we get the lastcontent record.
		$nodes = $this->getNodes($lastIds);

		// Check if we have any prefixes
		$phrasevars = array();
		foreach ($nodes as $node)
		{
			if (!empty($node['prefixid']))
			{
				$phrasevars[] = 'prefix_' .  $node['prefixid'] . '_title_plain';
				$phrasevars[] = 'prefix_' .  $node['prefixid'] . '_title_rich';
			}
		}
		$phrases = array();
		if ($phrasevars)
		{
			$phrases = vB_Api::instanceInternal('phrase')->fetch($phrasevars);
		}

		$lastIds = array();
		foreach ($channels AS $key => $channel)
		{
			$nodeid = $channel['lastcontent']['nodeid'];

			if (isset($nodes[$nodeid]))
			{
				$node =& $nodes[$nodeid];
				$channels[$key]['lastcontent']['title'] = $node['title'];
				$channels[$key]['lastcontent']['created'] = $node['created'];
				$channels[$key]['lastcontent']['parentid'] = $node['parentid'];
				//if this is a starter we have all the information we need.
				$channels[$key]['lastcontent']['starter']['nodeid'] = $node['starter'];

				if ($node['starter'] == $node['nodeid'])
				{
					$channels[$key]['lastcontent']['starter']['routeid'] = $node['routeid'];
					$channels[$key]['lastcontent']['starter']['title'] = $node['title'];
				}
				else
				{
					//We need another query
					$lastIds[$node['starter']] = $node['starter'];
				}

				if (!empty($node['prefixid']))
				{
					$channels[$key]['lastcontent']['starter']['prefixid'] = $node['prefixid'];
					if (!empty($phrases['prefix_' .  $node['prefixid'] . '_title_plain']))
					{
						$node['prefix_plain'] = $phrases['prefix_' .  $node['prefixid'] . '_title_plain'];
					}
					if (!empty($phrases['prefix_' .  $node['prefixid'] . '_title_rich']))
					{
						$node['prefix_rich'] = $phrases['prefix_' .  $node['prefixid'] . '_title_rich'];
					}
				}

				if (!empty($node['prefix_rich']))
				{
					$channels[$key]['lastcontent']['starter']['prefix_rich'] = $node['prefix_rich'];
				}

				if (!empty($node['prefix_plain']))
				{
					$channels[$key]['lastcontent']['starter']['prefix_plain'] = $node['prefix_plain'];
				}
			}
		}

		//Now get any lastcontent starter information we need
		if (!empty ($lastIds))
		{
			$nodes = $this->getNodes($lastIds);
			foreach ($channels AS $channelId => $channel)
			{
				$nodeid = $channels[$channelId]['lastcontent']['starter']['nodeid'];
				if (isset($nodes[$nodeid]))
				{
					$node =& $nodes[$nodeid];
					$channels[$channelId]['lastcontent']['starter']['routeid'] = $node['routeid'];
					$channels[$channelId]['lastcontent']['starter']['title'] = $node['title'];
				}
			}
		}

		return $channels;
	}

	/**
	 * Returns the node read time for the current user and the given nodeid
	 *
	 * @param	int	Node id
	 *
	 * @return	int	Read time for the node
	 */
	public function getNodeReadTime($nodeid)
	{
		$nodeid = (int) $nodeid;

		$user = vB::getCurrentSession()->fetch_userinfo();

		$nodeRead = vB::getDbAssertor()->getRow('noderead', array(
			'userid' => $user['userid'],
			'nodeid' => $nodeid,
		));

		return $nodeRead['readtime'];
	}

	/**
	 * Returns the first immediate child node of the given node that was created
	 * after the given timestamp
	 *
	 * @param	int	Parent Node ID
	 * @param	int	Time stamp
	 *
	 * @return	int	Node ID
	 */
	public function getFirstChildAfterTime($parentNodeId, $timeStamp)
	{
		$parentNodeId = (int) $parentNodeId;
		$timeStamp = (int) $timeStamp;

		while(true)
		{
			$newReplies = vB::getDbAssertor()->getRows('vBForum:getRepliesAfterCutoff', array(
				'starter' => $parentNodeId,
				'cutoff' => $timeStamp,
			));

			if (empty($newReplies))
			{
				// topic has no more replies
				break;
			}

			foreach ($newReplies AS $newReply)
			{
				if (vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $newReply['nodeid']))
				{
					return $newReply['nodeid'];
				}
				$timeStamp = $newReply['publishdate'];
			}
		}

		return false;
	}


	/**
	 * Insert/Update nodeview for nodeid
	 * @param	int		$nodeid		nodeid for which we should increment the view count
	 * @return 	mixed				return true upon completion, or an error message
	 */
	public function incrementNodeview($nodeid)
	{
		$nodeid = intval($nodeid);

		// no permissions to check for. Any views (including guests') should count.

		// counting views should only be for starters
		$node = vB_Library::instance('node')->getNodeBare($nodeid);
		if ($node['starter'] != $node['nodeid'])
		{
			return array('error' => 'Nodeid is not a starter');
		}

		// we're not implementing the threadviewslive option, VBV-5055
		vB::getDbAssertor()->assertQuery('vBForum:updateNodeview', array('nodeid' =>  $nodeid));

		return true;
	}

	/**
	 * Fetch nodeview count(s) for nodeid(s), used by mergeNodeViewsForTopics() below
	 * @param	array	$nodeids		array of nodeid(s) for which we're grabbing the count(s)
	 * @return 	array					array with keys $nodeids & corresponding counts for the values
	 *										array(<nodeid1> => <view count of nodeid1>, <nodeid2> => <view count of nodeid2>...)
	 */
	protected function getNodeviews($nodeids)
	{
		if (!is_array($nodeids))
		{
			$nodeids = array($nodeids);
		}
		$nodeids = array_map('intval', $nodeids);

		// anyone can see the # of views a node has.


		$viewsQry = vB::getDbAssertor()->assertQuery('vBForum:nodeview', array('nodeid' =>  $nodeids));

		// if a record for a nodeid wasn't found in nodeviews, then it has 0 count.
		$nodes = array_combine($nodeids, array_fill(0, count($nodeids), 0));
		foreach ($viewsQry AS $row)
		{
			$nodes[$row['nodeid']] = $row['count'];
		}

		return $nodes;
	}

	/**
	 * Merges the nodeviews into the topics array, used by the display_Topics template
	 * @param	array	$topicsArray	search results array with nodeids for keys, and each element having a 'content' array
	 * @return 	array					array with keys $nodeids & corresponding counts for the values
	 *										array(<nodeid1> => <view count of nodeid1>, <nodeid2> => <view count of nodeid2>...)
	 */
	public function mergeNodeviewsForTopics($topicsArray)
	{
		// assuming that topicsArray will always have nodeids for keys. Otherwise we'd have to walk through the array twice.
		$nodeids = array_keys($topicsArray);
		$nodeViews = $this->getNodeviews($nodeids);

		foreach ($topicsArray AS $nodeid => &$nodeData)
		{
			$nodeData['content']['views'] = $nodeViews[$nodeid];
		}

		return $topicsArray;
	}


	/** Return only the safe data for a node for which previewonly is set
	 *
	 *	@param mixed	existing node data
	 *
	 *	@return mixed	cleaned node data
	 **/
	public function getPreviewOnly($node)
	{

		$result = array();

		foreach ($this->previewFields as $fieldname)
		{
			if (isset($node[$fieldname]))
			{
				$result[$fieldname] = $node[$fieldname];
			}
		}
		return $result;
	}

	/** Return the specified node's createpermissions for editing for the current user
	 *
	 *	@param array	existing node data array including nodeid, starter, channelid, contenttypeid
	 *
	 *	@return array	key 'createpermissions':
	 *						boolean false if they cannot edit the node
	 *						an array with keys contenttypes => integer 1 if they can create the specified
	 *							contenttype, 0 if they cannot
	 **/
	public function getCreatepermissionsForEdit($node)
	{
		// This function is only meant to be called from the createcontent controller's actionLoadEditor()
		// It's not meant to be very versatile.
		if (!is_array($node)
			OR empty($node['nodeid']) OR empty($node['starter'])
			OR empty($node['channelid']) OR empty($node['userid'])
			OR empty($node['contenttypeid'])
		)
		{
			return array('createpermissions' => false);
		}

		// if the user can't edit this node, then we're out.
		if (!vB_Library_Content::getContentLib($node['contenttypeid'])->getCanEdit($node))
		{
			return array('createpermissions' => false);
		}

		/*
		 *	If we got to this point, the user can edit this node. However, to save any edits to the node, we need
		 *	to know if we can basically re-create this node. In other words, we need to know if we can create a
		 *	reply to this node's parent.
		 */
		 $createpermissions = vB::getUserContext()->getCanCreate($node['parentid']);
		 /*
		  * Now, this bit is to handle the case where the createpermissions has changed between the post's creation
		  *	and edit time. Since content API's validate() only cares about edit permissions for ACTION_UPDATE (which
		  *	getCanEdit() should very closely mimic, if not outright replace in a refactor marathon in the future),
		  *	the user technically can update the post even if they can't create an analogous one anymore. I think the
		  * best solution for this conflict is to grab the contenttype of the existing post, and set createpermission
		  * for that so the front-end will load the editor for that type. Again, this affects front-end behavior only.
		  */
		// TODO: make a legitimate map function that we can use everywhere for this. Is 'contenttypeclass' always set?
		// May need to map from {contenttypeid} -> {type string specified in bitfields}
		$type = 'vbforum_' . strtolower($node['contenttypeclass']);
		$createpermissions[$type] = 1;
		return array('createpermissions' => $createpermissions);
	}

	/** Return whether the user can delete the node or not. Used by createcontent controller
	 *
	 *	@param array	$node		Existing node data array including nodeid, starter, channelid, contenttypeid
	 *	@param boolean	$specific	(OPTIONAL) Whether to specifically check for hard delete or soft delete. By default
	 *									it is false, meaning it will check whether user can soft OR hard delete the node
	 *	@param boolean	$hard		(OPTIONAL) Only used if $specific is true. Whether it's checking if user can hard
	 *									delete (true) or soft delete (false)
	 *
	 *	@return array	key 'candelete':
	 *						boolean true if they can delete the node
	 **/
	public function getCanDeleteForEdit($node, $specific = false, $hard = false)
	{
		// This function is only meant to be called from the createcontent controller's actionLoadEditor()
		// It's not meant to be very versatile.
		if (!is_array($node)
			OR empty($node['nodeid']) OR empty($node['starter'])
			OR empty($node['channelid']) OR empty($node['userid'])
			OR empty($node['contenttypeid'])
		)
		{
			return array('candelete' => false);
		}

		// Let's grab some params that getCanDelete() requires if we want to use the $hard param.
		$userContext = vB::getUserContext();
		$starter = vB_Library::instance('node')->getNodeBare($node['starter']);
		$channelid = $starter['parentid'];
		$channelPerms = vB::getUserContext()->fetchPermsForChannels(array($channelid));
		$thisChannelPerms = $channelPerms[$channelid];
		$thisChannelPerms['global'] = $channelPerms['global'];

		$contentLib = vB_Library_Content::getContentLib($node['contenttypeid']);

		if ($specific)
		{
			$canDelete = $contentLib->getCanDelete($node, $userContext, $thisChannelPerms, $hard);
		}
		else
		{
			$canDelete = (
				$contentLib->getCanDelete($node, $userContext, $thisChannelPerms, false)
				OR $contentLib->getCanDelete($node, $userContext, $thisChannelPerms, true)
			);
		}

		return array("candelete" => $canDelete);
	}

}
