<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2011

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright	ocProducts Ltd
 * @package		core
 */

/*
support.php contains further support functions, which are shared between the installer and the main installation (i.e. global.php and global2.php are not used by the installer, and the installer emulates these functions functionality via minikernel.php).
*/

/**
 * Standard code module initialisation function.
 */
function init__support()
{
	global $PAGE_NAME_CACHE,$GETTING_PAGE_NAME;
	$PAGE_NAME_CACHE=NULL;
	$GETTING_PAGE_NAME=false;

	global $IS_MOBILE,$IS_MOBILE_TRUTH;
	$IS_MOBILE=NULL;
	$IS_MOBILE_TRUTH=NULL;

	// Heavily optimised! Ended up with preg_replace after trying lots of things
	global $HTML_ESCAPE_1,$HTML_ESCAPE_1_STRREP,$HTML_ESCAPE_2;
	$HTML_ESCAPE_1=array('/&/'/*,'/쯧,'/*/,'/"/','/\'/','/</','/>/'/*,'/�/'*/);
	$HTML_ESCAPE_1_STRREP=array('&'/*,'짬'/,'"','\'','<','>'/*,'�'*/);
	$HTML_ESCAPE_2=array('&amp;'/*,'&quot;','&quot;'*/,'&quot;','&#039;','&lt;','&gt;'/*,'&pound;'*/);
	
	global $BOT_MAP,$CACHE_BOT_TYPE;
	$BOT_MAP=NULL;
	$CACHE_BOT_TYPE=false;
	
	global $LOCALE_FILTER;
	$LOCALE_FILTER=NULL;
	
	global $HAS_COOKIES;
	$HAS_COOKIES=NULL;

	global $BROWSER_MATCHES_CACHE;
	$BROWSER_MATCHES_CACHE=array();

	$GLOBALS['MSN_DB']=NULL;

	// This is like NULL, but is a higher-precedence NULL that can also survive string layers (such as HTML forms). It should only be used when:
	//  - 'NULL' or '' or '-1' aren't appropriate (although '-1' is only appropriate when dealing with numbers held in strings, really).
	//  - OR, as the standard "ignore this field" indicator for query_update (so that "fractional edits" can happen without requiring a secondary API set or a messed up primary API)
	if (!defined('STRING_MAGIC_NULL')) define('STRING_MAGIC_NULL','!--:)abcUNLIKELY');
	// This is similar, but for integers. As before, it should only be used when NULL and -1 aren't appropiate OR as the "ignore this field" indicator.
	if (!defined('INTEGER_MAGIC_NULL')) define('INTEGER_MAGIC_NULL',1634817353); // VERY unlikely to occur, but is both a 32bit unsigned and a 32 bit signed number

	global $ZONE_DEFAULT_PAGES;
	$ZONE_DEFAULT_PAGES=array();

	global $PHP_REP_FROM,$PHP_REP_TO,$PHP_REP_TO_TWICE;
	$PHP_REP_FROM=array('\\',"\n",'$','"');
	$PHP_REP_TO=array('\\\\','\n','\$','\\"');
	$PHP_REP_TO_TWICE=array('\\\\\\\\','\\n','\\\\$','\\\\\"');

	global $IS_WIDE,$IS_WIDE_HIGH;
	$IS_WIDE=NULL;
	$IS_WIDE_HIGH=NULL;

	global $ADDON_INSTALLED_CACHE;
	$ADDON_INSTALLED_CACHE=array();
}

/**
 * Search for a template.
 *
 * @param  ID_TEXT			The codename of the template being loaded
 * @param  ?LANGUAGE_NAME 	The language to load the template in (templates can embed language references) (NULL: users own language)
 * @param  ID_TEXT			The theme to use
 * @param  string				File type suffix of template file (e.g. .tpl)
 * @param  string				Subdirectory type to look in
 * @set    templates css
 * @return ?array				List of parameters needed for the _do_template function to be able to load the template (NULL: could not find the template)
 */
function find_template_place($codename,$lang,$theme,$suffix,$type)
{
	global $FILE_ARRAY,$CURRENT_SHARE_USER;
	
	$prefix_default=get_file_base().'/themes/';
	$prefix=($theme=='default')?$prefix_default:(get_custom_file_base().'/themes/');

	if (!isset($FILE_ARRAY))
	{
		if ((is_file($prefix.$theme.'/'.$type.'_custom/'.$codename.$suffix)) && (!in_safe_mode()))
			$place=array($theme,'/'.$type.'_custom/');
		elseif (is_file($prefix.$theme.'/'.$type.'/'.$codename.$suffix))
			$place=array($theme,'/'.$type.'/');
		elseif (($CURRENT_SHARE_USER!==NULL) && ($theme!='default') && (is_file(get_file_base().'/themes/'.$theme.'/'.$type.'_custom/'.$codename.$suffix)))
			$place=array($theme,'/'.$type.'_custom/');
		elseif (($CURRENT_SHARE_USER!==NULL) && ($theme!='default') && (is_file(get_file_base().'/themes/'.$theme.'/'.$type.'/'.$codename.$suffix)))
			$place=array($theme,'/'.$type.'/');
		elseif (($CURRENT_SHARE_USER!==NULL) && (is_file(get_custom_file_base().'/themes/default/'.$type.'_custom/'.$codename.$suffix)))
			$place=array('default','/'.$type.'_custom/');
		elseif (($CURRENT_SHARE_USER!==NULL) && (is_file(get_custom_file_base().'/themes/default/'.$type.'/'.$codename.$suffix)))
			$place=array('default','/'.$type.'/');
		elseif ((is_file($prefix_default.'default'.'/'.$type.'_custom/'.$codename.$suffix)) && (!in_safe_mode()))
			$place=array('default','/'.$type.'_custom/');
		elseif (is_file($prefix_default.'default'.'/'.$type.'/'.$codename.$suffix))
			$place=array('default','/'.$type.'/');
		else $place=NULL;

		if ($place===NULL) // Get desparate, search in themes other than current and default
		{
			$dh=opendir(get_file_base().'/themes');
			while (($possible_theme=readdir($dh)))
			{
				if ((substr($possible_theme,0,1)!='.') && ($possible_theme!='default') && ($possible_theme!=$theme) && ($possible_theme!='map.ini') && ($possible_theme!='index.html'))
				{
					$fullpath=get_custom_file_base().'/themes/'.$possible_theme.'/'.$type.'_custom/'.$codename.$suffix;
					if (is_file($fullpath))
					{
						$place=array($possible_theme,'/'.$type.'_custom/');
						break;
					}
				}
			}
			closedir($dh);
		}
	} else
	{
		$place=array('default','/'.$type.'/');
	}
	
	return $place;
}

/**
 * Find whether panels and the header/footer areas won't be shown.
 *
 * @return BINARY		Result.
 */
function is_wide_high()
{
	global $IS_WIDE_HIGH;
	if (!is_null($IS_WIDE_HIGH)) return $IS_WIDE_HIGH;

	$IS_WIDE_HIGH=get_param_integer('wide_high',get_param_integer('keep_wide_high',get_param_integer('wide_print',0)));
	return $IS_WIDE_HIGH;
}

/**
 * Find whether panels will be shown.
 *
 * @return BINARY		Result.
 */
function is_wide()
{
	global $IS_WIDE;
	if (!is_null($IS_WIDE)) return $IS_WIDE;
	
	global $ZONE;
	$IS_WIDE=get_param_integer('wide',get_param_integer('keep_wide',(is_wide_high()==1)?1:$ZONE['zone_wide']));
	if ($IS_WIDE==0) return 0;

	// Need to check it is allowed
	$theme=$GLOBALS['FORUM_DRIVER']->get_theme();
	$ini_path=(($theme=='default')?get_file_base():get_custom_file_base()).'/themes/'.$theme.'/theme.ini';
	if (is_file($ini_path))
	{
		require_code('files');
		$details=better_parse_ini_file($ini_path);
		if ((isset($details['supports_wide'])) && ($details['supports_wide']=='0'))
		{
			$IS_WIDE=0;
			return $IS_WIDE;
		}
	}
	
	return $IS_WIDE;
}

/**
 * Get string length, with utf-8 awareness where possible/required.
 *
 * @param  string		The string to get the length of.
 * @return integer	The string length.
 */
function ocp_mb_strlen($in)
{
	if (strtolower(get_charset())!='utf-8') return strlen($in);
	if (function_exists('mb_strlen')) return @mb_strlen($in); // @ is because there could be invalid unicode involved
	if (function_exists('iconv_strlen')) return @iconv_strlen($in);
	return strlen($in);
}

/**
 * Return part of a string, with utf-8 awareness where possible/required.
 *
 * @param  string		The subject.
 * @param  integer	The start position.
 * @param  ?integer  The length to extract (NULL: all remaining).
 * @param  boolean	Whether to force unicode as on.
 * @return ~string	String part (false: $start was over the end of the string).
 */
function ocp_mb_substr($in,$from,$amount=NULL,$force=false)
{
	if (is_null($amount)) $amount=ocp_mb_strlen($in)-$from;

	if ((!$force) && (strtolower(get_charset())!='utf-8')) return substr($in,$from,$amount);

	if (function_exists('iconv_substr')) return @iconv_substr($in,$from,$amount);
	if (function_exists('mb_substr')) return @mb_substr($in,$from,$amount);

	$ret=substr($in,$from,$amount);
	$end=ord(substr($ret,-1));
	if (($end>=192) && ($end<=223)) $ret.=substr($in,$from+$amount,1);
	if ($from!=0)
	{
		$start=ord(substr($ret,0,1));
		if (($start>=192) && ($start<=223)) $ret=substr($in,$from-1,1).$ret;
	}
	return $ret;
}

/**
 * Make a string lowercase, with utf-8 awareness where possible/required.
 *
 * @param  string	Subject.
 * @return string	Result.
 */
function ocp_mb_strtolower($in)
{
	if (strtolower(get_charset())!='utf-8') return strtolower($in);

	if (function_exists('mb_strtolower')) return mb_strtolower($in);

	return strtolower($in);
}

/**
 * Make a string uppercase, with utf-8 awareness where possible/required.
 *
 * @param  string	Subject.
 * @return string	Result.
 */
function ocp_mb_strtoupper($in)
{
	if (strtoupper(get_charset())!='utf-8') return strtoupper($in);

	if (function_exists('mb_strtoupper')) return mb_strtoupper($in);

	return strtoupper($in);
}

/**
 * Find whether a file/directory is writeable. This function is designed to get past that the PHP is_writable function does not work properly on Windows.
 *
 * @param  PATH			The file path
 * @return boolean		Whether the file is writeable
 */
function is_writable_wrap($path)
{
	if (strtoupper(substr(PHP_OS,0,3))!='WIN') return is_writable($path);
	
	if (!file_exists($path)) return false;
	
	if (is_dir($path))
	{
		/*if (false) // ideal, but too dangerous as sometimes you can write files but not delete again
		{
			$test=@fopen($path.'/ocp.delete.me','wt');
			if ($test!==false)
			{
				fclose($test);
				unlink($path.'/ocp.delete.me');
				return true;
			}
			return false;
		}*/

		return is_writable($path); // imperfect unfortunately; but unlikely to cause a problem
	} else
	{
		$test=@fopen($path,'ab');
		if ($test!==false)
		{
			fclose($test);
			return true;
		}
		return false;
	}
}

/**
 * Discern the cause of a file-write error, and show an appropriate error message.
 *
 * @param PATH			File path that could not be written
 */
function intelligent_write_error($path)
{
	if (file_exists($path))
	{
		warn_exit(do_lang_tempcode('WRITE_ERROR',escape_html($path)));
	}
	elseif (file_exists(dirname($path)))
	{
		if (strpos($path,'/templates_cached/')!==false) critical_error('PASSON',do_lang('WRITE_ERROR_CREATE',escape_html($path),escape_html(dirname($path))));
		warn_exit(do_lang_tempcode('WRITE_ERROR_CREATE',escape_html($path),escape_html(dirname($path))));
	} else
	{
		warn_exit(do_lang_tempcode('WRITE_ERROR_MISSING_DIRECTORY',escape_html(dirname($path)),escape_html(dirname(dirname($path)))));
	}
}

/**
 * Discern the cause of a file-write error, and return an appropriate error message.
 *
 * @param  PATH			File path that could not be written
 * @return tempcode		Message
 */
function intelligent_write_error_inline($path)
{
	if (file_exists($path))
		return do_lang_tempcode('WRITE_ERROR',escape_html($path));
	elseif (file_exists(dirname($path)))
		return do_lang_tempcode('WRITE_ERROR_CREATE',escape_html($path),escape_html(dirname($path)));
	else
		return do_lang_tempcode('WRITE_ERROR_MISSING_DIRECTORY',escape_html(dirname($path)),escape_html(dirname(dirname($path))));
	return new ocp_tempcode();
}

/**
 * Find whether a fractional edit is underway.
 *
 * @return boolean		Whether a fractional edit is underway
 */
function fractional_edit()
{
	return post_param_integer('fractional_edit',0)==1;
}

/**
 * Helper function for usort to sort a list by string length.
 *
 * @param  string			The first string to compare
 * @param  string			The second string to compare
 * @return integer		The comparison result (0 for equal, -1 for less, 1 for more)
 */
function strlen_sort($a,$b)
{
	if (!isset($a)) $a='';
	if (!isset($b)) $b='';
	if ($a==$b) return 0;
	if (!is_string($a))
	{
		global $M_SORT_KEY;
		return (strlen($a[$M_SORT_KEY])<strlen($b[$M_SORT_KEY]))?-1:1;
	}
	return (strlen($a)<strlen($b))?-1:1;
}

/**
 * Find whether we have no forum on this website.
 *
 * @return boolean		Whether we have no forum on this website
 */
function has_no_forum()
{
	if (get_forum_type()=='none') return true;
	if ((get_forum_type()=='ocf') && (!addon_installed('ocf_forum'))) return true;
	return false;
}

/**
 * Check to see if an addon is installed. This only works with addons written with addon_registry hooks, which is all the bundled ocPortal addons; it is unlikely to work with third-party addons.
 *
 * @param  ID_TEXT		The module name
 * @return boolean		Whether it is
 */
function addon_installed($addon)
{
	global $ADDON_INSTALLED_CACHE;
	if (isset($ADDON_INSTALLED_CACHE[$addon])) return $ADDON_INSTALLED_CACHE[$addon];
	$answer=is_file(get_file_base().'/sources/hooks/systems/addon_registry/'.filter_naughty($addon).'.php') || is_file(get_file_base().'/sources_custom/hooks/addon_registry/'.filter_naughty($addon).'.php');
	$ADDON_INSTALLED_CACHE[$addon]=$answer;
	return $answer;
}

/**
 * Convert a float to a "technical string representation of a float".
 *
 * @param  float			The number
 * @param  integer		The number of decimals to keep
 * @return string			The string converted
 */
function float_to_raw_string($num,$dec_keep=0)
{
	$str=number_format($num,5,'.','');
	if (strpos($str,'.')!==false)
	{
		$str=rtrim($str,'0');
		if ($dec_keep==0) $str=rtrim($str,'.');
		$decs=strlen($str)-strpos($str,'.')-1;
		for ($i=0;$i<$dec_keep-$decs;$i++)
		{
			$str.='0';
		}
	}
	return $str;
}

/**
 * Format the given float number as a nicely formatted string.
 *
 * @param  float			The value to format
 * @param  integer		The number of fractional digits
 * @return string			Nicely formatted string
 */
function float_format($val,$frac_digits=2)
{
	$locale=function_exists('localeconv')?localeconv():array('decimal_point'=>'.','thousands_sep'=>',');
	//$frac_digits=$locale['frac_digits']; // This seems to not work on all PHP configurations
	if ($locale['thousands_sep']=='') $locale['thousands_sep']=',';
	return number_format($val,$frac_digits,$locale['decimal_point'],$locale['thousands_sep']);
}

/**
 * Format the given integer number as a nicely formatted string.
 *
 * @param  integer		The value to format
 * @return string			Nicely formatted string
 */
function integer_format($val)
{
	$locale=function_exists('localeconv')?localeconv():array('decimal_point'=>'.','thousands_sep'=>',');
	if ($locale['thousands_sep']=='') $locale['thousands_sep']=',';
	return number_format($val,0,$locale['decimal_point'],$locale['thousands_sep']);
}

/**
 * Helper function to sort a list of maps by the value at $key in each of those maps.
 *
 * @param  array			The first to compare
 * @param  array			The second to compare
 * @return integer		The comparison result (0 for equal, -1 for less, 1 for more)
 */
function multi_sort($a,$b)
{
	global $M_SORT_KEY;
	$keys=explode(',',is_string($M_SORT_KEY)?$M_SORT_KEY:strval($M_SORT_KEY));
	$first_key=$keys[0];
	if ($first_key[0]=='!') $first_key=substr($first_key,1);
	
	if ((is_string($a[$first_key])) || (is_object($a[$first_key])))
	{
		$ret=0;
		do
		{
			$key=array_shift($keys);
			$av=$a[$key];
			$bv=$b[$key];

			// If calling, must put an "@" around the uasort call because of a PHP bug
			if (is_object($av)) $av=$av->evaluate();
			if (is_object($bv)) $bv=$bv->evaluate();

			if ($key[0]=='!') // Flip around
			{
				$key=substr($key,1);
				$ret=-strnatcasecmp($av,$bv);
			} else
			{
				$ret=strnatcasecmp($av,$bv);
			}
		}
		while ((count($keys)!=0) && ($ret==0));
		return $ret;
	}

	do
	{
		$key=array_shift($keys);
		if ($key[0]=='!') // Flip around
		{
			$key=substr($key,1);
			$ret=($a[$key]>$b[$key])?-1:(($a[$key]==$b[$key])?0:1);
		} else
		{
			$ret=($a[$key]>$b[$key])?1:(($a[$key]==$b[$key])?0:-1);
		}
	}
	while ((count($keys)!=0) && ($ret==0));
	return $ret;
}

/*!*
 * Finds if a function is being run underneath another function, and exit if there is a death message to output. This function should only be used when coding and does not work on <PHP4.3.
 *
 * @param  string			The function to check running underneath
 * @param  ?string		The message to exit with (NULL: return, do not exit)
 * @return boolean		Whether we are
 */
/*function debug_running_underneath($function,$death_message=NULL)
{
	if (!function_exists('debug_backtrace')) return false;

	$stack=debug_backtrace();
	foreach ($stack as $level)
	{
		if (in_array($function,$level))
		{
			if (!is_null($death_message)) fatal_exit($death_message);
			return true;
		}
	}
	return false;
}*/

/**
 * Require all code relating to the OCF forum
 */
function ocf_require_all_forum_stuff()
{
	require_lang('ocf');

	require_code('ocf_members');
	require_code('ocf_topics');
	require_code('ocf_posts');
	require_code('ocf_moderation');
	require_code('ocf_groups');
	require_code('ocf_forums');
	require_code('ocf_general');
}

/**
 * Turn the tempcode lump into a standalone page (except for header/footer which is assumed already handled elsewhere).
 *
 * @param  tempcode		The tempcode to put into a nice frame
 * @param  ?mixed			'Additional' message (NULL: none)
 * @param  string			The type of special message
 * @set    inform warn ""
 * @param  boolean		Whether to automatically include the header and footer templates
 * @return tempcode		Standalone page
 */
function globalise($middle,$message=NULL,$type='',$include_header_and_footer=false)
{
	require_code('site');

	global $DONE_HEADER;
	
	global $CYCLES; $CYCLES=array(); // Here we reset some Tempcode environmental stuff, because template compilation or preprocessing may have dirtied things

	if (is_null($GLOBALS['HELPER_PANEL_TUTORIAL'])) $GLOBALS['HELPER_PANEL_TUTORIAL']='';
	if (is_null($GLOBALS['HELPER_PANEL_HTML'])) $GLOBALS['HELPER_PANEL_HTML']='';
	if (is_null($GLOBALS['HELPER_PANEL_TEXT'])) $GLOBALS['HELPER_PANEL_TEXT']='';
	if (is_null($GLOBALS['HELPER_PANEL_PIC'])) $GLOBALS['HELPER_PANEL_PIC']='';
	$_message=(!is_null($message))?do_template('ADDITIONAL',array('_GUID'=>'b4c9f0a0bbfb9344d00c29db8ff5715d','TYPE'=>$type,'MESSAGE'=>$message)):new ocp_tempcode();
	if (get_option('show_docs')=='0') $GLOBALS['HELPER_PANEL_TUTORIAL']='';
	$global=new ocp_tempcode();
	$bail_out=(isset($DONE_HEADER) && $DONE_HEADER);
	if (($include_header_and_footer) && (!$bail_out)) $global->attach(do_header());
	$global->attach(do_template('GLOBAL',array('_GUID'=>'592faa2c0e8bf2dc3492de2c11ca7131','HELPER_PANEL_TUTORIAL'=>$GLOBALS['HELPER_PANEL_TUTORIAL'],'HELPER_PANEL_HTML'=>$GLOBALS['HELPER_PANEL_HTML'],'HELPER_PANEL_TEXT'=>$GLOBALS['HELPER_PANEL_TEXT'],'HELPER_PANEL_PIC'=>$GLOBALS['HELPER_PANEL_PIC'],'MESSAGE_TOP'=>$GLOBALS['ATTACHED_MESSAGES'],'MESSAGE'=>$_message,'MIDDLE'=>$middle,'BREADCRUMBS'=>breadcrumbs())));
	if ($include_header_and_footer) $global->attach(do_footer($bail_out));
	$global->handle_symbol_preprocessing();
	
	if (get_value('xhtml_strict')==='1')
	{
		$global=make_xhtml_strict($global);
	}
	
	return $global;
}

/**
 * Create file with unique file name, but works around compatibility issues between servers.
 *
 * @param  string		The prefix of the temporary file name.
 * @return ~string	The name of the temporary file (false: error).
 */
function ocp_tempnam($prefix)
{
	$problem_saving=((ini_get('safe_mode')=='1') || ((@strval(ini_get('open_basedir'))!='') && (preg_match('#(^|:|;)/tmp($|:|;|/)#',ini_get('open_basedir'))==0)));
	$local_path=get_custom_file_base().'/safe_mode_temp/';
	$server_path='/tmp/';
	$tmp_path=$problem_saving?$local_path:$server_path;
	$tempnam=tempnam($tmp_path,$prefix);
	if (($tempnam===false) && (!$problem_saving))
	{
		$problem_saving=true;
		$tempnam=tempnam($local_path,$prefix);
	}
	return $tempnam;
}

/**
 * Take a Tempcode object and run some hackerish code to make it XHTML-strict.
 *
 * @param  object			Tempcode object
 * @return object			Tempcode object (no longer cache safe)
 */
function make_xhtml_strict($global)
{
	$_global=$global->evaluate();
	$_global=str_replace(
		'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
		'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
		$_global);
	$_global=preg_replace('#(<a\s[^>]*)onclick="([^"]*)"(\s[^>]*)target="_blank"#','${1}onclick="this.target=\'_blank\'; ${2}"${3}',$_global);
	$_global=preg_replace('#(<a\s[^>]*)target="_blank"(\s[^>]*)onclick="([^"]*)"#','${1}onclick="this.target=\'_blank\'; ${3}"${2}',$_global);
	$_global=preg_replace('#(<a\s[^>]*)target="_blank"#','${1}onclick="this.target=\'_blank\';"',$_global);
	$_global=preg_replace('#(<form\s[^>]*)onsubmit="([^"]*)"(\s[^>]*)target="_blank"#','${1}onsubmit="this.target=\'_blank\'; ${2}"${3}',$_global);
	$_global=preg_replace('#(<form\s[^>]*)target="_blank"(\s[^>]*)onsubmit="([^"]*)"#','${1}onsubmit="this.target=\'_blank\'; ${3}"${2}',$_global);
	$_global=preg_replace('#(<form\s[^>]*)target="_blank"#','${1}onsubmit="this.target=\'_blank\';"',$_global);
	$_global=preg_replace('#(<(a|form)\s[^>]*)target="[^"]*"#','${1}',$_global);
	return make_string_tempcode($_global);
}

/**
 * Peek at a stack element.
 *
 * @param  array			The stack to peek in
 * @param  integer		The depth into the stack we are peaking
 * @return mixed			The result of the peeking
 */
function array_peek($array,$depth_down=1)
{
	$count=count($array);
	if ($count-$depth_down<0) return NULL;
	return $array[$count-$depth_down];
}

/**
 * See if the current URL matches the given ocPortal match tags.
 *
 * @param  string		Match tags
 * @param  boolean		Check against POSTed data too
 * @return boolean		Whether there is a match
 */
function match_key_match($match_tag,$support_post=false)
{
	$req_func=$support_post?'either_param':'get_param';

	$potentials=explode(',',$match_tag);
	foreach ($potentials as $potential)
	{
		$parts=explode(':',$potential);
		if (($parts[0]=='_WILD') || ($parts[0]=='_SEARCH')) $parts[0]=get_zone_name();
		if ((!array_key_exists(1,$parts)) || ($parts[1]=='_WILD')) $parts[1]=get_page_name();
		if (($parts[0]==get_zone_name()) && ($parts[1]==get_page_name()))
		{
			$bad=false;
			for ($i=2;$i<count($parts);$i++)
			{
				if ($parts[$i]!='')
				{
					if (($i==2) && (strpos($parts[$i],'=')===false))
					{
						$parts[$i]='type='.$parts[$i];
					}
					elseif (($i==3) && (strpos($parts[$i],'=')===false))
					{
						$parts[$i]='id='.$parts[$i];
					}
				}

				$subparts=explode('=',$parts[$i]);
				if ((count($subparts)!=2) || (call_user_func_array($req_func,array($subparts[0],'misc'))!=$subparts[1]))
				{
					$bad=true;
					continue;
				}
			}
			if (!$bad) return true;
		}
	}
	return false;
}

/**
 * Get the name of the current page
 *
 * @return ID_TEXT			The current page name
 */
function get_page_name()
{
	global $PAGE_NAME_CACHE;
	if (isset($PAGE_NAME_CACHE)) return $PAGE_NAME_CACHE;
	global $ZONE,$GETTING_PAGE_NAME;
	if ($GETTING_PAGE_NAME) return 'unknown';
	$GETTING_PAGE_NAME=true;
	$page=get_param('page','');
	if (strlen($page)>80)
	{
		warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
	}
	if (($page=='') && (!is_null($ZONE)))
	{
		$page=ocp_srv('QUERY_STRING');
		if ((strpos($page,'=')!==false) || ($page==''))
		{
			$page=$ZONE['zone_default_page'];
			if (is_null($page)) $page='';
		}
	}
	$page=filter_naughty($page);
	if (!is_null($ZONE)) $PAGE_NAME_CACHE=$page;
	$GETTING_PAGE_NAME=false;
	return $page;
}

/**
 * Take a list of maps, and make one of the values of each array the index of a map to the map
 *
 * @param  string			The key key of our maps that reside in our map
 * @param  array			The list of maps
 * @return array			The collapsed map
 */
function list_to_map($map_value,$list)
{
	$i=0;

	$new_map=array();

	foreach ($list as $map)
	{
		$key=$map[$map_value];
		$new_map[$key]=$map;

		$i++;
	}

	if ($i>0) return $new_map;
	return array();
}

/**
 * Take a list of maps of just two elements, and make it into a single map
 *
 * @param  string			The key key of our maps that reside in our map
 * @param  string			The value key of our maps that reside in our map
 * @param  array			The map of maps
 * @return array			The collapsed map
 */
function collapse_2d_complexity($key,$value,$list)
{
	$new_map=array();
	foreach ($list as $map)
	{
		$new_map[$map[$key]]=$map[$value];
	}
	
	return $new_map;
}

/**
 * Take a list of maps of just one element, and make it into a single map
 *
 * @param  string			The key of our maps that reside in our map
 * @param  array			The map of maps
 * @return array			The collapsed map
 */
function collapse_1d_complexity($key,$list)
{
	$new_map=array();
	foreach ($list as $map)
	{
		$new_map[]=$map[$key];
	}
	
	return $new_map;
}

/**
 * Get server environment variables.
 *
 * @param  string			The variable name
 * @return string			The variable value ('' means unknown)
 */
function ocp_srv($key)
{
	if (isset($_SERVER[$key])) return /*stripslashes*/($_SERVER[$key]);
	if ((isset($_ENV)) && (isset($_ENV[$key]))) return /*stripslashes*/($_ENV[$key]);

	if ($key=='HTTP_HOST')
	{
		return 'localhost';
	}

	if ($key=='SERVER_ADDR')
	{
		return ocp_srv('LOCAL_ADDR');
	}

	return '';
}

/**
 * Find whether an IP address is valid
 *
 * @param  IP				IP address to check.
 * @return boolean		Whether the IP address is valid.
 */
function is_valid_ip($ip)
{
	if ($ip=='') return false;
	$parts=array();
	if (preg_match('#^(\d+)\.(\d+)\.(\d+)\.(\d+)$#',$ip,$parts)!=0)
	{
		if (intval($parts[1])>255) return false;
		if (intval($parts[2])>255) return false;
		if (intval($parts[3])>255) return false;
		if (intval($parts[4])>255) return false;
		return true;
	}
	if (preg_match('#^[\d:a-fA-F]*$#',$ip)!=0)
	{
		return true;
	}
	return false;
}

/**
 * Attempt to get the IP address of the current user
 *
 * @param  integer		The number of groups to include in the IP address (rest will be replaced with *'s). For IP6, this is doubled.
 * @set    1 2 3 4
 * @return IP				The users IP address (blank: could not find a valid one)
 */
function get_ip_address($amount=4)
{
//	return strval(mt_rand(0,255)).'.'.strval(mt_rand(0,255)).'.'.strval(mt_rand(0,255)).'.'.strval(mt_rand(0,255)); // Nice little test for if sessions break
	
	$fw=ocp_srv('HTTP_X_FORWARDED_FOR');
	if (ocp_srv('HTTP_CLIENT_IP')!='') $fw=ocp_srv('HTTP_CLIENT_IP');
	if (($fw!='') && ($fw!='127.0.0.1') && (substr($fw,0,8)!='192.168.') && (substr($fw,0,3)!='10.') && (is_valid_ip($fw)) && ($fw!=ocp_srv('SERVER_ADDR'))) $ip=$fw;
	else $ip=ocp_srv('REMOTE_ADDR');
	
	if (!is_valid_ip($ip)) return '';

	// Bizarro-filter (found "in the wild")
	$pos=strpos($ip,',');
	if ($pos!==false) $ip=substr($ip,0,$pos);

	$ip=preg_replace('#%14$#','',$ip);

	if (strpos($ip,':')!==false)
	{
		if (substr_count($ip,':')<7)
		{
			$ip=str_replace('::',str_repeat(':',(7-substr_count($ip,':'))+2),$ip);
		}
		$parts=explode(':',$ip);
		for ($i=0;$i<$amount*2;$i++)
		{
			$parts[$i]=isset($parts[$i])?str_pad($parts[$i],4,'0',STR_PAD_LEFT):'0000';
		}
		for ($i=$amount*2;$i<8;$i++)
		{
			$parts[$i]='*';
		}
		return implode(':',$parts);
	} else
	{
		$parts=explode('.',$ip);
		for ($i=0;$i<$amount;$i++)
		{
			if (!array_key_exists($i,$parts)) $parts[$i]='0';
		}
		for ($i=$amount;$i<4;$i++)
		{
			$parts[$i]='*';
		}
		return implode('.',$parts);
	}
}

/**
 * Get a string of the users web browser
 *
 * @return string			The web browser string
 */
function get_browser_string()
{
	return ocp_srv('HTTP_USER_AGENT');
}

/**
 * Get the user's operating system
 *
 * @return string			The operating system string
 */
function get_os_string()
{
	if (ocp_srv('HTTP_UA_OS')!='') return ocp_srv('HTTP_UA_OS');
	elseif (ocp_srv('HTTP_USER_AGENT')!='')
	{
		// Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586)
		// We need to get the stuff in the brackets
		$matches=array();
		if (preg_match('#\(([^\)]*)\)#',ocp_srv('HTTP_USER_AGENT'),$matches)!=0)
		{
			$ret=$matches[1];
			$ret=preg_replace('#^compatible; (MSIE[^;]*; )?#','',$ret);
			return $ret;
		}
	}
	return '';
}

/**
 * Find if Cron is installed
 *
 * @return boolean		Whether Cron is installed
 */
function cron_installed()
{
	$last_cron=get_value('last_cron');
	if (is_null($last_cron)) return false;
	return intval($last_cron)>(time()-60*60*5);
}

/**
 * Find the country an IP address long is located in
 *
 * @param  ?IP				The IP to geolocate (NULL: current user's IP)
 * @return ?string		The country initials (NULL: unknown)
 */
function geolocate_ip($ip=NULL)
{
	if (is_null($ip)) $ip=get_ip_address();

	if (!addon_installed('stats')) return NULL;

	$long_ip=ip2long($ip);
	if ($long_ip===false) return NULL; // No IP6 support

	$query='SELECT * FROM '.get_table_prefix().'ip_country WHERE begin_num<='.sprintf('%u',$long_ip).' AND end_num>='.sprintf('%u',$long_ip);
	$results=$GLOBALS['SITE_DB']->query($query);

	if (!array_key_exists(0,$results)) return NULL;
	elseif (!is_null($results[0]['country'])) return $results[0]['country'];
	else return NULL;
}

/**
 * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax.
 *
 * @param  string			The general IP address that is potentially wildcarded
 * @param  IP				The specific IP address we are checking
 * @return boolean		Whether the IP addresses correlate
 */
function compare_ip_address($wild,$full)
{
	$wild_parts=explode((strpos($full,'.')!==false)?'.':':',$wild);
	$full_parts=explode((strpos($full,'.')!==false)?'.':':',$full);
	foreach ($wild_parts as $i=>$wild_part)
	{
		if (($wild_part!='*') && ($wild_part!=$full_parts[$i])) return false;
	}
	return true;
}

/**
 * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax. IP4-only variant
 *
 * @param  string			The general IP address that is potentially wildcarded
 * @param  array			The exploded parts of the specific IP address we are checking
 * @return boolean		Whether the IP addresses correlate
 */
function compare_ip_address_ip4($wild,$full_parts)
{
	$wild_parts=explode('.',$wild);
	foreach ($wild_parts as $i=>$wild_part)
	{
		if (($wild_part!='*') && ($wild_part!=$full_parts[$i])) return false;
	}
	return true;
}

/**
 * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax. IP6-only variant
 *
 * @param  string			The general IP address that is potentially wildcarded
 * @param  array			The exploded parts of the specific IP address we are checking
 * @return boolean		Whether the IP addresses correlate
 */
function compare_ip_address_ip6($wild,$full_parts)
{
	$wild_parts=explode(':',$wild);
	foreach ($wild_parts as $i=>$wild_part)
	{
		if (($wild_part!='*') && ($wild_part!=$full_parts[$i])) return false;
	}
	return true;
}

/**
 * Get the XHTML for the flagrant text message.
 *
 * @return tempcode		The flagrant text message
 */
function get_flagrant()
{
	if (!addon_installed('flagrant')) return new ocp_tempcode();
	
	$system=(mt_rand(0,1)==0);
	$_flagrant=NULL;

	if ((!$system) || (get_option('system_flagrant')==''))
	{
		$_flagrant=persistant_cache_get('FLAGRANT');
		if (is_null($_flagrant))
		{
			$flagrant=$GLOBALS['SITE_DB']->query_value_null_ok_full('SELECT the_message FROM '.get_table_prefix().'text WHERE active_now=1 AND activation_time+days*60*60*24>'.strval(time()),true/*in case tablemissing*/);
			if (is_null($flagrant))
			{
				persistant_cache_set('FLAGRANT',false);
			} else
			{
				$_flagrant=get_translated_tempcode($flagrant);
				persistant_cache_set('FLAGRANT',$_flagrant);
			}
		}
		if ($_flagrant===false) $_flagrant=NULL;
	}
	if (is_null($_flagrant))
	{
		return make_string_tempcode(get_option('system_flagrant'));
	} else
	{
		return do_lang_tempcode('_COMMUNITY_MESSAGE',$_flagrant);
	}
}

/**
 * Log an action
 *
 * @param  ID_TEXT		The type of activity just carried out (a lang string)
 * @param  ?SHORT_TEXT	The most important parameter of the activity (e.g. id) (NULL: none)
 * @param  ?SHORT_TEXT	A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (NULL: none)
 */
function log_it($type,$a=NULL,$b=NULL)
{
	require_code('support2');
	_log_it($type,$a,$b);
}

/**
 * Escape a string to fit within PHP double quotes.
 *
 * @param  string			String in
 * @return string			Resultant string
 */
function php_addslashes($in)
{
	global $PHP_REP_FROM,$PHP_REP_TO;
	return str_replace($PHP_REP_FROM,$PHP_REP_TO,$in);
	//return str_replace("\n",'\n',str_replace('$','\$',str_replace('\\\'','\'',addslashes($in))));
}

/**
 * Remove any duplication inside the list of rows (each row being a map). Duplication is defined by rows with correspinding IDs.
 *
 * @param  array				The rows to remove duplication of
 * @param  string				The ID field
 * @return array				The filtered rows
 */
function remove_duplicate_rows($rows,$id_field='id')
{
	$ids_seen=array();
	$rows2=array();
	foreach ($rows as $row)
	{
		if (!array_key_exists($row[$id_field],$ids_seen))
			$rows2[]=$row;

		$ids_seen[$row[$id_field]]=1;
	}

	return $rows2;
}

/**
 * Update the member tracker for the currently viewing user.
 */
function member_tracking_update()
{
	global $ZONE;
	$page=get_param('page',$ZONE['zone_default_page']);
	$type=get_param('type','/');
	$id=get_param('id','/',true);
	if ($type=='/') $type='';
	if ($id=='/') $id='';

	$GLOBALS['SITE_DB']->query('DELETE FROM '.get_table_prefix().'member_tracking WHERE mt_time<'.strval(time()-60*intval(get_option('users_online_time'))).' OR (mt_member_id='.strval((integer)get_member()).' AND '.db_string_equal_to('mt_type',$type).' AND '.db_string_equal_to('mt_id',$id).' AND '.db_string_equal_to('mt_page',$page).')');

	$GLOBALS['SITE_DB']->query_insert('member_tracking',array(
		'mt_member_id'=>get_member(),
		'mt_cache_username'=>$GLOBALS['FORUM_DRIVER']->get_username(get_member()),
		'mt_time'=>time(),
		'mt_page'=>$page,
		'mt_type'=>$type,
		'mt_id'=>$id
	),false,true); // Ignore errors for race conditions
}

/**
 * Get a map of members viewing the specified ocPortal location.
 *
 * @param  ?ID_TEXT		The page they need to be viewing (NULL: don't care)
 * @param  ?ID_TEXT		The page-type they need to be viewing (NULL: don't care)
 * @param  ?SHORT_TEXT	The type-id they need to be viewing (NULL: don't care)
 * @param  boolean		Whether this has to be done over the forum driver (multi site network)
 * @return ?array			A map of member-ids to rows about them (NULL: Too many)
 */
function get_members_viewing($page=NULL,$type=NULL,$id=NULL,$forum_layer=false)
{
	// Update the member tracking
	member_tracking_update();

	global $ZONE;
	if (is_null($page)) $page=get_param('page',$ZONE['zone_default_page']);
	if (is_null($type)) $type=get_param('type','/');
	if (is_null($id)) $id=get_param('id','/',true);
	if ($type=='/') $type='';
	if ($id=='/') $id='';

	$map=array();
	if ((!is_null($page)) && ($page!='')) $map['mt_page']=$page;
	if ((!is_null($type)) && ($type!='')) $map['mt_type']=$type;
	if ((!is_null($id)) && ($id!='')) $map['mt_id']=$id;
	$map['session_invisible']=0;
	$db=($forum_layer?$GLOBALS['FORUM_DB']:$GLOBALS['SITE_DB']);
	$results=$db->query_select('member_tracking t LEFT JOIN '.$db->get_table_prefix().'sessions s ON t.mt_member_id=s.the_user',array('*'),$map,'ORDER BY mt_member_id',200);
	if (count($results)==200) return NULL;

	$results=remove_duplicate_rows($results,'mt_member_id');

	$out=array();
	foreach ($results as $row)
	{
		if (!member_blocked(get_member(),$row['mt_member_id'])) $out[$row['mt_member_id']]=$row;
	}
	return $out;
}

/**
 * Find whether the current user is invisible.
 *
 * @return boolean		Whether the current user is invisible
 */
function is_invisible()
{
	global $SESSION_CACHE;
	$s=get_session_id();
	foreach ($SESSION_CACHE as $row)
	{
		if (!array_key_exists('the_user',$row)) continue; // Workaround to HipHop PHP weird bug

		if (($row['the_session']==$s) && ($row['session_invisible']==1))
		{
			return true;
		}
	}
	return false;
}

/**
 * Get the number of users on the site in the last 5 minutes. The function also maintains the statistic via the sessions table.
 *
 * @return integer		The number of users on the site
 */
function get_num_users_site()
{
	global $NUM_USERS_SITE,$PEAK_USERS_EVER;
	$users_online_time_seconds=60*intval(get_option('users_online_time'));
	$NUM_USERS_SITE=get_value_newer_than('users_online',time()-$users_online_time_seconds/2);
	if (is_null($NUM_USERS_SITE))
	{
		$count=0;
		get_online_members(false,NULL,$count);
		$NUM_USERS_SITE=strval($count);
		set_value('users_online',$NUM_USERS_SITE);
	}
	if ((intval($NUM_USERS_SITE)>intval(get_option('maximum_users'))) && (intval(get_option('maximum_users'))>1) && (get_page_name()!='login') && (!has_specific_permission(get_member(),'access_overrun_site')))
	{
		$GLOBALS['HTTP_STATUS_CODE']='503';
		header('HTTP/1.0 503 Service Unavailable');

		critical_error('BUSY',do_lang('TOO_MANY_USERS'));
	}
	if (addon_installed('stats'))
	{
		$PEAK_USERS_EVER=get_value_newer_than('user_peak',time()-$users_online_time_seconds*10);
		if ((is_null($PEAK_USERS_EVER)) || ($PEAK_USERS_EVER==''))
		{
			$_peak_users_user=$GLOBALS['SITE_DB']->query_value_null_ok('usersonline_track','MAX(peak)',NULL,'',true);
			$PEAK_USERS_EVER=is_null($_peak_users_user)?$NUM_USERS_SITE:strval($_peak_users_user);
			set_value('user_peak',$PEAK_USERS_EVER);
		}
		if ($NUM_USERS_SITE>$PEAK_USERS_EVER)
		{
			// In case the record is beaten more than once within the same second
			$time=time();
			$GLOBALS['SITE_DB']->query_delete('usersonline_track',array('date_and_time'=>$time),'',1,NULL,true);

			// New record
			$GLOBALS['SITE_DB']->query_insert('usersonline_track',array('date_and_time'=>$time,'peak'=>intval($NUM_USERS_SITE)),false,true);
		}
	}
	return intval($NUM_USERS_SITE);
}

/**
 * Get the largest amount of users ever to be on the site at the same time.
 *
 * @return integer		The number of peak users
 */
function get_num_users_peak()
{
	global $PEAK_USERS_EVER;
	return intval($PEAK_USERS_EVER);
}

/**
 * Get the specified string, but with all characters escaped.
 *
 * @param  mixed			The input string
 * @return string			The escaped string
 */
function escape_html($string)
{
//	if ($string==='') return $string; // Optimisation
	if (is_object($string)) return $string;

	/*if ($GLOBALS['XSS_DETECT'])
	{
		if (ocp_is_escaped($string))
		{
			@var_dump(debug_backtrace());
			@exit('String double-escaped');
		}
	}*/
	
	global $HTML_ESCAPE_1_STRREP,$HTML_ESCAPE_2,$XSS_DETECT;
	
	$ret=str_replace($HTML_ESCAPE_1_STRREP,$HTML_ESCAPE_2,$string);

	if ($XSS_DETECT)
	{
		ocp_mark_as_escaped($ret);
	}
	
	return $ret;
}

/**
 * Find the base URL for documentation.
 *
 * @return URLPATH		The base URL for documentation
 */
function brand_base_url()
{
	$value=get_value('rebrand_base_url');
	if (is_null($value)) $value='http://ocportal.com';
	return $value;
}

/**
 * See's if the current browser matches some special property code. Assumes users are keeping up on newish browsers (except for IE users, who are 6+)
 *
 * @param  string			The property code
 * @set    android ios wysiwyg windows mac linux odd_os mobile opera ie_decent ie_new ie ie_old gecko konqueror safari odd_browser has_artificial_monopoly has_fanboys quirk__internalised_list_indent quirk__quirk__list_indent_in_ul_instead_of_li chrome ie5 ie6 ie7+ ie8+ ie9+
 * @return boolean		Whether there is a match
 */
function browser_matches($code)
{
	global $BROWSER_MATCHES_CACHE;
	if (isset($BROWSER_MATCHES_CACHE[$code])) return $BROWSER_MATCHES_CACHE[$code];
	
	$browser=strtolower(ocp_srv('HTTP_USER_AGENT'));
	$os=strtolower(ocp_srv('HTTP_UA_OS')).' '.$browser;
	$is_opera=strpos($browser,'opera')!==false;
	$is_konqueror=strpos($browser,'konqueror')!==false;
	$is_safari=strpos($browser,'applewebkit')!==false;
	$is_chrome=strpos($browser,'chrome/')!==false;
	$is_gecko=(strpos($browser,'gecko')!==false) && !$is_opera && !$is_konqueror && !$is_safari;
	$is_ie=(strpos($browser,'msie')!==false) && !$is_opera;
	$is_ie_old=((strpos($browser,'msie 6')!==false) || (strpos($browser,'msie 5')!==false)) && ($is_ie);
	$is_ie_decent=(!$is_ie_old) && (strpos($browser,'msie 7')===false) && $is_ie;
	$is_ie5=(strpos($browser,'msie 5')!==false) && ($is_ie);
	$is_ie9=(strpos($browser,'msie 8')===false) && ($is_ie_decent);
	$is_ie_new=(!$is_ie_old) && ($is_ie);
	$is_iceweasel=(strpos($browser,'iceweasel')!==false);

	switch ($code)
	{
		case 'android':
			$BROWSER_MATCHES_CACHE[$code]=strpos($browser,'android')!==false;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ios':
			$BROWSER_MATCHES_CACHE[$code]=strpos($browser,'iphone')!==false;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'true_xhtml':
			$BROWSER_MATCHES_CACHE[$code]=$is_opera || $is_konqueror || $is_safari || $is_gecko;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'wysiwyg':
			if ((get_value('no_wysiwyg')==='1') || (is_mobile()))
			{
				$BROWSER_MATCHES_CACHE[$code]=false;
				return false;
			}
			$BROWSER_MATCHES_CACHE[$code]=true; // As of ocPortal 5.1, using CKEditor
			return $BROWSER_MATCHES_CACHE[$code];
		case 'windows':
			$BROWSER_MATCHES_CACHE[$code]=(strpos($os,'windows')!==false) || (strpos($os,'win32')!==false);
			return $BROWSER_MATCHES_CACHE[$code];
		case 'mac':
			$BROWSER_MATCHES_CACHE[$code]=strpos($os,'mac')!==false;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'linux':
			$BROWSER_MATCHES_CACHE[$code]=strpos($os,'linux')!==false;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'odd_os':
			$BROWSER_MATCHES_CACHE[$code]=(strpos($os,'windows')===false) && (strpos($os,'mac')===false) && (strpos($os,'linux')===false);
			return $BROWSER_MATCHES_CACHE[$code];
		case 'mobile':
			$BROWSER_MATCHES_CACHE[$code]=is_mobile();
			return $BROWSER_MATCHES_CACHE[$code];
		case 'opera':
			$BROWSER_MATCHES_CACHE[$code]=$is_opera;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie5':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie5;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie6':
		case 'ie_old':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie_old;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie7+':
		case 'ie_new':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie_new;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie8+':
		case 'ie_decent':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie_decent;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'ie9+':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie9;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'has_artificial_monopoly':
			$BROWSER_MATCHES_CACHE[$code]=$is_ie;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'has_fanboys':
			$BROWSER_MATCHES_CACHE[$code]=$is_gecko; // change to Chrome?
			return $BROWSER_MATCHES_CACHE[$code];
		case 'no_multi_wysiwyg':
			$BROWSER_MATCHES_CACHE[$code]=false;//$is_gecko; once once needed, but Firefox is much more stable now
			return $BROWSER_MATCHES_CACHE[$code];
		case 'chrome':
			$BROWSER_MATCHES_CACHE[$code]=$is_chrome;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'gecko':
			$BROWSER_MATCHES_CACHE[$code]=$is_gecko;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'konqueror':
			$BROWSER_MATCHES_CACHE[$code]=$is_konqueror;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'safari':
			$BROWSER_MATCHES_CACHE[$code]=$is_safari;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'odd_browser':
			$BROWSER_MATCHES_CACHE[$code]=!$is_opera && !$is_konqueror && !$is_safari && !$is_gecko && !$is_ie;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'quirk__internalised_list_indent':
			$BROWSER_MATCHES_CACHE[$code]=$is_gecko;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'quirk__list_indent_in_ul_instead_of_li':
			$BROWSER_MATCHES_CACHE[$code]=$is_opera;
			return $BROWSER_MATCHES_CACHE[$code];
		case 'itunes':
			$BROWSER_MATCHES_CACHE[$code]=(strpos($browser,'itunes')!==false);
			return $BROWSER_MATCHES_CACHE[$code];
	}

	// Should never get here
	return false;
}

/**
 * Look at the user's browser, and decide if they are viewing on a mobile device or not.
 *
 * @param  ?string		The user agent (NULL: get from environment, current user's browser)
 * @param  boolean		Whether to always tell the truth (even if the current page does not have mobile support)
 * @return boolean		Whether the user is using a mobile device
 */
function is_mobile($user_agent=NULL,$truth=false)
{
	$user_agent_given=($user_agent!==NULL);
	if ($user_agent===NULL) $user_agent=ocp_srv('HTTP_USER_AGENT');
	
	global $IS_MOBILE,$IS_MOBILE_TRUTH;

	if (!$user_agent_given)
	{
		if (($truth?$IS_MOBILE_TRUTH:$IS_MOBILE)!==NULL) return $truth?$IS_MOBILE_TRUTH:$IS_MOBILE;
	}

	if ((!function_exists('get_option')) || (get_option('mobile_support')=='0'))
	{
		$IS_MOBILE=false;
		$IS_MOBILE_TRUTH=false;
		return false;
	}

	if ((isset($GLOBALS['FORUM_DRIVER'])) && (!$truth) && (($theme=$GLOBALS['FORUM_DRIVER']->get_theme())!='default'))
	{
		$ini_path=(($theme=='default')?get_file_base():get_custom_file_base()).'/themes/'.$theme.'/theme.ini';
		if (is_file($ini_path))
		{
			require_code('files');
			$details=better_parse_ini_file($ini_path);
			if ((isset($details['mobile_pages'])) && ($details['mobile_pages']!='') && (preg_match('#(^|,)\s*'.str_replace('#','\#',preg_quote(get_page_name())).'\s*(,|$)#',$details['mobile_pages'])==0))
			{
				$IS_MOBILE=false;
				return false;
			}
		}
	}

	if (!$user_agent_given)
	{
		$val=get_param_integer('keep_mobile',NULL);
		if ($val!==NULL)
		{
			if (isset($GLOBALS['FORUM_DRIVER']))
				$IS_MOBILE=($val==1);
			$IS_MOBILE_TRUTH=$IS_MOBILE;
			return $IS_MOBILE;
		}
	}

	// The set of browsers
	$browsers=array(
						// Implication by technology claims
						'WML',
						'WAP',
						'Wap',
						'MIDP', // Mobile Information Device Profile

						// Generics
						'Mobile',
						'Smartphone',
						'WebTV',

						// Well known/important browsers/brands
						'Minimo', // By Mozilla
						'Fennec', // By Mozilla (being outmoded by minimo)
						'Mobile Safari', // Usually Android
						'lynx',
						'Links',
						'iPhone',
						'iPod',
						'Opera Mobi',
						'Opera Mini',
						'BlackBerry',
						'Windows Phone',
						'Windows CE',
						'Symbian',
						'nook browser', // Barnes and Noble
						'Blazer', // Palm
						'PalmOS',
						'webOS', // Palm
						'SonyEricsson',

						// Games consoles
						'Nintendo',
						'PlayStation Portable',

						// Less well known but common browsers
						'UP.Browser', // OpenWave
						'UP.Link', // OpenWave again?
						'NetFront',
						'Teleca',
						'UCWEB',

						// Specific lamely-identified devices/brands
						'DDIPOCKET',
						'SEMC-Browser',
						'DoCoMo',
						'Xda',
						'ReqwirelessWeb', // Siemens/Samsung

						// Specific services
						'AvantGo',
						);

	$exceptions=array(
						'iPad',
						);

	if (is_file(get_file_base().'/text_custom/pdas.txt'))
	{
		require_code('files');
		$pdas=better_parse_ini_file((get_file_base().'/text_custom/pdas.txt'));
		foreach ($pdas as $key=>$val)
		{
			if ($val==1)
			{
				$browsers[]=$key;
			} else
			{
				$exceptions[]=$key;
			}
		}
	}

	// The test
	$result=(preg_match('/('.implode('|',$browsers).')/i',$user_agent)!=0) && (preg_match('/('.implode('|',$exceptions).')/i',$user_agent)==0);
	if (!$user_agent_given)
	{
		if (isset($GLOBALS['FORUM_DRIVER']))
		{
			$IS_MOBILE=$result;
			$IS_MOBILE_TRUTH=$IS_MOBILE;
		}
	}
	return $result;
}

/**
 * Get the name of a webcrawler bot, or NULL if no bot detected
 *
 * @return ?string			Webcrawling bot name (NULL: not a bot)
 */
function get_bot_type()
{
	global $CACHE_BOT_TYPE;
	if ($CACHE_BOT_TYPE!==false) return $CACHE_BOT_TYPE;
	
	$agent=strtolower(ocp_srv('HTTP_USER_AGENT'));

	global $BOT_MAP;
	if (is_null($BOT_MAP))
	{
		if (is_file(get_file_base().'/text_custom/bots.txt'))
		{
			require_code('files');
			$BOT_MAP=better_parse_ini_file(get_file_base().'/text_custom/bots.txt');
		} else
		{
			$BOT_MAP=array(
				'zyborg'=>'Looksmart',
				'googlebot'=>'Google',
				'teoma'=>'Teoma',
				'scooter'=>'Altavista',
				'jeeves'=>'Ask Jeeves',
				'InfoSeek'=>'Infoseek',
				'Ultraseek'=>'Infoseek',
				'ia_archiver'=>'Alexa/Archive.org',
				'slurp'=>'Inktomi/Yahoo/Hot Bot',
				'Yahoo'=>'Yahoo/Overture',
				'MSNBOT'=>'MSN',
				'ArchitextSpider'=>'Excite',
				'Lycos'=>'Lycos',
				'mercator'=>'Altavista',
				'MantraAgent'=>'LookSmart',
				'wisenutbot'=>'Looksmart',
				'paros'=>'Paros',
				'Sqworm'=>'Aol.com',
			);
		}
	}
	foreach ($BOT_MAP as $id=>$name)
	{
		$name=trim($name);
		if ($name=='') continue;
		if (strpos($agent,strtolower($id))!==false)
		{
			$CACHE_BOT_TYPE=$name;
			return $name;
		}
	}
	if ((strpos($agent,'bot')!==false) || (strpos($agent,'spider')!==false))
	{
		$to_a=strpos($agent,' ');
		if ($to_a===false) $to_a=strlen($agent);
		$to_b=strpos($agent,'/');
		if ($to_b===false) $to_b=strlen($agent);
		$CACHE_BOT_TYPE=substr($agent,0,min($to_a,$to_b));
		return $agent;
	}
	$CACHE_BOT_TYPE=NULL;
	return NULL;
}

/**
 * Read a multi code from a named parameter stub.
 *
 * @param  ID_TEXT	The parameter stub (stub of a series of POST parameters, made by ocf_get_forum_multi_code_field's field or similar).
 * @return SHORT_TEXT The multi code.
 */
function read_multi_code($param)
{
	$type=post_param($param);
	if ($type=='*') return $type;
	if (!array_key_exists($param.'_list',$_POST)) return '';
	$in=implode(',',$_POST[$param.'_list']);
	return $type.$in;
}

/**
 * Turn an array into a humanely readable string.
 *
 * @param  array			Array to convert
 * @return string			A humanely readable version of the array.
 */
function flatten_slashed_array($array)
{
	$ret='';
	foreach ($array as $key=>$val)
	{
		if (is_array($val)) $val=flatten_slashed_array($val);

		if (get_magic_quotes_gpc()) $val=stripslashes($val);

		$ret.='<param>'.(is_integer($key)?strval($key):$key).'='.$val.'</param>'."\n"; // $key may be integer, due to recursion line for list fields, above
	}
	return $ret;
}

/**
 * Get a word-filtered version of the specified text.
 *
 * @param  string			Text to filter
 * @return string			Filtered version of the input text
 */
function wordfilter_text($text)
{
	if (!addon_installed('wordfilter')) return $text;

	require_code('word_filter');
	return check_word_filter($text,NULL,true);
}

/**
 * XML escape the input string.
 *
 * @param  string			Input string
 * @param  integer		Quote style
 * @return string			Escaped version of input string
 */
function xmlentities($string,$quote_style=ENT_COMPAT)
{
	$ret=str_replace('>','&gt;',str_replace('<','&lt;',str_replace('"','&quot;',str_replace('&','&amp;',$string))));
	if (function_exists('ocp_mark_as_escaped')) ocp_mark_as_escaped($ret);
	return $ret;
}

/**
 * Determine whether the user's browser supports cookies or not.
 * Unfortunately this function will only return true once a user has been to the site more than once... ocPortal will set a cookie, and if it perseveres, that indicates cookies work.
 *
 * @return boolean		Whether the user has definitely got cookies
 */
function has_cookies() // Will fail on users first visit, but then will catch on
{
	global $HAS_COOKIES;
	if ($HAS_COOKIES!==NULL) return $HAS_COOKIES;

	/*if (($GLOBALS['DEBUG_MODE']) && (get_param_integer('keep_debug_has_cookies',0)==0) && (!running_script('occle')))	We know this works by now, was tested for years. Causes annoyance when developing
	{
		$_COOKIE=array();
		return false;
	}*/
	
	if (count($_COOKIE)>0)
	{
		$HAS_COOKIES=true;
		return true;
	}
	require_code('users_active_actions');
	ocp_setcookie('has_cookies','1');
	$HAS_COOKIES=false;
	return false;
}

/**
 * Determine whether the user's browser supports JavaScript or not.
 * Unfortunately this function will only return true once a user has been to the site more than once... Javascript will set a cookie, indicating it works.
 *
 * @return boolean		Whether the user has definitely got Javascript
 */
function has_js()
{
	if (get_param_integer('keep_has_js',0)==1) return true;
	if (get_param_integer('keep_has_js',NULL)===0) return false;
	//if (browser_matches('ie5')) return true; // Dual running masks cookies
	if (!function_exists('get_option')) return true;
	if (get_option('detect_javascript',true)==='0') return true;
	return ((array_key_exists('js_on',$_COOKIE)) && ($_COOKIE['js_on']=='1'));
}

/**
 * Get a randomised password.
 *
 * @return string			The randomised password
 */
function get_rand_password()
{
	return substr(uniqid(strval(mt_rand(0,32767)),true),0,10);
}

/**
 * Assign this to explicitly declare that a variable may be of mixed type, and initialise to NULL.
 *
 * @return ?mixed	Of mixed type (NULL: default)
 */
function mixed()
{
	return NULL;
}

/**
 * Get meta information for specified resource
 *
 * @param  ID_TEXT		The type of resource (e.g. download)
 * @param  ID_TEXT		The ID of the resource
 * @return array			The first element is the meta keyword string for the specified resource, and the other is the meta description string.
 */
function seo_meta_get_for($type,$id)
{
	$cache=persistant_cache_get(array('seo',$type,$id));
	if (!is_null($cache)) return $cache;

	$rows=$GLOBALS['SITE_DB']->query_select('seo_meta',array('*'),array('meta_for_type'=>$type,'meta_for_id'=>$id),'',1);
	if (!array_key_exists(0,$rows))
	{
		$cache=array('','');
	} else
	{
		$cache=array(get_translated_text($rows[0]['meta_keywords']),get_translated_text($rows[0]['meta_description']));
	}
	persistant_cache_set(array('seo',$type,$id),$cache);
	return $cache;
}

/**
 * Load the specified resource's meta information into the system for use on this page.
 * Also, if the title is specified then this is used for the page title.
 *
 * @param  ID_TEXT		The type of resource (e.g. download)
 * @param  ID_TEXT		The ID of the resource
 * @param  ?string		The page-specific title to use, in Comcode or plain-text format with possible HTML entities included [Comcode will later be stripped] (NULL: none)
 */
function seo_meta_load_for($type,$id,$title=NULL)
{
	$result=seo_meta_get_for($type,$id);
	global $SEO_KEYWORDS,$SEO_DESCRIPTION,$SEO_TITLE;
	if ($result[0]!='') $SEO_KEYWORDS=array_map('trim',explode(',',$result[0]));
	if ($result[1]!='') $SEO_DESCRIPTION=$result[1];
	if (!is_null($title)) $SEO_TITLE=str_replace('&ndash;','-',str_replace('&copy;','(c)',str_replace('&#039;','\'',$title)));
}

/**
 * Get Tempcode for tags, based on loaded up from SEO keywords (seo_meta_load_for).
 *
 * @param  ?ID_TEXT		The search code for this tag content (e.g. downloads) (NULL: there is none)
 * @param  ?array			Explicitly pass a list of tags instead (NULL: use loaded ones)
 * @return tempcode		Loaded tag output (or blank if there are none)
 */
function get_loaded_tags($limit_to=NULL,$the_tags=NULL)
{
	if (!addon_installed('search')) return new ocp_tempcode();

	if (is_null($the_tags))
	{
		global $SEO_KEYWORDS;
		$the_tags=$SEO_KEYWORDS;
	}

	$tags=array();
	if (!is_null($the_tags))
	{
		$search_limiter_no=array('all_defaults'=>'1');
		if (!is_null($limit_to)) $search_limiter_no['search_'.$limit_to]='1';

		if (!is_null($limit_to))
		{
			$search_limiter_yes=array();
			$search_limiter_yes['search_'.$limit_to]='1';
		} else
		{
			$search_limiter_yes=$search_limiter_no;
		}

		foreach ($the_tags as $tag)
		{
			$tag=trim($tag);
			if ($tag=='') continue;
			
			$tags[]=array(
				'TAG'=>$tag,
				'LINK_LIMITEDSCOPE'=>build_url(array('page'=>'search','type'=>'results','content'=>$tag,'only_search_meta'=>'1')+$search_limiter_yes,get_module_zone('search')),
				'LINK_FULLSCOPE'=>build_url(array('page'=>'search','type'=>'results','content'=>$tag,'only_search_meta'=>'1')+$search_limiter_no,get_module_zone('search')),
			);
		}
	}
	
	return do_template('TAGS',array('TAGS'=>$tags,'TYPE'=>is_null($limit_to)?'':$limit_to));
}

/**
 * Get the default page for a zone.
 *
 * @param  ID_TEXT		Zone name
 * @return ID_TEXT		Default page
 */
function get_zone_default_page($zone_name)
{
	if ($zone_name=='_SELF') $zone_name=get_zone_name();
	
	$p_test=persistant_cache_get(array('ZONE',$zone_name));
	if (!is_null($p_test))
		return $p_test['zone_default_page'];

	global $ZONE;
	if (($ZONE['zone_name']==$zone_name) && (!is_null($ZONE['zone_default_page'])))
	{
		return $ZONE['zone_default_page'];
	} else
	{
		global $ZONE_DEFAULT_PAGES;
		if (!isset($ZONE_DEFAULT_PAGES[$zone_name]))
		{
			$_zone_default_page=NULL;
			if (function_exists('persistant_cache_set'))
			{
				$temp=persistant_cache_get('ALL_ZONES_TITLED');
				if (!is_null($temp))
				{
					$_zone_default_page=array();
					foreach ($temp as $_temp)
					{
						list($_zone_name,,,$zone_default_page)=$_temp;
						$_zone_default_page[]=array('zone_name'=>$_zone_name,'zone_default_page'=>$zone_default_page);
					}
				}
			}
			if (is_null($_zone_default_page))
				$_zone_default_page=$GLOBALS['SITE_DB']->query_select('zones',array('zone_name','zone_default_page'),NULL/*array('zone_name'=>$zone_name)*/,'ORDER BY zone_title',50/*reasonable limit; zone_title is sequential for default zones*/);
			$ZONE_DEFAULT_PAGES[$zone_name]='start';
			$ZONE_DEFAULT_PAGES['collaboration']='start'; // Set this in case collaboration zone removed but still referenced. Performance tweak!
			foreach ($_zone_default_page as $zone_row)
				$ZONE_DEFAULT_PAGES[$zone_row['zone_name']]=$zone_row['zone_default_page'];
		}

		return $ZONE_DEFAULT_PAGES[$zone_name];
	}
}
