<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2012

 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 ****

*/

/*EXTRA FUNCTIONS: fb*/

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

/**
 * Standard code module initialisation function.
 */
function init__database()
{
	if (defined('DB_MAX_KEY_SIZE')) return;

	global $QUERY_LIST,$QUERY_COUNT,$NO_QUERY_LIMIT,$NO_DB_SCOPE_CHECK,$QUERY_FILE_LOG,$SITE_INFO;
	$QUERY_LIST=array();
	$QUERY_COUNT=0;
	$NO_QUERY_LIMIT=false;
	$NO_DB_SCOPE_CHECK=false;
	if (((!isset($SITE_INFO['no_extra_logs'])) || ($SITE_INFO['no_extra_logs']=='0')) && (is_file(get_custom_file_base().'/data_custom/queries.log')))
	{
		$QUERY_FILE_LOG=fopen(get_custom_file_base().'/data_custom/queries.log','at');
	} else
	{
		$QUERY_FILE_LOG=NULL;
	}

	// This will be generalised some-time
	require_code('database/'.get_db_type());
	$GLOBALS['DB_STATIC_OBJECT']=object_factory('Database_Static_'.get_db_type());

	// Create our main database objects
	global $TABLE_LANG_FIELDS;
	$TABLE_LANG_FIELDS=array();
	if ((array_key_exists('db_site',$SITE_INFO)) || (array_key_exists('db_site_user',$SITE_INFO)))
	{
		$GLOBALS['SITE_DB']=new database_driver(get_db_site(),get_db_site_host(),get_db_site_user(),get_db_site_password(),get_table_prefix());
	}

	// Limits (we also limit field names to not conflict with keywords - those defined in database_action.php)
	define('DB_MAX_KEY_SIZE',500);
	define('DB_MAX_PRIMARY_KEY_SIZE',251);
	define('DB_MAX_ROW_SIZE',8000);
	define('DB_MAX_FIELD_IDENTIFIER_SIZE',31);
	define('DB_MAX_IDENTIFIER_SIZE',32);
	// We have to take into account that chars might take 3 bytes - but we'll assume unicode is only used on db's with higher limits
	define('DB_MAX_KEY_SIZE_UNICODE',1000);
	define('DB_MAX_ROW_SIZE_UNICODE',24000);

	global $UPON_QUERY_HOOKS;
	$UPON_QUERY_HOOKS=NULL;
}

/**
 * Called once our DB connection becomes active.
 */
function _general_db_init()
{
	global $TABLE_LANG_FIELDS;
	if (count($TABLE_LANG_FIELDS)>0) return;

	$TABLE_LANG_FIELDS=function_exists('persistent_cache_get')?persistent_cache_get('TABLE_LANG_FIELDS'):NULL;
	if ($TABLE_LANG_FIELDS===NULL)
	{
		$TABLE_LANG_FIELDS=array();

		$_table_lang_fields=$GLOBALS['SITE_DB']->query('SELECT m_name,m_table FROM '.get_table_prefix().'db_meta WHERE '.db_string_equal_to('m_type','SHORT_TRANS').' OR '.db_string_equal_to('m_type','LONG_TRANS').' OR '.db_string_equal_to('m_type','*SHORT_TRANS').' OR '.db_string_equal_to('m_type','*LONG_TRANS').' OR '.db_string_equal_to('m_type','?SHORT_TRANS').' OR '.db_string_equal_to('m_type','?LONG_TRANS'),NULL,NULL,true);
		if ($_table_lang_fields!==NULL)
		{
			foreach ($_table_lang_fields as $lang_field)
			{
				if ($lang_field['m_table']=='f_member_custom_fields') continue;

				if (!isset($TABLE_LANG_FIELDS[$lang_field['m_table']]))
					$TABLE_LANG_FIELDS[$lang_field['m_table']]=array();

				$TABLE_LANG_FIELDS[$lang_field['m_table']][]=$lang_field['m_name'];
			}
		}

		if (function_exists('persistent_cache_set'))
			persistent_cache_set('TABLE_LANG_FIELDS',$TABLE_LANG_FIELDS);
	}
}

/**
 * Find whether the database may run GROUP BY unfettered with restrictions on the SELECT'd fields having to be represented in it or aggregate functions
 *
 * @return boolean		Whether it can
 */
function can_arbitrary_groupby()
{
	if (!method_exists($GLOBALS['DB_STATIC_OBJECT'],'can_arbitrary_groupby')) return false;
	return $GLOBALS['DB_STATIC_OBJECT']->can_arbitrary_groupby();
}

/**
 * Assemble part of a WHERE clause for doing full-text search
 *
 * @param  string			Our match string (assumes "?" has been stripped already)
 * @param  boolean		Whether to do a boolean full text search
 * @return string			Part of a WHERE clause for doing full-text search
 */
function db_full_text_assemble($content,$boolean)
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_full_text_assemble($content,$boolean);
}

/**
 * Get the ID of the first row in an auto-increment table (used whenever we need to reference the first).
 *
 * @return integer			First ID used
 */
function db_get_first_id()
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_get_first_id();
}

/**
 * Encode an SQL statement fragment for a conditional to see if two strings are equal.
 *
 * @param  ID_TEXT		The attribute
 * @param  string			The comparison
 * @return string			The SQL
 */
function db_string_equal_to($attribute,$compare)
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_string_equal_to($attribute,$compare);
}

/**
 * Encode an SQL statement fragment for a conditional to see if two strings are not equal.
 *
 * @param  ID_TEXT		The attribute
 * @param  string			The comparison
 * @return string			The SQL
 */
function db_string_not_equal_to($attribute,$compare)
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_string_not_equal_to($attribute,$compare);
}

/**
 * Encode a LIKE string comparision fragement for the database system. The pattern is a mixture of characters and ? and % wilcard symbols.
 *
 * @param  string			The pattern
 * @return string			The encoded pattern
 */
function db_encode_like($pattern)
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_encode_like($pattern);
}

/**
 * Find whether full-text-search is present
 *
 * @param  array			A DB connection
 * @return boolean		Whether it is
 */
function db_has_full_text($db)
{
	if (count($db)>4) // Okay, we can't be lazy anymore
	{
		$db=call_user_func_array(array($GLOBALS['DB_STATIC_OBJECT'],'db_get_connection'),$db);
		_general_db_init();
	}

	return $GLOBALS['DB_STATIC_OBJECT']->db_has_full_text($db);
}

/**
 * Find whether subquery support is present
 *
 * @param  array			A DB connection
 * @return boolean		Whether it is
 */
function db_has_subqueries($db)
{
	if (!method_exists($GLOBALS['DB_STATIC_OBJECT'],'db_has_subqueries')) return true;

	if (count($db)>4) // Okay, we can't be lazy anymore
	{
		$db=call_user_func_array(array($GLOBALS['DB_STATIC_OBJECT'],'db_get_connection'),$db);
		_general_db_init();
	}

	return $GLOBALS['DB_STATIC_OBJECT']->db_has_subqueries($db);
}

/**
 * Find whether expression ordering support is present
 *
 * @param  array			A DB connection
 * @return boolean		Whether it is
 */
function db_has_expression_ordering($db)
{
	if (count($db)>4) // Okay, we can't be lazy anymore
	{
		$db=call_user_func_array(array($GLOBALS['DB_STATIC_OBJECT'],'db_get_connection'),$db);
		_general_db_init();
	}

	if (!method_exists($GLOBALS['DB_STATIC_OBJECT'],'db_has_expression_ordering')) return false;
	return $GLOBALS['DB_STATIC_OBJECT']->db_has_expression_ordering($db);
}

/**
 * Escape a string so it may be inserted into a query. If SQL statements are being built up and passed using db_query then it is essential that this is used for security reasons. Otherwise, the abstraction layer deals with the situation.
 *
 * @param  string			The string
 * @return string			The escaped string
 */
function db_escape_string($string)
{
	return $GLOBALS['DB_STATIC_OBJECT']->db_escape_string($string);
}

/**
 * Get the type of database installed, such as MySQL, or Oracle.
 *
 * @return string			The database type
 */
function get_db_type()
{
	global $SITE_INFO;
	if (!isset($SITE_INFO['db_type'])) return is_dir(get_custom_file_base().'/uploads/website_specific/'.get_db_site())?'xml':'mysql';
	return $SITE_INFO['db_type'];
}

/**
 * Find ocPortal was installed to use persistent database connections or not.
 *
 * @return boolean			Whether to use persistent database connections
 */
function get_use_persistent()
{
	global $SITE_INFO;
	return array_key_exists('use_persistent',$SITE_INFO)?($SITE_INFO['use_persistent']=='1'):false;
}

/**
 * Get the table prefixes used for all ocPortal tables, commonly used when you are installing ocPortal in the same database as your forums. The default table prefix is 'ocp4_'. Note that anything that might write to an arbitrary db, must ask that db for it's table prefix (if it needs it of course... the db abstracts away most needs for it)
 *
 * @return string			The table prefix
 */
function get_table_prefix()
{
	global $SITE_INFO;
	if (!isset($SITE_INFO['table_prefix'])) return 'ocp'.strval(ocp_version()).'_';
	return $SITE_INFO['table_prefix'];
}

/**
 * Get the host of the database ('localhost', for example).
 *
 * @return string			The database host
 */
function get_db_site_host()
{
	global $SITE_INFO;
	return array_key_exists('db_site_host',$SITE_INFO)?$SITE_INFO['db_site_host']:'localhost';
}

/**
 * Get the name of the database.
 *
 * @return string			The database site
 */
function get_db_site()
{
	global $SITE_INFO;
	if ((!array_key_exists('db_site',$SITE_INFO)) || ($SITE_INFO['db_site']===NULL)) return basename(get_file_base());
	return $SITE_INFO['db_site'].(($GLOBALS['CURRENT_SHARE_USER']===NULL)?'':('_'.$GLOBALS['CURRENT_SHARE_USER']));
}

/**
 * Get the database username.
 *
 * @return string			The database username
 */
function get_db_site_user()
{
	global $SITE_INFO;
	if ($GLOBALS['CURRENT_SHARE_USER']!==NULL) return substr(md5($SITE_INFO['db_forums_user'].'_'.$GLOBALS['CURRENT_SHARE_USER']),0,16);
	return ((array_key_exists('db_site_user',$SITE_INFO)) && ($SITE_INFO['db_site_user']!==NULL))?$SITE_INFO['db_site_user']:'root';
}

/**
 * Get the database password.
 *
 * @return string			The database password
 */
function get_db_site_password()
{
	global $SITE_INFO;
	return array_key_exists('db_site_password',$SITE_INFO)?$SITE_INFO['db_site_password']:'';
}

/**
 * Get the host of the forum database ('localhost', for example).
 *
 * @return string			The database host
 */
function get_db_forums_host()
{
	global $SITE_INFO;
	return array_key_exists('db_forums_host',$SITE_INFO)?$SITE_INFO['db_forums_host']:(array_key_exists('db_site_host',$SITE_INFO)?$SITE_INFO['db_site_host']:'localhost');
}

/**
 * Get the name of the forum database.
 *
 * @return string			The forum database site
 */
function get_db_forums()
{
	global $SITE_INFO;
	if (!array_key_exists('db_forums',$SITE_INFO)) return get_db_site();
	return $SITE_INFO['db_forums'].(($GLOBALS['CURRENT_SHARE_USER']===NULL)?'':('_'.$GLOBALS['CURRENT_SHARE_USER']));
}

/**
 * Get the forum database username.
 *
 * @return string			The forum database username
 */
function get_db_forums_user()
{
	global $SITE_INFO;
	if (!array_key_exists('db_forums_user',$SITE_INFO)) return get_db_site_user();
	if ($GLOBALS['CURRENT_SHARE_USER']!==NULL) return substr(md5($SITE_INFO['db_forums_user'].'_'.$GLOBALS['CURRENT_SHARE_USER']),0,16);
	return $SITE_INFO['db_forums_user'];
}

/**
 * Get the forum database password.
 *
 * @return string			The forum database password
 */
function get_db_forums_password()
{
	global $SITE_INFO;
	if (!array_key_exists('db_forums_password',$SITE_INFO)) return get_db_site_password();
	return $SITE_INFO['db_forums_password'];
}

/**
 * Get the time difference in microseconds between two PHP microtimes.
 * Original source: php.net
 *
 * @param  string			First microtime
 * @param  string			Second microtime
 * @return float			The time difference
 */
function microtime_diff($a,$b)
{
	$x=explode(' ',$a);
	$a_micro=floatval($x[0]);
	$a_int=(float)intval($x[1]);
	$y=explode(' ',$b);
	$b_micro=floatval($y[0]);
	$b_int=(float)intval($y[1]);
	if ($a_int>$b_int)
	{
		return ($a_int-$b_int)+($a_micro-$b_micro);
	}
	elseif ($a_int==$b_int)
	{
		if ($a_micro>$b_micro)
		{
			return ($a_int-$b_int)+($a_micro-$b_micro);
		}
		elseif ($a_micro<$b_micro)
		{
			return ($b_int-$a_int)+($b_micro-$a_micro);
		}
		else
		{
			return 0.0;
		}
	}
	else
	{ // $a_int<$b_int
		return ($b_int-$a_int)+($b_micro-$a_micro);
	}
}

/**
 * Database handling.
 * @package		core
 */
class database_driver
{
	var $table_prefix;
	var $connection_read,$connection_write;

	var $text_lookup_original_cache;
	var $text_lookup_cache;

	var $table_exists_cache;

	var $static_ob;

	var $dedupe_mode=false;

	/**
	 * Construct a database driver from connection parameters.
	 *
	 * @param string			The database name
	 * @param string			The database server
	 * @param string			The connection username
	 * @param string			The connection password
	 * @param string			The table prefix
	 * @param boolean			Whether to on error echo an error and return with a NULL, rather than giving a critical error
	 * @param ?object			Static call object (NULL: use global static call object)
	 */
	function database_driver($db_name,$db_host,$db_user,$db_password,$table_prefix,$fail_ok=false,$static=NULL)
	{
		$this->text_lookup_original_cache=array();
		$this->text_lookup_cache=array();
		$this->table_exists_cache=array();

		$servers=explode(',',$db_host);
		if (count($servers)==1)
		{
			$this->connection_write=array(get_use_persistent(),$db_name,$db_host,$db_user,$db_password,$fail_ok);
			$this->connection_read=$this->connection_write;
		} else
		{
			$this->connection_write=array(get_use_persistent(),$db_name,$servers[0],$db_user,$db_password,$fail_ok);
			$min=(count($servers)==2)?0:1;
			$this->connection_read=array(get_use_persistent(),$db_name,$servers[mt_rand($min,count($servers)-1)],$db_user,$db_password,$fail_ok);
		}
		$this->table_prefix=$table_prefix;

		if ($static!==NULL)
		{
			$this->static_ob=$static;
		} else
		{
			$this->static_ob=$GLOBALS['DB_STATIC_OBJECT'];
		}
	}

	/**
	 * Get the table prefixes used for all ocPortal tables, commonly used when you are installing ocPortal in the same database as your forums. The default table prefix is 'ocp4_'.
	 *
	 * @return string			The table prefix
	 */
	function get_table_prefix()
	{
		return $this->table_prefix;
	}

	/**
	 * Insert a row.
	 *
	 * @param  string			The table name
	 * @param  array			The insertion map
	 * @param  boolean		Whether to return the auto-insert-id
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 * @param  boolean		Whether we are saving as a 'volatile' file extension (used in the XML DB driver, to mark things as being non-syndicated to subversion)
	 * @return integer		The id of the new row
	 */
	function query_insert($table,$map,$ret=false,$fail_ok=false,$save_as_volatile=false)
	{
		if (($table=='cache') && (get_db_type()!='xml') && (get_option('filesystem_caching',true)==='1'))
		{
			global $FILECACHE_OBJECT;
			if ($FILECACHE_OBJECT===NULL) $this->initialise_filesystem_db();
			if ($FILECACHE_OBJECT!=$this)
				return $FILECACHE_OBJECT->query_insert($table,$map,$ret,$fail_ok,$save_as_volatile);
		}

		$keys='';
		$all_values=array();

		$eis=$this->static_ob->db_empty_is_null();

		foreach ($map as $key=>$value)
		{
			if ($keys!='') $keys.=', ';
			$keys.=$key;

			$_value=(!is_array($value))?array($value):$value;

			$v=mixed();
			foreach ($_value as $i=>$v)
			{
				if (!isset($all_values[$i])) $all_values[$i]='';
				$values=$all_values[$i];

				if ($values!='') $values.=', ';

				if ($value===NULL)
				{
					if (($eis) && (is_string($v)) && ($v=='')) $values.='\' \''; else $values.='NULL';
				}
				else
				{
					if (($eis) && (is_string($v)) && ($v==''))
					{
						$v=' ';
					}
					if (is_integer($v)) $values.=strval($v);
					elseif (is_float($v)) $values.=float_to_raw_string($v,10);
					elseif (($key=='begin_num') || ($key=='end_num')) $values.=$v; // Fudge, for all our known large unsigned integers
					else $values.='\''.$this->static_ob->db_escape_string($v).'\'';
				}

				$all_values[$i]=$values; // essentially appends, as $values was loaded from former $all_values[$i] value
			}
		}

		if (count($all_values)==1) // usually $all_values only has length of 1
		{
			if ((in_array($table,array('stats','banner_clicks','member_tracking','usersonline_track','download_logging'))) && (substr(get_db_type(),0,5)=='mysql'))
			{
				$query='INSERT DELAYED INTO '.$this->table_prefix.$table.' ('.$keys.') VALUES ('.$all_values[0].')';
			} else
			{
				$query='INSERT INTO '.$this->table_prefix.$table.' ('.$keys.') VALUES ('.$all_values[0].')';
			}
		} else
		{
			// So we can do batch inserts...
			$all_v='';
			foreach ($all_values as $v)
			{
				if ($all_v!='') $all_v.=', ';
				$all_v.='('.$v.')';
			}

			$query='INSERT INTO '.$this->table_prefix.$table.' ('.$keys.') VALUES '.$all_v;
		}

		return $this->_query($query,NULL,NULL,$fail_ok,$ret,NULL,'',$save_as_volatile);
	}

	/**
	 * Check if a table exists.
	 *
	 * @param  ID_TEXT		The table name
	 * @return boolean		Whether it exists
	 */
	function table_exists($tablename)
	{
		/*
		// Just works with MySQL (too complex to do for all SQL's http://forums.whirlpool.net.au/forum-replies-archive.cfm/523219.html)

		$full_tablename=$this->get_table_prefix().$tablename;

		$rows=$this->query("SHOW TABLES LIKE '".$full_tablename."'");
		foreach ($rows as $row)
			foreach ($row as $field)
				if ($field==$full_tablename) return true;
		return false;
		*/

		if (array_key_exists($tablename,$this->table_exists_cache))
		{
			return $this->table_exists_cache[$tablename];
		}

		$test=$this->query_value_null_ok('db_meta','m_name',array('m_table'=>$tablename));
		$this->table_exists_cache[$tablename]=($test!==NULL);
		return $this->table_exists_cache[$tablename];
	}

	/**
	 * Get the specified value from the database. This is the specified value of the first row returned. A fatal error is produced if there is no matching row.
	 *
	 * @param  string			The table name
	 * @param  string			The field to select
	 * @param  ?array			The WHERE map [will all be AND'd together] (NULL: no where conditions)
	 * @param  string			Something to tack onto the end
	 * @return mixed			The first value of the first row returned
	 */
	function query_value($table,$selected_value,$where_map=NULL,$end='')
	{
		$values=$this->query_select($table,array($selected_value),$where_map,$end,1,NULL);
		if ($values===NULL) return NULL; // error
		if (!array_key_exists(0,$values)) fatal_exit(do_lang_tempcode('QUERY_NULL',escape_html($this->_get_where_expand($this->table_prefix.$table,array($selected_value),$where_map,$end)))); // No result found
		return $this->_query_value($values);
	}

	/**
	 * Extract the first of the first of the list of maps.
	 *
	 * @param  array			The list of maps
	 * @return mixed			The first value of the first row in the list
	 */
	function _query_value($values)
	{
		if (!isset($values[0])) return NULL; // No result found
		$first=$values[0];
		$v=current($first); // Result found. Maybe a value of 'null'
		return $v;
	}

	/**
	 * Get the specified value from the database, or NULL if there is no matching row (or if the value itself is NULL). This is good for detection existence of records, or for use if they might may or may not be present.
	 *
	 * @param  string			The table name
	 * @param  string			The field to select
	 * @param  ?array			The WHERE map [will all be AND'd together] (NULL: no where conditions)
	 * @param  string			Something to tack onto the end
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 * @return ?mixed			The first value of the first row returned (NULL: nothing found, or null value found)
	 */
	function query_value_null_ok($table,$select,$where_map=NULL,$end='',$fail_ok=false)
	{
		$values=$this->query_select($table,array($select),$where_map,$end,1,NULL,$fail_ok);
		if ($values===NULL) return NULL; // error
		return $this->_query_value($values);
	}

	/**
	 * This function is a variant of query_value_null_ok, by the fact that it only accepts a complete (and perfect) SQL query, instead of assembling one itself from the specified parameters.
	 *
	 * @param  string			The complete SQL query
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 * @param  boolean		Whether to skip the query safety check
	 * @return ?mixed			The first value of the first row returned (NULL: nothing found, or null value found)
	 */
	function query_value_null_ok_full($query,$fail_ok=false,$skip_safety_check=false)
	{
		$values=$this->query($query,1,NULL,$fail_ok,$skip_safety_check);
		if ($values===NULL) return NULL; // error
		return $this->_query_value($values);
	}

	/**
	 * Deletes rows from the specified table, that match the specified conditions (if any). It may be limited to a row range (it is likely, only a maximum, of 1, will be used, if any kind of range at all).
	 *
	 * @param  string			The table name
	 * @param  ?array			The WHERE map [will all be AND'd together] (NULL: no conditions)
	 * @param  string			Something to tack onto the end of the statement
	 * @param  ?integer		The maximum number of rows to delete (NULL: no limit)
	 * @param  ?integer		The starting row to delete (NULL: no specific start)
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 */
	function query_delete($table,$where_map=NULL,$end='',$max=NULL,$start=NULL,$fail_ok=false)
	{
		if (($table=='cache') && (get_db_type()!='xml') && (get_option('filesystem_caching',true)==='1'))
		{
			global $FILECACHE_OBJECT;
			if ($FILECACHE_OBJECT===NULL) $this->initialise_filesystem_db();
			if ($FILECACHE_OBJECT!=$this)
			{
				$FILECACHE_OBJECT->query_delete($table,$where_map,$end,$max,$start,$fail_ok);
				return;
			}
		}

		if ($where_map===NULL)
		{
			$this->_query('DELETE FROM '.$this->table_prefix.$table.' '.$end,$max,$start,$fail_ok);
			return;
		}

		$where='';

		foreach ($where_map as $key=>$value)
		{
			if ($where!='') $where.=' AND ';

			if (is_float($value)) $where.=$key.'='.float_to_raw_string($value,10);
			elseif (is_integer($value)) $where.=$key.'='.strval($value);
			elseif (($key=='begin_num') || ($key=='end_num')) $where.=$key.'='.$value; // Fudge, for all our known large unsigned integers
			else
			{
				if ($value===NULL) $where.=$key.' IS NULL';
				else
				{
					if ((is_string($value)) && ($value=='') && ($this->static_ob->db_empty_is_null())) $where.=$key.' IS NULL'; //$value=' ';
					else $where.=db_string_equal_to($key,$value);
				}
			}
		}

		$query='DELETE FROM '.$this->table_prefix.$table.' WHERE ('.$where.') '.$end;
		$this->_query($query,$max,$start,$fail_ok);
	}

	/**
	 * Update (edit) a row in the database.
	 *
	 * @param  string			The table name
	 * @param  array			The UPDATE map
	 * @param  ?array			The WHERE map [will all be AND'd together] (NULL: no conditions)
	 * @param  string			Something to tack onto the end of the statement
	 * @param  ?integer		The maximum number of rows to update (NULL: no limit)
	 * @param  ?integer		The starting row to update (NULL: no specific start)
	 * @param  boolean		Whether to get the number of touched rows. WARNING: Do not use in core ocPortal code as it does not work on all database drivers
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 * @return ?integer		The number of touched records (NULL: hasn't been asked / error)
	 */
	function query_update($table,$update_map,$where_map=NULL,$end='',$max=NULL,$start=NULL,$num_touched=false,$fail_ok=false)
	{
		$where='';
		$update='';

		$value=mixed();

		if ($where_map!==NULL)
		{
			foreach ($where_map as $key=>$value)
			{
				if ($where!='') $where.=' AND ';

				if (is_float($value)) $where.=$key.'='.float_to_raw_string($value,10);
				elseif (is_integer($value)) $where.=$key.'='.strval($value);
				elseif (($key=='begin_num') || ($key=='end_num')) $where.=$key.'='.$value; // Fudge, for all our known large unsigned integers
				else
				{
					if ($value===NULL) $where.=$key.' IS NULL';
					else
					{
						if ((is_string($value)) && ($value=='') && ($this->static_ob->db_empty_is_null())) /*$where.=$key.' IS NULL';*/  $value=' ';
						/*else */$where.=db_string_equal_to($key,$value);
					}
				}
			}
		}

		foreach ($update_map as $key=>$value)
		{
			if (($value===STRING_MAGIC_NULL) || ($value===INTEGER_MAGIC_NULL)) continue;
			if ($update!='') $update.=', ';

			if ($value===NULL) $update.=$key.'=NULL';
			else
			{
				if (is_float($value)) $update.=$key.'='.float_to_raw_string($value,10);
				elseif (is_integer($value)) $update.=$key.'='.strval($value);
				elseif (($key=='begin_num') || ($key=='end_num')) $where.=$key.'='.$value; // Fudge, for all our known large unsigned integers
				else $update.=$key.'=\''.$this->static_ob->db_escape_string($value).'\'';
			}
		}
		if ($update=='') return NULL;

		if ($where=='')
		{
			return $this->_query('UPDATE '.$this->table_prefix.$table.' SET '.$update.' '.$end,$max,$start,$fail_ok,$num_touched);
		} else
		{
			return $this->_query('UPDATE '.$this->table_prefix.$table.' SET '.$update.' WHERE ('.$where.') '.$end,$max,$start,$fail_ok,$num_touched);
		}
	}

	/**
	 * Create a SELECT query from some abstract data.
	 *
	 * @param  string			The table to select from
	 * @param  ?array			List of field selections (NULL: all fields)
	 * @param  ?array			Map of conditions to enforce (NULL: no conditions)
	 * @param  string			Additional stuff to tack onto the query
	 * @return string			SQL query
	 */
	function _get_where_expand($table,$select_map=NULL,$where_map=NULL,$end='')
	{
		if ($select_map===NULL) $select_map=array('*');

		$select='';
		foreach ($select_map as $key)
		{
			if (!is_string($key)) $key=strval($key);

			if ($select!='') $select.=',';

			$select.=$key;
		}

		$where='';
		if (($where_map!==NULL) && ($where_map!=array()))
		{
			foreach ($where_map as $key=>$value)
			{
				if (!is_string($key)) fatal_exit('Parameters to the database API given in the wrong order. Please check the function call.');

				if ($where!='') $where.=' AND ';

				if (is_float($value)) $where.=$key.'='.float_to_raw_string($value,10);
				elseif (is_integer($value)) $where.=$key.'='.strval($value);
				elseif (($key=='begin_num') || ($key=='end_num')) $where.=$key.'='.$value; // Fudge, for all our known large unsigned integers
				else
				{
					if ($value===NULL) $where.=$key.' IS NULL';
					else
					{
						if (($value==='') && ($this->static_ob->db_empty_is_null())) /*$where.=$key.' IS NULL';*/ $value=' ';
						/*else */

						if ($key=='text_original') $table=str_replace(' LEFT JOIN '.$this->get_table_prefix().'translate ',' JOIN '.$this->get_table_prefix().'translate ',$table);

						$where.=db_string_equal_to($key,$value);
					}
				}
			}

			return 'SELECT '.$select.' FROM '.$table.' WHERE ('.$where.') '.$end;
		}
		return 'SELECT '.$select.' FROM '.$table.' '.$end;
	}

	/**
	 * Initialise a filesystem DB that we can use for caching.
	 */
	function initialise_filesystem_db()
	{
		global $FILECACHE_OBJECT;
		require_code('database/xml');
		$chain_db=new database_driver(get_custom_file_base().'/persistent_cache','','','',get_table_prefix(),false,object_factory('Database_Static_xml'));
		$chain_connection=&$chain_db->connection_write;
		if (count($chain_connection)>4) // Okay, we can't be lazy anymore
		{
			$chain_connection=call_user_func_array(array($chain_db->static_ob,'db_get_connection'),$chain_connection);
			_general_db_init();
		}
		$FILECACHE_OBJECT=$chain_db;
	}

	/**
	 * Get the database rows found matching the specified parameters. Unlike 'query', it doesn't take raw SQL -- it assembles SQL based the parameters requested.
	 *
	 * @param  string			The table name
	 * @param  ?array			The SELECT map (NULL: all fields)
	 * @param  ?array			The WHERE map [will all be AND'd together] (NULL: no conditions)
	 * @param  string			Something to tack onto the end of the SQL query
	 * @param  ?integer		The maximum number of rows to select (NULL: get all)
	 * @param  ?integer		The starting row to select (NULL: start at first)
	 * @param  boolean		Whether to allow failure (outputting a message instead of exiting completely)
	 * @param  ?array			Extra language fields to join in for cache-prefilling. You only need to send this if you are doing a JOIN and carefully craft your query so table field names won't conflict (NULL: none)
	 * @return array			The results
	 */
	function query_select($table,$select=NULL,$where_map=NULL,$end='',$max=NULL,$start=NULL,$fail_ok=false,$lang_fields=NULL)
	{
		if (($table=='cache') && (get_db_type()!='xml') && (get_option('filesystem_caching',true)==='1'))
		{
			global $FILECACHE_OBJECT;
			if ($FILECACHE_OBJECT===NULL) $this->initialise_filesystem_db();
			if ($FILECACHE_OBJECT!=$this)
				return $FILECACHE_OBJECT->query_select($table,$select,$where_map,$end,$max,$start,$fail_ok,$lang_fields);
		}

		$full_table=$this->table_prefix.$table;

		$field_prefix='';

		if ($select===NULL) $select=array('*');

		// Optimisation for entirely automatic translate table linkage (only done on non-joins, as this removes a whole lot of potential complexities -- if people are doing joins they go a little further to do this manually anyway; also we make sure we're operating on our site's table prefix so we don't collect meta info for the wrong table set)
		if ($lang_fields===NULL)
		{
			if (($table!='translate') && (strpos($table,' ')===false) && (isset($GLOBALS['SITE_DB'])) && ($this->table_prefix==$GLOBALS['SITE_DB']->table_prefix))
			{
				global $TABLE_LANG_FIELDS;
				$lang_fields_provisional=isset($TABLE_LANG_FIELDS[$table])?$TABLE_LANG_FIELDS[$table]:array();
				$lang_fields=array();

				if ($lang_fields_provisional!=array())
				{
					$full_table.=' main';
					foreach ($select as $i=>$s)
					{
						if (!is_string($s))
						{
							$lang_fields_provisional=array();
							break; // Bad API call, but we'll let it fail naturally
						}

						if (preg_match('#^[A-Za-z\_\*]+$#',$s)!=0)
							$select[$i]='main.'.$s;
					}
					if ($where_map!==NULL)
					{
						foreach ($where_map as $i=>$s)
						{
							if (!is_string($i))
							{
								$lang_fields_provisional=array();
								break; // Bad API call, but we'll let it fail naturally
							}

							if (preg_match('#^[A-Za-z\_]+$#',$i)!=0)
							{
								unset($where_map[$i]);
								$where_map['main.'.$i]=$s;
							}
						}
					}
					if ($end!='')
					{
						$end=preg_replace('#(^|,|\s)([a-z]+)($|,|\s)#','${1}main.${2}${3}',$end);
					}

					$field_prefix='main.';

					foreach ($lang_fields_provisional as $lang_field)
					{
						if ((in_array($field_prefix.$lang_field,$select)) || (in_array($field_prefix.'*',$select)))
						{
							$lang_fields[]=$lang_field;
						}
					}
				}
			}
		}

		return $this->_query($this->_get_where_expand($full_table,$select,$where_map,$end),$max,$start,$fail_ok,false,$lang_fields,$field_prefix);
	}

	/**
	 * This function is a raw query executor. It shouldn't usually be used unless you need to write SQL involving 'OR' statements or other complexities. There are abstracted versions available which you probably want instead (mainly, query_select).
	 *
	 * @param  string			The complete SQL query
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to output an error on failure
	 * @param  boolean		Whether to skip the query safety check
	 * @param  ?array			Extra language fields to join in for cache-prefilling. You only need to send this if you are doing a JOIN and carefully craft your query so table field names won't conflict (NULL: none)
	 * @param  string			All the core fields have a prefix of this on them, so when we fiddle with language lookup we need to use this (only consider this if you're setting $lang_fields)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function query($query,$max=NULL,$start=NULL,$fail_ok=false,$skip_safety_check=false,$lang_fields=NULL,$field_prefix='')
	{
		if (!$skip_safety_check)
		{
			$_query=strtolower($query);
			$queries=1;//substr_count($_query,'insert into ')+substr_count($_query,'replace into ')+substr_count($_query,'update ')+substr_count($_query,'select ')+substr_count($_query,'delete from '); Not reliable
			if ((strpos(preg_replace('#\'[^\']*\'#','\'\'',str_replace('\\\'','',$_query)),' union ')!==false) || ($queries>1)) log_hack_attack_and_exit('SQL_INJECTION_HACK',$query);
		}

		return $this->_query($query,$max,$start,$fail_ok,false,$lang_fields,$field_prefix);
	}

	/**
	 * This function is a very basic query executor. It shouldn't usually be used by you, as there are specialised abstracted versions available.
	 *
	 * @param  string			The complete SQL query
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to output an error on failure
	 * @param  boolean		Whether to get an insert ID
	 * @param  ?array			Extra language fields to join in for cache-prefilling. You only need to send this if you are doing a JOIN and carefully craft your query so table field names won't conflict (NULL: none)
	 * @param  string			All the core fields have a prefix of this on them, so when we fiddle with language lookup we need to use this (only consider this if you're setting $lang_fields)
	 * @param  boolean		Whether we are saving as a 'volatile' file extension (used in the XML DB driver, to mark things as being non-syndicated to subversion)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _query($query,$max=NULL,$start=NULL,$fail_ok=false,$get_insert_id=false,$lang_fields=NULL,$field_prefix='',$save_as_volatile=false)
	{
		global $QUERY_COUNT,$NO_QUERY_LIMIT,$QUERY_LOG,$QUERY_LIST,$DEV_MODE,$IN_MINIKERNEL_VERSION,$QUERY_FILE_LOG,$UPON_QUERY_HOOKS;

		if ($QUERY_FILE_LOG!==NULL)
		{
			fwrite($QUERY_FILE_LOG,$query.';'.chr(10).chr(10));
		}

		if ($DEV_MODE)
		{
			if ((get_forum_type()!='none') && (strpos($query,get_table_prefix().'f_')!==false) && (strpos($query,get_table_prefix().'f_')<100) && (strpos($query,'f_welcome_emails')===false) && ($this->connection_write===$GLOBALS['SITE_DB']->connection_write) && (is_ocf_satellite_site()) && (!$GLOBALS['NO_DB_SCOPE_CHECK']))
			{
				/*file_put_contents(get_file_base().'/uploads/downloads/test.txt',var_export(debug_backtrace(),true));
				@exit($query);
				@debug_print_backtrace();*/
				fatal_exit('Using OCF queries on the wrong driver');
			}
		}

		if (!$NO_QUERY_LIMIT)
		{
			$QUERY_COUNT++;
			//@exit('!');
			//if ($QUERY_COUNT>10) @ob_end_clean();@print('Query: '.$query.chr(10));
		}
		static $fb=NULL;
		if ($fb===NULL) $fb=function_exists('fb');
		if (($fb) && (!headers_sent()) && (get_param_integer('keep_firephp_queries',0)==1) && (function_exists('fb')))
		{
			fb('Query: '.$query);
		}
		if (($QUERY_COUNT==68) && (get_param_integer('keep_no_query_limit',0)==0) && (count($_POST)==0) && (get_page_name()!='admin_importer') && ($IN_MINIKERNEL_VERSION==0) && (get_param('special_page_type','')!='query'))
		{
			$NO_QUERY_LIMIT=true;
			$log_path=get_custom_file_base().'/data_custom/big_query_screens.log';
			if (is_writable_wrap($log_path))
			{
				$myfile=fopen($log_path,'at');
				fwrite($myfile,get_self_url_easy().chr(10));
				fclose($myfile);
			}
			if ($DEV_MODE)
			{
				$QUERY_COUNT=0;
				fatal_exit(do_lang_tempcode('TOO_MANY_QUERIES'));
			}
		}

		$lang_strings_expecting=array();
		if ((isset($lang_fields[0])) && (function_exists('user_lang')))
		{
			$lang=user_lang(); // We can we assume this, as we will cache against it -- if subsequently code wants something else it'd be a cache miss which is fine

			foreach ($lang_fields as $i=>$field)
			{
				$_i=strval($i);

				$join=' LEFT JOIN '.$this->table_prefix.'translate t'.$_i.' ON t'.$_i.'.id='.$field_prefix.$field.' AND '.db_string_equal_to('t'.$_i.'.language',$lang);
				$_query=strtoupper($query);
				$from_pos=strpos($_query,' FROM ');
				$where_pos=strpos($_query,' WHERE ');
				if ($where_pos===false)
				{
					$_where_pos=0;
					do
					{
						$_where_pos=strpos($_query,' GROUP BY ',$_where_pos+1);
						if ($_where_pos!==false) $where_pos=$_where_pos;
					}
					while ($_where_pos!==false);
				}
				if ($where_pos===false)
				{
					$_where_pos=0;
					do
					{
						$_where_pos=strpos($_query,' ORDER BY ',$_where_pos+1);
						if ($_where_pos!==false) $where_pos=$_where_pos;
					}
					while ($_where_pos!==false);
				}
				if ($where_pos!==false)
				{
					$query=substr($query,0,$where_pos).$join.substr($query,$where_pos);
				} else
				{
					$query.=$join;
				}
				$original='t'.$_i.'.text_original AS t'.$_i.'__text_original';
				$parsed='t'.$_i.'.text_parsed AS t'.$_i.'__text_parsed';
				$query=substr($query,0,$from_pos).','.$original.','.$parsed.substr($query,$from_pos);
				$lang_strings_expecting[]=array($field,'t'.$_i.'__text_original','t'.$_i.'__text_parsed');
			}
		}

		if ($start<0) $start=0;
		if ($max<0) $max=1;

		if ($QUERY_LOG)
		{
			$before=microtime(false);
		}
		if ((substr(strtoupper($query),0,7)=='SELECT ') || (substr(strtoupper($query),0,8)=='(SELECT '))
		{
			$connection=&$this->connection_read;
		} else
		{
			$connection=&$this->connection_write;
		}
		if (isset($connection[4])) // Okay, we can't be lazy anymore
		{
			$connection=call_user_func_array(array($this->static_ob,'db_get_connection'),$connection);
			_general_db_init();
		}

		// Special handling for searches, which are slow and specific - we want to recognise if previous active searches were the same and kill them (as this would have been a double form submit)
		if (($this->dedupe_mode) && (substr(get_db_type(),0,5)=='mysql'))
		{
			$query.='/* '.strval(get_session_id()).' */'; // Identify query to session, for accurate de-duping

			$real_query=$query;
			if (($max!==NULL) && ($start!==NULL)) $real_query.=' LIMIT '.strval($start).','.strval($max);
			elseif ($max!==NULL) $real_query.=' LIMIT '.strval($max);
			elseif ($start!==NULL) $real_query.=' LIMIT '.strval($start).',30000000';

			$ret=$this->static_ob->db_query('SHOW FULL PROCESSLIST',$connection);
			foreach ($ret as $process)
			{
				if ($process['Info']==$real_query)
				{
					$this->static_ob->db_query('KILL '.strval($process['Id']),$connection,NULL,NULL,true);
				}
			}
		}

		$ret=$this->static_ob->db_query($query,$connection,$max,$start,$fail_ok,$get_insert_id,false,$save_as_volatile);
		if ($QUERY_LOG)
		{
			$after=microtime(false);
			$text=(!is_null($max))?$query.' ('.strval((integer)$start).'-'.strval((integer)$start+$max).')':$query;
			$out=array('time'=>microtime_diff($after,$before),'text'=>$text);
			$QUERY_LIST[]=$out;
		}

		// Run hooks, if any exist
		if ($UPON_QUERY_HOOKS===NULL)
		{
			if (!function_exists('find_all_hooks')) return $ret;

			$UPON_QUERY_HOOKS=array();
			if (!running_script('restore'))
			{
				$hooks=find_all_hooks('systems','upon_query');
				foreach (array_keys($hooks) as $hook)
				{
					require_code('hooks/systems/upon_query/'.filter_naughty($hook));
					$UPON_QUERY_HOOKS[$hook]=object_factory('upon_query_'.filter_naughty($hook),true);
				}
			}
		}
		foreach ($UPON_QUERY_HOOKS as $ob)
		{
			if ($ob!==NULL)
				$ob->run($this,$query,$max,$start,$fail_ok,$get_insert_id,$ret);
		}

		// Copy results to lang cache, but only if not null AND unset to avoid any confusion
		if ($ret!==NULL)
		{
			$cnt_orig=count($this->text_lookup_original_cache);
			$cnt_parsed=count($this->text_lookup_cache);
			foreach ($lang_strings_expecting as $bits)
			{
				list($field,$original,$parsed)=$bits;

				foreach ($ret as $i=>$row)
				{
					$entry=$row[$field];

					if (($row[$original]!==NULL) && ($cnt_orig<=1000))
					{
						$this->text_lookup_original_cache[$entry]=$row[$original];
						$cnt_orig++;
					}
					if (($row[$parsed]!==NULL) && ($cnt_parsed<=1000))
					{
						$this->text_lookup_cache[$entry]=$row[$parsed];
						$cnt_parsed++;
					}

					unset($ret[$i][$original]);
					unset($ret[$i][$parsed]);
				}
			}
		}

		return $ret;
	}

	/**
	 * Create a table with the given name and the given array of field name to type mappings.
	 * If a field type starts '*', then it is part of that field's key. If it starts '?', then it is an optional field.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  array			The fields
	 * @param  boolean		Whether to skip the size check for the table (only do this for addon modules that don't need to support anything other than mySQL)
	 * @param  boolean		Whether to skip the check for NULL string fields
	 */
	function create_table($table_name,$fields,$skip_size_check=false,$skip_null_check=false)
	{
		require_code('database_helper');
		_helper_create_table($this,$table_name,$fields,$skip_size_check,$skip_null_check);
	}

	/**
	 * Add an index to a table without disturbing the contents, after the table has been created.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The index name
	 * @param  array			The fields
	 * @param  ID_TEXT		The name of the unique key field for the table
	 */
	function create_index($table_name,$index_name,$fields,$unique_key_field='id')
	{
		require_code('database_helper');
		_helper_create_index($this,$table_name,$index_name,$fields,$unique_key_field);
	}

	/**
	 * Delete an index from a table.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The index name
	 */
	function delete_index_if_exists($table_name,$index_name)
	{
		require_code('database_helper');
		_helper_delete_index_if_exists($this,$table_name,$index_name);
	}

	/**
	 * Drop the given table, or if it doesn't exist, silently return.
	 *
	 * @param  ID_TEXT		The table name
	 */
	function drop_if_exists($table)
	{
		require_code('database_helper');
		_helper_drop_if_exists($this,$table);
	}

	/**
	 * Rename the given table.
	 *
	 * @param  ID_TEXT		The old table name
	 * @param  ID_TEXT		The new table name
	 */
	function rename_table($old,$new)
	{
		require_code('database_helper');
		_helper_rename_table($this,$old,$new);
	}

	/**
	 * Adds a field to an existing table.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The field name
	 * @param  ID_TEXT		The field type
	 * @param  ?mixed			The default value (NULL: no default)
	 */
	function add_table_field($table_name,$name,$_type,$default=NULL)
	{
		require_code('database_helper');
		_helper_add_table_field($this,$table_name,$name,$_type,$default);
	}

	/**
	 * Change the type of a DB field in a table. Note: this function does not support ascession/decession of translatability
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The field name
	 * @param  ID_TEXT		The new field type
	 * @param  ?ID_TEXT		The new field name (NULL: leave name)
	 */
	function alter_table_field($table_name,$name,$_type,$new_name=NULL)
	{
		require_code('database_helper');
		_helper_alter_table_field($this,$table_name,$name,$_type,$new_name);
	}

	/**
	 * Change the primary key of a table.
	 *
	 * @param  ID_TEXT		The name of the table to create the index on
	 * @param  array			A list of fields to put in the new key
	 */
	function change_primary_key($table_name,$new_key)
	{
		require_code('database_helper');
		_helper_change_primary_key($this,$table_name,$new_key);
	}

	/**
	 * If a text field has picked up Comcode support, we will need to run this.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The field name
	 * @param  ID_TEXT		The tables key field name
	 * @param  integer		The translation level to use
	 * @set    1 2 3 4
	 * @param  boolean		Whether our data is already stored in Tempcode assembly format
	 */
	function promote_text_field_to_comcode($table_name,$name,$key='id',$level=2,$in_assembly=false)
	{
		require_code('database_helper');
		_helper_promote_text_field_to_comcode($this,$table_name,$name,$key,$level,$in_assembly);
	}

	/**
	 * Delete the specified field from the specified table.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  ID_TEXT		The field name
	 */
	function delete_table_field($table_name,$name)
	{
		require_code('database_helper');
		_helper_delete_table_field($this,$table_name,$name);
	}

	/**
	 * If we've changed what $type is stored as, this function will need to be called to change the typing in the DB.
	 *
	 * @param  ID_TEXT		The field type
	 */
	function refresh_field_definition($type)
	{
		require_code('database_helper');
		_helper_refresh_field_definition($this,$type);
	}

	/**
	 * Find if a table is locked for more than 5 seconds. Only works with MySQL.
	 *
	 * @param  ID_TEXT		The table name
	 * @return boolean		Whether the table is locked
	 */
	function table_is_locked($table)
	{
		if (in_array($table,array('stats','banner_clicks','member_tracking','usersonline_track','download_logging')))
			return false; // Actually, we have delayed insert for these so locking is not an issue


		if (substr(get_db_type(),0,5)!='mysql') return false;

		$tries=0;
		do
		{
			if (substr($table,0,2)!='f_')
			{
				$db_name=get_db_site();
				$db=$GLOBALS['SITE_DB'];
			} else
			{
				$db_name=get_db_forums();
				$db=$GLOBALS['FORUM_DB'];
			}
			$locks=$db->query('SHOW OPEN TABLES FROM `'.$db_name.'` WHERE `Table`=\''.$db->get_table_prefix().$table.'\' AND In_use>=1',NULL,NULL,true);
			if (is_null($locks)) return false; // MySQL version older than 5.0 (e.g. 4.1.x)
			$locked=count($locks)>=1;
			$tries++;
			if ($locked)
			{
				sleep(1);
			}
		}
		while (($locked) && ($tries<5));
		return $locked;
	}

}


