<?php
if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
   || #################################################################### ||G
   || # 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_Api_Content extends vB_Api
{
	const ACTION_ADD = 1;
	const ACTION_UPDATE = 2;
	const ACTION_VIEW = 3;
	const ACTION_DELETE = 4;
	const ACTION_APPROVE = 5;
	const ACTION_PUBLISH = 6;

	//needed primarily for permissions
	protected $usercontext;

	//Let's keep a pointer to the assertor
	/**
	 * @var vB_dB_Assertor
	 */
	protected $assertor;


	//the node API, which we will use a lot.
	protected $nodeApi;

	//the fields in the node table
	protected $nodeFields ;

	//we'll need some checks based on the options setting.
	protected $options;

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

	protected $notifications = array();

	protected $library;

	//Standard constructor
	protected function __construct()
	{
		parent::__construct();
		//The table for the type-specific data.
		$this->assertor = vB::getDbAssertor();
		$this->nodeApi = vB_Api::instanceInternal('node');
		$this->nodeFields = $this->nodeApi->getNodeFields();
	}

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

	/*** 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.
	 *
	 * 	@return	integer		the new nodeid
	 ***/
	public function add($data, $options = array())
	{
		if (!$this->validate($data, vB_Api_Content::ACTION_ADD))
		{
			throw new vB_Exception_Api('no_create_permissions');
		}
		$this->cleanInput($data);
		return $this->library->add($data, $options);
	}


	/** Does basic input cleaning for input data
	 	@param	mixed	array of fieldname => data pairs

	 	@return	mixed	the same data after cleaning.
	 */
	public function cleanInput(&$data, $nodeid = false)
	{
		if (isset($data['userid']))
		{
			unset($data['userid']);
		}

		if (isset($data['authorname']))
		{
			unset($data['authorname']);
		}

		if (isset($data['parentid']))
		{
			if (vB::getUserContext()->getChannelPermission('forumpermission', 'canusehtml', $data['parentid']))
			{
				return;
			}
		}
		else if ($nodeid)
		{
			if (vB::getUserContext()->getChannelPermission('forumpermission', 'canusehtml', $nodeid))
			{
				return;
			}
		}

		$cleaner = vB::getCleaner();
		foreach(array('title', 'htmltitle', 'keywords', 'pagetext', 'description', 'urlident', 'prefixid') as $fieldname)
		{
			if (isset($data[$fieldname]))
			{
				$data[$fieldname] = $cleaner->clean($data[$fieldname], vB_Cleaner::TYPE_NOHTML);
			}
		}
	}



	/*** validates that the current can create a node with these values
	 *
	 *	@param	mixed		Array of field => value pairs which define the record.
	 *	@param	action		Parameters to be checked for permission
	 *
	 * 	@return	bool
	 ***/
	public function validate(&$data, $action = self::ACTION_ADD, $nodeid = false, $nodes = false)
	{
		// Each descendant should override this function and add their own
		// check of individual required fields.
		if ((defined('VB_AREA') AND VB_AREA == 'Upgrade') OR vB::getUserContext()->isSuperAdmin())
		{
			return true;
		}

		if ($nodeid == false and !empty($data['nodeid']))
		{
			$nodeid = $data['nodeid'];
		}

		//we need a nodeid (or parentid if we are adding) or we cannot answer the question.
		if ($action ==  vB_Api_Content::ACTION_ADD)
		{
			if (empty($data['parentid']) OR !intval($data['parentid']))
			{
				throw new vB_Exception_Api('invalid_data');
			}
			$parentid = $data['parentid'];
		}
		else
		{
			if (!$nodeid)
			{
				if (empty($data['nodeid']))
				{
					throw new Exception('invalid_data');
				}
			}

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

			if (!$nodes)
			{
				$nodes = vB_Api::instanceInternal('node')->getNodes($nodeid);
			}
		}

		$userid = vB::getCurrentSession()->get('userid');
		switch ($action) {
			case   vB_Api_Content::ACTION_ADD:
				if (in_array($this->contenttype, array('vBForum_Text', 'vBForum_Poll', 'vBForum_PrivateMessage')))
				{
					// Only validate HV for specific content types.
					// To skip HV, please call library methods instead.
					vB_Api::instanceInternal('hv')->verifyToken($data['hvinput'], 'post');
				}

				//Check the node-specific permisisons first.
				$parent = vB_Api::instanceInternal('node')->getNode($parentid);

				//We need to know what the channel is, and see if it's a blog channel.
				if ($parent['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Channel'))
				{
					$channelid = $parentid;
				}
				else if ($parent['starter'] == $parentid)
				{
					//Check for the the parent's parent
					$channelid = $parent['parentid'];
				}
				else
				{
					// The channel is of course the starter's parent.
					$starter = vB_Api::instanceInternal('node')->getNode($parent['starter']);
					$channelid = $starter['parentid'];
				}

				//we need the channel information.
				if ($channelid == $parentid)
				{
					$channel = &$parent;
				}
				else
				{
					$channel  = vB_Api::instanceInternal('node')->getNode($channelid);
				}

				//Check if the user who this visitor message is for has enabled visitor messages.
				if (isset($data['setfor']) AND $data['setfor'] > 0)
				{
					$vm_user = vB_User::fetchUserinfo($data['setfor']);
					if (!$vm_user['vm_enable'])
					{
						return false;
					}
				}

				if ($this->contenttype != 'vBForum_Channel')
				{
					$sgapi = vB_Api::instanceInternal('socialgroup');
					$blogapi = vB_Api::instanceInternal('blog');

					$isSGChannel = $sgapi->isSGNode($channel['nodeid']);
					$isBlogChannel = $blogapi->isBlogNode($channel['nodeid']);

					if ($isSGChannel OR $isBlogChannel)
					{
						if ($isSGChannel)
						{
							$channel['type'] = 'sg';
						}
						else
						{
							$channel['type'] = 'blog';
						}

						if ($channel['commentperms'] == 0)
						{
							//Only blog members can contribute. We check for the channel
							if (!$userid OR $blogapi->isChannelMember($channelid) != 1)
							{
								//Not O.K.
								return false;
							}

							//If this is the starter
							if ($isBlogChannel AND $parentid == $channelid)
							{
								//This is a blog post. Only owners and moderators can start them.
								$adminPerms = $channelInfo = $blogapi->getBlogAdminPerms($channelid);
								if (empty($adminPerms) OR (!$adminPerms['canmoderate']))
								{
									return false;
								}
							}

							if ($isSGChannel AND $sgapi->isChannelMember($channelid) != 1)
							{
								// SG only Group Members can post
								return false;
							}

							//handle channel-specific settings from user.
							$this->checkChannelSettings($channel, $data, $userid);
						}
						else
						{
							//We need to check to see if it's a blog channel
							if ($isBlogChannel)
							{
								//If the current user is not registered they can't post
								if (!$userid)
								{
									return false;
								}

								if ($parentid == $channelid)
								{
									//This is a blog post. Only owners and moderators can start them.
									$adminPerms = $channelInfo = $blogapi->getBlogAdminPerms($channelid);

									if (empty($adminPerms) OR (!$adminPerms['canmoderate']))
									{
										return false;
									}
								}
							}
							elseif ($isSGChannel)
							{
								//If the current user is not registered they can't post
								if (!$userid)
								{
									return false;
								}
							}
							$this->checkChannelSettings($channel, $data, $userid);
						}
					}
				}

				//check the showPublished.
				if (($parent['showpublished'] == 0) )
				{
					if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $parentid))
					{
						return false;
					}
				}

				//if the parent has canPost
				if (vB::getUserContext()->getChannelPermission('createpermissions', $this->contenttype, $parentid))
				{
					return true;
				}
				return false;
				break;

			case   vB_Api_Content::ACTION_UPDATE:
				//There are a couple of ways this user could be allowed to edit this record.
				// As a moderator
				foreach ($nodes as $node)
				{
					if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid'], false, $node['parentid']))
					{
						continue;
					}
					//Or because this is their content and it's within the date range. We need to pull the record.

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

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

					//if we got here the user isn't authorized to update this record.
					return false;
				}
				return true;
				break;


			case   vB_Api_Content::ACTION_VIEW:
				foreach ($nodes as $node)
				{
					if (empty($node['nodeid']) OR !is_numeric($node['nodeid']) )
					{
						$check = current($node);

						if (!empty($check['nodeid']) AND is_numeric($check['nodeid']) )
						{
							$node = $check;
						}
					}
					//This hits the 99% case
					if ((($node['userid'] == $userid) OR (($node['viewperms'] == 2) AND ($node['showpublished'] > 0))) 
						AND ($node['showapproved'] OR (!$node['showapproved'] AND $node['userid'] == $userid))
					)
					{
						continue;
					}

					if (!$node['showapproved'] AND !vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $node['nodeid'], false, $node['parentid']))
					{
						throw new vB_Exception_NodePermission($node['nodeid']);
					}
					
					if (!$node['showpublished'] AND !vB::getUserContext()->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid'], false, $node['parentid']))
					{
						throw new vB_Exception_NodePermission($node['nodeid']);
					}

					if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid'], false, $node['parentid']))
					{
						continue;
					}
					if ($node['viewperms'] == 0)
					{
						//Only blog members can view.  We need to find the channel
						if ($node['contenttypeid'] == vB_Types::instance()->getContentTypeId('vBForum_Channel'))
						{
							$checkNodeId = $node['nodeid'];
						}
						else if ($node['starter'] == $node['nodeid'])
						{
							//Check for the the parent's parent
							$checkNodeId = $node['parentid'];
						}
						else
						{
							// The channel is of course the starter's parent.
							$starter = vB_Api::instanceInternal('node')->getNode($node['starter']);
							$checkNodeId = $starter['parentid'];
						}

						$groupInTopic = vB_Api::instanceInternal('user')->getGroupInTopic($userid, $checkNodeId);

						if (!$groupInTopic OR empty($groupInTopic) OR !empty($groupInTopic['errors']))
						{
							//someone with moderator permission can view
							if (vB::getUserContext()->getChannelPermission('moderatorpermissions', 'caneditposts', $node['nodeid'], false, $node['parentid']))
							{
								continue;
							}
							//Not O.K.
							throw new vB_Exception_NodePermission($node['nodeid']);
						}

						$validGroups = vB_Api::instanceInternal('usergroup')->fetchPrivateGroups();
						$found = false;
						foreach($groupInTopic as $pair)
						{
							if (in_array($pair['groupid'], $validGroups))
							{
								$found = true;
								break;
							}
						}
						if (!$found)
						{
							throw new vB_Exception_NodePermission($node['nodeid']);
						}
					}
					else if (($node['viewperms'] == 1) AND ($userid < 1))
					{
						throw new vB_Exception_NodePermission($node['nodeid']);
					}


					if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'canview', $node['nodeid'], false, $node['parentid']))
					{
						throw new vB_Exception_NodePermission($node['nodeid']);
					}
				}

				return true;

				break;

			case   vB_Api_Content::ACTION_DELETE:
				foreach ($nodes as $node)
				{
					if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canremoveposts', $node['nodeid']))
					{
						return false;
					}
				}
				return true;
				break;
			case   vB_Api_Content::ACTION_APPROVE:
				foreach ($nodes AS $node)
				{
					return vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmoderateposts', $node['nodeid']);
				}
				break;
			case   vB_Api_Content::ACTION_PUBLISH:
				foreach ($nodes AS $node)
				{
					return vB::getUserContext()->getChannelPermission('forumpermissions', 'canpublish', $node['nodeid']);
				}
				break;
			default:
			;
		} // switch
	}

	/*** Permanently deletes a node
	 *	@param	integer	The nodeid of the record to be deleted
	 *
	 *	@return	boolean
	 ***/
	public function delete($nodeid)
	{
		$data = false;

		if (!$this->validate($data, self::ACTION_DELETE, $nodeid))
		{
			throw new vB_Exception_Api('no_delete_permissions');
		}

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

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

	/*** Is this record in a published state based on the times?
	*
	*	@param	mixed
	*
	*	@return	bool
	****/
	public function isPublished($data)
	{
		return $this->library->isPublished($data);
	}


	/*** 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)
	{
		if (!$this->validate($data, self::ACTION_UPDATE, $nodeid))
		{
			throw new vB_Exception_Api('no_update_permissions');
		}
		$this->cleanInput($data);
		return $this->library->update($nodeid, $data);
	}

	/*** Returns the node content as an associative array
	 *	@param	integer	The id in the primary table
	 *	@param array permissions
	 *	@param bool	appends to the content the channel routeid and title, and starter route and title the as an associative array

	 *	@return	int
	 ***/
	public function getContent($nodeid, $permissions = false)
	{
		$temporary = $this->library->getContent($nodeid, $permissions);
		$data = array();

		if (!$this->validate($data, self::ACTION_VIEW, $nodeid, $temporary))
		{
			throw new vB_Exception_Api('no_permissions');
		}
		return $temporary;
	}

	/*** 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
	 *
	 *	@return	mixed
	 ***/
	public function getFullContent($nodeid, $permissions = false)
	{
		$temporary = $this->library->getFullContent($nodeid, $permissions);
		$data = array();

		if (!$this->validate($data, self::ACTION_VIEW, $nodeid, $temporary))
		{
			throw new vB_Exception_Api('no_permissions');
		}
		return $temporary;
	}


	/*** Gets the main conversation node.
	*
	* 	@param	int		the nodeid
	* 	@return	mixed	the main conversation node
	*/
	public function getConversationParent($nodeid)
	{
		return $this->library->getConversationParent($nodeid);
	}


	/***Returns the ancestry
	 *
	 * 	@param	int		the nodeid
	 * 	@return	mixed	array of partial node records
	 */
	public function getParents($nodeid)
	{
		return $this->library->getParents($nodeid);
	}

	/*** Finds the correct conversation starter for a node
	 *
	 *	@param	int		nodeid of the item being checked
	 *
	 *	@return	int		the conversation starter's nodeid
	 ***/
	protected function getStarter($nodeid)
	{
		return $this->library->getStarter($nodeid);
	}


	/*** Processing after a move. In this case, set the text counts and "last" data.
	 *
	 * 	@param	int		the nodeid
	 * 	@param	int		the old parentid
	 * 	@param	int		the new parentid
	 */

	public function afterMove($nodeid, $oldparent, $newparent)
	{
		//We add the various counts to the new parent, and subtract from the old.
		$node = $this->nodeApi->getNode($nodeid);

		$published = $this->isPublished($node);
		$this->assertor->assertQuery('vBForum:UpdateParentCount',
		array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
		'nodeid' => $oldparent,
		'textChange' => (-1 * $published), 'textUnpubChange' => (-1 * !$published)));

		$this->assertor->assertQuery('vBForum:UpdateParentCount',
		array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
		'nodeid' => $newparent,
		'textChange' => (1 * $published), 'textUnpubChange' => (1 * !$published)));

		$oldParentAncestors = $newParentAncestors = array();

		// fetch ancestors
		$ancestors = $this->assertor->assertQuery('vBForum:getParents', array(
			vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
			'nodeid' => array($oldparent, $newparent)
		));
		if ($ancestors)
		{
			foreach($ancestors AS $ancestor)
			{
				if ($ancestor['child'] == $oldparent)
				{
					if (array_key_exists($ancestor['parent'], $newParentAncestors))
					{
						// common ancestor, remove it
						unset($newParentAncestors[$ancestor['parent']]);
					}
					else
					{
						$oldParentAncestors[$ancestor['parent']] = $ancestor['parent'];
					}
				}
				else if ($ancestor['child'] == $newparent)
				{
					if (array_key_exists($ancestor['parent'], $oldParentAncestors))
					{
						// common ancestor, remove it
						unset($oldParentAncestors[$ancestor['parent']]);
					}
					else
					{
						$newParentAncestors[$ancestor['parent']] = $ancestor['parent'];
					}
				}
			}
		}

		if (!empty($oldParentAncestors))
		{
			$this->assertor->assertQuery('vBForum:UpdateAncestorCount',
				array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
				'nodeid' => $oldParentAncestors,
				'totalChange' => -1 * ($node['totalcount'] + (1 * $published)), 'totalUnpubChange' => -1 * ($node['totalunpubcount'] + (1 * !$published)))
			);
		}

		if (!empty($newParentAncestors))
		{
			$this->assertor->assertQuery('vBForum:UpdateAncestorCount',
				array(vB_dB_Query::TYPE_KEY =>vB_dB_Query::QUERY_STORED,
				'nodeid' => $newParentAncestors,
				'totalChange' => ($node['totalcount'] + (1 * $published)), 'totalUnpubChange' => ($node['totalunpubcount'] + (1 * !$published))));
		}

		// reset last content for all parents that have the deleted node
		$this->assertor->update('node',
				array(
					'lastcontent' => 0,
					'lastcontentid' => 0,
					'lastcontentauthor' => '',
					'lastauthorid' => 0),
				array(
					'nodeid' => $oldparent
				)
		);

		$this->assertor->assertQuery('vBForum:updateLastData', array('parentid' => $oldparent, 'timenow' => vB::getRequest()->getTimeNow()));
		$this->assertor->assertQuery('vBForum:updateLastData', array('parentid' => $newparent, 'timenow' => vB::getRequest()->getTimeNow()));
	}

	/**
	 * 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)
	{
		return $this->library->getIndexableContent($nodeId, $include_attachments);
	}


	/**
	 * Returns an array with bbcode options for the node.
	 * @param type $nodeId
	 */
	public function getBbcodeOptions($nodeId)
	{
		// This method needs to be overwritten for each relevant contenttype
		return array();
	}

	/** Gives the current board time- needed to set publishdate.
	*
	*	@return INT
	*
	**/
	public function getTimeNow()
	{
		return vB::getRequest()->getTimeNow();
	}

	/** This returns the text to quote a node. Used initially for private messaging.
	*
	* 	@param	integer		the nodeid of the quoted item
	*
	*	@return	string		quote text.
	**/
	public function getQuoteText($nodeid)
	{
		//This must be implemented in the child class
		{
			throw new vB_Exception_Api('feature_not_implemented');
		}
	}


	/** This returns the text to quote a node. Used initially for private messaging.
	 *
	 * 	@param	integer		the nodeid of the quoted item
	 *
	 *	@return	string		quote text.
	 **/
	public function createQuoteText($nodeid, $pageText)
	{
		//This must be implemented in the child class
		{
			throw new vB_Exception_Api('feature_not_implemented');
		}
	}


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


	/** determines whether a specific node is a visitor message
	 *
	 *	@param	int
	 *
	 *	@return bool
	 **/
	public function isVisitorMessage($nodeid)
	{
		return $this->library->isVisitorMessage($nodeid);
	}

	/** Extracts the video and photo content from text.
	 *
	 *	@param	string
	 *
	 *	@return	mixed	array of "photo", "video". Each is an array of images.
	 **/
	public function extractMedia($rawtext)
	{
		$photos = array();
		$videos = array();
		$filter = '~\[video.*\[\/video~i';
		$matches = array();
		$count = preg_match_all($filter, $rawtext, $matches);
		return $matches;
		$options = explode(';', $option);
		$provider = strtolower($options[0]);
		$code = $options[1];

		if (!$code OR !$provider)
		{
			return '[video=' . $option . ']' . $url . '[/video]';
}

		$templater = new vB5_Template('bbcode_video');
		$templater->register('url', $url);
		$templater->register('provider', $provider);
		$templater->register('code', $code);

	}

	/**
	 * Adds content info to $result so that merged content can be edited.
	 * This method needs to be implemented in the content subclasses that support merging.
	 * @param array $result
	 * @param array $content
	 */
	public function mergeContentInfo(&$result, $content)
	{
		// content cannot be merged unless this method is implemented
		throw new vB_Exception_Api('merge_invalid_contenttypes');
	}

	/*** For posts to blog-type channel, checks and if appropriate sets publishdate to zero or throws an exception.
	 *
	 *	@param	mixed	the channel to which we are posting
	 *	@param	mixed	reference to the data we are going to add
	 ***/
	protected function checkChannelSettings($channel, &$data, $userid)
	{
		if (vB::getUserContext()->isSuperAdmin())
		{
			return true;
		}

		if ($channel['type'] == 'blog' AND vB_Api::instanceInternal('blog')->fetchOwner($channel['nodeid']) == $userid)
		{
			return true;
		}

		//If we set the OPTION_ALLOW_POST off then unless we're owner or superadmin we can't add
		if (!intval($channel['allow_post']))
		{
			throw new vB_Exception_Api('no_permission');
		}
	}
}


