<?php
/**
 * This file implements the Short Links plugin for b2evolution
 *
 * Creates wiki links
 *
 * 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
 * @ignore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * @package plugins
 */
class shortlinks_plugin extends Plugin
{
	var $code = 'b2evWiLi';
	var $name = 'Short Links';
	var $priority = 35;
	var $version = '7.2.5';
	var $group = 'rendering';
	var $short_desc;
	var $long_desc;
	var $help_topic = 'wiki-links-plugin';
	var $number_of_installs = 1;

	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->short_desc = T_('Wiki Links converter');
		$this->long_desc = T_('You can create links with [[CamelCase]] or ((CamelCase)) which will try to link to the category or the post with the slug "camel-case". See manual for more.');
	}


	/**
	 * Define here default custom settings that are to be made available
	 *     in the backoffice for collections, private messages and newsletters.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::get_custom_setting_definitions()}.
	 */
	function get_custom_setting_definitions( & $params )
	{
		return array(
			'link_types' => array(
				'label' => T_('Link types to allow'),
				'type' => 'checklist',
				'options' => array(
						array( 'absolute_urls',         sprintf( $this->T_('Absolute URLs (starting with %s or %s) in brackets'), '<code>http://</code>, <code>https://</code>, <code>mailto://</code>', '<code>//</code>' ), 1 ),
						array( 'abs_url_target_blank',  $this->T_('Open in new tab').' (<code>target="_blank"</code>)', 1, NULL, NULL, NULL, NULL, NULL, array( 'style' => 'margin-left:20px' ) ),
						array( 'abs_url_optimize',      $this->T_('If an absolute URL references a collection on this system, try to optimize and keep just the slug '), 1, NULL, NULL, NULL, NULL, NULL, array( 'style' => 'margin-left:20px' ) ),
						array( 'relative_urls',         sprintf( $this->T_('Relative URLs (starting with %s followed by a letter or digit) in brackets'), '<code>/</code>' ), 0 ),
						array( 'rel_url_optimize',      $this->T_('If a relative URL references a collection on this system, try to optimize and keep just the slug'), 1, NULL, NULL, NULL, NULL, NULL, array( 'style' => 'margin-left:20px' ) ),
						array( 'anchor',                sprintf( $this->T_('Current page anchor URLs (starting with %s) in brackets'), '<code>#</code>' ), 1 ),
						array( 'cat_slugs',             $this->T_('Category slugs in brackets'), 1 ),
						array( 'item_slugs',            $this->T_('Item slugs in brackets'), 1 ),
						array( 'item_id',               $this->T_('Item ID in brackets'), 1 ),
						array( 'cat_without_brackets',  $this->T_('WikiWords without brackets matching category slugs'), 0 ),
						array( 'item_without_brackets', $this->T_('WikiWords without brackets matching item slugs'), 0 ),
					),
				),
			);
	}


	/**
	 * Define here default collection/blog settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::get_coll_setting_definitions()}.
	 */
	function get_coll_setting_definitions( & $params )
	{
		$default_values = array(
				'default_post_rendering' => 'opt-out'
			);

		$default_params = array_merge( $params, $default_values );

		return parent::get_coll_setting_definitions( $default_params );
	}


	/**
	 * Define here default message settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_msg_setting_definitions( & $params )
	{
		// set params to allow rendering for messages by default
		$default_params = array_merge( $params, array( 'default_msg_rendering' => 'opt-out' ) );
		return parent::get_msg_setting_definitions( $default_params );
	}


	/**
	 * Define here default email settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_email_setting_definitions( & $params )
	{
		// set params to allow rendering for emails by default:
		$default_params = array_merge( $params, array( 'default_email_rendering' => 'opt-out' ) );
		return parent::get_email_setting_definitions( $default_params );
	}


	/**
	 * Define here default shared settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_shared_setting_definitions( & $params )
	{
		// set params to allow rendering for shared container widgets by default:
		$default_params = array_merge( $params, array( 'default_shared_rendering' => 'opt-out' ) );
		return parent::get_shared_setting_definitions( $default_params );
	}


	/**
	 * Event handler: Called when displaying an item/post's content as HTML.
	 *
	 * This is different from {@link RenderItemAsHtml()}, because it gets called
	 * on every display (while rendering gets cached).
	 *
	 * @param array Associative array of parameters
	 * @return boolean Have we changed something?
	 */
	function DisplayItemAsHtml( & $params )
	{
		$content = & $params['data'];

		// Replace the create post links with simple text if current user has no perm to create a post:
		$content = replace_outside_code_tags( '#<a[^>]+href="([^"]+)"[^>]+data-function="create_post" data-coll="(\d+)"[^>]*>(.+?)</a>#i', array( $this, 'callback_replace_post_links' ), $content, 'replace_content_callback' );

		return true;
	}


	/**
	 * Perform rendering
	 *
	 * @param array Associative array of parameters
	 *   'data': the data (by reference). You probably want to modify this.
	 *   'format': see {@link format_to_output()}. Only 'htmlbody' and 'entityencoded' will arrive here.
	 * @return boolean true if we can render something for the required output format
	 */
	function RenderItemAsHtml( & $params )
	{
		$content = & $params['data'];

		// Get collection from given params:
		$setting_Blog = $this->get_Blog_from_params( $params );

		// Get currently rendering Item:
		$this->current_Item = $this->get_Item_from_params( $params );

		$this->link_types = $this->get_coll_setting( 'link_types', $setting_Blog );

		return $this->render_content( $content );
	}


	/**
	 * Perform rendering of Message content
	 *
	 * NOTE: Use default coll settings of comments as messages settings
	 *
	 * @see Plugin::RenderMessageAsHtml()
	 */
	function RenderMessageAsHtml( & $params )
	{
		$content = & $params['data'];

		$this->link_types = $this->get_msg_setting( 'link_types' );

		return $this->render_content( $content );
	}


	/**
	 * Perform rendering of Email content
	 *
	 * NOTE: Use default coll settings of comments as messages settings
	 *
	 * @see Plugin::RenderEmailAsHtml()
	 */
	function RenderEmailAsHtml( & $params )
	{
		$content = & $params['data'];

		$this->render_type = 'email';
		$this->link_types = $this->get_email_setting( 'link_types' );

		return $this->render_content( $content );
	}


	/**
	 * Render content of Item, Comment, Message
	 *
	 * @todo get rid of global $blog
	 *
	 * @param string Content
	 * @return boolean
	 */
	function render_content( & $content )
	{
		global $admin_url, $blog, $evo_charset, $post_ID;

		// Add regexp modifier 'u' to work with UTF-8 strings correctly:
		$regexp_modifier = ( $evo_charset == 'utf-8' ) ? 'u' : '';

		// -------- ABSOLUTE BRACKETED URLS -------- :
		if( ! empty( $this->link_types['absolute_urls'] ) )
		{	// If it is allowed by plugin setting
			$search_urls = '*
				( \[\[ | \(\( )                    # Lookbehind for (( or [[
				( (https?://|mailto://|//)[^<>{}\s\]]+ ) # URL
				( \s \.[a-z0-9_\-\.]+ )?           # Style classes started and separated with dot (Optional)
				( \s _[a-z0-9_\-]+ )?              # Link target started with _ (Optional)
				( \s [^\n\r]+? )?                  # Custom link text instead of URL (Optional)
				( \]{2,} | \){2,} )                # Lookahead for )) or ]]
				*ix'; // x = extended (spaces + comments allowed)
			$content = replace_outside_code_and_short_tags( $search_urls, array( $this, 'callback_replace_bracketed_urls' ), $content, 'replace_content', 'preg_callback' );
		}

		// -------- RELATIVE BRACKETED URLS -------- :
		if( ! empty( $this->link_types['relative_urls'] ) )
		{	// If it is allowed by plugin setting
			$search_urls = '*
				( \[\[ | \(\( )                    # Lookbehind for (( or [[
				( (/)[^/][^<>{}\s\]]+ ) # URL
				( \s \.[a-z0-9_\-\.]+ )?           # Style classes started and separated with dot (Optional)
				( \s _[a-z0-9_\-]+ )?              # Link target started with _ (Optional)
				( \s [^\n\r]+? )?                  # Custom link text instead of URL (Optional)
				( \]{2,} | \){2,} )                # Lookahead for )) or ]]
				*ix'; // x = extended (spaces + comments allowed)
			$content = replace_outside_code_and_short_tags( $search_urls, array( $this, 'callback_replace_bracketed_urls' ), $content, 'replace_content', 'preg_callback' );
		}

/* QUESTION: fplanque, implementation of this planned? then use make_clickable() - or remove this comment
	$ret = preg_replace("#([\n ])aim:([^,< \n\r]+)#i", "\\1<a href=\"aim:goim?screenname=\\2\\3&message=Hello\">\\2\\3</a>", $ret);

	$ret = preg_replace("#([\n ])icq:([^,< \n\r]+)#i", "\\1<a href=\"http://wwp.icq.com/scripts/search.dll?to=\\2\\3\">\\2\\3</a>", $ret);

	$ret = preg_replace("#([\n ])www\.([a-z0-9\-]+)\.([a-z0-9\-.\~]+)((?:/[^,< \n\r]*)?)#i", "\\1<a href=\"http://www.\\2.\\3\\4\">www.\\2.\\3\\4</a>", $ret);

	$ret = preg_replace("#([\n ])([a-z0-9\-_.]+?)@([^,< \n\r]+)#i", "\\1<a href=\"mailto:\\2@\\3\">\\2@\\3</a>", $ret); */

		// To use function replace_special_chars()
		load_funcs('locales/_charset.funcs.php');

		// -------- STANDALONE WIKIWORDS -------- :
		if( ! empty( $this->link_types['cat_without_brackets'] ) ||
		    ! empty( $this->link_types['item_without_brackets'] ) )
		{	// Create the links from standalone WikiWords

			$search_wikiwords = array();
			$replace_links = array();

			// STANDALONE WIKIWORDS:
			$search = '/
					(?<= \s | ^ )													# Lookbehind for whitespace
					([\p{Lu}]+[\p{Ll}0-9_]+([\p{Lu}]+[\p{L}0-9_]+)+)	# WikiWord or WikiWordLong
					(?= [\.,:;!\?] \s | \s | $ )											# Lookahead for whitespace or punctuation
				/x'.$regexp_modifier;	// x = extended (spaces + comments allowed)

			if( preg_match_all( $search, $content, $matches, PREG_SET_ORDER ) )
			{
				// Construct array of wikiwords to look up in post urltitles
				$wikiwords = array();
				foreach( $matches as $match )
				{
					// Convert the WikiWord to an urltitle
					$WikiWord = $match[0];
					$Wiki_Word = preg_replace( '*([^\p{Lu}_])([\p{Lu}])*'.$regexp_modifier, '$1-$2', $WikiWord );
					$wiki_word = utf8_strtolower( $Wiki_Word );
					// echo '<br />Match: [', $WikiWord, '] -> [', $wiki_word, ']';
					$wiki_word = replace_special_chars( $wiki_word );
					$wikiwords[ $WikiWord ] = $wiki_word;
				}

				// Lookup all urltitles at once in DB and preload cache:
				if( ! empty( $this->link_types['cat_without_brackets'] ) )
				{
					$ChapterCache = & get_ChapterCache();
					$ChapterCache->load_urlname_array( $wikiwords );
				}
				if( ! empty( $this->link_types['item_without_brackets'] ) )
				{
					$ItemCache = & get_ItemCache();
					$ItemCache->load_urltitle_array( $wikiwords );
				}

				// Construct arrays for replacing wikiwords by links:
				foreach( $wikiwords as $WikiWord => $wiki_word )
				{
					// WikiWord
					$search_wikiwords[] = '/
						(?<= \s | ^ ) 						# Lookbehind for whitespace or start
						(?<! evo_shortlink_broken">)
						'.$WikiWord.'							# Specific WikiWord to replace
						(?= [\.,:;!\?] \s | \s | $ )							# Lookahead for whitespace or end of string
						/sx';	// s = dot matches newlines, x = extended (spaces + comments allowed)


					// Find matching Item or Chapter:
					if( ! empty( $this->link_types['item_without_brackets'] ) &&
					    ( $Item = & $ItemCache->get_by_urltitle( $wiki_word, false, false ) ) )
					{	// Replace WikiWord with post permanent link if item is found:
						$replace_links[] = '<a href="'.$Item->get_permanent_url().'">'.$Item->get( 'title' ).'</a>';
					}
					elseif( ! empty( $this->link_types['cat_without_brackets'] ) &&
					        ( $Chapter = & $ChapterCache->get_by_urlname( $wiki_word, false, false ) ) )
					{	// Replace WikiWord with category permanent link if Chapter is found:
						$replace_links[] = '<a href="'.$Chapter->get_permanent_url().'">'.$Chapter->get( 'name' ).'</a>';
					}
					else
					{	// Replace WikiWord with broken link if Item and Chapter are not found:
						$replace_links[] = $this->get_broken_link( $wiki_word, $WikiWord );
					}
				}
			}

			// Replace all found standalone words with links:
			$content = replace_outside_code_and_short_tags( $search_wikiwords, $replace_links, $content );
		}

		// -------- BRACKETED WIKIWORDS -------- :
		if( ! empty( $this->link_types['anchor'] ) ||
		    ! empty( $this->link_types['cat_slugs'] ) ||
		    ! empty( $this->link_types['item_slugs'] ) ||
		    ! empty( $this->link_types['item_id'] ) )
		{	// If it is allowed by plugin settings:
			$search_anchor_slug_itemid = ( empty( $this->link_types['anchor'] ) && empty( $this->link_types['cat_slugs'] ) && empty( $this->link_types['item_slugs'] ) ) ?
					'([0-9]+) # Only item ID' :
					'([\p{L}0-9#]+[\p{L}0-9#_\-]*) # Anything from Wikiword to WikiWordLong';
			$search = '/
					(?<= \(\( | \[\[ )            # Lookbehind for (( or [[
					'.$search_anchor_slug_itemid.'
					(?=
						( \s .*? )?                 # Custom link text instead of post or chapter title with optional style classes
						( \){2,} | \]{2,} )             # Lookahead for )) or ]]
					)
				/x'.$regexp_modifier; // x = extended (spaces + comments allowed)
			if( preg_match_all( $search, $content, $matches, PREG_SET_ORDER ) )
			{
				// Construct array of wikiwords to look up in post urltitles
				$wikiwords = array();
				foreach( $matches as $match )
				{
					// Convert the WikiWord to an urltitle
					$WikiWord = $match[0];
					if( preg_match( '/^[\p{Ll}0-9#_\-]+$/'.$regexp_modifier, $WikiWord ) )
					{	// This WikiWord already matches a slug format
						$Wiki_Word = $WikiWord;
						$wiki_word = $Wiki_Word;
					}
					else
					{	// Convert WikiWord to slug format
						$Wiki_Word = preg_replace( array( '*([^\p{Lu}#_])([\p{Lu}#])*'.$regexp_modifier, '*([^0-9])([0-9])*'.$regexp_modifier ), '$1-$2', $WikiWord );
						$wiki_word = utf8_strtolower( $Wiki_Word );
					}
					// Remove additional params from $wiki_word, it should be cleared. We keep the params in $WikiWord and parse them below.
					$wiki_word = preg_replace( '/^([^#]+)(#.+)?$/i', '$1', $wiki_word );
					$wiki_word = replace_special_chars( $wiki_word );
					$wikiwords[ $WikiWord ] = $wiki_word;
				}

				// Lookup all urltitles at once in DB and preload cache:
				if( ! empty( $this->link_types['cat_slugs'] ) )
				{
					$ChapterCache = & get_ChapterCache();
					$ChapterCache->load_urlname_array( $wikiwords );
				}
				if( ! empty( $this->link_types['item_slugs'] ) )
				{
					$ItemCache = & get_ItemCache();
					$ItemCache->load_urltitle_array( $wikiwords );
				}

				// Replace wikiwords by links:
				foreach( $wikiwords as $WikiWord => $wiki_word )
				{
					// Initialize current wiki word which is used in callback function callback_replace_bracketed_words():
					$this->current_WikiWord = $WikiWord;
					$this->current_wiki_word = $wiki_word;

					// Fix for regexp:
					$WikiWord = preg_quote( $WikiWord, '#' );

					// [[WikiWord]]
					// [[WikiWord text]]
					// [[WikiWord .style.classes text]]
					// ((WikiWord))
					// ((WikiWord text))
					// ((WikiWord .style.classes text))
					$search_wikiword = '*
						( \[\[ | \(\( )          # Lookbehind for (( or [[
						'.$WikiWord.'            # Specific WikiWord to replace
						( \s \.[a-z0-9_\-\.]+ )? # Style classes started and separated with dot (Optional)
						( \s _[a-z0-9_\-]+ )?    # Link target started with _ (Optional)
						( \s .+? )?              # Custom link text instead of post/chapter title (Optional)
						( \]{2,} | \){2,} )      # Lookahead for )) or ]]
						*isx'; // s = dot matches newlines, x = extended (spaces + comments allowed)

					$content = replace_outside_code_and_short_tags( $search_wikiword, array( $this, 'callback_replace_bracketed_words' ), $content, 'replace_content', 'preg_callback' );
				}
			}
		}

		return true;
	}


	/**
	 * Callback function for replace_outside_code_tags to render links like [[http://site.com/page.html .style.classes text]] or ((http://site.com/page.html .style.classes text))
	 *
	 * @param array Matches of regexp
	 * @return string A processed link to the requested URL
	 */
	function callback_replace_bracketed_urls( $m )
	{
		if( ! ( $m[1] == '[[' && substr( $m[7], 0, 2 ) == ']]' ) &&
		    ! ( $m[1] == '((' && substr( $m[7], 0, 2 ) == '))' ) )
		{	// Wrong pattern, Return original text:
			return $m[0];
		}

		if( strlen( $m[7] ) > 2 )
		{	// Fix Custom link text with appending chars ] or ) if it is ended with chars ] or ):
			$m[6] .= substr( $m[7], 0, -2 );
		}

		// Clear custom link text:
		$custom_link_text = utf8_trim( $m[6] );

		// Clear custom link style classes:
		$custom_link_class = utf8_trim( str_replace( '.', ' ', $m[4] ) );

		if( $m[3] != '/' && ! empty( $this->link_types['abs_url_target_blank'] ) )
		{	// Force target to "_blank" for absolute URLs when it is defined in plugin settings:
			$custom_link_target = '_blank';
		}
		else
		{	// Use custom link target:
			$custom_link_target = utf8_trim( $m[5] );
		}

		// Build a link from bracketed URL:
		$r = '<a href="'.$m[2].'"';
		$r .= empty( $custom_link_class ) ? '' : ' class="'.$custom_link_class.'"';
		$r .= empty( $custom_link_target ) ? '' : ' target="'.$custom_link_target.'"';
		$r .= '>';
		$r .= empty( $custom_link_text ) ? $m[2] : $custom_link_text;
		$r .= '</a>';

		return $r;
	}


	/**
	 * Callback function for replace_outside_code_tags to render links like [[wiki-word .style.classes text]] or ((wiki-word .style.classes text))
	 *
	 * @param array Matches of regexp
	 * @return string A processed link to post/chapter URL OR a suggestion text to create new post from unfound post urltitle
	 */
	function callback_replace_bracketed_words( $m )
	{
		global $blog, $evo_charset, $admin_url;

		if( ! ( $m[1] == '[[' && substr( $m[5], 0, 2 ) == ']]' ) &&
		    ! ( $m[1] == '((' && substr( $m[5], 0, 2 ) == '))' ) )
		{	// Wrong pattern, Return original text:
			return $m[0];
		}

		if( strlen( $m[5] ) > 2 )
		{	// Fix Custom link text with appending chars ] or ) if it is ended with chars ] or ):
			$m[4] .= substr( $m[5], 0, -2 );
		}

		$ItemCache = & get_ItemCache();
		$ChapterCache = & get_ChapterCache();

		// Add regexp modifier 'u' to work with UTF-8 strings correctly:
		$regexp_modifier = ( $evo_charset == 'utf-8' ) ? 'u' : '';

		// Parse wiki word to find additional param for atrr "id":
		$url_params = '';
		preg_match( '/^([^#]+)(#(.+))?$/i', $this->current_WikiWord, $WikiWord_match );
		if( empty( $WikiWord_match ) )
		{
			preg_match( '/#(?<=#).*/', $this->current_WikiWord, $WikiWord_match );
			$WikiWord_match[1] = isset( $WikiWord_match[0] ) ? $WikiWord_match[0] : null;
			$anchor = $WikiWord_match[1];
		}

		if( isset( $WikiWord_match[3] ) )
		{	// wiki word has attr "id"
			$url_params .= '#'.$WikiWord_match[3];
		}

		// Use title of wiki word without attribute part:
		$WikiWord = $WikiWord_match[1];

		// Find matching Chapter or Item:
		$permalink = '';
		$link_text = preg_replace( array( '*([^\p{Lu}_])([\p{Lu}])*'.$regexp_modifier, '*([^0-9])([0-9])*'.$regexp_modifier ), '$1 $2', $WikiWord );
		$link_text = ucwords( str_replace( '-', ' ', $link_text ) );

		if( ! empty( $this->link_types['item_id'] ) && is_numeric( $this->current_wiki_word ) && ( $Item = & $ItemCache->get_by_ID( $this->current_wiki_word, false, false ) ) )
		{	// Item is found
			$permalink = $Item->get_permanent_url();
			$existing_link_text = $Item->get( 'title' );
		}
		elseif( ! empty( $this->link_types['cat_slugs'] ) && $Chapter = & $ChapterCache->get_by_urlname( $this->current_wiki_word, false, false ) )
		{	// Chapter is found
			$permalink = $Chapter->get_permanent_url();
			$existing_link_text = $Chapter->get( 'name' );
		}
		elseif( ! empty( $this->link_types['item_slugs'] ) && $Item = & $ItemCache->get_by_urltitle( $this->current_wiki_word, false, false ) )
		{	// Item is found
			$permalink = $Item->get_permanent_url();
			$existing_link_text = $Item->get( 'title' );
		}
		elseif( ! empty( $this->link_types['anchor'] ) && isset( $anchor ) && ! empty( $this->current_Item ) )
		{	// Use Item's URL with anchor:
			if( $this->current_Item->get_permalink_type() != 'none' )
			{	// Use full permanent URL like 'http://site.com/item-slug#anchor' only for normal Item that have separate single of intro page,
				// For Item without permanent URL like "Content Blcok" or "Special" we should use only relative URL like '#anchor' in order
				// to link always to currently opened URL, because they are included inside of another Item. NOTE: for proper work skin must not use html tag `<base href="/skin_folder" />`.
				$permalink = $this->current_Item->get_permanent_url();
			}
			$permalink = $url_params == '' ? $permalink.$anchor : $url_params;
			$existing_link_text = $this->current_Item->get( 'title' );
			unset($anchor);
		}

		// Clear custom link text:
		$custom_link_text = utf8_trim( $m[4] );

		// Clear custom link style classes:
		$custom_link_class = utf8_trim( str_replace( '.', ' ', $m[2] ) );

		// Clear custom link target:
		$custom_link_target = utf8_trim( $m[3] );

		if( ! empty( $permalink ) )
		{	// Chapter or Item are found in DB
			$custom_link_class = empty( $custom_link_class ) ? '' : ' class="'.$custom_link_class.'"';
			$custom_link_target = empty( $custom_link_target ) ? '' : ' target="'.$custom_link_target.'"';

			if( ! empty( $custom_link_text ) )
			{	// [[WikiWord custom link text]] or ((WikiWord custom link text)) or [[WikiWord .style.classes custom link text]] or ((WikiWord .style.classes custom link text))
				return '<a href="'.$permalink.$url_params.'"'.$custom_link_class.$custom_link_target.'>'.$custom_link_text.'</a>';
			}
			elseif( $m[1] == '[[' )
			{	// [[Wikiword]] or [[Wikiword .style.classes]]
				return '<a href="'.$permalink.$url_params.'"'.$custom_link_class.$custom_link_target.'>'.$existing_link_text.'</a>';
			}
			else
			{	// ((Wikiword)) or ((Wikiword .style.classes))
				return '<a href="'.$permalink.$url_params.'"'.$custom_link_class.$custom_link_target.'>'.$link_text.'</a>';
			}
		}
		else
		{	// Chapter and Item are not found in DB
			if( ( empty( $this->link_types['item_id'] ) && is_numeric( $this->current_wiki_word ) ) ||
			    ( empty( $this->link_types['anchor'] ) && isset( $anchor ) ) )
			{	// Return original text if no found by numeric wikiword and "Item ID in brackets" is disabled:
				return $m[0];
			}
			else
			{	// Display a link to suggest to create new post from wiki word:
				return $this->get_broken_link( $this->current_wiki_word, ( empty( $custom_link_text ) ? $link_text : $custom_link_text ), $custom_link_class );
			}
		}
	}


	/**
	 * Get HTML code for broken link
	 *
	 * @param string Post slug
	 * @param string Link/Span text
	 * @param string Link/Span class
	 * @return string
	 */
	function get_broken_link( $post_slug, $text, $class = '' )
	{
		global $blog, $admin_url, $evo_charset;

		if( isset( $this->render_type ) && $this->render_type == 'email' )
		{	// Don't render broken link for Email Campaign because it is impossible
			// to check user permission when content will be viewed on email inbox:
			return $text;
		}

		// Add regexp modifier 'u' to work with UTF-8 strings correctly:
		$regexp_modifier = ( $evo_charset == 'utf-8' ) ? 'u' : '';

		$class = empty( $class ) ? '' : $class.' ';

		if( is_numeric( $post_slug ) && ! is_numeric( $text ) )
		{	// Try to use custom text if it is provided instead of post ID to suggest a link to create new post:
			$post_slug = preg_replace( array( '*([^\p{Lu}#_])([\p{Lu}#])*'.$regexp_modifier, '*([^0-9])([0-9])*'.$regexp_modifier ), '$1-$2', utf8_strtolower( $text ) );
		}

		if( isset( $blog ) && ! is_numeric( $post_slug ) )
		{	// Suggest to create new post from given word:
			$before_wikiword = '<a'
				.' href="#"'
				.' class="'.$class.'evo_shortlink_broken"'
				// Add these data attributes in order to display this link only for user who can really create a post:
				.' data-function="create_post" data-coll="'.$blog.'">';
			$after_wikiword = '</a>';
		}
		else
		{	// Don't allow to create new post from numeric wiki word:
			$before_wikiword = '<span class="'.$class.'evo_shortlink_broken">';
			$after_wikiword = '</span>';
		}

		return $before_wikiword.$text.$after_wikiword;
	}


	/**
	 * Callback function to replace the links for creating new posts if current user has no permission
	 *
	 * @param array Matches
	 * @return string
	 */
	function callback_replace_post_links( $matches )
	{
		if( ! isset( $matches[1], $matches[2], $matches[3] ) )
		{	// Return a source string when no enough data to check user permissions:
			return $matches[0];
		}

		$BlogCache = & get_BlogCache();
		$Blog = & $BlogCache->get_by_ID( $matches[2], false, false );

		// Get an URL to create new post,
		// If this function return an empty string then current user has no permission:
		$new_post_url = $Blog ? $Blog->get_write_item_url( 0, $matches[3] ) : false;

		if( ! $new_post_url )
		{	// If user has no permission to create a post for the collection,
			// display only a link text without providing a link to create new post:
			return $matches[3];
		}

		// If user has a permission to create a post for the collection,
		// display the source link but replace the source URL with new generated,
		// because it may be different between back- and front-office and also between
		// anonymous and logged in users (disp=edit vs disp=anonpost):
		return preg_replace( '# href="[^"]+"#i', ' href="'.$new_post_url.'" title="'.format_to_output( T_('Create').'...', 'htmlattr' ).'"', $matches[0] );
	}


	/**
	 * 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.
	 *
	 * @todo dh> This seems to be a lot of Javascript. Please try exporting it in a
	 *       (dynamically created) .js src file. Then we could use cache headers
	 *       to let the browser cache it.
	 * @param array Associative array of parameters
	 * @return boolean did we display a toolbar?
	 */
	function AdminDisplayToolbar( & $params )
	{
		if( ! empty( $params['Item'] ) )
		{	// Item is set, get Blog from post:
			$edited_Item = & $params['Item'];
			$Collection = $Blog = & $edited_Item->get_Blog();
		}

		if( empty( $Blog ) )
		{	// Item is not set, try global Blog:
			global $Collection, $Blog;
			if( empty( $Blog ) )
			{	// We can't get a Blog, this way "apply_rendering" plugin collection setting is not available:
				return false;
			}
		}

		$apply_rendering = $this->get_coll_setting( 'coll_apply_rendering', $Blog );
		if( empty( $apply_rendering ) || $apply_rendering == 'never' )
		{	// Plugin is not enabled for current case, so don't display a toolbar:
			return false;
		}

		// Print toolbar on screen:
		return $this->DisplayCodeToolbar( $Blog, $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 )
	{
		if( ! empty( $params['Comment'] ) )
		{	// Comment is set, get Blog from comment:
			$Comment = & $params['Comment'];
			if( ! empty( $Comment->item_ID ) )
			{
				$comment_Item = & $Comment->get_Item();
				$Collection = $Blog = & $comment_Item->get_Blog();
			}
		}

		if( empty( $Blog ) )
		{	// Comment is not set, try global Blog:
			global $Collection, $Blog;
			if( empty( $Blog ) )
			{	// We can't get a Blog, this way "apply_comment_rendering" plugin collection setting is not available:
				return false;
			}
		}

		$apply_rendering = $this->get_coll_setting( 'coll_apply_comment_rendering', $Blog );
		if( empty( $apply_rendering ) || $apply_rendering == 'never' )
		{	// Plugin is not enabled for current case, so don't display a toolbar:
			return false;
		}

		// Print toolbar on screen
		return $this->DisplayCodeToolbar( $Blog, $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 )
	{
		$apply_rendering = $this->get_msg_setting( 'msg_apply_rendering' );
		if( ! empty( $apply_rendering ) && $apply_rendering != 'never' )
		{	// Print toolbar on screen:
			return $this->DisplayCodeToolbar( NULL, $params );
		}
		return false;
	}


	/**
	 * 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' )
		{	// Print toolbar on screen:
			return $this->DisplayCodeToolbar( NULL, $params );
		}
		return false;
	}


	/**
	 * Display Toolbar
	 *
	 * @param object Blog
	 */
	function DisplayCodeToolbar( $Blog = NULL, $params = array() )
	{
		global $Hit, $baseurl, $debug;

		if( $Hit->is_lynx() )
		{	// let's deactivate toolbar on Lynx, because they don't work there:
			return false;
		}

		$params = array_merge( array(
				'js_prefix' => '', // Use different prefix if you use several toolbars on one page
			), $params );

		// Load js to work with textarea:
		require_js_defer( 'functions.js', 'blog', true );

		// Load js and css for modal window:
		$this->require_js_defer( 'shortlinks.js', true );
		$this->require_css( 'shortlinks.css', true );

		// Initialize JavaScript to build and open window:
		echo_modalwindow_js();

		// Initialize Javascript to build shortlinks modal window;
		$this->init_js_lang_vars();

		$js_config = array(
				'js_prefix'            => $params['js_prefix'],
				'plugin_code'          => $this->code,

				'toolbar_title_before' => format_to_js( $this->get_template( 'toolbar_title_before' ) ),
				'toolbar_title_after'  => format_to_js( $this->get_template( 'toolbar_title_after' ) ),
				'toolbar_group_before' => format_to_js( $this->get_template( 'toolbar_group_before' ) ),
				'toolbar_group_after'  => format_to_js( $this->get_template( 'toolbar_group_after' ) ),
				'toolbar_title'        => T_('Short Links:'),

				'button_title'         => T_('Link to a Post'),
				'button_value'         => T_('Link to a Post'),
				'button_class'         => $this->get_template( 'toolbar_button_class' ),
			);

		if( is_ajax_request() )
		{
			?>
			<script>
				jQuery( document ).ready( function() {
						window.evo_init_shortlinks_toolbar( <?php echo evo_json_encode( $js_config ); ?> );
					} );
			</script>
			<?php
		}
		else
		{
			expose_var_to_js( 'shortlinks_toolbar_'.$params['js_prefix'], $js_config, 'evo_init_shortlinks_toolbar_config' );
		}

		echo $this->get_template( 'toolbar_before', array( '$toolbar_class$' => $params['js_prefix'].$this->code.'_toolbar' ) );
		echo $this->get_template( 'toolbar_after' );

		return true;
	}


	/**
	 * Initialize JavaScript to build and open window for shortlinks
	 */
	function init_js_lang_vars( $relative_to = 'rsc_url' )
	{
		// TODO: Include in rsc/js/src/evo_init_plugin_shortlinks.js
		global $Blog;

		// Initialize variables for the file "shortlinks.js":
		echo '<script>
		var shortlinks_coll_urlname = \''.( empty( $Blog ) ? 'undefined' : $Blog->urlname ).'\';
		var shortlinks_title_link_to_post = \''.TS_('Link to a Post').get_manual_link( 'shortlinks-plugin-link-post-dialog' ).'\';
		var shortlinks_collections = \''.TS_('Collections').'\';
		var shortlinks_insert_full_cover_image = \''.TS_('Insert full cover image').'\';
		var shortlinks_insert_title = \''.TS_('Insert title').'\';
		var shortlinks_insert_thumbnail_of_cover = \''.TS_('Insert thumbnail of cover or first image').'\';
		var shortlinks_insert_excerpt = \''.TS_('Insert excerpt').'\';
		var shortlinks_insert_teaser = \''.TS_('Insert teaser').'\';
		var shortlinks_insert_read_more_link = \''.TS_('Insert "Read more" link').'\';
		var shortlinks_slug = \''.TS_('Slug').'\';
		var shortlinks_mode = \''.TS_('Mode').'\';
		var shortlinks_use_title = \''.TS_('Use title of destination post as link text').'\';
		var shortlinks_use_slug_words = \''.TS_('Use slug words as link text').'\';
		var shortlinks_classes = \''.TS_('Classes').'\';
		var shortlinks_target = \''.TS_('Target').'\';
		var shortlinks_none = \''.TS_('None').'\';
		var shortlinks_blank = \''.TS_('Blank').'\';
		var shortlinks_parent = \''.TS_('Parent').'\';
		var shortlinks_top = \''.TS_('Top').'\';
		var shortlinks_text = \''.TS_('Text').'\';
		var shortlinks_search = \''.TS_('Search').'\';
		var shortlinks_clear = \''.TS_('Clear').'\';
		var shortlinks_back = \''.TS_('Back').'\';
		var shortlinks_insert_short_link = \''.TS_('Insert Short Link').'\';
		var shortlinks_insert_with_options = \''.TS_('Insert with options').'...'.'\';
		var shortlinks_insert_snippet_link = \''.TS_('Insert Snippet + Link').'...'.'\';
		var shortlinks_select_item = \''.TS_('Please select at least one item option to insert.').'\';
		var shortlinks_insert_link = \''.TS_('Insert Link').'\';
		var shortlinks_read_more = \''.TS_('Read more').'\';
		var shortlinks_link_to_post = \''.TS_('Link to a Post').'\';
		</script>';
	}


	/**
	 * Event handler: called at the beginning of {@link Item::dbinsert() inserting
	 * an item/post in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'Item': the related Item (by reference)
	 */
	function PrependItemInsertTransact( & $params )
	{
		$Item = & $params['Item'];

		// Get collection from given params:
		$setting_Blog = $this->get_Blog_from_params( $params );

		if( ! $this->is_renderer_enabled( $this->get_coll_setting( 'coll_apply_rendering', $setting_Blog ), $Item->get_renderers_validated() ) )
		{	// Don't try to optimize when this plugin is not applied for Items:
			return;
		}

		// Get settings to know what should be optimized:
		$this->link_types = $this->get_coll_setting( 'link_types', $setting_Blog );

		// Optimize URLs:
		$Item->set( 'content', $this->optimize_urls( $Item->get( 'content' ) ) );
	}


	/**
	 * Event handler: called at the beginning of {@link Item::dbupdate() updating
	 * an item/post in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'Item': the related Item (by reference)
	 */
	function PrependItemUpdateTransact( & $params )
	{
		$this->PrependItemInsertTransact( $params );
	}


	/**
	 * Event handler: called at the beginning of {@link Comment::dbinsert() inserting
	 * a Comment in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'Comment': the related Comment (by reference)
	 */
	function PrependCommentInsertTransact( & $params )
	{
		$Comment = & $params['Comment'];

		// Get collection from given params:
		$setting_Blog = $this->get_Blog_from_params( $params );

		if( ! $this->is_renderer_enabled( $this->get_coll_setting( 'coll_apply_comment_rendering', $setting_Blog ), $Comment->get_renderers_validated() ) )
		{	// Don't try to optimize when this plugin is not applied for Comments:
			return;
		}

		// Get settings to know what should be optimized:
		$this->link_types = $this->get_coll_setting( 'link_types', $setting_Blog );

		// Optimize URLs:
		$Comment->set( 'content', $this->optimize_urls( $Comment->get( 'content' ) ) );
	}


	/**
	 * Event handler: called at the beginning of {@link Comment::dbupdate() updating
	 * a Comment in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'Comment': the related Comment (by reference)
	 */
	function PrependCommentUpdateTransact( & $params )
	{
		$this->PrependCommentInsertTransact( $params );
	}


	/**
	 * Event handler: called at the beginning of {@link Message::dbinsert_discussion() inserting
	 * an Message in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'Message': the related Message (by reference)
	 */
	function PrependMessageInsertTransact( & $params )
	{
		$Message = & $params['Message'];

		if( ! $this->is_renderer_enabled( $this->get_msg_setting( 'msg_apply_rendering' ), $Message->get_renderers_validated() ) )
		{	// Don't try to optimize when this plugin is not applied for Items:
			return;
		}

		$this->link_types = $this->get_msg_setting( 'link_types' );

		$Message->set( 'text', $this->optimize_urls( $Message->get( 'text' ) ) );
	}


	/**
	 * Event handler: called at the beginning of {@link EmailCampaign::dbinsert() inserting
	 * an Email Campaign in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'EmailCampaign': the related EmailCampaign (by reference)
	 */
	function PrependEmailInsertTransact( & $params )
	{
		$EmailCampaign = & $params['EmailCampaign'];

		if( ! $this->is_renderer_enabled( $this->get_email_setting( 'email_apply_rendering' ), $EmailCampaign->get_renderers_validated() ) )
		{	// Don't try to optimize when this plugin is not applied for Items:
			return;
		}

		$this->link_types = $this->get_email_setting( 'link_types' );

		$EmailCampaign->set( 'email_text', $this->optimize_urls( $EmailCampaign->get( 'email_text' ) ) );
		//$EmailCampaign->set( 'email_html', $this->optimize_urls( $EmailCampaign->get( 'email_html' ) ) );
		//$EmailCampaign->set( 'email_plaintext', $this->optimize_urls( $EmailCampaign->get( 'email_plaintext' ) ) );
	}


	/**
	 * Event handler: called at the beginning of {@link EmailCampaign::dbupdate() updating
	 * an Email Campaign in the database}.
	 *
	 * @param array Associative array of parameters
	 *   - 'EmailCampaign': the related EmailCampaign (by reference)
	 */
	function PrependEmailUpdateTransact( & $params )
	{
		$this->PrependEmailInsertTransact( $params );
	}


	/**
	 * Optimize URLs in content
	 *
	 * @param string Source content
	 * @return string Optimized content
	 */
	function optimize_urls( $content )
	{
		if( ! empty( $this->link_types['abs_url_optimize'] ) )
		{	// Optimize absolute URLs:
			$content = replace_outside_code_and_short_tags( '*
					( \[\[ | \(\( ) # Lookbehind for (( or [[
					( ( (https?://|//).+/ ) ( [^/][^<>{}\s\]\)]+ ) ) # URL
					( \s.+ )?       # Additional attributes like style classes, link target, custon link text (Optional)
					( \]{2,} | \){2,} ) # Lookahead for )) or ]]
					*ix', // x = extended (spaces + comments allowed)
				array( $this, 'optimize_urls_callback' ), $content, 'replace_content', 'preg_callback' );
		}

		if( ! empty( $this->link_types['rel_url_optimize'] ) )
		{	// Optimize relative URLs:
			$content = replace_outside_code_and_short_tags( '*
					( \[\[ | \(\( ) # Lookbehind for (( or [[
					( ( /(.+/)? ) ( [^/][^<>{}\s\]\)]+ ) ) # URL
					( \s.+ )?       # Additional attributes like style classes, link target, custon link text (Optional)
					( \]{2,} | \){2,} ) # Lookahead for )) or ]]
					*ix', // x = extended (spaces + comments allowed)
				array( $this, 'optimize_urls_callback' ), $content, 'replace_content', 'preg_callback' );
		}

		return $content;
	}


	/**
	 * Callback function for URLs optimization
	 *
	 * @param array $m
	 * @return string
	 */
	function optimize_urls_callback( $m )
	{
		if( ! ( $m[1] == '[[' && substr( $m[7], 0, 2 ) == ']]' ) &&
		    ! ( $m[1] == '((' && substr( $m[7], 0, 2 ) == '))' ) )
		{	// Wrong pattern, Return original text:
			return $m[0];
		}

		if( preg_match( '#^(https?://|//)$#', $m[4] ) &&
		    ! is_internal_url( $m[3] ) )
		{	// This is an absolute URL but this is an external URLs,
			// don't optimize this URL to slug:
			return $m[0];
		}

		if( strlen( $m[7] ) > 2 )
		{	// Fix Custom link text with appending chars ] or ) if it is ended with chars ] or ):
			$m[6] .= substr( $m[7], 0, -2 );
		}

		$slug = $m[5];
		$anchor_position = strpos( $slug, '#' );
		if( $anchor_position !== false )
		{	// Remove anchor from slug:
			$slug = substr( $slug, 0, $anchor_position );
		}
		$SlugCache = & get_SlugCache();
		if( ! $SlugCache->get_by_name( $slug, false, false ) )
		{	// The Slug is not found in system, Keep it as is without optimization:
			return $m[0];
		}

		// Return short link tag only with slug(without absolute or relative path):
		return $m[1].$m[5].$m[6].$m[7];
	}
}
?>
