<?php
/**
* @version	$Id: post_helper.php 16685 2021-03-15 08:40:04Z alex $
* @package	In-Bulletin
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class PostHelper extends kHelper {

		var $postOptionBits = Array (
			'show_sig' => 128,
			'disable_bbcode' => 64,
			'disable_smileys' => 32,
		);

		/**
		 * Checks if specific option is set for post
		 *
		 * @param string $option_name
		 * @param Array $options
		 * @return bool
		 */
		function GetPostOption($option_name, $options)
		{
			if (!isset($this->postOptionBits[$option_name])) {
				return false;
			}

			$option_bit = $this->postOptionBits[$option_name];
			return ($options & $option_bit) == $option_bit;
		}

		/**
		 * Sets given option bit (by name) to post options
		 *
		 * @param string $option_name
		 * @param int $option_value
		 * @param Array $options
		 * @return bool
		 */
		function SetPostOption($option_name, $option_value, &$options)
		{
			if (!isset($this->postOptionBits[$option_name])) {
				return false;
			}

			$option_bit = $this->postOptionBits[$option_name];
			if ($option_value) {
				$options |= $option_bit;
			}
			else {
				$options = $options &~ $option_bit;
			}

			return true;
		}

		/**
		 * Returns post options map to virtual field names
		 *
		 * @return Array
		 */
		function getOptionsMap()
		{
			$options_map = Array (
				'show_sig' => 'ShowSignatures',
				'disable_smileys' => 'DisableSmileys',
				'disable_bbcode' => 'DisableBBCodes',
			);

			return $options_map;
		}

		/**
	     * @return void
	     * @param int $date
	     * @desc Set any field to category & all it's parent categories
	     */
		 function PropagateCategoryField($category_id, $field_name, $field_value)
		 {
			 $id_field = $this->Application->getUnitOption('c', 'IDField');
			 $table_name = $this->Application->getUnitOption('c', 'TableName');

			 $sql = 'SELECT ParentPath
		 			FROM ' . $table_name . '
		 			WHERE ' . $id_field . ' = ' . $category_id;
			 $parent_path = $this->Conn->GetOne($sql);

			 $parent_categories = explode('|', substr($parent_path, 1, -1));
			 if ( !$parent_categories ) {
				 return;
			 }

			 $fields_hash = Array ($field_name => $field_value);

			 $this->Conn->doUpdate($fields_hash, $table_name, $id_field . ' IN (' . implode(',', $parent_categories) . ')');
		 }

		/**
		 * Sets today posts count & today date for topic
		 *
		 * @param kCatDBItem $object
		 * @param int $post_date
		 * @param int $increment_by
		 * @return bool
		 */
		function updateTodayPostsCount(&$object, $post_date, $increment_by = 1)
		{
			$date_now = adodb_date('Y-m-d');

			if (adodb_date('Y-m-d', $post_date) != $date_now) {
				return true;
			}

			// last post update date was today or not
			$today_posts = ($date_now == $object->GetDBField('TodayDate')) ? $object->GetDBField('TodayPosts') : 0;

			$object->SetDBField('TodayDate', $date_now);
			$object->SetDBField('TodayPosts', $today_posts + $increment_by);

			return $object->Update();
		}

		function updatePostCount($topic_id, $increment = 1)
		{
			$id_field = $this->Application->getUnitOption('bb', 'IDField');
			$table_name = $this->Application->getUnitOption('bb', 'TableName');

			// helps in case, when 2 (or more) users tries to post in same topic at same time
			$sql = 'UPDATE '.$table_name.'
					SET Posts = Posts '.($increment > 0 ? '+' : '-').' '.abs($increment).'
					WHERE '.$id_field.' = '.$topic_id;
			$this->Conn->Query($sql);

			// returns new value
			$sql = 'SELECT Posts
					FROM '.$table_name.'
					WHERE '.$id_field.' = '.$topic_id;
			return $this->Conn->GetOne($sql);
		}
		/**
		 * Replaces all special formatting in post before displaing it to user
		 *
		 * @param string $post_body
		 * @param int $post_options bit array of post options
		 * @param Array $sub_blocks block names for rendering smileys & bbcodes
		 * @return string
		 */
		function parsePostBody($post_body, $post_options, $sub_blocks)
		{
			// 1. escape all html sequences
			$post_body = htmlspecialchars($post_body, ENT_NOQUOTES, CHARSET); // don't touch quotes in bbcode attribute values

			// 2. replace censored words
			$post_body = $this->CensorText($post_body);

			// 3. replace bb codes
        	if (!$this->GetPostOption('disable_bbcode', $post_options)) {
        		$post_body = $this->replaceBBCodes($post_body, $sub_blocks['bbcode']);
        	}

			// 4. replace smileys
        	if (!$this->GetPostOption('disable_smileys', $post_options)) {
        		$post_body = $this->replaceSmileys($post_body, $sub_blocks['smileys']);
        	}

        	// 5. add enters (because we don't use HTML in post body)
        	$post_body = nl2br($post_body);

        	// 6. replace quoted text
			return $this->replacePostQuote($post_body, $sub_blocks['quote']);
		}

		function replacePostQuote($text, $render_as)
		{
			if (preg_match('/\[quote id=([\d]+)\](.*)\[\/quote\]/s', $text, $regs)) {
				/** @var kDBItem $post */
				$post = $this->Application->recallObject('bb-post.-item', null, Array ('skip_autoload' => true));

				$post->Load($regs[1]);

				$block_params = Array ('name' => $render_as, 'PrefixSpecial' => 'bb-post.-item', 'Prefix' => 'bb-post', 'Special' => '-item', 'strip_nl' => 2);
				$parsed_quote = $this->Application->ParseBlock($block_params);
				return str_replace($regs[0], $parsed_quote, $text);
			}

			return $text;
		}

		/**
		 * Replaces bad words with good words (censorship process)
		 *
		 * @param string $text
		 * @return string
		 */
		function CensorText($text)
		{
			static $censor_words = null;

			if (!isset($censor_words)) {
				$sql = 'SELECT Replacement, BadWord
						FROM '.TABLE_PREFIX.'Censorship';
				$censor_words = $this->Conn->GetCol($sql, 'BadWord');
			}

			foreach ($censor_words as $replace_from => $replace_to) {
				$text = str_replace($replace_from, $replace_to, $text);
			}

			return $text;
		}

		function replaceSmileys($text, $smiley_element)
		{
			static $smileys = null;

			if (!isset($smileys)) {
				$sql = 'SELECT em.EmotionImage, em.KeyStroke
						FROM '.TABLE_PREFIX.'Emoticon em
						WHERE em.Enabled = 1
						ORDER BY CHAR_LENGTH(em.KeyStroke) DESC';
				$smileys = $this->Conn->GetCol($sql, 'KeyStroke');
			}

			$block_params = Array ('name' => $smiley_element, 'smiley_url' => '#SMILEY_URL#');
			$smiley_mask = trim($this->Application->ParseBlock($block_params));

			$base_url = rtrim($this->Application->BaseURL(),'/');
			foreach ($smileys as $key_stoke => $image_url) {
				if (strpos($text, $key_stoke) === false) {
					continue;
				}

				$smiley_html = str_replace('#SMILEY_URL#', $base_url.SMILEYS_PATH.$image_url, $smiley_mask);
				$text = str_replace($key_stoke, $smiley_html, $text);
			}

			return $text;
		}

		/**
		 * Sort params by name and then by length
		 *
		 * @param string $a
		 * @param string $b
		 * @return int
		 * @access private
		 */
		function CmpParams($a, $b)
		{
			list ($a, ) = explode(':', $a);
			list ($b, ) = explode(':', $b);

			$a_len = strlen($a);
			$b_len = strlen($b);
			if ($a_len == $b_len) return 0;
			return $a_len > $b_len ? -1 : 1;
		}

		function replaceBBCodes($text, $bbcode_element)
		{
		  	// convert phpbb bbcodes to in-bulletin bbcodes
		  	$text = $this->preformatBBCodes($text);

		  	$tags_defs = explode(';', $this->Application->ConfigValue('BBTags')); // 'b:;i:;u:;ul:type|align;font:color|face|size;url:href;img:src|border';

		  	usort($tags_defs, Array (&$this, 'CmpParams'));

		  	foreach($tags_defs as $tag) {
		  		list ($tag_name, $tag_params) = explode(':', $tag);
		  		$tag_params = $tag_params ? array_flip(explode('|', $tag_params)) : 0;
				$that = $this;
				$text = preg_replace_callback(
					'/\[' . $tag_name . '(.*)\](.*)\[\/' . $tag_name . ' *\]/Uis',
					function ($matches) use ($that, $tag_name, $tag_params) {
						return $that->checkBBCodeAttribs($tag_name, $matches[1], $matches[2], $tag_params);
					},
					$text
				);
		  	}

		  	// additional processing for [url], [*], [img] bbcode
			$text = preg_replace('/<url>(.*)<\/url>/Usi','<url href="$1">$1</url>',$text);
			$text = preg_replace('/<font>(.*)<\/font>/Usi','$1',$text); // skip empty fonts
		  	$text = str_replace(	Array('<url','</url>','[*]'),
		  							Array('<a target="_blank"','</a>','<li>'),
		  							$text);

		  	// bbcode [code]xxx[/code] processing
			$that = $this;
			$text = preg_replace_callback(
				'/\[code\](.*)\[\/code\]/Uis',
				function ($matches) use ($that, $bbcode_element) {
					return $that->replaceCodeBBCode($matches, $bbcode_element);
				},
				$text
			);

		  	return $text;
		}

		/**
		 * Convert phpbb url bbcode to valid in-bulletin's format
		 *
		 * @param string $text
		 * @return string
		 */
		function preformatBBCodes($text)
		{
			// 1. urls
			$text = preg_replace('/\[url=(.*)\](.*)\[\/url\]/Ui','[url href="$1"]$2[/url]',$text);
			$text = preg_replace('/\[url\](.*)\[\/url\]/Ui','[url href="$1"]$1[/url]',$text);

			// 2. images
			$text = preg_replace('/\[img\](.*)\[\/img\]/Ui','[img src="$1" border="0"][/img]',$text);

			// 3. color
			$text = preg_replace('/\[color=(.*)\](.*)\[\/color\]/Ui','[font color="$1"]$2[/font]',$text);

			// 4. size
			$text = preg_replace('/\[size=(.*)\](.*)\[\/size\]/Ui','[font size="$1"]$2[/font]',$text);

			// 5. lists
			$text = preg_replace('/\[list(.*)\](.*)\[\/list\]/Uis','[ul]$2[/ul]',$text);

			// 6. email to link
			$text = preg_replace('/\[email\](.*)\[\/email\]/Ui','[url href="mailto:$1"]$1[/url]',$text);

			//7. b tag
			$text = preg_replace('/\[(b|i|u):(.*)\](.*)\[\/(b|i|u):(.*)\]/Ui','[$1]$3[/$4]',$text);

			//8. code tag
			$text = preg_replace('/\[code:(.*)\](.*)\[\/code:(.*)\]/Uis','[code]$2[/code]',$text);

			return $text;
		}

		/**
		 * Removes not allowed params from tag and returns result
		 *
		 * @param string $BBCode bbcode to check
		 * @param string $TagParams params string entered by user
		 * @param string $TextInside text between opening and closing bbcode tag
		 * @param string $ParamsAllowed list of allowed parameter names ("|" separated)
		 * @return string
		 */
		function checkBBCodeAttribs($BBCode, $TagParams, $TextInside, $ParamsAllowed)
		{
			// unescape escaped quotes in tag
			$TagParams = str_replace('\"', '"', $TagParams);
			$TextInside = str_replace('\"', '"', $TextInside);

			$params_extracted = preg_match_all('/ +([^=]*)=["\']?([^ "\']*)["\']?/is', $TagParams, $extracted_params, PREG_SET_ORDER);

			if ($ParamsAllowed && $params_extracted) {
				$ret = Array();
				foreach ($extracted_params as $param) {
					$param_name = strtolower(trim( $param[1] ));
					$param_value = trim($param[2]);

					// 1. prevent hacking
					if ($BBCode == 'url' && $param_name == 'href') {
						if (strpos(strtolower($param_value), 'script:') !== false) {
							// script tag found in "href" parameter of "url" bbcode (equals to hacking) -> remove bbcode
							return $TextInside;
						}
					}

					// 2. leave only allowed params & remove all not allowed
					if (isset($ParamsAllowed[$param_name])) {
						$ret[] = $param_name.'="'.$param_value.'"';
					}
				}

				$ret = count($ret) ? ' '.implode(' ', $ret) : '';
				return '<'.$BBCode.$ret.'>'.$TextInside.'</'.$BBCode.'>';
			}

			return '<'.$BBCode.'>'.$TextInside.'</'.$BBCode.'>';
		}

		function highlightCode($code, $strip_tabs = 0)
		{
			if ($strip_tabs) {
				$code = preg_replace('/(\t){'.$strip_tabs.'}(.*)/', '\\2', $code);
			}

			$code = str_replace( Array('\\', '/') , Array('_no_match_string_', '_n_m_s_'), $code);
			$code = highlight_string('<?php'.$code.'?>', true);
			$code = str_replace( Array('_no_match_string_', '_n_m_s_'), Array('\\', '/'), $code);
			$code = preg_replace('/&lt;\?(.*)php(.*)\?&gt;/Us', '\\2', $code);

			$code = preg_replace('/<code><font color="(.*)">([\r\n]+)/si', '<code><font color="\\1">', $code);
			$code = preg_replace('/([\r\n]+)<\/font>([\r\n]+)<\/code>/si', '</font></code>', $code);

			return $code;
		}

		/**
		 * Callback function for preg_replace string processing, replaces [code]php code[/code] bbcode in post
		 *
		 * @param array  $matches        Intermediate result of preg_replace operation.
		 * @param string $bbcode_element BBCode element replacement.
		 *
		 * @return string
		 * @see    parsePostBody about why we unescape here.
		 */
		public function replaceCodeBBCode(array $matches, $bbcode_element)
		{
			static $bbcode_mask = null;

			if (!isset($bbcode_mask)) {
				$block_params = Array ('name' => $bbcode_element, 'bb_code' => '#BB_CODE#');
				$bbcode_mask = trim($this->Application->ParseBlock($block_params));
			}

			$input_string = trim(str_replace('\"', '"', kUtil::unescape($matches[1], kUtil::ESCAPE_HTML)));
			$input_string = $this->highlightCode($input_string);
			$input_string = preg_replace("/\r<br \/>/s", "\r", $input_string); // undo nl2br added in highlighting
			$input_string = str_replace('#BB_CODE#', $input_string, $bbcode_mask);

			return $input_string;
		}

		/**
		 * Returns subscription manager by name
		 *
		 * @param string $name
		 * @param array $arguments
		 * @return kSubscriptionManager
		 * @throws InvalidArgumentException
		 */
		public function getSubscriptionManager($name, $arguments = Array ())
		{
			if ( $name != 'CategoryTopics' && $name != 'TopicPosts' ) {
				throw new InvalidArgumentException('Unknown subscription manager "' . $name . '"');
			}

			/** @var kSubscriptionManager $manager */
			$manager = $this->Application->makeClass('kSubscriptionManager');

			$fields_hash = Array ();

			$user_id = isset($arguments[1]) ? $arguments[1] : $this->Application->RecallVar('user_id');

			switch ( $name ) {
				case 'CategoryTopics':
					$category_id = isset($arguments[0]) ? $arguments[0] : $this->Application->GetVar('m_cat_id');

					$fields_hash = Array (
						'EmailTemplateId' => $manager->getEmailTemplateId('TOPIC.ADD.SUB'),
						'UserId' => $user_id,
						'CategoryId' => $category_id,
					);
					break;

				case 'TopicPosts':
					$fields_hash = Array (
						'EmailTemplateId' => $manager->getEmailTemplateId('POST.ADD.SUB'),
						'UserId' => $user_id,
						'ParentItemId' => $arguments[0],
					);
					break;
			}

			$manager->add($fields_hash);

			return $manager;
		}
	}