<?php
/**
 * This file implements general purpose functions.
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
 * Parts of this file are copyright (c)2005-2006 by PROGIDISTRI - {@link http://progidistri.com/}.
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * Dependencies
 */
load_funcs('antispam/model/_antispam.funcs.php');
load_funcs('tools/model/_email.funcs.php');
load_funcs('sessions/model/_cookie.funcs.php');

// @todo sam2kb> Move core functions get_admin_skins, get_filenames, cleardir_r, rmdir_r and some other
// to a separate file, and split files_Module from _core_Module
load_funcs('files/model/_file.funcs.php');

// Load utf8 support functions
load_funcs( '_ext/_portable_utf8.php' );

load_funcs( '_ext/random/random.php' );


/**
 * Call a method for all modules in a row
 *
 * @param string the name of the method which should be called
 * @param array params
 * @return array[module_name][return value], or NULL if the method doesn't have any return value
 */
function modules_call_method( $method_name, $params = NULL )
{
	global $modules;

	$result = NULL;

	foreach( $modules as $module )
	{
		$Module = & $GLOBALS[$module.'_Module'];
		if( $params == NULL )
		{
			$ret = $Module->{$method_name}();
		}
		else
		{
			$ret = $Module->{$method_name}( $params );
		}
		if( isset( $ret ) )
		{
			$result[$module] = $ret;
		}
	}

	return $result;
}


/**
 * Call a method for all modules in a row and update params by reference
 *
 * @param string the name of the method which should be called
 * @param array params
 * @return array[module_name][return value], or NULL if the method doesn't have any return value
 */
function modules_call_method_reference_params( $method_name, & $params )
{
	global $modules;

	$result = NULL;

	foreach( $modules as $module )
	{
		$Module = & $GLOBALS[$module.'_Module'];
		$ret = $Module->{$method_name}( $params );
		if( isset( $ret ) )
		{
			$result[$module] = $ret;
		}
	}

	return $result;
}


/**
 * Loads the b2evo database scheme.
 *
 * This gets updated through {@link db_delta()} which generates the queries needed to get
 * to this scheme.
 *
 * @param boolean set true to load installed plugins table as well, leave it on false otherwise
 *        - currently used only on table normalization
 *
 * Please see {@link db_delta()} for things to take care of.
 */
function load_db_schema( $inlcude_plugins = false )
{
	global $schema_queries;
	global $modules, $inc_path;
	global $db_storage_charset, $DB;

	if( empty( $db_storage_charset ) )
	{ // If no specific charset has been requested for datstorage, use the one of the current connection (optimize for speed - no conversions)
		$db_storage_charset = $DB->get_connection_charset();
	}

	// Load modules:
	foreach( $modules as $module )
	{
		echo get_install_format_text_and_log( 'Loading module: <code>'.$module.'/model/_'.$module.'.install.php</code><br />', 'br' );
		require_once $inc_path.$module.'/model/_'.$module.'.install.php';
	}

	if( $inlcude_plugins )
	{ // Load all plugins table into the schema queries
		global $Plugins;

		if( empty( $Plugins ) )
		{
			load_class( 'plugins/model/_plugins.class.php', 'Plugins' );
			$Plugins = new Plugins();
		}

		$admin_Plugins = & get_Plugins_admin();
		$admin_Plugins->restart();
		while( $loop_Plugin = & $admin_Plugins->get_next() )
		{ // loop through all installed plugins
			$create_table_queries = $loop_Plugin->GetDbLayout();
			foreach( $create_table_queries as $create_table_query )
			{
				if( ! preg_match( '|^\s*CREATE TABLE\s+(IF NOT EXISTS\s+)?([^\s(]+).*$|is', $create_table_query, $match) )
				{ // Could not parse the CREATE TABLE command
					continue;
				}
				$schema_queries[$match[2]] = array( 'Creating table for plugin', $create_table_query );
				$DB->dbaliases[] = '#\b'.$match[2].'\b#';
				$DB->dbreplaces[] = $match[2];
			}
		}
	}
}


/**
 * @deprecated kept only for plugin backward compatibility (core is being modified to call getters directly)
 * To be removed, maybe in b2evo v5.
 *
 * @return DataObjectCache
 */
function & get_Cache( $objectName )
{
	global $Plugins;
	global $$objectName;

	if( isset( $$objectName ) )
	{	// Cache already exists:
		return $$objectName;
	}

	$func_name = 'get_'.$objectName;

	if( function_exists($func_name) )
	{
		return $func_name();
	}
	else
	{
		debug_die( 'getCache(): Unknown Cache type get function:'.$func_name.'()' );
	}
}


/**
 * Load functions file
 */
function load_funcs( $funcs_path )
{
	global $inc_path;
	require_once $inc_path.$funcs_path;
}


/**
 * Shutdown function: save HIT and update session!
 *
 * This is registered in _main.inc.php with register_shutdown_function()
 * This is called by PHP at the end of the script.
 *
 * NOTE: before PHP 4.1 nothing can be echoed here any more, but the minimum PHP requirement for b2evo is PHP 4.3
 */
function shutdown()
{
	/**
	 * @var Hit
	 */
	global $Hit;

	/**
	 * @var Session
	 */
	global $Session;

	global $Settings, $Debuglog, $DB, $Timer;

	// Try forking a background process and let the parent return as fast as possbile.
	if( is_callable('pcntl_fork') && function_exists('posix_kill') && defined('STDIN') )
	{
		if( $pid = pcntl_fork() )
			return; // Parent

		function shutdown_kill()
		{
			posix_kill(posix_getpid(), SIGHUP);
		}

		if ( ob_get_level() )
		{	// Discard the output buffer and close
			ob_end_clean();
		}

		fclose(STDIN);  // Close all of the standard
		fclose(STDOUT); // file descriptors as we
		fclose(STDERR); // are running as a daemon.

		register_shutdown_function('shutdown_kill');

		if( posix_setsid() < 0 )
			return;

		if( $pid = pcntl_fork() )
			return;     // Parent

		// Now running as a daemon. This process will even survive
		// an apachectl stop.
	}

	$Timer->resume('shutdown');

	// echo '*** SHUTDOWN FUNC KICKING IN ***';

	// fp> do we need special processing if we are in CLI mode?  probably earlier actually
	// if( ! $is_cli )

	// Note: it might be useful at some point to do special processing if the script has been aborted or has timed out
	// connection_aborted()
	// connection_status()

	// Save the current HIT, but set delayed since the hit ID will not be required here:
	$Hit->log( true );

	// Update the SESSION:
	$Session->dbsave();

	// Get updates here instead of slowing down normal display of the dashboard
	load_funcs( 'dashboard/model/_dashboard.funcs.php' );
	b2evonet_get_updates();

	// Auto pruning of old HITS, old SESSIONS and potentially MORE analytics data:
	if( $Settings->get( 'auto_prune_stats_mode' ) == 'page' )
	{ // Autopruning is requested
		load_class( 'sessions/model/_hitlist.class.php', 'Hitlist' );
		Hitlist::dbprune(); // will prune once per day, according to Settings
	}

	// Calling debug_info() here will produce complete data but it will be after </html> hence invalid.
	// Then again, it's for debug only, so it shouldn't matter that much.
	debug_info();

	// Update the SESSION again, at the very end:
	// (e.g. "Debuglogs" may have been removed in debug_info())
	$Session->dbsave();

	while( $DB->transaction_nesting_level )
	{	// Rollback all transactions which could not finished correctly by some unknown error,
		// Used to avoid errors like "Lock wait timeout exceeded; try restarting transaction":
		$DB->rollback();
	}

	$Timer->pause('shutdown');
}


/***** Formatting functions *****/

/**
 * Format a string/content for being output
 *
 * @author fplanque
 * @todo htmlspecialchars() takes a charset argument, which we could provide ($evo_charset?)
 * @param string raw text
 * @param string format, can be one of the following
 * - raw: do nothing
 * - htmlbody: display in HTML page body: allow full HTML
 * - entityencoded: Special mode for RSS 0.92: allow full HTML but escape it
 * - htmlhead: strips out HTML (mainly for use in Title)
 * - htmlattr: use as an attribute: escapes quotes, strip tags
 * - formvalue: use as a form value: escapes quotes and < > but leaves code alone
 * - text: use as plain-text, e.g. for ascii-mails
 * - xml: use in an XML file: strip HTML tags
 * - xmlattr: use as an attribute: strips tags and escapes quotes
 * @return string formatted text
 */
function format_to_output( $content, $format = 'htmlbody' )
{
	global $Plugins, $evo_charset;

	switch( $format )
	{
		case 'raw':
			// do nothing!
			break;

		case 'htmlbody':
			// display in HTML page body: allow full HTML
			$content = convert_chars($content, 'html');
			break;

		case 'urlencoded':
			// Encode string to be passed as part of an URL
			$content = rawurlencode( $content );
			break;

		case 'entityencoded':
			// Special mode for RSS 0.92: apply renders and allow full HTML but escape it
			$content = convert_chars($content, 'html');
			$content = htmlspecialchars( $content, ENT_QUOTES, $evo_charset );
			break;

		case 'htmlfeed':
			// For use in RSS <content:encoded><![CDATA[ ... ]]></content:encoded>
			// allow full HTML + absolute URLs...
			$content = make_rel_links_abs($content);
			$content = convert_chars($content, 'html');
			$content = str_replace(']]>', ']]&gt;', $content); // encode CDATA closing tag to prevent injection/breaking of the <![CDATA[ ... ]]>
			break;

		case 'htmlhead':
			// Strips out HTML (mainly for use in Title)
			$content = strip_tags($content);
			$content = convert_chars($content, 'html');
			break;

		case 'htmlattr':
			// use as an attribute: strips tags and escapes quotes
			// TODO: dh> why not just htmlspecialchars?fp> because an attribute can never contain a tag? dh> well, "onclick='return 1<2;'" would get stripped, too. I'm just saying: why mess with it, when we can just use htmlspecialchars.. fp>ok
			$content = strip_tags($content);
			$content = convert_chars($content, 'html');
			$content = str_replace( array('"', "'"), array('&quot;', '&#039;'), $content );
			break;

		case 'htmlspecialchars':
		case 'formvalue':
			// Replace special chars to &amp;, &quot;, &#039;|&apos;, &lt; and &gt; :
			// Handles & " ' < > to &amp; &quot; &apos; &lt; &gt;
			$content = htmlspecialchars( $content, ENT_QUOTES | ENT_HTML5, $evo_charset );
			break;

		case 'xml':
			// use in an XML file: strip HTML tags
			$content = strip_tags($content);
			$content = convert_chars($content, 'xml');
			break;

		case 'xmlattr':
			// use as an attribute: strips tags and escapes quotes
			$content = strip_tags($content);
			$content = convert_chars($content, 'xml');
			$content = str_replace( array('"', "'"), array('&quot;', '&#039;'), $content );
			break;

		case 'text':
			// use as plain-text, e.g. for ascii-mails
			$content = strip_tags( $content );
			$trans_tbl = get_html_translation_table( HTML_ENTITIES );
			$trans_tbl = array_flip( $trans_tbl );
			$content = strtr( $content, $trans_tbl );
			$content = preg_replace( '/[ \t]+/', ' ', $content);
			$content = trim($content);
			break;

		case 'syslog':
			// Replace special chars to &amp;, &quot;, &#039;|&apos;, &lt; and &gt; :
			// Handles & " ' < > to &amp; &quot; &apos; &lt; &gt;
			$content = htmlspecialchars( $content, ENT_QUOTES | ENT_HTML5, $evo_charset );
			$content = preg_replace( "/\[\[(.+?)]]/is", "<code>$1</code>", $content ); // Replaces [[...]] into <code>...</code>
			break;

		default:
			debug_die( 'Output format ['.$format.'] not supported.' );
	}

	return $content;
}


/*
 * autobrize(-)
 */
function autobrize($content) {
	$content = callback_on_non_matching_blocks( $content, '~<code>.+?</code>~is', 'autobrize_callback' );
	return $content;
}

/**
 * Adds <br>'s to non code blocks
 *
 * @param string $content
 * @return string content with <br>'s added
 */
function autobrize_callback( $content )
{
	$content = preg_replace("/<br>\n/", "\n", $content);
	$content = preg_replace("/<br \/>\n/", "\n", $content);
	$content = preg_replace("/(\015\012)|(\015)|(\012)/", "<br />\n", $content);
	return($content);
}

/*
 * unautobrize(-)
 */
function unautobrize($content)
{
	$content = callback_on_non_matching_blocks( $content, '~<code>.+?</code>~is', 'unautobrize_callback' );
	return $content;
}

/**
 * Removes <br>'s from non code blocks
 *
 * @param string $content
 * @return string content with <br>'s removed
 */
function unautobrize_callback( $content )
{
	$content = preg_replace("/<br>\n/", "\n", $content);   //for PHP versions before 4.0.5
	$content = preg_replace("/<br \/>\n/", "\n", $content);
	return($content);
}

/**
 * Add leading zeroes to a number when necessary.
 *
 * @param string The original number.
 * @param integer How many digits shall the number have?
 * @return string The padded number.
 */
function zeroise( $number, $threshold )
{
	return str_pad( $number, $threshold, '0', STR_PAD_LEFT );
}


/**
 * Get a limited text-only excerpt
 *
 * @param string
 * @param int Maximum length
 * @return string
 */
function excerpt( $str, $maxlen = 254, $tail = '&hellip;' )
{
	// Add spaces
	$str = str_replace( array( '<p>', '<br', '</tr><tr', '</th><th', '</td><td' ), array( ' <p>', ' <br', '</tr> <tr', '</th> <th', '</td> <td' ), $str );

	// Remove <code>
	$str = preg_replace( '#<code>(.+)</code>#is', '', $str );

	// Strip tags:
	$str = strip_tags( $str );

	// Remove spaces:
	$str = preg_replace( '/[ \t]+/', ' ', $str);
	$str = trim( $str );

	// Ger rid of all new lines and Display the html tags as source text:
	$str = trim( preg_replace( '#[\r\n\t\s]+#', ' ', $str ) );

	$str = strmaxlen( $str, $maxlen, $tail, 'raw', true );

	return $str;
}


/**
 * Get a limited text-only excerpt based on number of words
 *
 * @param string
 * @param integer Maximum length
 * @param array Params
 * @return string
 */
function excerpt_words( $str, $maxwords = 50, $params = array() )
{
	// Add spaces
	$str = str_replace( array( '<p>', '<br' ), array( ' <p>', ' <br' ), $str );

	// Remove <code>
	$str = preg_replace( '#<code>(.+)</code>#is', '', $str );

	// Strip tags:
	$str = strip_tags( $str );

	// Remove spaces:
	$str = preg_replace( '/[ \t]+/', ' ', $str);
	$str = trim( $str );

	// Ger rid of all new lines and Display the html tags as source text:
	$str = trim( preg_replace( '#[\r\n\t\s]+#', ' ', $str ) );

	$str = strmaxwords( $str, $maxwords, $params );

	return $str;
}


/**
 * Crop string to maxlen with &hellip; (default tail) at the end if needed.
 *
 * If $format is not "raw", we make sure to not cut in the middle of an
 * HTML entity, so that strmaxlen('1&amp;2', 3, NULL, 'formvalue') will not
 * become/stay '1&amp;&hellip;'.
 *
 * @param string
 * @param int Maximum length
 * @param string Tail to use, when string gets cropped. Its length gets
 *               substracted from the total length (with HTML entities
 *               being decoded). Default is "&hellip;" (HTML entity)
 * @param string Format, see {@link format_to_output()}
 * @param boolean Crop at whitespace, if possible?
 *        (any word split at the end will get its head removed)
 * @return string
 */
function strmaxlen( $str, $maxlen = 50, $tail = NULL, $format = 'raw', $cut_at_whitespace = false  )
{
	if( is_null($tail) )
	{
		$tail = '&hellip;';
	}

	$str = utf8_rtrim($str);

	if( utf8_strlen( $str ) > $maxlen )
	{
		// Replace all HTML entities by a single char. html_entity_decode for example
		// would not handle &hellip;.
		$tail_for_length = preg_replace('~&\w+?;~', '.', $tail);
		$tail_length = utf8_strlen( html_entity_decode($tail_for_length) );
		$len = $maxlen-$tail_length;
		if( $len < 1 )
		{ // special case; $tail length is >= $maxlen
			$len = 0;
		}
		$str_cropped = utf8_substr( $str, 0, $len );
		if( $format != 'raw' )
		{ // if the format isn't raw we make sure that we do not cut in the middle of an HTML entity
			$maxlen_entity = 7; # "&amp;" is 5, min 3!
			$str_inspect = utf8_substr($str_cropped, 1-$maxlen_entity);
			$pos_amp = utf8_strpos($str_inspect, '&');
			if( $pos_amp !== false )
			{ // there's an ampersand at the end of the cropped string
				$look_until = $pos_amp;
				$str_cropped_len = utf8_strlen($str_cropped);
				if( $str_cropped_len < $maxlen_entity )
				{ // we have to look at least for the length of an entity
					$look_until += $maxlen_entity-$str_cropped_len;
				}
				if( strpos(utf8_substr($str, $len, $look_until), ';') !== false )
				{
					$str_cropped = utf8_substr( $str, 0, $len-utf8_strlen($str_inspect)+$pos_amp);
				}
			}
		}

		if( $cut_at_whitespace )
		{
			// Get the first character being cut off. Note: we can't use $str[index] in case of utf8 strings!
			$first_cut_off_char = utf8_substr( $str, utf8_strlen( $str_cropped ), 1 );
			if( ! ctype_space( $first_cut_off_char ) )
			{ // first character being cut off is not whitespace
				// Get the chars as an array from the cropped string to be able to get chars by position
				$str_cropped_chars = preg_split('//u',$str_cropped, -1, PREG_SPLIT_NO_EMPTY);
				$i = utf8_strlen($str_cropped);
				while( $i && isset( $str_cropped_chars[ --$i ] ) && ! ctype_space( $str_cropped_chars[ $i ] ) )
				{}
				if( $i )
				{
					$str_cropped = utf8_substr($str_cropped, 0, $i);
				}
			}
		}

		$str = format_to_output(utf8_rtrim($str_cropped), $format);
		$str .= $tail;

		return $str;
	}
	else
	{
		return format_to_output($str, $format);
	}
}


/**
 * Crop string to maxwords preserving tags.
 *
 * @param string
 * @param int Maximum number words
 * @param mixed array Optional parameters
 * @return string
 */
function strmaxwords( $str, $maxwords = 50, $params = array() )
{
	$params = array_merge( array(
			'cutting_mark'    => '&hellip;',
			'continued_link'  => '',
			'continued_text'  => '&hellip;',
			'continued_class' => '',
			'always_continue' => false,
		), $params );

	// STATE MACHINE:
	$in_tag = false;
	$have_seen_non_whitespace = false;
	$end = strlen( $str );  // If we use utf8_strlen here(), we also need to access UTF chars below
	for( $i = 0; $i < $end; $i++ )
	{
		switch( $char = $str[$i] )		// This is NOT UTF-8
		{
			case '<' :	// start of a tag
				$in_tag = true;
				break;

			case '>' : // end of a tag
				$in_tag = false;
				break;

			case ctype_space($char): // This is a whitespace char...
				if( ! $in_tag )
				{	// it's a word gap:
					// Eat any additional whitespace:
					while( isset($str[$i+1]) && ctype_space($str[$i+1]) )
					{
						$i++;
					}
					if( isset($str[$i+1]) && $have_seen_non_whitespace )
					{ // only decrement words, if there's been at least one non-space char before.
						--$maxwords;
					}
				}
				// ignore white space in a tag...
				break;

			default:
				$have_seen_non_whitespace = true;
				break;
		}

		if( $maxwords < 1 )
		{	// We have reached the cutting point:
			break;
		}
	}

	if( $maxwords < 1 )
	{	// Cutting is necessary:
		// restrict content to required number of words:
		$str = utf8_substr( $str, 0, $i );

		// Add a cutting mark:
		$str .= $params['cutting_mark'];

		// balance the tags out:
		$str = balance_tags( $str );
		// remove empty tags:
		$str = preg_replace( '~<([\s]+?)[^>]*?></\1>~is', '', $str );
	}

	if( $params['always_continue'] || $maxwords < 1 )
	{ // we want a continued text if avoid_end_hellip is not set:
		if( ! isset( $params['avoid_end_hellip'] ) )
		{
			$str .= ' <a href="'.$params['continued_link'].'" class="'.$params['continued_class'].'">'.$params['continued_text'].'</a>';
		}
	}

	return $str;
}


/**
 * Convert all non ASCII chars (except if UTF-8, GB2312 or CP1251) to &#nnnn; unicode references.
 * Also convert entities to &#nnnn; unicode references if output is not HTML (eg XML)
 *
 * Preserves < > and quotes.
 *
 * fplanque: simplified
 * sakichan: pregs instead of loop
 */
function convert_chars( $content, $flag = 'html' )
{
	global $b2_htmltrans, $evo_charset;

	/**
	 * Translation of invalid Unicode references range to valid range.
	 * These are Windows CP1252 specific characters.
	 * They would look weird on non-Windows browsers.
	 * If you've ever pasted text from MSWord, you'll understand.
	 *
	 * You should not have to change this.
	 */
	static $b2_htmltranswinuni = array(
		'&#128;' => '&#8364;', // the Euro sign
		'&#130;' => '&#8218;',
		'&#131;' => '&#402;',
		'&#132;' => '&#8222;',
		'&#133;' => '&#8230;',
		'&#134;' => '&#8224;',
		'&#135;' => '&#8225;',
		'&#136;' => '&#710;',
		'&#137;' => '&#8240;',
		'&#138;' => '&#352;',
		'&#139;' => '&#8249;',
		'&#140;' => '&#338;',
		'&#142;' => '&#382;',
		'&#145;' => '&#8216;',
		'&#146;' => '&#8217;',
		'&#147;' => '&#8220;',
		'&#148;' => '&#8221;',
		'&#149;' => '&#8226;',
		'&#150;' => '&#8211;',
		'&#151;' => '&#8212;',
		'&#152;' => '&#732;',
		'&#153;' => '&#8482;',
		'&#154;' => '&#353;',
		'&#155;' => '&#8250;',
		'&#156;' => '&#339;',
		'&#158;' => '&#382;',
		'&#159;' => '&#376;'
	);

	// Convert highbyte non ASCII/UTF-8 chars to urefs:
	if( ! in_array(strtolower($evo_charset), array( 'utf8', 'utf-8', 'gb2312', 'windows-1251') ) )
	{ // This is a single byte charset
		// fp> why do we actually bother doing this:?
		$content = preg_replace_callback(
			'/[\x80-\xff]/',
			'_convert_chars_callback',
			$content);
	}

	// Convert Windows CP1252 => Unicode (valid HTML)
	// TODO: should this go to input conversions instead (?)
	$content = strtr( $content, $b2_htmltranswinuni );

	if( $flag == 'html' )
	{ // we can use entities
		// Convert & chars that are not used in an entity
		$content = preg_replace('/&(?![#A-Za-z0-9]{2,20};)/', '&amp;', $content);
	}
	else
	{ // unicode, xml...
		// Convert & chars that are not used in an entity
		$content = preg_replace('/&(?![#A-Za-z0-9]{2,20};)/', '&#38;', $content);

		// Convert HTML entities to urefs:
		$content = strtr($content, $b2_htmltrans);
	}

	return( $content );
}


/**
 * Callback for preg_replace_callback in convert_chars()
 */
function _convert_chars_callback( $matches )
{
	return "&#".ord( $matches[0] ).";";
}


/**
 * Get number of bytes in $string. This works around mbstring.func_overload, if
 * activated for strlen/mb_strlen.
 * @param string
 * @return int
 */
function evo_bytes( $string )
{
	$fo = ini_get('mbstring.func_overload');
	if( $fo && $fo & 2 && function_exists('mb_strlen') )
	{ // overloading of strlen is enabled
		return mb_strlen( $string, 'ASCII' );
	}
	return strlen($string);
}


/**
 * mbstring wrapper for strtolower function
 *
 * @deprecated by {@link utf8_strtolower()}
 *
 * fp> TODO: instead of those "when used" ifs, it would make more sense to redefine
 * mb_strtolower beforehand if it doesn"t exist (it would then just be a fallback
 * to the strtolower + a Debuglog->add() )
 *
 * @param string
 * @return string
 */
function evo_strtolower( $string )
{
	global $current_charset;

	if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strtolower') )
	{
		return mb_strtolower( $string, $current_charset );
	}

	return strtolower($string);
}


/**
 * mbstring wrapper for strlen function
 *
 * @deprecated by {@link utf8_strlen()}
 *
 * @param string
 * @return string
 */
function evo_strlen( $string )
{
	global $current_charset;

	if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strlen') )
	{
		return mb_strlen( $string, $current_charset );
	}

	return strlen($string);
}

/**
 * mbstring wrapper for strpos function
 *
 * @deprecated by {@link utf8_strpos()}
 *
 * @param string
 * @param string
 * @return int
 */
function evo_strpos( $string , $needle , $offset = null )
{
	global $current_charset;

	if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strpos') )
	{
		return mb_strpos( $string, $needle, $offset ,$current_charset );
	}

	return strpos( $string , $needle , $offset );
}


/**
 * mbstring wrapper for substr function
 *
 * @deprecated by {@link utf8_substr()}
 *
 * @param string
 * @param int start position
 * @param int string length
 * @return string
 */
function evo_substr( $string, $start = 0, $length = '#' )
{
	global $current_charset;

	if( ! $length )
	{ // make mb_substr and substr behave consistently (mb_substr returns string for length=0)
		return '';
	}
	if( $length == '#' )
	{
		$length = utf8_strlen($string);
	}

	if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_substr') )
	{
		return mb_substr( $string, $start, $length, $current_charset );
	}

	return substr( $string, $start, $length );
}


/**
 * Split $text into blocks by using $pattern and call $callback on the non-matching blocks.
 *
 * The non-matching block's text is the first param to $callback and additionally $params gets passed.
 *
 * This gets used to make links clickable or replace smilies.
 *
 * E.g., to replace only in non-HTML tags, call it like:
 * <code>callback_on_non_matching_blocks( $text, '~<[^>]*>~s', 'your_callback' );</code>
 *
 * {@internal This function gets tested in misc.funcs.simpletest.php.}}
 *
 * @param string Text to handle
 * @param string Regular expression pattern that defines blocks to exclude.
 * @param callback Function name or object/method array to use as callback.
 *               Each non-matching block gets passed as first param, additional params may be
 *               passed with $params.
 * @param array Of additional ("static") params to $callback.
 * @return string
 */
function callback_on_non_matching_blocks( $text, $pattern, $callback, $params = array() )
{
	global $evo_non_matching_blocks;

	if( preg_match_all( $pattern, $text, $matches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER ) )
	{	// $pattern matches, call the callback method on full text except of matching blocks

		// Create an unique string in order to replace all matching blocks temporarily
		$unique_replacement = md5( time() + rand() );

		if( ! isset( $evo_non_matching_blocks ) )
		{	// Init cache array once:
			$evo_non_matching_blocks = array();
		}
		// Use level in order to don't mix from other recursive calls:
		$level = count( $evo_non_matching_blocks );
		$evo_non_matching_blocks[ $level ] = array();

		foreach( $matches[0] as $l => $l_matching )
		{	// Build arrays with a source code of the matching blocks and with temporary replacement
			if( substr( $l_matching[0], 1, 1 ) == '[' )
			{	// Exception for match out short tags,
				// We should not replace a char(usually a space) before short tag like [image:123]:
				$l_matching[0] = substr( $l_matching[0], 1 );
			}
			$evo_non_matching_blocks[ $level ][ '?'.$l.$unique_replacement.$l.'?' ] = $l_matching[0];
		}

		// Replace all matching blocks with temporary text like '?X219a33da9c1b8f4e335bffc015df8c96X?'
		// where X is index of match block in array $matches[0]
		// It is used to avoid any changes in the matching blocks
		$text = str_replace( $evo_non_matching_blocks[ $level ], array_keys( $evo_non_matching_blocks[ $level ] ), $text );

		// Callback:
		$callback_params = $params;
		array_unshift( $callback_params, $text );
		$text = call_user_func_array( $callback, $callback_params );

		// Revert a source code of the matching blocks in content
		$text = str_replace( array_keys( $evo_non_matching_blocks[ $level ] ), $evo_non_matching_blocks[ $level ], $text );

		return $text;
	}

	$callback_params = $params;
	array_unshift( $callback_params, $text );
	return call_user_func_array( $callback, $callback_params );
}


/**
 * Replace non matching blocks from temp strings like '?X219a33da9c1b8f4e335bffc015df8c96X?' back to original string like '<code>$some_var = "some value";</code>'
 *
 * @param array Matches
 * @return array Matches
 */
function fix_non_matching_blocks( $matches )
{
	global $evo_non_matching_blocks;

	if( ! is_array( $matches ) )
	{	// This function works only with array of matches
		return $matches;
	}

	if( ! isset( $evo_non_matching_blocks ) ||
	    ! is_array( $evo_non_matching_blocks ) )
	{	// Nothing to fix, Return source param value:
		return $matches;
	}

	$level = count( $evo_non_matching_blocks ) - 1;

	if( empty( $evo_non_matching_blocks[ $level ] ) )
	{	// Nothing to fix, Return source param value:
		return $matches;
	}

	foreach( $matches as $m => $match )
	{
		if( is_array( $match ) )
		{	// Fix recursively:
			foreach( $match as $s => $sub_match )
			{
				$matches[ $m ][ $s ] = str_replace( array_keys( $evo_non_matching_blocks[ $level ] ), $evo_non_matching_blocks[ $level ], $sub_match );
			}
		}
		else
		{	// Revert back to original values from temp strings:
			$matches[ $m ] = str_replace( array_keys( $evo_non_matching_blocks[ $level ] ), $evo_non_matching_blocks[ $level ], $match );
		}
	}

	return $matches;
}


/**
 * Perform a global regular expression match outside of blocks <code></code>, <pre></pre>, markdown codeblocks ``
 *
 * @param string Pattern to search for
 * @param string Content
 * @param array Array of all matches in multi-dimensional array
 * @return integer|boolean Number of full pattern matches (which might be zero), or FALSE if an error occurred.
 */
function preg_match_outcode( $search, $content, & $matches )
{
	if( stristr( $content, '<code' ) !== false || stristr( $content, '<pre' ) !== false || strstr( $content, '`' ) !== false )
	{	// Call preg_match_all() on everything outside code/pre and markdown codeblocks:
		$result = callback_on_non_matching_blocks( $content,
			'~(`|<(code|pre)[^>]*>).*?(\1|</\2>)~is',
			'preg_match_outcode_callback', array( $search, & $matches ) );
		// Revert codeblocks back if they are located inside search regexp:
		$matches = fix_non_matching_blocks( $matches );
		return $result;
	}
	else
	{	// No code/pre blocks, search in the whole thing:
		return preg_match_all( $search, $content, $matches );
	}
}


/**
 * Used for function callback_on_non_matching_blocks(), because there is different order of params
 *
 * @param string Pattern to search for
 * @param string Content
 * @param array Array of all matches in multi-dimensional array
 * @return integer|boolean Number of full pattern matches (which might be zero), or FALSE if an error occurred.
 */
function preg_match_outcode_callback( $content, $search, & $matches )
{
	return preg_match_all( $search, $content, $matches );
}


/**
 * Replace content outside of blocks <code></code>, <pre></pre>, markdown codeblocks ``
 *
 * @deprecated Use new function replace_outside_code_tags()
 *
 * @param array|string Search list
 * @param array|string Replace list or Callback function
 * @param string Source content
 * @param string Callback function name
 * @param string Type of callback function: 'preg' -> preg_replace(), 'preg_callback' -> preg_replace_callback(), 'str' -> str_replace() (@see replace_content())
 * @return string Replaced content
 */
function replace_content_outcode( $search, $replace, $content, $replace_function_callback = 'replace_content', $replace_function_type = 'preg' )
{
	return replace_outside_code_tags( $search, $replace, $content, $replace_function_callback, $replace_function_type );
}


/**
 * Replace content outside of blocks <code></code>, <pre></pre>, markdown codeblocks ``
 *
 * @param array|string Search list
 * @param array|string Replace list or Callback function
 * @param string Source content
 * @param string Callback function name
 * @param string Type of callback function: 'preg' -> preg_replace(), 'preg_callback' -> preg_replace_callback(), 'str' -> str_replace() (@see replace_content())
 * @return string Replaced content
 */
function replace_outside_code_tags( $search, $replace, $content, $replace_function_callback = 'replace_content', $replace_function_type = 'preg' )
{
	if( ! empty( $search ) )
	{
		if( stristr( $content, '<code' ) !== false ||
		    stristr( $content, '<pre' ) !== false ||
		    strstr( $content, '`' ) !== false )
		{	// Call replace_content() on everything outside code/pre, and markdown codeblocks:
			$content = callback_on_non_matching_blocks( $content,
				'~(`.*?`|'
				.'<code[^>]*>.*?</code>|'
				.'<pre[^>]*>.*?</pre>)~is',
				$replace_function_callback, array( $search, $replace, $replace_function_type ) );
		}
		else
		{	// No code/pre blocks, replace on the whole thing
			$content = call_user_func( $replace_function_callback, $content, $search, $replace, $replace_function_type );
		}
	}

	return $content;
}



/**
 * Replace content outside of blocks <code></code>, <pre></pre>, markdown codeblocks `` AND also outside of short tags like [image:123]
 *
 * @deprecated Use new function replace_outside_code_and_short_tags()
 *
 * @param array|string Search list
 * @param array|string Replace list or Callback function
 * @param string Source content
 * @param string Callback function name
 * @param string Type of callback function: 'preg' -> preg_replace(), 'preg_callback' -> preg_replace_callback(), 'str' -> str_replace() (@see replace_content())
 * @return string Replaced content
 */
function replace_content_outcode_shorttags( $search, $replace, $content, $replace_function_callback = 'replace_content', $replace_function_type = 'preg' )
{
	return replace_outside_code_and_short_tags( $search, $replace, $content, $replace_function_callback, $replace_function_type );
}


/**
 * Replace content outside of blocks <code></code>, <pre></pre>, markdown codeblocks `` AND also outside of short tags like [image:123]
 *
 * @param array|string Search list
 * @param array|string Replace list or Callback function
 * @param string Source content
 * @param string Callback function name
 * @param string Type of callback function: 'preg' -> preg_replace(), 'preg_callback' -> preg_replace_callback(), 'str' -> str_replace() (@see replace_content())
 * @return string Replaced content
 */
function replace_outside_code_and_short_tags( $search, $replace, $content, $replace_function_callback = 'replace_content', $replace_function_type = 'preg' )
{
	if( ! empty( $search ) )
	{
		if( stristr( $content, '<code' ) !== false ||
		    stristr( $content, '<pre' ) !== false ||
		    strstr( $content, '`' ) !== false ||
		    preg_match( '/\[[a-z]+:[^\]`]+\]/i', $content ) )
		{	// Call replace_content() on everything outside code/pre, markdown codeblocks and short tags:
			$content = callback_on_non_matching_blocks( $content,
				'~(`.*?`|'
				.'<code[^>]*>.*?</code>|'
				.'<pre[^>]*>.*?</pre>|'
				.'[^\[]\[[a-z]+:[^\]`]+\])~is',
				$replace_function_callback, array( $search, $replace, $replace_function_type ) );
		}
		else
		{	// No code/pre blocks, replace on the whole thing
			$content = call_user_func( $replace_function_callback, $content, $search, $replace, $replace_function_type );
		}
	}

	return $content;
}


/**
 * Replace content, Used for function callback_on_non_matching_blocks(), because there is different order of params
 *
 * @param string Source content
 * @param array|string Search list
 * @param array|string Replace list
 * @param string Type of function: 'preg' -> preg_replace(), 'preg_callback' -> preg_replace_callback(), 'str' -> str_replace()
 * @param string The maximum possible replacements for each pattern in each subject string. Defaults to -1 (no limit).
 * @return string Replaced content
 */
function replace_content( $content, $search, $replace, $type = 'preg', $limit = -1 )
{
	if( $limit == 0 )
	{	// Strange request to nothing replace, Return original content:
		return $content;
	}

	switch( $type )
	{
		case 'str':
			if( $limit == -1 )
			{	// Unlimited replace:
				return str_replace( $search, $replace, $content );
			}
			else
			{	// Limited replace:
				$pos = strpos( $content, $search );
				if( $pos !== false )
				{	// Do the limited replacements:
					for( $p = 0; $p < $limit; $p++ )
					{
						if( $pos === false )
						{	// Stop searching:
							break;
						}
						$content = substr_replace( $content, $replace, $pos, strlen( $search ) );
						// Go to next searched substring:
						$pos = strpos( $content, $search, $pos + strlen( $replace ) );
					}
				}
				return $content;
			}

		case 'preg_callback':
			return preg_replace_callback( $search, $replace, $content, $limit );

		default: // 'preg'
			return preg_replace( $search, $replace, $content, $limit );
	}
}


/**
 * Replace content by callback, Used for function callback_on_non_matching_blocks(), because there is different order of params
 *
 * @param string Source content
 * @param array|string Search list
 * @param array|string Replace callback
 * @return string Replaced content
 */
function replace_content_callback( $content, $search, $replace_callback )
{
	global $evo_replace_outside_code_tags_callback;

	// Store here the requested callback function in order to use pre-processor function fix_replace_content_callback() before we call the original requested callback function:
	$evo_replace_outside_code_tags_callback = $replace_callback;

	return preg_replace_callback( $search, 'fix_replace_content_callback', $content );
}


/**
 * Replace non matching blocks from temp strings like '?X219a33da9c1b8f4e335bffc015df8c96X?' back to original string like '<code>$some_var = "some value";</code>'
 *
 * @param Array Matches
 * @return mixed
 */
function fix_replace_content_callback( $m )
{
	global $evo_replace_outside_code_tags_callback;

	// Replace non matching blocks from temp strings like '?X219a33da9c1b8f4e335bffc015df8c96X?' back to original string like '<code>$some_var = "some value";</code>'
	$m = fix_non_matching_blocks( $m );

	// Call real function with fixed blocks in matches:
	$result = call_user_func_array( $evo_replace_outside_code_tags_callback, array( $m ) );

	// Clear temp var:
	unset( $evo_replace_outside_code_tags_callback );

	return $result;
}


/**
 * Split a content by separators outside <code> and <pre> blocks
 *
 * @param string|array Separators
 * @param string Content
 * @param boolean TRUE - parenthesized expression of separator will be captured and returned as well
 * @return array The result of explode() function
 */
function split_outcode( $separators, $content, $capture_separator = false )
{
	// Check if the separators exists in content
	if( ! is_array( $separators ) )
	{ // Convert string to array with one element
		$separators = array( $separators );
	}
	$separators_exists = false;
	if( is_array( $separators ) )
	{ // Find in array
		foreach( $separators as $separator )
		{
			if( strpos( $content, $separator ) !== false )
			{ // Separator is found
				$separators_exists = true;
				break;
			}
		}
	}

	if( $separators_exists )
	{ // There are separators in content, Split the content:

		// Initialize temp values for replace the separators
		if( $capture_separator )
		{
			$rplc_separators = array();
			foreach( $separators as $s => $separator )
			{
				$rplc_separators[] = '#separator'.$s.'='.md5( rand() ).'#';
			}
		}
		else
		{
			$rplc_separators = '#separator='.md5( rand() ).'#';
		}
		// Replace the content separators with temp value
		if( strpos( $content, '<code' ) !== false || strpos( $content, '<pre' ) !== false )
		{ // Call replace_separators_callback() on everything outside code/pre:
			$content = callback_on_non_matching_blocks( $content,
				'~<(code|pre)[^>]*>.*?</\1>~is',
				'replace_content', array( $separators, $rplc_separators, 'str' ) );
		}
		else
		{ // No code/pre blocks, replace on the whole thing
			$content = str_replace( $separators, $rplc_separators, $content );
		}

		if( $capture_separator )
		{ // Save the separators
			$split_regexp = '~('.implode( '|', $rplc_separators ).')~s';
			$content_parts = preg_split( $split_regexp, $content, -1, PREG_SPLIT_DELIM_CAPTURE );
			foreach( $content_parts as $c => $content_part )
			{
				if( ( $s = array_search( $content_part, $rplc_separators ) ) !== false )
				{ // Replace original separator back
					$content_parts[ $c ] = $separators[ $s ];
				}
			}
			return $content_parts;
		}
		else
		{ // Return only splitted content(without separators)
			return explode( $rplc_separators, $content );
		}
	}
	else
	{ // No separators in content, Return whole content as one element of array
		return array( $content );
	}
}


/**
 * Remove short tags like [image:], [video:] and etc. that are inside <p> blocks and before <br> and move them before the paragraph
 *
 * @param string Source content
 * @param string Search pattern
 * @param function Optional callback function that accepts search pattern and current paragraph as arguments and returns the new_paragraph
 * @param array Params
 * @return string Content
 */
function move_short_tags( $content, $pattern = NULL, $callback = NULL, $params = array() )
{
	$params = array_merge( array(
			'check_code_block' => true,
		), $params );

	if( isset( $params['check_code_block'] ) && $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
	{	// Call $this->render_collection_data() on everything outside code/pre:
		$params['check_code_block'] = false;
		$content = callback_on_non_matching_blocks( $content,
			'~<(code|pre)[^>]*>.*?</\1>~is',
			'move_short_tags', array( $pattern, $callback, $params ) );
		return $content;
	}

	// Get individual paragraphs:
	preg_match_all( '#(<p[\s*|>])?.*?<(/p|br\s?/?)>#i', $content, $paragraphs );

	if( is_null( $pattern ) )
	{	// Default pattern:
		$pattern = '#\[(image|video|audio|include|cblock|/?div|(parent:|item:[^:\]]+:)?(subscribe|emailcapture|compare|fields)):[^\]]+\]#i';
	}

	foreach( $paragraphs[0] as $i => $current_paragraph )
	{
		if( $callback )
		{
			$new_paragraph = call_user_func( $callback, $pattern, $current_paragraph );
		}
		else
		{
			// get short tags in each paragraph
			preg_match_all( $pattern, $current_paragraph, $matches );
			$new_paragraph = $current_paragraph;

			if( $matches[0] )
			{
				$new_paragraph = str_replace( $matches[0], '', $current_paragraph );

				// convert &nbsp; to space
				$x = str_replace( "\xC2\xA0", ' ', $new_paragraph );

				if( preg_match( '#^(<p[\s*|>])?\s*<(/p|br\s?/?)>$#i', $x ) === 1 )
				{	// Remove paragraph the if moving out the short tag will result to an empty paragraph:
					$new_paragraph = '';
				}

				$new_paragraph = implode( '', $matches[0] ).$new_paragraph;
			}
		}
		$content = str_replace( $current_paragraph, $new_paragraph, $content );
	}

	return $content;
}


/**
 * Make links clickable in a given text.
 *
 * It replaces only text which is not between <a> tags already.
 *
 * @todo dh> this should not replace links in tags! currently fails for something
 *           like '<img src=" http://example.com/" />' (not usual though!)
 * fp> I am trying to address this by not replacing anything inside tags
 * fp> This should be replaced by a clean state machine (one single variable for current state)
 *
 * {@internal This function gets tested in misc.funcs.simpletest.php.}}
 *
 * @param string Text
 * @param string Url delimeter
 * @param string Callback function name
 * @param string Additional attributes for tag <a>
 * @param boolean TRUE to exclude links from header tags like h1, h2, etc.
 * @return string
 */
function make_clickable( $text, $moredelim = '&amp;', $callback = 'make_clickable_callback', $additional_attrs = '', $exclude_headers = false )
{
	$r = '';
	$inside_bracket_short_tag = false;
	$inside_tag = false;
	$in_a_tag = false;
	$in_code_tag = false;
	$in_tag_quote = false;
	$in_header_tag = false;
	$from_pos = 0;
	$i = 0;
	$n = strlen($text);

	// Not using callback_on_non_matching_blocks(), because it requires
	// wellformed HTML and the implementation below should be
	// faster and less memory intensive (tested for some example content)
	while( $i < $n )
	{	// Go through each char in string... (we will fast forward from tag to tag)
		if( $inside_bracket_short_tag )
		{	// State: We're currently inside bracket short tag like [image:], [emailcapture:], [fields:], [compare:] and etc.:
			if( $text[ $i ] == ']' )
			{	// End of bracket short tag:
				$inside_bracket_short_tag = false;
				$r .= substr( $text, $from_pos, $i - $from_pos + 1 );
				$from_pos = $i + 1;
			}
		}
		elseif( $inside_tag )
		{	// State: We're currently inside some tag:
			switch( $text[$i] )
			{
				case '>':
					if( $in_tag_quote )
					{ // This is in a quoted string so it doesn't really matter...
						break;
					}
					// end of tag:
					$inside_tag = false;
					$r .= substr($text, $from_pos, $i-$from_pos+1);
					$from_pos = $i+1;
					// $r .= '}';
					break;

				case '"':
				case '\'':
					// This is the beginning or the end of a quoted string:
					if( ! $in_tag_quote )
					{
						$in_tag_quote = $text[$i];
					}
					elseif( $in_tag_quote == $text[$i] )
					{
						$in_tag_quote = false;
					}
					break;
			}
		}
		elseif( $in_a_tag )
		{	// In a link but no longer inside <a>...</a> tag or any other embedded tag like <strong> or whatever
			switch( $text[$i] )
			{
				case '<':
					if( strtolower(substr($text, $i+1, 3)) == '/a>' )
					{	// Ok, this is the end tag of the link:
						// $r .= substr($text, $from_pos, $i-$from_pos+4);
						// $from_pos = $i+4;
						$i += 4;
						// pre_dump( 'END A TAG: '.substr($text, $from_pos, $i-$from_pos) );
						$r .= substr($text, $from_pos, $i-$from_pos);
						$from_pos = $i;
						$in_a_tag = false;
						$in_tag_quote = false;
						$in_header_tag = false;
					}
					break;
			}
		}
		elseif( $in_code_tag )
		{	// In a code but no longer inside <code>...</code> tag or any other embedded tag like <strong> or whatever
			switch( $text[$i] )
			{
				case '<':
					if( strtolower(substr($text, $i+1, 5)) == '/code' )
					{	// Ok, this is the end tag of the code:
						// $r .= substr($text, $from_pos, $i-$from_pos+4);
						// $from_pos = $i+4;
						$i += 7;
						// pre_dump( 'END A TAG: '.substr($text, $from_pos, $i-$from_pos) );
						$r .= substr($text, $from_pos, $i-$from_pos);
						$from_pos = $i;
						$in_code_tag = false;
						$in_tag_quote = false;
						$in_header_tag = false;
					}
					break;
			}
		}
		elseif( $in_header_tag )
		{	// In a code but no longer inside <h#>...</h#> tags or any other embedded tag like <strong> or whatever
			switch( $text[$i] )
			{
				case '<':
					if( strtolower( substr( $text, $i+1, 3 ) ) == '/'.$in_header_tag )
					{	// Ok, this is the end tag of the header:
						$i += 5;
						$r .= substr( $text, $from_pos, $i - $from_pos );
						$from_pos = $i;
						$in_code_tag = false;
						$in_tag_quote = false;
						$in_header_tag = false;
					}
					break;
			}
		}
		else
		{ // State: we're not currently in any tag:
			// Find next tag opening:
			if( ( $b = strpos( $text, '[', $i ) ) !== false &&
			    $b < strpos( $text, '<', $i ) )
			{	// Check if a bracket '[]' short tag is really opening but not after html tag:
				// (we are finding here short tags like [image:], [emailcapture:], [fields:], [compare:] and etc., see full list in the Item->render_inline_tags())
				$start_b = $b;
				$b++;
				$short_tag_name = '';
				while( isset( $text[ $b ] ) && $text[ $b ] != ':' && $text[ $b ] != ']' )
				{	// Get short tag name between chars '[' and ( ':' or ']' ):
					$short_tag_name .= $text[ $b ];
					$b++;
				}
				// We are inside bracket short tag if its name contains only letters:
				$inside_bracket_short_tag = preg_match( '/^[a-z]+$/', $short_tag_name );
				if( $inside_bracket_short_tag )
				{	// Set index to call user func below between two inline short tags:
					$i = $start_b;
				}
			}

			if( ! $inside_bracket_short_tag )
			{	// Try to find opening HTML tag only if bracket short tag is not opened currently:
				$i = strpos($text, '<', $i);
				if( $i === false )
				{ // No more opening tags:
					break;
				}

				$inside_tag = true;
				$in_tag_quote = false;
				// s$r .= '{'.$text[$i+1];

				if( ($text[$i+1] == 'a' || $text[$i+1] == 'A') && ctype_space($text[$i+2]) )
				{ // opening "A" tag
					$in_a_tag = true;
				}

				if( ( substr( $text, $i+1, 4 ) == 'code') )
				{ // opening "code" tag
					$in_code_tag = true;
				}

				if( $exclude_headers && preg_match( '/^h[1-6]$/', substr( $text, $i+1, 2 ), $h_match ) )
				{	// opening "h1" - "h6" tags:
					$in_header_tag = $h_match[0];
				}
			}

			// Make the text before the opening < clickable:
			$r .= call_user_func_array( $callback, array( substr( $text, $from_pos, $i-$from_pos ), $moredelim, $additional_attrs ) );
			$from_pos = $i;

			// $i += 2;
		}

		$i++;
	}

	// the remaining part:
	if( $in_a_tag )
	{ // may happen for invalid html:
		$r .= substr($text, $from_pos);
	}
	else
	{	// Make remplacements in the remaining part:
		$r .= call_user_func_array( $callback, array( substr( $text, $from_pos ), $moredelim, $additional_attrs ) );
	}

	return $r;
}


/**
 * Callback function for {@link make_clickable()}.
 *
 * original function: phpBB, extended here for AIM & ICQ
 * fplanque restricted :// to http:// and mailto://
 * Fixed to not include trailing dot and comma.
 *
 * fp> I'm thinking of moving this into the autolinks plugin (only place where it's used)
 *     and break it up into something more systematic.
 *
 * @param string Text
 * @param string Url delimeter
 * @param string Additional attributes for tag <a>
 * @return string The clickable text.
 */
function make_clickable_callback( $text, $moredelim = '&amp;', $additional_attrs = '' )
{
	if( !empty( $additional_attrs ) )
	{
		$additional_attrs = ' '.trim( $additional_attrs );
	}

	// Add style class to break long urls:
	$additional_attrs = stripos( $additional_attrs, ' class="' ) === false
		? $additional_attrs.' class="linebreak"'
		: preg_replace( '/ class="([^"]*)"/i', ' class="$1 linebreak"', $additional_attrs );

	$pattern_domain = '([\p{L}0-9\-]+\.[\p{L}0-9\-.\~]+)'; // a domain name (not very strict)
	$text = preg_replace(
		/* Tblue> I removed the double quotes from the first RegExp because
				  it made URLs in tag attributes clickable.
				  See http://forums.b2evolution.net/viewtopic.php?p=92073 */
		array( '#(^|[\s>\(]|\[url=)(https?|mailto)://([^"<>{}\s]+[^".,:;!\?<>{}\s\]\)])#i',
			'#(^|[\s>\(]|\[url=)aim:([^",<\s\]\)]+)#i',
			'#(^|[\s>\(]|\[url=)icq:(\d+)#i',
			'#(^|[\s>\(]|\[url=)www\.'.$pattern_domain.'([^"<>{}\s]*[^".,:;!\?\s\]\)])#i',
			'#(^|[\s>\(]|\[url=)([a-z0-9\-_.]+?)@'.$pattern_domain.'([^".,:;!\?&<\s\]\)]+)#i', ),
		array( '$1<a href="$2://$3"'.$additional_attrs.'>$2://$3</a>',
			'$1<a href="aim:goim?screenname=$2$3'.$moredelim.'message='.rawurlencode(T_('Hello')).'"'.$additional_attrs.'>$2$3</a>',
			'$1<a href="http://wwp.icq.com/scripts/search.dll?to=$2"'.$additional_attrs.'>$2</a>',
			'$1<a href="http://www.$2$3$4"'.$additional_attrs.'>www.$2$3$4</a>',
			'$1<a href="mailto:$2@$3$4"'.$additional_attrs.'>$2@$3$4</a>', ),
		$text );

	return $text;
}


/***** // Formatting functions *****/

/**
 * Convert timestamp to MySQL/ISO format.
 *
 * @param integer UNIX timestamp
 * @return string Date formatted as "Y-m-d H:i:s"
 */
function date2mysql( $ts )
{
	if( $ts > 0 )
	{	// Allow only positive timestamp value:
		return date( 'Y-m-d H:i:s', $ts );
	}
	else
	{	// If timestamp is wrong(NULL or FALSE or negative value) use this mimimum date instead of default 1970-01-01 00:00:00,
		// because with negative timezone like -2:00 it may returns 1969-12-31 22:00:00 which creates MySQL error "Incorrect datetime value".
		return '2000-01-01 00:00:00';
	}
}

/**
 * Convert a MYSQL date to a UNIX timestamp.
 *
 * @param string Date formatted as "Y-m-d H:i:s"
 * @param boolean true to use GM time
 * @return integer UNIX timestamp
 */
function mysql2timestamp( $m, $useGM = false )
{
	$func = $useGM ? 'gmmktime' : 'mktime';
	return $func(
		intval( substr( $m, 11, 2 ) ),  // hour
		intval( substr( $m, 14, 2 ) ),  // minute
		intval( substr( $m, 17, 2 ) ),  // second
		intval( substr( $m, 5, 2 ) ),   // month
		intval( substr( $m, 8, 2 ) ),   // day
		intval( substr( $m, 0, 4 ) ) ); // year
}

/**
 * Convert a MYSQL date -- WITHOUT the time -- to a UNIX timestamp
 *
 * @param string Date formatted as "Y-m-d"
 * @param boolean true to use GM time
 * @return integer UNIX timestamp
 */
function mysql2datestamp( $m, $useGM = false )
{
	$func = $useGM ? 'gmmktime' : 'mktime';
	return $func( 0, 0, 0, substr($m,5,2), substr($m,8,2), substr($m,0,4) );
}

/**
 * Format a MYSQL date to current locale date format.
 *
 * @param string MYSQL date YYYY-MM-DD HH:MM:SS
 */
function mysql2localedate( $mysqlstring )
{
	return mysql2date( locale_datefmt(), $mysqlstring );
}

function mysql2localetime( $mysqlstring )
{
	return mysql2date( locale_timefmt(), $mysqlstring );
}

function mysql2localedatetime( $mysqlstring )
{
	return mysql2date( locale_datefmt().' '.locale_timefmt(), $mysqlstring );
}

function mysql2localedatetime_spans( $mysqlstring, $datefmt = NULL, $timefmt = NULL )
{
	if( is_null( $datefmt ) )
	{
		$datefmt = locale_datefmt();
	}
	if( is_null( $timefmt ) )
	{
		$timefmt = locale_timefmt();
	}

	return '<span class="date">'
					.mysql2date( $datefmt, $mysqlstring )
					.'</span> <span class="time">'
					.mysql2date( $timefmt, $mysqlstring )
					.'</span>';
}


/**
 * Format a MYSQL date.
 *
 * @param string enhanced format string
 * @param string MYSQL date YYYY-MM-DD HH:MM:SS
 * @param boolean true to use GM time
 */
function mysql2date( $dateformatstring, $mysqlstring, $useGM = false )
{
	$m = $mysqlstring;
	if( empty($m) || ($m == '0000-00-00 00:00:00' ) )
		return false;

	// Get a timestamp:
	$unixtimestamp = mysql2timestamp( $m );

	return date_i18n( $dateformatstring, $unixtimestamp, $useGM );
}


/**
 * Date internationalization: same as date() formatting but with i18n support.
 *
 * @todo dh> support for MySQL date format instead of $unixtimestamp? This would simplify callees, where currently mktime() is used.
 * @param string enhanced format string
 * @param integer UNIX timestamp
 * @param boolean true to use GM time
 */
function date_i18n( $dateformatstring, $unixtimestamp, $useGM = false )
{
	global $month, $month_abbrev, $weekday, $weekday_abbrev, $weekday_letter;
	global $localtimenow, $time_difference;

	if( $dateformatstring == 'isoZ' )
	{ // full ISO 8601 format
		$dateformatstring = 'Y-m-d\TH:i:s\Z';
	}

	if( $useGM )
	{ // We want a Greenwich Meridian time:
		// TODO: dh> what's the point of the substraction? UNIX timestamp should contain no time_difference in the first place?! Otherwise it should be substracted for !$useGM, too.
		// TODO: dh> Why does $useGM do not get the special symbols handling?
		$r = gmdate($dateformatstring, ($unixtimestamp - $time_difference));
	}
	else
	{ // We want default timezone time:

		/*
		Special symbols:
			'b': wether it's today (1) or not (0)
			'l': weekday
			'D': weekday abbrev
			'e': weekday letter
			'F': month
			'M': month abbrev
		*/

		#echo $dateformatstring, '<br />';

		// protect special symbols, that date() would need proper locale set for
		$protected_dateformatstring = preg_replace( '/(?<!\\\)([blDeFM])/', '@@@\\\$1@@@', $dateformatstring );

		#echo $protected_dateformatstring, '<br />';

		$r = date( $protected_dateformatstring, $unixtimestamp );

		if( $protected_dateformatstring != $dateformatstring )
		{ // we had special symbols, replace them

			$istoday = ( date('Ymd',$unixtimestamp) == date('Ymd',$localtimenow) ) ? '1' : '0';
			$datemonth = date('m', $unixtimestamp);
			$dateweekday = date('w', $unixtimestamp);

			// replace special symbols
			$r = str_replace( array(
						'@@@b@@@',
						'@@@l@@@',
						'@@@D@@@',
						'@@@e@@@',
						'@@@F@@@',
						'@@@M@@@',
						),
					array( $istoday,
						trim(T_($weekday[$dateweekday])),
						trim(T_($weekday_abbrev[$dateweekday])),
						trim(T_($weekday_letter[$dateweekday])),
						trim(T_($month[$datemonth])),
						trim(T_($month_abbrev[$datemonth])) ),
					$r );
		}
	}

	return $r;
}


/**
 * Add given # of days to a timestamp
 *
 * @param integer timestamp
 * @param integer days
 * @return integer timestamp
 */
function date_add_days( $timestamp, $days )
{
	return mktime( date('H',$timestamp), date('m',$timestamp), date('s',$timestamp),
								date('m',$timestamp), date('d',$timestamp)+$days, date('Y',$timestamp)  );
}

/**
 * Format dates into a string in a way similar to sprintf()
 */
function date_sprintf( $string, $timestamp )
{
	global $date_sprintf_timestamp;
	$date_sprintf_timestamp = $timestamp;

	return preg_replace_callback( '/%\{(.*?)\}/', 'date_sprintf_callback', $string );
}

function date_sprintf_callback( $matches )
{
	global $date_sprintf_timestamp;

	return date_i18n( $matches[1], $date_sprintf_timestamp );
}


/**
 * Get date name when date was happened
 *
 * @param integer Timestamp
 * @return string Name of date (Today, Yesterday, x days ago, x months ago, x years ago)
 */
function date_ago( $timestamp )
{
	global $servertimenow;

	$days = floor( ( $servertimenow - $timestamp ) / 86400 );
	$months = ceil( $days / 31 );

	if( $days < 1 )
	{	// Today
		return T_('Today');
	}
	elseif( $days == 1 )
	{	// Yesterday
		return T_('Yesterday');
	}
	elseif( $days > 1 && $days <= 31 )
	{	// Days
		return sprintf( T_('%s days ago'), $days );
	}
	elseif( $days > 31 && $months <= 12 )
	{	// Months
		return sprintf( $months == 1 ? T_('%s month ago') : T_('%s months ago'), $months );
	}
	else
	{	// Years
		$years = floor( $months / 12 );
		return sprintf( $years == 1 ? T_('%s year ago') : T_('%s years ago'), $years );
	}
}


/**
 * Convert seconds to readable period
 *
 * @param integer Seconds
 * @param boolean TRUE to use short format(only first letter of period name)
 * @return string Readable time period
 */
function seconds_to_period( $seconds, $short_format = false )
{
	$periods = array(
		array( 31536000, T_('1 year'),   T_('%s years'),   /* TRANS: Short for "1year" */  T_('%sy') ), // 365 days
		array( 2592000,  T_('1 month'),  T_('%s months'),  /* TRANS: Short for "1month" */ T_('%sm') ), // 30 days
		array( 86400,    T_('1 day'),    T_('%s days'),    /* TRANS: Short for "1day" */   T_('%sd') ),
		array( 3600,     T_('1 hour'),   T_('%s hours'),   /* TRANS: Short for "1hour" */  T_('%sh') ),
		array( 60,       T_('1 minute'), T_('%s minutes'), /* TRANS: Short for "1minute" */T_('%smn') ),
		array( 1,        T_('1 second'), T_('%s seconds'), /* TRANS: Short for "1second" */T_('%ss') ),
	);

	foreach( $periods as $p_info )
	{
		$period_value = intval( $seconds / $p_info[0] * 10 ) /10;
		if( $period_value >= 1 )
		{ // Stop on this period
			if( $short_format )
			{	// Use short format:
				$period_text = sprintf( $p_info[3], $period_value );
			}
			elseif( $period_value == 1 )
			{ // One unit of period
				$period_text = $p_info[1];
			}
			else
			{ // Two and more units of period
				$period_text = sprintf( $p_info[2], $period_value );
			}
			break;
		}
	}

	if( !isset( $period_text ) )
	{ // 0 seconds
		$period_text = $short_format ? sprintf( T_('%ss'), 0 ) : sprintf( T_('%s seconds'), 0 );
	}

	return $period_text;
}


/**
 * Converts an ISO 8601 date to MySQL DateTime format.
 *
 * @param string date and time in ISO 8601 format {@link http://en.wikipedia.org/wiki/ISO_8601}.
 * @return string date and time in MySQL DateTime format Y-m-d H:i:s.
 */
function iso8601_to_datetime( $iso_date )
{
	return preg_replace('#([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(Z|[\+|\-][0-9]{2,4}){0,1}#', '$1-$2-$3 $4:$5:$6', $iso_date);
}


/**
 * Converts a MySQL DateTime to ISO 8601 date format.
 *
 * @param string date and time in MySQL DateTime format Y-m-d H:i:s
 * @return string date and time in ISO 8601 format {@link http://en.wikipedia.org/wiki/ISO_8601}.
 */
function datetime_to_iso8601( $datetime, $useGM = false )
{
	$iso_date = mysql2date('U', $datetime);

	if( $useGM )
	{
		$iso_date = gmdate('Ymd', $iso_date).'T'.gmdate('H:i:s', $iso_date);
	}
	else
	{
		$iso_date = date('Ymd', $iso_date).'T'.date('H:i:s', $iso_date);
	}

	return $iso_date;
}


/**
 *
 * @param integer year
 * @param integer month (0-53)
 * @param integer 0 for sunday, 1 for monday
 */
function get_start_date_for_week( $year, $week, $startofweek )
{
	$new_years_date = mktime( 0, 0, 0, 1, 1, $year );
	$weekday = date('w', $new_years_date);
	// echo '<br> 1st day is a: '.$weekday;

	// How many days until start of week:
	$days_to_new_week = (7 - $weekday + $startofweek) % 7;
	// echo '<br> days to new week: '.$days_to_new_week;

	// We now add the required number of days to find the 1st sunday/monday in the year:
	//$first_week_start_date = $new_years_date + $days_to_new_week * 86400;
	//echo '<br> 1st week starts on '.date( 'Y-m-d H:i:s', $first_week_start_date );

	// We add the number of requested weeks:
	// This will fail when passing to Daylight Savings Time: $date = $first_week_start_date + (($week-1) * 604800);
	$date = mktime( 0, 0, 0, 1, $days_to_new_week + 1 + ($week-1) * 7, $year );
	// echo '<br> week '.$week.' starts on '.date( 'Y-m-d H:i:s', $date );

	return $date;
}



/**
 * Get start and end day of a week, based on day f the week and start-of-week
 *
 * Used by Calendar
 *
 * @param date
 * @param integer 0 for Sunday, 1 for Monday
 */
function get_weekstartend( $date, $startOfWeek )
{
	while( date('w', $date) <> $startOfWeek )
	{
		// echo '<br />'.date('Y-m-d H:i:s w', $date).' - '.$startOfWeek;
		// mktime is needed so calculations work for DST enabling. Example: March 30th 2008, start of week 0 sunday
		$date = mktime( 0, 0, 0, date('m',$date), date('d',$date)-1, date('Y',$date) );
	}
	// echo '<br />'.date('Y-m-d H:i:s w', $date).' = '.$startOfWeek;
	$week['start'] = $date;
	$week['end']   = $date + 604800; // 7 days

	// pre_dump( 'weekstartend: ', date( 'Y-m-d', $week['start'] ), date( 'Y-m-d', $week['end'] ) );

	return( $week );
}


/**
 * Get datetime rounded to lower minute. This is meant to remove seconds and
 * leverage MySQL's query cache by having SELECT queries remain identical for 60 seconds instead of just 1.
 *
 * @param integer UNIX timestamp
 * @param string Format (defaults to "Y-m-d H:i"). Use "U" for UNIX timestamp.
 */
function remove_seconds($timestamp, $format = 'Y-m-d H:i')
{
	return date($format, floor($timestamp/60)*60);
}


/**
 * Convert from seconds to months, days, hours, minutes and seconds
 *
 * @param integer duration in seconds
 * @return array of [ years, months, days, hours, minutes, seconds ]
 */
function get_duration_fields( $duration )
{
	$result = array();

	$year_seconds = 31536000; // 1 year
	$years = floor( $duration / $year_seconds );
	$duration = $duration - $years * $year_seconds;
	$result[ 'years' ] = $years;

	$month_seconds = 2592000; // 1 month
	$months = floor( $duration / $month_seconds );
	$duration = $duration - $months * $month_seconds;
	$result[ 'months' ] = $months;

	$day_seconds = 86400; // 1 day
	$days = floor( $duration / $day_seconds );
	$duration = $duration - $days * $day_seconds;
	$result[ 'days' ] = $days;

	$hour_seconds = 3600; // 1 hour
	$hours = floor( $duration / $hour_seconds );
	$duration = $duration - $hours * $hour_seconds;
	$result[ 'hours' ] = $hours;

	$minute_seconds = 60; // 1 minute
	$minutes = floor( $duration / $minute_seconds );
	$duration = $duration - $minutes * $minute_seconds;
	$result[ 'minutes' ] = $minutes;

	$result[ 'seconds' ] = $duration;
	return $result;
}


/**
 * Get a title of duration
 *
 * @param integer Duration in seconds
 * @param array Titles
 * @return string Duration title
 */
function get_duration_title( $duration, $titles = array() )
{
	$titles = array_merge( array(
		'year'   => T_('Last %d years'),
		'month'  => T_('Last %d months'),
		'day'    => T_('Last %d days'),
		'hour'   => T_('Last %d hours'),
		'minute' => T_('Last %d minutes'),
		'second' => T_('Last %d seconds'),
		), $titles );

	$delay_fields = get_duration_fields( $duration );

	if( ! empty( $delay_fields[ 'years' ] ) )
	{ // Years
		return sprintf( $titles['year'], $delay_fields[ 'years' ] );
	}
	elseif( ! empty( $delay_fields[ 'months' ] ) )
	{ // Months
		return sprintf( $titles['month'], $delay_fields[ 'months' ] );
	}
	elseif( ! empty( $delay_fields[ 'days' ] ) )
	{ // Days
		return sprintf( $titles['day'], $delay_fields[ 'days' ] );
	}
	elseif( ! empty( $delay_fields[ 'hours' ] ) )
	{ // Hours
		return sprintf( $titles['hour'], $delay_fields[ 'hours' ] );
	}
	elseif( ! empty( $delay_fields[ 'minutes' ] ) )
	{ // Minutes
		return sprintf( $titles['minute'], $delay_fields[ 'minutes' ] );
	}
	else
	{ // Seconds
		return sprintf( $titles['second'], $delay_fields[ 'seconds' ] );
	}
}

function get_lastseen_date( $date, $view = 'exact_date', $cheat = 0 )
{
	$result = mysql2localedate( $date );

	if( $view == 'blurred_date' )
	{
		$result = (int)( ( ( time() - strtotime( $date ) ) / 86400 ) - $cheat);

		if( $result < 0 )
		{
			$result = 0;
		}

		if( $result < 3 )
		{
			$result = T_('less than 3 days ago');
		}
		elseif( $result < 7 )
		{
			$result = T_('less than a week ago');
		}
		elseif( $result < 30 )
		{
			$result = T_('less than a month ago');
		}
		elseif( $result < 90 )
		{
			$result = T_('less than 3 months ago');
		}
		else
		{
			$result = T_('more than 3 months ago');
		}
	}

	return $result;
}


/**
 * Validate variable
 *
 * @param string param name
 * @param string validator function name
 * @param boolean true if variable value can't be empty
 * @param custom error message
 * @return boolean true if OK
 */
function param_validate( $variable, $validator, $required = false, $custom_msg = NULL )
{
	/* Tblue> Note: is_callable() does not check whether a function is
	 *        disabled (http://www.php.net/manual/en/function.is-callable.php#79151).
	 */
	if( ! is_callable( $validator ) )
	{
		debug_die( 'Validator function '.$validator.'() is not callable!' );
	}

	if( ! isset( $GLOBALS[$variable] ) )
	{	// Variable not set, we cannot handle this using the validator function...
		if( $required )
		{	// Add error:
			param_check_not_empty( $variable, $custom_msg );
			return false;
		}

		return true;
	}

	if( $GLOBALS[$variable] === '' && ! $required )
	{	// Variable is empty or not set. That's fine since it isn't required:
		return true;
	}

	$msg = $validator( $GLOBALS[$variable] );

	if( !empty( $msg ) )
	{
		if( !empty( $custom_msg ) )
		{
			$msg = $custom_msg;
		}

		param_error( $variable, $msg );
		return false;
	}

	return true;
}


/**
 * Checks if the param is a decimal number
 *
 * @param string decimal to check
 * @return boolean true if OK
 */
function is_decimal( $decimal )
{
	return preg_match( '#^[0-9]*(\.[0-9]+)?$#', $decimal );
}


/**
 * Checks if the param is an integer (no float, e.g. 3.14).
 *
 * @param string number to check
 * @return boolean true if OK
 */
function is_number( $number )
{
	return preg_match( '#^[0-9]+$#', $number );
}


/**
 * Check that email address looks valid.
 *
 * @param string email address to check
 * @param string Format to use ('simple', 'rfc')
 *    'simple':
 *      Single email address.
 *    'rfc':
 *      Full email address, may include name (RFC2822)
 *      - example@example.org
 *      - Me <example@example.org>
 *      - "Me" <example@example.org>
 * @param boolean Return the match or boolean
 *
 * @return bool|array Either true/false or the match (see {@link $return_match})
 */
function is_email( $email, $format = 'simple', $return_match = false )
{
	#$chars = "/^([a-z0-9_]|\\-|\\.)+@(([a-z0-9_]|\\-)+\\.)+[a-z]{2,4}\$/i";

	switch( $format )
	{
		case 'rfc':
		case 'rfc2822':
			/**
			 * Regexp pattern converted from: http://www.regexlib.com/REDetails.aspx?regexp_id=711
			 * Extended to allow escaped quotes.
			 */
			$pattern_email = '/^
				(
					(?>[a-zA-Z\d!\#$%&\'*+\-\/=?^_`{|}~]+\x20*
						|"( \\\" | (?=[\x01-\x7f])[^"\\\] | \\[\x01-\x7f] )*"\x20*)* # Name
					(<)
				)?
				(
					(?!\.)(?>\.?[a-zA-Z\d!\#$%&\'*+\-\/=?^_`{|}~]+)+
					|"( \\\" | (?=[\x01-\x7f])[^"\\\] | \\[\x01-\x7f] )* " # quoted mailbox name
				)
				@
				(
					((?!-)[a-zA-Z\d\-]+(?<!-)\.)+[a-zA-Z]{2,}
					|
					\[(
						( (?(?<!\[)\.)(25[0-5] | 2[0-4]\d | [01]?\d?\d) ){4}
						|
						[a-zA-Z\d\-]*[a-zA-Z\d]:( (?=[\x01-\x7f])[^\\\[\]] | \\[\x01-\x7f] )+
					)\]
				)
				(?(3)>) # match ">" if it was there
				$/x';
			break;

		case 'simple':
		default:
			// '/^\S+@[^\.\s]\S*\.[a-z]{2,}$/i'
			$pattern_email = '~^(([_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-z0-9-]+)*(\.[a-z]{2,}))$~i';
			break;
	}

	if( strpos( $email, '@' ) !== false && strpos( $email, '.' ) !== false )
	{
		if( $return_match )
		{
			preg_match( $pattern_email, $email, $match );
			return $match;
		}
		else
		{
			return (bool)preg_match( $pattern_email, $email );
		}
	}
	else
	{
		return $return_match ? array() : false;
	}
}


/**
 * Checks if the phone number is valid
 *
 * @param string phone number to check
 * @return boolean true if OK
 */
function is_phone( $phone )
{
	return preg_match( '|^\+?[\-*#/(). 0-9]+$|', $phone );
}


/**
 * Checks if the url is valid
 *
 * @param string url to check
 * @return boolean true if OK
 */
function is_url( $url )
{
	if( validate_url( $url, 'posting', false ) )
	{
		return false;
	}

	return true;
}


/**
 * Checks if the word is valid
 *
 * @param string word to check
 * @return boolean true if OK
 */
function is_word( $word )
{
	return preg_match( '#^[A-Za-z]+$#', $word );
}


/**
 * Check if the login is valid (in terms of allowed chars)
 *
 * @param string login
 * @return boolean|string TRUE if OK, FALSE if error, special error cases: 'usr', 'long'
 */
function is_valid_login( $login, $force_strict_logins = false )
{
	global $Settings;

	$strict_logins = isset( $Settings ) ? $Settings->get('strict_logins') : 1;

	// NOTE: in some places usernames are typed in by other users (messaging) or admins.
	// Having cryptic logins with hard to type letters is a PITA.

	// Step 1
	// Forbid the following characters in logins
	if( preg_match( '~[\'"><@&\s]~', $login ) )
	{	// WARNING: allowing ' or " or > or < will open security issues!
		// NOTE: allowing @ will make some "average" users use their email address (not good for their spam health)
		// NOTE: we do not allow whitespace in logins
		return false;
	}

	// Step 2
	if( ($strict_logins || $force_strict_logins) && ! preg_match( '~^[A-Za-z0-9_.\-]+$~', $login ) )
	{	// WARNING: allowing special chars like latin 1 accented chars ( \xDF-\xF6\xF8-\xFF ) will create issues with
		// user media directory names (tested on Max OS X) -- Do no allow any of this until we have a clean & safe media dir name generator.

		// fp> TODO: check why a dash '-' prevents renaming the fileroot
		return false;
	}
	elseif( ! $strict_logins )
	{	// We allow any character that is not explicitly forbidden in Step 1
		// Enforce additional limitations
		$login = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '', $login ); // Kill octets
		$login = preg_replace( '/&.+?;/', '', $login ); // Kill entities
	}

	// Step 3
	// Special case, the login is valid however we forbid it's usage.
	// param_check_valid_login() will display a special error message in this case.
	if( preg_match( '~^usr_~', $login ) )
	{	// Logins cannot start with 'usr_', this prefix is reserved for system use
		// We create user media directories for users with non-ASCII logins in format /media/users/usr_55/, where 55 is user ID
		return 'usr';
	}

	// Step 4
	// To avoid MySQL erro on insert long data
	if( utf8_strlen( $login ) > 20 )
	{	// Don't allow long login:
		return 'long';
	}

	return true;
}


/**
 * Checks if the color is valid
 * Allowed formats: #09ABEF, rgba(100,200,255,0.99)
 *
 * @param string color to check
 * @return boolean true if OK
 */
function is_color( $color )
{
	return preg_match( '~^(#([a-f0-9]{3}){1,2}|rgba\(\d{1,3},\d{1,3},\d{1,3},(1|0(\.\d{1,2})?)\))$~i', $color );
}


/**
 * Check if the login is valid (user exists)
 *
 * @param string login
 * @return boolean true if OK
 */
function user_exists( $login )
{
	global $DB;

	$SQL = new SQL( 'Get a count of users with login '.$login );
	$SQL->SELECT( 'COUNT(*)' );
	$SQL->FROM( 'T_users' );
	$SQL->WHERE( 'user_login = '.$DB->quote( $login ) );

	$var = $DB->get_var( $SQL );
	return $var > 0 ? true : false; // PHP4 compatibility
}


/**
 * Are we running on a Windows server?
 */
function is_windows()
{
	return ( strtoupper(substr(PHP_OS,0,3)) == 'WIN' );
}


/**
 * Get all "a" tags from the given content
 *
 * @param string content
 * @return array all <a../a> part from the given content
 */
function get_atags( $content )
{
	$tags = array();

	if( preg_match_all( '#(<a[^>]+>(.*?)</a>|&lt;a.+&gt;(.*?)&lt;/a&gt;)#i', $content, $result ) )
	{
		$tags = $result[0];
	}

	return $tags;
}


/**
 * Add class to an html tag
 *
 * @param string HTML content
 * @param string Class to add to the tag in the HTML
 * @param integer Number of tags to add the class to
 * @return string HTML content with the added class
 */
function add_tag_class( $content, $class, $limit = 1 )
{
	global $add_class;
	$add_class = $class;

	$content = preg_replace_callback( '/<[^\/].*?>/i', 'callback_add_tag_class',
		$content, $limit );

	unset( $add_class );

	return $content;
}
function callback_add_tag_class( $matches )
{
	global $add_class;
	// Check if class attribute is already defined
	if( preg_match( '/\sclass\s*=/i', $matches[0] ) )
	{ // Insert class
		//$content = preg_replace( '/(<.*)(class\s*=\s*")(.*)"/i', '$1$2$3 '.$class.'"', $content, $limit );
		return preg_replace( '/(<[a-zA-z_:]\s*?.*?\s*?class=")(.*?)(".*?>)/i', '$1$2 '.$add_class.'$3', $matches[0] );
	}
	else
	{
		return preg_replace( '/>/i', ' class="'.$add_class.'"$1>', $matches[0] );
	}
}


/**
 * Get all "img" tags from the given content
 *
 * @param string content
 * @return array all <img../img> part from the given content
 */
function get_imgtags( $content )
{
	$tag = 'img';
	$regexp = '{<'.$tag.'[^>]*[ (</'.$tag.'>) | (/>) ]}';

	preg_match_all( $regexp, $content, $result );
	return $result[0];
}


/**
 * Get all urls from the given content
 *
 * @param string content
 * @return array all url from content
 */
function get_urls( $content )
{
	$regexp = '^(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2,4}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:\/(?:[-\w~!$+|.,;=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,;*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,;*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)?^';

	preg_match_all( $regexp, $content, $result );
	return $result[0];
}


function xmlrpc_getposttitle($content)
{
	global $post_default_title;
	if (preg_match('/<title>(.+?)<\/title>/is', $content, $matchtitle))
	{
		$post_title = $matchtitle[1];
	}
	else
	{
		$post_title = $post_default_title;
	}
	return($post_title);
}


/**
 * Also used by post by mail
 *
 * @deprecated by xmlrpc_getpostcategories()
 */
function xmlrpc_getpostcategory($content)
{
	if (preg_match('/<category>([0-9]+?)<\/category>/is', $content, $matchcat))
	{
		return $matchcat[1];
	}

	return false;
}


/**
 * Extract categories out of "<category>" tag from $content.
 *
 * NOTE: w.bloggar sends something like "<category>00000013,00000001,00000004,</category>" to
 *       blogger.newPost.
 *
 * @return false|array
 */
function xmlrpc_getpostcategories($content)
{
	$cats = array();

	if( preg_match('~<category>(\d+\s*(,\s*\d*)*)</category>~i', $content, $match) )
	{
		$cats = preg_split('~\s*,\s*~', $match[1], -1, PREG_SPLIT_NO_EMPTY);
		foreach( $cats as $k => $v )
		{
			$cats[$k] = (int)$v;
		}
	}

	return $cats;
}


/*
 * xmlrpc_removepostdata(-)
 */
function xmlrpc_removepostdata($content)
{
	$content = preg_replace('/<title>(.*?)<\/title>/si', '', $content);
	$content = preg_replace('/<category>(.*?)<\/category>/si', '', $content);
	$content = trim($content);
	return($content);
}


/**
 * Echo the XML-RPC call Result and optionally log into file
 *
 * @param object XMLRPC response object
 * @param boolean true to echo
 * @param mixed File resource or == '' for no file logging.
 */
function xmlrpc_displayresult( $result, $display = true, $log = '' )
{
	if( ! $result )
	{ // We got no response:
		if( $display ) echo T_('No response!')."<br />\n";
		return false;
	}

	if( $result->faultCode() )
	{ // We got a remote error:
		if( $display ) echo T_('Remote error'), ': ', $result->faultString(), ' (', $result->faultCode(), ")<br />\n";
		debug_fwrite($log, $result->faultCode().' -- '.$result->faultString());
		return false;
	}

	// We'll display the response:
	$val = $result->value();
	$value = xmlrpc_decode_recurse($result->value());

	if( is_array($value) )
	{
		$out = '';
		foreach($value as $l_value)
		{
			if( is_array( $l_value ) )
			{
				$out .= ' [';
				foreach( $l_value as $lv_key => $lv_val )
				{
					$out .= $lv_key.' => '.( is_array( $lv_val ) ? '{'.implode( '; ', $lv_val ).'}' : $lv_val ).'; ';
				}
				$out .= '] ';
			}
			else
			{
				$out .= ' ['.$l_value.'] ';
			}
		}
	}
	else
	{
		$out = $value;
	}

	debug_fwrite($log, $out);

	if( $display ) echo T_('Response').': '.$out."<br />\n";

	return $value;
}


/**
 * Log the XML-RPC call Result into LOG object
 *
 * @param object XMLRPC response object
 * @param Log object to add messages to
 * @return boolean true = success, false = error
 */
function xmlrpc_logresult( $result, & $message_Log, $log_payload = true )
{
	if( ! $result )
	{ // We got no response:
		$message_Log->add( T_('No response!'), 'error' );
		return false;
	}

	if( $result->faultCode() )
	{ // We got a remote error:
		$message_Log->add( T_('Remote error').': '.$result->faultString().' ('.$result->faultCode().')', 'error' );
		return false;
	}

	if( $log_payload )
	{
		// We got a response:
		$value = xmlrpc_decode_recurse($result->value());

		if( is_array($value) )
		{
			$out = '';
			foreach($value as $l_value)
			{
				$out .= ' ['.var_export($l_value, true).'] ';
			}
		}
		else
		{
			$out = $value;
		}

		$message_Log->add( T_('Response').': '.$out, 'success' );
	}

	return true;
}



function debug_fopen($filename, $mode) {
	global $debug;
	if ($debug == 1 && ( !empty($filename) ) )
	{
		$fp = fopen($filename, $mode);
		return $fp;
	} else {
		return false;
	}
}

function debug_fwrite($fp, $string)
{
	global $debug;
	if( $debug && $fp )
	{
		fwrite($fp, $string);
	}
}

function debug_fclose($fp)
{
	global $debug;
	if( $debug && $fp )
	{
		fclose($fp);
	}
}



/**
 * Wrap pre tag around {@link var_dump()} for better debugging.
 *
 * @param $var__var__var__var__,... mixed variable(s) to dump
 * @return true
 */
function pre_dump( $var__var__var__var__ )
{
	global $is_cli;

	#echo 'pre_dump(): '.debug_get_backtrace(); // see where a pre_dump() comes from

	$func_num_args = func_num_args();
	$count = 0;

	if( ! empty($is_cli) )
	{ // CLI, no encoding of special chars:
		$count = 0;
		foreach( func_get_args() as $lvar )
		{
			var_dump($lvar);

			$count++;
			if( $count < $func_num_args )
			{ // Put newline between arguments
				echo "\n";
			}
		}
	}
	elseif( function_exists('xdebug_var_dump') )
	{ // xdebug already does fancy displaying:

		// no limits:
		$old_var_display_max_children = @ini_set('xdebug.var_display_max_children', -1); // default: 128
		$old_var_display_max_data = @ini_set('xdebug.var_display_max_data', -1); // max string length; default: 512
		$old_var_display_max_depth = @ini_set('xdebug.var_display_max_depth', -1); // default: 3

		echo "\n<div style=\"padding:1ex;border:1px solid #00f;\">\n";
		foreach( func_get_args() as $lvar )
		{
			xdebug_var_dump($lvar);

			$count++;
			if( $count < $func_num_args )
			{ // Put HR between arguments
				echo "<hr />\n";
			}
		}
		echo '</div>';

		// restore xdebug settings:
		@ini_set('xdebug.var_display_max_children', $old_var_display_max_children);
		@ini_set('xdebug.var_display_max_data', $old_var_display_max_data);
		@ini_set('xdebug.var_display_max_depth', $old_var_display_max_depth);
	}
	else
	{
		$orig_html_errors = @ini_set('html_errors', 0); // e.g. xdebug would use fancy html, if this is on; we catch (and use) xdebug explicitly above, but just in case

		echo "\n<pre style=\"padding:1ex;border:1px solid #00f;overflow:auto\">\n";
		foreach( func_get_args() as $lvar )
		{
			ob_start();
			var_dump($lvar); // includes "\n"; do not use var_export() because it does not detect recursion by design
			$buffer = ob_get_contents();
			ob_end_clean();
			echo htmlspecialchars($buffer);

			$count++;
			if( $count < $func_num_args )
			{ // Put HR between arguments
				echo "<hr />\n";
			}
		}
		echo "</pre>\n";
		@ini_set('html_errors', $orig_html_errors);
	}
	evo_flush();
	return true;
}


/**
 * Get a function trace from {@link debug_backtrace()} as html table.
 *
 * Adopted from {@link http://us2.php.net/manual/de/function.debug-backtrace.php#47644}.
 *
 * @todo dh> Add support for $is_cli = true (e.g. in case of MySQL error)
 *
 * @param integer|NULL Get the last x entries from the stack (after $ignore_from is applied). Anything non-numeric means "all".
 * @param array After a key/value pair matches a stack entry, this and the rest is ignored.
 *              For example, array('class' => 'DB') would exclude everything after the stack
 *              "enters" class DB and everything that got called afterwards.
 *              You can also give an array of arrays which means that every condition in one of the given array must match.
 * @param integer Number of stack entries to include, after $ignore_from matches.
 * @return string HTML table
 */
function debug_get_backtrace( $limit_to_last = NULL, $ignore_from = array( 'function' => 'debug_get_backtrace' ), $offset_ignore_from = 0 )
{
	if( ! function_exists( 'debug_backtrace' ) ) // PHP 4.3.0
	{
		return 'Function debug_backtrace() is not available!';
	}

	$r = '';

	$backtrace = debug_backtrace();
	$count_ignored = 0; // remember how many have been ignored
	$limited = false;   // remember if we have limited to $limit_to_last

	if( $ignore_from )
	{	// we want to ignore from a certain point
		$trace_length = 0;
		$break_because_of_offset = false;

		for( $i = count($backtrace); $i > 0; $i-- )
		{	// Search the backtrace from behind (first call).
			$l_stack = & $backtrace[$i-1];

			if( $break_because_of_offset && $offset_ignore_from < 1 )
			{ // we've respected the offset, but need to break now
				break; // ignore from here
			}

			foreach( $ignore_from as $l_ignore_key => $l_ignore_value )
			{	// Check if we want to ignore from here
				if( is_array($l_ignore_value) )
				{	// It's an array - all must match
					foreach( $l_ignore_value as $l_ignore_mult_key => $l_ignore_mult_val )
					{
						if( !isset($l_stack[$l_ignore_mult_key]) /* not set with this stack entry */
							|| strcasecmp($l_stack[$l_ignore_mult_key], $l_ignore_mult_val) /* not this value (case-insensitive) */ )
						{
							continue 2; // next ignore setting, because not all match.
						}
					}
					if( $offset_ignore_from-- > 0 )
					{
						$break_because_of_offset = true;
						break;
					}
					break 2; // ignore from here
				}
				elseif( isset($l_stack[$l_ignore_key])
					&& !strcasecmp($l_stack[$l_ignore_key], $l_ignore_value) /* is equal case-insensitive */ )
				{
					if( $offset_ignore_from-- > 0 )
					{
						$break_because_of_offset = true;
						break;
					}
					break 2; // ignore from here
				}
			}
			$trace_length++;
		}

		$count_ignored = count($backtrace) - $trace_length;

		$backtrace = array_slice( $backtrace, 0-$trace_length ); // cut off ignored ones
	}

	$count_backtrace = count($backtrace);
	if( is_numeric($limit_to_last) && $limit_to_last < $count_backtrace )
	{	// we want to limit to a maximum number
		$limited = true;
		$backtrace = array_slice( $backtrace, 0, $limit_to_last );
		$count_backtrace = $limit_to_last;
	}

	$r .= '<div style="padding:1ex; margin-bottom:1ex; text-align:left; color:#000; background-color:#ddf;">
					<h3>Backtrace:</h3>'."\n";
	if( $count_backtrace )
	{
		$r .= '<ol style="font-family:monospace;">';

		$i = 0;
		foreach( $backtrace as $l_trace )
		{
			if( ++$i == $count_backtrace )
			{
				$r .= '<li style="padding:0.5ex 0;">';
			}
			else
			{
				$r .= '<li style="padding:0.5ex 0; border-bottom:1px solid #77d;">';
			}
			$args = array();
			if( isset($l_trace['args']) && is_array( $l_trace['args'] ) )
			{	// Prepare args:
				foreach( $l_trace['args'] as $l_arg )
				{
					$l_arg_type = gettype($l_arg);
					switch( $l_arg_type )
					{
						case 'integer':
						case 'double':
							$args[] = $l_arg;
							break;
						case 'string':
							$args[] = '"'.strmaxlen(str_replace("\n", '\n', $l_arg), 255, NULL, 'htmlspecialchars').'"';
							break;
						case 'array':
							$args[] = 'Array('.count($l_arg).')';
							break;
						case 'object':
							$args[] = 'Object('.get_class($l_arg).')';
							break;
						case 'resource':
							$args[] = htmlspecialchars((string)$l_arg);
							break;
						case 'boolean':
							$args[] = $l_arg ? 'true' : 'false';
							break;
						default:
							$args[] = $l_arg_type;
					}
				}
			}

			$call = "<strong>\n";
			if( isset($l_trace['class']) )
			{
				$call .= htmlspecialchars($l_trace['class']);
			}
			if( isset($l_trace['type']) )
			{
				$call .= htmlspecialchars($l_trace['type']);
			}
			$call .= htmlspecialchars($l_trace['function'])."( </strong>\n";
			if( $args )
			{
				$call .= ' '.implode( ', ', $args ).' ';
			}
			$call .='<strong>)</strong>';

			$r .= $call."<br />\n";

			$r .= '<strong>';
			if( isset($l_trace['file']) )
			{
				$r .= "File: </strong> ".$l_trace['file'];
			}
			else
			{
				$r .= '[runtime created function]</strong>';
			}
			if( isset($l_trace['line']) )
			{
				$r .= ' on line '.$l_trace['line'];
			}

			$r .= "</li>\n";
		}
		$r .= '</ol>';
	}
	else
	{
		$r .= '<p>No backtrace available.</p>';
	}

	// Extra notes, might be to much, but explains why we stopped at some point. Feel free to comment it out or remove it.
	$notes = array();
	if( $count_ignored )
	{
		$notes[] = 'Ignored last: '.$count_ignored;
	}
	if( $limited )
	{
		$notes[] = 'Limited to'.( $count_ignored ? ' remaining' : '' ).': '.$limit_to_last;
	}
	if( $notes )
	{
		$r .= '<p class="small">'.implode( ' - ', $notes ).'</p>';
	}

	$r .= "</div>\n";

	return $r;
}


/**
 * Outputs Unexpected Error message. When in debug mode it also prints a backtrace.
 *
 * This should be used instead of die() everywhere.
 * This should NOT be used instead of exit() anywhere.
 * Dying means the application has encontered an unexpected situation,
 * i-e: something that should never occur during normal operation.
 * Examples: database broken, user changed URL by hand...
 *
 * @param string Message to output
 * @param array Additional params
 *        - "status" (Default: '500 Internal Server Error')
 *        - "debug_info" - Use this info instead of $additional_info when debug is ON
 */
function debug_die( $additional_info = '', $params = array() )
{
	global $debug, $baseurl;
	global $log_app_errors, $app_name, $is_cli, $display_errors_on_production, $is_api_request, $is_cron_job_executing;

	$params = array_merge( array(
		'status'     => '500 Internal Server Error',
		'debug_info' => '',
		), $params );

	if( $debug && ! empty( $params['debug_info'] ) )
	{ // Display 'debug_info' when debug is ON
		$additional_info = $params['debug_info'];
	}

	// Send the predefined cookies:
	evo_sendcookies();

	if( $is_api_request )
	{	// REST API or XMLRPC request:

		// Set JSON content type:
		headers_content_mightcache( 'application/json', 0, '#', false ); // Do NOT cache error messages! (Users would not see they fixed them)
		header_http_response( $params['status'] );

		echo json_encode( array(
				'error_status' => $params['status'],
				'error_info'   => $additional_info,
			) );

		die(1); // Error code 1. Note: This will still call the shutdown function.
	}
	elseif( $is_cron_job_executing )
	{	// If debug die has been called during cron job executing:
		$cron_job_error = '<div style="background-color: #ddd; padding: 1ex; margin-bottom: 1ex;">'
				.'<h3>Additional information about this error:</h3>'
				.$additional_info
			.'</div>'
			.debug_get_backtrace();
		throw new Exception( str_replace( "\n", '', $cron_job_error ) );
	}
	elseif( $is_cli )
	{ // Command line interface, e.g. in cron_exec.php:
		echo '== '.T_('An unexpected error has occurred!')." ==\n";
		echo T_('If this error persists, please report it to the administrator.')."\n";
		if( $debug || $display_errors_on_production )
		{ // Display additional info only in debug mode or when it was explicitly set by display_errors_on_production setting because it can reveal system info to hackers and greatly facilitate exploits
			echo T_('Additional information about this error:')."\n";
			echo strip_tags( $additional_info )."\n\n";
		}
	}
	else
	{
		// Attempt to output an error header (will not work if the output buffer has already flushed once):
		// This should help preventing indexing robots from indexing the error :P
		if( ! headers_sent() )
		{
			load_funcs('_core/_template.funcs.php');
			headers_content_mightcache( 'text/html', 0, '#', false );  // Do NOT cache error messages! (Users would not see they fixed them)
			$status_header = $_SERVER['SERVER_PROTOCOL'].' '.$params['status'];
			header($status_header);
		}

		$too_many_connections = false;
		if( ! empty( $additional_info ) )
		{	//Handling of "Too many connections":
			$find_token = 'Too many connections';

			if( preg_match( "/{$find_token}/i", $additional_info ) ) 
			{
				load_funcs( 'skins/_skin.funcs.php' );
				$too_many_connections = true;
				require skin_fallback_path( 'too_many_connections.main.php', 6 );
				http_response_code( 503 );
			}
		}

		if( ! $too_many_connections )
		{
			echo '<div style="background-color: #fdd; padding: 1ex; margin-bottom: 1ex;">';
			echo '<h3 style="color:#f00;">'.T_('An unexpected error has occurred!').'</h3>';
			echo '<p>'.T_('If this error persists, please report it to the administrator.').'</p>';
			echo '<p><a href="'.$baseurl.'">'.T_('Go back to home page').'</a></p>';
			echo '</div>';

			if( ! empty( $additional_info ) )
			{
				echo '<div style="background-color: #ddd; padding: 1ex; margin-bottom: 1ex;">';
				if( $debug || $display_errors_on_production )
				{ // Display additional info only in debug mode or when it was explicitly set by display_errors_on_production setting because it can reveal system info to hackers and greatly facilitate exploits
					echo '<h3>'.T_('Additional information about this error:').'</h3>';
					echo $additional_info;
				}
				else
				{
					echo '<p><i>Enable debugging to get additional information about this error.</i></p>' . get_manual_link('debugging','How to enable debug mode?');
				}
				echo '</div>';

				// Append the error text to AJAX log if it is AJAX request
				ajax_log_add( $additional_info, 'error' );
				ajax_log_display();
			}
		}
	}

	if( $log_app_errors > 1 || $debug )
	{ // Prepare backtrace
		$backtrace = debug_get_backtrace();

		if( $log_app_errors > 1 || $is_cli )
		{
			$backtrace_cli = trim(strip_tags($backtrace));
		}
	}

	if( $log_app_errors )
	{ // Log error through PHP's logging facilities:
		$log_message = $app_name.' error: ';
		if( ! empty($additional_info) )
		{
			$log_message .= trim( strip_tags($additional_info) );
		}
		else
		{
			$log_message .= 'No info specified in debug_die()';
		}

		// Get file and line info:
		$file = 'Unknown';
		$line = 'Unknown';
		if( function_exists('debug_backtrace') /* PHP 4.3 */ )
		{ // get the file and line
			foreach( debug_backtrace() as $v )
			{
				if( isset($v['function']) && $v['function'] == 'debug_die' )
				{
					$file = isset($v['file']) ? $v['file'] : 'Unknown';
					$line = isset($v['line']) ? $v['line'] : 'Unknown';
					break;
				}
			}
		}
		$log_message .= ' in '.$file.' at line '.$line;

		if( $log_app_errors > 1 )
		{ // Append backtrace:
			// indent after newlines:
			$backtrace_cli = preg_replace( '~(\S)(\n)(\S)~', '$1  $2$3', $backtrace_cli );
			$log_message .= "\nBacktrace:\n".$backtrace_cli;
		}
		$log_message .= "\nREQUEST_URI:  ".( isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-' );
		$log_message .= "\nHTTP_REFERER: ".( isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '-' );

		error_log( str_replace("\n", ' / ', $log_message), 0 /* PHP's system logger */ );
	}


	// DEBUG OUTPUT:
	if( $debug )
	{
		if( $is_cli )
			echo $backtrace_cli;
		else
			echo $backtrace;
	}

	// EXIT:
	if( ! $is_cli )
	{ // Attempt to keep the html valid (but it doesn't really matter anyway)
		echo '</body></html>';
	}

	die(1);	// Error code 1. Note: This will still call the shutdown function.
}


/**
 * Outputs Bad request Error message. When in debug mode it also prints a backtrace.
 *
 * This should be used when a bad user input is detected.
 *
 * @param string Message to output (HTML/TEXT)
 * @param string Output format: 'html', 'text'
 */
function bad_request_die( $additional_info = '', $error_code = '400 Bad Request', $format = 'html' )
{
	global $debug, $baseurl, $is_api_request;

	// Send the predefined cookies:
	evo_sendcookies();

	if( $is_api_request )
	{	// REST API or XMLRPC request:

		// Set JSON content type:
		headers_content_mightcache( 'application/json', 0, '#', false ); // Do NOT cache error messages! (Users would not see they fixed them)
		header_http_response( $error_code );

		echo json_encode( array(
				'error_status' => $error_code,
				'error_info'   => $additional_info,
			) );

		die(2); // Error code 2. Note: this will still call the shutdown function.
	}

	// Attempt to output an error header (will not work if the output buffer has already flushed once):
	// This should help preventing indexing robots from indexing the error :P
	if( ! headers_sent() )
	{
		load_funcs('_core/_template.funcs.php');
		headers_content_mightcache( 'text/html', 0, '#', false );		// Do NOT cache error messages! (Users would not see they fixed them)
		header_http_response( $error_code );
	}

	if( $format == 'text' )
	{	// Display error message in TEXT format:
		echo $additional_info;
		// Display AJAX Log if it was initialized:
		ajax_log_display();
		die(2); // Error code 2. Note: this will still call the shutdown function.
	}
	// else display error message in HTML format:

	if( ! function_exists( 'T_' ) )
	{	// Load locale funcs to initialize function "T_" because it is used below:
		load_funcs( 'locales/_locale.funcs.php' );
	}

	echo '<div style="background-color: #fdd; padding: 1ex; margin-bottom: 1ex;">';
	echo '<h3 style="color:#f00;">'.T_('Bad Request!').'</h3>';
	echo '<p>'.T_('The parameters of your request are invalid.').'</p>';
	echo '<p>'.T_('If you have obtained this error by clicking on a link INSIDE of this site, please report the bad link to the administrator.').'</p>';
	echo '<p><a href="'.$baseurl.'">'.T_('Go back to home page').'</a></p>';
	echo '</div>';

	if( !empty( $additional_info ) )
	{
		echo '<div style="background-color: #ddd; padding: 1ex; margin-bottom: 1ex;">';
		if( $debug )
		{	// Display additional info only in debug mode because it can reveal system info to hackers and greatly facilitate exploits
			echo '<h3>'.T_('Additional information about this error:').'</h3>';
			echo $additional_info;
		}
		else
		{
			echo '<p><i>Enable debugging to get additional information about this error.</i></p>' . get_manual_link('debugging','How to enable debug mode?');
		}
		echo '</div>';

		// Append the error text to AJAX log if it is AJAX request
		ajax_log_add( $additional_info, 'error' );
	}

	if( $debug )
	{
		echo debug_get_backtrace();
	}

	// Attempt to keep the html valid (but it doesn't really matter anyway)
	echo '</body></html>';

	// Display AJAX Log if it was initialized:
	ajax_log_display();

	die(2); // Error code 2. Note: this will still call the shutdown function.
}


/**
 * Outputs debug info, according to {@link $debug} or $force param. This gets called typically at the end of the page.
 *
 * @param boolean true to force output regardless of {@link $debug}
 * @param boolean true to force clean output (without HTML) regardless of {@link $is_cli}
 */
function debug_info( $force = false, $force_clean = false )
{
	global $debug, $debug_done, $debug_jslog, $debug_jslog_done, $Debuglog, $DB, $obhandler_debug, $Timer, $ReqHost, $ReqPath, $is_cli;
	global $cache_imgsize, $cache_File;
	global $Session;
	global $db_config, $tableprefix, $http_response_code, $disp, $disp_detail, $robots_index, $robots_follow, $content_type_header;
	/**
	 * @var Hit
	 */
	global $Hit;

	// Detect content-type
	$content_type = NULL;
	foreach(headers_list() as $header)
	{
		if( stripos($header, 'content-type:') !== false )
		{ // content type sent
			# "Content-Type:text/html;charset=utf-8" => "text/html"
			$content_type = explode( ':', $header, 2 );
			$content_type = explode( ';', array_pop( $content_type ) );
			$content_type = trim( array_shift( $content_type ) );
			break;
		}
	}

	// ---- Print AJAX Log
	if( empty( $debug_jslog_done ) && ( $debug || $debug_jslog ) && $content_type == 'text/html' )
	{	// Display debug jslog once
		global $rsc_url, $app_version_long;

		$relative_to = ( is_admin_page() ? 'rsc_url' : 'blog' );

		require_js_defer( '#jqueryUI#', $relative_to, true );
		require_css( '#jqueryUI_css#', $relative_to, NULL, NULL, '#', true );
		require_js_defer( 'debug_jslog.js', $relative_to, true );
		require_js_defer( 'ext:jquery/cookie/jquery.cookie.min.js', $relative_to, true );

		$jslog_style_cookies = param_cookie( 'jslog_style', 'string' );
		$jslog_styles = array();
		if( !empty( $jslog_style_cookies ) )
		{	// Get styles only from cookies
			$jslog_style_cookies = explode( ';', $jslog_style_cookies );
			foreach( $jslog_style_cookies as $jsc => $style )
			{
				if( strpos( $style, 'height' ) !== false /*|| ( strpos( $style, 'display' ) !== false && !$debug_jslog )*/ )
				{	// Unset the height param from defined styles ( and the display param if jslog is disabled )
					unset( $jslog_style_cookies[$jsc] );
				}
			}
			$jslog_styles[] = implode( ';', $jslog_style_cookies );
		}
		else
		{
			if( !is_logged_in() )
			{	// Align top when evobar is hidden
				$jslog_styles[] = 'top:0';
			}
			if( $debug_jslog )
			{	// Display the jslog
				$jslog_styles[] = 'display:block';
			}
		}
		$jslog_styles = count( $jslog_styles ) > 0 ? ' style="'.implode( ';', $jslog_styles ).'"' : '';

		$close_url = url_add_param( $_SERVER['REQUEST_URI'], 'jslog' );
		echo '<div id="debug_ajax_info" class="debug"'.$jslog_styles.'>';
		echo '<div class="jslog_titlebar">'.
				'AJAX Debug log'.get_manual_link('ajax_debug_log').
				action_icon( T_('Close'), 'close', $close_url, NULL, NULL, NULL, array( 'class' => 'jslog_switcher' ) ).
			'</div>';
		echo '<div id="jslog_container"></div>';
		echo '<div class="jslog_statusbar">'.
				'<a href="'.$_SERVER['REQUEST_URI'].'#" class="jslog_clear">'.T_('Clear').'</a>'.
			'</div>';
		echo '</div>';

		// Make sure debug jslog output only happens once:
		$debug_jslog_done = true;
	}
	// ----

	// clean output:
	$clean = $is_cli || $force_clean;

	if( ! $force )
	{
		if( ! empty( $debug_done ) )
		{ // Already displayed!
			return;
		}

		if( empty( $debug ) || // No debug output desired:
		    ( $debug < 2 && $content_type != 'text/html' ) ) // Do not display, if no content-type header has been sent or it's != "text/html" (debug > 1 skips this)
		{
			global $evo_last_handled_error;
			if( ! empty( $evo_last_handled_error ) )
			{ // If script has been stoppped by some error
				// Display a message when debug is OFF and error has occured
				$debug_off_title = 'An unexpected error has occured!';
				$debug_off_msg1 = 'We apologize for the inconvenience.';
				$debug_off_msg2 = 'This error has been automatically reported and we will work to resolve it as fast as possible.';
				if( $clean )
				{ // CLI mode
					echo '*** '.$debug_off_title.' ***'."\n\n"
						.$debug_off_msg1."\n"
						.$debug_off_msg2;
				}
				else
				{ // View from browser
					echo '<div style="margin:1em auto;padding:10px;background:#FEFFFF;border:2px solid #F00;border-radius:6px;text-align:center;">'
							.'<h2 style="margin:0;color:#F00;">'.$debug_off_title.'</h2>'
							.'<p>'.$debug_off_msg1.'</p>'
							.'<p style="margin-bottom:0">'.$debug_off_msg2.'</p>'
						.'</div>';
				}
			}
			return;
		}
	}
	//Make sure debug output only happens once:
	$debug_done = true;

	$printf_format = '| %-45s | %-5s | %-7s | %-5s |';
	$table_headerlen = 73;
	/* This calculates the number of dashes to print e. g. on the top and
	 * bottom of the table and after the header, making the table look
	 * better (looks like the tables of the mysql command line client).
	 * Normally, the value won't change, so it's hardcoded above. If you
	 * change the printf() format above, this might be useful.
	preg_match_all( '#\d+#', $printf_format, $table_headerlen );
	$table_headerlen = array_sum( $table_headerlen[0] ) +
									strlen( preg_replace( '#[^ \|]+#', '',
												$printf_format ) ) - 2;
	*/

	$ReqHostPathQuery = $ReqHost.$ReqPath.( empty( $_SERVER['QUERY_STRING'] ) ? '' : '?'.$_SERVER['QUERY_STRING'] );

	echo "\n\n\n";
	if( ! $clean )
	{
		echo '<div class="debug" id="debug_info">';
	}

	// FULL DEBUG INFO(s) FROM PREVIOUS SESSION(s), after REDIRECT(s):
	$get_redirected_debuginfo_from_sess_ID = param( 'get_redirected_debuginfo_from_sess_ID', 'integer' );
	if( ! empty( $get_redirected_debuginfo_from_sess_ID ) )
	 {	// Get Session by ID for debug info from redirected page:
		// (This is used for redirect from different domain)
		$debug_info_Session = new Session( $get_redirected_debuginfo_from_sess_ID );
	}
	elseif( isset( $Session ) )
	{	// Use current Session for debug info from redirected page:
		$debug_info_Session = $Session;
	}

	if( isset( $debug_info_Session ) && ! empty( $debug_info_Session->ID ) )
	{	// Get debug info from redirected page:
		$sess_debug_infos = $debug_info_Session->get( 'debug_infos' );
	}

	if( ! empty( $sess_debug_infos ) )
	{	// Display debug info from redirected page:
		$count_sess_debug_infos = count( $sess_debug_infos );
		if( $count_sess_debug_infos > 1 )
		{	// Links to those Debuglogs:
			if( $clean )
			{	// kind of useless, but anyway...
				echo "\n".'There are '.$count_sess_debug_infos.' debug infos from redirected pages.'."\n";
			}
			else
			{
				echo '<p>There are '.$count_sess_debug_infos.' debug infos from redirected pages: ';
				for( $i = 1; $i <= $count_sess_debug_infos; $i++ )
				{
					echo '<a href="'.$ReqHostPathQuery.'#debug_sess_debug_info_'.$i.'">#'.$i.'</a> ';
				}
				echo '</p>';
			}
		}

		foreach( $sess_debug_infos as $k => $sess_debug_info )
		{
			if( $clean )
			{
				echo "\n".'== Debug messages from redirected page (#'.( $k + 1 ).') =='."\n"
					.'See below for the Debuglog from the current request.'."\n";
				echo gzdecode( $sess_debug_info );
			}
			else
			{
				echo '<div class="debug_session">';
				echo '<h3 id="debug_sess_debug_info_'.( $k + 1 ).'" style="color:#f00">Debug messages from redirected page (#'.( $k + 1 ).')</h3>'
					// link to real Debuglog:
					.'<p><a href="'.$ReqHostPathQuery.'#debug_current">See below for the debug from the current request.</a></p>';
				$sess_debug_info = gzdecode( $sess_debug_info );
				// Fix all anchors to proper work with SESSION debug info and do NOT mix with current debug info where same achors are used but without number suffix ($k + 1):
				$anchors_regexp = '(evo_debug_queries|debug_info_cat_[^"]+)';
				$sess_debug_info = preg_replace( '/id="'.$anchors_regexp.'"/', 'id="$1_'.( $k + 1 ).'"', $sess_debug_info );
				echo preg_replace( '/href="([^#"]*)#'.$anchors_regexp.'"/', 'href="'.$ReqHostPathQuery.'#$2_'.( $k + 1 ).'"', $sess_debug_info );
				echo '</div>';
			}
		}

		if( ! $clean )
		{	// Anchor to scrolldown to current debug:
			echo '<a id="debug_current"></a>';
		}

		// link to first sess_debug_infos:
		if( $clean )
		{
			echo 'See above for the debug info(s) from before the redirect.'."\n";
		}
		else
		{
			echo '<p><a href="'.$ReqHostPathQuery.'#debug_sess_debug_info_1">See above for the debug info(s) from before the redirect.</a></p>';
		}

		// Delete logs since they have been displayed...
		// EXCEPT if we are redirecting, because in this case we won't see these logs in a browser (only in request debug tools)
		// So in that case we want them to move over to the next page...
		if( $http_response_code < 300 || $http_response_code >= 400 )
		{	// This is NOT a 3xx redirect, assume debuglogs have been seen & delete them:
			$debug_info_Session->delete( 'debug_infos' );
		}

		echo "\n\n\n";
	}
	// END OF DEBUG FROM PREVIOUS SESSIONS, after REDIRECT(s).

	$debug_info_title = empty( $count_sess_debug_infos ) ? 'Debug info' : 'Debug info from Current page';
	echo ( $clean ? '*** '.$debug_info_title.' ***'."\n\n" : '<h2>'.$debug_info_title.'</h2>' );

	if( !$obhandler_debug )
	{ // don't display changing items when we want to test obhandler

		// ---------------------------

		echo '<div class="log_container"><div>';

		echo 'HTTP Response code: '.$http_response_code;
		echo $clean ? "\n" : '<br />';

		echo '$content_type_header: '.$content_type_header;
		echo $clean ? "\n" : '<br />';

		echo '$disp: '.$disp.' -- detail: '.$disp_detail;
		echo $clean ? "\n" : '<br />';

		echo '$robots_index: '.$robots_index;
		echo $clean ? "\n" : '<br />';

		echo '$robots_follow: '.$robots_follow;
		echo $clean ? "\n" : '<br />';

		echo '</div></div>';
	}

	if( !$obhandler_debug )
	{ // don't display changing items when we want to test obhandler

		// ================================== DB Summary ================================
		if( isset($DB) )
		{
			echo '<div class="log_container"><div>';
			echo $DB->num_queries.' SQL queries executed in '.$Timer->get_duration( 'SQL QUERIES' )." seconds\n";
			if( ! $clean )
			{
				echo ' &nbsp; <a href="'.$ReqHostPathQuery.'#evo_debug_queries">scroll down to details</a><p>';
			}
			echo '</div></div>';
		}

		// ========================== Timer table ================================
		$time_page = $Timer->get_duration( 'total' );
		$timer_rows = array();
		foreach( $Timer->get_categories() as $l_cat )
		{
			if( $l_cat == 'sql_query' )
			{
				continue;
			}
			$timer_rows[ $l_cat ] = $Timer->get_duration( $l_cat );
		}
		// Don't sort to see orginal order of creation
		// arsort( $timer_rows );
		// ksort( $timer_rows );

		// Remove "total", it will get output as the last one:
		$total_time = $timer_rows['total'];
		unset($timer_rows['total']);

		$percent_total = $time_page > 0 ? number_format( 100/$time_page * $total_time, 2 ) : '0';

		if( $clean )
		{
			echo '== Timers =='."\n\n";
			echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n";
			printf( $printf_format."\n", 'Category', 'Time', '%', 'Count' );
			echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n";
		}
		else
		{
			echo '<table class="debug_timer"><thead>'
				.'<tr><td colspan="4" class="center">Timers</td></tr>' // dh> TODO: should be TH. Workaround so that tablesorter does not pick it up. Feedback from author requested.
				.'<tr><th>Category</th><th>Time</th><th>%</th><th>Count</th></tr>'
				.'</thead>';

			// Output "total":
			echo "\n<tfoot><tr>"
				.'<td>total</td>'
				.'<td class="right red">'.$total_time.'</td>'
				.'<td class="right">'.$percent_total.'%</td>'
				.'<td class="right">'.$Timer->get_count('total').'</td></tr></tfoot>';

			echo '<tbody>';
		}

		$table_rows_collapse = array();
		foreach( $timer_rows as $l_cat => $l_time )
		{
			$percent_l_cat = $time_page > 0 ? number_format( 100/$time_page * $l_time, 2 ) : '0';

			if( $clean )
			{
				$row = sprintf( $printf_format, $l_cat, $l_time, $percent_l_cat.'%', $Timer->get_count( $l_cat ) );
			}
			else
			{
				$row = "\n<tr>"
					.'<td>'.$l_cat.'</td>'
					.'<td class="right">'.$l_time.'</td>'
					.'<td class="right">'.$percent_l_cat.'%</td>'
					.'<td class="right">'.$Timer->get_count( $l_cat ).'</td></tr>';
			}

			// Maybe ignore this row later, but not for clean display.
			if( ! $clean && ( $percent_l_cat < 1  ) )
			{	// Hide everything that tool less tahn 5% of the time
				$table_rows_collapse[] = $row;
			}
			else
			{
				echo $row."\n";
			}
		}
		$count_collapse = count($table_rows_collapse);
		// Collapse ignored rows, allowing to expand them with Javascript:
		if( $count_collapse > 5 )
		{
			echo '<tr><td colspan="4" class="center">';
			echo '<a href="" onclick="jQuery(this).closest(\'tbody\').next().toggle(); return false;">+ '.$count_collapse.' queries &lt; 1%</a> </td></tr>';
			echo '</tbody>';
			echo '<tbody style="display:none">';
		}
		echo implode( "\n", $table_rows_collapse )."\n";

		if ( $clean )
		{ // "total" (done in tfoot for html above)
			echo sprintf( $printf_format, 'total', $total_time, $percent_total.'%', $Timer->get_count('total') );
			echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n\n";
		}
		else
		{
			echo "\n</tbody></table>";

			// add jquery.tablesorter to the "Debug info" table.
			$relative_to = ( is_admin_page() ? 'rsc_url' : 'blog' );
			require_js_defer( 'ext:jquery/tablesorter/jquery.tablesorter.min.js', $relative_to, true );
			require_js_defer( 'src/evo_init_debug_timer.js', $relative_to, true );
		}


		// ================================ Opcode caching ================================
		echo '<div class="log_container"><div>';
		echo 'Opcode cache: '.get_active_opcode_cache();
		echo $clean ? "\n" : '<p>';

		// ================================ User caching ================================
		echo 'User cache: '.get_active_user_cache();
		echo $clean ? "\n" : '<p>';
		echo '</div></div>';


		// ================================ Memory Usage ================================
		echo '<div class="log_container"><div>';

		foreach( array( // note: 8MB is default for memory_limit and is reported as 8388608 bytes
			'memory_get_usage' => array( 'display' => 'Memory usage', 'high' => 8000000 ),
			'memory_get_peak_usage' => array( 'display' => 'Memory peak usage', 'high' => 8000000 ) ) as $l_func => $l_var )
		{
			if( function_exists( $l_func ) )
			{
				$_usage = $l_func();

				if( $_usage > $l_var['high'] )
				{
					echo $clean ? '[!!] ' : '<span style="color:red; font-weight:bold">';
				}

				echo $l_var['display'].': '.bytesreadable( $_usage, ! $clean );

				if( ! $clean && $_usage > $l_var['high'] )
				{
					echo '</span>';
				}
				echo $clean ? "\n" : '<br />';
			}
		}

		echo 'Len of serialized $cache_imgsize: '.strlen(serialize($cache_imgsize));
		echo $clean ? "\n" : '<br />';
		echo 'Len of serialized $cache_File: '.strlen(serialize($cache_File));
		echo $clean ? "\n" : '<br />';

		echo '</div></div>';
	}


	// CURRENT DEBUGLOG (with list of categories at top):
	$log_categories = array( 'error', 'note', 'all' ); // Categories to output (in that order)
	$log_container_head = $clean ? ( "\n".'== Debug messages =='."\n" ) : '<h3 id="debug_debuglog">Debug messages</h3>';

	if ( ! $clean )
	{
		$log_cats = array_keys($Debuglog->get_messages( $log_categories )); // the real list (with all replaced and only existing ones)
		$log_head_links = array();
		foreach( $log_cats as $l_cat )
		{
			$log_head_links[] .= '<a href="'.$ReqHostPathQuery.'#debug_info_cat_'.str_replace( ' ', '_', $l_cat ).'">'.$l_cat.'</a>';
		}
		$log_container_head .= implode( ' | ', $log_head_links );

		echo format_to_output(
			$Debuglog->display( array(
					'container' => array( 'string' => $log_container_head, 'template' => false ),
					'all' => array( 'string' => '<h4 id="debug_info_cat_%s">%s:</h4>', 'template' => false ) ),
				'', false, $log_categories ),
			'htmlbody' );

		echo '<h3 id="evo_debug_queries">DB</h3>';
	}
	else
	{
		echo format_to_output(
			$Debuglog->display( array(
					'container' => array( 'string' => $log_container_head, 'template' => false ),
					'all' => array( 'string' => '= %s ='."\n\n", 'template' => false ) ),
				'', false, $log_categories, '', 'raw', false ),
			'raw' );

		echo "\n".'== DB =='."\n\n";
	}

	if($db_config)
	{
		if ( ! $clean )
		{
			echo '<pre>';
		}

		echo 'Config DB Username: '.$db_config['user']."\n".
			'Config DB Database: '.$db_config['name']."\n".
			 'Config DB Host: '.(isset($db_config['host']) ? $db_config['host'] : 'unset (localhost)')."\n".
			 'Config DB tables prefix: '.$tableprefix."\n".
			 'Config DB connection charset: '.$db_config['connection_charset']."\n";

		echo $clean ? "\n" : '</pre>';
	}

	if( !isset($DB) )
	{
		echo 'No DB object.'.( $clean ? "\n" : '' );
	}
	else
	{
		echo '<pre>Current DB charset: '.$DB->get_connection_charset()."</pre>\n";

		$DB->dump_queries( ! $clean );
	}

	if ( ! $clean )
	{
		echo '</div>';
	}
}


/**
 * Exit when request is blocked
 *
 * @param string Block type: 'IP', 'Domain', 'Country'
 * @param string Log message
 * @param string Syslog origin type: 'core', 'plugin'
 * @param integer Syslog origin ID
 */
function exit_blocked_request( $block_type, $log_message, $syslog_origin_type = 'core', $syslog_origin_ID = NULL )
{
	global $debug;

	// Write system log for the request:
	syslog_insert( $log_message, 'warning', NULL, NULL, $syslog_origin_type, $syslog_origin_ID );

	// Print out this text to inform an user:
	echo 'This request has been blocked.';

	if( $debug )
	{ // Display additional info on debug mode:
		echo ' ('.$block_type.')';
	}

	// EXIT:
	exit( 0 );
}


/**
 * Check if the current request exceed the post max size limit.
 * If too much data was sent add an error message and call header redirect.
 */
function check_post_max_size_exceeded()
{
	global $Messages;

	if( ( $_SERVER['REQUEST_METHOD'] == 'POST' ) && empty( $_POST ) && empty( $_FILES ) && ( $_SERVER['CONTENT_LENGTH'] > 0 ) )
	{
		// Check post max size ini setting
		$post_max_size = ini_get( 'post_max_size' );

		// Convert post_max_size value to bytes
		switch ( substr( $post_max_size, -1 ) )
		{
			case 'G':
				$post_max_size = $post_max_size * 1024;
			case 'M':
				$post_max_size = $post_max_size * 1024;
			case 'K':
				$post_max_size = $post_max_size * 1024;
		}

		// Add error message and redirect back to the referer url
		$Messages->add( sprintf( T_('You have sent too much data (too many large files?) for the server to process (%s sent / %s maximum). Please try again by sending less data/files at a time.'), bytesreadable( $_SERVER['CONTENT_LENGTH'] ), bytesreadable( $post_max_size ) ) );
		header_redirect( $_SERVER['HTTP_REFERER'] );
		exit(0); // Already exited here
	}
}


/**
 * Prevent email header injection.
 */
function mail_sanitize_header_string( $header_str, $close_brace = false )
{
	// Prevent injection! (remove everything after (and including) \n or \r)
	$header_str = preg_replace( '~(\r|\n).*$~s', '', trim($header_str) );

	if( $close_brace && strpos( $header_str, '<' ) !== false && strpos( $header_str, '>' ) === false )
	{ // We have probably stripped the '>' at the end!
		$header_str .= '>';
	}

	return $header_str;
}

/**
 * Encode to RFC 1342 "Representation of Non-ASCII Text in Internet Message Headers"
 *
 * @param string
 * @param string 'Q' for Quoted printable, 'B' for base64
 */
function mail_encode_header_string( $header_str, $mode = 'Q' )
{
	global $evo_charset;

	/* mbstring way  (did not work for Alex RU)
	if( function_exists('mb_encode_mimeheader') )
	{ // encode subject
		$orig = mb_internal_encoding();
		mb_internal_encoding('utf-8');
		$r = mb_encode_mimeheader( $header_str, 'utf-8', $mode );
		mb_internal_encoding($orig);
		return $r;
	}
	*/

	if( preg_match( '~[^a-z0-9!*+\-/ ]~i', $header_str ) )
	{ // If the string actually needs some encoding
		if( $mode == 'Q' )
		{ // Quoted printable is best for reading with old/text terminal mail reading/debugging stuff:
			$header_str = preg_replace_callback( '#[^a-z0-9!*+\-/ ]#i', 'mail_encode_header_string_callback', $header_str );
			$header_str = str_replace( ' ', '_', $header_str );
			$header_str = '=?'.$evo_charset.'?Q?'.$header_str.'?=';
		}
		else
		{ // Base 64 -- Alex RU way:
			$header_str = '=?'.$evo_charset.'?B?'.base64_encode( $header_str ).'?=';
		}
	}

	return $header_str;
}


/**
 * Callback function for mail header encoding
 *
 * @param array Matches
 * @return string
 */
function mail_encode_header_string_callback( $matches )
{
	return sprintf( '=%02x', ord( stripslashes( $matches[0] ) ) );
}


/**
 * Get setting's value from General or User's settings
 *
 * @param integer User ID
 * @param string Setting ( email | name )
 * @return string Setting's value
 */
function user_get_notification_sender( $user_ID, $setting )
{
	global $Settings;

	$setting_name = 'notification_sender_'.$setting;

	if( empty( $user_ID ) )
	{	// Get value from general settings
		return $Settings->get( $setting_name );
	}

	$UserCache = & get_UserCache();
	if( $User = & $UserCache->get_by_ID( $user_ID ) )
	{
		if( $User->check_status( 'is_validated' ) )
		{	// User is Activated or Autoactivated or Manually activated
			global $UserSettings;
			if( $UserSettings->get( $setting_name, $user_ID ) == '' )
			{	// The user's setting is not defined yet
				// Update the user's setting from general setting
				$UserSettings->set( $setting_name, $Settings->get( $setting_name ), $user_ID );
				$UserSettings->dbupdate();
			}
			else
			{	// User has a defined setting; Use this
				return $UserSettings->get( $setting_name, $user_ID );
			}
		}
	}

	return $Settings->get( $setting_name );
}


/**
 * Get limit for email sending for currently executing cron job
 *
 * @return integer 0 means either unlimited email sending or no cron job is executing at the moment
 */
function get_cron_job_emails_limit()
{
	global $executing_cron_task_key;

	if( isset( $executing_cron_task_key ) )
	{	// If any cron job is really executing now:
		global $Settings;
		return intval( $Settings->get( 'cjob_maxemail_'.$executing_cron_task_key ) );
	}

	// No currently executing cron job:
	return 0;
}


/**
 * Check if current executing cron job should be stopped because of email sending limit
 *
 * 1. THIS MUST BE CALLED ONLY FROM INSIDE THE MAIN LOOP of the cronjob (*.job.php), not from within a generic function.
 * 2. THE GOAL is to NOT overload the sending  mail server by creating a PAUSE afer (approximately) the pax number of email to send in a chunk
 * 2B. The goal is that the next execution of the cron job will continue sending emails where the current job has left off.
 * 2C. The goal is NOT to miss sending notifications by generating send errors!
 * 3. Counter increase must be done SEPARATELY when an email is sent.
 *
 * @return boolean TRUE if cron job execution can continue, FALSE - when max emails was reached
 */
function check_cron_job_emails_limit()
{
	// Get max number of emails for current cron job:
	$current_cron_job_emails_limit = get_cron_job_emails_limit();

	if( $current_cron_job_emails_limit > 0 )
	{	// If current cron job has a limit for sending of emails:
		global $executing_cron_task_emails_count;
		if( $executing_cron_task_emails_count >= $current_cron_job_emails_limit )
		{	// The limit was reached:
			global $executing_cron_task_emails_reached, $mail_log_message;
			$mail_log_message = 'Stopping execution because max number of emails ('.$current_cron_job_emails_limit.') has been sent.';
			if( empty( $executing_cron_task_emails_reached ) )
			{	// Append warning to cron log only if max emails is not reached yet:
				cron_log_append( $mail_log_message."\n", 'warning' );
				// Set flag to don't show the above warning twice:
				$executing_cron_task_emails_reached = true;
			}
			// Stop current job execution:
			return false;
		}
	}

	// Allow to continue current cron job execution:
	return true;
}


/**
 * Get additional error message on failed mail sending
 *
 * @return string
 */
function get_send_mail_error()
{
	global $Settings;

	if( $Settings->get( 'email_service' ) == 'smtp' && ! $Settings->get( 'force_email_sending' ) )
	{	// Only SMTP is used:
		return T_('Recipient email address does not seem to work.');
	}
	else
	{	// PHP "mail" function is used by default or it was forced after failed SMTP sending:
		return T_('Possible reason: the PHP mail() function may have been disabled on the server.');
	}
}


/**
 * Sends an email, wrapping PHP's mail() function.
 * ALL emails sent by b2evolution must be sent through this function (for consistency and for logging)
 *
 * {@link $current_locale} will be used to set the charset.
 *
 * Note: we use a single \n as line ending, though it does not comply to {@link http://www.faqs.org/rfcs/rfc2822 RFC2822}, but seems to be safer,
 * because some mail transfer agents replace \n by \r\n automatically.
 *
 * @todo Unit testing with "nice addresses" This gets broken over and over again.
 *
 * @param string Recipient email address.
 * @param string Recipient name.
 * @param string Subject of the mail
 * @param string|array The message text OR Array: 'charset', 'full', 'html', 'text'
 * @param string From address, being added to headers (we'll prevent injections); see {@link http://securephp.damonkohler.com/index.php/Email_Injection}.
 *               Defaults to {@link GeneralSettings::get('notification_sender_email') } if NULL.
 * @param string From name.
 * @param array Additional headers ( headername => value ). Take care of injection!
 * @param integer User ID
 * @param integer Email Campaign ID
 * @param integer Automation ID
 * @return boolean True if mail could be sent (not necessarily delivered!), false if not - (return value of {@link mail()})
 */
function send_mail( $to, $to_name, $subject, $message, $from = NULL, $from_name = NULL, $headers = array(), $user_ID = NULL, $email_campaign_ID = NULL, $automation_ID = NULL )
{
	global $servertimenow, $email_send_simulate_only, $email_send_allow_php_mail;

	/**
	 * @var string|NULL This global var stores a last mail log message
	 */
	global $mail_log_message;
	$mail_log_message = NULL;

	global $debug, $app_name, $app_version, $current_locale, $current_charset, $evo_charset, $locales, $Debuglog, $Settings, $demo_mode, $mail_log_insert_ID;

	$message_data = $message;
	if( is_array( $message_data ) && isset( $message_data['full'] ) )
	{ // If content is multipart
		$message = $message_data['full'];
	}
	elseif( is_string( $message_data ) )
	{ // Convert $message_data to array
		$message_data = array( 'full' => $message );
	}

	// Replace secret content in the mail logs message body
	$message = preg_replace( '~\$secret_content_start\$.*\$secret_content_end\$~', '***secret-content-removed***', $message );
	// Remove secret content marks from the message
	$message_data = str_replace( array( '$secret_content_start$', '$secret_content_end$' ), '', $message_data );

	// Memorize email address
	$to_email_address = $to;

	$NL = "\r\n";

	if( $demo_mode )
	{ // Debug mode restriction: Sending email in demo mode is not allowed
		return false;
	}

	if( !is_array( $headers ) )
	{ // Make sure $headers is an array
		$headers = array( $headers );
	}

	if( empty( $from ) )
	{
		$from = user_get_notification_sender( $user_ID, 'email' );
	}

	if( empty( $from_name ) )
	{
		$from_name = user_get_notification_sender( $user_ID, 'name' );
	}

	// Pass these data for SMTP mailer
	$message_data['to_email'] = $to;
	$message_data['to_name'] = empty( $to_name ) ? NULL : $to_name;
	$message_data['from_email'] = $from;
	$message_data['from_name'] = empty( $from_name ) ? NULL : $from_name;

	$return_path = $Settings->get( 'notification_return_path' );

	// Add real name into $from...
	if( ! is_windows() )
	{	// fplanque: Windows XP, Apache 1.3, PHP 4.4, MS SMTP : will not accept "nice" addresses.
		if( !empty( $to_name ) )
		{
			$to = '"'.mail_encode_header_string($to_name).'" <'.$to.'>';
		}
		if( !empty( $from_name ) )
		{
			$from = '"'.mail_encode_header_string($from_name).'" <'.$from.'>';
		}
	}

	$from = mail_sanitize_header_string( $from, true );
	// From has to go into headers
	$headers['From'] = $from;
	if( !empty( $return_path ) )
	{	// Set a return path
		$headers['Return-Path'] = $return_path;
	}

	// echo 'sending email to: ['.htmlspecialchars($to).'] from ['.htmlspecialchars($from).']';

	$clear_subject = $subject;
	$subject = mail_encode_header_string($subject);

	$message = str_replace( array( "\r\n", "\r" ), $NL, $message );

	if( !isset( $headers['Content-Type'] ) )
	{	// Specify charset and content-type of email
		$headers['Content-Type'] = 'text/plain; charset='.$current_charset;
	}
	$headers['MIME-Version'] = '1.0';

	$headers['Date'] = gmdate( 'r', $servertimenow );

	// ADDITIONAL HEADERS:
	$headers['X-Mailer'] = $app_name.' '.$app_version.' - PHP/'.phpversion();
	$ip_list = implode( ',', get_ip_list() );
	if( !empty( $ip_list ) )
	{ // Add X-Remote_Addr param only if its value is not empty
		$headers['X-Remote-Addr'] = $ip_list;
	}

	// COMPACT HEADERS:
	$headerstring = get_mail_headers( $headers, $NL );

	// Create initial email log with empty message
	$email_key = generate_random_key();
	mail_log( $user_ID, $to_email_address, $clear_subject, NULL, $headerstring, 'ready_to_send', $email_key, $email_campaign_ID, $automation_ID );

	// Replace tracking code placeholders
	$message = str_replace( array( '$email_key$', '$mail_log_ID$' ), array( $email_key, $mail_log_insert_ID ), $message );
	$message_data = str_replace( array( '$email_key$', '$mail_log_ID$' ), array( $email_key, $mail_log_insert_ID ), $message_data );

	// Set an additional parameter for the return path:
	switch( $Settings->get( 'sendmail_params' ) )
	{
		case 'return':
			$sendmail_params = '-r $return-address$';
			break;
		case 'from':
			$sendmail_params = '-f $return-address$';
			break;
		case 'custom':
			$sendmail_params = $Settings->get( 'sendmail_params_custom' );
			break;
	}
	if( ! empty( $sendmail_params ) )
	{
		$additional_parameters = str_replace(
			array( '$from-address$', '$return-address$' ),
			array( $message_data['from_email'], ( empty( $return_path ) ? $message_data['from_email'] : $return_path ) ),
			$sendmail_params );
	}
	else
	{
		$additional_parameters = '';
	}

	// Remove email key markers from message that will be sent to actual email
	$message_data = str_replace( array( '$email_key_start$', '$email_key_end$' ), '', $message_data );

	if( mail_is_blocked( $to_email_address ) )
	{ // Check if the email address is blocked
		$mail_log_message = 'Sending mail to "'.$to_email_address.'" FAILED, because this email is marked with spam or permanent errors.';
		$Debuglog->add( htmlspecialchars( $mail_log_message ), 'error' );

		update_mail_log( $mail_log_insert_ID, 'blocked', $message );

		return false;
	}

	// Simulate email sending when:
	//  - this is forced by config
	//  - php mail sending is disabled by config and SMTP sending is not enabled
	$simulate_email_sending = $email_send_simulate_only || ( ! $email_send_allow_php_mail && ! $Settings->get( 'smtp_enabled' ) );

	if( $simulate_email_sending )
	{	// The email sending is turned on simulation mode, Don't send a real message:
		$send_mail_result = true;
	}
	else
	{	// Send email message on real mode:
		try
		{	// Try to send:
			$send_mail_result = evo_mail( $to, $subject, $message_data, $headers, $additional_parameters );
		}
		catch( Exception $ex )
		{	// Unexpected error:
			$send_mail_result = false;
			// Log the caught error:
			$mail_log_message = $ex->getMessage();
			// Insert a returned email's data into DB:
			load_funcs( 'cron/model/_decode_returned_emails.funcs.php' );
			$content = dre_limit_by_terminators( $mail_log_message );
			$email_data = dre_get_email_data( $content, $mail_log_message, 'Empty headers' );
			if( empty( $email_data['address'] ) )
			{	// Use current email address if no email address is detected in the error message:
				$email_data['address'] = $to_email_address;
			}
			dre_insert_returned_email( $email_data );
		}
	}

	if( get_cron_job_emails_limit() > 0 )
	{	// Increase global counter if currently executing cron jobs has a limit for mail sending:
		global $executing_cron_task_emails_count;
		$executing_cron_task_emails_count++;
	}


	if( ! $send_mail_result )
	{	// The message has not been sent successfully
		$mail_log_message = 'Sending mail from "'.$from.'" to "'.$to.'", Subject "'.$subject.'" FAILED'.( $mail_log_message === NULL ? '' : ', Error: '.$mail_log_message ).'.';
		update_mail_log( $mail_log_insert_ID, 'error', $message );
		if( $debug > 1 )
		{ // We agree to die for debugging...
			debug_die( htmlspecialchars( $mail_log_message ) );
		}
		else
		{ // Soft debugging only....
			$Debuglog->add( htmlspecialchars( $mail_log_message ), 'error' );
			return false;
		}
	}

	$mail_log_message = 'Sent mail from "'.$from.'" to "'.$to.'", Subject "'.$subject.'".';
	$Debuglog->add( htmlspecialchars( $mail_log_message ) );

	update_mail_log( $mail_log_insert_ID, ( $simulate_email_sending ? 'simulated' : 'ok' ), $message );

	return true;
}


/**
 * Sends an email to User
 *
 * @param integer Recipient ID.
 * @param string Subject of the mail
 * @param string Email template name
 * @param array Email template params
 * @param boolean Force to send this email even if the user is not activated. By default not activated user won't get emails.
 *                Pasword reset, and account activation emails must be always forced.
 * @param array Additional headers ( headername => value ). Take care of injection!
 * @param string Use this param if you want use different email address instead of $User->email
 * @return boolean True if mail could be sent (not necessarily delivered!), false if not - (return value of {@link mail()})
 */
function send_mail_to_User( $user_ID, $subject, $template_name, $template_params = array(), $force_on_non_activated = false, $headers = array(), $force_email_address = '' )
{
	global $UserSettings, $Settings, $current_charset;

	/**
	 * @var string|NULL This global var stores a last mail log message
	 */
	global $mail_log_message;
	$mail_log_message = NULL;

	$UserCache = & get_UserCache();
	if( $User = $UserCache->get_by_ID( $user_ID ) )
	{
		if( !$User->check_status( 'can_receive_any_message' ) )
		{ // user status doesn't allow to receive nor emails nor private messages
			$mail_log_message = 'Sending mail to User #'.$User->ID.'('.$User->get( 'login' ).') is FAILED, because user status "'.$User->get( 'status' ).'" doesn\'t allow to receive any message.';
			return false;
		}

		if( !( $User->check_status( 'is_validated' ) || $force_on_non_activated ) )
		{ // user is not activated and non activated users should not receive emails, unless force_on_non_activated is turned on
			$mail_log_message = 'Sending mail to User #'.$User->ID.'('.$User->get( 'login' ).') is FAILED, because user is not validated with status "'.$User->get( 'status' ).'".';
			return false;
		}

		// Check if a new email to User with the corrensponding email type is allowed
		switch( $template_name )
		{
			case 'account_activate':
			case 'account_delete_warning':
				if( $Settings->get( 'validation_process' ) == 'easy' && !$template_params['is_reminder'] )
				{ // this is not a notification email
					break;
				}
			// Check a day notification limit for user settings "Notify me by email whenever":
			case 'private_message_new':
				// 'notify_messages' - "I receive a private message."
			case 'private_messages_unread_reminder':
				// 'notify_unread_messages' - "I have unread private messages for more than X seconds."(X = $Settings->get( 'unread_message_reminder_threshold' ))
			case 'comment_new':
				// 'notify_comment_mentioned' - "I have been mentioned on a comment.",
				// 'notify_published_comments' - "a comment is published on one of my posts.",
				// 'notify_comment_moderation' - "a comment is posted and I have permissions to moderate it.",
				// 'notify_edit_cmt_moderation' - "a comment is modified and I have permissions to moderate it.",
				// 'notify_meta_comment_mentioned' - "I have been mentioned on an internal comment.",
				// 'notify_meta_comments' - "an internal comment is posted.".
			case 'comment_spam':
				// 'notify_spam_cmt_moderation' - "a comment is reported as spam and I have permissions to moderate it."
			case 'comments_unmoderated_reminder':
				// 'send_cmt_moderation_reminder' - "comments are awaiting moderation for more than X seconds."(X = $Settings->get( 'comment_moderation_reminder_threshold' ))
			case 'post_new':
				// 'notify_post_mentioned' - "I have been mentioned on a post.",
				// 'notify_post_moderation' - "a post is created and I have permissions to moderate it."
				// 'notify_edit_pst_moderation' - "someone proposed a change on a post and I have permissions to moderate it."
			case 'post_proposed_change':
				// 'notify_post_proposed' - "someone proposed a change on a post and I have permissions to moderate it."
			case 'post_assignment':
				// 'notify_post_assignment' - "a post was assigned to me."
			case 'posts_unmoderated_reminder':
				// 'send_pst_moderation_reminder' - "posts are awaiting moderation for more than X seconds."(X = $Settings->get( 'post_moderation_reminder_threshold' ))
			case 'posts_stale_alert':
				// 'send_pst_stale_alert' - "there are stale posts and I have permission to moderate them."
			case 'account_activate':
			case 'account_delete_warning':
				// 'send_activation_reminder' - "my account was deactivated or is not activated for more than X seconds."(X - $Settings->get( 'activate_account_reminder_threshold' ))
			case 'account_inactive':
				// 'send_inactive_reminder' - "my account has been inactive for more than X months."(X - $Settings->get( 'inactive_account_reminder_threshold' ))
			case 'account_new':
				// 'notify_new_user_registration' - "a new user has registered."
			case 'account_activated':
				// 'notify_activated_account' - "an account was activated."
			case 'account_closed':
				// 'notify_closed_account' - "an account was closed."
			case 'account_reported':
				// 'notify_reported_account' - "an account was reported."
			case 'account_changed':
				// 'notify_changed_account' - "an account was changed."
			case 'scheduled_task_error_report':
				// 'notify_cronjob_error' - "a scheduled task ends with an error or timeout."
			case 'list_new_subscriber':
				// 'notify_list_new_subscriber' - "one of my Lists gets a new subscriber."
			case 'list_lost_subscriber':
				// 'notify_list_lost_subscriber' - "one of my Lists loses a subscriber."
			case 'automation_owner_notification':
				// 'notify_automation_owner' - "one of my automations wants to notify me."
				$email_limit_setting = 'notification_email_limit';
				$email_counter_setting = 'last_notification_email';
				if( !check_allow_new_email( $email_limit_setting, $email_counter_setting, $User->ID ) )
				{ // more notification email is not allowed today
					$mail_log_message = 'Sending mail to User #'.$User->ID.'('.$User->get( 'login' ).') is FAILED, because user is already limited to receive more notifications for TODAY.';
					return false;
				}
				break;
			case 'newsletter':
				// this is a newsletter email
				$email_limit_setting = 'newsletter_limit';
				$email_counter_setting = 'last_newsletter';
				if( !check_allow_new_email( $email_limit_setting, $email_counter_setting, $User->ID ) )
				{ // more newsletter email is not allowed today
					$mail_log_message = 'Sending mail to User #'.$User->ID.'('.$User->get( 'login' ).') is FAILED, because user is already limited to receive more newsletters for TODAY.';
					return false;
				}
				break;
			case 'newsletter_test':
				// this is a newsletter email, used to send test email by current admin
				$template_name = 'newsletter';
				break;
		}

		// Update notification sender's info from General settings
		$User->update_sender();

		switch( $UserSettings->get( 'email_format', $User->ID ) )
		{	// Set Content-Type from user's setting "Email format"
			case 'auto':
				$template_params['boundary'] = 'b2evo-'.md5( rand() );
				$headers['Content-Type'] = 'multipart/mixed; boundary="'.$template_params['boundary'].'"';
				break;
			case 'html':
				$headers['Content-Type'] = 'text/html; charset='.$current_charset;
				break;
			case 'text':
				$headers['Content-Type'] = 'text/plain; charset='.$current_charset;
				break;
		}

		if( ! isset( $template_params['recipient_User'] ) )
		{ // Set recipient User, it should be defined for each template because of email footer
			$template_params['recipient_User'] = $User;
		}

		// Pass all email headers to template because they may be used there, e.g. in email footer template:
		$template_params['email_headers'] = $headers;

		// Get a message text from template file
		$message = mail_template( $template_name, $UserSettings->get( 'email_format', $User->ID ), $template_params, $User );

		// Autoinsert user's data
		$subject = mail_autoinsert_user_data( $subject, $User, 'text', NULL, NULL, $template_params );

		// erhsatingin > moved to mail_template()
		//$message = mail_autoinsert_user_data( $message, $User );

		$to_email = !empty( $force_email_address ) ? $force_email_address : $User->email;

		// Params for email log:
		$email_campaign_ID = empty( $template_params['ecmp_ID'] ) ? NULL : $template_params['ecmp_ID'];
		$automation_ID = empty( $template_params['autm_ID'] ) ? NULL : $template_params['autm_ID'];

		if( send_mail( $to_email, NULL, $subject, $message, NULL, NULL, $headers, $user_ID, $email_campaign_ID, $automation_ID ) )
		{ // email was sent, update last email settings;
			if( isset( $email_limit_setting, $email_counter_setting ) )
			{ // User Settings(email counters) need to be updated
				update_user_email_counter( $email_limit_setting, $email_counter_setting, $user_ID );
			}
			return true;
		}
	}

	// No user or email could not be sent
	return false;
}


/**
 * Sends an email to anonymous User
 *
 * @param integer Recipient ID.
 * @param string Subject of the mail
 * @param string Email template name
 * @param array Email template params
 * @param boolean Force to send this email even if the user is not activated. By default not activated user won't get emails.
 *                Pasword reset, and account activation emails must be always forced.
 * @param array Additional headers ( headername => value ). Take care of injection!
 * @param string Use this param if you want use different email address instead of $User->email
 * @return boolean True if mail could be sent (not necessarily delivered!), false if not - (return value of {@link mail()})
 */
function send_mail_to_anonymous_user( $user_email, $user_name, $subject, $template_name, $template_params = array(), $force_on_non_activated = false, $headers = array(), $force_email_address = '' )
{
	/**
	 * @var string|NULL This global var stores a last mail log message
	 */
	global $mail_log_message;
	$mail_log_message = NULL;

	// Check if a new email to anonymous user with the corrensponding email type is allowed:
	switch( $template_name )
	{
		case 'comment_new':
			// Notify anonymous user of replies:
			if( isset( $template_params['comment_ID'] ) && ! check_allow_new_anon_email( $template_params['comment_ID'] ) )
			{	// more notification email is not allowed today:
				$mail_log_message = 'Sending mail to anonymous user #'.$user_email.'('.$user_name.') is FAILED, because user is already limited to receive more notifications for TODAY.';
				return false;
			}
			break;
	}

	$template_params['boundary'] = 'b2evo-'.md5( rand() );
	$headers['Content-Type'] = 'multipart/mixed; boundary="'.$template_params['boundary'].'"';

	if( ! isset( $template_params['anonymous_recipient_name'] ) )
	{	// Set recipient User, it should be defined for each template because of email footer:
		$template_params['anonymous_recipient_name'] = $user_name;
	}

	// Get a message text from template file:
	$message = mail_template( $template_name, 'auto', $template_params );

	// Autoinsert user's data:
	$subject = mail_autoinsert_user_data( $subject, NULL, 'text', $user_email, $user_name, $template_params );

	// Params for email log:
	$email_campaign_ID = empty( $template_params['ecmp_ID'] ) ? NULL : $template_params['ecmp_ID'];
	$automation_ID = empty( $template_params['autm_ID'] ) ? NULL : $template_params['autm_ID'];

	if( send_mail( $user_email, $user_name, $subject, $message, NULL, NULL, $headers, NULL, $email_campaign_ID, $automation_ID ) )
	{	// Email was sent:
		if( isset( $template_params['comment_ID'] ) )
		{	// Anonymous user settings(email counters) need to be updated:
			update_anon_user_email_counter( $template_params['comment_ID'] );
		}
		return true;
	}

	// No user or email could not be sent:
	return false;
}


/**
 * Autoinsert user's data into subject or message of the email
 *
 * @param string Text
 * @param object User
 * @param string Format: 'html', 'text'
 * @param string Email of anonymous user
 * @param string Name of anonymous user
 * @param array Email template params
 * @return string Text
*/
function mail_autoinsert_user_data( $text, $User = NULL, $format = 'text', $user_email = NULL, $user_name = NULL, $params = array() )
{
	if( ! $User && ! ( $user_email || $user_email ) )
	{	// No user data:
		return $text;
	}

	if( $User )
	{	// Get data of registered User:
		global $UserSettings;

		if( $format == 'html' )
		{
			$username = $User->get_colored_login( array(
					'mask'        => '$avatar$ $login$',
					'login_text'  => 'name',
					'use_style'   => true,
					'extra_class' => 'normal_weight',
					'protocol'    => 'http:',
				) );

			$user_login = $User->get_colored_login( array(
					'mask'        => '$avatar$ $login$',
					'use_style'   => true,
					'protocol'    => 'http:',
				) );
		}
		else
		{
			$username = $User->get_username();
			$user_login = $User->login;
		}

		$firstname = $User->get( 'firstname' );
		$lastname = $User->get( 'lastname' );
		$firstname_and_login = empty( $firstname ) ? $user_login : $firstname.' ('.$user_login.')';
		$firstname_or_login = empty( $firstname ) ? $user_login : $firstname;
		$user_email = $User->email;
		$user_ID = $User->ID;
		$unsubscribe_key = '$secret_content_start$'.md5( $User->ID.$User->unsubscribe_key ).'$secret_content_end$';
		if( isset( $params['template_mode'] ) && $params['template_mode'] == 'preview' )
		{	// Don't use real value of reminder key on preview email template, e.g. on review message of email campaign:
			$reminder_key = '$reminder_key$';
		}
		else
		{	// Use real value of reminder key:
			$reminder_key = $UserSettings->get( 'last_activation_reminder_key', $user_ID );
			if( empty( $reminder_key ) && strpos( $text, '$reminder_key$' ) !== false )
			{	// If reminder key was not generated yet we need create it in order user can active account even if did request the activation email yet:
				$reminder_key = generate_random_key( 32 );
				$UserSettings->set( 'last_activation_reminder_key', $reminder_key, $user_ID );
				$UserSettings->dbupdate();
			}
		}
		$newsletter_ID = isset( $params['enlt_ID'] ) ? $params['enlt_ID'] : '';
	}
	else
	{	// Get data of anonymous user:
		$username = $user_name;
		$user_login = $user_name;
		$firstname = $user_name;
		$lastname = $user_name;
		$firstname_and_login = $user_name;
		$firstname_or_login = $user_name;
		$user_email = $user_email;
		$user_ID = '';
		$unsubscribe_key = '';
		$reminder_key = '';
		$newsletter_ID = '';
	}

	$rpls_from = array( '$login$', '$username$', '$firstname$', '$lastname$', '$firstname_and_login$', '$firstname_or_login$', '$email$', '$user_ID$', '$unsubscribe_key$', '$reminder_key$', '$newsletter_ID$' );
	$rpls_to = array( $user_login, $username, $firstname, $lastname, $firstname_and_login, $firstname_or_login, $user_email, $user_ID, $unsubscribe_key, $reminder_key, $newsletter_ID );

	return str_replace( $rpls_from, $rpls_to, $text );
}


/**
 * Get a mail message text by template name
 *
 * @param string Template name
 * @param string Email format ( auto | html | text )
 * @param array Params
 * @param object User
 * @return string|array Mail message OR Array of the email contents when message is multipart content
 */
function mail_template( $template_name, $format = 'auto', $params = array(), $User = NULL )
{
	global $current_charset;
	global $track_email_image_load, $track_email_click_html, $track_email_click_plain_text;

	$params = array_merge( array(
			'add_email_tracking' => true,
			'template_parts' => array(
					'header' => NULL,
					'footer' => NULL
				),
			'default_template_tag' => NULL
		), $params );

	// Set this param to know current template name inside code of email templates like header and footer:
	$params['template_name'] = $template_name;

	if( !empty( $params['locale'] ) )
	{ // Switch to locale for current email template
		locale_temp_switch( $params['locale'] );
	}

	// Set extension of template
	$template_exts = array();
	switch( $format )
	{
		case 'auto':
			// $template_exts['non-mime'] = '.txt.php'; // The area that is ignored by MIME-compliant clients
			$template_exts['text'] = '.txt.php';
			$template_exts['html'] = '.html.php';
			$boundary = $params['boundary'];
			$boundary_alt = 'b2evo-alt-'.md5( rand() );
			$template_headers = array(
					'text' => 'Content-Type: text/plain; charset='.$current_charset,
					'html' => 'Content-Type: text/html; charset='.$current_charset,
				);
			// Store all contents in this array for multipart message
			$template_contents = array(
					'charset' => $current_charset, // Charset for email message
					'full' => '', // Full content with html and plain
					'html' => '', // HTML
					'text' => '', // Plain text
				);
			break;

		case 'html':
			$template_exts['html'] = '.html.php';
			break;

		case 'text':
			$template_exts['text'] = '.txt.php';
			break;
	}

	$template_message = '';

	if( isset( $boundary, $boundary_alt ) )
	{ // Start new boundary content
		$template_message .= "\n".'--'.$boundary."\n";
		$template_message .= 'Content-Type: multipart/alternative; boundary="'.$boundary_alt.'"'."\n\n";
	}

	foreach( $template_exts as $format => $ext )
	{
		$formated_message = '';

		if( isset( $boundary, $boundary_alt ) && $format != 'non-mime' )
		{ // Start new boundary alt content
			$template_message .= "\n".'--'.$boundary_alt."\n";
		}

		if( isset( $template_headers[ $format ] ) )
		{ // Header data for each content
			$template_message .= $template_headers[ $format ]."\n\n";
		}

		// Get mail template
		ob_start();
		emailskin_include( $template_name.$ext, $params );
		$formated_message .= ob_get_clean();

		if( ! empty( $User ) )
		{ // Replace $login$ with gender colored link + icon in HTML format, and with simple login text in PLAIN TEXT format
			$formated_message = mail_autoinsert_user_data( $formated_message, $User, $format, NULL, NULL, $params );
		}
		elseif( ! empty( $params['anonymous_recipient_name'] ) )
		{	// Replace anonymous name:
			$formated_message = str_replace( '$name$', $params['anonymous_recipient_name'], $formated_message );
			if( ! empty( $params['anonymous_unsubscribe_key'] ) )
			{	// Replace anonymous unsubscribe key:
				$formated_message = str_replace( '$unsubscribe_key$', $params['anonymous_unsubscribe_key'], $formated_message );
			}
		}

		if( $params['add_email_tracking'] )
		{
			$tracking_params = array(
					'content_type' => $format,
					'image_load' => isset( $track_email_image_load ) ? $track_email_image_load : true,
					'link_click_html' => isset( $track_email_click_html ) ? $track_email_click_html : true ,
					'link_click_text' => isset( $track_email_click_plain_text ) ? $track_email_click_plain_text : true,
					'template_parts' => $params['template_parts'],
					'default_template_tag' => $params['default_template_tag']
				);
			$formated_message = add_email_tracking( $formated_message, '$mail_log_ID$', '$email_key$', $tracking_params );
		}

		// Remove template parts markers
		$template_part_markers = array();
		foreach( $params['template_parts'] as $part => $row )
		{
			$template_part_markers[] = '$template-content-'.$part.'-start$';
			$template_part_markers[] = '$template-content-'.$part.'-end$';
		}
		$formated_message = str_replace( $template_part_markers, '', $formated_message  );
		$template_message .= $formated_message;

		if( isset( $template_contents ) )
		{ // Multipart content
			$template_contents[ $format ] = $formated_message;
		}
	}

	if( isset( $boundary, $boundary_alt ) )
	{ // End all boundary contents
		$template_message .= "\n".'--'.$boundary_alt.'--'."\n";
		$template_message .= "\n".'--'.$boundary.'--'."\n";
	}

	if( !empty( $params['locale'] ) )
	{ // Restore previous locale
		locale_restore_previous();
	}

	if( isset( $template_contents ) )
	{ // Return array for multipart content
		$template_contents['full'] = $template_message;
		return $template_contents;
	}
	else
	{ // Return string if email message contains one content (html or text)
		return $template_message;
	}
}


/**
 * Include email template from folder /skins_email/custom/ or /skins_email/
 *
 * @param string Template name
 * @param array Params
 */
function emailskin_include( $template_name, $params = array(), $template_part = NULL )
{
	global $emailskins_path, $rsc_url;

	/**
	* @var Log
	*/
	global $Debuglog;
	global $Timer;

	$timer_name = 'emailskin_include('.$template_name.')';
	$Timer->resume( $timer_name );

	$is_customized = false;

	// Try to include custom template firstly
	$template_path = $emailskins_path.'custom/'.$template_name;
	if( file_exists( $template_path ) )
	{ // Include custom template file if it exists
		$Debuglog->add( 'emailskin_include: '.rel_path_to_base( $template_path ), 'skins' );
		if( ! empty( $template_part ) )
		{
			echo '$template-content-'.$template_part.'-start$';
		}
		require $template_path;
		if( ! empty( $template_part ) )
		{
			echo '$template-content-'.$template_part.'-end$';
		}
		// This template is customized, Don't include standard template
		$is_customized = true;
	}

	if( !$is_customized )
	{ // Try to include standard template only if custom template doesn't exist
		$template_path = $emailskins_path.$template_name;
		if( file_exists( $template_path ) )
		{ // Include standard template file if it exists
			$Debuglog->add( 'emailskin_include: '.rel_path_to_base( $template_path ), 'skins' );
			if( ! empty( $template_part ) )
			{
				echo '$template-content-'.$template_part.'-start$';
			}
			require $template_path;
			if( ! empty( $template_part ) )
			{
				echo '$template-content-'.$template_part.'-end$';
			}
		}
	}

	$Timer->pause( $timer_name );
}


/**
 * Get attribute "style" by class name for element in email templates
 *
 * @param string Class name
 * @param boolean TRUE to return string as ' style="css_properties"' otherwise only 'css_properties'
 * @return string
 */
function emailskin_style( $class, $set_attr_name = true )
{
	global $emailskins_styles;

	if( ! is_array( $emailskins_styles ) )
	{ // Load email styles only first time
		global $emailskins_path;
		require_once $emailskins_path.'_email_style.php';

		foreach( $emailskins_styles as $classes => $styles )
		{
			if( strpos( $classes, ',' ) !== false )
			{ // This style is used for several classes
				unset( $emailskins_styles[ $classes ] );
				$classes = explode( ',', $classes );
				foreach( $classes as $class_name )
				{
					$class_name = trim( $class_name );
					if( isset( $emailskins_styles[ $class_name ] ) )
					{
						$emailskins_styles[ $class_name ] .= $styles;
					}
					else
					{
						$emailskins_styles[ $class_name ] = $styles;
					}
				}
			}
		}
	}

	if( strpos( $class, '+' ) !== false )
	{ // Several classes should be applied this
		$classes = explode( '+', $class );
		$style = '';
		foreach( $classes as $c => $class )
		{
			$style .= emailskin_style( $class, false );
		}

		return empty( $style ) ? '' : ( $set_attr_name ? ' style="'.$style.'"' : $style );
	}
	elseif( isset( $emailskins_styles[ $class ] ) )
	{ // One class
		$style = trim( str_replace( array( "\r", "\n", "\t" ), '', $emailskins_styles[ $class ] ) );
		$style = str_replace( ': ', ':', $style );

		return $set_attr_name ? ' style="'.$style.'"' : $style;
	}

	return '';
}


/**
 * If first parameter evaluates to true printf() gets called using the first parameter
 * as args and the second parameter as print-pattern
 *
 * @param mixed variable to test and output if it's true or $disp_none is given
 * @param string printf-pattern to use (%s gets replaced by $var)
 * @param string printf-pattern to use, if $var is numeric and > 1 (%s gets replaced by $var)
 * @param string printf-pattern to use if $var evaluates to false (%s gets replaced by $var)
 */
function disp_cond( $var, $disp_one, $disp_more = NULL, $disp_none = NULL )
{
	if( is_numeric($var) && $var > 1 )
	{
		printf( ( $disp_more === NULL ? $disp_one : $disp_more ), $var );
		return true;
	}
	elseif( $var )
	{
		printf( $disp_one, $var );
		return true;
	}
	else
	{
		if( $disp_none !== NULL )
		{
			printf( $disp_none, $var );
			return false;
		}
	}
}


/**
 * Create IMG tag for an action icon.
 *
 * @param string TITLE text (IMG and A link)
 * @param string icon code for {@link get_icon()}
 * @param string URL where the icon gets linked to (empty to not wrap the icon in a link)
 * @param string word to be displayed after icon (if no icon gets displayed, $title will be used instead!)
 * @param integer 1-5: weight of the icon. The icon will be displayed only if its weight is >= than the user setting threshold.
 *                     Use 5, if it's a required icon - all others could get disabled by the user. (Default: 4)
 * @param integer 1-5: weight of the word. The word will be displayed only if its weight is >= than the user setting threshold.
 *                     (Default: 1)
 * @param array Additional attributes to the A tag. The values must be properly encoded for html output (e.g. quotes).
 *        It may also contain these params:
 *         - 'use_js_popup': if true, the link gets opened as JS popup. You must also pass an "id" attribute for this!
 *         - 'use_js_size': use this to override the default popup size ("500, 400")
 *         - 'class': defaults to 'action_icon', if not set; use "" to not use it
 * @param array Attributes for the icon
 * @return string The generated action icon link.
 */
function action_icon( $title, $icon, $url, $word = NULL, $icon_weight = NULL, $word_weight = NULL, $link_attribs = array(), $icon_attribs = array() )
{
	global $UserSettings;

	$link_attribs['href'] = $url;
	$link_attribs['title'] = $title;

	if( is_null($icon_weight) )
	{
		$icon_weight = 4;
	}
	if( is_null($word_weight) )
	{
		$word_weight = 1;
	}

	if( ! isset($link_attribs['class']) )
	{
		$link_attribs['class'] = 'action_icon';
	}

	if( get_icon( $icon, 'rollover' ) )
	{
		if( empty($link_attribs['class']) )
		{
			$link_attribs['class'] = 'rollover';
		}
		else
		{
			$link_attribs['class'] .= ' rollover';
		}

		if( get_icon( $icon, 'sprite' ) )
		{ // Set class "rollover_sprite" If image uses sprite
			$link_attribs['class'] .= '_sprite';
		}
	}
	//$link_attribs['class'] .= $icon != '' ? ' '.$icon : ' noicon';

	// "use_js_popup": open link in a JS popup
	// TODO: this needs to be rewritten with jQuery instead
	if( false && ! empty($link_attribs['use_js_popup']) )
	{
		$popup_js = 'var win = new PopupWindow(); win.setUrl( \''.$link_attribs['href'].'\' ); win.setSize(  ); ';

		if( isset($link_attribs['use_js_size']) )
		{
			if( ! empty($link_attribs['use_js_size']) )
			{
				$popup_size = $link_attribs['use_js_size'];
			}
		}
		else
		{
			$popup_size = '500, 400';
		}
		if( isset($popup_size) )
		{
			$popup_js .= 'win.setSize( '.$popup_size.' ); ';
		}
		$popup_js .= 'win.showPopup(\''.$link_attribs['id'].'\'); return false;';

		if( empty( $link_attribs['onclick'] ) )
		{
			$link_attribs['onclick'] = $popup_js;
		}
		else
		{
			$link_attribs['onclick'] .= $popup_js;
		}
		unset($link_attribs['use_js_popup']);
		unset($link_attribs['use_js_size']);
	}

	$display_icon = empty( $UserSettings ) ? false : ($icon_weight >= $UserSettings->get('action_icon_threshold'));
	$display_word = empty( $UserSettings ) ? false : ($word_weight >= $UserSettings->get('action_word_threshold'));

	$a_body = '';

	if( $display_icon || ! $display_word )
	{	// We MUST display an action icon in order to make the user happy:
		// OR we default to icon because the user doesn't want the word either!!

		$icon_attribs = array_merge( array(
				'title' => false, // No need to set attribute "ttile" for icon because parent <a> already has it
			), $icon_attribs );

		if( $icon_s = get_icon( $icon, 'imgtag', $icon_attribs, true ) )
		{
			$a_body .= $icon_s;
		}
		else
		{ // fallback to word
			$display_word = true;
		}
	}

	if( $display_word )
	{	// We MUST display an action word in order to make the user happy:

		if( $display_icon )
		{ // We already have an icon, display a SHORT word:
			if( !empty($word) )
			{	// We have provided a short word:
				$a_body .= $word;
			}
			else
			{	// We fall back to alt:
				$a_body .= get_icon( $icon, 'legend' );
			}
		}
		else
		{	// No icon display, let's display a LONG word/text:
			$a_body .= trim( $title, ' .!' );
		}

		// Add class "hoverlink" for icon with text
		$link_attribs['class'] .= ' hoverlink';
	}

	// Format title attribute because it may contains the unexpected chars from translatable strings:
	if( isset( $link_attribs['title'] ) )
	{
		$link_attribs['title'] = format_to_output( $link_attribs['title'], 'htmlattr' );
	}

	// NOTE: We do not use format_to_output with get_field_attribs_as_string() here, because it interferes with the Results class (eval() fails on entitied quotes..) (blueyed)
	return '<a'.get_field_attribs_as_string( $link_attribs, false ).'>'.$a_body.'</a>';
}


/**
 * Get properties of an icon.
 *
 * Note: to get a file type icon, use {@link File::get_icon()} instead.
 *
 * @uses get_icon_info()
 * @param string icon for what? (key)
 * @param string what to return for that icon ('imgtag', 'alt', 'legend', 'file', 'url', 'size' {@link imgsize()})
 * @param array additional params
 *   - 'class' => class name when getting 'imgtag',
 *   - 'size' => param for 'size',
 *   - 'title' => title attribute for 'imgtag'
 * @param boolean true to include this icon into the legend at the bottom of the page (works for 'imgtag' only)
 * @return mixed False on failure, string on success.
 */
function get_icon( $iconKey, $what = 'imgtag', $params = NULL, $include_in_legend = false )
{
	global $admin_subdir, $Debuglog, $use_strict;
	global $conf_path;
	global $rsc_path, $rsc_uri;

	if( ! function_exists('get_icon_info') )
	{
		require_once $conf_path.'_icons.php';
	}

	$icon = get_icon_info($iconKey);
	if( ! $icon )
	{
		$Debuglog->add('No image defined for '.var_export( $iconKey, true ).'!', 'icons');
		return false;
	}

	if( !isset( $icon['file'] ) && $what != 'imgtag' )
	{
		$icon['file'] = 'icons/icons_sprite.png';
	}

	switch( $what )
	{
		case 'rollover':
			if( isset( $icon['rollover'] ) )
			{ // Image has rollover available
				global $b2evo_icons_type;

				if( isset( $b2evo_icons_type ) && ( ! empty( $icon['glyph'] ) || ! empty( $icon['fa'] ) ) )
				{ // Glyph and font-awesome icons don't have rollover effect
					return false;
				}
				return $icon['rollover'];
			}
			return false;
			/* BREAK */


		case 'file':
			return $rsc_path.$icon['file'];
			/* BREAK */


		case 'alt':
			if( isset( $icon['alt'] ) )
			{ // alt tag from $map_iconfiles
				return $icon['alt'];
			}
			else
			{ // fallback to $iconKey as alt-tag
				return $iconKey;
			}
			/* BREAK */


		case 'legend':
			if( isset( $icon['legend'] ) )
			{ // legend tag from $map_iconfiles
				return $icon['legend'];
			}
			else
			if( isset( $icon['alt'] ) )
			{ // alt tag from $map_iconfiles
				return $icon['alt'];
			}
			else
			{ // fallback to $iconKey as alt-tag
				return $iconKey;
			}
			/* BREAK */


		case 'class':
			if( isset($icon['class']) )
			{
				return $icon['class'];
			}
			else
			{
				return '';
			}
			/* BREAK */

		case 'url':
			return $rsc_uri.$icon['file'];
			/* BREAK */

		case 'size':
			if( !isset( $icon['size'] ) )
			{
				$Debuglog->add( 'No iconsize for ['.$iconKey.']', 'icons' );

				$icon['size'] = imgsize( $rsc_path.$icon['file'] );
			}

			switch( $params['size'] )
			{
				case 'width':
					return $icon['size'][0];

				case 'height':
					return $icon['size'][1];

				case 'widthxheight':
					return $icon['size'][0].'x'.$icon['size'][1];

				case 'width':
					return $icon['size'][0];

				case 'string':
					return 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'"';

				default:
					return $icon['size'];
			}
			/* BREAK */


		case 'xy':
			if( isset( $icon['xy'] ) )
			{ // Return data for style property "background-position"
				return "-".$icon['xy'][0]."px -".$icon['xy'][1]."px";
			}
			return false;


		case 'sprite':
			if( isset( $icon['xy'] ) )
			{	// Image uses spite file
				return true;
			}
			return false;
			/* BREAK */


		case 'imgtag':
			global $b2evo_icons_type;

			if( isset( $b2evo_icons_type ) )
			{ // Specific icons type is defined
				$current_icons_type = $b2evo_icons_type;
				if( $current_icons_type == 'fontawesome-glyphicons' )
				{ // Use fontawesome icons as a priority over the glyphicons
					$current_icons_type = isset( $icon['fa'] ) ? 'fontawesome' : 'glyphicons';
				}
				switch( $current_icons_type )
				{
					case 'glyphicons':
						// Use glyph icons of bootstrap
						$icon_class_prefix = 'glyphicon glyphicon-';
						$icon_param_name = 'glyph';
						$icon_content = '&nbsp;';
						break;

					case 'fontawesome':
						// Use the icons from http://fortawesome.github.io/Font-Awesome/icons/
						$icon_class_prefix = 'fa fa-';
						$icon_param_name = 'fa';
						$icon_content = '';
						break;
				}
			}

			if( isset( $icon_class_prefix ) && ! empty( $icon[ $icon_param_name ] ) )
			{ // Use glyph or fa icon if it is defined in icons config
				if( isset( $params['class'] ) )
				{ // Get class from params
					$params['class'] = $icon_class_prefix.$icon[ $icon_param_name ].' '.$params['class'];
				}
				else
				{ // Set default class
					$params['class'] = $icon_class_prefix.$icon[ $icon_param_name ];
				}

				$styles = array();
				if( isset( $params['color'] ) )
				{	// Set color from params:
					$styles[] = 'color:'.$params['color'];
					unset( $params['color'] );
				}
				elseif( isset( $icon['color-'.$icon_param_name] ) )
				{ // Set a color for icon only for current type
					if( $icon['color-'.$icon_param_name] != 'default' )
					{
						$styles[] = 'color:'.$icon['color-'.$icon_param_name];
					}
				}
				elseif( isset( $icon['color'] ) )
				{ // Set a color for icon for all types
					if( $icon['color'] != 'default' )
					{
						$styles[] = 'color:'.$icon['color'];
					}
				}
				if( isset( $icon['color-over'] ) )
				{ // Set a color for mouse over event
					$params['data-color'] = $icon['color-over'];
				}
				if( isset( $icon['toggle-'.$icon_param_name] ) )
				{ // Set a color for mouse over event
					$params['data-toggle'] = $icon['toggle-'.$icon_param_name];
				}

				if( ! isset( $params['title'] ) )
				{ // Use 'alt' for 'title'
					if( isset( $params['alt'] ) )
					{
						$params['title'] = $params['alt'];
						unset( $params['alt'] );
					}
					else if( ! isset( $params['alt'] ) && isset( $icon['alt'] ) )
					{
						$params['title'] = $icon['alt'];
					}
				}

				if( isset( $params['title'] ) && $params['title'] === false )
				{	// Disable title:
					unset( $params['title'] );
				}

				// Format title and alt attributes because they may contain the unexpected chars from translatable strings:
				if( isset( $params['title'] ) )
				{
					// Use 'htmlspecialchars' format instead of 'htmlattr' because html tags should not be stripped e.g. in bootsrap tooltips:
					$params['title'] = format_to_output( $params['title'], 'htmlspecialchars' );
				}
				if( isset( $params['alt'] ) )
				{
					$params['alt'] = format_to_output( $params['alt'], 'htmlattr' );
				}

				if( isset( $icon['size-'.$icon_param_name] ) )
				{ // Set a size for icon only for current type
					if( isset( $icon['size-'.$icon_param_name][0] ) )
					{ // Width
						$styles['width'] = 'width:'.$icon['size-'.$icon_param_name][0].'px';
					}
					if( isset( $icon['size-'.$icon_param_name][1] ) )
					{ // Height
						$styles['height'] = 'height:'.$icon['size-'.$icon_param_name][1].'px';
					}
				}

				if( isset( $params['style'] ) )
				{ // Keep styles from params
					$styles[] = $params['style'];
				}
				if( ! empty( $styles ) )
				{ // Init attribute 'style'
					$params['style'] = implode( ';', $styles );
				}

				// Add all the attributes:
				$params = get_field_attribs_as_string( $params, false );

				$r = '<span'.$params.'>'.$icon_content.'</span>';
			}
			elseif( ! isset( $icon['file'] ) )
			{ // Use span tag with sprite instead of img
				$styles = array();

				if( isset( $params['xy'] ) )
				{ // Get background position from params
					$styles[] = "background-position: ".$params['xy'][0]."px ".$params['xy'][1]."px";
					unset( $params['xy'] );
				}
				else if( isset( $icon['xy'] ) )
				{ // Set background position in the icons_sprite.png
					$styles[] = "background-position: -".$icon['xy'][0]."px -".$icon['xy'][1]."px";
				}

				if( isset( $params['size'] ) )
				{ // Get sizes from params
					$icon['size'] = $params['size'];
					unset( $params['size'] );
				}
				if( isset( $icon['size'] ) )
				{ // Set width & height
					if( $icon['size'][0] != 16 )
					{
						$styles[] = "width: ".$icon['size'][0]."px";
					}
					if( $icon['size'][1] != 16 )
					{
						$styles[] = "height: ".$icon['size'][1]."px; line-height: ".$icon['size'][1]."px";
					}
				}

				if( isset( $params['style'] ) )
				{ // Get styles from params
					$styles[] = $params['style'];
				}
				if( count( $styles ) > 0 )
				{
					$params['style'] = implode( '; ', $styles);
				}

				if( ! isset( $params['title'] ) )
				{	// Use 'alt' for 'title'
					if( isset( $params['alt'] ) )
					{
						$params['title'] = $params['alt'];
						unset( $params['alt'] );
					}
					else if( ! isset( $params['alt'] ) && isset( $icon['alt'] ) )
					{
						$params['title'] = $icon['alt'];
					}
				}

				if( isset( $params['title'] ) && $params['title'] === false )
				{	// Disable title:
					unset( $params['title'] );
				}

				// Format title and alt attributes because they may contain the unexpected chars from translatable strings:
				if( isset( $params['title'] ) )
				{
					$params['title'] = format_to_output( $params['title'], 'htmlattr' );
				}
				if( isset( $params['alt'] ) )
				{
					$params['alt'] = format_to_output( $params['alt'], 'htmlattr' );
				}

				if( isset( $params['class'] ) )
				{	// Get class from params
					$params['class'] = 'icon '.$params['class'];
				}
				else
				{	// Set default class
					$params['class'] = 'icon';
				}

				// Add all the attributes:
				$params = get_field_attribs_as_string( $params, false );

				$r = '<span'.$params.'>&nbsp;</span>';
			}
			else
			{ // Use img tag
				$r = '<img src="'.$rsc_uri.$icon['file'].'" ';

				if( !$use_strict )
				{	// Include non CSS fallbacks - transitional only:
					$r .= 'border="0" align="top" ';
				}

				// Include class (will default to "icon"):
				if( ! isset( $params['class'] ) )
				{
					if( isset($icon['class']) )
					{	// This icon has a class
						$params['class'] = $icon['class'];
					}
					else
					{
						$params['class'] = '';
					}
				}

				// Include size (optional):
				if( isset( $icon['size'] ) )
				{
					$r .= 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'" ';
				}

				// Include alt (XHTML mandatory):
				if( ! isset( $params['alt'] ) )
				{
					if( isset( $icon['alt'] ) )
					{ // alt-tag from $map_iconfiles
						$params['alt'] = $icon['alt'];
					}
					else
					{ // $iconKey as alt-tag
						$params['alt'] = $iconKey;
					}
				}

				// Format alt attribute because it may contains the unexpected chars from translatable strings:
				if( isset( $params['alt'] ) )
				{
					$params['alt'] = format_to_output( $params['alt'], 'htmlattr' );
				}

				// Add all the attributes:
				$r .= get_field_attribs_as_string( $params, false );

				// Close tag:
				$r .= '/>';


				if( $include_in_legend && ( $IconLegend = & get_IconLegend() ) )
				{ // This icon should be included into the legend:
					$IconLegend->add_icon( $iconKey );
				}
			}
			return $r;
			/* BREAK */

		case 'noimg':
			global $b2evo_icons_type;

			if( isset( $b2evo_icons_type ) )
			{ // Specific icons type is defined
				$current_icons_type = $b2evo_icons_type;
				if( $current_icons_type == 'fontawesome-glyphicons' )
				{ // Use fontawesome icons as a priority over the glyphicons
					$current_icons_type = isset( $icon['fa'] ) ? 'fontawesome' : 'glyphicons';
				}
				switch( $current_icons_type )
				{
					case 'glyphicons':
						// Use glyph icons of bootstrap
						$icon_param_name = 'glyph';
						break;

					case 'fontawesome':
						// Use the icons from http://fortawesome.github.io/Font-Awesome/icons/
						$icon_param_name = 'fa';
						break;
				}
			}

			$styles = array();
			if( isset( $icon_param_name ) && ! empty( $icon[ $icon_param_name ] ) )
			{ // Use glyph or fa icon if it is defined in icons config
				if( isset( $icon['size-'.$icon_param_name] ) )
				{ // Set a size for icon only for current type
					if( isset( $icon['size-'.$icon_param_name][0] ) )
					{ // Width
						$styles['width'] = 'width:'.$icon['size-'.$icon_param_name][0].'px';
					}
					if( isset( $icon['size-'.$icon_param_name][1] ) )
					{ // Height
						$styles['width'] = 'height:'.$icon['size-'.$icon_param_name][1].'px';
					}
					if( isset( $icon['size'] ) )
					{ // Unset size for sprite icon
						unset( $icon['size'] );
					}
				}
			}

			// Include size (optional):
			if( isset( $icon['size'] ) )
			{
				$params['size'] = $icon['size'];
			}
			$styles[] = 'margin:0 2px';

			if( isset( $params['style'] ) )
			{ // Keep styles from params
				$styles[] = $params['style'];
			}
			if( ! empty( $styles ) )
			{ // Init attribute 'style'
				$params['style'] = implode( ';', $styles );
			}

			return get_icon( 'pixel', 'imgtag', $params );
			/* BREAK */
			/*
			$blank_icon = get_icon_info('pixel');

			$r = '<img src="'.$rsc_uri.$blank_icon['file'].'" ';

			// TODO: dh> add this only for !$use_strict, like above?
			// Include non CSS fallbacks (needed by bozos... and basic skin):
			$r .= 'border="0" align="top" ';

			// Include class (will default to "noicon"):
			if( ! isset( $params['class'] ) )
			{
				if( isset($icon['class']) )
				{	// This icon has a class
					$params['class'] = $icon['class'];
				}
				else
				{
					$params['class'] = 'no_icon';
				}
			}

			// Include size (optional):
			if( isset( $icon['size'] ) )
			{
				$r .= 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'" ';
			}

			// Include alt (XHTML mandatory):
			if( ! isset( $params['alt'] ) )
			{
				$params['alt'] = '';
			}

			// Add all the attributes:
			$r .= get_field_attribs_as_string( $params, false );

			// Close tag:
			$r .= '/>';

			return $r;*/
			/* BREAK */
	}
}


/**
 * @param string date (YYYY-MM-DD)
 * @param string time
 */
function form_date( $date, $time = '' )
{
	return substr( $date.'          ', 0, 10 ).' '.$time;
}


/**
 * Get list of client IP addresses from REMOTE_ADDR and HTTP_X_FORWARDED_FOR,
 * in this order. '' is used when no IP could be found.
 *
 * @param boolean True, to get only the first IP (probably REMOTE_ADDR)
 * @param boolean True, to convert IPv6 to IPv4 format
 * @return array|string Depends on first param.
 */
function get_ip_list( $firstOnly = false, $convert_to_ipv4 = false )
{
	$r = array();

	if( ! empty( $_SERVER['REMOTE_ADDR'] ) )
	{
		foreach( explode( ',', $_SERVER['REMOTE_ADDR'] ) as $l_ip )
		{
			$l_ip = trim( $l_ip );
			if( ! empty( $l_ip ) )
			{
				if( $convert_to_ipv4 )
				{ // Convert IP address to IPv4 format(if it is in IPv6 format)
					$l_ip = int2ip( ip2int( $l_ip ) );
				}
				$r[] = $l_ip;
			}
		}
	}

	if( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) )
	{ // IP(s) behind Proxy - this can be easily forged!
		foreach( explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) as $l_ip )
		{
			$l_ip = trim( $l_ip );
			if( ! empty( $l_ip ) && $l_ip != 'unknown' )
			{
				if( $convert_to_ipv4 )
				{ // Convert IP address to IPv4 format(if it is in IPv6 format)
					$l_ip = int2ip( ip2int( $l_ip ) );
				}
				$r[] = $l_ip;
			}
		}
	}

	if( ! isset( $r[0] ) )
	{ // No IP found.
		$r[] = '';
	}

	// Remove the duplicates
	$r = array_unique( $r );

	return $firstOnly ? $r[0] : $r;
}


/**
 * Get list of IP addresses with link to back-office page if User has an access
 *
 * @param object|NULL User
 * @param array|NULL List of IP addresses
 * @param string Text of link, Use '#' to display IP address
 * @return array List of IP addresses
 */
function get_linked_ip_list( $ip_list = NULL, $User = NULL, $link_text = '#' )
{
	if( $User === NULL )
	{ // Get current User by default
		global $current_User;
		$User = & $current_User;
	}

	if( $ip_list === NULL )
	{ // Get IP addresses by function get_ip_list()
		$ip_list = get_ip_list( false, true );
	}

	if( ! empty( $User ) &&
	    $User->check_perm( 'admin', 'restricted' ) &&
	    $User->check_perm( 'spamblacklist', 'view' ) )
	{ // User has an access to backoffice, Display a link for each IP address
		global $admin_url;
		foreach( $ip_list as $i => $ip_address )
		{
			if( $link_text == '#' )
			{ // Use IP address aslink text
				$link_text = $ip_address;
			}
			$ip_list[ $i ] = '<a href="'.$admin_url.'?ctrl=antispam&amp;tab3=ipranges&amp;ip_address='.$ip_address.'">'.$link_text.'</a>';
		}
	}

	return $ip_list;
}


/**
 * Get the base domain (without protocol and any subdomain) of an URL.
 *
 * Gets a max of 3 domain parts (x.y.tld)
 *
 * @param string URL
 * @return string the base domain (may become empty, if found invalid)
 */
function get_base_domain( $url )
{
	global $evo_charset;

	// Chop away the protocol part(http,htpps,ftp) and the path:
	$domain = preg_replace( '~^([a-z]+://)?([^:/#]+)(.*)$~i', '\\2', $url );

	if( empty( $domain ) || preg_match( '~^(\d+\.)+\d+$~', $domain ) )
	{	// Empty or All numeric = IP address, don't try to cut it any further:
		return $domain;
	}

	// Get the base domain up to 2 or 3 levels (x.y.tld):
	// NOTE: "_" is not really valid, but for Windows it is..
	// NOTE: \w includes "_"

	// Convert URL to IDN:
	$domain = idna_encode( $domain );

	if( preg_match( '~\.(com|net|org|int|edu|gov|mil)$~i', $domain ) )
	{	// Use max 2 level domain for very well known TLDs:
		// (for example: "sub3.sub2.sub1.domain.com" will be "domain.com")
		$max_level = 2;
	}
	else
	{	// Use max 3 level domain for all others:
		// (for example: "sub3.sub2.sub1.domain.fr" will be "sub1.domain.fr")
		$max_level = 3;
	}

	// Limit domain by 2 or 3 level depending on TLD:
	if( ! preg_match( '~ ( \w (\w|-|_)* \. ){0,'.( $max_level - 1 ).'}   \w (\w|-|_)* $~ix', $domain, $match ) )
	{	// Return an empty if domain doesn't match to proper format:
		return '';
	}

	// Convert all symbols of domain name to UTF-8:
	$domain = convert_charset( idna_decode( $match[0] ), $evo_charset, 'UTF-8' );

	// Remove any prefix like "www.", "www2.", "www9999." and etc.:
	$domain = preg_replace( '~^www[0-9]*\.~i', '', $domain );

	return $domain;
}


/**
 * Generate login from registration information
 * 
 * @param string Email address
 * @param string First name
 * @param string Last name
 * @param string Nickname
 * @param boolean Use random alphanumeric string as login
 * @return string Generated login
 */
function generate_login_from_register_info( $email = NULL, $firstname = NULL, $lastname = NULL, $nickname = NULL, $use_random = false )
{
	global $Settings;

	if( ! empty( $firstname ) || ( ! empty( $lastname ) && ( $Settings->get( 'registration_no_username') == 'firstname.lastname' ) ) )
	{ // Firstname or lastname given, let's use these:
		$login = array();
		if( ! empty( $firstname ) )
		{
			$login[] = trim( $firstname );
		}

		if( $Settings->get( 'registration_no_username' ) == 'firstname.lastname' )
		{	// We can use lastname too:
			if( ! empty( $lastname ) )
			{
				$login[] = trim( $lastname );
			}
		}
		$login = preg_replace( '/[\s]+/', '_', utf8_strtolower( implode( '.', $login ) ) );
		$login = generate_login_from_string( $login );
	}
	elseif( ! empty( $email ) )
	{ // Get the login from email address:
		$login = preg_replace( '/^([^@]+)@(.+)$/', '$1', utf8_strtolower( $email ) );
		$login = preg_replace( '/[\'"><@\s]/', '', $login );

		if( strpos( $login, '.' ) && ( $Settings->get( 'registration_no_username' ) == 'firstname' ) )
		{ // Get only the part before the "." if it has one
			$temp_login = $login;
			$login = substr( $login, 0, strpos( $login, '.' ) );
			$login = generate_login_from_string( $login );

			if( empty( $login ) )
			{ // Resulting login empty, use full email address
				$login = generate_login_from_string( $temp_login );
			}
		}
		else
		{
			$login = generate_login_from_string( $login );
		}
	}
	elseif( ! empty( $nickname ) )
	{
		$login = preg_replace( '/[\s]+/', '_', utf8_strtolower( trim( $nickname ) ) );
		$login = generate_login_from_string( $login );
	}
	elseif( $use_random )
	{	// Nothing else to use as login, use random numbers:
		$login = 'user_'.rand( 1, 999 );
		$login = generate_login_from_string( $login );
	}
	else
	{
		return '';
	}

	return $login;
}


/**
 * Generate login from string
 *
 * @param string string to generate login from
 * @return string login
 */
function generate_login_from_string( $login )
{
	global $Settings;

	// Normalize login
	load_funcs('locales/_charset.funcs.php');
	$login = replace_special_chars( $login, NULL, true );

	if( $Settings->get( 'strict_logins' ) )
	{ // We allow only the plain ACSII characters, digits, the chars _ and . and -
		$login = preg_replace( '/[^A-Za-z0-9_.\-]/', '', $login );
	}
	else
	{ // We allow any character that is not explicitly forbidden in Step 1
		// Enforce additional limitations
		$login = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '', $login ); // Kill octets
		$login = preg_replace( '/&.+?;/', '', $login ); // Kill entities
	}

	$login = preg_replace( '/^usr_/i', '', $login );

	// Trim to allowed login length
	$max_login_length = 20;
	if( strlen( $login ) > $max_login_length )
	{
		$login = substr( $login, 0, $max_login_length );
	}

	if( ! empty( $login ) )
	{
		// Check and search free login name if current is already in use
		$login_name = $login;
		$login_number = 1;
		$UserCache = & get_UserCache();
		while( $UserCache->get_by_login( $login_name ) )
		{
			$num_suffix_length = strlen( $login_number );
			if( strlen( $login_name ) + $num_suffix_length > $max_login_length )
			{
				$login_name = substr( $login, 0, $max_login_length - $num_suffix_length ).$login_number;
			}
			else
			{
				$login_name = $login.$login_number;
			}
			$login_number++;
		}
		$login = $login_name;
	}

	return $login;
}


/**
 * Generate a valid key of size $length.
 *
 * @param integer length of key
 * @param string chars to use in generated key
 * @return string key
 */
function generate_random_key( $length = 32, $keychars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' )
{
	$key = '';
	$rnd_max = strlen($keychars) - 1;

	for( $i = 0; $i < $length; $i++ )
	{
		$key .= $keychars[random_int(0, $rnd_max)]; // get a random character out of $keychars
	}

	return $key;
}


/**
 * Generate a random password with no ambiguous chars
 *
 * @param integer length of password
 * @return string password
 */
function generate_random_passwd( $length = NULL )
{
	// fp> NOTE: do not include any characters that would make autogenerated passwords ambiguous
	// 1 (one) vs l (L) vs I (i)
	// O (letter) vs 0 (digit)

	if( empty($length) )
	{
		$length = rand( 8, 14 );
	}

	return generate_random_key( $length, 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' );
}


function is_create_action( $action )
{
	$action_parts = explode( '_', $action );

	switch( $action_parts[0] )
	{
		case 'new':
		case 'new_switchtab':
		case 'copy':
		case 'create':	// we return in this state after a validation error
		case 'preview':
			return true;

		case 'edit':
		case 'edit_switchtab':
		case 'propose':
		case 'update':	// we return in this state after a validation error
		case 'delete':
		// The following one's a bit far fetched, but can happen if we have no sheet display:
		case 'unlink':
		case 'view':
		case 'extract':
			return false;

		default:
			debug_die( 'Unhandled action in form: '.strip_tags($action_parts[0]) );
	}
}


/**
 * Compact a date in a number keeping only integer value of the string
 *
 * @param string date
 */
function compact_date( $date )
{
	return preg_replace( '#[^0-9]#', '', $date );
}


/**
 * Decompact a date in a date format ( Y-m-d h:m:s )
 *
 * @param string date
 */
function decompact_date( $date )
{
	$date0 = $date;

	return  substr($date0,0,4).'-'.substr($date0,4,2).'-'.substr($date0,6,2).' '
								.substr($date0,8,2).':'.substr($date0,10,2).':'.substr($date0,12,2);
}

/**
 * Check the format of the phone number param and
 * format it in a french number if it is.
 *
 * @param string phone number
 */
function format_phone( $phone, $hide_country_dialing_code_if_same_as_locale = true )
{
	global $CountryCache;

	$dialing_code = NULL;

	if( substr( $phone, 0, 1 ) == '+' )
	{	// We have a dialing code in the phone, so we extract it:
		$dialing_code = $CountryCache->extract_country_dialing_code( substr( $phone, 1 ) );
	}

	if( !is_null( $dialing_code ) && ( locale_dialing_code() == $dialing_code )
			&& $hide_country_dialing_code_if_same_as_locale )
	{	// The phone dialing code is same as locale and we want to hide it in this case
		if( ( strlen( $phone ) - strlen( $dialing_code ) ) == 10 )
		{	// We can format it like a french phone number ( 0x.xx.xx.xx.xx )
			$phone_formated = format_french_phone( '0'.substr( $phone, strlen( $dialing_code )+1 ) );
		}
		else
		{ // ( 0xxxxxxxxxxxxxx )
			$phone_formated = '0'.substr( $phone, strlen( $dialing_code )+1 );
		}

	}
	elseif( !is_null( $dialing_code ) )
	{	// Phone has a dialing code
		if( ( strlen( $phone ) - strlen( $dialing_code ) ) == 10 )
		{ // We can format it like a french phone number with the dialing code ( +dialing x.xx.xx.xx.xx )
			$phone_formated = '+'.$dialing_code.format_french_phone( ' '.substr( $phone, strlen( $dialing_code )+1 ) );
		}
		else
		{ // ( +dialing  xxxxxxxxxxx )
			$phone_formated = '+'.$dialing_code.' '.substr( $phone, strlen( $dialing_code )+1 );
		}
	}
	else
	{
		if( strlen( $phone ) == 10 )
		{ //  We can format it like a french phone number ( xx.xx.xx.xx.xx )
			$phone_formated = format_french_phone( $phone );
		}
		else
		{	// We don't format phone: TODO generic format phone ( xxxxxxxxxxxxxxxx )
			$phone_formated = $phone;
		}
	}

	return $phone_formated;
}


/**
 * Format a string in a french phone number
 *
 * @param string phone number
 */
function format_french_phone( $phone )
{
	return substr($phone, 0 , 2).'.'.substr($phone, 2, 2).'.'.substr($phone, 4, 2)
					.'.'.substr($phone, 6, 2).'.'.substr($phone, 8, 2);
}


/**
 * Get the manual url for the given topic
 *
 * @param string topic
 * @return string url to the manual
 */
function get_manual_url( $topic )
{
	// fp> TODO: this below is a temmporary hack while we work on the new manual:
	return 'http://b2evolution.net/man/'.str_replace( '_', '-', strtolower( rawurlencode( $topic ) ) );
}


/**
 * Generate a link to a online help resource.
 * testing the concept of online help (aka webhelp).
 * this function should be relocated somewhere better if it is taken onboard by the project
 *
 * @todo replace [?] with icon,
 * @todo write url suffix dynamically based on topic and language
 *
 * QUESTION: launch new window with javascript maybe?
 * @param string Topic
 *        The topic should be in a format like [\w]+(/[\w]+)*, e.g features/online_help.
 * @param string link text, leave it NULL to get link with manual icon
 * @param string a word to be displayed after the manual icon (if no icon gets displayed, $title will be used instead!)
 * @param integer 1-5: weight of the word. The word will be displayed only if its weight is >= than the user setting threshold. (Default: 1)
 * @return string
 */
function get_manual_link( $topic, $link_text = NULL, $action_word = NULL, $word_weight = 1 )
{
	global $online_help_links;

	if( $online_help_links )
	{
		$manual_url = get_manual_url( $topic );

		if( $link_text == NULL )
		{
			if( $action_word == NULL )
			{
				$action_word = T_('Manual');
			}
			$webhelp_link = action_icon( T_('Open relevant page in online manual'), 'manual', $manual_url, $action_word, 5, $word_weight, array( 'target' => '_blank' ) );
		}
		else
		{
			$webhelp_link = '<a href="'.$manual_url.'" target = "_blank">'.$link_text.'</a>';
		}

		return ' '.$webhelp_link;
	}
	else
	{
		return '';
	}
}


/**
 * Build a string out of $field_attribs, with each attribute
 * prefixed by a space character.
 *
 * @param array Array of field attributes.
 * @param boolean Use format_to_output() for the attributes?
 * @return string
 */
function get_field_attribs_as_string( $field_attribs, $format_to_output = true )
{
	$r = '';
	foreach( $field_attribs as $l_attr => $l_value )
	{
		if( $l_value === NULL )
		{ // don't generate empty attributes (it may be NULL if we pass 'value' => NULL as field_param for example, because isset() does not match it!)
			// sam2kb> what about alt="" how do we handle this?
			// I've removed the "=== ''" check now. Should not do any harm. IIRC NULL is what we want to avoid here.
			continue;
		}

		if( $format_to_output )
		{
			$r .= ' '.$l_attr.'="'.format_to_output( $l_value, 'formvalue' ).'"';
		}
		else
		{
			$r .= ' '.$l_attr.'="'.$l_value.'"';
		}
	}

	return $r;
}


/**
 * Update values of HTML tag attributes
 * This will only update the first HTML tag at the beginning of the passed param.
 *
 * @param string HTML tag
 * @param array Attributes
 * @param array Actions for each attribute:
 *              'append'  - Append to existing attribute value (Default for all)
 *              'skip'    - Skip if attribute already exists
 *              'replace' - Replace attribute to new value completely
 * @return string Updated HTML tag
 */
function update_html_tag_attribs( $html_tag, $new_attribs, $attrib_actions = array() )
{
	// Check for a valid html tag at the beginning of the string:
	if( ! preg_match( '#^<([\S]+)[^>]*>#i', $html_tag, $tag_match ) )
	{	// Wrong HTML tag format, Return original string:
		return $html_tag;
	}

	$html_tag_name = $tag_match[1];

	// Get the remaining string after the first HTML tag:
	$trailing_str = str_replace( $tag_match[0], '', $html_tag );

	$old_attribs = array();
	$updated_attribs = array();
	if( preg_match_all( '@(\S+)=("|\'|)(.*)("|\'|>)@isU', $html_tag, $attr_matches ) )
	{	// Get all existing attributes:
		foreach( $attr_matches[1] as $o => $old_attr_name )
		{
			$old_attribs[ $old_attr_name ] = $attr_matches[3][ $o ];
			if( ! isset( $new_attribs[ $old_attr_name ] ) )
			{	// This attribute is not updated, keep current value:
				$updated_attribs[] = $old_attr_name.'="'.format_to_output( $attr_matches[3][ $o ], 'formvalue' ).'"';
			}
		}
	}

	foreach( $new_attribs as $new_attrib_name => $new_attrib_value )
	{
		if( isset( $old_attribs[ $new_attrib_name ] ) )
		{	// If attribute exists in original HTML tag then Update it depending on selected action:
			$attrib_action = isset( $attrib_actions[ $new_attrib_name ] ) ? $attrib_actions[ $new_attrib_name ] : 'append';
			switch( $attrib_action )
			{
				case 'skip':
					// Don't update old value:
					$new_attrib_value = $old_attribs[ $new_attrib_name ];
					break;

				case 'replace':
					// Replace  old value with new:
					// $new_attrib_value = $new_attrib_value;
					break;

				case 'append':
				default:
					// Append new value to old:
					$new_attrib_value = $old_attribs[ $new_attrib_name ].' '.$new_attrib_value;
					break;
			}
		}
		// ELSE If attribute doesn't exist in original HTML tag then create new one.

		$updated_attribs[] = $new_attrib_name.'="'.format_to_output( $new_attrib_value, 'formvalue' ).'"';
	}

	return '<'.$html_tag_name.' '.implode( ' ', $updated_attribs ).'>'.$trailing_str;
}


/**
 * Is the current page an install page?
 *
 * @return boolean
 */
function is_install_page()
{
	global $is_install_page;

	return isset( $is_install_page ) && $is_install_page === true; // check for type also, because of register_globals!
}


/**
 * Is the current page an admin/backoffice page?
 *
 * @return boolean
 */
function is_admin_page()
{
	global $is_admin_page;

	return isset( $is_admin_page ) && $is_admin_page === true; // check for type also, because of register_globals!
}


/**
 * Is the current page a default 'Front' page of a blog?
 *
 * @return boolean
 */
function is_front_page()
{
	global $is_front;

	return isset( $is_front ) && $is_front === true;
}


/**
 * Is pro version?
 *
 * @return boolean
 */
function is_pro()
{
	global $app_pro;

	return isset( $app_pro ) && $app_pro === true;
}


/**
 * Check if current version is PRO otherwise redirect to info page about PRO version
 */
function check_pro()
{
	if( ! is_pro() )
	{	// Restrict the checking feature for NOT PRO version:
		global $admin_url;

		header_redirect( $admin_url.'?ctrl=pro_only' );
	}
}


/**
 * Does the given url require logged in user
 *
 * @param string url
 * @param boolean set true to also check if url is login screen or not
 * @return boolean
 */
function require_login( $url, $check_login_screen )
{
	global $Settings, $dispatcher;
	if( preg_match( '#/'.preg_quote( $dispatcher, '#' ).'([&?].*)?$#', $url ) )
	{ // admin always require logged in user
		return true;
	}

	if( $check_login_screen &&  preg_match( '#/login.php([&?].*)?$#', $url ) )
	{
		return true;
	}

	$disp_names = 'threads|messages|contacts';
	if( !$Settings->get( 'allow_anonymous_user_list' ) )
	{
		$disp_names .= '|users';
	}
	if( !$Settings->get( 'allow_anonymous_user_profiles' ) )
	{
		$disp_names .= '|user';
	}
	if( $check_login_screen )
	{
		$disp_names .= '|login';
	}
	if( preg_match( '#disp=('.$disp_names.')#', $url ) )
	{ // $url require logged in user
		return true;
	}

	return false;
}


/**
 * Implode array( 'x', 'y', 'z' ) to something like 'x, y and z'. Useful for displaying list to the end user.
 *
 * If there's one element in the table, it is returned.
 * If there are at least two elements, the last one is concatenated using $implode_last, while the ones before are imploded using $implode_by.
 *
 * @todo dh> I don't think using entities/HTML as default for $implode_last is sane!
 *           Use "&" instead and make sure that the output for HTML is HTML compliant..
 * @todo Support for locales that have a different kind of enumeration?!
 * @return string
 */
function implode_with_and( $arr, $implode_by = ', ', $implode_last = ' &amp; ' )
{
	switch( count($arr) )
	{
		case 0:
			return '';

		case 1:
			$r = array_shift($arr);
			return $r;

		default:
			$r = implode( $implode_by, array_slice( $arr, 0, -1 ) )
			    .$implode_last.array_pop( $arr );
			return $r;
	}
}


/**
 * Display an array as a list:
 *
 * @param array
 * @param string
 * @param string
 * @param string
 * @param string
 * @param string
 */
function display_list( $items, $list_start = '<ul>', $list_end = '</ul>', $item_separator = '',
												$item_start = '<li>', $item_end = '</li>', $force_hash = NULL, $max_items = NULL, $link_params = array() )
{
	if( !is_null($max_items) && $max_items < 1 )
	{
		return;
	}

	if( !empty( $items ) )
	{
		echo $list_start;
		$count = 0;
		$first = true;

		foreach( $items as $item )
		{	// For each list item:

			$link = resolve_link_params( $item, $force_hash, $link_params );
			if( empty( $link ) )
			{
				continue;
			}

			$count++;
			if( $count>1 )
			{
				echo $item_separator;
			}
			echo $item_start.$link.$item_end;

			if( !is_null($max_items) && $count >= $max_items )
			{
				break;
			}
		}
		echo $list_end;
	}
}


/**
 * Credits stuff.
 */
function display_param_link( $params )
{
	echo resolve_link_params( $params );
}


/**
 * Resolve a link based on params (credits stuff)
 *
 * @param array
 * @param integer
 * @param array
 * @return string
 */
function resolve_link_params( $item, $force_hash = NULL, $params = array() )
{
	global $current_locale;

	// echo 'resolve link ';

	if( is_array( $item ) )
	{
		if( isset( $item[0] ) )
		{	// Older format, which displays the same thing for all locales:
			return generate_link_from_params( $item, $params );
		}
		else
		{	// First get the right locale:
			// echo $current_locale;
			foreach( $item as $l_locale => $loc_item )
			{
				if( $l_locale == substr( $current_locale, 0, strlen($l_locale) ) )
				{	// We found a matching locale:
					//echo "[$l_locale/$current_locale]";
					if( is_array( $loc_item[0] ) )
					{	// Randomize:
						$loc_item = hash_link_params( $loc_item, $force_hash );
					}

					return generate_link_from_params( $loc_item, $params );
				}
			}
			// No match found!
			return '';
		}
	}

	// Super old format:
	return $item;
}


/**
 * Get a link line, based url hash combined with probability percentage in first column
 *
 * @param array of arrays
 * @param display for a specific hash key
 */
function hash_link_params( $link_array, $force_hash = NULL )
{
	global $ReqHost, $ReqPath, $ReqURI;

	static $hash;

	if( !is_null($force_hash) )
	{
		$hash = $force_hash;
	}
	elseif( !isset($hash) )
	{
		$key = $ReqHost.$ReqPath;

		global $Collection, $Blog;
		if( !empty($Blog) && strpos( $Blog->get_setting('single_links'), 'param_' ) === 0 )
		{	// We are on a blog that doesn't even have clean URLs for posts
			$key .= $ReqURI;
		}

		$hash = 0;
		for( $i=0; $i<strlen($key); $i++ )
		{
			$hash += ord($key[$i]);
		}
		$hash = $hash % 100 + 1;

		// $hash = rand( 1, 100 );
		global $debug, $Debuglog;
		if( $debug )
		{
			$Debuglog->add( 'Hash key: '.$hash, 'request' );
		}
	}
	//	echo "[$hash] ";

	foreach( $link_array as $link_params )
	{
		// echo '<br>'.$hash.'-'.$link_params[ 0 ];
		if( $hash <= $link_params[ 0 ] )
		{	// select this link!
			// pre_dump( $link_params );
			array_shift( $link_params );
			return $link_params;
		}
	}
	// somehow no match, return 1st element:
	$link_params = $link_array[0];
	array_shift( $link_params );
	return $link_params;
}


/**
 * Generate a link from params (credits stuff)
 *
 * @param array
 * @param array
 */
function generate_link_from_params( $link_params, $params = array() )
{
	$url = $link_params[0];
	if( empty( $url ) )
	{
		return '';
	}

	// Make sure we are not missing any param:
	$params = array_merge( array(
			'type'        => 'link',
			'img_url'     => '',
			'img_width'   => '',
			'img_height'  => '',
			'title'       => '',
			'target'      => '_blank',
			'rel'         => 'noopener',
		), $params );

	$text = $link_params[1];
	if( is_array($text) )
	{
		$text = hash_link_params( $text );
		$text = $text[0];
	}
	if( empty( $text ) )
	{
		return '';
	}

	$r = '<a href="'.$url.'"';

	if( !empty($params['target'] ) )
	{
		$r .= ' target="'.$params['target'].'"';
	}

	if( !empty($params['rel'] ) )
	{
		$r .= ' rel="'.$params['rel'].'"';
	}

	if( $params['type'] == 'img' )
	{
		return $r.' title="'.$params['title'].'"><img src="'.$params['img_url'].'" alt="'
						.$text.'" title="'.$params['title'].'" width="'.$params['img_width'].'" height="'.$params['img_height']
						.'" border="0" /></a>';
	}

	return $r.'>'.$text.'</a>';
}


/**
 * Send a result as javascript
 * automatically includes any Messages ( @see Log::display() )
 * no return from function as it terminates processing
 *
 * @author Yabba
 *
 * @todo dh> Move this out into some more specific (not always included) file.
 *
 * @param array $methods javascript funtions to call with array of parameters
 *		format : 'function_name' => array( param1, parm2, param3 )
 * @param boolean $send_as_html Wrap the script into an html page with script tag; default is to send as js file
 * @param string $target prepended to function calls : blank or window.parent
 */
function send_javascript_message( $methods = array(), $send_as_html = false, $target = '' )
{
	// lets spit out any messages
	global $Messages, $param_input_err_messages;
	ob_start();
	$Messages->display();
	$output = ob_get_clean();

	// Initialize JavaScript params to send what field should be marked are error
	$js_error_params = array();
	if( ! empty( $param_input_err_messages ) && is_array( $param_input_err_messages ) )
	{
		foreach( $param_input_err_messages as $param_name => $param_error )
		{
			$js_error_params[] = $param_name.': \''.format_to_js( $param_error ).'\'';
		}
	}
	$js_error_params = '{'.implode( ', ', $js_error_params ).'}';

	// set target
	$target = ( $target ? $target : param( 'js_target', 'string' ) );
	if( $target )
	{	// add trailing [dot]
		$target = trim( $target, '.' ).'.';
	}

	// target should be empty or window.parent.
	if( $target && $target != 'window.parent.' )
	{
		debug_die( 'Unexpected javascript target' );
	}

	if( $output )
	{	// we have some messages
		$output = $target.'DisplayServerMessages( \''.format_to_js( $output ).'\', '.$js_error_params.' );'."\n";
	}

	if( !empty( $methods ) )
	{	// we have a methods to call
		foreach( $methods as $method => $param_list )
		{	// loop through each requested method
			$params = array();
			$internal_scripts = array();
			if( !is_array( $param_list ) )
			{	// lets make it an array
				$param_list = array( $param_list );
			}
			foreach( $param_list as $param )
			{	// add each parameter to the output
				if( is_array( $param ) )
				{	// This is an array:
					$param = json_encode( $param );
				}
				elseif( !is_numeric( $param ) )
				{	// This is a string
					if( preg_match_all( '#<script(.*?)?>(.*?)</script>#is', $param, $match_scripts ) )
					{	// Extract internal scripts from content:
						$param = str_replace( $match_scripts[0], '', $param );
						foreach( $match_scripts[2] as $i => $internal_script_code )
						{
							if( $internal_script_code === '' && strpos( $match_scripts[1][ $i ], 'src=' ) !== false )
							{	// Append external JS file to <head> to execute code:
								$internal_script_code = 'jQuery( \'head\' ).append( \''.format_to_js( $match_scripts[0][ $i ] ).'\' );';
							}
							$internal_scripts[] = $internal_script_code;
						}
					}
					// Quote the string to javascript format:
					$param = '\''.format_to_js( $param ).'\'';
				}
				$params[] = $param;// add param to the list
			}
			// Add method and parameters:
			$output .= $target.$method.'('.implode( ',', $params ).');'."\n";
			// Append all internal scripts from content on order to execute this properly:
			foreach( $internal_scripts as $internal_script )
			{
				$output .= "\n// Internal script from {$target}{$method}():\n".$internal_script;
			}
		}
	}

	// Send the predefined cookies:
	evo_sendcookies();

	if( $send_as_html )
	{	// we want to send as a html document
		if( ! headers_sent() )
		{	// Send headers only when they are not send yet to avoid an error:
			headers_content_mightcache( 'text/html', 0 );		// Do NOT cache interactive communications.
		}
		echo '<html><head></head><body><script>'."\n";
		echo $output;
		echo '</script></body></html>';
	}
	else
	{	// we want to send as js
		if( ! headers_sent() )
		{	// Send headers only when they are not send yet to avoid an error:
			headers_content_mightcache( 'text/javascript', 0 );		// Do NOT cache interactive communications.
		}
		global $baseurl;
		if( empty( $_SERVER['HTTP_REFERER'] ) ||
		    ! ( $referer_url = parse_url( $_SERVER['HTTP_REFERER'] ) ) || empty( $referer_url['host'] ) ||
		    ! ( $baseurl_info = parse_url( $baseurl ) ) || empty( $baseurl_info['host'] ) ||
		    ( $baseurl_info['host'] != $referer_url['host'] ) )
		{	// Deny request from other server:
			echo 'alert( \''.sprintf( TS_('This action can only be performed with HTTP_REFERER = %s'), $baseurl ).' \');';
		}
		else
		{	// Send JS content only for allowed referer:
			echo $output;
		}
	}

	exit(0);
}


/**
 * Basic tidy up of strings for using in JavaScript
 *
 * @author Yabba
 * @author Tblue
 *
 * @param string Unformatted raw data
 * @return string Formatted data
 */
function format_to_js( $unformatted )
{
	// Convert the following chars:
	// \               => \\
	// '               => \'
	// \n              => \\n
	// newline         => \n
	// \r              => \\r
	// carriage return => \r
	// \t              => \\t
	// tab space       => \t
	return addcslashes( $unformatted, "\\'\n\r\t" );
}


/**
 * Get available cort oprions for items
 *
 * @param integer|NULL Collection ID to get only enabled item types for the collection, NULL to get all item types
 * @param boolean true to enable none option
 * @param boolean true to also return custom field options, false otherwise
 * @return array key=>name or array( 'general' => array( key=>name ), 'custom' => array( key=>name ) )
 */
function get_available_sort_options( $coll_ID = NULL, $allow_none = false, $include_custom_fields = false )
{
	$options = array();

	if( $allow_none )
	{ // Enable none option
		$options[''] = T_('None');
	}

	$options = array_merge( $options, array(
		'datestart'                => T_('Date issued (Default)'),
		'order'                    => T_('Order (as explicitly specified)'),
		//'datedeadline'           => T_('Deadline'),
		'title'                    => T_('Title'),
		'datecreated'              => T_('Date created'),
		'datemodified'             => T_('Date last modified'),
		'last_touched_ts'          => T_('Date last touched'),
		'contents_last_updated_ts' => T_('Contents last updated'),
		'urltitle'                 => T_('URL "filename"'),
		'priority'                 => T_('Priority'),
		'numviews'                 => T_('Number of members who have viewed the post (If tracking enabled)'),
		'RAND'                     => T_('Random order!'),
	) );

	if( $include_custom_fields )
	{	// We need to include custom fields as well:
		global $DB;

		$SQL = new SQL( 'Get custom fields for items sort options' );
		$SQL->SELECT( 'DISTINCT( CONCAT( "custom_", itcf_type, "_", itcf_name ) ), itcf_name' );
		$SQL->FROM( 'T_items__type_custom_field' );
		$SQL->FROM_add( 'INNER JOIN T_items__type ON ityp_ID = itcf_ityp_ID' );
		$SQL->WHERE( 'ityp_usage = "post"' );
		if( ! empty( $coll_ID ) )
		{	// Restrict by enabled item types for the given collection:
			$SQL->FROM_add( 'INNER JOIN T_items__type_coll ON itc_ityp_ID = ityp_ID' );
			$SQL->WHERE_and( 'itc_coll_ID = '.$DB->quote( $coll_ID ) );
		}
		$custom_fields = $DB->get_assoc( $SQL );

		// Add custom fields as available sort options:
		return array( 'general' => $options, 'custom' => $custom_fields );
	}

	return $options;
}


/**
 * Get available sort options for blogs
 *
 * @return array key=>name
 */
function get_coll_sort_options()
{
	return array(
		'order'        => T_('Order (Default)'),
		'ID'           => T_('Blog ID'),
		'name'         => T_('Name'),
		'shortname'    => T_('Short name'),
		'tagline'      => T_('Tagline'),
		'shortdesc'    => T_('Short Description'),
		'urlname'      => T_('URL "filename"'),
		'RAND'         => T_('Random order!'),
	);
}


/**
 * Converts array to form option list
 *
 * @param array of option values and descriptions
 * @param integer|array selected keys
 * @param array provide a choice for "none_value" with value ''
 * @return string
 */
function array_to_option_list( $array, $default = '', $allow_none = array() )
{
	if( !is_array( $default ) )
	{
		$default = array( $default );
	}

	$r = '';

	if( !empty($allow_none) )
	{
		$r .= '<option value="'.$allow_none['none_value'].'"';
		if( empty($default) ) $r .= ' selected="selected"';
		$r .= '>'.format_to_output($allow_none['none_text']).'</option>'."\n";
	}

	foreach( $array as $k=>$v )
	{
		$r .=  '<option value="'.format_to_output($k,'formvalue').'"';
		if( in_array( $k, $default ) ) $r .= ' selected="selected"';
		$r .= '>';
		$r .= format_to_output( $v, 'htmlbody' );
		$r .=  '</option>'."\n";
	}

	return $r;
}


/**
 * Get a value from a volatile/lossy cache.
 *
 * @param string key
 * @param boolean success (by reference)
 * @return mixed True in case of success, false in case of failure. NULL, if no backend is available.
 */
function get_from_mem_cache( $key, & $success )
{
	global $Timer;

	$Timer->resume( 'get_from_mem_cache', false );

	if( function_exists( 'apcu_fetch' ) )
	{	// APCu
		$r = apcu_fetch( $key, $success );
	}
	elseif( function_exists( 'apc_fetch' ) )
	{	// APC
		$r = apc_fetch( $key, $success );
	}
	elseif( function_exists( 'xcache_get' ) && ini_get( 'xcache.var_size' ) > 0 )
	{	// XCache
		$r = xcache_get( $key );
	}

	if( ! isset($success) )
	{ // set $success for implementation that do not set it itself (only APC does so)
		$success = isset($r);
	}
	if( ! $success )
	{
		$r = NULL;

		global $Debuglog;
		$Debuglog->add( 'No caching backend available for reading "'.$key.'".', 'cache' );
	}

	$Timer->pause( 'get_from_mem_cache', false );

	return $r;
}


/**
 * Set a value to a volatile/lossy cache.
 *
 * There's no guarantee that the data is still available, since e.g. old
 * values might get purged.
 *
 * @param string key
 * @param mixed Data. Objects would have to be serialized.
 * @param int Time to live (seconds). Default is 0 and means "forever".
 * @return mixed
 */
function set_to_mem_cache( $key, $payload, $ttl = 0 )
{
	global $Timer;

	$Timer->resume( 'set_to_mem_cache', false );

	if( function_exists( 'apcu_store' ) )
	{	// APCu
		$r = apcu_store( $key, $payload, $ttl );
	}
	elseif( function_exists( 'apc_store' ) )
	{	// APC
		$r = apc_store( $key, $payload, $ttl );
	}
	elseif( function_exists( 'xcache_set' ) && ini_get( 'xcache.var_size' ) > 0 )
	{	// XCache
		$r = xcache_set( $key, $payload, $ttl );
	}
	else
	{	// No available cache module:
		global $Debuglog;
		$Debuglog->add( 'No caching backend available for writing "'.$key.'".', 'cache' );
		$r = NULL;
	}

	$Timer->pause( 'set_to_mem_cache', false );

	return $r;
}


/**
 * Remove a given key from the volatile/lossy cache.
 *
 * @param string key
 * @return boolean True on success, false on failure. NULL if no backend available.
 */
function unset_from_mem_cache( $key )
{
	if( function_exists( 'apc_delete' ) )
	{	// APC
		return apc_delete( $key );
	}

	if( function_exists( 'xcache_unset' ) )
	{	// XCache
		return xcache_unset( gen_key_for_cache( $key ) );
	}

	if( function_exists( 'apcu_delete' ) )
	{	// APCu
		return apcu_delete( $key );
	}
}


/**
 * Generate order by clause
 *
 * @param string The order values are separated by space or comma
 * @param string An order direction: ASC, DESC
 * @param string DB prefix
 * @param string ID field name with prefix
 * @param array Names of DB fields(without prefix) that are available
 * @return string The order fields are separated by comma
 */
function gen_order_clause( $order_by, $order_dir, $dbprefix, $dbIDname_disambiguation, $available_fields = NULL )
{
	$order_by = str_replace( ' ', ',', $order_by );
	$orderby_array = explode( ',', $order_by );

	$order_dir = explode( ',', str_replace( ' ', ',', $order_dir ) );

	if( is_array( $available_fields ) )
	{ // Exclude the incorrect fields from order clause
		foreach( $orderby_array as $i => $orderby_field )
		{
			if( ! in_array( $orderby_field, $available_fields ) )
			{
				unset( $orderby_array[ $i ] );
			}
		}
	}

	// Format each order param with default column names:
	foreach( $orderby_array as $i => $orderby_value )
	{ // If the order_by field contains a '.' character which is a table separator we must not use the prefix ( E.g. temp_table.value )
		$use_dbprefix = ( strpos( $orderby_value, '.' ) !== false ) ? '' : $dbprefix;
		$orderby_array[ $i ] = $use_dbprefix.$orderby_value.' '.( isset( $order_dir[ $i ] ) ? $order_dir[ $i ] : $order_dir[0] );
	}

	// Add an ID parameter to make sure there is no ambiguity in ordering on similar items:
	$orderby_array[] = $dbIDname_disambiguation.' '.$order_dir[0];

	$order_by = implode( ', ', $orderby_array );

	// Special case for RAND:
	$order_by = str_replace( $dbprefix.'RAND ', 'RAND() ', $order_by );

	return $order_by;
}


/**
 * Get the IconLegend instance.
 *
 * @return IconLegend or false, if the user has not set "display_icon_legend"
 */
function & get_IconLegend()
{
	static $IconLegend;

	if( ! isset($IconLegend) )
	{
		global $UserSettings;
		if( $UserSettings->get('display_icon_legend') )
		{
			/**
			 * Icon Legend
			 */
			load_class( '_core/ui/_iconlegend.class.php', 'IconLegend' );
			$IconLegend = new IconLegend();
		}
		else
		{
			$IconLegend = false;
		}
	}
	return $IconLegend;
}


/**
 * Get name of active opcode cache, or "none".
 * {@internal Anyone using something else, please extend.}}
 * @return string
 */
function get_active_opcode_cache()
{
	if( function_exists('opcache_invalidate') )
	{
		return 'OPCache';
	}

	if( function_exists('apc_delete_file') )
	{
		return 'APC';
	}

	// xcache: xcache.var_size must be > 0. xcache_set is not necessary (might have been disabled).
	if( ini_get('xcache.size') > 0 )
	{
		return 'xcache';
	}

	if( ini_get('eaccelerator.enable') )
	{
		$eac_info = eaccelerator_info();
		if( $eac_info['cache'] )
		{
			return 'eAccelerator';
		}
	}

	return 'none';
}


/**
 * Get name of active user cache, or "none".
 * {@internal Anyone using something else, please extend.}}
 * @return string
 */
function get_active_user_cache()
{
	if( function_exists( 'apcu_cache_info' ) /* && ini_get( 'apc.enabled' ) */ )
	{
		return 'APCu';
	}

	if( function_exists( 'apc_cache_info' ) /* && ini_get( 'apc.enabled' ) */ )
	{
		return 'APC';
	}

	// xcache: xcache.var_size must be > 0. xcache_set is not necessary (might have been disabled).
	if( ini_get('xcache.size') > 0 )
	{
		return 'xcache';
	}

	return 'none';
}


/**
 * Invalidate all page caches.
 * This function should be processed every time, when some users or global settings was modified,
 * and this modification has an imortant influence for the front office display.
 * Modifications that requires to invalidate all page caches:
 *   - installing/removing/reloading/enabling/disabling plugins
 *   - editing user settings like allow profile pics, new users can register, user settings>display
 */
function invalidate_pagecaches()
{
	global $DB, $Settings, $servertimenow;

	// get current server time
	$timestamp = ( empty( $servertimenow ) ? time() : $servertimenow );

	// get all blog ids
	if( $blog_ids = $DB->get_col( 'SELECT blog_ID FROM T_blogs' ) )
	{	// build invalidate query
		$query = 'REPLACE INTO T_coll_settings ( cset_coll_ID, cset_name, cset_value ) VALUES';
		foreach( $blog_ids as $blog_id )
		{
			$query .= ' ('.$blog_id.', "last_invalidation_timestamp", '.$timestamp.' ),';
		}
		$query = substr( $query, 0, strlen( $query ) - 1 );
		$DB->query( $query, 'Invalidate blogs\'s page caches' );
	}

	// Invalidate general cache content also
	$Settings->set( 'last_invalidation_timestamp', $timestamp );
	$Settings->dbupdate();
}


/**
* Get $ReqPath, $ReqURI
*
* @return array ($ReqPath,$ReqURI);
*/
function get_ReqURI()
{
	global $Debuglog;

	// Investigation for following code by Isaac - http://isaacschlueter.com/
	if( isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI']) )
	{ // Warning: on some IIS installs it it set but empty!
		$Debuglog->add( 'vars: vars: Getting ReqURI from REQUEST_URI', 'request' );
		$ReqURI = $_SERVER['REQUEST_URI'];

		// Build requested Path without query string:
		$pos = strpos( $ReqURI, '?' );
		if( false !== $pos )
		{
			$ReqPath = substr( $ReqURI, 0, $pos  );
		}
		else
		{
			$ReqPath = $ReqURI;
		}
	}
	elseif( isset($_SERVER['URL']) )
	{ // ISAPI
		$Debuglog->add( 'vars: Getting ReqPath from URL', 'request' );
		$ReqPath = $_SERVER['URL'];
		$ReqURI = isset($_SERVER['QUERY_STRING']) && !empty( $_SERVER['QUERY_STRING'] ) ? ($ReqPath.'?'.$_SERVER['QUERY_STRING']) : $ReqPath;
	}
	elseif( isset($_SERVER['PATH_INFO']) )
	{ // CGI/FastCGI
		if( isset($_SERVER['SCRIPT_NAME']) )
		{
			$Debuglog->add( 'vars: Getting ReqPath from PATH_INFO and SCRIPT_NAME', 'request' );

			if ($_SERVER['SCRIPT_NAME'] == $_SERVER['PATH_INFO'] )
			{	/* both the same so just use one of them
				 * this happens on a windoze 2003 box
				 * gotta love microdoft
				 */
				$Debuglog->add( 'vars: PATH_INFO and SCRIPT_NAME are the same', 'request' );
				$Debuglog->add( 'vars: Getting ReqPath from PATH_INFO only instead', 'request' );
				$ReqPath = $_SERVER['PATH_INFO'];
			}
			else
			{
				$ReqPath = $_SERVER['SCRIPT_NAME'].$_SERVER['PATH_INFO'];
			}
		}
		else
		{ // does this happen??
			$Debuglog->add( 'vars: Getting ReqPath from PATH_INFO only!', 'request' );

			$ReqPath = $_SERVER['PATH_INFO'];
		}
		$ReqURI = isset($_SERVER['QUERY_STRING']) && !empty( $_SERVER['QUERY_STRING'] ) ? ($ReqPath.'?'.$_SERVER['QUERY_STRING']) : $ReqPath;
	}
	elseif( isset($_SERVER['ORIG_PATH_INFO']) )
	{ // Tomcat 5.5.x with Herbelin PHP servlet and PHP 5.1
		$Debuglog->add( 'vars: Getting ReqPath from ORIG_PATH_INFO', 'request' );
		$ReqPath = $_SERVER['ORIG_PATH_INFO'];
		$ReqURI = isset($_SERVER['QUERY_STRING']) && !empty( $_SERVER['QUERY_STRING'] ) ? ($ReqPath.'?'.$_SERVER['QUERY_STRING']) : $ReqPath;
	}
	elseif( isset($_SERVER['SCRIPT_NAME']) )
	{ // Some Odd Win2k Stuff
		$Debuglog->add( 'vars: Getting ReqPath from SCRIPT_NAME', 'request' );
		$ReqPath = $_SERVER['SCRIPT_NAME'];
		$ReqURI = isset($_SERVER['QUERY_STRING']) && !empty( $_SERVER['QUERY_STRING'] ) ? ($ReqPath.'?'.$_SERVER['QUERY_STRING']) : $ReqPath;
	}
	elseif( isset($_SERVER['PHP_SELF']) )
	{ // The Old Stand-By
		$Debuglog->add( 'vars: Getting ReqPath from PHP_SELF', 'request' );
		$ReqPath = $_SERVER['PHP_SELF'];
		$ReqURI = isset($_SERVER['QUERY_STRING']) && !empty( $_SERVER['QUERY_STRING'] ) ? ($ReqPath.'?'.$_SERVER['QUERY_STRING']) : $ReqPath;
	}
	else
	{
		$ReqPath = false;
		$ReqURI = false;
		?>
		<p class="error">
		Warning: $ReqPath could not be set. Probably an odd IIS problem.
		</p>
		<p>
		Go to your <a href="<?php echo $baseurl.$install_subdir ?>phpinfo.php">phpinfo page</a>,
		look for occurences of <code><?php
		// take the baseurlroot out..
		echo preg_replace('#^'.preg_quote( $baseurlroot, '#' ).'#', '', $baseurl.$install_subdir )
		?>phpinfo.php</code> and copy all lines
		containing this to the <a href="http://forums.b2evolution.net">forum</a>. Also specify what webserver
		you're running on.
		<br />
		(If you have deleted your install folder &ndash; what is recommended after successful setup &ndash;
		you have to upload it again before doing this).
		</p>
		<?php
	}

	$r = array( $ReqPath, $ReqURI );

	// Format several danger chars to urlencoded format to avoid issues:
	$r = str_replace( array( '"', '\'', '<', '>' ), array( '%22', '%27', '%3C', '%3E' ), $r );

	return $r;
}


/**
 * Get URL to REST API script depending on current collection base url from front-office or site base url from back-office
 *
 * Note: For back-office or no collection page _init_hit.inc.php should be called before this call, because ReqHost and ReqPath must be initialized
 *
 * @return string URL to htsrv folder
 */
function get_restapi_url()
{
	global $restapi_script;

	return get_htsrv_url().$restapi_script;
}


/**
 * Force URL from http to https protocol
 *
 * @param string Original URL
 * @param boolean|string TRUE to force without settings checking,
 *                       FALSE to keep original URL without forcing,
 *                       'login' - Force only when it is enabled by setting "Require SSL"
 * @return string Forced URL
 */
function force_https_url( $url, $force_https = true )
{
	if( $force_https === 'login' )
	{	// Force url to use https if it is defiend in the setting "Require SSL":
		global $Settings;
		$force_https = (boolean)$Settings->get( 'require_ssl' );
	}

	if( $force_https === true )
	{	// Force URL only when it is requested by param or enabled by setting checking above:
		$url = preg_replace( '#^http://#i', 'https://', $url );
	}

	return $url;
}


/**
 * Check and redirect if the requested URL must be used as https instead of http
 *
 * @param boolean|string TRUE to force without settings checking,
 *                       FALSE to keep original URL without forcing,
 *                       'login' - Force only when it is enabled by setting "Require SSL"
 * @param string Original URL, NULL - to use current URL = $ReqURL
 */
function check_https_url( $force_https = true, $url = NULL )
{
	if( $url === NULL )
	{	// Use current URL by default:
		global $ReqURL;
		if( empty( $ReqURL ) )
		{	// If this URL is not defined yet:
			return;
		}
		$url = $ReqURL;
	}

	// Try to force the requested URL:
	$forced_url = force_https_url( $url, $force_https );

	if( $forced_url != $url )
	{	// If the requested is wrong then redirect to correct what must be used instead:
		header_redirect( $forced_url );
	}
}


/**
 * Get URL to htsrv folder depending on current collection base url from front-office or site base url from back-office
 *
 * Note: For back-office or no collection page _init_hit.inc.php should be called before this call, because ReqHost and ReqPath must be initialized
 *
 * @param boolean TRUE to use https URL
 * @return string URL to htsrv folder
 */
function get_htsrv_url( $force_https = false )
{
	global $Collection, $Blog;

	if( is_admin_page() || empty( $Blog ) )
	{	// For back-office or when no collection page:
		return get_samedomain_htsrv_url( $force_https );
	}
	else
	{	// For current collection:
		return $Blog->get_local_htsrv_url( NULL, $force_https );
	}
}


/**
 * Get htsrv url on the same domain as the http request came from
 *
 * Note: _init_hit.inc.php should be called before this call, because ReqHost and ReqPath must be initialized
 *
 * @param boolean|string TRUE to use https URL
 * @return string URL to htsrv folder
 */
function get_samedomain_htsrv_url( $force_https = false )
{
	global $ReqHost, $ReqPath, $baseurl, $htsrv_url, $htsrv_subdir, $Collection, $Blog, $is_cli;

	// Force URL if it is required and enabled by settings:
	$req_htsrv_url = force_https_url( $htsrv_url, $force_https );

	// Cut htsrv folder from end of the URL:
	$req_htsrv_url = substr( $req_htsrv_url, 0, strlen( $req_htsrv_url ) - strlen( $htsrv_subdir ) );

	if( $is_cli || empty( $ReqHost ) || strpos( $ReqHost.$ReqPath, $req_htsrv_url ) !== false )
	{	// If current request path contains the required htsrv URL
		// or $ReqHost is not initialized e-g on install
		// or this is CLI mode where $ReqHost is not defined:
		return $req_htsrv_url.$htsrv_subdir;
	}

	$baseurl_parts = @parse_url( $baseurl );
	$req_url_parts = @parse_url( $ReqHost );
	$htsrv_url_parts = @parse_url( $req_htsrv_url );
	if( ( ! isset( $baseurl_parts['host'] ) ) ||
	    ( ! isset( $req_url_parts['host'] ) ) ||
	    ( ! isset( $htsrv_url_parts['host'] ) ) )
	{
		debug_die( 'Invalid hosts!' );
	}

	if( $req_url_parts['host'] == $baseurl_parts['host'] &&
	    ! isset( $req_url_parts['path'] ) &&
	    isset( $baseurl_parts['path'] ) )
	{	// Don't miss folder of base url from url like http://site.com/folder/:
		$req_url_parts['path'] = $baseurl_parts['path'];
	}

	$req_domain = rtrim( $req_url_parts['host'].( isset( $req_url_parts['path'] ) ? $req_url_parts['path'] : '' ), '/' );
	$htsrv_domain = rtrim( $htsrv_url_parts['host'].( isset( $htsrv_url_parts['path'] ) ? $htsrv_url_parts['path'] : '' ), '/' );

	// Replace domain + path of htsrv URL with current request:
	$samedomain_htsrv_url = substr_replace( $req_htsrv_url, $req_domain, strpos( $req_htsrv_url, $htsrv_domain ), strlen( $htsrv_domain ) );

	// Revert htsrv folder to end of the URL which has been cut above:
	$samedomain_htsrv_url .= $htsrv_subdir;

	// fp> The following check would apply well if we always had 301 redirects.
	// But it's possible to turn them off in SEO settings for some page and not others (we don't know which here)
  // And some kinds of pages do not have 301 redirections implemented yet, e-g: disp=users
  /*
	if( ( !is_admin_page() ) && ( !empty( $Blog ) ) && ( $samedomain_htsrv_url != $Blog->get_htsrv_url( $secure ) ) )
	{
		debug_die( 'The blog is configured to have /htsrv/ at:<br> '.$Blog->get_htsrv_url( $secure ).'<br>but in order to stay on the current domain, we would need to use:<br>'.$samedomain_htsrv_url.'<br>Maybe we have a missing redirection to the proper blog url?' );
	}
	*/

	return $samedomain_htsrv_url;
}


/**
 * Set max execution time
 *
 * @param integer seconds
 * @return string the old value on success, false on failure.
 */
function set_max_execution_time( $seconds )
{
	if( function_exists( 'set_time_limit' ) )
	{
		set_time_limit( $seconds );
	}
	return @ini_set( 'max_execution_time', $seconds );
}


/**
 * Sanitize a comma-separated list of numbers (IDs)
 *
 * @param string
 * @param bool Return array if true, string otherwise
 * @param bool Quote each element (for use in SQL queries)
 * @return string
 */
function sanitize_id_list( $str, $return_array = false, $quote = false )
{
	if( is_null($str) )
	{	// Allow NULL values
		$str = '';
	}

	// Explode and trim
	$array = array_map( 'trim', explode(',', $str) );

	// Convert to integer and remove all empty values
	$array = array_filter( array_map('intval', $array) );

	if( !$return_array && $quote )
	{	// Quote each element and return a string
		global $DB;
		return $DB->quote($array);
	}
	return ( $return_array ? $array : implode(',', $array) );
}


/**
 * A wrapper for json_encode function
 * We need to pass valid UTF-8 string to json_encode, otherwise it may return NULL
 *
 * @param mixed
 * @return string
 */
function evo_json_encode( $a = false )
{
	if( is_string( $a ) )
	{ // Convert to UTF-8
		$a = current_charset_to_utf8( $a );
	}
	elseif( is_array( $a ) )
	{ // Recursively convert to UTF-8
		array_walk_recursive( $a, 'current_charset_to_utf8' );
	}

	$result = json_encode( $a );
	if( $result === false )
	{ // If json_encode returns FALSE because of some error we should set correct json empty value as '[]' instead of false
		$result = '[]';
	}

	return $result;
}


/**
 * A helper function to conditionally convert a string from current charset to UTF-8
 *
 * @param string
 * @return string
 */
function current_charset_to_utf8( & $a )
{
	global $current_charset;

	if( is_string( $a ) && $current_charset != '' && $current_charset != 'utf-8' )
	{ // Convert string to utf-8 if it has another charset
		$a = convert_charset( $a, 'utf-8', $current_charset );
	}

	return $a;
}


if( !function_exists( 'property_exists' ) )
{
	/**
	 * Create property_exists function if it does not exist ( PHP < 5.1 )
	 * @param object
	 * @param string
	 *
	 * @return bool
	 */
	function property_exists( $class, $property )
	{
		if( is_object( $class ) )
		{
			$vars = get_object_vars( $class );
		}
		else
		{
			$vars = get_class_vars( $class );
		}
		return array_key_exists( $property, $vars );
	}
}


// fp>vitaliy: move to a file that is not included everywhere!
/**
 * Update global $http_response_code and call function header()
 *
 * NOTICE: When you start to use new code please add it to the hits filter "HTTP resp"
 *         in the file "/inc/sessions/views/_stats_view.funcs.php",
 *         function filter_hits(), array $resp_codes
 *
 * @param string Header
 * @param integer Header response code
 */
function header_http_response( $string, $code = NULL )
{
	global $http_response_code;

	$string = 'HTTP/1.1 '. $string;

	if( is_null( $code ) )
	{
		if( preg_match( '/(\d{3})/', $string, $matches ) )
		{
			$http_response_code = (int)$matches[0];
		}
	}
	else
	{
		$http_response_code = $code;
	}

	header( $string );
}


/**
 * Add a trailing slash, if none present
 *
 * @param string the path/url
 * @return string the path/url with trailing slash
 */
function trailing_slash( $path )
{
	if( empty($path) || utf8_substr( $path, -1 ) == '/' )
	{
		return $path;
	}
	else
	{
		return $path.'/';
	}
}


/**
 * Remove trailing slash, if present
 *
 * @param string the path/url
 * @return string the path/url without trailing slash
 */
function no_trailing_slash( $path )
{
	if( utf8_substr( $path, -1 ) == '/' )
	{
		return utf8_substr( $path, 0, utf8_strlen( $path )-1 );
	}
	else
	{
		return $path;
	}
}


/**
 * Convert integer to IP address
 *
 * @param integer Number
 * @return string IP address
 */
function int2ip( $int )
{
	$ip = array();
	$ip[0] = (int) ( $int / 256 / 256 / 256 );
	$ip[1] = (int) ( ( $int - ( $ip[0] * 256 * 256 * 256 ) ) / 256 / 256 );
	$ip[2] = (int) ( ( $int - ( $ip[0] * 256 * 256 * 256 ) - ( $ip[1] * 256 * 256 ) ) / 256 );
	$ip[3] = $int - ( $ip[0] * 256 * 256 * 256 ) - ( $ip[1] * 256 * 256 ) - ( $ip[2] * 256 );

	return $ip[0].'.'.$ip[1].'.'.$ip[2].'.'.$ip[3];
}


/**
 * Check if the given string is a valid IPv4 or IPv6 address value
 *
 * @param string IP
 * @return boolean true if valid, false otherwise
 */
function is_valid_ip_format( $ip )
{
	return filter_var( $ip, FILTER_VALIDATE_IP ) !== false;
}




/**
 * Convert IP address to integer (get only 32bits of IPv6 address)
 *
 * @param string IP address
 * @return integer Number
 */
function ip2int( $ip )
{
	if( ! is_valid_ip_format( $ip ) )
	{ // IP format is incorrect
		return 0;
	}

	if( $ip == '::1' )
	{	// Reserved IP for localhost
		$ip = '127.0.0.1';
	}

	$parts = unpack( 'N*', inet_pton( $ip ) );
	// In case of IPv6 return only a parts of it
	$result = ( strpos( $ip, '.' ) !== false ) ? $parts[1] /* IPv4*/ : $parts[4] /* IPv6*/;

	if( $result < 0 )
	{ // convert unsigned int to signed from unpack.
		// this should be OK as it will be a PHP float not an int
		$result += 4294967296;
	}

	return $result;
}


/**
 * Check if URL has a domain in IP format
 *
 * @param string URL
 * @return boolean
 */
function is_ip_url_domain( $url )
{
	$url_data = parse_url( $url );

	if( $url_data === false || ! isset( $url_data['host'] ) )
	{	// Wrong url:
		return false;
	}

	// Check if host is IP address:
	return is_valid_ip_format( $url_data['host'] );
}


/**
 * Save text data to file, create target file if it doesn't exist
 *
 * @param string data to be written
 * @param string filename (full path to a file)
 * @param string fopen mode
 */
function save_to_file( $data, $filename, $mode = 'a' )
{
	global $Settings, $evo_save_file_error_msg;

	if( ! file_exists( $filename ) )
	{	// Try to create a target file:
		if( ! @touch( $filename ) )
		{	// If file could not be created:
			$evo_save_file_error_msg = T_('File could not be created!');
			return false;
		}

		// Doesn't work during installation
		if( ! empty( $Settings ) )
		{
			$chmod = $Settings->get( 'fm_default_chmod_file' );
			@chmod( $filename, octdec( $chmod ) );
		}
	}

	if( ! is_writable( $filename ) )
	{	// File is not writable:
		$evo_save_file_error_msg = T_('File is not writable!');
		return false;
	}

	if( ! ( $f = @fopen( $filename, $mode ) ) )
	{	// Could not open file:
		$evo_save_file_error_msg = T_('File could not be opened for writing data!');
		return false;
	}
	if( ! @fwrite( $f, $data ) )
	{	// Could not write data into file:
		$evo_save_file_error_msg = T_('Data could not be written to the file!');
		return false;
	}
	@fclose( $f );

	if( ! file_exists( $filename ) )
	{	// Additonal check for existing file on disk:
		$evo_save_file_error_msg = T_('File doesn\'t exist!');
		return false;
	}

	// Reset error log on success result:
	$evo_save_file_error_msg = '';

	// Return file name on success result:
	return $filename;
}


/**
 * Check if current request is AJAX
 *
 * @return boolean TRUE/FALSE
 */
function is_ajax_request()
{
	global $is_ajax_request;

	return isset( $is_ajax_request ) && $is_ajax_request === true;
}


/**
 * Check if current request is AJAX content
 * Used in order to get only content of the requested page
 *
 * @param string Template name
 * @return boolean TRUE/FALSE
 */
function is_ajax_content( $template_name = '' )
{
	global $ajax_content_mode;

	// Template names of content: @see skin_include()
	$content_templates = array( '$disp$', '_item_block.inc.php', '_item_content.inc.php' );

	return !empty( $ajax_content_mode ) &&
		$ajax_content_mode === true &&
		!in_array( $template_name, $content_templates );
}


/**
 * Add a message to AJAX Log
 *
 * @param string Message
 * @param string|array Category, default is to use the object's default category.
 *        Can also be an array of categories to add the same message to.
 */
function ajax_log_add( $message, $category = NULL )
{
	global $ajax_Log;

	if( ! isset( $ajax_Log ) ||
	    ! ( $ajax_Log instanceof Log ) )
	{	// AJAX Log is not initialized:
		return;
	}

	// Add a message to AJAX Log:
	$ajax_Log->add( $message, $category );
}


/**
 * Display AJAX log
 */
function ajax_log_display()
{
	global $ajax_Log, $debug, $debug_jslog, $current_debug, $current_debug_jslog;

	if( ! ( $debug || $debug_jslog || $current_debug || $current_debug_jslog ) )
	{	// At least one debug must be enabled:
		return;
	}

	if( ! isset( $ajax_Log ) ||
	    ! ( $ajax_Log instanceof Log ) )
	{	// AJAX Log is not initialized:
		return;
	}

	// Print out AJAX Log messages:
	$ajax_Log->display( NULL, NULL, true, 'all',
		array(
				'error' => array( 'class' => 'jslog_error', 'divClass' => false ),
				'note'  => array( 'class' => 'jslog_note',  'divClass' => false ),
			), 'ul', 'jslog' );
}


/**
 * Insert system log into DB
 *
 * @param string Message text
 * @param string Log type: 'info', 'warning', 'error', 'critical_error'
 * @param string Object type: 'comment', 'item', 'user', 'file', 'email_log' or leave default NULL if none of them
 * @param integer Object ID
 * @param string Origin type: 'core', 'plugin'
 * @param integer Origin ID
 * @param integer User ID
 */
function syslog_insert( $message, $log_type, $object_type = NULL, $object_ID = NULL, $origin_type = 'core', $origin_ID = NULL, $user_ID = NULL )
{
	global $servertimenow;

	$Syslog = new Syslog();
	$Syslog->set_user( $user_ID );
	$Syslog->set( 'type', $log_type );
	$Syslog->set_origin( $origin_type, $origin_ID );
	$Syslog->set_object( $object_type, $object_ID );
	$Syslog->set_message( $message );
	$Syslog->set( 'timestamp', date2mysql( $servertimenow ) );
	$Syslog->dbinsert();
}


/**
 * Get a param to know where script is calling now, Used for JS functions
 *
 * @return string
 */
function request_from()
{
	global $request_from;

	if( !empty( $request_from ) )
	{ // AJAX request
		return $request_from;
	}

	if( is_admin_page() )
	{ // Backoffice
		global $ctrl;
		return !empty( $ctrl ) ? $ctrl : 'admin';
	}
	else
	{ // Frontoffice
		return 'front';
	}
}


/**
 * Get an error message text about file permissions
 */
function get_file_permissions_message()
{
	return sprintf( T_( '(Please check UNIX file permissions on the parent folder. %s)' ), get_manual_link( 'file-permissions' ) );
}


/**
 * Flush the output buffer
 */
function evo_flush()
{
	global $Timer, $disable_evo_flush;

	if( isset( $disable_evo_flush ) && $disable_evo_flush )
	{	// This function is disabled (for example, used for ajax/rest api requests)
		return;
	}

	// To fill the output buffer in order to flush data:
	// NOTE: Uncomment the below line if flush doesn't work as expected
	// echo str_repeat( ' ', 4096 );

	// New line char is required as flag of that output portion is printed out:
	echo "\n";

	$zlib_output_compression = ini_get( 'zlib.output_compression' );
	if( empty( $zlib_output_compression ) || $zlib_output_compression == 'Off' )
	{ // This function helps to turn off output buffering
		// But do NOT use it when zlib.output_compression is ON, because it creates the die errors

		// fp/yura TODO: we need to optimize this: We want to flush to screen and continue caching.
		//               This needs investigation and checking other similar places.
		global $PageCache;
		if( ! ( isset( $PageCache ) && ! empty( $PageCache->is_collecting ) ) )
		{ // Only when page cache is not running now because a notice error can appears in function PageCache->end_collect()
			@ob_end_flush();
		}
	}
	flush();

	if( isset( $Timer ) && $Timer->get_state( 'first_flush' ) == 'running' )
	{ // The first fulsh() was called, stop the timer
		$Timer->pause( 'first_flush' );
	}
}

// ---------- APM : Application Performance Monitoring -----------

/**
 * Name the transaction for the APM.
 * This avoids that every request be called 'index.php' or 'evoadm.php' or 'cron_exec.php'
 *
 * @param mixed $request_transaction_name
 */
function apm_name_transaction( $request_transaction_name )
{
	if(extension_loaded('newrelic'))
	{	// New Relic is installed on the server for monitoring.
		newrelic_name_transaction( $request_transaction_name );
	}
}

/**
 * Log a custom metric
 *
 * @param mixed $name name of the custom metric
 * @param mixed $value assumed to be in milliseconds (ms)
 */
function apm_log_custom_metric( $name, $value )
{
	if(extension_loaded('newrelic'))
	{	// New Relic is installed on the server for monitoring.
		newrelic_custom_metric( 'Custom/'.$name, $value );
	}
}

/**
 * Log a custom param
 *
 * @param mixed $name name of the custom param
 * @param mixed $value of the custom param
 */
function apm_log_custom_param( $name, $value )
{
	if(extension_loaded('newrelic'))
	{	// New Relic is installed on the server for monitoring.
		newrelic_add_custom_parameter( $name, $value );
	}
}


/**
 * Echo JavaScript to edit values of column in the table list
 *
 * @param array Params
 */
function echo_editable_column_js( $params = array() )
{
	$params = array_merge( array(
			'column_selector' => '', // jQuery selector of cell
			'ajax_url'        => '', // AJAX url to update a column value
			'options'         => array(), // Key = Value of option, Value = Title of option. Do not use Javascript code to populate this - use 'options_eval' param to do this.
			'new_field_name'  => '', // Name of _POST variable that will be send to ajax request with new value
			'ID_value'        => '', // jQuery to get value of ID
			'ID_name'         => '', // ID of field in DB
			'tooltip'         => T_('Click to edit'),
			'colored_cells'   => false, // Use TRUE when colors are used for background of cell
			'print_init_tags' => true, // Use FALSE to don't print <script> tags if it is already used inside js
			'field_type'      => 'select', // Type of the editable field: 'select', 'text'
			'field_class'     => '', // Class of the editable field
			'null_text'       => '', // Null text of an input field, Use TS_() to translate it
			'callback_code'   => '', // Additional JS code after main callback code
		), $params );

	expose_var_to_js( $params['column_selector'], $params, 'evo_init_editable_column_config' );
}


/**
 * Get a button class name depending on template
 *
 * @param string Type: 'button', 'button_text', 'button_group'
 * @param string TRUE - to get class value for jQuery selector
 * @return string Class name
 */
function button_class( $type = 'button', $jQuery_selector = false )
{
	// Default class names
	$classes = array(
			'button'       => 'roundbutton', // Simple button with icon
			'button_red'   => 'roundbutton_red', // Button with red background
			'button_green' => 'roundbutton_green', // Button with green background
			'text'         => 'roundbutton_text', // Button with text
			'text_primary' => 'roundbutton_text', // Button with text with special style color
			'text_success' => 'roundbutton_text', // Button with text with special style color
			'text_danger'  => 'roundbutton_text', // Button with text with special style color
			'group'        => 'roundbutton_group', // Group of the buttons
		);

	if( is_admin_page() )
	{ // Some admin skins may have special class names
		global $AdminUI;
		if( ! empty( $AdminUI ) )
		{
			$template_classes = $AdminUI->get_template( 'button_classes' );
		}
	}
	else
	{ // Some front end skins may have special class names
		global $Skin;
		if( ! empty( $Skin ) )
		{
			$template_classes = $Skin->get_template( 'button_classes' );
		}
	}
	if( !empty( $template_classes ) )
	{ // Get class names from admin template
		$classes = array_merge( $classes, $template_classes );
	}

	$class_name = isset( $classes[ $type ] ) ? $classes[ $type ] : '';

	if( $jQuery_selector && ! empty( $class_name ) )
	{ // Convert class name to jQuery selector
		$class_name = '.'.str_replace( ' ', '.', $class_name );
	}

	return $class_name;
}


/**
 * Initialize JavaScript to build and open window
 */
function echo_modalwindow_js()
{
	global $AdminUI, $Collection, $Blog, $modal_window_js_initialized;

	if( ! empty( $modal_window_js_initialized ) )
	{ // Don't print out these functions twice
		return;
	}

	// TODO: asimo> Should not use AdminUI templates for the openModalWindow function. The style part should be handled by css.
	if( is_admin_page() && isset( $AdminUI ) && $AdminUI->get_template( 'modal_window_js_func' ) !== false )
	{ // Use the modal functions from back-office skin
		$skin_modal_window_js_func = $AdminUI->get_template( 'modal_window_js_func' );
	}
	elseif( ! is_admin_page() && ! empty( $Blog ) )
	{ // Use the modal functions from front-office skin
		$blog_skin_ID = $Blog->get_skin_ID();
		$SkinCache = & get_SkinCache();
		$Skin = & $SkinCache->get_by_ID( $blog_skin_ID, false, false );
		if( $Skin && $Skin->get_template( 'modal_window_js_func' ) !== false )
		{
			$skin_modal_window_js_func = $Skin->get_template( 'modal_window_js_func' );
		}
	}

	if( ! empty( $skin_modal_window_js_func ) && is_string( $skin_modal_window_js_func ) && function_exists( $skin_modal_window_js_func ) )
	{ // Call skin function only if it exists
		call_user_func( $skin_modal_window_js_func );
		$modal_window_js_initialized = true;
		return;
	}

	$modal_window_js_initialized = true;
}

/**
 * Initialize JavaScript to build and open window for bootstrap skins
 */
function echo_modalwindow_js_bootstrap()
{
	// Initialize variables for the file "bootstrap-evo_modal_window.js":
	echo '<script>
		var evo_js_lang_close = \''.TS_('Close').'\'
		var evo_js_lang_loading = \''.TS_('Loading...').'\';
		var evo_js_lang_edit_image = \''.TS_('Insert or edit inline image').'\';
		var evo_js_lang_select_image_insert = \''.TS_('Select image to insert').'\';
		var evo_js_lang_alert_before_insert_item = \''.TS_('Save post to start uploading files').'\';
		var evo_js_lang_alert_before_insert_comment = \''.TS_('Save comment to start uploading files').'\';
		var evo_js_lang_alert_before_insert_emailcampaign = \''.TS_('Save email campaign to start uploading files').'\';
		var evo_js_lang_alert_before_insert_message = \''.TS_('Save message to start uploading files').'\';
	</script>';
}

/**
 * Initialize JavaScript to build and open WHOIS window
 */
function echo_whois_js_bootstrap()
{
	echo '<script>
		var evo_js_lang_close = \''.TS_('Close').'\';
		var evo_js_lang_loading = \''.TS_('Loading...').'\';
		var evo_js_lang_whois_title = \''.TS_('Querying WHOIS server...').'\';
	</script>';
}


/**
 * Handle fatal error in order to display info message when debug is OFF
 */
function evo_error_handler()
{
	global $evo_last_handled_error;

	// Get last error
	$error = error_get_last();

	if( ! empty( $error ) && $error['type'] === E_ERROR )
	{ // Save only last fatal error
		$evo_last_handled_error = $error;
	}

	// fp> WTF?!? and what about warnings?
	// fp> And where do we die()? why is there not a debug_die() here?
	// There should be ONE MILLION COMMENTS in this function to explain what we do!

}


/**
 * Get icon to collapse/expand fieldset
 *
 * @param string ID of fieldset
 * @param array Params
 * @return string Icon with hidden input field
 */
function get_fieldset_folding_icon( $id, $params = array() )
{
	if( ! is_logged_in() )
	{	// Only loggedin users can fold fieldset
		return;
	}

	$params = array_merge( array(
			'before'     => '',
			'after'      => ' ',
			'deny_fold'  => false, // TRUE to don't allow fold the block and keep it opened always on page loading
			'default_fold' => NULL, // Set default "fold" value for current icon
			'fold_value' => NULL,
		), $params );

	if( $params['deny_fold'] )
	{	// Deny folding for this case
		$value = 0;
	}
	elseif( ! is_null( $params['fold_value'] ) )
	{	// Fold value is specified, use this:
		$value = intval( $params['fold_value'] );
	}
	else
	{	// Get the fold value from user settings
		global $UserSettings, $Collection, $Blog, $ctrl;
		if( empty( $Blog ) || ( isset( $ctrl ) && in_array( $ctrl, array( 'plugins', 'user' ) ) ) )
		{	// Get user setting value
			$value = $UserSettings->get( 'fold_'.$id );
		}
		else
		{	// Get user-collection setting
			$value = $UserSettings->get_collection_setting( 'fold_'.$id, $Blog->ID );
		}
		if( $value === NULL && $params['default_fold'] !== NULL )
		{	// Use custom default value for this icon:
			$value = $params['default_fold'];
		}
		$value = intval( $value );
	}

	// Icon
	if( $value )
	{
		$icon_current = 'filters_show';
		$icon_reverse = 'filters_hide';
		$title_reverse = T_('Collapse');
	}
	else
	{
		$icon_current = 'filters_hide';
		$icon_reverse = 'filters_show';
		$title_reverse = T_('Expand');
	}
	$icon = get_icon( $icon_current, 'imgtag', array(
			'id'         => 'icon_folding_'.$id,
			'data-xy'    => get_icon( $icon_reverse, 'xy' ),
			'data-title' => format_to_output( $title_reverse, 'htmlattr' ),
		) );

	// Hidden input to store current value of the folding status
	$hidden_input = '<input type="hidden" name="folding_values['.$id.']" id="folding_value_'.$id.'" value="'.$value.'" />';

	return $hidden_input.$params['before'].$icon.$params['after'];
}


/**
 * Output JavaScript code to collapse/expand fieldset
 */
function echo_fieldset_folding_js()
{
	if( ! is_logged_in() )
	{ // Only loggedin users can fold fieldset
		return;
	}

	expose_var_to_js( 'evo_fieldset_folding_config', true );
}


/**
 * Save the values of fieldset folding into DB
 *
 * @param integer Blog ID is used to save setting per blog, NULL- to don't save per blog
 */
function save_fieldset_folding_values( $blog_ID = NULL )
{
	if( ! is_logged_in() )
	{ // Only loggedin users can fold fieldset
		return;
	}

	$folding_values = param( 'folding_values', 'array:integer' );

	if( empty( $folding_values ) )
	{ // No folding values go from request, Exit here
		return;
	}

	global $UserSettings;

	foreach( $folding_values as $key => $value )
	{
		$setting_name = 'fold_'.$key;
		if( $blog_ID !== NULL )
		{ // Save setting per blog
			$setting_name .= '_'.$blog_ID;
		}
		$UserSettings->set( $setting_name, $value );
	}

	// Update the folding setting for current user
	$UserSettings->dbupdate();
}


/**
 * Save the values of active tab pane into DB
 *
 * @param integer Blog ID is used to save setting per blog, NULL- to don't save per blog
 */
function save_active_tab_pane_value( $blog_ID = NULL )
{
	if( ! is_logged_in() )
	{ // Only loggedin users can fold fieldset
		return;
	}

	$tab_pane_value = param( 'tab_pane_active', 'array:string' );

	if( empty( $tab_pane_value ) )
	{ // No tab pane value go from request, Exit here
		return;
	}

	global $UserSettings;

	if( is_array( $tab_pane_value ) && ! empty( $tab_pane_value ) )
	{	// Get first key:
		$key = array_keys( $tab_pane_value );
		$key = trim( $key[0] );
	}
	else
	{
		$key = '';
	}

	$value = ( isset( $tab_pane_value[ $key ] ) ) ? $tab_pane_value[ $key ] : '';
	$value = trim( $value );
	
	if( ! empty( $key ) &&  ! empty( $value ) )
	{
		$setting_name = 'active_'.$key;

		if( $blog_ID !== NULL )
		{ // Save setting per blog
			$setting_name .= '_'.$blog_ID;
		}
		$UserSettings->set( $setting_name, $value );

		// Update the folding setting for current user
		$UserSettings->dbupdate();
	}
}


/**
 * Get html code of bootstrap dropdown element
 *
 * @param array Params
 */
function get_status_dropdown_button( $params = array() )
{
	$params = array_merge( array(
			'name'             => '',
			'value'            => '',
			'title_format'     => '',
			'options'          => NULL,
			'exclude_statuses' => array( 'trash' ),
		), $params );

	if( $params['options'] === NULL )
	{	// Get status options by title format:
		$status_options = get_visibility_statuses( $params['title_format'], $params['exclude_statuses'] );
	}
	else
	{	// Use status options from params:
		$status_options = $params['options'];
	}
	$status_icon_options = get_visibility_statuses( 'icons', $params['exclude_statuses'] );

	$r = '<div class="btn-group dropdown autoselected" data-toggle="tooltip" data-placement="top" data-container="body" title="'.get_status_tooltip_title( $params['value'] ).'">';
	$r .= '<button type="button" class="btn btn-status-'.$params['value'].' dropdown-toggle" data-toggle="dropdown" aria-expanded="false">'
					.'<span>'.$status_options[ $params['value'] ].'</span>'
				.' <span class="caret"></span></button>';
	$r .= '<ul class="dropdown-menu" role="menu" aria-labelledby="'.$params['name'].'">';
	foreach( $status_options as $status_key => $status_title )
	{
		$r .= '<li rel="'.$status_key.'" role="presentation"><a href="#" role="menuitem" tabindex="-1">'.$status_icon_options[ $status_key ].' <span>'.$status_title.'</span></a></li>';
	}
	$r .= '</ul>';
	$r .= '</div>';

	return $r;
}

/**
 * Output JavaScript code to work with dropdown bootstrap element
 */
function echo_form_dropdown_js()
{
	// Build a string to initialize javascript array with button titles
	$tooltip_titles = get_visibility_statuses( 'tooltip-titles' );
	$tooltip_titles_js_array = array();
	foreach( $tooltip_titles as $status => $tooltip_title )
	{
		$tooltip_titles_js_array[] = $status.': \''.TS_( $tooltip_title ).'\'';
	}
	$tooltip_titles_js_array = implode( ', ', $tooltip_titles_js_array );
?>
<script>
jQuery( '.btn-group.dropdown.autoselected li a' ).on( 'click', function()
{
	var item_status_tooltips = {<?php echo $tooltip_titles_js_array ?>};
	var item = jQuery( this ).parent();
	var status = item.attr( 'rel' );
	var btn_group = item.parent().parent();
	var button = jQuery( item.parent().parent().find( 'button:first' ) );
	var field_name = jQuery( this ).parent().parent().attr( 'aria-labelledby' );

	// Change status class name to new changed for all buttons:
	button.attr( 'class', button.attr( 'class' ).replace( /btn-status-[^\s]+/, 'btn-status-' + status ) );
	// Update selector button to status title:
	button.find( 'span:first' ).html( item.find( 'span:last' ).html() ); // update selector button to status title
	// Update hidden field to new status value:
	jQuery( 'input[type=hidden][name=' + field_name + ']' ).val( status );
	// Hide dropdown menu:
	item.parent().parent().removeClass( 'open' );

	// Update tooltip
	btn_group.tooltip( 'hide' ).attr( 'data-original-title', item_status_tooltips[status] ).tooltip( 'show' );

	return false;
} );
</script>
<?php
}


/**
 * Get baseurl depending on current called script
 *
 * @return string URL
 */
function get_script_baseurl()
{
	if( isset( $_SERVER['SERVER_NAME'] ) )
	{ // Set baseurl from current server name

		$temp_baseurl = 'http://'.$_SERVER['SERVER_NAME'];

		if( isset( $_SERVER['SERVER_PORT'] ) )
		{
			if( $_SERVER['SERVER_PORT'] == '443' )
			{	// Rewrite that as https:
				$temp_baseurl = 'https://'.$_SERVER['SERVER_NAME'];
			}
			elseif( $_SERVER['SERVER_PORT'] == '8890' )
			{	// Used for testing
				$temp_baseurl = 'https://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'];
			}
			elseif( $_SERVER['SERVER_PORT'] != '80' )
			{ // Get also a port number
				$temp_baseurl .= ':'.$_SERVER['SERVER_PORT'];
			}
		}

		if( isset( $_SERVER['SCRIPT_NAME'] ) )
		{ // Get also the subfolders, when script is called e.g. from http://localhost/blogs/b2evolution/
			$temp_baseurl .= preg_replace( '~(.*/)[^/]*$~', '$1', $_SERVER['SCRIPT_NAME'] );
		}
	}
	else
	{ // Use baseurl from config
		global $baseurl;
		$temp_baseurl = $baseurl;
	}

	return $temp_baseurl;
}


/**
 * Get badge to inform the settings are edited only by collection/user admins
 *
 * @param string Type: 'coll', 'user'
 * @param string Manual URL, '#' - default, false - don't set URL
 * @param string Text
 * @param string Title
 * @param string Value
 * @return string
 */
function get_admin_badge( $type = 'coll', $manual_url = '#', $text = '#', $title = '#', $value = NULL )
{
	switch( $type )
	{
		case 'coll':
			if( $text == '#' )
			{	// Use default text:
				$text = T_('Coll. Admin');
			}
			if( $title == '#' )
			{	// Use default title:
				$title = T_('This can only be edited by users with the Collection Admin permission.');
			}
			if( $manual_url == '#' )
			{	// Use default manual url:
				$manual_url = 'collection-admin';
			}
			break;

		case 'user':
			if( $text == '#' )
			{	// Use default text:
				$text = T_('User Admin');
			}
			if( $title == '#' )
			{	// Use default title:
				$title = T_('This can only be edited by users with the User Admin permission.');
			}
			if( $manual_url == '#' )
			{	// Use default manual url:
				$manual_url = 'user-admin';
			}
			break;

		default:
			// Unknown badge type:
			return '';
	}

	if( empty( $manual_url ) )
	{	// Don't use a link:
		$r = ' <b';
	}
	else
	{	// Use link:
		$r = ' <a href="'.get_manual_url( $manual_url ).'" target="_blank"';
	}
	$r .= ' class="badge badge-warning"';
	if( ! empty( $title ) && $title != '#' )
	{	// Use title for tooltip:
		$r .= ' data-toggle="tooltip" data-placement="top" title="'.format_to_output( $title, 'htmlattr' ).'"';
	}
	$r .= '>';
	$r .= $text;
	if( empty( $manual_url ) )
	{	// End of text formatted badge:
		$r .= '</b>';
	}
	else
	{	// End of the link:
		$r .= '</a>';
	}

	return $r;
}


/**
 * Compares two "PHP-standardized" version number strings
 *
 * @param string First version number, Use 'current' for global $app_version
 * @param string Second version number
 * @param string If the third optional operator argument is specified, test for a particular relationship.
 *               The possible operators are: <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne respectively.
 *               This parameter is case-sensitive, values should be lowercase.
 * @return integer|boolean -1 if the first version is lower than the second, 0 if they are equal, and 1 if the second is lower.
 *                         When using the optional operator argument, the function will return TRUE if the relationship is the one specified by the operator, FALSE otherwise.
 */
function evo_version_compare( $version1, $version2, $operator = NULL )
{
	if( $version1 === 'current' )
	{	// Use current version of application:
		global $app_version;
		$version1 = $app_version;
	}

	preg_match( '#^([\d\.]+)(-.+)?$#', $version1, $m_ver1 );
	preg_match( '#^([\d\.]+)(-.+)?$#', $version2, $m_ver2 );

	if( isset( $m_ver1[1], $m_ver2[1] ) && $m_ver1[1] == $m_ver2[1] )
	{	// If versions number is same:
		$version1_suffix = ( isset( $m_ver1[2] ) ? $m_ver1[2] : '' );
		$version2_suffix = ( isset( $m_ver2[2] ) ? $m_ver2[2] : '' );

		if( $version1_suffix == '-PRO' )
		{	// Remove "PRO" suffix to compare such versions as upper than "stable", "alpha", "beta" and etc.:
			$version1 = $m_ver1[1];
			if( $version2_suffix === '' )
			{	// Add suffix "stable" in order to make version(without suffix) lower than "PRO":
				$version2 .= '-stable';
			}
		}
		elseif( $version2_suffix == '-stable' )
		{	// Remove "stable" suffix to compare such versions as upper than "alpha", "beta" and etc. except of "PRO":
			$version2 = $m_ver2[1];
		}

		if( $version2_suffix == '-PRO' )
		{	// Remove "PRO" suffix to compare such versions as upper than "stable", "alpha", "beta" and etc.:
			$version2 = $m_ver2[1];
			if( $version1_suffix === '' )
			{	// Add suffix "stable" in order to make version(without suffix) lower than "PRO":
				$version1 .= '-stable';
			}
		}
		elseif( $version1_suffix == '-stable' )
		{	// Remove "stable" suffix to compare such versions as upper than "alpha", "beta" and etc. except of "PRO":
			$version1 = $m_ver1[1];
		}
	}

	if( is_null( $operator ) )
	{	// To return integer:
		return version_compare( $version1, $version2 );
	}
	else
	{	// To return boolean:
		return version_compare( $version1, $version2, $operator );
	}
}


/**
 * Get text for install page depending on param $display == 'cli'
 *
 * @param string Original text
 * @param string Format (Used for CLI mode)
 * @return string Prepared text
 */
function get_install_format_text_and_log( $text, $format = 'string' )
{
	global $display, $logs_path, $log_file_handle, $avoid_log_file;

	if( empty( $display ) || $display != 'cli' )
	{	// Don't touch text for non CLI modes:
		if( ! $avoid_log_file )
		{	// Include in log file:
			prepare_install_log_message( $text );
		}
		return $text;
	}

	// Don't remove these HTML tags on CLI mode:
	$allowable_html_tags = '<evo:error><evo:warning><evo:success><evo:note><evo:login><evo:password>';

	// Remove all new lines because we build them from requested format:
	$text = str_replace( array( "\n", "\r" ), '', $text );

	// Keep all URLs and display them
	$text = preg_replace( '/<a[^>]+href="([^"]+)"[^>]*>(.+)<\/a>/i', '$2(URL: $1)', $text );

	// Remove HTML tags from text:
	$text = strip_tags( $text, $allowable_html_tags );

	switch( $format )
	{
		case 'h2':
			// Header 2
			$text = "\n\n----- ".$text." -----\n\n";
			break;

		case 'br':
			// Paragraph:
			$text = $text."\n";
			break;

		case 'p':
			// Paragraph:
			$text = "\n".$text."\n\n";
			break;

		case 'p-start':
			// Start paragraph:
			$text = "\n".$text;
			break;

		case 'p-start-br':
			// Start paragraph:
			$text = "\n".$text."\n";
			break;

		case 'p-end':
			// End paragraph:
			$text = $text."\n\n";
			break;

		case 'li':
			// List item:
			$text = "\n- ".$text."\n";
			break;

		case 'code':
			// Code:
			$text = "\n================\n".$text."\n================\n";
			break;
	}

	// Replace all html entities like "&nbsp;", "&raquo;", "&laquo;" to readable chars:
	$text = html_entity_decode( $text );
	
	if( ! $avoid_log_file )
	{	// Include in log file:
		prepare_install_log_message( $text );
	}
	
	return $text;
}


/**
* Start to log into file on disk
*/
function start_install_log( $log_file_name )
{	// TODO: Factorize with start_log():
	global $rsc_url, $app_version_long, $log_file_handle, $logs_path, $servertimenow;

	// Get file path for log:
	$log_file_path = $logs_path.date( 'Y-m-d-H-i-s', $servertimenow ).'-'.$log_file_name.'.html';
	
	// Check log path is writeable or not: 
	if ( ! is_writable( $logs_path ) ) {

		return false;
	}
	
	// Try to create log file:
	if( ! ( $log_file_handle = fopen( $log_file_path, 'w' ) ) )
	{	
		return false;
	}

	// Write header of the log file:
	install_log_to_file( '<!DOCTYPE html>'."\r\n"
		.'<html lang="en-US">'."\r\n"
		.'<head>'."\r\n"
		.'<link href="'.$rsc_url.'css/bootstrap/bootstrap.css?v='.$app_version_long.'" type="text/css" rel="stylesheet" />'."\r\n"
		.'</head>'."\r\n"
		.'<body>'."\r\n"
		.'<div style="padding:5px">' );
}


/**
* End of log into file on disk
*/
function end_install_log()
{	// TODO: Factorize with end_log():
	global $log_file_handle;

	// Write footer of the log file:
	install_log_to_file( '</div>'."\r\n"
		.'</body>'."\r\n"
		.'</html>' );

	if( isset( $log_file_handle ) && $log_file_handle )
	{	// Close the log file:
		fclose( $log_file_handle );
	}
}


/**
* Log a message on screen and into file on disk
*
* @param string Message
* @param string Type: 'success', 'error', 'warning'
* @param string HTML tag for type/styled log: 'p', 'span', 'b', etc.
* @param boolean TRUE to display label
*/
function prepare_install_log_message( $message, $type = NULL, $type_html_tag = 'p', $display_label = true )
{	// TODO: Factorize with log():
	global $log_file_handle;

	if( ! isset( $log_file_handle ) || ! $log_file_handle )
	{	
		return false;
	}
	
	$message = get_install_log( $message, $type, $type_html_tag, $display_label );

	if( $message === false )
	{	// Skip when message should not be displayed:
		return;
	}

	// Try to store a message into the log file on the disk:
	install_log_to_file( $message );
}


/**
* Get a log message
*
* @param string Message
* @param string Type: 'success', 'error', 'warning', 'info'
* @param string HTML tag for type/styled log: 'p', 'span', 'b', etc.
* @param boolean TRUE to display label
* @return string|FALSE Formatted log message, FALSE - when message should not be displayed
*/
function get_install_log( $message, $type = NULL, $type_html_tag = 'p', $display_label = true )
{	// TODO: Factorize with get_log():
	if( $message === '' )
	{	// Don't log empty strings:
		return false;
	}

	switch( $type )
	{
		case 'success':
			$before = '<'.$type_html_tag.' class="text-success"> ';
			$after = '</'.$type_html_tag.'>';
			break;

		case 'error':
			$before = '<'.$type_html_tag.' class="text-danger">'.( $display_label ? '<span class="label label-danger">ERROR</span>' : '' ).' ';
			$after = '</'.$type_html_tag.'>';
			break;

		case 'warning':
			$before = '<'.$type_html_tag.' class="text-warning">'.( $display_label ? '<span class="label label-warning">WARNING</span>' : '' ).' ';
			$after = '</'.$type_html_tag.'>';
			break;

		case 'info':
			$before = '<'.$type_html_tag.' class="text-info">'.( $display_label ? '<span class="label label-info">INFO</span>' : '' ).' ';
			$after = '</'.$type_html_tag.'>';
			break;

		default:
			$before = '';
			$after = '';
			break;
	}

	return $before.$message.$after;
}


/**
* Log a message into file on disk
*
* @param string Message
*/
function install_log_to_file( $message )
{	// TODO: Factorize with log_to_file():
	global $log_file_handle;

	if( ! isset( $log_file_handle ) || ! $log_file_handle )
	{	
		return false;
	}

	// Put a message into the log file on the disk:
	fwrite( $log_file_handle, $message."\r\n" );
}


/**
 * Check if password should be transmitted in hashed format during Login
 *
 * @return boolean TRUE - hashed password will be transmitted, FALSE - raw password will be transmitted
 */
function can_use_hashed_password()
{
	global $transmit_hashed_password;

	if( isset( $transmit_hashed_password ) )
	{	// Get value from already defined var:
		return $transmit_hashed_password;
	}

	global $Settings, $Plugins;

	// Allow to transmit hashed password only when:
	// - it is enabled by general setting "Password hashing during Login"
	// - no plugins that automatically disable this option during Login
	$transmit_hashed_password = (bool)$Settings->get( 'js_passwd_hashing' ) && !(bool)$Plugins->trigger_event_first_true( 'LoginAttemptNeedsRawPassword' );

	return $transmit_hashed_password;
}


/**
 * Convert inline file tags like [image|file:123:link title:.css_class_name] or [inline:123:.css_class_name] into HTML tags
 *
 * @param string Source content
 * @param object Source object: Item, Comment, EmailCampaign, Message
 * @param array Params
 * @return string Content
 */
function render_inline_files( $content, $Object, $params = array() )
{
	$params = array_merge( array(
			'check_code_block' => false,
			'clear_paragraph'  => true,
			'render_tag_image'     => true,
			'render_tag_file'      => true,
			'render_tag_inline'    => true,
			'render_tag_video'     => true,
			'render_tag_audio'     => true,
			'render_tag_thumbnail' => true,
			'render_tag_folder'    => true,
		), $params );

		$render_tags = array();
		if( $params['render_tag_image'] )
		{	// Render short tag [image:]
			$render_tags[] = 'image';
		}
		if( $params['render_tag_file'] )
		{	// Render short tag [file:]
			$render_tags[] = 'file';
		}
		if( $params['render_tag_inline'] )
		{	// Render short tag [inline:]
			$render_tags[] = 'inline';
		}
		if( $params['render_tag_video'] )
		{	// Render short tag [video:]
			$render_tags[] = 'video';
		}
		if( $params['render_tag_audio'] )
		{	// Render short tag [audio:]
			$render_tags[] = 'audio';
		}
		if( $params['render_tag_thumbnail'] )
		{	// Render short tag [thumbnail:]
			$render_tags[] = 'thumbnail';
		}
		if( $params['render_tag_folder'] )
		{	// Render short tag [folder:]
			$render_tags[] = 'folder';
		}

		if( empty( $render_tags ) )
		{	// No tags for rendering:
			return $content;
		}

	if( $params['check_code_block'] && ( ( stristr( $content, '<code' ) !== false ) || ( stristr( $content, '<pre' ) !== false ) ) )
	{	// Call render_inline_files() on everything outside code/pre:
		$params['check_code_block'] = false;
		$content = callback_on_non_matching_blocks( $content,
			'~<(code|pre)[^>]*>.*?</\1>~is',
			'render_inline_files', array( $Object, $params ) );
		return $content;
	}

	// No code/pre blocks, replace on the whole thing

	if( $params['clear_paragraph'] )
	{	// Remove block level short tags inside <p> blocks and move them before the paragraph:
		$content = move_short_tags( $content );
	}

	// Find all matches with inline tags
	preg_match_all( '/\[('.implode( '|', $render_tags ).'):(\d+)(:?)([^\]]*)\]/i', $content, $inlines );

	if( !empty( $inlines[0] ) )
	{	// There are inline tags in the content...

		$rendered_tags = render_inline_tags( $Object, $inlines[0], $params );
		if( $rendered_tags )
		{	// Do replacing if the object really contains inline attached tags:
			foreach( $rendered_tags as $current_link_tag => $rendered_link_tag )
			{
				$content = str_replace( $current_link_tag, $rendered_link_tag, $content );
			}
		}
	}

	return $content;
}


/**
 * Convert inline tags like [image:|file:|inline:|video:|audio:|thumbnail:|folder:] into HTML tags
 *
 * @param object Source object: Item, Comment, EmailCampaign, Message
 * @param array Inline tags
 * @param array Params
 * @return array Associative array of rendered HTML tags with inline tags as key
 */
function render_inline_tags( $Object, $tags, $params = array() )
{
	global $Plugins;
	$inlines = array();

	$object_class = get_class( $Object );

	$params = array_merge( array(
				'before'                   => '<div>',
				'before_image'             => '<div'.( $object_class == 'EmailCampaign' ? emailskin_style( '.image_block' ) : ' class="image_block"' ).'>',
				'before_image_legend'      => '<div'.( $object_class == 'EmailCampaign' ? emailskin_style( '.image_legend' ) : ' class="image_legend"' ).'>',
				'after_image_legend'       => '</div>',
				'after_image'              => '</div>',
				'after'                    => '</div>',
				'image_size'               => 'fit-400x320',
				'image_link_to'            => 'original', // Can be 'orginal' (image) or 'single' (this post)
				'limit'                    => 1000, // Max # of images displayed
			), $params );

	if( !isset( $LinkList ) )
	{	// Get list of attached Links only first time:
		if( $Object->ID == 0 )
		{	// Get temporary object ID on preview new creating object:
			$temp_link_owner_ID = param( 'temp_link_owner_ID', 'integer', NULL );
		}
		else
		{	// Don't use temporary object for existing object:
			$temp_link_owner_ID = NULL;
		}

		switch( $object_class )
		{
			case 'Item':
				$LinkOwner = new LinkItem( $Object, $temp_link_owner_ID );
				$prepare_plugin_event_name = 'PrepareForRenderItemAttachment';
				$render_plugin_event_name = 'RenderItemAttachment';
				break;

			case 'Comment':
				$LinkOwner = new LinkComment( $Object, empty( $Object->temp_link_owner_ID ) ? $temp_link_owner_ID : $Object->temp_link_owner_ID );
				$prepare_plugin_event_name = 'PrepareForRenderCommentAttachment';
				$render_plugin_event_name = 'RenderCommentAttachment';
				break;

			case 'EmailCampaign':
				$LinkOwner = new LinkEmailCampaign( $Object );
				$prepare_plugin_event_name = 'PrepareForRenderEmailAttachment';
				$render_plugin_event_name = 'RenderEmailAttachment';
				break;

			case 'Message':
				$LinkOwner = new LinkMessage( $Object, $temp_link_owner_ID );
				$prepare_plugin_event_name = 'PrepareForRenderMessageAttachment';
				$render_plugin_event_name = 'RenderMessageAttachment';
				break;

			default:
				// Wrong source object type:
				return false;
		}
		$LinkList = $LinkOwner->get_attachment_LinkList( $params['limit'] );
	}

	if( empty( $LinkList ) )
	{	// This Object has no attached files for 'inline' position, Exit here:
		return false;
	}

	// Render inline tags by active plugins:
	$plugins_inlines = $Plugins->trigger_collect( 'RenderInlineTags', array_merge( $params, array(
			'Object'      => $Object,
			'inline_tags' => $tags,
		) ) );
	foreach( $plugins_inlines as $plugin_ID => $plugin_inlines )
	{
		$inlines = array_merge( $inlines, $plugin_inlines );
	}

	foreach( $tags as $current_inline )
	{
		if( isset( $inlines[$current_inline] ) )
		{	// Skip inline tag if it has been already rendered before, e-g by some plugin in the event "RenderInlineTags" above:
			continue;
		}

		if( ! preg_match( '/\[(image|file|inline|video|audio|thumbnail|folder):(\d+)(:?)([^\]]*)\]/i', $current_inline, $inline ) )
		{	// Don't render a not supported inline tag:
			$inlines[$current_inline] = $current_inline;
			continue;
		}

		$inline_type = $inline[1]; // image|file|inline|video|audio|thumbnail|folder
		$current_link_ID = (int) $inline[2];

		if( empty( $current_link_ID ) )
		{	// Invalid link ID, Go to next match
			$inlines[$current_inline] = $current_inline;
			continue;
		}

		if( ! ( $Link = & $LinkList->get_by_field( 'link_ID', $current_link_ID ) ) )
		{	// Link ID is not part of the linked files for position "inline"
			$inlines[$current_inline] = $current_inline;
			continue;
		}

		if( ! ( $File = & $Link->get_File() ) )
		{	// No File object:
			global $Debuglog;
			$Debuglog->add( sprintf( 'Link ID#%d of '.$object_class.' #%d does not have a file object!', $Link->ID, $Object->ID ), array( 'error', 'files' ) );
			$inlines[$current_inline] = $current_inline;
			continue;
		}

		if( ! $File->exists() )
		{	// File doesn't exist:
			global $Debuglog;
			$Debuglog->add( sprintf( 'File linked to '.$object_class.' #%d does not exist (%s)!', $Object->ID, $File->get_full_path() ), array( 'error', 'files' ) );
			$inlines[$current_inline] = $current_inline;
			continue;
		}

		$params['File'] = $File;
		$params['Link'] = $Link;
		$params[ $object_class ] = $Object;

		$current_file_params = array();

		switch( $inline_type )
		{
			case 'image':
			case 'inline': // valid file type: image
				if( $File->is_image() )
				{
					$current_image_params = $params;
					$image_href = false;
					$image_rel = NULL;
					$image_additional_class = false;

					if( ! empty( $inline[3] ) ) // check if second colon is present
					{
						// Get the inline params: caption and class
						$inline_params = explode( ':', $inline[4] );
						$opt_index = 0;

						// Caption:
						if( $inline_type != 'inline' && ! empty( $inline_params[0] ) )
						{	// Caption is set, so overwrite the image link title
							if( $inline_params[0] == '-' )
							{	// Caption display is disabled
								$current_image_params['image_link_title'] = '';
								$current_image_params['hide_image_link_title'] = true;
							}
							else
							{	// New image caption was set
								$current_image_params['image_link_title'] = strip_tags( $inline_params[0] );
							}

							$current_image_params['image_desc'] = $current_image_params['image_link_title'];
							$current_file_params['title'] = $inline_params[0];
						}
						if( $inline_type == 'image' )
						{	// Caption has always a reserved placefor image short tag:
							$opt_index++;
						}

						// RegExp to detect HRef option:
						$href_regexp = '#^(https?|\(\((.*?)\)\))$#i';

						// Alt text:
						$current_image_params['image_alt'] = '';
						if( isset( $inline_params[ $opt_index ] ) &&
						    substr( $inline_params[ $opt_index ], 0, 1 ) != '.' &&
						    ! preg_match( $href_regexp, $inline_params[ $opt_index ] ) &&
						    ( $inline_type == 'image' || ! in_array( $inline_params[ $opt_index ], array( 'small', 'medium', 'large', 'original' ) ) ) )
						{	// Override the image File's alt text with provided in current inline tag:
							if( $inline_params[ $opt_index ] == '-' )
							{	// Alt text display is disabled:
								$current_image_params['image_alt'] = '-';
							}
							else
							{	// New image alt text was set:
								$current_image_params['image_alt'] = strip_tags( $inline_params[ $opt_index ] );
							}
							$opt_index++;
						}

						// HRef:
						if( $inline_type != 'inline' &&
						    ! empty( $inline_params[ $opt_index ] ) &&
						    preg_match( $href_regexp, $inline_params[ $opt_index ], $href_match ) )
						{
							if( stripos( $href_match[0], 'http' ) === 0 )
							{	// Absolute URL:
								$image_href = $href_match[0].':'.$inline_params[ $opt_index + 1 ];
								$image_rel = ''; // reset default attribute "rel" to don't display colorbox on click
								$opt_index++;
							}
							else
							{	// Item slug:
								$ItemCache = & get_ItemCache();
								if( $href_match[2] === '' )
								{	// No link, Display image tag without link tag around:
									$image_href = '';
								}
								elseif( $slug_Item = & $ItemCache->get_by_urltitle( $href_match[2], false, false ) )
								{	// Use a link with item permanent url around image tag:
									$image_href = $slug_Item->get_permanent_url();
								}
								else
								{	// Wrong Item provided, Singal with special red class:
									$image_href = '';
									$image_additional_class = 'imgerror';
								}
								$image_rel = ''; // reset default attribute "rel" to don't display colorbox on click
							}
							$opt_index++;
						}

						$current_image_params['image_link_to'] = ( $image_href === false ? 'original' : $image_href );
						$current_image_params['image_link_rel'] = $image_rel;

						// TODO: Size:

						// Class Name(s):
						$inline_param_class = ( empty( $inline_params[ $opt_index ] ) ? '' : $inline_params[ $opt_index ] );
						if( $image_additional_class !== false )
						{	// Append additional class, e.g. on wrong provided item slug:
							$inline_param_class .= '.'.$image_additional_class;
						}
						if( ! empty( $inline_param_class ) )
						{	// A class name is set for the inline tags
							$image_extraclass = strip_tags( trim( str_replace( '.', ' ', $inline_param_class ) ) );

							if( preg_match('#^[A-Za-z0-9\s\-_]+$#', $image_extraclass ) )
							{
								if( $object_class == 'EmailCampaign' )
								{	// Append extra class to image/file inline img tags:
									$current_image_params['image_class'] = $image_extraclass;
								}
								else
								{	// Inject extra class name(s) into 'before_image' param:
									$current_image_params['before_image'] = update_html_tag_attribs( $current_image_params['before_image'], array( 'class' => $image_extraclass ) );
								}
								$current_file_params['class'] = $image_extraclass;
							}
						}
					}

					foreach( $current_image_params as $param_key => $param_value )
					{	// Pass all params by reference, in order to give possibility to modify them by plugin
						// So plugins can add some data before/after image tags (E.g. used by infodots plugin)
						$current_image_params[ $param_key ] = & $current_image_params[ $param_key ];
					}

					// Prepare params before rendering attachment:
					$Plugins->trigger_event_first_true_with_params( $prepare_plugin_event_name, $current_image_params );

					// Render attachments by plugin, Append the html content to $current_image_params['data'] and to $r:
					if( count( $Plugins->trigger_event_first_true_with_params( $render_plugin_event_name, $current_image_params ) ) != 0 )
					{	// This attachment has been rendered by a plugin (to $current_image_params['data']):
						$inlines[ $current_inline ] = $current_image_params['data'];
						break;
					}

					if( $inline_type == 'image' )
					{	// Generate the IMG tag with all the alt, title and desc if available:
						switch( $object_class )
						{
							case 'Item':
								// Get the IMG tag with link to original image or to Item page:
								$inlines[ $current_inline ] = $Object->get_attached_image_tag( $Link, $current_image_params );
								break;

							case 'EmailCampaign':
								// Get the IMG tag without link for email content:
								$image_style = '';
								if( ! empty( $current_image_params['image_class'] ) )
								{	// Convert classes to style format:
									$image_style = emailskin_style( '.'.str_replace( ' ', '+.', $current_image_params['image_class'] ), false );
									// We cannot use class attribute on email campaign content:
									unset( $current_image_params['image_class'] );
								}

								$inlines[ $current_inline ] = $Link->get_tag( array_merge( $current_image_params, array(
										'image_link_to' => $image_href,
										'image_style'   => 'border: none; max-width: 100%; height: auto;'.$image_style,
										'add_loadimg'   => false,
									) ) );
								break;

							default:
								// Get the IMG tag with link to original big image:
								$inlines[ $current_inline ] = $Link->get_tag( array_merge( $params, $current_image_params ) );
								break;
						}
					}
					elseif( $inline_type == 'inline' )
					{	// Generate simple IMG tag with resized image size:
						switch( $object_class )
						{
							case 'EmailCampaign':
								$image_style = '';
								if( ! empty( $current_file_params['class'] ) )
								{	// Convert classes to style format:
									$image_style = emailskin_style( '.'.str_replace( ' ', '+.', $current_file_params['class'] ), false );
									// We cannot use class attribute on email campaign content:
									unset( $current_file_params['class'] );
								}
								$inlines[ $current_inline ] = $File->get_tag( '', '', '', '', $current_image_params['image_size'], '', '', '',
										'', '', $current_image_params['image_alt'], '', '', 1, NULL, 'border: none; max-width: 100%; height: auto;'.$image_style, false );
								break;

							default:
								$inlines[ $current_inline ] = $File->get_tag( '', '', '', '', $current_image_params['image_size'], '', '', '',
										( empty( $current_file_params['class'] ) ? '' : $current_file_params['class'] ), '', $current_image_params['image_alt'], '' );
						}
					}
				}
				else
				{ // not an image file, do not process
					$inlines[$current_inline] = $current_inline;
				}
				break;

			case 'thumbnail':
				if( $File->is_image() )
				{
					global $thumbnail_sizes;

					$thumbnail_alt = '';
					$thumbnail_href = false;
					$thumbnail_rel = NULL;
					$thumbnail_additional_class = false;
					$thumbnail_size = 'medium';
					$thumbnail_position = 'left';

					$thumbnail_classes = array();

					if( ! empty( $inline[3] ) ) // check if second colon is present
					{
						// Get optional inline params: HRef, Size, Alignment, Class
						$inline_params = explode( ':', $inline[4] );
						$opt_index = 0;

						// RegExp to detect HRef option:
						$href_regexp = '#^(https?|\(\((.*?)\)\))$#i';

						// Alt text:
						if( isset( $inline_params[ $opt_index ] ) &&
						    substr( $inline_params[ $opt_index ], 0, 1 ) != '.' &&
						    ! preg_match( $href_regexp, $inline_params[ $opt_index ] ) &&
						    ! in_array( $inline_params[ $opt_index ], array( 'small', 'medium', 'large', 'left', 'right' ) ) )
						{	// Override the image File's alt text with provided in current inline tag:
							if( $inline_params[ $opt_index ] == '-' )
							{	// Alt text display is disabled:
								$thumbnail_alt = '-';
							}
							else
							{	// New image alt text was set:
								$thumbnail_alt = strip_tags( $inline_params[ $opt_index ] );
							}
							$opt_index++;
						}

						// HRef:
						if( ! empty( $inline_params[ $opt_index ] ) &&
						    preg_match( $href_regexp, $inline_params[ $opt_index ], $href_match ) )
						{
							if( stripos( $href_match[0], 'http' ) === 0 )
							{	// Absolute URL:
								$thumbnail_href = $href_match[0].':'.$inline_params[ $opt_index + 1 ];
								$thumbnail_rel = ''; // reset default attribute "rel" to don't display colorbox on click
								$opt_index++;
							}
							else
							{	// Item slug:
								$ItemCache = & get_ItemCache();
								if( $href_match[2] === '' )
								{	// No link, Display image tag without link tag around:
									$thumbnail_href = '';
								}
								elseif( $slug_Item = & $ItemCache->get_by_urltitle( $href_match[2], false, false ) )
								{	// Use a link with item permanent url around image tag:
									$thumbnail_href = $slug_Item->get_permanent_url();
								}
								else
								{	// Wrong Item provided, Singal with special red class:
									$thumbnail_href = '';
									$thumbnail_additional_class = 'imgerror';
								}
								$thumbnail_rel = ''; // reset default attribute "rel" to don't display colorbox on click
							}
							$opt_index++;
						}

						// Size:
						$valid_thumbnail_sizes = array( 'small', 'medium', 'large' );
						if( ! empty( $inline_params[ $opt_index ] ) && in_array( $inline_params[ $opt_index ], $valid_thumbnail_sizes ) )
						{
							$thumbnail_size = $inline_params[ $opt_index ];
							$opt_index++;
						}

						// Alignment:
						$valid_thumbnail_positions = array( 'left', 'right' );
						if( ! empty( $inline_params[ $opt_index ] ) && in_array( $inline_params[ $opt_index ], $valid_thumbnail_positions ) )
						{
							$thumbnail_position = $inline_params[ $opt_index ];
							$opt_index++;
						}

						// Class:
						$inline_param_class = ( empty( $inline_params[ $opt_index ] ) ? '' : $inline_params[ $opt_index ] );
						if( $thumbnail_additional_class !== false )
						{	// Append additional class, e.g. on wrong provided item slug:
							$inline_param_class .= '.'.$thumbnail_additional_class;
						}
						if( ! empty( $inline_param_class ) )
						{	// A class name is set for the inline tags
							$extra_classes = explode( '.', ltrim( $inline_param_class, '.' ) );
						}
					}

					switch( $thumbnail_size )
					{
						case 'small':
							$thumbnail_size = 'fit-128x128';
							break;

						case 'large':
							$thumbnail_size = 'fit-320x320';
							break;

						case 'medium':
						default:
							$thumbnail_size = 'fit-192x192';
							break;
					}

					$thumbnail_classes[] = 'evo_thumbnail';
					$thumbnail_classes[] = 'evo_thumbnail__'.$thumbnail_position;
					if( isset( $extra_classes ) )
					{
						$thumbnail_classes = array_merge( $thumbnail_classes, $extra_classes );
					}

					$current_image_params = array(
						'before_image'        => '',
						'before_image_legend' => '', // can be NULL
						'after_image_legend'  => '',
						'after_image'         => '',
						'image_size'          => $thumbnail_size,
						'image_link_to'       => ( $thumbnail_href === false ? 'original' : $thumbnail_href ),
						'image_link_title'    => '',	// can be text or #title# or #desc#
						'image_link_rel'      => $thumbnail_rel,
						'image_class'         => implode( ' ', $thumbnail_classes ),
						'image_alt'           => $thumbnail_alt,
					);

					switch( $object_class )
					{
						case 'Item':
							// Get the IMG tag with link to original image or to Item page:
							$inlines[ $current_inline ] = $Object->get_attached_image_tag( $Link, $current_image_params );
							break;

						case 'EmailCampaign':
							// Get the IMG tag without link for email content:
							$image_style = '';
							if( ! empty( $current_image_params['image_class'] ) )
							{	// Convert classes to style format:
								$image_style = emailskin_style( '.'.str_replace( ' ', '+.', $current_image_params['image_class'] ), false );
								// We cannot use class attribute on email campaign content:
								unset( $current_image_params['image_class'] );
							}
							$inlines[ $current_inline ] = $Link->get_tag( array_merge( $current_image_params, array(
									'image_link_to' => $thumbnail_href,
									'image_style'   => 'border: none; max-width: 100%; height: auto;'.$image_style,
									'add_loadimg'   => false,
								) ) );
							break;

						default:
							// Get the IMG tag with link to original big image:
							$inlines[ $current_inline ] = $Link->get_tag( array_merge( $params, $current_image_params ) );
							break;
					}
				}
				else
				{	// not an image file, do not process
					$inlines[$current_inline] = $current_inline;
				}
				break;

			case 'file': // valid file types: image, video, audio, other
				$valid_file_types = array( 'image', 'video', 'audio', 'other' );
				if( in_array( $File->get_file_type(), $valid_file_types ) )
				{
					if( ! empty( $inline[3] ) ) // check if second colon is present
					{
						// Get the file caption
						$caption = $inline[4];

						if( ! empty( $caption ) )
						{ // Caption is set
							$current_file_params['title'] = strip_tags( $caption );
						}
					}

					if( empty( $current_file_params['title'] ) )
					{ // Use real file name as title when it is not defined for inline tag
						$file_title = $File->get( 'title' );
						$current_file_params['title'] = ' '.( empty( $file_title ) ? $File->get_name() : $file_title );
					}
					elseif( $current_file_params['title'] == '-' )
					{ // Don't display a title in this case, Only file icon will be displayed
						$current_file_params['title'] = '';
					}
					else
					{ // Add a space between file icon and title
						$current_file_params['title'] = ' '.$current_file_params['title'];
					}

					$inlines[$current_inline] = '<a href="'.$File->get_url().'"'
						.( empty( $current_file_params['class'] ) ? '' : ' class="'.$current_file_params['class'].'"' )
						.'>'.$File->get_icon( $current_file_params ).$current_file_params['title'].'</a>';
				}
				else
				{ // not a valid file type, do not process
					$inlines[$current_inline] = $current_inline;
				}
				break;

			case 'video':	// valid file type: video
				if( $File->is_video() )
				{
					$current_video_params = $params;
					// Create an empty dummy element where the plugin is expected to append the rendered video
					$current_video_params['data'] = '';

					foreach( $current_video_params as $param_key => $param_value )
					{ // Pass all params by reference, in order to give possibility to modify them by plugin
						// So plugins can add some data before/after tags (E.g. used by infodots plugin)
						$current_video_params[ $param_key ] = & $current_video_params[ $param_key ];
					}

					// Prepare params before rendering attachment:
					$Plugins->trigger_event_first_true_with_params( $prepare_plugin_event_name, $current_video_params );

					// Render attachments by plugin:
					if( count( $Plugins->trigger_event_first_true_with_params( $render_plugin_event_name, $current_video_params ) ) != 0 )
					{	// This attachment has been rendered by a plugin (to $current_video_params['data']):
						$inlines[$current_inline] = $current_video_params['data'];
					}
					else
					{ // no plugin available or was able to render the tag
						$inlines[$current_inline] = $current_inline;
					}
				}
				else
				{ // not a video file, do not process
					$inlines[$current_inline] = $current_inline;
				}
				break;

			case 'audio': // valid file type: audio
				if( $File->is_audio() )
				{
					$current_audio_params = $params;
					// Create an empty dummy element where the plugin is expected to append the rendered video
					$current_audio_params['data'] = '';

					foreach( $current_audio_params as $param_key => $param_value )
					{ // Pass all params by reference, in order to give possibility to modify them by plugin
						// So plugins can add some data before/after tags (E.g. used by infodots plugin)
						$current_audio_params[ $param_key ] = & $current_audio_params[ $param_key ];
					}

					// Prepare params before rendering attachment:
					$Plugins->trigger_event_first_true_with_params( $prepare_plugin_event_name, $current_audio_params );

					// Render attachments by plugin:
					if( count( $Plugins->trigger_event_first_true_with_params( $render_plugin_event_name, $current_audio_params ) ) != 0 )
					{	// This attachment has been rendered by a plugin (to $current_audio_params['data']):
						$inlines[$current_inline] = $current_audio_params['data'];
					}
					else
					{ // no plugin available or was able to render the tag
						$inlines[$current_inline] = $current_inline;
					}
				}
				else
				{ // not a video file, do not process
					$inlines[$current_inline] = $current_inline;
				}
				break;

			case 'folder':
				if( $File->is_dir() )
				{
					$current_folder_params = $params;

					if( ! empty( $inline[3] ) ) // check if second colon is present
					{
						if( preg_match( '/^\d+$/', $inline[4] ) )
						{ // limit number of images
							$current_folder_params['gallery_image_limit'] = (int) $inline[4];
						}
					}

					$inlines[$current_inline] = $File->get_gallery( $current_folder_params );
				}
				else
				{
					$inlines[$current_inline] = $current_inline;
				}
				break;

			default:
				$inlines[$current_inline] = $current_inline;
		}
	}

	return $inlines;
}


/**
 * Convert date format from locale for jQuery datepicker plugin
 *
 * @param string Date format of locale from DB; for example: Y-m-d
 * @return string Date format for jQuery datepicker plugin; for example: yy-mm-dd
 */
function php_to_jquery_date_format( $php_format )
{
	$tokens = array(
			// Day
			'd' => 'dd',
			'D' => 'D',
			'j' => 'd',
			'l' => 'DD',
			'N' => '',
			'S' => '',
			'w' => '',
			'z' => 'o',
			// Week
			'W' => '',
			// Month
			'F' => 'MM',
			'm' => 'mm',
			'M' => 'M',
			'n' => 'm',
			't' => '',
			// Year
			'L' => '',
			'o' => '',
			'Y' => 'yy',
			'y' => 'y',
			// Time
			'a' => '',
			'A' => '',
			'B' => '',
			'g' => '',
			'G' => '',
			'h' => '',
			'H' => '',
			'i' => '',
			's' => '',
			'u' => '' );

	$js_format = "";
	$escaping = false;
	for( $i = 0; $i < strlen( $php_format ); $i++ )
	{
		$char = $php_format[$i];
		if($char === '\\') // PHP date format escaping character
		{
			$i++;
			if( $escaping ) $js_format .= $php_format[$i];
			else $js_format .= '\'' . $php_format[$i];
			$escaping = true;
		}
		else
		{
			if( $escaping )
			{
				$jqueryui_format .= "'"; $escaping = false;
			}
			if( isset( $tokens[$char] ) )
			{
				$js_format .= $tokens[$char];
			}
			else
			{
				$js_format .= $char;
			}
		}
	}

	return $js_format;
}


/**
 * Check if given string is HTML
 *
 * @param string String to check if HTML
 * @return boolean True if string is HTML
 */
function is_html( $string )
{
	return $string != strip_tags( $string ) ? true : false;
}


/**
 * Get date format from current locale for jQuery datepicker plugin
 *
 * @return string Date format; for example: yy-mm-dd
 */
function jquery_datepicker_datefmt()
{
	return php_to_jquery_date_format( locale_input_datefmt() );
}


/**
 * Get month names as string of JavaScript array for jQuery datepicker plugin
 *
 * @return string
 */
function jquery_datepicker_month_names()
{
	$months = array(
			TS_('January'),
			TS_('February'),
			TS_('March'),
			TS_('April'),
			TS_('May'),
			TS_('June'),
			TS_('July'),
			TS_('August'),
			TS_('September'),
			TS_('October'),
			TS_('November'),
			TS_('December')
		);

	return '[\''.implode( '\', \'', $months ).'\']';
}


/**
 * Get week day names as string of JavaScript array for jQuery datepicker plugin
 *
 * @return string
 */
function jquery_datepicker_day_names()
{
	$days = array(
			TS_('Sun'),
			TS_('Mon'),
			TS_('Tue'),
			TS_('Wed'),
			TS_('Thu'),
			TS_('Fri'),
			TS_('Sat')
		);

	foreach( $days as $d => $day )
	{
		$days[ $d ] = utf8_substr( $day, 0, 2 );
	}

	return '[\''.implode( '\', \'', $days ).'\']';
}


/**
 * Find the dates without data and fill them with 0 to display on graph and table
 *
 * @param array Source data
 * @param array Default data, e.g. array( 'hits' => 0 )
 * @param string Start date of log in format 'YYYY-mm-dd'
 * @param string End date of log in format 'YYYY-mm-dd'
 * @return array Fixed data
 */
function fill_empty_days( $data, $default_data, $start_date, $end_date )
{
	$fixed_data = array();
	$start_date = date( 'Y-n-j', strtotime( $start_date) );
	$end_date = date( 'Y-n-j', strtotime( $end_date) );

	if( empty( $data ) )
	{
		return $fixed_data;
	}

	// Get additional fields which must be exist in each array item of new filled empty day below:
	$additional_fields = array_diff_key( $data[0], array( 'year' => 0, 'month' => 0, 'day' => 0 ) );

	// Check if data array contains start and end dates:
	$start_date_is_contained = empty( $start_date );
	$end_date_is_contained = empty( $end_date );

	if( ! $start_date_is_contained || ! $end_date_is_contained )
	{
		foreach( $data as $row )
		{
			$this_date = $row['year'].'-'.$row['month'].'-'.$row['day'];
			if( $this_date == $start_date )
			{	// The start date is detected:
				$start_date_is_contained = true;
			}
			if( $this_date == $end_date )
			{	// The start date is detected:
				$end_date_is_contained = true;
			}
			if( $start_date_is_contained && $end_date_is_contained )
			{	// Stop array searching here because we have found the dates:
				break;
			}
		}
	}

	if( ! $start_date_is_contained )
	{	// Add item to array with 0 for start date if stats has no data for the date:
		array_push( $data, array_merge( array(
				'year'     => date( 'Y', strtotime( $start_date ) ),
				'month'    => date( 'n', strtotime( $start_date ) ),
				'day'      => date( 'j', strtotime( $start_date ) ),
		), $default_data ) + $additional_fields );
	}
	if( ! $end_date_is_contained )
	{	// Add item to array with 0 for end date if stats has no data for the date:
		array_unshift( $data, array_merge( array(
				'year'     => date( 'Y', strtotime( $end_date ) ),
				'month'    => date( 'n', strtotime( $end_date ) ),
				'day'      => date( 'j', strtotime( $end_date ) ),
		), $default_data ) + $additional_fields );
	}

	foreach( $data as $row )
	{
		$this_date = $row['year'].'-'.$row['month'].'-'.$row['day'];

		if( isset( $prev_date ) && $prev_date != $this_date )
		{	// If data are from another day:
			$prev_time = strtotime( $prev_date ) - 86400;
			$this_time = strtotime( $this_date );

			if( $prev_time != $this_time )
			{	// If previous date is not previous day(it means some day has no data):
				$empty_days = ( $prev_time - $this_time ) / 86400;
				for( $d = 0; $d < $empty_days; $d++ )
				{	// Add each empty day to array with default data:
					$empty_day = $prev_time - $d * 86400;
					$fixed_data[] = array_merge( array(
							'year'     => date( 'Y', $empty_day ),
							'month'    => date( 'n', $empty_day ),
							'day'      => date( 'j', $empty_day ),
					), $default_data ) + $additional_fields;
				}
			}
		}

		$prev_date = $row['year'].'-'.$row['month'].'-'.$row['day'];
		$fixed_data[] = $row;
	}

	return $fixed_data;
}


/**
 * Get image file used for social media
 *
 * @param object Item object
 * @param array Params
 * @return object Image File or Link object
 */
function get_social_media_image( $Item = NULL, $params = array() )
{
	$params = array_merge( array(
			'use_item_cat_fallback'      => true,
			'use_coll_fallback'          => true,
			'use_site_fallback'          => true,
			'return_as_link'             => false,
		), $params );

	$social_media_image = NULL;

	if( ! empty( $Item ) )
	{	// Try to get attached images
		$LinkOwner = new LinkItem( $Item );
		if(  $LinkList = $LinkOwner->get_attachment_LinkList( 1000, 'cover,background,teaser,teaserperm,teaserlink', 'image', array(
				'sql_select_add' => ', CASE WHEN link_position = "cover" THEN 1 WHEN link_position IN ( "teaser", "teaserperm", "teaserlink" ) THEN 2 ELSE 3 END AS link_priority',
				'sql_order_by'   => 'link_priority ASC, link_order ASC' ) ) )
		{	// Item has linked files:
			while( $Link = & $LinkList->get_next() )
			{
				if( ! ( $File = & $Link->get_File() ) )
				{	// No File object:
					global $Debuglog;
					$Debuglog->add( sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $Item->ID ), array( 'error', 'files' ) );
					continue;
				}

				if( ! $File->exists() )
				{	// File doesn't exist:
					global $Debuglog;
					$Debuglog->add( sprintf( 'File linked to item #%d does not exist (%s)!', $Item->ID, $File->get_full_path() ), array( 'error', 'files' ) );
					continue;
				}

				if( $File->is_image() )
				{	// Use only image files for og:image tag:
					if( $params['return_as_link'] )
					{
						return $Link;
					}
					else
					{
						return $File;
					}
					break;
				}
			}
		}

		if( $params['use_item_cat_fallback'] )
		{	// No attached image from Item, let's try getting one from the Item's default chapter
			$FileCache = & get_FileCache();
			if( $default_Chapter = & $Item->get_main_Chapter() )
			{	// Try social media boilerplate image first:
				$social_media_image_file_ID = $default_Chapter->get( 'social_media_image_file_ID', false );
				if( $social_media_image_file_ID > 0 && ( $File = & $FileCache->get_by_ID( $social_media_image_file_ID  ) ) && $File->is_image() )
				{
					return $File;
				}
			}
		}
	}

	global $Blog;

	if( $params['use_coll_fallback'] && $Blog )
	{	// Try to get collection social media boiler plate and collection image/logo:
		$FileCache = & get_FileCache();
		$social_media_image_file_ID = $Blog->get_setting( 'social_media_image_file_ID', false );
		if( $social_media_image_file_ID > 0 && ( $File = & $FileCache->get_by_ID( $social_media_image_file_ID  ) ) && $File->is_image() )
		{	// Try social media boiler plate first:
			return $File;
		}
	}

	if( $params['use_site_fallback'] )
	{	// Use social media boilerplate logo if configured
		global $Settings;

		$FileCache = & get_FileCache();
		$social_media_image_file_ID = intval( $Settings->get( 'social_media_image_file_ID' ) );
		if( $social_media_image_file_ID > 0 && ( $File = $FileCache->get_by_ID( $social_media_image_file_ID, false ) ) && $File->is_image() )
		{
			return $File;
		}
	}

	return NULL;
}


/**
 * Opens modal to insert inline image tags.
 * Used by the following plugins:
 * - evo_TinyMCE
 * - evo_inlines
 *
 * @param array Params
 */
function insert_image_links_block( $params )
{
	global $current_User, $inc_path, $Blog, $blog, $LinkOwner;
	global $is_admin_page;

	load_funcs( 'links/model/_link.funcs.php' );

	$params = array_merge( array(
			'target_type' => NULL,
		), $params );

	if( ! empty( $params['blog'] ) )
	{
		$BlogCache = & get_BlogCache();
		$blog = $params['blog'];
		$Blog = $BlogCache->get_by_ID( $blog );
	}

	$temp_ID = empty( $params['temp_ID'] ) ? NULL : $params['temp_ID'];
	$is_admin_page = is_logged_in() && isset( $params['request_from'] ) && ( $params['request_from'] == 'back' );

	switch( $params['target_type'] )
	{
		case 'Item':
			if( ! isset( $params['target_ID'] ) && ! isset( $params['temp_ID'] ) )
			{
				return;
			}

			$ItemCache = & get_ItemCache();
			$edited_Item = & $ItemCache->get_by_ID( $params['target_ID'], false, false );

			if( empty( $blog ) && $edited_Item )
			{
				$Blog = $edited_Item->get_Blog();
				$blog = $Blog->ID;
			}

			if( isset( $GLOBALS['files_Module'] )
				&& ( ( $edited_Item && check_user_perm( 'item_post!CURSTATUS', 'edit', false, $edited_Item ) ) || ( empty( $edited_Item ) && $params['temp_ID'] ) )
				&& check_user_perm( 'files', 'view', false ) )
			{	// Files module is enabled, but in case of creating new posts we should show file attachments block only if user has all required permissions to attach files
				load_class( 'links/model/_linkitem.class.php', 'LinkItem' );
				global $LinkOwner; // Initialize this object as global because this is used in many link functions
				$LinkOwner = new LinkItem( $edited_Item, $temp_ID );
			}
			break;

		case 'Comment':
			if( ! isset( $params['target_ID'] ) )
			{
				return;
			}

			$CommentCache = & get_CommentCache();
			$edited_Comment = & $CommentCache->get_by_ID( $params['target_ID'] );
			$comment_Item = & $edited_Comment->get_Item();

			if( empty( $blog ) && $comment_Item )
			{
				$Blog = $comment_Item->get_Blog();
				$blog = $Blog->ID;
			}

			if( isset( $GLOBALS['files_Module'] )
				&& check_user_perm( 'comment!CURSTATUS', 'edit', false, $edited_Comment )
				&& check_user_perm( 'files', 'view', false ) )
			{	// Files module is enabled, but in case of creating new comments we should show file attachments block only if user has all required permissions to attach files
				load_class( 'links/model/_linkcomment.class.php', 'LinkComment' );
				global $LinkOwner; // Initialize this object as global because this is used in many link functions
				$LinkOwner = new LinkComment( $edited_Comment );
			}
			break;

		case 'EmailCampaign':
			if( ! isset( $params['target_ID'] ) )
			{
				return;
			}

			$EmailCampaignCache = & get_EmailCampaignCache();
			$edited_EmailCampaign = $EmailCampaignCache->get_by_ID( $params['target_ID'] );

			if( isset( $GLOBALS['files_Module'] )
				&& check_user_perm( 'emails', 'edit', false )
				&& check_user_perm( 'files', 'view', false ) )
			{	// Files module is enabled, but in case of creating new email campaign  we should show file attachments block only if user has all required permissions to attach files
				load_class( 'links/model/_linkemailcampaign.class.php', 'LinkEmailCampaign' );
				global $LinkOwner; // Initialize this object as global because this is used in many link functions
				$LinkOwner = new LinkEmailCampaign( $edited_EmailCampaign );
			}
			break;

		case 'Message':
			if( ! isset( $params['target_ID'] ) && ! isset( $params['temp_ID'] ) )
			{
				return;
			}

			$MessageCache = & get_MessageCache();
			$edited_Message = $MessageCache->get_by_ID( $params['target_ID'], false, false );

			if( isset( $GLOBALS['files_Module'] )
				&& check_user_perm( 'perm_messaging', 'reply' )
				&& check_user_perm( 'files', 'view', false ) )
			{	// Files module is enabled, but in case of creating new messages we should show file attachments block only if user has all required permissions to attach files
				load_class( 'links/model/_linkmessage.class.php', 'LinkMessage' );
				global $LinkOwner; // Initialize this object as global because this is used in many link functions
				$LinkOwner = new LinkMessage( $edited_Message, $temp_ID );
			}
			break;

		default:
			return;
	}

	global $fm_mode;
	$fm_mode = 'file_select';
	// Set a different drag and drop fieldset prefix:
	$fieldset_prefix = 'modal_';

	if( is_admin_page() )
	{
		global $UserSettings, $adminskins_path, $AdminUI;

		$admin_skin = $UserSettings->get( 'admin_skin', $current_User->ID );
		require_once $adminskins_path.$admin_skin.'/_adminUI.class.php';
		$AdminUI = new AdminUI();
	}
	else
	{
		global $Skin, $inc_path;

		init_fontawesome_icons();

		$blog_skin_ID = $Blog->get_skin_ID();
		$SkinCache = & get_SkinCache();
		$Skin = & $SkinCache->get_by_ID( $blog_skin_ID );
	}

	require $inc_path.'links/views/_link_list.view.php';
}


/**
 * Get line for CSV file from provided array
 *
 * @param array Row data
 * @param string Delimiter
 * @param string Enclosure
 * @param string End of line
 * @return string
 */
function get_csv_line( $row, $delimiter = ';', $enclosure = '"', $eol = "\n" )
{
	foreach( $row as $r => $cell )
	{
		$row[ $r ] = str_replace( $enclosure, $enclosure.$enclosure, $cell );
		if( strpos( $cell, $delimiter ) !== false )
		{
			$row[ $r ] = $enclosure.$row[ $r ].$enclosure;
		}
	}

	return implode( $delimiter, $row ).$eol;
}


/**
 * Display a panel to upload files before import
 *
 * @param array Params
 * @return array Already uploaded files in the requested folder
 */
function display_importer_upload_panel( $params = array() )
{
	global $admin_url, $media_path;

	$params = array_merge( array(
			'folder'                 => '',
			'allowed_extensions'     => 'csv', // Allowed extensions to import, separated by |
			'infolder_extensions'    => false, // Allowed extensions inside folders, separated by |, FALSE - to don't find files in subfolders
			'folder_with_extensions' => false, // Find folders which contain at least one file with extensions(separated by |) in subfolders recursively
			'find_attachments'       => false,
			'display_type'           => false,
			'help_slug'              => '',
			'refresh_url'            => '',
		), $params );

	evo_flush();

	// Get available files to import from the folder /media/import/
	$import_files = get_import_files( $params['folder'], $params['allowed_extensions'], $params['infolder_extensions'], $params['find_attachments'], $params['folder_with_extensions'] );

	load_class( '_core/ui/_table.class.php', 'Table' );
	$Table = new Table( NULL, 'import' );

	$Table->cols = array();
	$Table->cols[] = array( 'th' => T_('Import'), 'td_class' => 'shrinkwrap' );
	$Table->cols[] = array( 'th' => T_('File') );
	if( $params['display_type'] )
	{	// Display file type:
		$Table->cols[] = array( 'th' => T_('Type') );
	}
	$Table->cols[] = array( 'th' => T_('Date'), 'td_class' => 'shrinkwrap' );

	// Get link to manual page:
	$manual_link = ( empty( $params['help_slug'] ) ? '' : get_manual_link( $params['help_slug'] ) );

	$Table->title = T_('Potential files to be imported').$manual_link;
	if( ! empty( $params['refresh_url'] ) )
	{	// Display a link to refresh the uploaded files:
		$Table->title .= ' - '.action_icon( T_('Refresh'), 'refresh', $params['refresh_url'], T_('Refresh'), 3, 4 );
	}

	$FileRootCache = & get_FileRootCache();
	$FileRoot = & $FileRootCache->get_by_type_and_ID( 'import', '0', true );
	$import_perm_view = check_user_perm( 'files', 'view', false, $FileRoot );
	if( $import_perm_view )
	{ // Current user must has access to the import dir
		if( check_user_perm( 'files', 'edit_allowed', false, $FileRoot ) )
		{ // User has full access
			$import_title = T_('Upload/Manage import files');
		}
		else if( check_user_perm( 'files', 'add', false, $FileRoot ) )
		{ // User can only upload the files to import root
			$import_title = T_('Upload import files');
		}
		else
		{ // Only view
			$import_title = T_('View import files');
		}
		$Table->title .= ' - '
			.action_icon( $import_title, 'folder', $admin_url.'?ctrl=files&amp;root=import_0&amp;path='.$params['folder'], $import_title, 3, 4,
				array( 'onclick' => 'return import_files_window()' )
			).' <span class="note">(popup)</span>';
	}
	$Table->display_init();

	echo $Table->params['before'];

	// TITLE:
	$Table->display_head();

	if( empty( $import_files ) )
	{	// No files to import:
		$Table->total_pages = 0;
		$Table->no_results_text = '<div class="center">'.T_('We have not found any suitable file to perform the import. Please read the details at the manual page.').$manual_link.'</div>';

		// BODY START:
		$Table->display_body_start();
		$Table->display_list_start();
		$Table->display_list_end();
		// BODY END:
		$Table->display_body_end();
	}
	else
	{	// Display the files to import in table:

		// TABLE START:
		$Table->display_list_start();

		// COLUMN HEADERS:
		$Table->display_col_headers();
		// BODY START:
		$Table->display_body_start();

		foreach( $import_files as $import_file )
		{
			$Table->display_line_start();

			// Checkbox to import
			$Table->display_col_start();
			echo '<input type="radio" name="import_file" value="'.$import_file['path'].'"'.( get_param( 'import_file' ) == $import_file['path'] ? ' checked="checked"' : '' ).' />';
			$Table->display_col_end();

			// File
			$Table->display_col_start();
			echo $import_file['name'];
			$Table->display_col_end();

			// Type
			if( $params['display_type'] )
			{	// Display file type:
				$Table->display_col_start();
				echo $import_file['type'];
				$Table->display_col_end();
			}

			// File date
			$Table->display_col_start();
			echo date( locale_datefmt().' '.locale_timefmt(), $import_file['date'] );
			$Table->display_col_end();

			$Table->display_line_end();

			evo_flush();
		}

		// BODY END:
		$Table->display_body_end();

		// TABLE END:
		$Table->display_list_end();
	}

	echo $Table->params['after'];

?>
<script>
jQuery( '.table_scroll td' ).on( 'click', function()
{
	jQuery( this ).parent().find( 'input[type=radio]' ).prop( 'checked', true );
} );
</script>
<?php
	if( $import_perm_view )
	{	// Current user must has access to the import dir:

		// Initialize JavaScript to build and open window:
		echo_modalwindow_js();
?>
<script>
function import_files_window()
{
	openModalWindow( '<span class="loader_img absolute_center" title="<?php echo T_('Loading...'); ?>"></span>',
		'90%', '80%', true, '<?php echo TS_('Add/Link files'); ?>', '', true );
	jQuery.ajax(
	{
		type: 'POST',
		url: '<?php echo get_htsrv_url(); ?>async.php',
		data:
		{
			'action': 'import_files',
			'path': '<?php echo $params['folder']; ?>',
			'crumb_import': '<?php echo get_crumb( 'import' ); ?>',
		},
		success: function( result )
		{
			openModalWindow( result, '90%', '80%', true, '<?php echo TS_('Upload/Manage import files'); ?>', '' );
		}
	} );
	return false;
}

jQuery( document ).on( 'click', '#modal_window button[data-dismiss=modal]', function()
{	// Reload page on closing modal window to display new uploaded files:
	location.reload();
} );
</script>
<?php
	}

	return $import_files;
}


/**
 * Get available files to import from the requested folder
 *
 * @param string Sub folder in the folder /media/import/
 * @param string Allowed extensions to import, separated by |
 * @param string|boolean Allowed extensions inside folders, separated by |, FALSE - to don't find files in subfolders
 * @param boolean TRUE - to find folder of attachments
 * @param string|boolean Find folders which contain at least one file with extensions(separated by |) recursively in all subfolders
 * @return array Files
 */
function get_import_files( $folder = '', $allowed_extensions = 'xml|txt|zip', $infolder_extensions = 'xml|txt', $find_attachments = true, $folder_with_extensions = false )
{
	global $media_path;

	// Get all files from the import folder:
	$root_path = $media_path.'import/'.( empty( $folder ) ? '' : $folder.'/' );
	$files = get_filenames( $root_path, array(
			'flat' => false
		) );

	$import_files = array();

	if( empty( $files ) )
	{ // No access to the import folder OR it is empty
		return $import_files;
	}

	$file_paths = array();
	foreach( $files as $folder_name => $file )
	{
		if( is_array( $file ) )
		{	// It is a folder
			if( $infolder_extensions !== false )
			{	// Find files inside:
				foreach( $file as $key => $sub_file )
				{
					if( is_string( $sub_file ) && preg_match( '/\.('.$infolder_extensions.')$/i', $sub_file, $file_matches ) )
					{
						$file_paths[] = array( $sub_file, $file_matches[1] );
					}
				}
			}
			if( $folder_with_extensions !== false && check_folder_with_extensions( $root_path.$folder_name, $folder_with_extensions ) )
			{	// Use full folder as single import pack when it contains file with requested extensions:
				$file_paths[] = array( $root_path.$folder_name, '$dir$' );
			}
		}
		elseif( is_string( $file ) && preg_match( '/\.('.$allowed_extensions.')$/i', $file, $file_matches ) )
		{	// File in the root:
			$file_paths[] = array( $file, $file_matches[1] );
		}
	}

	$media_path_length = strlen( $media_path.'import/'.( empty( $folder ) ? '' : $folder.'/' ) );

	foreach( $file_paths as $file_data )
	{
		switch( $file_data[1] )
		{
			case '$dir$':
				$file_type = T_('Folder');
				break;

			case 'zip':
				$file_type = T_('Compressed Archive');
				break;

			default:
				if( $find_attachments && ( $file_attachments_folder = get_import_attachments_folder( $file_data[0] ) ) )
				{	// Probably it is a file with attachments folder:
					$file_type = sprintf( T_('Complete export (attachments folder: %s)'), '<code>'.substr( $file_attachments_folder, strlen( dirname( $file_data[0] ) ) + 1, -1 ).'</code>' );
				}
				else
				{	// Single XML file without attachments folder:
					$file_type = T_('Basic export').( $find_attachments ? ' ('.T_('no attachments folder found').')' : '' );
				}
				break;
		}

		$import_files[] = array(
				'path' => $file_data[0],
				'name' => substr( $file_data[0], $media_path_length ),
				'type' => $file_type,
				'date' => filemtime( $file_data[0] ),
			);
	}

	// Sort import files by date DESC:
	usort( $import_files, 'sort_import_files_callback' );

	return $import_files;
}


/**
 * Callback function to sort import files by date DESC
 *
 * @param array Import file data
 * @param array Import file data
 * @return boolean
 */
function sort_import_files_callback( $a, $b )
{
	if( $a['date'] == $b['date'] )
	{	// Sort by file name with same dates:
		return $a['name'] < $b['name'] ? -1 : 1;
	}

	return ( $a['date'] > $b['date'] ? -1 : 1 );
}


/**
 * Find attachments folder path for given import file path
 *
 * @param string File path
 * @param boolean TRUE to use first found folder if no reserved folders not found before
 * @return string Folder path
 */
function get_import_attachments_folder( $file_path, $first_folder = false )
{
	$file_name = basename( $file_path );
	$file_folder_path = dirname( $file_path ).'/';
	$folder_full_name = preg_replace( '#\.[^\.]+$#', '', $file_name );
	$folder_part_name = preg_replace( '#_[^_]+$#', '', $folder_full_name );

	// Find and get first existing folder with attachments:
	if( is_dir( $file_folder_path.$folder_full_name ) )
	{	// 1st priority folder:
		return $file_folder_path.$folder_full_name.'/';
	}
	if( is_dir( $file_folder_path.$folder_part_name.'_files' ) )
	{	// 2nd priority folder:
		return $file_folder_path.$folder_part_name.'_files/';
	}
	if( is_dir( $file_folder_path.$folder_part_name.'_attachments' ) )
	{	// 3rd priority folder:
		return $file_folder_path.$folder_part_name.'_attachments/';
	}
	if( is_dir( $file_folder_path.'b2evolution_export_files' ) )
	{	// 4th priority folder:
		return $file_folder_path.'b2evolution_export_files/';
	}
	if( is_dir( $file_folder_path.'export_files' ) )
	{	// 5th priority folder:
		return $file_folder_path.'export_files/';
	}
	if( is_dir( $file_folder_path.'import_files' ) )
	{	// 6th priority folder:
		return $file_folder_path.'import_files/';
	}
	if( is_dir( $file_folder_path.'files' ) )
	{	// 7th priority folder:
		return $file_folder_path.'files/';
	}
	if( is_dir( $file_folder_path.'attachments' ) )
	{	// 8th priority folder:
		return $file_folder_path.'attachments/';
	}
	if( is_dir( $file_folder_path.'uploads' ) )
	{	// 9th priority folder:
		return $file_folder_path.'uploads/';
	}
	if( is_dir( $file_folder_path.'wp-content/uploads' ) )
	{	// 10th priority folder:
		return $file_folder_path.'wp-content/uploads/';
	}

	if( $first_folder )
	{	// Try to use first found folder:
		$files = scandir( $file_folder_path );
		foreach( $files as $file )
		{
			if( $file == '.' || $file == '..' )
			{	// Skip reserved dir names of the current path:
				continue;
			}
			if( is_dir( $file_folder_path.$file ) )
			{	// 11th priority folder:
				return $file_folder_path.$file.'/';
			}
		}
	}

	// File has no attachments folder
	return false;
}


/**
 * Clear string list of IDs to exclude wrong not ID/number/integer value
 *
 * @param string ID values separated by second param
 * @param string Separator
 * @return string Fixed list
 */
function clear_ids_list( $ids_list, $separator = ',' )
{
	if( $ids_list === '' )
	{	// Empty list:
		return $ids_list;
	}

	if( strpos( $ids_list, '-' ) === 0 )
	{	// Remove first '-' char from start, which is used for exluding list:
		$ids_list = substr( $ids_list, 1 );
	}

	$ids_list = explode( $separator, $ids_list );

	foreach( $ids_list as $i => $ID )
	{
		if( ! is_number( $ID ) )
		{	// Remove a not number value from list:
			unset( $ids_list[ $i ] );
		}
	}

	return implode( $separator, $ids_list );
}


/**
 * Check if the given allowed
 *
 * @param string The options which should be checked
 * @param string String of options separated by comma, '*' - allow all options, use '-' before options if they should be excluded/denied
 * @return boolean
 */
function is_allowed_option( $checked_option, $allowed_options )
{
	if( $allowed_options === '*' )
	{	// All options are allowed:
		return true;
	}

	$is_allowed_options = true;
	$allowed_options = $allowed_options;
	if( substr( $allowed_options, 0, 1 ) == '-' )
	{	// The options should excluded/denied:
		$allowed_options = substr( $allowed_options, 1 );
		$is_allowed_options = false;
	}
	$allowed_options = explode( ',', $allowed_options );
	$is_in_checked_array = in_array( $checked_option, $allowed_options );

	return ( $is_allowed_options ? $is_in_checked_array : ! $is_in_checked_array );
}


/**
 * Converts path into an array
 *
 * @param string Path usually in dot notation
 * @param string Value of the path property
 * @param string Separator string
 * @return array Schema property array
 */
function convert_path_to_array( $property, $value, $separator = '.' )
{
	$output = NULL;

	foreach( array_reverse( explode( $separator, $property ) ) as $key )
	{
		if( empty( $output ) )
		{
			$output = array( $key => $value );
		}
		else
		{
			$output = array( $key => $output );
		}
	}

	return $output;
}


/**
 * Get customizer url
 *
 * @param object Collection
 * @return string Customizer URL
 */
function get_customizer_url( $url_Blog = NULL )
{
	global $customizer_relative_url, $Blog;

	if( $url_Blog === NULL && isset( $Blog ) )
	{	// Use current collection:
		$url_Blog = $Blog;
	}

	return $url_Blog->get_baseurl_root().$customizer_relative_url;
}


/**
 * Convert operator alias to jQuery QueryBuilder format
 *
 * @param string Alias Operator
 * @return string Query Builder Operator
 */
function get_querybuilder_operator( $operator )
{
	switch( $operator )
	{
		case '=':
			return 'equal';
		case '!=':
		case '<>':
			return 'not_equal';
		case '<':
			return 'less';
		case '<=':
			return 'less_or_equal';
		case '>':
			return 'greater';
		case '>=':
			return 'greater_or_equal';
		default:
			return $operator;
	}
}


/**
 * Temporary function to check if we should use defer when loading scripts.
 */
function use_defer()
{
	global $disp, $ReqPath,
		$use_defer,
		$use_defer_for_backoffice,
		$use_defer_for_loggedin_users,
		$use_defer_for_anonymous_users,
		$use_defer_for_anonymous_disp_register,
		$use_defer_for_anonymous_disp_register_finish,
		$use_defer_for_anonymous_disp_users,
		$use_defer_for_anonymous_disp_anonpost,
		$use_defer_for_loggedin_disp_single_page,
		$use_defer_for_loggedin_disp_front,
		$use_defer_for_loggedin_disp_profile,
		$use_defer_for_loggedin_disp_pwdchange,
		$use_defer_for_loggedin_disp_edit,
		$use_defer_for_loggedin_disp_proposechange,
		$use_defer_for_loggedin_disp_edit_comment,
		$use_defer_for_loggedin_disp_comments,
		$use_defer_for_loggedin_disp_visits,
		$use_defer_for_loggedin_disp_messages,
		$use_defer_for_loggedin_disp_threads,
		$use_defer_for_loggedin_disp_users,
		$use_defer_for_loggedin_disp_contacts,
		$use_defer_for_default_register_form;
	
	$r =  $use_defer
		   && ( is_admin_page() ? $use_defer_for_backoffice : true )
		   && ( is_logged_in() ? $use_defer_for_loggedin_users : $use_defer_for_anonymous_users )
		   && ( $disp == 'register' ? $use_defer_for_anonymous_disp_register : true )
		   && ( $disp == 'register_finish' ? $use_defer_for_anonymous_disp_register_finish : true )
		   && ( $disp == 'users' ? $use_defer_for_anonymous_disp_users : true )
		   && ( $disp == 'anonpost' ? $use_defer_for_anonymous_disp_anonpost : true )
		   && ( empty( $disp ) && $ReqPath == '/htsrv/register.php' ? $use_defer_for_default_register_form : true )
		   && ( is_logged_in() && in_array( $disp, array( 'single', 'page' ) ) ? $use_defer_for_loggedin_disp_single_page : true )
		   && ( is_logged_in() && $disp == 'front' ? $use_defer_for_loggedin_disp_front : true )
		   && ( is_logged_in() && $disp == 'profile' ? $use_defer_for_loggedin_disp_profile : true )
		   && ( is_logged_in() && $disp == 'pwdchange' ? $use_defer_for_loggedin_disp_pwdchange : true )
		   && ( is_logged_in() && $disp == 'edit' ? $use_defer_for_loggedin_disp_edit : true )
		   && ( is_logged_in() && $disp == 'proposechange' ? $use_defer_for_loggedin_disp_proposechange : true )
		   && ( is_logged_in() && $disp == 'edit_comment' ? $use_defer_for_loggedin_disp_edit_comment : true )
		   && ( is_logged_in() && $disp == 'comments' ? $use_defer_for_loggedin_disp_comments : true )
		   && ( is_logged_in() && $disp == 'visits' ? $use_defer_for_loggedin_disp_visits : true )
		   && ( is_logged_in() && $disp == 'messages' ? $use_defer_for_loggedin_disp_messages : true )
		   && ( is_logged_in() && $disp == 'threads' ? $use_defer_for_loggedin_disp_threads : true )
		   && ( is_logged_in() && $disp == 'users' ? $use_defer_for_loggedin_disp_users : true )
		   && ( is_logged_in() && $disp == 'contacts' ? $use_defer_for_loggedin_disp_contacts : true );

	return $r;
}


/**
 * Get rendering error
 *
 * @param string Error message
 * @param string HTML tag: <p>, <span>, <div>
 * @return string
 */
function get_rendering_error( $error_message, $html_tag = 'p' )
{
	if( ! in_array( $html_tag, array( 'p', 'span', 'div' ) ) )
	{	// Force not allowed html tag:
		$html_tag = 'p';
	}

	return '<'.$html_tag.' class="evo_rendering_error">'.$error_message.'</'.$html_tag.'>';
}


/**
 * Display rendering error
 *
 * @param string Error message
 * @param string HTML tag: <p>, <span>, <div>
 */
function display_rendering_error( $error_message, $html_tag = 'p' )
{
	echo get_rendering_error( $error_message, $html_tag );
}


/**
 * Clean up rendering errors `<p class="evo_rendering_error">...</p>` from provided content
 *
 * @param string Content with rendering error
 * @return string Content without rendering error
 */
function clear_rendering_errors( $content )
{
	return preg_replace( '#<([a-z]+) class="evo_rendering_error">.+?</\1>#', '', $content );
}
?>