<?php
/**
 * This file implements the BBcode plugin for b2evolution
 *
 * BB style formatting, like [b]bold[/b]
 *
 * b2evolution - {@link http://b2evolution.net/}
 * Released under GNU GPL License - {@link http://b2evolution.net/about/gnu-gpl-license}
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

load_class( '../plugins/_custom_tags.plugin.php', 'custom_tags_plugin' );

/**
 * @package plugins
 */
class bbcode_plugin extends custom_tags_plugin
{
	var $code = 'b2evBBco';
	var $name = 'BB code';
	var $priority = 50;
	var $version = '7.2.3';
	var $group = 'rendering';
	var $short_desc;
	var $long_desc;
	var $help_topic = 'bb-code-plugin';
	var $number_of_installs = 1;

	/*
	 * Internal
	 */
	var $toolbar_label = 'BB code:';

	var $configurable_post_list = true;
	var $configurable_comment_list = true;
	var $configurable_message_list = true;
	var $configurable_email_list = true;

	var $post_search_list;
	var $post_replace_list;
	var $comment_search_list;
	var $comment_replace_list;
	var $msg_search_list;
	var $msg_replace_list;
	var $email_search_list;
	var $email_replace_list;

	var $default_search_list = '[b] #\[b](.+?)\[/b]#is
[i] #\[i](.+?)\[/i]#is
[s] #\[s](.+?)\[/s]#is
[color] !\[color=(#?[A-Za-z0-9]+?)](.+?)\[/color]!is
[size] #\[size=([0-9]+?)](.+?)\[/size]#is
[font] #\[font=([A-Za-z0-9 ;\-]+?)](.+?)\[/font]#is
 #\[quote(=?)](.+?)\[/quote]#is
 #\[quote=([^\]\#]*?)\#([cp][0-9]+)](.+?)\[/quote]#is
[quote] #\[quote=([^\]]*?)](.+?)\[/quote]#is
[indent] #\[indent](.+?)\[/indent]#is
[list=1] #\[list=1](.+?)\[/list]#is
[list=a] #\[list=a](.+?)\[/list]#is
[list] #\[list](.+?)\[/list]#is
[*] #\[\*](.+?)(\n|\[/list\])#is
[bg] !\[bg=(#?[A-Za-z0-9]+?)](.+?)\[/bg]!is
[clear] #\[clear]#is';

	var $default_replace_list = '<strong>$1</strong>
<em>$1</em>
<span style="text-decoration:line-through">$1</span>
<span style="color:$1">$2</span>
<span style="font-size:$1px">$2</span>
<span style="font-family:$1">$2</span>
<blockquote>$2</blockquote>
<strong class="quote_author">$1 wrote <a href="#$2">earlier</a>:</strong><blockquote>$3</blockquote>
<strong class="quote_author">$1 wrote:</strong><blockquote>$2</blockquote>
<div class="indented">$1</div>
<ol type="1">$1</ol>
<ol type="a">$1</ol>
<ul>$1</ul>
<li>$1</li>
<span style="background-color:$1">$2</span>
<div class="clear"></div>';


	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->short_desc = T_('BB formatting e-g [b]bold[/b]');
		$this->long_desc = T_('Supported tags and the BB code toolbar are configurable.
Supported tags by default are: [b] [i] [s] [color=...] [size=...] [font=...] [quote] [list=1] [list=a] [list] [*] [bg=] [clear]');
	}


	/**
	 * Parse BB code
	 *   ( The main purpose of this function is to parsing multilevel lists tags,
	 *     i.e. when one [list] is contained inside other [list] )
	 *
	 * @param string Content
	 * @param array Search list
	 * @param array Replace list
	 * @return string Content
	 */
	function parse_bbcode( $content, $search_list, $replace_list )
	{
		if( empty( $content ) )
		{ // No content, Exit here
			return $content;
		}

		$complex = array();
		$simple = array();

		foreach( $search_list as $i => $search_text )
		{ // Split the masks to separate things
			preg_match( '#(\\\\\[.+\])\(.+\)(\\\\\[.+\])#is', $search_text, $search_tags );
			preg_match( '#(<.+>)\$.+(<.+>)#is', $replace_list[ $i ], $replace_tags );
			if( !empty( $search_tags ) )
			{
				$complex[] = array(
						's_full'  => $search_text,
						's_start' => '!^'.$search_tags[1].'!is',
						's_end'   => '!^'.$search_tags[2].'!is',
						'r_full'  => $replace_list[ $i ],
						'r_start' => $replace_tags[1],
						'r_end'   => $replace_tags[2],
					);
			}
			else
			{
				$simple[] = array(
						's_full' => $search_text,
						'r_full' => $replace_list[ $i ],
					);
			}
		}
		//pre_dump($complex);

		foreach( $simple as $tag_info )
		{ // Make simple replacements
			$content = preg_replace( $tag_info['s_full'], $tag_info['r_full'], $content );
		}

		// Complex parsing with multilevel tags structure
		$c_pos = 0;
		$content_length = strlen( $content );
		$opened_tags = array();
		$new_content = $content;
		while( $c_pos < $content_length - 2 )
		{
			if( $content[ $c_pos ].$content[ $c_pos + 1 ] == '[/' )
			{ // Closed tag
				if( !empty( $opened_tags ) )
				{	// Get closed tag that was opened as last
					$sub_content = substr( $content, $c_pos );
					$closed_tag_info = $complex[ $opened_tags[ count( $opened_tags ) - 1 ] ];
					if( preg_match( $closed_tag_info['s_end'], $sub_content ) )
					{
						$first_part = str_replace( $sub_content, '', $new_content );
						$sub_content = preg_replace( $closed_tag_info['s_end'], $closed_tag_info['r_end'], $sub_content );
						$new_content = $first_part.$sub_content;
						//pre_dump( 'CLOSE', $opened_tags, $closed_tag_info['r_end'], $closed_tag_info['s_end'], $new_content );
						// Remove this tag from opened tags array
						array_pop( $opened_tags );
					}
				}
			}
			elseif( $content[ $c_pos ] == '[' )
			{ // Opened tag
				$sub_content = substr( $content, $c_pos );
				foreach( $complex as $tag_num => $tag_info )
				{
					if( preg_match( $tag_info['s_start'], $sub_content ) )
					{
						$first_part = str_replace( $sub_content, '', $new_content );
						$sub_content = preg_replace( $tag_info['s_start'], $tag_info['r_start'], $sub_content );
						$new_content = $first_part.$sub_content;
						// Add this tag to know what tag was opened as last
						$opened_tags[] = $tag_num;
						//pre_dump( 'OPEN', $opened_tags, $tag_info['s_start'], $new_content );
						break;
					}
				}
			}
			$c_pos++;
		}
		//pre_dump($new_content);

		$new_content = $this->parse_anchor_links( $new_content );

		return $new_content;
	}


	/**
	 * Parse anchor links, Set absolute path for each link with relative anchor like <a href="#">
	 *
	 * @param string Content
	 * @return string Content
	 */
	function parse_anchor_links( $content )
	{
		if( preg_match_all( '/ href="#(c|p)([0-9]+)"/i', $content, $matches ) )
		{
			$CommentCache = & get_CommentCache();
			$ItemCache = & get_ItemCache();
			foreach( $matches[0] as $m => $full_match )
			{
				$object_ID = $matches[2][$m];
				$new_url = '';
				switch( $matches[1][$m] )
				{ // Object type:
					case 'p':
						// Item
						if( $Item = & $ItemCache->get_by_ID( $object_ID, false, false ) )
						{ // Replace anchor url with item permanent url
							$new_url = $Item->get_permanent_url().'#p'.$object_ID;
						}
						break;

					case 'c':
						// Comment
						if( $Comment = & $CommentCache->get_by_ID( $object_ID, false, false ) )
						{ // Replace anchor url with comment permanent url
							$new_url = $Comment->get_permanent_url();
						}
						break;

					default:
						// Incorrect object type, Skip this url
						continue 2;
				}
				if( !empty( $new_url ) )
				{ // Replace relative anchor url with new absolute url
					$content = str_replace( $full_match, ' href="'.$new_url.'"', $content );
				}
			}
		}

		return $content;
	}

	/**
	 * The following function are here so the events will be registered
	 * @see Plugins_admin::get_registered_events()
	 */
	function RenderItemAsHtml( & $params )
	{
		parent::RenderItemAsHtml( $params );
	}

	function RenderMessageAsHtml( & $params )
	{
		parent::RenderMessageAsHtml( $params );
	}

	function RenderEmailAsHtml( & $params )
	{
		parent::RenderItemAsHtml( $params );
	}


	/**
	 * Do the same as for HTML.
	 *
	 * @see RenderItemAsHtml()
	 */
	function RenderItemAsXml( & $params )
	{
		$this->RenderItemAsHtml( $params );
	}


	/**
	 *
	 * Render comments if required
	 *
	 * @see Plugin::FilterCommentContent()
	 */
	function FilterCommentContent( & $params )
	{
		$Comment = & $params['Comment'];
		$comment_Item = & $Comment->get_Item();
		$item_Blog = & $comment_Item->get_Blog();
		if( in_array( $this->code, $Comment->get_renderers_validated() ) )
		{ // apply_comment_rendering is set to always render
			$content = & $params['data'];
			if( !isset( $this->comment_search_list ) )
			{	// Init comment search list only first time:
				$this->comment_search_list = $this->prepare_search_list( $this->get_coll_setting( 'coll_comment_search_list', $item_Blog ) );
			}

			if( !isset( $this->comment_replace_list ) )
			{	// Init comment replace list only first time:
				$this->comment_replace_list = $this->prepare_replace_list( $this->get_coll_setting( 'coll_comment_replace_list', $item_Blog ) );
			}

			$content = replace_outside_code_and_short_tags( $this->comment_search_list, $this->comment_replace_list, $content, array( $this, 'parse_bbcode' ) );
		}
	}


	/**
	 * Event handler: Called when displaying editor toolbars on post/item form.
	 *
	 * This is for post/item edit forms only. Comments, PMs and emails use different events.
	 *
	 * @param array Associative array of parameters
	 * @return boolean did we display a toolbar?
	 */
	function AdminDisplayToolbar( & $params )
	{
		$params['target_type'] = 'Item';
		return $this->DisplayCodeToolbar( $params );
	}


	/**
	 * Event handler: Called when displaying editor toolbars on comment form.
	 *
	 * @param array Associative array of parameters
	 * @return boolean did we display a toolbar?
	 */
	function DisplayCommentToolbar( & $params )
	{
		$params['target_type'] = 'Comment';
		return $this->DisplayCodeToolbar( $params );
	}


	/**
	 * Event handler: Called when displaying editor toolbars for message.
	 *
	 * @param array Associative array of parameters
	 * @return boolean did we display a toolbar?
	 */
	function DisplayMessageToolbar( & $params )
	{
		$params['target_type'] = 'Message';
		return $this->DisplayCodeToolbar( $params );
	}


	/**
	 * Event handler: Called when displaying editor toolbars for email.
	 *
	 * @param array Associative array of parameters
	 * @return boolean did we display a toolbar?
	 */
	function DisplayEmailToolbar( & $params )
	{
		$apply_rendering = $this->get_email_setting( 'email_apply_rendering' );
		if( ! empty( $apply_rendering ) && $apply_rendering != 'never' )
		{
			$params['target_type'] = 'EmailCampaign';
			return $this->DisplayCodeToolbar( $params );
		}
		return false;
	}


	/**
	 * Prepare a search list
	 *
	 * @param string String value of a search list
	 * @return array The search list as array
	 */
	function prepare_search_list( $search_list_string )
	{
		$search_list_array = explode( "\n", str_replace( "\r", '', $search_list_string ) );

		foreach( $search_list_array as $l => $line )
		{	// Remove button name from regexp string
			$line = explode( ' ', $line, 2 );
			if( empty( $line[1] ) )
			{	// Bad format of search string
				unset( $search_list_array[ $l ] );
			}
			else
			{	// Replace this line with regexp value (to delete a button name)
				$search_list_array[ $l ] = $line[1];
			}
		}

		return $search_list_array;
	}
}

?>
