<?php if (!defined('VB_ENTRY')) die('Access denied.');
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 5.5.2
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2019 MH Sub I, LLC dba vBulletin. 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   # ||
|| ###################################################################### ||
\*========================================================================*/

/**
*/
class vB_Tags
{
	use vB_Trait_NoSerialize;

	/********************************************************
	 *	Private Members
	 ********************************************************/

	protected $nodeid;
	protected $contentinfo = null;
	protected $tags = null;
	protected $currentUserId = false;
	protected $owner = false;
	protected $assertor;

	/********************************************************
	*	Constructors / Factory Methods
	********************************************************/

	/**
	*	Create a taggable content item.
	*
	* @param int id for the content item to be tagged. Can be 0 if it is used only for validating tags
	* @param array content info -- database record for item to be tagged, values vary by
	*	specific content item.  For performance reasons this can be included, otherwise the
	* 	data will be fetched if needed from the provided id.
	*/

	public function __construct($nodeid = 0, $contentinfo = false)
	{
		$this->nodeid = $nodeid;
		$this->assertor = vB::getDbAssertor();
		$this->currentUserId = vB::getCurrentSession()->get('userid');

		// If this is node related fetch the required info
		if ($this->nodeid)
		{
			$this->owner = $this->getNodeOwner($this->nodeid);
			if ($contentinfo)
			{
				$this->contentinfo = $contentinfo;
			}
			else
			{
				$this->loadContentInfo();
			}
		}
	}

	/**
	*	Takes a list of tags and returns a list of valid tags
	*
	* Tags are transformed to removed tabs and newlines
	* Tags may be lowercased based on options
	* Tags matching synomyns will
	* Duplicate will be eliminated (case insensitive)
	* Invalid tags will be removed.
	*
	* Fetch the valid tags from a list. Filters are length, censorship, perms (if desired).
	*
	* @param	string|array	List of tags to add (comma delimited, or an array as is). If array, ensure there are no commas.
	* @param	array			(output) List of errors that happens
	* @param	boolean		Whether to expand the error phrase
	*
	* @return	array			List of valid tags
	*/
	public function filterTagList($taglist, &$errors, $evalerrors = true)
	{
		$options = vB::getDatastore()->get_value('options');
		$errors = array();
		if (!is_array($taglist))
		{
			$taglist = $this->splitTagList($taglist);
		}
		//This seems like a terrible place to put this, but I don't know where else it should go.
		if ($options['tagmaxlen'] <= 0 OR $options['tagmaxlen'] >= 100)
		{
			$options['tagmaxlen'] = 100;
		}

		$validRaw = array();

		foreach ($taglist AS $tagtext)
		{
			$tagtext = trim(preg_replace('#[ \r\n\t]+#', ' ', $tagtext));
			if ($this->isTagValid($tagtext, $errors))
			{
				$validRaw[] = ($options['tagforcelower'] ? vB_String::vBStrToLower($tagtext) : $tagtext);
			}
		}

		if (empty($validRaw))
		{
			$errors['no_valid_tags'] = 'no_valid_tags_found';
			return array();
		}
		$validRaw = $this->convertSynonyms($validRaw, $errors);
		// we need to essentially do a case-insensitive array_unique here
		$validUnique = array_unique(array_map('vB_String::vBStrToLower', $validRaw));
		$valid = array();
		foreach (array_keys($validUnique) AS $key)
		{
			$valid[] = $validRaw["$key"];
		}
		$validUnique = array_values($validUnique); // make the keys jive with $valid
		//if requested compose the error messages to strings
		if ($evalerrors)
		{
			$errors = fetch_error_array($errors);
		}
		return $valid;
	}

	/**
	*	Delete tag attachments for a list of content items
	*
	* @param array $nodeidids
	*/
	protected function deleteTagAttachmentsList($nodeids)
	{
		if ($this->contentinfo === null)
		{
			$this->loadContentInfo();
		}

		$canDelete = true;
		$unDeletableTags = "";

		foreach ($this->contentinfo as $tag)
		{
			if (!($this->canDeleteTag($tag)))
			{
				$canDelete = false;
				$unDeletableTags .= $tag["tagtext"] . ",";
			}
		}

		if (!$canDelete)
		{
			throw new vB_Exception_Api('no_delete_permissions_for_tag');
		}

		foreach($nodeids as $nodeid)
		{
			$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE, vB_dB_Query::CONDITIONS_KEY =>
				array('nodeid' => $nodeid));
			$this->assertor->assertQuery('vBForum:tagnode', $data);

			$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE, vB_dB_Query::CONDITIONS_KEY =>
				array('nodeid' => $nodeid), 'taglist' => '');
			$this->assertor->assertQuery('vBForum:node', $data);
		}
		$this->invalidateTagList();
	}

	/********* provides a list of content types

	/**
	*	Checks to see if the tag is valid.
	*
	* Does not check the validity of any tag associations.
	* @param 	string $tagtext tag text to validate
	* @param	array	$errors (output) List of errors that happens
	*/
	protected function isTagValid($tagtext, &$errors)
	{
		static $taggoodwords = null;
		static $tagbadwords = null;
		$options = vB::getDatastore()->get_value('options');

		// construct stop words and exception lists (if not previously constructed)
		if (is_null($taggoodwords) or is_null($tagbadwords))
		{
			// filter the stop words by adding custom stop words (tagbadwords) and allowing through exceptions (taggoodwords)
			if (!is_array($tagbadwords))
			{
				$tagbadwords = preg_split('/\s+/s', vB_String::vBStrToLower($options['tagbadwords']), -1, PREG_SPLIT_NO_EMPTY);
			}

			if (!is_array($taggoodwords))
			{
				$taggoodwords = preg_split('/\s+/s', vB_String::vBStrToLower($options['taggoodwords']), -1, PREG_SPLIT_NO_EMPTY);
			}

			// get the stop word list; allow multiple requires
			// merge hard-coded badwords and tag-specific badwords
			$tagbadwords = array_merge(vB_Badwords::getBadWords(), $tagbadwords);
		}

		if ($tagtext === '')
		{
			return false;
		}

		if (in_array(vB_String::vBStrToLower($tagtext), $taggoodwords))
		{
			return true;
		}

		$charStrlen = vB_String::vbStrlen($tagtext, true);
		if ($options['tagminlen'] AND $charStrlen < $options['tagminlen'])
		{
			$errors['min_length'] = array('tag_too_short_min_x', $options['tagminlen']);
			return false;
		}

		if ($charStrlen > $options['tagmaxlen'])
		{
			$errors['max_length'] = array('tag_too_long_max_x', $options['tagmaxlen']);
			return false;
		}

		if (strlen($tagtext) > 100)
		{
			// only have 100 bytes to store a tag
			$errors['max_length'] = array('tag_too_long_max_x', $options['tagmaxlen']);
			return false;
		}

		$censored = fetch_censored_text($tagtext);
		if ($censored != $tagtext)
		{
			// can't have tags with censored text
			$errors['censor'] = 'tag_no_censored';
			return false;
		}

		if (count($this->splitTagList($tagtext)) > 1)
		{
			// contains a delimiter character
		//	$errors['comma'] = $evalerrors ? fetch_error('tag_no_comma') : 'tag_no_comma';
			$errors['comma'] = 'tag_no_comma';
			return false;
		}

		if (in_array(strtolower($tagtext), $tagbadwords))
		{
			if(isset($errors['common']))
			{
				$tagtext = trim($errors['common'][1]).', '.$tagtext;
			}
			$errors['common'] = array('tag_x_not_be_common_words', ' '.$tagtext.' ');
			return false;
		}
		return true;
	}

	/**
	* Splits the tag list based on an admin-specified set of delimiters (and comma).
	*
	* @param	string	List of tags
	*
	* @return	array	Tags in seperate array entries
	* temporarily make public
	*/
	protected function splitTagList($taglist)
	{
		static $delimiters = array();
		$taglist = unhtmlspecialchars($taglist);
		$options = vB::getDatastore()->get_value('options');

		if (empty($delimiters))
		{
			$delimiter_list = $options['tagdelimiter'];
			$delimiters = array(',');

			// match {...} segments as is, then remove them from the string
			if (preg_match_all('#\{([^}]*)\}#s', $delimiter_list, $matches, PREG_SET_ORDER))
			{
				foreach ($matches AS $match)
				{
					if ($match[1] !== '')
					{
						$delimiters[] = preg_quote($match[1], '#');
					}
					$delimiter_list = str_replace($match[0], '', $delimiter_list);
				}
			}

			// remaining is simple, space-delimited text
			foreach (preg_split('#\s+#', $delimiter_list, -1, PREG_SPLIT_NO_EMPTY) AS $delimiter)
			{
				$delimiters[] = preg_quote($delimiter, '#');
			}
		}

		$taglist = preg_split('#(' . implode('|', $delimiters) . ')#', $taglist, -1, PREG_SPLIT_NO_EMPTY);

		return array_map('vB_String::htmlSpecialCharsUni', $taglist);
	}

	/**
	*	Converts synomyns to canonical tags
	*
	* If a tag is converted a message will be added to the error array to alert the user
	* Does not handle removing duplicates created by the coversion process
	*
	* @param array array of tags to convert
	* @param array array of errors (in/out param)
	*
	*	@return array the new list of tags
	*/
	protected function convertSynonyms($tags, &$errors)
	{
		$set = $this->assertor->assertQuery(
			'vBForum:getCanonicalTags', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'tags' => $tags));

		$map = array();

		if ($set->valid())
		{
			$row = $set->current();
			while ($set->valid())
			{
				$map[vB_String::vBStrToLower($row['tagtext'])] = $row['canonicaltagtext'];
				$row = $set->next();
			}

		}
		$newTags = array();
		foreach ($tags as $key => $tag)
		{
			$tag_lower = vB_String::vBStrToLower($tag);
			if (array_key_exists($tag_lower, $map))
			{
				$errors["$tag_lower-convert"] = array('tag_x_converted_to_y', $tag, $map[$tag_lower]);
				$newTags[] = $map[$tag_lower];
			}
			else
			{
				$newTags[] = $tag;
			}
		}
		return $newTags;
	}


	/********************************************************
	*	Public Methods
	********************************************************/

	/**
	*	Determines if a user can delete a tag associated with this content item
	*
	* A user can delete his or her own tags.
	* A user with moderator rights can delete a tag.
	* If not otherwise specified a user can delete a tag if they own the content item
	*
	*	This function requires that content info is set.
	*
	*	@param int The user id for the tag/content association
	* @return bool
	*/
	protected function canDeleteTag($tag, &$errors = array())
	{
		$tagInfo = $this->fetchTagInfo($tag);
		//Check if tag exists
		if(!is_array($tagInfo))
		{
			$errors['invalid_tags'] = array('x_is_invalid_tag', $tag);
			return false;
		}
		// Attempt some decent content agnostic defaults
		// Content types that care should override this function

		//the user can delete his own tag associations
		if ($this->currentUserId == $tagInfo['userid'])
		{
			return true;
		}

		//moderators can delete tags
		if ($this->canModerateTag())
		{
			return true;
		}

		//the object's owner can delete tags
		if ($this->isOwnedByCurrentUser())
		{
			return true;
		}

		$errors['no_delete_permissions'] = array('no_delete_tag_permission');
		return false;
	}

	/**
	*	Checks to see if the user has permission to "moderate" tags for this content items.
	*
	* This is specific to the content type and defaults to false.
	*	This function requires that content info be set.
	*
	* @return bool
	*/
	protected function canModerateTag()
	{
		// Basic logic is that only super admin can moderate tags.
		$usercontext = vB::getUserContext();

		//admin and mods can Moderate tag
		return ($usercontext->getChannelPermission('moderatorpermissions', 'canmoderatetags', $this->nodeid));
	}

	/**
	*	Checks to see if the user can add tags to this content item
	*
	*	This function requires that content info be set.
	* @return bool
	*/
	protected function canAddTag($targetid = false)
	{
		$usercontext = vB::getUserContext();

		if (!$targetid)
		{
			$targetid = $this->nodeid;
		}

		if ($this->isOwnedByCurrentUser())
		{
			return ($usercontext->getChannelPermission('forumpermissions', 'cantagown', $targetid));
		}
		else
		{
			return ($usercontext->getChannelPermission('forumpermissions', 'cantagothers', $targetid));
		}
	}

	/**
	*	Can the current user manage existing tags?
	*
	*	The only current operation on existing tags is to remove them from the content
	* item.
	*
	*	This is odd.  It controls whether or not we show the checkboxes beside the
	* tags in tagUI (and if we check at all for deletes).  It exists primarily to
	* capture some logic in the thread to handle the situation where a user can
	* delete tags but not add them (if a user can add tags we'll always display the
	* delete UI in the legacy logic).  Note that there is a seperate check for each
	* tag to determine if the user can actually delete that particular tag.  Most
	* new types aren't likely to require that kind of granularity and probably
	* won't need to to extend this function.
	*
	*	This function requires that content info be set.
	*
	* @return bool
	*/
	public function canManageTag($nodeid = 0)
	{
		if (!$nodeid)
		{
			$nodeid = $this->nodeid;
		}

		// By default, logged in users can add tags
		$usercontext = vB::getUserContext();
		if ($usercontext->getChannelPermission('moderatorpermissions', 'canmoderatetags', $nodeid))
		{
			return true;
		}

		if ($this->isOwnedByCurrentUser())
		{
			return ($usercontext->getChannelPermission('forumpermissions', 'candeletetagown', $nodeid));
		}
		else
		{
			return false;
		}
	}

	/**
	*	Determines if the current user owns this content item
	*
	*	Ownership is a content specific concept.  For example the "owner" of a thread
	* is the thread starter.
	*	This function requires that content info be set.
	*
	* @return bool
	*/
	protected function isOwnedByCurrentUser()
	{
		if (!$this->nodeid)
		{
			throw new vB_Exception_Api('invalid_node_id');
		}
		// Attempt some decent content agnostic defaults
		// Content types that care should override this function

		if (empty($this->owner))
		{
			$this->owner = $this->getNodeOwner($this->nodeid);
		}

		return ($this->owner == $this->currentUserId);
	}

	/**
	 * Get the id of the node owner
	 * @return <int> the id of the owner
	 */
	protected function getNodeOwner($node)
	{
		$node = vB_Library::instance('node')->getNodeBare($node);
		return $node['userid'];
	}

	/**
	*	Get the user permission to create tags
	*
	* @return bool
	*/
	function checkUserPermission()
	{
		$usercontext = vB::getUserContext();
		return $usercontext->getChannelPermission('forumpermissions', 'cantagown', $this->nodeid);
	}

	/**
	*	Get the tag limits for the content type
	*
	*	This function requires that content info be set.
	*
	* @return array ('content_limit' => total tags for content type, 'user_limit' => total tags the
	*		current user can have on this item)
	*/
	public function fetchTagLimits()
	{
		$usercontext = vB::getUserContext();

		if ($this->isOwnedByCurrentUser())
		{
			$user_limit = $usercontext->getChannelLimits($this->nodeid, 'maxstartertags');
		}
		else
		{
			$user_limit = $usercontext->getChannelLimits($this->nodeid, 'maxothertags');
		}

		$tagmaxtopic = $usercontext->getChannelLimits($this->nodeid, 'maxtags');
		return array('content_limit' => $tagmaxtopic, 'user_limit' => $user_limit);
	}

 	/**
	* Adds tags to the content item. Tags are created if they don't already exist
	* (assuming the user has permissions)
	*
	*	If a tag cannot be processed it is dropped from the list and an error is returned, however
	* this does not prevent valid tags from being processed.
	*
	* @param	string|array	List of tags to add (comma delimited, or an array as is).
	*												If array, ensure there are no commas.
	*
	* @return	array			Array of errors, if any
	*/
	public function addTagsToContent($taglist)
	{
		$this->invalidateTagList();

		if (!$this->nodeid OR !$this->canAddTag())
		{
			return array();
		}

		$errors = array();
		$limits = $this->fetchTagLimits();

		if (!$taglist)
		{
			return $errors;
		}

		//Let's first remove existing and verify we're not exceeding the limits.
		$taglist = $this->filterTagListContentLimits($taglist, $limits, $errors);
		if (empty($taglist))
		{
			return $errors;
		}

		$inserts = $taglist;
		$existing = $this->assertor->assertQuery('vBForum:tag', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
			'tagtext' => $taglist));

		if ($existing AND $existing->valid())
		{
			$tag = $existing->current();
			while($existing->valid())
			{
				$key = array_search($tag['tagtext'], $inserts);

				if ($key !== false)
				{
					unset($inserts[$key]);
				}
				$tag = $existing->next();
			}
		}

		if (!empty($inserts))
		{
			$usercontext = vB::getUserContext();
			if (!$usercontext->hasPermission('genericpermissions', 'cancreatetag'))
			{
				$errors['nopermission'] = array('tag_no_create');
				return $errors;
			}

			// create new tags
			$taglistInsert = array();
			foreach ($inserts AS $tag)
			{
				$taglistInsert[] = array(
					'tagtext' => $tag,
					'dateline' => vB::getRequest()->getTimeNow()
				);
			}
			$this->assertor->assertQuery('insertignoreValues', array('table' => 'tag', 'values' => $taglistInsert));
		}

		// now associate with content item
		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'tags' => $taglist,
		'nodeid' => intval($this->nodeid),
		'userid' => $this->currentUserId,
		'dateline' => vB::getRequest()->getTimeNow());
 		$this->assertor->assertQuery('vBForum:addTagContent', $data);

		//existing content info is invalid now.
		$this->invalidateTagList();
		// do any content type specific updates for new tags
		$this->updateNode();

		return $errors;
	}


	/**
	*	Copy the tag attachments from one item to another
	*
	*	Copying of tag attachements from an item of a different type is supported.
	*
	*	@param $sourceid The id of the item whose tags should be copied
	*/
	public function copyTagAttachments($sourceid)
	{
		if (!$this->nodeid OR !$this->canAddTag($sourceid))
		{
			return array();
		}
		//user can move source node
		if (!$this->canManageTag($sourceid))
		{
			return array();
		}

		//checking available space for tags
		$remaining = $this->getSpace($sourceid);
		if ($remaining <= 0)
		{
			throw new vB_Exception_Api('tag_limit_error');
		}

		$this->invalidateTagList();
		$data = array(
			'nodeid' => intval($this->nodeid),
			'sourceid' => intval($sourceid),
			vB_dB_Query::PARAM_LIMIT => $remaining,
		);

		$this->assertor->assertQuery('vBForum:copyTagContent', $data);
		$this->updateNode();
	}

	public function moveTagAttachments($sourceid)
	{
		if (!$sourceid)
		{
			return false;
		}

		if (!$this->nodeid OR !$this->canAddTag($sourceid))
		{
			return array();
		}

		//user can move source node
		if (!$this->canManageTag($sourceid))
		{
			return array();
		}

		//checking available space for tags
		$remaining = $this->getSpace($sourceid);
		if ($remaining <= 0)
		{
			throw new vB_Exception_Api('tag_limit_error');
		}


		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
					'nodeid' => intval($this->nodeid),
					'sourceid' => intval($sourceid),
					vB_dB_Query::PARAM_LIMIT => $remaining);
		$this->assertor->assertQuery('vBForum:copyTagContent', $data);

		$this->removeTagInfo($sourceid, true, $remaining);
		$this->invalidateTagList();
		$this->updateNode();
	}

	protected function removeTagInfo($nodeid, $isMoving = false, $remaining = 0)
	{
		$taglist = '';
		if ($isMoving)
		{
			$sourceTagList = $this->assertor->getRow('vBForum:getNodeTagList', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
						'nodeid' => $nodeid));
			$targetTagList = $this->assertor->getRow('vBForum:getNodeTagList', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
						'nodeid' => $this->nodeid));
			$sourceTagList = explode(",", $sourceTagList['taglist']);
			$targetTagList = explode(",", $targetTagList['taglist']);
			$ignored = array_intersect($sourceTagList, $targetTagList);
			//If there are no similar tags between both nodes
			if (!$ignored)
			{
				$tmp = array_diff($sourceTagList, $targetTagList);
				foreach ($tmp as $key => $tag)
				{
					if ($key >= $remaining)
					{
						$ignored[] = $tag;
					}
				}
			}

			$ignoredTags = array();
			foreach ($ignored as $tag)
			{
				$taglist .= $tag . ",";
				$ignoredTags[] = $tag;
			}
			$taglist = substr($taglist, 0, -1);
			$ignoredTags = sizeof($ignoredTags) > 0 ? $ignoredTags : '';

			//Delete Records
			$this->assertor->assertQuery('vBForum:deleteTags', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
						'nodeid' => $nodeid,
				'ignoredTags' => $ignoredTags));
		}
		else
		{
			$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
				vB_dB_Query::CONDITIONS_KEY => array('nodeid' => $nodeid));
			$this->assertor->assertQuery('vBForum:tagnode', $data);
		}
		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
			vB_dB_Query::CONDITIONS_KEY =>  array('nodeid' => $nodeid), 'taglist' => $taglist);
		$this->assertor->assertQuery('vBForum:node', $data);
	}

	/**
	*	Merge the tag attachments for one or more tagged items to this item
	*
	*	Designed to handle the results of merging items (the tags also need to be
	* merged).  Items merged are assumed to be the same type as this item. Merged
	* tags are detached from the items they are merged from.
	*
	*	@param array $sourceids The id of the item whose tags should be merged
	*
	*/
	public function mergeTagAttachments($sourceids)
	{
		//check if user can move nodes
		$canMerge = true;
		foreach ($sourceids as $source)
		{
			if (!$this->canManageTag($source))
			{
				$canMerge = false;
			}
		}

		if (!$canMerge AND !$this->canManageTag())
		{
			throw new vB_Exception_Api('no_merge_permissions');
		}

		foreach ($sourceids as $key => $sourceid)
		{
			if (!$this->canAddTag($sourceid))
			{
				unset($sourceids[$key]);
			}
		}

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

		$remaining = array();
		foreach ($sourceids as $sourceid)
		{
			//checking available space for tags
			$remaining[$sourceid] = $this->getSpace($sourceid);
			if ($remaining[$sourceid] <= 0)
			{
				$canMerge = false;
			}
		}

		if ($canMerge)
		{
			$remaining = min($remaining);
			$this->invalidateTagList();
			$safeids = array_map('intval', $sourceids);

			//some places like to include the target id in the array of
			//merged items.  This fixes that.
			$safeids = array_diff($safeids, array($this->nodeid));

			$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
				'sourceid' => $safeids,
				'nodeid' => $this->nodeid,
				vB_dB_Query::PARAM_LIMIT => $remaining);
			$this->assertor->assertQuery('vBForum:mergeTagContent', $data);

			//Anything that didn't get deleted is a duplicate, to be removed
			foreach($safeids as $safeid)
			{
				$this->removeTagInfo($safeid);
			}

			$this->updateNode();
		}
		else
		{
			throw new vB_Exception_Api('tag_limit_error');
		}
	}

	/**
	*	Delete tag attachments for this item
	*/
	protected function deleteTagAttachments($nodeid, $taglist, &$errors)
	{
		if (!is_array($taglist))
		{
			$taglist = $this->splitTagList($taglist);
		}

		$notValidTags = "";
		foreach ($taglist as $key => $tag)
		{
			if (!($this->canDeleteTag($tag, $errors)))
			{
				$notValidTags .= $tag . ",";
				unset($taglist[$key]);
			}
		}
		if (!$this->contentinfo)
		{
			$this->contentinfo = $this->fetchContentInfo();
		}
		// Ensure node has tags
		if (sizeof($this->contentinfo) > 0)
		{
			$tags = array();
			foreach($this->contentinfo as $existingTag)
			{
				if (in_array($existingTag['tagtext'], $taglist))
				{
					$tags[] = $existingTag;
				}
			}
			//If we have tags to remove
			if (sizeof($tags) > 0)
			{
				foreach($tags as $tag)
				{
					$data = array(
						vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,
						vB_dB_Query::CONDITIONS_KEY => array(
							'nodeid' => $nodeid,
							'tagid' => $tag['tagid']
						)
					);
					$this->assertor->assertQuery('vBForum:tagnode', $data);
				}
				$this->invalidateTagList();
				$this->updateNode();
			}
		}
	}
	protected function getSpace($sourceNode)
	{
		$limits = $this->fetchTagLimits();
		$contentTagLimit = isset($limits['content_limit']) ? intval($limits['content_limit']) : 0;
		$userTagLimit = isset($limits['user_limit']) ? intval($limits['user_limit']) : 0;
		$existingTagCount = $this->fetchExistingTagCount();

		//Get the tags count from an specific node (source) excluding tags already contained in $this->nodeid
			$sourceNodeTags = $this->assertor->getRow('vBForum:filteredTagsCount',
				array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
					'targetid' => $this->nodeid,
					'sourceid' => $sourceNode
				)
			);
		$sourceNodeTagsCount = $sourceNodeTags["filteredTags"];
		if (!$sourceNodeTagsCount)
		{
				throw new vB_Exception_Api('no_tags_to_move_merge_copy');
		}

		$contentTagsRemaining = PHP_INT_MAX;
		if ($contentTagLimit)
		{
				$contentTagsRemaining = $contentTagLimit - $existingTagCount - $sourceNodeTagsCount;
		}

		$userTagsRemaining = PHP_INT_MAX;
		if ($userTagLimit)
		{
				$userTagCount = 0;
				if ($this->contentinfo === null)
				{
					$this->loadContentInfo();
				}
				foreach($this->contentinfo as $tag)
				{
					if ($tag["userid"] == $this->currentUserId)
					{
						$userTagCount++;
					}
				}
				$userTagsRemaining = $userTagLimit - $userTagCount - $sourceNodeTagsCount;
		}
		$userTagsRemaining = ($userTagsRemaining < 0 ? ($sourceNodeTagsCount + $userTagsRemaining) : $sourceNodeTagsCount);
		$contentTagsRemaining = ($contentTagsRemaining < 0 ? ($sourceNodeTagsCount + $contentTagsRemaining) : $sourceNodeTagsCount);
		$remainingTags = min($userTagsRemaining, $contentTagsRemaining);

		return $remainingTags;
	}

	/**
	 * Update the node value
	 */
	protected function updateNode()
	{
		$taglist = $this->fetchExistingTagList();
		if (empty($taglist))
		{
			$taglist = '';
		}
		else
		{
			$taglist = implode(',' ,$taglist);
		}

		$data = array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE, vB_dB_Query::CONDITIONS_KEY =>
			array('nodeid' => $this->nodeid), 'taglist' => $taglist);
		$this->assertor->assertQuery('vBForum:node', $data);
	}

	/**
	 *	Delete all tag attachments for this item
	 */
	public function deleteTags($tags = '')
	{
		$errors = array();
		if (empty($tags))
		{
			$this->invalidateTagList();
			$this->deleteTagAttachmentsList( array($this->nodeid));
		}
		else
		{
			$this->deleteTagAttachments($this->nodeid, $tags, $errors);
		}

		return $errors;
	}

	/**
	* Filters the tag list to exclude invalid tags based on the content item the tags
	* are assigned to.
	*
	*	Calls filterTagList internally to handle invalid tags.
	*
	* @param	string|array	List of tags to add (comma delimited, or an array as is).
	*  											If array, ensure there are no commas.
	* @param	array			array of tag limit constraints.  If a limit is not specified a suitable
	*										default will be used (currently unlimited, but a specific default should
	*										not be relied on). Current limits recognized are 'content_limit' which
	*										is the maximum number of tags for a content item and 'user_limit' which
	*										is the maximum number of tags the current user can add to the content item.
	* @param	int				The maximum number of tags the current user can assign to this item (0 is unlimited)
	* @param	boolean		Whether to check the browsing user's create tag perms
	* @param	boolean		Whether to expand the error phrase
	*
	* @return	array			List of valid tags.  If there are too many tags to add, the list will
	*		be truncated first.  An error will be set in this case.
	*/
	protected function filterTagListContentLimits (
		$taglist,
		$limits,
		&$errors,
		$checkBrowserPerms = true,
		$evalerrors = true
	)
	{
		$contentTagLimit = isset($limits['content_limit']) ? intval($limits['content_limit']) : 0;
		$userTagLimit = isset($limits['user_limit']) ? intval($limits['user_limit']) : 0;
		//Note that this call ensures we have loaded content, so no need to check that later
		$existingTagCount = $this->fetchExistingTagCount();

		if ($contentTagLimit AND $existingTagCount >= $contentTagLimit)
		{
		//	$errors['threadmax'] = $evalerrors ? fetch_error('item_has_max_allowed_tags') : 'item_has_max_allowed_tags';
			$errors['threadmax'] = 'item_has_max_allowed_tags';
			return array();
		}
		$validTags = $this->filterTagList($taglist, $errors, $evalerrors);
		$validTagsLower = array_map('vB_String::vBStrToLower', $validTags);

		if ($validTags)
		{

			if ($checkBrowserPerms AND !$this->canAddTag())
			{
				// can't create tags, need to throw errors about bad ones
				$newTags = array_flip($validTagsLower);
				foreach ($this->contentinfo AS $tag)
				{
					unset($newTags[vB_String::vBStrToLower($tag['tagtext'])]);
					$tag = $existing->next();
				}

				if ($newTags)
				{
					// trying to create tags without permissions. Remove and throw an error
					//	$errors['no_create'] = $evalerrors ? fetch_error('tag_no_create') : 'tag_no_create';
					$errors['no_create'] = 'tag_no_create';

					foreach ($newTags AS $newTag => $key)
					{
						// remove those that we can't add from the list
						unset($validTags["$key"], $validTagsLower["$key"]);
					}
				}
			}

			// determine which tags are already in the thread and just ignore them
			$userTagCount = 0;

			if ($this->contentinfo === null)
			{
				$this->loadContentInfo();
			}
			foreach ($this->contentinfo as $tag)
			{
				if ($tag['userid'] == $this->currentUserId)
				{
					$userTagCount++;
				}

				// tag is in thread, find it and remove
				if (($key = array_search(vB_String::vBStrToLower($tag['tagtext']), $validTagsLower)) !== false)
				{
					unset($validTags["$key"], $validTagsLower["$key"]);
				}
			}

 			//approximate "unlimited" as PHP_INT_MAX -- makes the min logic cleaner
			$contentTagsRemaining = PHP_INT_MAX;
			if ($contentTagLimit)
			{
				$contentTagsRemaining = $contentTagLimit - $existingTagCount - count($validTags);

			}

			$userTagsRemaining = PHP_INT_MAX;
			if ($userTagLimit)
			{
				$userTagsRemaining = $userTagLimit - $userTagCount - count($validTags);
			}

			$remainingTags = min($contentTagsRemaining, $userTagsRemaining);
			if ($remainingTags < 0)
			{
				//	$errors['threadmax'] = $evalerrors ?
				//		fetch_error('number_tags_add_exceeded_x', vb_number_format($remainingTags * -1)) :
				//		array('number_tags_add_exceeded_x', vb_number_format($remainingTags * -1));
				$errors['threadmax'] = array('number_tags_add_exceeded_x', vb_number_format($remainingTags * -1));

				$allowedTagCount = count($validTags) + $remainingTags;
				if ($allowedTagCount > 0)
				{
					$validTags = array_slice($validTags, 0, count($validTags) + $remainingTags);
				}
				else
				{
					$validTags = array();
				}
			}
		}
		return $validTags;
	}

	/**
	*	Get the number of existing tags for this item
	*
	*	@return int the tag count
	*/
	public function fetchExistingTagCount()
	{
		if (is_null($this->tags))
		{
			$this->fetchExistingTagList();
		}
		return count($this->contentinfo);
	}


	/**
	*	Get the list of tags associated with this item
	*
	* @return array Array of tag text for the associated tags
	*/
	public function fetchExistingTagList()
	{
		if (!is_null($this->tags))
		{
			return $this->tags;
		}

		if ($this->contentinfo === null)
		{
			$this->loadContentInfo();
		}

		$this->tags = array();
		foreach ($this->contentinfo AS $tag)
		{
			$this->tags[] = $tag['tagtext'];
		}

		return $this->tags;
	}

	/**
	*	Allow access to the content array
	*
	* Lazy loads content info array.  Used internally so that we only load this if
	* we actually need it (and don't load it multiple times).
	*
	*	This function is exposed publicly for the benefit of code that needs the
	* content array but may not know precisely how to load it (because it isn't
	* aware of the type of content being tagged).
	*
	* Actually, this is a bad idea precisely because the code doesn't know what
	* type its dealing with.  Its a paint to have to create a bunch of getters
	* for the details, but we need to do just that to ensure a consistant
	* interface.
	*
	*	@return array Content info array.
	*/
	protected function fetchContentInfo()
	{
		if ($this->contentinfo === null)
		{
			$this->loadContentInfo();
		}
		return $this->contentinfo;
	}

	/*
	 * Return the taglist of the current node.
	 * The taglist info for each tag is userid, tagtext, tagid
	 */
	public function getNodeTags()
	{
		if ($this->contentinfo === null)
		{
			$this->loadContentInfo();
		}

		return array_values($this->contentinfo);
	}

	/********************************************************
	*	Private Methods
	********************************************************/

	/**
	*	Load the Content Info
	*
	* Actually loads the content info for this type
	*
	*	@return array The content info
	*/
	protected function loadContentInfo()
	{
		if (!$this->nodeid)
		{
			throw new vB_Exception_Api('invalid_node_id');
		}

		$this->contentinfo = array();
		$query = $this->assertor->assertQuery('vBForum:getTags', array('nodeid' => $this->nodeid));
		if ($query AND $query->valid())
		{
			$tag = $query->current();
			while ($query->valid())
			{
				$this->contentinfo[$tag['tagid']] = $tag;
				$tag = $query->next();
			}
		}
	}

	/**
	*	Invalidates the cached list of tags for this item.
	*
	*	Should be called by any method that alters the tag
	* types.
	*/
	protected function invalidateTagList()
	{
		$this->contentinfo = $this->tags = null;
	}

	protected function fetchTagInfo($tag)
	{
		if (sizeof($this->contentinfo) == 0)
		{
			$this->loadContentInfo();
		}

		if (sizeof($this->contentinfo) > 0)
		{
			foreach($this->contentinfo as $nodeTag)
			{
				if (in_array($tag, $nodeTag))
				{
					$tag = $nodeTag;
				}
			}
			return $tag;
		}
	}
}

/*=========================================================================*\
|| #######################################################################
|| # NulleD By - prowebber.ru
|| # CVS: $RCSfile$ - $Revision: 99787 $
|| #######################################################################
\*=========================================================================*/
