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


/**
 * vB_Api_Content
 *
 * @package vBApi
 * @author ebrown
 * @copyright Copyright (c) 2011
 * @version $Id$
 * @access public
 */
abstract class vB_Library_Content extends vB_Library
{
	//override in client- the text name
	protected $contenttype = false;

	//set internally
	protected $contenttypeid;

	//The table for the type-specific data.
	protected $tablename;

	//Whether we change the parent's text count- 1 or zero
	protected $textCountChange = 0;

	//Whether this shows on a section listing
	protected $inlist = 1;

	//Does this content show author signature?
	protected $showSignature = false;

	//We need a way to skip flood check for types like Photos, where we'll upload several together.
	protected $doFloodCheck = true;

	//This defines the cache levels.  If the user requests say node Data, and we only have
	//cached content data- pass the data anyway.
	const CACHELEVEL_NODE = 1;
	const CACHELEVEL_CONTENT = 2;
	const CACHELEVEL_FULLCONTENT = 3;

	protected function __construct()
	{
		parent::__construct();
		$this->contenttypeid = vB_Types::instance()->getContentTypeId($this->contenttype);
		//The table for the type-specific data.
		$this->assertor = vB::getDbAssertor();
		$this->nodeApi = vB_Api::instanceInternal('node');
		$this->nodeFields = $this->nodeApi->getNodeFields();
		$this->options = vB::getDatastore()->getValue('options');
	}

	public function fetchContentType()
	{
		return $this->contenttype;
	}


	public function fetchContentTypeId()
	{
		return $this->contenttypeid;
	}

	/**
	 * Returns textCountChange property
	 * @return int
	 */
	public function getTextCountChange()
	{
		return $this->textCountChange;
	}

	/**
	 * Returns inlist property
	 * @return int
	 */
	public function getInlist()
	{
		return $this->inlist;
	}



	/*** Adds a new node.
	 *
	 *	@param	mixed		Array of field => value pairs which define the record.
	 *  @param	array		Array of options for the content being created. See subclasses for more info. skipFloodCheck and skipNotifications are useful
	 *
	 * 	@return	integer		the new nodeid
	 ***/
	public function add($data, array $options = array())
	{
		if (empty($data['parentid']))
		{
			throw new vB_Exception_Api('need_parent_node');
		}

		// *************************************
		// * Fill some default data if missing *
		// *************************************
		if (empty($data['userid']))
		{
			$user = vB::getCurrentSession()->fetch_userinfo();
			$data['authorname'] = $user['username'];
			$userid = $data['userid'] = $user['userid'];
		}
		else
		{
			$userid = $data['userid'];
			if (empty($data['authorname']))
			{
				$data['authorname'] = vB_Api::instanceInternal('user')->fetchUserName($userid);
			}
		}

		if (empty($data['ipaddress']))
		{
			$data['ipaddress'] = vB::getRequest()->getIpAddress();
		}

		$parentInfo = self::fetchFromCache($data['parentid'], self::CACHELEVEL_FULLCONTENT);

		if ($parentInfo AND $parentInfo['found'] AND $parentInfo['found'][$data['parentid']])
		{
			$parentInfo = $parentInfo['found'][$data['parentid']];
		}
		else
		{
			$parentInfo = vB_Library::instance('node')->getNodeContent($data['parentid']);
			$parentInfo = $parentInfo[$data['parentid']];
		}

		//we don't add to a closed channel.
		if ($parentInfo['open'] == 0)
		{
			throw new vB_Exception_Api('invalid_request');
		}

		//We shouldn't pass the open or show open fields
		unset($data['open']);
		unset($data['showopen']);

		//We shouldn't pass the approved or showapproved open fields
		unset($data['approved']);
		unset($data['showapproved']);

		//We always inherit the parents "protected" value
		if (!isset($data['protected']) || $data['protected'] != 1)
		{
			$data['protected'] = $parentInfo['protected'];
		}

		//We also inherit hasowner UNLESS we are a channel
		if ($this->contenttype != 'vBForum_Channel')
		{
			$data['hasowner'] = $parentInfo['hasowner'];
		}

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

		// we cannot add content to category channels
		if ($parentInfo['contenttypeid'] == $channelContentTypeId AND $parentInfo['category'] != 0
			AND $this->contenttypeid != $channelContentTypeId)
		{
			throw new vB_Exception_Api('invalid_request');
		}

		// the starter cannot be a channel, so if this is the case set the starter after adding the node
		if ($this->contenttypeid != $channelContentTypeId AND $parentInfo['contenttypeid'] != $channelContentTypeId)
		{
			if (!empty($parentInfo['starter']))
			{
				$data['starter'] = $parentInfo['starter'];
			}
			else
			{
				//if the parent is a text type but the starter isn't set then we need to get the
				$data['starter'] = $this->getStarter($data['parentid']);
			}
		}

		// Verify prefixid
		if ($this->contenttypeid != $channelContentTypeId AND !empty($data['prefixid']))
		{
			$this->verifyPrefixid($data['prefixid']);
		}
		else
		{
			// Channel can't have a prefix
			unset($data['prefixid']);
		}

		// Verify post iconid
		if ($this->contenttypeid != $channelContentTypeId AND !empty($data['iconid']))
		{
			$this->verifyPostIconid($data['iconid']);
		}
		else
		{
			// Channels can't have a post icon
			unset($data['iconid']);
		}

		//we generally do a flood check- when was this user's last post?
		if (empty($options['skipFloodCheck']) AND ($this->options['floodchecktime'] > 0) AND ($this->doFloodCheck) AND !vB::getUserContext()->isAdministrator())
		{
			if ($lastPostElapsed = $this->isFlood($data))
			{
				throw new vB_Exception_Api('postfloodcheck', array($this->options['floodchecktime'],
						$this->options['floodchecktime'] - $lastPostElapsed));
			}
		}

		$data['lastupdate'] =  vB::getRequest()->getTimeNow();

		if (empty($data['created']))
		{
			$data['created'] =  vB::getRequest()->getTimeNow();
		}

		// set publishdate to now...
		if (!isset($data['publishdate']))
		{
			$data['publishdate'] = vB::getRequest()->getTimeNow();
		}

		//It's possible we already have a nodeid.
		$nodevals = array();

		if ($this->isPublished($data))
		{
			$nodevals['showpublished'] = 1;
		}
		else
		{
			$nodevals['showpublished'] = 0;
		}

		// inherit showpublish from the parent
		if (($parentInfo['showpublished'] == 0) AND ($this->contenttype != 'vBForum_PrivateMessage'))
		{
			$nodevals['showpublished'] = 0;
		}

		$nodevals['inlist'] = $this->inlist;

		//If this user doesn't have the featured permission and they are trying to set it,
		//Let's just quietly unsert it.
		if (isset($data['featured']))
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'cansetfeatured', $data['parentid']))
			{
				unset($data['featured']);
			}
		}

		if (empty($data['htmltitle']) AND !empty($data['title']))
		{
			$data['htmltitle'] = vB_String::htmlSpecialCharsUni($data['title']);
		}

		$parentid = $data['parentid'];
		//let's set the appropriate fields in the node table
		foreach ($data as $field => $value)
		{
			if (in_array($field, $this->nodeFields))
			{
				$nodevals[$field] = $value;
				if (!isset($this->contentDupFields) OR !in_array($field, $this->contentDupFields))
				{
					unset($data[$field]);
				}
			}
		}

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

		if (empty($nodevals['userid']))
		{
			$nodevals['userid'] = vB::getCurrentSession()->get('userid');
		}

		if (empty($this->contenttypeid))
		{
			$this->contenttypeid = vB_Types::instance()->getContentTypeId($this->contenttype);
		}

		$nodevals['contenttypeid'] = $this->contenttypeid;
		$nodevals[vB_dB_Query::TYPE_KEY] = vB_dB_Query::QUERY_INSERT;

		//check for next update needed
		if (!empty($nodevals['publishdate']) AND !empty($nodevals['unpublishdate']))
		{
			$nodevals['nextupdate'] = min($nodevals['publishdate'], $data['unpublishdate']);
		}
		else if (!empty($nodevals['unpublishdate']))
		{
			$nodevals['nextupdate'] = $nodevals['unpublishdate'];
		}
		else
		{
			$nodevals['nextupdate'] = 0;
		}

		if (empty($nodevals['nodeoptions']) OR !is_numeric($nodevals['nodeoptions']))
		{
			// default auto-accept subscriber disabled for blog and sg nodes
			$specialChannel = false;
			try
			{
				if (vB_Api::instanceInternal('blog')->isBlogNode($nodevals['parentid']) OR vB_Api::instanceInternal('socialgroup')->isSGNode($nodevals['parentid']))
				{
					$specialChannel = true;
				}
			}
			catch (vB_Exception_Api $ex)
			{
				// just ignore
			}
			$nodevals['nodeoptions'] = ($specialChannel) ? 10 : 138;
		}

		$nodeid = $this->assertor->assertQuery('vBForum:node', $nodevals);

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

		if (is_array($nodeid))
		{
			$nodeid = $nodeid[0];
		}


		//Let's set the lastcontent and lastcontentid values
		if ($this->contenttype != 'vBForum_Channel')
		{
			vB::getDbAssertor()->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY=> vB_dB_Query::QUERY_UPDATE,
				'nodeid' => $nodeid, 'lastcontent' =>  vB::getRequest()->getTimeNow(), 'lastcontentid' => $nodeid));
		}

		$cacheEvents = array("fUserContentChg_" . $nodevals['userid']);
		if ($this->contenttypeid == $channelContentTypeId)
		{
			$cacheEvents[] = "nodeChg_" . $nodevals['parentid'] ;
		}
		else
		{
			$channelNodeid = 0;
			if (!isset($nodevals['starter']))
			{
				//The only reason this would be unset is that THIS is the starter.
				$update = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
				'starter' => $nodeid, 'nodeid' => $nodeid);
				$this->assertor->assertQuery('vBForum:node', $update);
				//Since this is the starter, the parent is the channel.
				$cacheEvents[] = "nodeChg_" . $parentid;
			}
			else
			{
				//we need cache events to for the starter, which we have, and the
				//channel, which we don't. But we know it's the parent of the starter.
				//Rather than calling the node api, let's do the query directly. Should be faster.
				$starterNodeInfo = $this->assertor->getRow('vBForum:node', array('nodeid' => $nodevals['starter']));
				$cacheEvents[] = "nodeChg_" . $nodevals['starter'];
				$cacheEvents[] = "nodeChg_" . $starterNodeInfo['parentid'] ;
				$cacheEvents[] = "fUserContentChg_" . $starterNodeInfo['userid'];
				$channelNodeid = $starterNodeInfo['parentid'];
			}

			// Check "Moderate comments before displaying"
			// If !$channelNodeid, the node is a starter not a comment so we skip it.
			if ($channelNodeid)
			{
				$channel = $this->nodeApi->getNode($channelNodeid);
				// If it's the owner of channel who posted the comment, we need to approve it
				// TODO: we may need to allow Admin and moderator to bypass the limit?
				if ($nodevals['userid'] != $channel['userid'] AND $channel['moderate_comments'])
				{
					$this->assertor->assertQuery('vBForum:node', array(
						vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
						'approved' => 0,
						'showapproved' => 0,
						vB_dB_Query::CONDITIONS_KEY => array(
							'nodeid' => $nodeid,
						)
					));
				}
			}
		}

		vB_Cache::instance()->event($cacheEvents);
		vB_Cache::instance(vB_Cache::CACHE_FAST)->event($cacheEvents);

		//Now update the closure table.
		$this->assertor->assertQuery('vBForum:closure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT,
			'parent' => $nodeid, 'child' => $nodeid,'depth' => 0, 'publishdate' => $nodevals['publishdate']) );

		$this->assertor->assertQuery('vBForum:addClosure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'nodeid' => $nodeid) );

		$this->updateNodeOptions($nodeid, $data);

		if (!empty($nodevals['nextupdate']))
		{
			$nodevals['nodeid'] = $nodeid;
			$this->assertor->assertQuery('vBForum:setNextUpdate', array(
				'nodeid' => $nodeid, 'publishdate' => $nodevals['publishdate'],
				'unpublishdate'	=> (isset($nodevals['unpublishdate']) ? $nodevals['unpublishdate'] :0 )));
		}

		//handle the 'index' setting;
		$index = empty($data['noIndex']);
		unset($data['noIndex']);

		// Insert the content-type specific data
		if (!is_array($this->tablename))
		{
			$tables = array($this->tablename);
		}
		else
		{
			$tables = $this->tablename;
		}

		foreach ($tables as $table)
		{
			$structure = $this->assertor->fetchTableStructure('vBForum:' . $table);
			$queryData = array();
			$queryData[vB_dB_Query::TYPE_KEY] = vB_dB_Query::QUERY_INSERT;
			$queryData['nodeid'] = $nodeid;
			foreach ($structure['structure'] as $fieldname)
			{
				if (isset($data[$fieldname]))
				{
					$queryData[$fieldname] = $data[$fieldname];
				}
			}
			$this->assertor->assertQuery('vBForum:' . $table, $queryData);
		}


		//If published and if this is a text class we should update the text counts.
		if ($textCountChange = $this->textCountChange)
		{
			if ($nodevals['showpublished'])
			{
				$this->updateParentCounts($nodeid, $textCountChange, 0, $textCountChange, 0, 1);
			}
			else
			{
				$this->updateParentCounts($nodeid, 0, $textCountChange, 0, $textCountChange, 0);
			}
		}

		//updating the posts count for this user
		$this->updatePostsCount($userid);

		if($index)
		{
			vB_Api::instance('Search')->index($nodeid);
		}

		vB_Api::instance('Search')->purgeCacheForCurrentUser();
		vB_Cache::instance(vB_Cache::CACHE_FAST)->event("fContentChg_$parentid");
		vB_Cache::instance()->event("fContentChg_$parentid");
		// update tags
		if (!empty($data['tags']))
		{
			$tagRet = vB_Api::instanceInternal('tags')->addTags($nodeid, $data['tags']);
		}

		//Let's see if we need to send notifications
		// first we need to get the current node.
		//Private messages are different. Let the subclass handle them.
		$notifications = array();
		if (($this->contenttype != 'vBForum_PrivateMessage') AND empty($options['skipNotification']))
		{
			$node = $this->nodeApi->getNode($nodeid);

			//If this is a visitor message we always send a message
			if ($this->isVisitorMessage($nodeid))
			{
				$notifications[] = array('about' => vB_Library_Content_Privatemessage::NOTIFICATION_TYPE_VM,
					'aboutid' => $node['userid'], 'userid' => $userid);
			}
			else if (($node['starter'] > 0) AND ($node['starter'] != $nodeid))
			{
				//See if we need to send moderation messages.
				if (empty($node['publishdate']))
				{
					$starter = $this->nodeApi->getNode($node['starter']);
					$moderators = vB_Api::instanceInternal('blog')->fetchContribuitors($starter['parentid']);
					$moderators[] = vB_Api::instanceInternal('blog')->fetchOwner($starter['parentid']);
					foreach (array_unique($moderators) as $moderator)
					{
						$notifications[] = array('about' => vB_Library_Content_Privatemessage::NOTIFICATION_TYPE_MODERATE,
							'aboutid' => $nodeid, 'userid' => $moderator);
					}
				}

				//If this is in response to the starter, it's a reply.
				$starter = $this->nodeApi->getNode($node['starter']);
				if ($node['starter'] == $node['parentid'])
				{
					$notifications[] = array('about' => vB_Library_Content_Privatemessage::NOTIFICATION_TYPE_REPLY,
						'aboutid' => $node['starter'], 'userid' => $starter['userid']);
				}
				else
				{
					$notifications[] = array('about' => vB_Library_Content_Privatemessage::NOTIFICATION_TYPE_COMMENT,
						'aboutid' => $node['starter'], 'userid' => $starter['userid']);
					$parent = $this->nodeApi->getNode($nodevals['parentid']);
					$notifications[] = array('about' => vB_Library_Content_Privatemessage::NOTIFICATION_TYPE_REPLY,
						'aboutid' => $node['parentid'] , 'userid' => $parent['userid']);

				}
			}
		}

		// @TODO VBV-2303 we can't send notifications as guest... private message library do checkFolders() which won't be valid for guests.
		if (!empty($notifications) AND empty($options['skipNotifications']) AND intval(vB::getCurrentSession()->get('userid')))
		{
			$this->sendNotifications($notifications);
		}

		$this->nodeApi->clearCacheEvents($parentid);

		//For the children
		return $nodeid;

	}

	protected function isFlood($data)
	{
		$isFlood = false;

		$lastNode = vB::getDbAssertor()->assertQuery('vBForum:node',
			array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				vB_dB_Query::CONDITIONS_KEY => array(
					array('field' => 'userid', 'value' => $data['userid'], 'operator' => vB_dB_Query::OPERATOR_EQ),
					array('field' => 'created', 'value' =>  vB::getRequest()->getTimeNow() - $this->options['floodchecktime'], 'operator' => vB_dB_Query::OPERATOR_GT)
				)
			),
			array('field' => array('created'), 'direction' => array(vB_dB_Query::SORT_DESC))
		);

		if ($lastNode AND $lastNode->valid())
		{
			$node = $lastNode->current();
			$lastPostElapsed = vB::getRequest()->getTimeNow() - $node['created'];
			if ($lastPostElapsed < $this->options['floodchecktime'])
			{
				$isFlood = true;
			}
		}

		if ($isFlood)
		{
			return $lastPostElapsed;
		}
		return false;
	}

	/** Sets node options from individual fields.
	 *
	 * 	@param	int
	 * 	@param	mixed	array of field => value pairs. Both must be strings, but non-matching keys will be ignored.
	 *
	 */
	protected function updateNodeOptions($nodeid, $data)
	{
		//set noteoptions if we got them.
		if (!isset($data['nodeoptions']))
		{
			$nodeOptions = vB_Api::instanceInternal('node')->getOptions();
			$options = array();
			foreach ($nodeOptions AS $optionKey => $optionVal)
			{
				if (isset($data[$optionKey]))
				{
					$options[$optionKey] = $data[$optionKey];
				}
			}

			if (!empty($options))
			{
				vB_Api::instanceInternal('node')->setNodeOptions($nodeid, $options);
			}
		}
		else
		{
			vB_Api::instanceInternal('node')->setNodeOptions($nodeid, $data['nodeoptions']);
		}

	}

	/** Updates the parent counts and data when saving a node.
	 *
	 *	@param	mixed	node record
	 *
	 ***/
	protected function updateParentCounts($nodeid, $textChange, $textUnPubChange, $totalPubChange, $totalUnPubChange, $published)
	{
		$parents = $this->assertor->assertQuery('vBForum:getParents', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'nodeid' => $nodeid));

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

		$parentids = array_unique($parentids);

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

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

			$nodelib = vB_Library::instance('node');
			$nodelib->clearCacheEvents($parentids);
			foreach ($parentids AS $parentid)
			{
				$this->assertor->assertQuery('vBForum:updateLastData', array('parentid' => $parentid, 'timenow' => vB::getRequest()->getTimeNow()));
			}
		}
	}


	/**
	 * updates the number of posts of a user
	 * @param array $data - have to contain the key 'userid' for the user that needs a post count update
	 */
	protected function updatePostsCount($userid)
	{
		$query = $this->assertor->assertQuery('vBForum:fetchUserPostsCount', array(
			vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
			'userid' => $userid)
		);

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

		$res = $query->current();
		$totalPosts = $res['posts'];

		$update = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
		'userid' => $userid, 'posts' => $totalPosts);
		$this->assertor->assertQuery('user', $update);
		return $totalPosts;
	}


	/*** Permanently deletes a node
	 *	@param	integer	The nodeid of the record to be deleted
	 *
	 *	@return	boolean
	 ***/
	public function delete($nodeid)
	{
		//We need to see the child counts for this node, because we need to remove that quantity
		//from the parents.
		$prior = $this->assertor->getRow('vBForum:node', array(	'nodeid' => $nodeid));
		if ($prior['protected'])
		{
			//O.K. if it's not a channel.
			if ($prior['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Channel'))
			{
				throw new vB_Exception_Api('no_delete_permissions');
			}

		}
		$clearcacheNodes = array($nodeid, $prior['parentid'], $prior['starter']);
		//We need to delete children. We will only delete immediate children. That means recursion, but the child contenttypes need to do their own cleanup
		$childApis = array();


		$childNodes = $this->assertor->assertQuery('vBForum:node', array(vB_dB_Query::TYPE_KEY =>  vB_dB_Query::QUERY_SELECT,
			'parentid' => $nodeid));

		foreach ($childNodes as $childNode)
		{
			if (!isset($childApis[$childNode['contenttypeid']]))
			{
				$childApis[$childNode['contenttypeid']] = array();
			}
			$childApis[$childNode['contenttypeid']][] = $childNode['nodeid'];
			$clearcacheNodes = $childNode['nodeid'];
		}
		//First we delete children
		$success = true;
		if (!empty($childApis))
		{
			foreach ($childApis as $contenttypeid => $nodeids)
			{
				$childApi = self::getContentApi($contenttypeid);
				foreach ($nodeids as $childnodeid)
				{
					$childApi->delete($childnodeid);
				}
			}
		}

		//Now the children should be gone.
		//delete the node record.
		$data = array(vB_dB_Query::TYPE_KEY =>  vB_dB_Query::QUERY_DELETE, 'nodeid' => $nodeid);

		// Delete the content-type specific data
		if (!is_array($this->tablename))
		{
			$tables = array($this->tablename);
		}
		else
		{
			$tables = $this->tablename;
		}
		foreach ($tables as $table)
		{
			$success = $success AND $this->assertor->assertQuery('vBForum:' . $table, $data);
		}

		if ($success)
		{
			$this->assertor->assertQuery('vBForum:node', $data);
			//Clear cached query info that would be significantly impacted
			$starterNodeInfo = $this->assertor->getRow('vBForum:node', array('nodeid' => $prior['starter']));
			$cacheEvents = array("nodeChg_" . $prior['starter'], "nodeChg_" . $starterNodeInfo['parentid'],
				'fUserContentChg_' . $starterNodeInfo['userid'], 'fUserContentChg_' . $prior['userid']) ;
			vB_Cache::instance()->event($cacheEvents);
			vB_Cache::instance(vB_Cache::CACHE_FAST)->event($cacheEvents);

			//updating the posts count for this user
			if($prior['userid'])
			{
				$this->updatePostsCount($prior['userid']);
			}
			//removing the index
			vB_Api::instanceInternal('Search')->delete($nodeid);

			vB_Api::instanceInternal('Search')->purgeCacheForCurrentUser();
			$this->assertor->assertQuery('vBForum:closure',
				array(vB_dB_Query::TYPE_KEY =>  vB_dB_Query::QUERY_DELETE, 'child' => $nodeid));
			$this->nodeApi->clearCacheEvents($clearcacheNodes);
			return true;
		}
		$this->nodeApi->clearCacheEvents($clearcacheNodes);
		return false;
	}

	/*** Is this record in a published state based on the times?
	 *
	 *	@param	mixed
	 *
	 *	@return	bool
	 ****/
	public function isPublished($data)
	{
		if (empty($data['publishdate']))
		{
			return false;
		}
		$timeNow = vB::getRequest()->getTimeNow();

		return ($data['publishdate'] > 0) AND
		($data['publishdate'] <= $timeNow) AND
		(empty($data['unpublishdate']) OR ($data['unpublishdate'] <= 0) OR ($data['unpublishdate'] >= $timeNow) OR
		($data['unpublishdate'] < $data['publishdate']));
	}


	/*** updates a record
	 *
	 *	@param	mixed		array of nodeid's
	 *	@param	mixed		array of permissions that should be checked.
	 *
	 * 	@return	boolean
	 ***/
	public function update($nodeid, $data)
	{
		$channelContentTypeId = vB_Types::instance()->getContentTypeId('vBForum_Channel');

		// Verify prefixid
		if ($this->contenttypeid != $channelContentTypeId AND !empty($data['prefixid']))
		{
			$this->verifyPrefixid($data['prefixid']);
		}
		else
		{
			// Channel can't have a prefix
			unset($data['prefixid']);
		}

		// Verify post iconid
		if ($this->contenttypeid != $channelContentTypeId AND !empty($data['iconid']))
		{
			$this->verifyPostIconid($data['iconid']);
		}
		else
		{
			// Channels can't have a post icon
			unset($data['iconid']);
		}

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

		//If this user doesn't have the featured permission and they are trying to set it,
		//Let's just quietly unset it.
		if (isset($data['featured']))
		{
			if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'cansetfeatured', $data['parentid']))
			{
				unset($data['featured']);
			}
		}

		//We can't allow directly setting parentid. That should only happen through the node api move function
		//And there are number of other fields that shouldn't be changed here. We have methods for the ones that can be changed at all.
		foreach (array('open', 'showopen', 'approved', 'showapproved', 'protected', 'parentid') as $field)
		{
			if (isset($data[$field]))
			{
				unset($data[$field]);
			}
		}


		//We need to see if we need to update.
		$prior = $this->assertor->getRow('vBForum:node', array(	'nodeid' => $nodeid));

		if (($prior['nextupdate'] > $prior['lastupdate']) AND ($prior['nextupdate'] <= $timeNow))
		{
			$this->timedCountUpdates($nodeid);
		}

		if (!isset($data['publishdate']))
		{
			if (!empty($prior['publishdate']))
			{
				$data['publishdate'] = $prior['publishdate'];
			}
			else
			{
				$data['publishdate'] = 0;
			}
		}

		if (!isset($data['unpublishdate']))
		{
			$data['unpublishdate'] = $prior['unpublishdate'];
		}

		if (empty($data['htmltitle']) AND !empty($data['title']))
		{
			$data['htmltitle'] = vB_String::htmlSpecialCharsUni($data['title']);
		}

		if ($this->isPublished($data))
		{
			$published = 1;
		}
		else
		{
			$published = 0;
		}

		$nodevals = array();
		$updateParents = false;
		//We need to compare the current publishdate and unpublishdate values against the
		// parent.
		//But we can skip this if neither publish or unpublishdate is set
		if ($published <> $prior['showpublished'])
		{
			$nodevals['showpublished'] = $published;
			$updateParents = true;
			//We are concerned about two possibilities. It could have gone from published to unpublished.
			//In either case we change by totalcount +1 (for ourselves.
			//Remember that published is always unpublished.

			//From unpublished to published.
			if ($published)
			{
				$textChange = $this->textCountChange;
				$textUnPubChange = -1 * $textChange;
				$totalPubChange = $prior['totalcount'] + $textChange;
				$totalUnPubChange = -1 * $totalPubChange;
			}
			//or from published to unpublished.
			else
			{
				$textChange = -1 * $this->textCountChange;
				$textUnPubChange = $this->textCountChange;
				$totalUnPubChange = $prior['totalcount'] + $this->textCountChange;
				$totalPubChange = -1 * $totalUnPubChange;
			}
		}

		//node table data.
		$data[vB_dB_Query::TYPE_KEY] =  vB_dB_Query::QUERY_UPDATE;
		$data['nodeid'] = $nodeid;
		$data['lastupdate'] =  $timeNow;
		//If the field passed is in the $nodeFields array then we update the node table.
		foreach ($data as $field => $value)
		{
			if (in_array($field, $this->nodeFields))
			{
				$nodevals[$field] = $value;
			}
		}

		$index = empty($data['noIndex']);
		unset($data['noIndex']);

		// Update the content-type specific data
		if (!is_array($this->tablename))
		{
			$tables = array($this->tablename);
		}
		else
		{
			$tables = $this->tablename;
		}

		$success = true;

		foreach ($tables as $table)
		{
			$structure = $this->assertor->fetchTableStructure('vBForum:' . $table);
			if (empty($structure) OR empty($structure['structure']))
			{
				throw new vB_Exception_Api('invalid_query_parameters');
			}
			$queryData = array();
			$queryData[vB_dB_Query::TYPE_KEY] = vB_dB_Query::QUERY_UPDATE;
			$queryData['nodeid'] = $nodeid;
			foreach ($structure['structure'] as $fieldname)
			{
				if (isset($data[$fieldname]))
				{
					$queryData[$fieldname] = $data[$fieldname];
				}
			}
			//Now we have at least a query type and a nodeid. We put those in above. So if we don't
			//have at least one other value there's no reason to try an update.
			if (count($queryData)> 2)
			{
				$success = $success AND $this->assertor->assertQuery('vBForum:' . $table, $queryData);
			}
		}

		if (isset($nodevals['publishdate']) OR isset($nodevals['unpublishdate']))
		{
			$nodevals['lastupdate'] = $timeNow;
			$nodevals[vB_dB_Query::TYPE_KEY] =  vB_dB_Query::QUERY_METHOD;
			$nodevals['nodeid'] = $nodeid;
			$this->assertor->assertQuery('vBForum:setNextUpdate', $nodevals);
			$this->assertor->assertQuery('vBForum:closure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
				vB_dB_Query::CONDITIONS_KEY => array('child' => $nodeid), 'publishdate' => $nodevals['publishdate']));
		}

		if ($success)
		{
			//Clear cached query info that would be significantly impacted
			$starterNodeInfo = $this->assertor->getRow('vBForum:node', array('nodeid' => $prior['starter']));
			$this->nodeApi->clearCacheEvents($nodeid);
			$events = array('fUserContentChg_' . $prior['userid'], 'fUserContentChg_' . $starterNodeInfo['userid']);
			vB_Cache::instance(vB_Cache::CACHE_FAST)->event($events);
			vB_Cache::instance()->event($events);

			if (isset($nodevals['publishdate']) OR isset($nodevals['unpublishdate']))
			{
				if (isset($nodevals['publishdate']))
				{
					if (isset($nodevals['unpublishdate']))
					{
						$this->assertor->assertQuery('vBForum:node',
						array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,'nodeid' => $nodeid, 'publishdate' => $nodevals['publishdate'],
						'unpublishdate' =>$nodevals['unpublishdate']));
						unset($nodevals['unpublishdate']);
					}
					else
					{
						$this->assertor->assertQuery('vBForum:node',
						array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,'nodeid' => $nodeid, 'publishdate' => $nodevals['publishdate']));
					}
					unset($nodevals['publishdate']);
				}
				else
				{
					$this->assertor->assertQuery('vBForum:node',
					array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,'nodeid' => $nodeid, 'unpublishdate' => $nodevals['unpublishdate']));
					unset($nodevals['unpublishdate']);
				}
			}

			// handle approved
			if (isset($nodevals['approved']))
			{
				if ($nodevals['approved'])
				{
					$approved = 1;
					$queryName = 'approveNode';
				}
				else
				{
					$approved = 0;
					$queryName = 'unapproveNode';
				}

				// set approved to parent...
				$this->assertor->assertQuery('vBForum:node',
				array(
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,'nodeid' => $nodeid, 'approved' => $approved
				)
				);

				// and handle showapproved
				$this->assertor->assertQuery('vBForum:' . $queryName, array('nodeid' => $nodeid));
				unset($nodevals['approved']);
			}

			if (isset($nodevals))
			{
				$nodevals[vB_dB_Query::TYPE_KEY] =  vB_dB_Query::QUERY_UPDATE;
				$nodevals['nodeid'] = $nodeid;
				$success = $this->assertor->assertQuery('vBForum:node', $nodevals);
			}

			//update the parent count if necessary
			if ($updateParents)
			{
				$this->updateParentCounts($nodeid, $textChange, $textUnPubChange, $totalPubChange, $totalUnPubChange, $published);
			}

			if($index)
			{
				vB_Api::instanceInternal('Search')->index($nodeid);
			}

			// update user tags
			$tags = !empty($data['tags']) ? explode(',', $data['tags']) : array();
			$tagRet = vB_Api::instanceInternal('tags')->updateUserTags($nodeid, $tags);

			// logging the action
			require_once(DIR . '/includes/functions_log_error.php');

			$loginfo = array(
					'nodeid' => $nodeid,
					'nodetitle' => empty($nodevals['title'])?'':$nodevals['title']
			);

			$this->updateNodeOptions($nodeid, $data);

			log_moderator_action($loginfo, 'node_x_edited', empty($nodevals['title'])?'':$nodevals['title']);

			$this->nodeApi->clearCacheEvents(array($nodeid, $prior['parentid']));
			return true;
		}

		$this->nodeApi->clearCacheEvents(array($nodeid, $prior['parentid']));
		return false;

	}


	/*** Returns a content api of the appropriate type
	 *
	 *	@param	int		the content type id
	 *
	 *	@return	mixed	content api object
	 ****/
	public static function getContentApi($contenttypeid)
	{
		return vB_Api::instanceInternal('Content_' . vB_Types::instance()->getContentTypeClass($contenttypeid));
	}

	/*** Returns a content api of the appropriate type
	 *
	 *	@param	int		the content type id
	 *
	 *	@return	mixed	content api object
	 ****/
	public static function getContentLib($contenttypeid)
	{
		return vB_Library::instance('Content_' . vB_Types::instance()->getContentTypeClass($contenttypeid));
	}

	/*** Returns the node content as an associative array
	 *	@param	integer	The id in the primary table
	 *	@param	mixed	array of permissions request- (array group, permission)

	 *	@return	int
	 ***/
	public function getContent($nodeids, $permissions = false)
	{
		//First see what we can load from cache.
		return $this->getFullContent($nodeids, $permissions, false);
	}

	/*** Returns the node content plus the channel routeid and title, and starter route and title the as an associative array
	 *	@param	integer	The id in the primary table
	 *	@param	mixed	array of permissions request- (array group, permission)
	 *
	 *	@return	mixed
	 ***/
	public function getFullContent($nodeids, $permissions = false, $full = true)
	{
		//First see what we can load from cache.
		if ($full)
		{
			$cached = self::fetchFromCache($nodeids, self::CACHELEVEL_FULLCONTENT);
		}
		else
		{
			$cached = self::fetchFromCache($nodeids, self::CACHELEVEL_CONTENT);
		}

		$nodesContent = array();
		$timeNow = vB::getRequest()->getTimeNow();
		foreach($cached['found'] AS $key => $cachedNode)
		{
			if (($cachedNode['nextupdate'] > 0) AND ($cachedNode['nextupdate'] > ($cachedNode['lastupdate']))
				AND ($cachedNode['nextupdate'] <= $timeNow))
			{
				unset($cached['found'][$key]);
				$cached['notfound'][] = $cachedNode['nodeid'];
			}
			else
			{
				$nodesContent[$key] = $cachedNode;
			}
		}


		//Now do we need to query?
		if (!empty($cached['notfound']))
		{
			if ($full)
			{
				$method = 'vBForum:getFullContent';
			}
			else
			{
				$method = 'vBForum:getContent';
			}

			$content = $this->assertor->getRows($method,
			array('tablename' => $this->tablename,
			'nodeid' => $cached['notfound']));

			//Now we merge these plus, if necessary, the permissions. First let's make an associative array of the core data.
			if (!$content OR !empty($content['errors']))
			{
				throw new vB_Exception_Api('invalid_data_requested: ' . var_export($nodeids, true)) ;
			}

			// Get prefix phrases
			$phrasevars = array();
			foreach ($content as $node)
			{
				if (!empty($node['prefixid']))
				{
					$phrasevars[] = 'prefix_' .  $node['prefixid'] . '_title_plain';
					$phrasevars[] = 'prefix_' .  $node['prefixid'] . '_title_rich';
				}
				if (!empty($node['starterprefixid']))
				{
					$phrasevars[] = 'prefix_' .  $node['starterprefixid'] . '_title_plain';
					$phrasevars[] = 'prefix_' .  $node['starterprefixid'] . '_title_rich';
				}
			}

			$phrases = array();
			if ($phrasevars)
			{
				$phrases = vB_Api::instanceInternal('phrase')->fetch($phrasevars);
			}

			//Check to see what needs updates.
			$requery = array();
			foreach ($content as $key => $node)
			{
				if (isset($node['nextupdate']) AND ($node['nextupdate'] > 0)  AND ($node['nextupdate'] > $node['lastupdate'])
					AND ($node['nextupdate'] <= $timeNow))
				{
					$this->timedCountUpdates($node['nodeid']);
					$requery[] = $node['nodeid'];
				}

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

				if ($phrases AND !empty($node['starterprefixid']))
				{
					$content[$key]['starterprefix_plain'] = $phrases['prefix_' .  $node['starterprefixid'] . '_title_plain'];
					$content[$key]['starterprefix_rich'] = $phrases['prefix_' .  $node['starterprefixid'] . '_title_rich'];
				}
			}

			if (!empty($requery))
			{
				$content2 = $this->assertor->getRows($method,
				array('tablename' => $this->tablename,
				'nodeid' => $requery));

				$content = array_merge($content, $content2);
			}

			if ($full)
			{
				self::writeToCache($content, self::CACHELEVEL_FULLCONTENT);
			}
			else
			{
				self::writeToCache($content, self::CACHELEVEL_CONTENT);
			}

			$nodesContent = array_merge($nodesContent, $content);
		}

		// @TODO need to sort out how to improve the content being cached. Not possible now cause we do some perm checks and add flags for the current user.
		$nodesContent = $this->assembleContent($nodesContent, $permissions);

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

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

		foreach ($nodeids AS $nodeid)
		{
			if (array_key_exists($nodeid, $nodesContent))
			{
				$result[$nodeid] = $nodesContent[$nodeid];

				if (($result[$nodeid]['hasowner'] > 0) AND ($result[$nodeid]['userid'] == $userid))
				{
					$result[$nodeid]['isowner'] = 1;
				}
				else
				{
					$result[$nodeid]['isowner'] = 0;
				}
			}
		}

		if (!empty($permissions))
		{
			$this->addPermissionInfo($result,$permissions);
		}

		return $result;
	}

	/** Determines whether the current user can edit a node
	*
	*	@param	int		The nodeid
	* 	@param	int		optional parentid
	* 	@param	int		optional usercontext
	*
	*	@return	bool
	**/
	public function getCanEdit($node, $parentid, $userContext)
	{
		if (vB::getUserContext()->isSuperAdmin())
		{
			return true;
		}

		if ($userContext->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid']))
		{
			return true;
		}

		if ($node['userid'] == vB::getCurrentSession()->get('userid'))
		{
			$limits = vB::getUserContext()->getChannelLimits($node['nodeid']);

			if (!$limits OR empty($limits['edit_time']))
			{
				//There is no edit timeout set;
				return true;
			}

			if ($node['publishdate'] + ($limits['edit_time'] * 3600) <= vB::getRequest()->getTimeNow())
			{
				return false;
			}
			return true;
		}
		return false;
	}

	/*** Assembles the response for detailed content
	 *
	 *	@param	mixed	assertor response object
	 *
	 *	@return	mixed	formatted data
	 ***/
	public function assembleContent(&$content)
	{
		// get the class name for this content type to add to results
		$contentTypeClass = vB_Types::instance()->getContentTypeClass($this->contenttypeid);
		$userContext = vB::getUserContext();
		//We can save some time by saving, for a page load, the list of
		// channels we already know the current user can't post.

		static $noComment = array();
		$results = array();
		foreach($content AS $record)
		{
			$record['canview'] = 1;

			if ($this->getCanEdit($record, $record['parentid'], $userContext))
			{
				$record['canedit'] = 1;
			}
			else
			{
				$record['canedit'] = 0;
			}

			if ($userContext->getChannelPermission('moderatorpermissions', 'canmoderateposts', $record['nodeid'], false, $record['parentid'] ))
			{
				$record['canmoderate'] = 1;
			}
			else
			{
				$record['canmoderate'] = 0;
			}
			$record['contenttypeclass'] = $contentTypeClass;

			if (($this->textCountChange > 0) AND $this->isPublished($record))
			{
				$record['textcount_1'] = $record['textcount'] + 1 ;
				$record['totalcount_1'] = $record['totalcount'] + 1 ;
			}
			else
			{
				$record['textcount_1'] = $record['textcount'];
				$record['totalcount_1'] = $record['totalcount'] ;
			}

			$authorContext = vB::getUserContext($record['userid']);
			$authorInfo = vB_User::fetchUserinfo($record['userid']);

			// Add signature if enabled
			$signature = $signaturepic = array();
			if ($this->showSignature)
			{
				$currentUserInfo = vB_User::fetchUserinfo();

				if (isset($authorInfo['signature']) AND !empty($authorInfo['signature']) AND
					$this->options['allow_signatures'] AND
					//					(!$currentUserInfo['userid'] OR $currentUserInfo['showsignatures']) AND
					$authorContext->hasPermission('genericpermissions', 'canusesignature')
					)
				{
					$signature['raw'] = trim($authorInfo['signature']);
					$signature['permissions'] = array(
						'dohtml' => $authorContext->hasPermission('signaturepermissions', 'allowhtml'),
						'dobbcode' => $authorContext->hasPermission('signaturepermissions', 'canbbcode'),
						'dobbimagecode' => $authorContext->hasPermission('signaturepermissions', 'allowimg'),
						'dosmilies' => $authorContext->hasPermission('signaturepermissions', 'allowsmilies'),
					);
				}
				if (isset($authorInfo['sigpic']) AND !empty($authorInfo['sigpic']) AND
					$authorContext->hasPermission('signaturepermissions', 'cansigpic'))
				{
					$signaturepic['sigpic'] = $authorInfo['sigpic'];
					$signaturepic['sigpicrevision'] = $authorInfo['sigpicrevision'];
					$signaturepic['sigpicdateline'] = $authorInfo['sigpicdateline'];;
				}
			}
			$record['signature'] = $signature;
			$record['signaturepic'] = $signaturepic;

			// Add userinfo for reputation
			if (($record['userid'] > 0) AND !empty($authorInfo))
			{
				$reputationLib = vB_Library::instance('reputation');
				$record['reputation'] = $authorInfo['reputation'];
				$record['showreputation'] = $authorInfo['showreputation'];
				$record['reputationlevelid'] = $authorInfo['reputationlevelid'];
				$record['reputationpower'] = $reputationLib->fetchReppower($authorInfo);
				$record['reputationimg'] = $reputationLib->fetchReputationImageInfo($authorInfo);
			}
			$record['allow_post'] = ($record['nodeoptions'] & vB_Api_Node::
			OPTION_ALLOW_POST) ? 1 : 0;
			$record['moderate_comments'] = ($record['nodeoptions'] & vB_Api_Node::OPTION_MODERATE_COMMENTS) ? 1 : 0;
			$record['approve_membership'] = ($record['nodeoptions'] & vB_Api_Node::OPTION_AUTOAPPROVE_MEMBERSHIP) ? 1 : 0;
			$record['invite_only'] = ($record['nodeoptions'] & vB_Api_Node::OPTION_NODE_INVITEONLY) ? 1 : 0;
			$record['createpermissions'] = $userContext->getCanCreate($record['nodeid']);

			//we can comment if there is at least one content type we can create, and the channel (in case this is a blog or
			//social group) doesn't have comments disabled. So that's going to require another scan of the records
			$record['can_comment'] = 0;
			if (($record['nodeoptions'] & vB_Api_Node::OPTION_ALLOW_POST) > 0)
			{
				$checkCommentPerms = array();
				foreach ($record['createpermissions'] AS $perm)
				{
					if (intval($perm))
					{
						$record['can_comment'] = 1;
						break;
					}
				}

				if ($record['can_comment'] == 1)
				{
					//If we were called with fullcontent then we already have the channel.
					if (!empty($record['channelid']))
					{
						$channel = $this->nodeApi->getNode($record['channelid']);
						//We need to check the nodeoptions field.
						if ($channel['nodeoptions'] & vB_Api_Node::OPTION_ALLOW_POST == 0)
						{
							$record['can_comment'] = 0;
						}
					}
					else
					{
						$checkCommentPerms[$record['nodeid']] = $record['parentid'];
					}
				}

				$record['can_flag'] = intval($userContext->fetchUserId()) ? 1 : 0;
			}

			$results[$record['nodeid']] = $record;
		}

		return $results;
	}

	/** This adds the permissions information to a nodelist if requested
	*
	*
	***/
	protected function addPermissionInfo(&$nodes, $permissions = array())
	{
		if (!empty($permissions))
		{
			foreach ($nodes AS $nodeKey => $node)
			{
				$nodes[$nodeKey]['permissions'] = array();
				foreach ($permissions as $permKey => $permission_types)
				{
					if (is_array($permission_types))
					{
						$nodes[$nodeKey]['permissions'][$permKey] = array();
						foreach ($permission_types as $permission_type)
						{
							$nodes[$nodeKey]['permissions'][$permKey][$permission_type] = vB::getUserContext()->getChannelPermission($permKey, $permission_type, $node['nodeid'], false,
									$node['parentid']);
						}
					}
				}
			}
		}
	}

	/** Checks for any subcontent that has either a publish or unpublish date
	 *
	 *	@param	int		the nodeid we are updating.
	 *
	 ***/
	public function timedCountUpdates($nodeid)
	{
		static $processed = array();

		if (in_array($nodeid, $processed))
		{
			return true;
		}
		$processed[] = $nodeid;
		$timeNow = vB::getRequest()->getTimeNow();

		//First get a list of expired items.
		$query = $this->assertor->assertQuery('vBForum:getNeedUpdate', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
		 'nodeid' => $nodeid, 'timenow' => $timeNow));

		$updates = array();
		foreach ($query as $node)
		{
			$updates[] = $node['nodeid'];
		}

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

		if (empty($updates))
		{
			return true;
		}

		$nodes = $this->nodeApi->getNodes($updates);
		foreach ($nodes as $node)
		{
			if (($node['publishdate'] > 0) AND
				($node['publishdate'] <= $timeNow) AND
				(($node['unpublishdate'] <= 0) OR ($node['unpublishdate'] >= $timeNow) OR
				($node['unpublishdate'] < $node['publishdate'])
				))
			{
				$published = 1;
			}
			else
			{
				$published = 0;
			}

			//We are concerned about two possibilities. It could have gone from published to unpublished.
			//From published to unpublished.
			if ($published)
			{
				$textChange = $this->textCountChange;
				$textUnPubChange = -1 * $textChange;
				$totalPubChange = $node['totalunpubcount'] + $textChange;
				$totalUnPubChange = -1 * $totalPubChange;
			}
			//or from unpublished to published.
			else
			{
				$textUnPubChange = $this->textCountChange;
				$textChange = -1 * $textUnPubChange;
				$totalUnPubChange = $node['totalcount'] + $textUnPubChange;
				$totalPubChange = -1 * $totalUnPubChange;
			}

			// now update the timestamps
			$nextUpdate = 0;

			if ($node['publishdate'] > $timeNow)
			{
				$nextUpdate = $node['publishdate'];
			}

			if ($node['unpublishdate'] > $timeNow AND
				(($nextUpdate == 0) OR ($nextUpdate > $node['unpublishdate'])))
			{
				$nextUpdate = $node['unpublishdate'];
			}

			$this->assertor->update('node', array(
				'lastupdate' => $timeNow,
				'nextupdate' => $nextUpdate,
				'showpublished' => $published
			), array('nodeid' => $node['nodeid']));

			$this->updateParentCounts($node['nodeid'], $textChange, $textUnPubChange, $totalPubChange, $totalUnPubChange, $published);

			$this->assertor->assertQuery('vBForum:setNextUpdate', array('lastupdate' => $timeNow, 'nodeid' => $node['nodeid'],
				'publishdate' => intval($node['publishdate']), 'unpublishdate' => intval($node['unpublishdate'])));

		}
		//The only reason this happens is that something has become either published or unpublished.
		vb_Cache::instance()->event('perms_changed');
	}


	public function getIndexableFromNode($node, $include_attachments = true)
	{
		//merge in the attachments if any
		if ($include_attachments)
		{
			$indexableContent = $this->getIndexableContentForAttachments($node['nodeid']);
		}
		else
		{
			$indexableContnet = array();
		}

		$indexableContent['title'] = $node['title'];
		return $indexableContent;

	}

	/**
	 * The classes  that inherit this should implement this function
	 * It should return the content that should be indexed
	 * If there is a title field, the array key for that field should be 'title',
	 * the rest of the text can have any key
	 * @param int $nodeId
	 * @return array $indexableContent
	 */
	public function getIndexableContent($nodeId, $include_attachments = true)
	{
		$node = $this->nodeApi->getNodeContent($nodeId);
		$node = $node[$nodeId];
		if (empty($node))
		{
			return false;
		}

		return $this->getIndexableFromNode($node, $include_attachments);
	}

	/*** Finds the correct conversation starter for a node
	 *
	 *	@param	int		nodeid of the item being checked
	 *
	 *	@return	int		the conversation starter's nodeid
	 ***/
	public function getStarter($nodeid)
	{
		$parents = $this->getParents($nodeid);
		$channelContentTypeId = vB_Types::instance()->getContentTypeId('vBForum_Channel');

		foreach ($parents as $sequence => $parent)
		{
			if ($parent['contenttypeid'] == $channelContentTypeId)
			{
				if ($sequence == 0)
				{
					return $nodeid;
				}
				else
				{
					return $parents[$sequence - 1]['nodeid'];
				}
			}
		}

	}

	/*** Gets the main conversation node.
	 *
	 * 	@param	int		the nodeid
	 * 	@return	mixed	the main conversation node
	 */
	public function getConversationParent($nodeid)
	{
		if (empty($nodeid))
		{
			return false;
		}
		$parentId = $this->getStarter($nodeid);
		$parentNode = $this->nodeApi->getNode($parentId);

		return $parentNode;
	}


	/** determines whether a specific node is a visitor message
	 *
	 *	@param	int
	 *
	 *	@return bool
	 **/
	public function isVisitorMessage($nodeid)
	{
		//Something is a visitor message if it's a descendant in the closure table of the protected channel
		// for visitor messages
		$test = vB_Library::instance('node') -> fetchClosureParent($nodeid, $this->nodeApi->fetchVMChannel());
		reset($test);
                $test = current($test);
		return ($test AND !empty($test['child']));
	}


	/** Sends notifications when specific events occur
	 *
	 *	@param	mixed	array of about, aboutid, sentto.
	 **/
	public function sendNotifications($notifications)
	{
		$messageLibrary = vB_Library::instance('Content_Privatemessage');
		foreach ($notifications as $notification)
		{
			$data = array('msgtype' => 'notification', 'about' => $notification['about'],
			'rawtext' => '',
			'aboutid' => $notification['aboutid'],
			'sentto' => $notification['userid']);
			$nodeid = $messageLibrary->addMessageNoFlood($data);
		}
	}

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

	/**
	 * Get the indexable content for attachments.  Separate this so that child classes
	 * can rewrite getIndexableContent entirely without having to copy as much code
	 */
	protected function getIndexableContentForAttachments($nodeId)
	{
		$attachments = vB_Api::instanceInternal('Node')->getNodeAttachments($nodeId);
		$indexableContent = array();
		foreach ($attachments as $attach)
		{
			if ($attach['nodeid'] == $nodeId)
			{
				continue;
			}
			$indexableContent[$attach['nodeid']] = $attach['caption'] . ' ' . $attach['filename'];
		}
		return $indexableContent;
	}



	/** returns the tables used by this content type.
	 *
	 *	@return	Array
	 *
	 **/
	public function fetchTableName()
	{
		if (is_array($this->tablename))
		{
			return $this->tablename;
		}
		return array($this->tablename);
	}

	/** This attempts to get the cached data for nodes
	*
	*	@param	mixed		integer or array of integers
	* 	@param	integer		one of the constants for level of data
	*
	*	@return	mixed		array('found' => array of node values per the constant, 'notfound' => array of nodeids);
	*
	***/
	public static function fetchFromCache($nodeids, $level)
	{
		if (!is_array($nodeids))
		{
			$nodeids = array($nodeids);
		}
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		$found = array();
		$notfound = array();
		foreach ($nodeids AS $nodeid)
		{

			if (!intval($nodeid))
			{
				throw new vB_Exception_Api('invalid_data');
			}
			switch ($level)
			{
				case self::CACHELEVEL_NODE:
					$hashKey = 'node_' . $nodeid . "_lvl1data";
					$data = $cache->read($hashKey);

					if (!empty($data))
					{
						//Got a hit.
						$found[$nodeid] = $data;
						break;
					}
					//Otherwise we try the next level.
				case self::CACHELEVEL_CONTENT:
					$hashKey = 'node_' . $nodeid . "_lvl2data";
					$data = $cache->read($hashKey);

					if (!empty($data))
					{
						//Got a hit.
						//If the user just wanted $node data, let's dump the content
						if ($level == self::CACHELEVEL_NODE)
						{
							unset($data['content']);
						}
						$found[$nodeid] = $data;
						break;
					}
					//Otherwise we try the next level.
				case self::CACHELEVEL_FULLCONTENT:
					$hashKey = 'node_' . $nodeid . "_lvl3data";
					$data = $cache->read($hashKey);

					if (!empty($data))
					{
						//Got a hit.
						$found[$nodeid] = $data;
						break;
					}
				//Nope- no data.
				$notfound[] = $nodeid;
			}
		}
		return array('found' => $found, 'notfound' => $notfound);
	}

	/** writes new cached data for nodes
	 *
	 *	@param	mixed		array of node values
	 * 	@param	integer		one of the constants for level of data
	 *
	 *	@return	mixed		array('found' => array of node values per the constant, 'notfound' => array of nodeids);
	 *
	 ***/
	public static function writeToCache($nodes, $level)
	{
		$cache = vB_Cache::instance(vB_Cache::CACHE_FAST);

		$trace = debug_backtrace(false);

		foreach ($nodes AS $node)
		{
			//make sure data was passed correctly
			if (!empty($node['nodeid']))
			{
				$hashKey = 'node_' . $node['nodeid'] . "_lvl" . $level .  "data";
				$cache->write($hashKey, $node, 1440, 'nodeChg_' . $node['nodeid']);
			}
		}
	}

	/**
	 * Publish this node to facebook feed
	 */
	protected function publishToFacebook($nodeId, array $options = array())
	{
		// did the user request publishing this to facebook?
		if (!isset($options['facebook']) || !isset($options['facebook']['fbpublish']) || !$options['facebook']['fbpublish'])
		{
			return;
		}

		// is facebook connect & publishing available?
		if (!vB_Facebook::isFacebookEnabled())
		{
			return;
		}

//		if (!in_array($this->contenttype, array('vBForum_Text', 'vBForum_Link', 'vBForum_Poll', 'vBForum_Video'), true))
//		{
//			return;
//		}

		// get node information
		//$node = $this->nodeApi->getNode($nodeId);
		$node = $this->nodeApi->getContentForNodes(array($nodeId));
		$node = array_shift($node);

		if (!$node || !empty($node['errors']))
		{
			return;
		}

		// is the node published/visible/public in vB?
		if ($node['showpublished'] != 1 || $node['approved'] != 1)
		{
			return;
		}

		$isStarter = ($node['nodeid'] == $node['starter']);

		// can new discussion, photo, link, poll etc be published?
		if ($isStarter && !$this->options['fbfeednewthread'])
		{
			return;
		}

		// can replies to discussion, photo, link, poll etc be published?
		if (!$isStarter && !$this->options['fbfeedpostreply'])
		{
			return;
		}

		// get node URL
		$nodeUrl = $options['facebook']['baseurl'] . vB_Api::instanceInternal('route')->getUrl($node['routeid'] . '|nosession', $node, array());

		if (!vB_Facebook::instance()->userIsLoggedIn())
		{
			return;
		}

		vB_Facebook::instance()->publishFeed('message', $node['title'], $nodeUrl, $node['content']['previewtext']);
	}

	protected function verifyPrefixid($prefixid)
	{
		if ($prefixid)
		{
			$prefix = $this->assertor->getRow('vBForum:prefix', array('prefixid' => $prefixid));

			if (!$prefix)
			{
				throw new vB_Exception_Api('invalid_prefixid');
			}

			require_once(DIR . '/includes/functions_prefix.php');

			if (!can_use_prefix($prefixid))
			{
				throw new vB_Exception_Api('invalid_prefixid');
			}
		}
	}

	/**
	 * Verifies that the post iconid is valid
	 *
	 * @param	int	post icon ID
	 */
	protected function verifyPostIconid($posticonid)
	{
		if ($posticonid)
		{
			$posticon = $this->assertor->getRow('icon', array('iconid' => $posticonid));

			if (!$posticon)
			{
				throw new vB_Exception_Api('invalid_posticonid');
			}
		}
	}
}


