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


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

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

	//When we parse the page.
	protected $bbcode_parser = false;

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

	//Whether we handle showapproved,approved fields internally or not
	protected $handleSpecialFields = 0;

	//for spam checking
	protected $spamType = false;
	protected $spamKey = false;
	protected $akismet;

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

	//Is text required for this content type?
	protected $textRequired = true;

	/** normal protector- protected to prevent direct instantiation **/
	protected function __construct()
	{
		parent::__construct();
		$this->library = vB_Library::instance('Content_Text');
		//see if we have spam checking set.
		if (isset($this->options['vb_antispam_type']) AND $this->options['vb_antispam_type'] > 0 AND !empty($this->options['vb_antispam_key']))
		{
			$this->spamType = $this->options['vb_antispam_type'];
			$this->spamKey = $this->options['vb_antispam_key'];
		}
	}

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

		if (!$this->validate($node, self::ACTION_DELETE, $nodeid))
		{
			throw new vB_Exception_Api('no_permission');
		}
		return $this->library->delete($nodeid);
	}

	/*** 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.
	 *						Available options include:
	 *							- nl2br: if TRUE, all \n will be converted to <br /> so that it's not removed by the html parser (e.g. comments).
	 *
	 * 	@return	integer		the new nodeid
	 ***/
	public function add($data, $options = array())
	{
		if ($this->textRequired AND empty($data['pagetext']) AND empty($data['rawtext']))
		{
			throw new vB_Exception_Api('text_required');
		}

		if (!$this->textRequired AND empty($data['pagetext']) AND empty($data['rawtext']))
		{
			// the duplicate check is based on the post text, which is not required,
			// so we need to skip it if there is no text
			$options['skipDupCheck'] = true;
		}

		$vboptions = vB::getDatastore()->getValue('options');
		$parentNode = vB_Api::instanceInternal('node')->getNode($data['parentid']);

		if (!empty($data['title']))
		{
			$strlen= vB_String::vbStrlen(trim($data['title']), true);
			if ($strlen > $vboptions['titlemaxchars'])
			{
				throw new vB_Exception_Api('maxchars_exceeded_x_title_y', array($vboptions['titlemaxchars'], $strlen));
			}
		}
		else
		{
			$channelcontentypeid = vB_Api::instanceInternal('contenttype')->fetchContentTypeIdFromClass('Channel');
			//title is requred for topics. VMs look like topics so they need to be exempt
			if ($parentNode['contenttypeid'] == $channelcontentypeid AND ($data['parentid'] != vB_Api::instanceInternal('node')->fetchVMChannel()))
			{
				throw new vB_Exception_Api('title_required');
			}
		}
		$isComment = ($parentNode['parentid'] == $parentNode['starter']);

		if ($isComment)
		{
			$minChars = $vboptions['commentminchars'];
			$maxChars = $vboptions['commentmaxchars'];
		}
		else
		{
			$minChars = $vboptions['postminchars'];
			$maxChars = $vboptions['postmaxchars'];
		}

		$strlen = vB_String::vbStrlen($this->library->parseAndStrip(empty($data['pagetext']) ? $data['rawtext'] : $data['pagetext']), true);

		if ($this->textRequired AND $strlen < $minChars)
		{
			throw new vB_Exception_Api('please_enter_message_x_chars', $minChars);
		}

		if($maxChars != 0 AND $strlen > $maxChars)
		{
			throw new vB_Exception_Api('maxchars_exceeded_x_y', array($maxChars, $strlen));
		}

		// If node is a starter and has no title
		if (!empty($data['starter']) AND ($data['starter'] != $data['parentid']) AND empty($data['title']))
		{
			return false;
		}

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

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

		if (!$this->validate($data, vB_Api_Content::ACTION_ADD))
		{
			throw new vB_Exception_Api('no_create_permissions');
		}

		//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
		if (!$this->handleSpecialFields)
		{
			unset($data['approved']);
			unset($data['showapproved']);
		}

		$nodeOptions = vB_Api::instanceInternal('node')->getOptions();
		$moderateNode = false;

		if ($parentNode['starter'] == 0)
		{
			$moderateNode = ($nodeOptions['moderate_topics'] & $parentNode['nodeoptions']) ? true : false;
		}
		else if (($nodeOptions['moderate_topics'] & $parentNode['nodeoptions'])
			OR ($nodeOptions['moderate_comments'] & $parentNode['nodeoptions']))
		{
			$moderateNode = true;
		}

		if (!vB::getUserContext()->getChannelPermission('forumpermissions', 'followforummoderation', $data['parentid'])
			OR $moderateNode)
		{
			$data['approved'] = 0;
			$data['showapproved'] = 0;
		}

		if (!isset($data['htmlstate']))
		{
			// We don't have a front end interface for this yet. So if they have permission to use HTML, give them HTML.
			// @TODO update this when we have a proper interface
			$data['htmlstate'] = 'on_nl2br';
		}

		$this->cleanInput($data);
		$this->cleanOptions($options);

		return $this->library->add($data, $options);
	}

	/*** 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 (!vB::getUserContext()->getChannelPermission('forumpermissions2', 'canusehtml', $data['parentid']))
		{
			// Regardless of this node's previous htmlstate, if the user doesn't have permission to use html, turn it off.
			$data['htmlstate'] = 'off';
		}

		return parent::update($nodeid, $data);
	}

	/** THis returns a string with quoted strings in bbcode format.
	*
	*	@param	mixed	array of integers
	*
	* 	@return	string
	***/

	public function getQuotes($nodeids)
	{
		return $this->library->getQuotes($nodeids);
	}

	public function getIndexableFromNode($node, $include_attachments = true)
	{
		$all_content = parent::getIndexableFromNode($node, $include_attachments = true);
		array_unshift($all_content, $node['rawtext']);
		return $all_content;
	}

	/**
	 * Adds content info to $result so that merged content can be edited.
	 * @param array $result
	 * @param array $content
	 */
	public function mergeContentInfo(&$result, $content)
	{
		if (vb::getUserContext()->getChannelPermission('forumpermissions', 'canviewthreads', $result['nodeid']))
		{
			$this->library->mergeContentInfo($result, $content);
		}
	}

	/** Gets the data the presentation layer needs to have to parse the rawtext.
	 *
	 *	@param		mixed	nodeId or array of nodeIds
	 *
	 *	@return		mixed	array includes bbcodeoptions, attachments, and rawtext
	 */
	public function getDataForParse($nodeIds)
	{
		if (is_int($nodeIds))
		{
			$nodeIds = array($nodeIds);
		}
		else if (!is_array($nodeIds))
		{
			throw new vB_Exception_Api('invalid_data');
		}

		$results = array();
		$options = vB::getDatastore()->getValue('bf_misc_forumoptions');
		$pmType = vB_Types::instance()->getContentTypeID('vBForum_PrivateMessage');
		$galleryTypeid = vB_Types::instance()->getContentTypeId('vBForum_Gallery');
		$photoTypeid = vB_Types::instance()->getContentTypeId('vBForum_Photo');
		$userContext = vB::getUserContext();
		$channelTypes = vB::getDatastore()->getValue('vBChannelTypes');

		if (!empty($nodeIds))
		{
			$nodes = $this->assertor->assertQuery('vBForum:getDataForParse', array('nodeid' => $nodeIds));

			foreach ($nodes AS $node)
			{

				try
				{
					if ($this->validate($node, self::ACTION_VIEW, $node['nodeid'], array($node)))
					{
						$attachments = $this->nodeApi->getNodeAttachments($node['nodeid']);
						// We don't need to show attachments for gallery. See VBV-6389.
						// Or rather, we need to unset attachments that are part of a gallery, but want to show other attachments. See VBV-11058
						if ($galleryTypeid == $node['contenttypeid'])
						{
							foreach ($attachments AS $key => &$attachment)
							{
								// attachments have contenttype vBForum_Attach, while photos of a gallery have contenttype vBForum_Photo
								if ($photoTypeid == $attachment['contenttypeid'])
								{
									unset($attachments[$key]);
								}
							}

						}

						if ($node['contenttypeid'] == $pmType)
						{
							$bbCodeOptions = vB_Api::instance('content_privatemessage')->getBbcodeOptions();
						}
						else if ($userContext->getChannelPermission('forumpermissions', 'canviewthreads', $node['nodeid'], false, $node['parentid']))
						{
							$bbCodeOptions = array();
							foreach($options AS $optionName => $optionVal)
							{
								$bbCodeOptions[$optionName] = (bool)($node['options'] & $optionVal);
							}
						}
						else
						{
							$bbCodeOptions = array();
						}
						$results[$node['nodeid']] =  array(
							'bbcodeoptions' => $bbCodeOptions,
							'rawtext'       => $node['rawtext'],
							'previewtext' 	=> $node['previewtext'],
							'attachments'   => $attachments,
							'title'			=> $node['title'],
							'channelid'     => $node['channelid'],
							'htmlstate'     => $node['htmlstate'],
							'disable_bbcode' => $node['nodeoptions'] & vB_Api_Node::OPTION_NODE_DISABLE_BBCODE);
					}
					else if ($node['public_preview'] > 0)
					{
						$results[$node['nodeid']] =  array(
							'bbcodeoptions' => array(),
							'rawtext'       => '',
							'title'			=> $node['title'],
							'channelid'     => $node['channelid'],
							'htmlstate'     => 'off',
							'preview_only'	=> 1,
							'disable_bbcode' => $node['nodeoptions'] & vB_Api_Node::OPTION_NODE_DISABLE_BBCODE);

						require_once(DIR . '/includes/class_bbcode.php');
						$tags = fetch_tag_list();
						$registry = vB::get_registry();
						$bbcode_parser = new vB_BbCodeParser($registry, $tags);
						$options = array('allowsmilies' => 1, 'allowbbcode', 'allowimagecode' => 1);

						if ($node['htmlstate'] != 'off')
						{
							$options['allowhtml'] = 1;
						}

						if ($node['nodeid'] == $node['starter'])
						{
							$channel = vB_Library::instance('node')->getNodeFullContent($node['parentid']);
						}
						else
						{
							$starter = $this->nodeApi->getNode($node['starter']);
							$channel = vB_Library::instance('node')->getNodeFullContent($starter['parentid']);
						}
						$channel = array_pop($channel);

						if ($channel['channeltype'] == 'article')
						{
							$options['allowPRBREAK'] =  1;
						}

						if (vB::getUserContext()->getChannelPermission('forumpermissions2', 'cangetimgattachment' , $node['nodeid']))
						{
							$options['allowimages'] =  1;
						}

						$results[$node['nodeid']]['previewtext'] = $bbcode_parser->getPreview($node['rawtext'], 0, FALSE, ($node['htmlstate'] == 'on_nl2br'), null, $options);
					}
					else
					{
						$results[$node['nodeid']] =  array(
							'bbcodeoptions' => array(),
							'rawtext'       => '',
							'previewtext' 	=> '',
							'title'       => '',
							'attachments'   => array(),
							'channelid'     => $node['channelid'],
							'htmlstate'     => 'off',
							'disable_bbcode' => $node['nodeoptions'] & vB_Api_Node::OPTION_NODE_DISABLE_BBCODE);	// not much point since there is no rawtext, but ensure that it's set.

					}
					//channeltype
					if (isset($channelTypes[$node['channelid']]))
					{
						$results[$node['nodeid']]['channeltype'] = $channelTypes[$node['channelid']];
						if ($channelTypes[$node['channelid']] == 'article')
						{
							$results[$node['nodeid']]['previewLength']  = vB::getDatastore()->getOption('def_cms_previewlength');

							// VBV-12048 For articles, if preview break is present, use the length of the preview text instead of
							// the global cms preview length
							$prbreak = stripos($results[$node['nodeid']]['rawtext'], '[PRBREAK][/PRBREAK]');
							if ($prbreak !== FALSE)
							{
								$results[$node['nodeid']]['previewLength']  = $prbreak;
							}
						}
						else
						{
							$results[$node['nodeid']]['previewLength'] = vB::getDatastore()->getOption('threadpreview');
						}
					}
					else
					{
						$results[$node['nodeid']]['channeltype'] = '';
					}

				}
				catch (exception $e)
				{
					//probably a permission error. We can continue with whatever is valid.
 				}
			}
		}

		return $results;
	}

	/** 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)
	{
		parent::cleanInput($data, $nodeid);

		$canUseHtml = vB::getUserContext()->getChannelPermission('forumpermissions2', 'canusehtml', (empty($nodeid) AND isset($data['parentid'])) ? $data['parentid'] : $nodeid);

		if (isset($data['htmlstate']))
		{
			if ($canUseHtml)
			{
				switch ($data['htmlstate'])
				{
					case 'on':
					case 'on_nl2br':
					case 'off':
						// We're ok, don't do anything.
						break;
					default:
						$data['htmlstate'] = 'off';
						break;
				}
			}
			else
			{
				// User can't use HTML.
				$data['htmlstate'] = 'off';
			}
		}
	}

	/*
	 *	Used by the createcontent controller's handleAttachmentUploads()  to fix temporary id references in the
	 *	specified node's rawtext to nodeids
	 *
	 *	@param		mixed	$nodeId				nodeid or array of nodeids
	 *	@param		array	$keysToAttachid		(optional) array({tempid of attachment} => {attachment's nodeid})
	 *												maps temporary-ids (array key) of newly added attachments and
	 *												corresponding nodeids (array value) of said attachments.
	 *												While this is optional for historical reasons, it must be provided
	 *												if any temporary id references in the rawtext need to be replaced.
	 *
	 *	@return		mixed	array includes bbcodeoptions, attachments, and rawtext
	 */
	public function fixAttachBBCode($nodeId, $keysToAttachid = array())
	{
		$content = vB_Api::instanceInternal('node')->getNodeFullContent($nodeId);
		$content = $content[$nodeId];
		$oldrawtext = $content['rawtext'];
		// keep the regex matched with the one in vB5_Template_Bbcode's handle_bbcode_img()
		if (preg_match_all('#\[attach(?:=(right|left|config))?\]([[:alnum:]_]+)\[/attach\]#i', $content['rawtext'], $matches))
		{
			foreach($matches[2] AS $key => $attachmentid)
			{
				$align = $matches[1]["$key"];
				// refer to wysiwyghtmlparser's handleWysiwygImg() for the code that replaces image tags with attach bbcodes
				if (preg_match('#temp_(\d+)_(\d+)_(\d+)$#', $attachmentid))
				{
					if (!empty($keysToAttachid[$attachmentid]))
					{
						$attachnodeid = $keysToAttachid[$attachmentid];
						$newattachbbcode = "[ATTACH" . (!empty($align) ? '=' . $align : '') . "]n" . $attachnodeid . "[/ATTACH]";
						$content['rawtext'] = str_replace($matches[0][$key], $newattachbbcode, $content['rawtext']);
					}
				}
			}
		}


		/*
		 * Commenting out this bit. Nothing prevents multiple attachments from having the same filedataid.
		 * As such, the code below doesn't make sense, since it assumes that a filedataid to attachnodeid mapping
		 * is invertible. Note: As of VBV-5936, the attachment list should always use nodeid as key, not filedataid,
		 * because otherwise multiple attachments using the same filedataid end up overwriting each other and weird
		 * things happen.
		 * Also, any <img /> tags are converted to bbcodes by wysiwyghtmlparser's handleWysiwygImg() before it gets
		 * here as far as I'm aware, so we really shouldn't be messing w/ the URLs like this.
		 * Update 1:
		 *	This might've been used because a filedata record requires publicview = 1 for anyone other than the owner
		 *	to view using the filedataid query (see filedata api's fetchImageByFiledataid()). It seems like publicview
		 *	is set exclusively for channel logos. So a filedata/fetch?filedataid= url should be converted to a
		 *	filedata/fetch?id= url. However, given the issue about how multiple attachments could share the same
		 *	filedataid, we should simply never insert the filedata URL by our hands. That said, I'm adding a version
		 *	of the old code in case the user manually enters the url.
		 */
		$filedataidToAttachid = array();
		foreach ($content['attachments'] AS $attachid => $attachment)
		{
			if (isset($attachment['filedataid']) AND empty($filedataidToAttachid[$attachment['filedataid']]))
			{
				$filedataidToAttachid[$attachment['filedataid']] = $attachid;
			}
		}

		if (preg_match_all('#filedata/fetch\?filedataid=(\d+)#si', $content['rawtext'], $matches))
		{
			foreach($matches[1] AS $key => $filedataid)
			{
				if (!empty($filedataidToAttachid[$filedataid]))
				{
					$attachnodeid = $filedataidToAttachid[$filedataid];
					$newattachbbcode = "filedata/fetch?id=" . $attachnodeid;
					$content['rawtext'] = str_replace($matches[0][$key], $newattachbbcode, $content['rawtext']);
				}
			}
		}

		/*
		 * Below was commented out @ 5.1.1 Alpha 8. If this doesn't cause any issues, we should completely remove this
		 * in a couple versions.
		 */
		/*

		if (preg_match_all('#filedata/fetch\?filedataid=(\d+)#si', $content['rawtext'], $matches))
		{
			foreach($matches[1] AS $key => $attachmentid)
			{
				if (!empty($content['attachments'][$attachmentid]))
				{
					$attachnodeid = $content['attachments'][$attachmentid]['nodeid'];
					$newattachbbcode = "filedata/fetch?id=" . $attachnodeid;
					$content['rawtext'] = str_replace($matches[0][$key], $newattachbbcode, $content['rawtext']);
				}
			}
		}
		*/

		if ($oldrawtext != $content['rawtext'])
		{
			$this->update($nodeId, array('parentid' => $content['parentid'], 'rawtext' => $content['rawtext'], 'fixattachbbcode' => 1));
		}

	}

	/**
	 * Populates the previewimage field
	 * for this node. To be called after the node is saved and the
	 * attachments added.
	 *
	 * Finds the first image in the post text that can be used as a
	 * previewimage (uploaded here), or uses the first image attachment.
	 *
	 * @param	int	Nodeid
	 */
	public function autoPopulatePreviewImage($nodeId)
	{
		$node = vB_Api::instanceInternal('node')->getNodeFullContent($nodeId);
		$node = $node[$nodeId];

		if ($node['channeltype'] != 'article')
		{
			return;
		}

		$attachmentids = array();
		$legacyattachmentids = array();

		// try images embedded in the text
		if (!empty($node['rawtext']))
		{
			// [ATTACH], [ATTACH=XXX], [IMG], [IMG=XXX]
			if (preg_match_all('#\[(ATTACH|IMG)(?:=[^]]+)?\](.*)\[\/\1\]#siU', $node['rawtext'], $matches))
			{
				if (!empty($matches[2]))
				{
					foreach ($matches[2] AS $match)
					{
						if (preg_match('#filedata\/fetch\?(?:filedata)?id=(\d+)#si', $match, $idmatch))
						{
							$attachmentids[] = (int) $idmatch[1];
						}
						else if (preg_match('#^n(\d+)$#siU', $match, $idmatch))
						{
							$attachmentids[] = (int) $idmatch[1];
						}
						else if (!empty($match) AND ctype_digit($match))
						{
							// legacy attachment without the 'n' prefix
							$legacyattachmentids[] = (int) $match;
						}
					}
				}
			}
		}

		// convert legacy attachment ids to current attachment ids
		if (!empty($legacyattachmentids))
		{
			$legacyattachments = vB_Api::instanceInternal('filedata')->fetchLegacyAttachments($legacyattachmentids);

			foreach ($legacyattachments AS $legacyattachment)
			{
				$attachmentids[] = $legacyattachment['nodeid'];
			}
			unset($legacyattachmentids, $legacyattachments, $legacyattachment);
		}

		// try attachments (this includes gallery items from the "photo" content type)
		$attachments = array();
		if (!empty($node['attachments']) AND is_array($node['attachments']))
		{
			foreach($node['attachments'] AS &$attachment)
			{
				$attachmentids[] = $attachment['nodeid'];
				$attachments[$attachment['nodeid']] =& $attachment;
			}
		}
		unset($attachment);

		$attachmentids = array_unique($attachmentids);

		if (empty($attachmentids))
		{
			return;
		}

		// use the first image found
		$attachment = null;
		$imageExtensions = array('gif', 'jpg', 'jpeg', 'jpe', 'png', 'bmp', 'tiff', 'tif', 'psd', 'pdf');
		foreach ($attachmentids AS $attachmentid)
		{
			if (in_array($attachments[$attachmentid]['extension'], $imageExtensions, true))
			{
				$attachment =& $attachments[$attachmentid];
				break;
			}
		}

		if ($attachment === null)
		{
			return;
		}

		$data = array(
			'parentid' => $node['parentid'],
			'previewimage' => $attachment['nodeid'],
			'nodeoptions' => $node['nodeoptions'], // workaround for update() setting default nodeoptions. VBV-11645
		);

		unset($attachment);

		if ($data['previewimage'] != $node['previewimage'])
		{
			$this->update($nodeId, $data);
		}
	}
}
