<?php
/**
* @package Mambo Open Source
* @subpackage Database
* @copyright (C) 2005 - 2006 Mambo Foundation Inc.
* @license http://www.gnu.org/copyleft/gpl.html GNU/GPL
*
* Mambo was originally developed by Miro (www.miro.com.au) in 2000. Miro assigned the copyright in Mambo to The Mambo Foundation in 2005 to ensure
* that Mambo remained free Open Source software owned and managed by the community.
* Mambo is Free Software
*/ 

/**
* Database connector class
*/
class database {
	/** @var string Internal variable to hold the query sql */
	var $_sql='';
	/** @var int Internal variable to hold the database error number */
	var $_errorNum=0;
	/** @var string Internal variable to hold the database error message */
	var $_errorMsg='';
	/** @var string Internal variable to hold the prefix used on all database tables */
	var $_table_prefix='';
	/** @var Internal variable to hold the connector resource */
	var $_resource='';
	/** @var Internal variable to hold the last query cursor */
	var $_cursor=null;
	/** @var boolean Debug option */
	var $_debug=0;
	/** @var array A log of queries */
	var $_log=array();
	/** @var string Null date */
	var $_null_date='0000-00-00 00:00:00';

	/**
	* Database object constructor
	* @param string Database host
	* @param string Database user name
	* @param string Database user password
	* @param string Database name
	* @param string Common prefix for all tables
	*/
	function database( $host='localhost', $user, $pass, $db, $table_prefix ) {
		// perform a number of fatality checks, then die gracefully
		if (!function_exists( 'mysql_connect' )) $this->forceOffline(1);
		if (!($this->_resource = @mysql_connect( $host, $user, $pass ))) $this->forceOffline(2);
		if (!mysql_select_db($db)) $this->forceOffline(3);
		$this->_table_prefix = $table_prefix;
	}

	function forceOffline ($error_number) {
			$mosSystemError = $error_number;
			$basePath = dirname( __FILE__ );
			include $basePath . '/../configuration.php';
			include $basePath . '/../offline.php';
			exit();
	}
	
	function getNullDate () {
	    return $this->_null_date;
	}
	/**
	* @param int
	*/
	function debug( $level ) {
	    $this->_debug = intval( $level );
	}

	function debug_trace () {
		trigger_error( $this->_errorNum, E_USER_NOTICE );
		//echo "<pre>" . $this->_sql . "</pre>\n";
		if (function_exists('debug_backtrace')) {
			foreach(debug_backtrace() as $back) {
			    if (@$back['file']) {
				    echo '<br />'.$back['file'].':'.$back['line'];
				}
			}
		}
	}
	/**
	* @return int The error number for the most recent query
	*/
	function getErrorNum() {
		return $this->_errorNum;
	}
	/**
	* @return string The error message for the most recent query
	*/
	function getErrorMsg() {
		return str_replace( array( "\n", "'" ), array( '\n', "\'" ), $this->_errorMsg );
	}
	/**
	* Get a database escaped string
	* @return string
	*/
	function getEscaped( $text ) {
		return mysql_escape_string( $text );
	}
	/**
	* Get a quoted database escaped string
	* @return string
	*/
	function Quote( $text ) {
		return '\'' . mysql_escape_string( $text ) . '\'';
	}
	/**
	* Sets the SQL query string for later execution.
	*
	* @param string The SQL query
	*/
	function setBareQuery ($sql) {
		$this->_sql = $sql;
	}
	/**
	* Sets the SQL query string for later execution.
	*
	* This function replaces a string identifier <var>$prefix</var> with the
	* string held is the <var>_table_prefix</var> class variable.
	*
	* @param string The SQL query
	* @param string The common table prefix
	*/
	function setQuery( $sql, $prefix='#__' ) {
		$this->setBareQuery ($this->replacePrefix($sql, $prefix));
//      This is maintenance code for catching particular SQL statements
//		if (strpos($this->_sql,'SELECT menutype') === 0) debug_print_backtrace();
	}

	/**
	 * This function replaces a string identifier <var>$prefix</var> with the
	 * string held is the <var>_table_prefix</var> class variable.
	 *
	 * @param string The SQL query
	 * @param string The common table prefix
	 * @author thede, David McKinnis
	 */
	function replacePrefix ($sql, $prefix='#__') {
		$done = '';
		while (strlen($sql)) {
			$single = preg_match("/\'([^\\\']|\\.)*'/", $sql,$matches_single,PREG_OFFSET_CAPTURE);
			if ($double = preg_match('/\"([^\\\"]|\\.)*"/', $sql,$matches_double,PREG_OFFSET_CAPTURE) OR $single) {
				if ($single == 0 OR ($double AND $matches_double[0][1] < $matches_single[0][1])) {
					$done .= str_replace($prefix, $this->_table_prefix, substr($sql,0,$matches_double[0][1])).$matches_double[0][0];
					$sql = substr($sql,$matches_double[0][1]+strlen($matches_double[0][0]));
				}
				else {
					$done .= str_replace($prefix, $this->_table_prefix, substr($sql,0,$matches_single[0][1])).$matches_single[0][0];
					$sql = substr($sql,$matches_single[0][1]+strlen($matches_single[0][0]));
				}
			}
			else return $done.str_replace($prefix, $this->_table_prefix,$sql);
		}
		return $done;
	}
	/**
	* @return string The current value of the internal SQL vairable
	*/
	function getQuery($sql='') {
		if ($sql == '') $sql = $this->_sql;
		return "<pre>" . htmlspecialchars( $sql ) . "</pre>";
	}
	/**
	* Execute the query
	* @return mixed A database resource if successful, FALSE if not.
	*/
	function query($sql = '') {
		global $mosConfig_debug;
		if ($sql == '') $sql = $this->_sql;
		if ($this->_debug) $this->_log[] = $sql;
		if ($this->_cursor = mysql_query($sql, $this->_resource)) {
			$this->_errorNum = 0;
			$this->_errorMsg = '';
			return $this->_cursor;
		}
		else {
			$this->_errorNum = mysql_errno( $this->_resource );
			$this->_errorMsg = mysql_error( $this->_resource )." SQL=$sql";
			if ($this->_debug) $this->debug_trace();
			return false;
		}
	}

	function query_batch( $abort_on_error=true, $p_transaction_safe = false) {
		$this->_errorNum = 0;
		$this->_errorMsg = '';
		if ($p_transaction_safe) {
			$si = mysql_get_server_info();
			preg_match_all( "/(\d+)\.(\d+)\.(\d+)/i", $si, $m );
			$prefix = '';
			if ($m[1] >= 4) $prefix = 'START TRANSACTION; ';
			elseif ($m[2] >= 23) {
				if ($m[3] >= 19) $prefix = 'BEGIN WORK; ';
				elseif ($m[3] >= 17) $prefix = 'BEGIN; ';
			}
			if ($prefix) $this->_sql = $prefix.$this->_sql.'; COMMIT;';
		}
		$query_split = preg_split ("/[;]+/", $this->_sql);
		$error = 0;
		foreach ($query_split as $command_line) {
			$command_line = trim( $command_line );
			if ($command_line != '') {
				if (!$this->query($command_line)) {
					$error = 1; echo 'xxx ';
					if ($abort_on_error) {
						return $this->_cursor;
					}
				}
			}
		}
		return $error ? false : true;
	}

	/**
	* Diagnostic function
	*/
	function explain() {
		if (!($cur = $this->query("EXPLAIN ".$this->_sql))) return null;
		$headline = $header = $body = '';
		$buf = '<table cellspacing="1" cellpadding="2" border="0" bgcolor="#000000" align="center">';
		$buf .= $this->getQuery("EXPLAIN ".$this->_sql);
		while ($row = mysql_fetch_assoc($cur)) {
			$body .= "<tr>";
			foreach ($row as $k=>$v) {
				if ($headline == '') $header .= "<th bgcolor=\"#ffffff\">$k</th>";
				$body .= "<td bgcolor=\"#ffffff\">$v</td>";
			}
			$headline = $header;
			$body .= "</tr>";
		}
		$buf .= "<tr>$headline</tr>$body</table><br />&nbsp;";
		mysql_free_result( $cur );
		return "<div style=\"background-color:#FFFFCC\" align=\"left\">$buf</div>";
	}
	/**
	* @return int The number of rows returned from the most recent query - SELECT only
	*/
	function getNumRows( $cur=null ) {
		return mysql_num_rows( $cur ? $cur : $this->_cursor );
	}

	/**
	* @return int The number of rows affected by the most recent query - INSERT, UPDATE, DELETE
	*/
	function getAffectedRows(  ) {
		return mysql_affected_rows( $this->_resource );
	}

	/**
	* Load an array of retrieved database objects or values
	* @param int Database cursor
	* @param string The field name of a primary key
	* @return array If <var>key</var> is empty as sequential list of returned records.
	* If <var>key</var> is not empty then the returned array is indexed by the value
	* the database key.  Returns <var>null</var> if the query fails.
	*/
	function &retrieveResults ($key='', $max=0, $result_type='row') {
		$results = array();
		$sql_function = 'mysql_fetch_'.$result_type;
		if ($cur = $this->query()) {
			while ($row = $sql_function($cur)) {
				if ($key != '') $results[$row->$key] = $row;
				else $results[] = $row;
				if ($max AND count($results) >= $max) break;
			}
			mysql_free_result($cur);
		}
		return $results;
	}
	/**
	* This method loads the first field of the first row returned by the query.
	*
	* @return The value returned in the query or null if the query failed.
	*/
	function loadResult() {
		$results =& $this->retrieveResults('', 1, 'row');
		if (count($results)) return $results[0][0];
		else return null;
	}

	/**
	* Load an array of single field results into an array
	*/
	function loadResultArray($numinarray = 0) {
		$results =& $this->retrieveResults('', 0, 'row');
		$values = array();
		foreach ($results as $result) $values[] = $result[$numinarray];
		if (count($values)) return $values;
		else return null;
	}
	/**
	* Load a assoc list of database rows
	* @param string The field name of a primary key
	* @return array If <var>key</var> is empty as sequential list of returned records.
	*/
	function loadAssocList( $key='' ) {
		$results =& $this->retrieveResults($key, 0, 'assoc');
		if (count($results)) return $results[0][0];
		else return null;
	}
	/**
	* Copy the named array content into the object as properties
	* only existing properties of object are filled. when undefined in hash, properties wont be deleted
	* @param array the input array
	* @param obj byref the object to fill of any class
	* @param string
	* @param boolean
	*/
	function mosBindArrayToObject( $array, &$obj, $ignore='', $prefix=NULL, $checkSlashes=true ) {
		if (!is_array($array) OR !is_object($obj)) return false;
		if ($prefix == null) $prefix = '';
		foreach (get_object_vars($obj) as $k => $v) {
			if( substr( $k, 0, 1 ) != '_' AND strpos($ignore, $k) === false) {
				if (isset($array[$prefix.$k])) {
					$obj->$k = ($checkSlashes AND get_magic_quotes_gpc()) ? $this->mosStripslashes( $array[$prefix.$k] ) : $array[$prefix.$k];
				}
			}
		}
		return true;
	}

	/**
	* Strip slashes from strings or arrays of strings
	* @param value the input string or array
	*/
	function mosStripslashes(&$value) {
	    if (is_string($value)) $ret = stripslashes($value);
		else {
		    if (is_array($value)) {
		        $ret = array();
		        while (list($key,$val) = each($value)) {
		            $ret[$key] = $this->mosStripslashes($val);
		        } // while
		    } else $ret = $value;
		} // if
	    return $ret;
	} // mosStripSlashes

	/**
	* This global function loads the first row of a query into an object
	*
	* If an object is passed to this function, the returned row is bound to the existing elements of <var>object</var>.
	* If <var>object</var> has a value of null, then all of the returned query fields returned in the object.
	* @param string The SQL query
	* @param object The address of variable
	*/
	function loadObject( &$object ) {
		if ($object != null) {
			$results =& $this->retrieveResults('', 1, 'assoc');
			if (count($results)) {
				$this->mosBindArrayToObject($results[0], $object, null, null, false);
				return true;
			}
		}
		else {
			$results =& $this->retrieveResults('', 1, 'object');
			if (count($results)) {
				$object = $results[0];
				return true;
			}
			else $object = null;
		}
		return false;
	}
	/**
	* Load a list of database objects
	* @param string The field name of a primary key
	* @return array If <var>key</var> is empty as sequential list of returned records.
	* If <var>key</var> is not empty then the returned array is indexed by the value
	* the database key.  Returns <var>null</var> if the query fails.
	*/
	function loadObjectList( $key='' ) {
		$results =& $this->retrieveResults($key, 0, 'object');
		if (count($results)) return $results;
		else return null;
	}
	/**
	* @return The first row of the query.
	*/
	function loadRow() {
		$results =& $this->retrieveResults('', 1, 'row');
		if (count($results)) return $results[0];
		else return null;
	}
	/**
	* Load a list of database rows (numeric column indexing)
	* @param string The field name of a primary key
	* @return array If <var>key</var> is empty as sequential list of returned records.
	* If <var>key</var> is not empty then the returned array is indexed by the value
	* the database key.  Returns <var>null</var> if the query fails.
	*/
	function loadRowList( $key='' ) {
		$results =& $this->retrieveResults('', 0, 'row');
		if (count($results)) return $results;
		else return null;
	}

	/**
	* Document::db_insertObject()
	*
	* { Description }
	*
	* @param [type] $keyName
	* @param [type] $verbose
	*/
	function insertObject( $table, &$object, $keyName = NULL, $verbose=false ) {
		$fmtsql = "INSERT INTO $table ( %s ) VALUES ( %s ) ";
		$fmtsql = $this->replacePrefix($fmtsql);
		$fields = array();
		foreach (get_object_vars( $object ) as $k => $v) {
			if (is_array($v) OR is_object($v) OR $v === NULL OR $k[0] == '_') continue;
			$fields[] = "`$k`";
			$values[] = "'" . $this->getEscaped( $v ) . "'";
		}
		if (!isset($fields)) die ('class database method insertObject - no fields');
		$this->setBareQuery( sprintf( $fmtsql, implode( ",", $fields ), implode( ",", $values ) ) );
		($verbose) && print "$sql<br />\n";
		if (!$this->query()) return false;
		$id = mysql_insert_id();
		($verbose) && print "id=[$id]<br />\n";
		if ($keyName && $id) $object->$keyName = $id;
		return true;
	}

	/**
	* Document::db_updateObject()
	*
	* { Description }
	*
	* @param [type] $updateNulls
	*/
	function updateObject( $table, &$object, $keyName, $updateNulls=true ) {
		$fmtsql = "UPDATE $table SET %s WHERE %s";
		$fmtsql = $this->replacePrefix($fmtsql);
		$tmp = array();
		foreach (get_object_vars( $object ) as $k => $v) {
			if (is_array($v) OR is_object($v) OR $k[0] == '_' OR ($v === null AND !$updateNulls)) continue;
			if( $k == $keyName ) { // PK not to be updated
				$where = "$keyName='" . $this->getEscaped( $v ) . "'";
				continue;
			}
			if ($v) $v = $this->getEscaped($v);
			$tmp[] = "`$k`='$v'";
		}
		if (!isset($tmp)) return true;
		if (!isset($where)) die ('database class updateObject method - no key value');
		$this->setBareQuery( sprintf( $fmtsql, implode( ",", $tmp ) , $where ) );
		return $this->query();
	}

	/**
	* @param boolean If TRUE, displays the last SQL statement sent to the database
	* @return string A standised error message
	*/
	function stderr( $showSQL = false ) {
		return "DB function failed with error number $this->_errorNum"
		."<br /><font color=\"red\">$this->_errorMsg</font>"
		.($showSQL ? "<br />SQL = <pre>$this->_sql</pre>" : '');
	}

	function insertid()
	{
		return mysql_insert_id();
	}

	function getVersion()
	{
		return mysql_get_server_info();
	}

	/**
	* Fudge method for ADOdb compatibility
	*/
	function GenID( $foo1=null, $foo2=null ) {
		return '0';
	}
	/**
	* @return array A list of all the tables in the database
	*/
	function getTableList() {
		$this->setQuery( 'SHOW tables' );
		$this->query();
		return $this->loadResultArray();
	}
	/**
	* @param array A list of table names
	* @return array A list the create SQL for the tables
	*/
	function getTableCreate( $tables ) {
		$result = array();

		foreach ($tables as $tblval) {
			$this->setQuery( 'SHOW CREATE table ' . $tblval );
			$this->query();
			$result[$tblval] = $this->loadResultArray( 1 );
		}

		return $result;
	}
	/**
	* @param array A list of table names
	* @return array An array of fields by table
	*/
	function getTableFields( $tables ) {
		$result = array();

		foreach ($tables as $tblval) {
			$this->setQuery( 'SHOW FIELDS FROM ' . $tblval );
			$this->query();
			$fields = $this->loadObjectList();
			foreach ($fields as $field) {
				$result[$tblval][$field->Field] = preg_replace("/[(0-9)]/",'', $field->Type );
			}
		}

		return $result;
	}

	function displayLogged () {
		echo count($this->_log).' queries executed';
		echo '<pre>';
	 	foreach ($this->_log as $k=>$sql) {
	 	    echo $k+1 . "\n" . $sql . '<hr />';
		}
	}

	/* Helper method - maybe should go into database itself */
	function doSQL ($sql) {
		$this->setQuery($sql);
		if (!$this->query()) {
			echo "<script> alert('".$this->getErrorMsg()."'); window.history.go(-1); </script>\n";
			exit();
		}
	}

	/* Helper method - maybe could go into database itself */
	function &doSQLget ($sql, $classname) {
		$this->setQuery($sql);
		$rows = $this->loadObjectList();
		$target = get_class_vars($classname);
		if ($rows) {
			foreach ($rows as $row) {
				$next = new $classname(0);
				foreach ($target as $field=>$value) {
					if (isset($row->$field)) $next->$field = $row->$field;
				}
				$result[] = $next;
			}
		}
		else $result = array();
		return $result;
	}


}

class mamboDatabase extends database {

	function mamboDatabase () {
		$host = mamboCore::get('mosConfig_host');
		$user = mamboCore::get('mosConfig_user');
		$pw = mamboCore::get('mosConfig_password');
		$db = mamboCore::get('mosConfig_db');
		$prefix = mamboCore::get('mosConfig_dbprefix');
		parent::database($host, $user, $pw, $db, $prefix);
	}

	function &getInstance () {
		static $instance;
		if (!is_object($instance)) $instance = new mamboDatabase();
		return $instance;
	}
}

/**
* mosDBAbstractRow Abstract Class.
* @abstract
* @package Mambo
* @subpackage Database
*
* Parent classes to all database derived objects.  Customisation will generally
* not involve tampering with this object.
* @package Mambo
* @author Martin Brampton counterpoint@mambo-foundation.org
*/
class mosDBAbstractRow {
	/** @var string Name of the table in the db schema relating to child class */
	var $_tbl = '';
	/** @var string Name of the primary key field in the table */
	var $_tbl_key = '';
	/** @var string Error message */
	var $_error = '';

	/**
	*	Object constructor to set table and key field
	*
	*	Can be overloaded/supplemented by the child class
	*	@param string $table name of the table in the db schema relating to child class
	*	@param string $key name of the primary key field in the table
	*/
	function mosDBAbstractRow ($table='', $keyname='id', $db='') {
		if ($table) $this->_tbl = $table;
		else $this->_tbl = $this->tableName();
		$this->_tbl_key = $keyname;
		if (is_object($db)) $this->_db = $db;
	}

	/**
	*	generic check method
	*
	*	can be overloaded/supplemented by the child class
	*	@return boolean True if the object is ok
	*/
	function check() {
		return true;
	}

	/**
	* Checks if this object lacks the property given by the parameter
	* @param string The name of the property
	* @return bool
	*/
	function lacks( $property ) {
		$thisclass = get_class($this);
		if (array_key_exists( $property, get_class_vars($thisclass) )) return false;
		$this->_error = T_(sprintf('WARNING: %s does not support %s.', $thisclass, $property));
		return true;
	}

	/**
	/* Move a database row object up or down through the ordering
	/* @param int positive to move up, negative to move down
	/* @param string Additional conditions on the WHERE clause to limit the effect
	*/
	function move( $direction, $where='' ) {
		$compops = array (-1 => '<', 0 => '=', 1 => '>');
		$relation = $compops[($direction>0)-($direction<0)];
		$ordering = ($relation == '<' ? 'DESC' : 'ASC');
		$k = $this->_tbl_key;
		$o1 = $this->ordering;
		$k1 = $this->$k;
		$database = isset($this->_db) ? $this->_db : mamboDatabase::getInstance();
		$sql = "SELECT $k, ordering FROM $this->_tbl WHERE ordering $relation $o1";
		$sql .= ($where ? "\n AND $where" : '').' ORDER BY ordering '.$ordering.' LIMIT 1';
		$database->setQuery( $sql );
		if ($database->loadObject($row)) {
			$o2 = $row->ordering;
			$k2 = $row->$k;
			$sql = "UPDATE $this->_tbl SET ordering = (ordering=$o1)*$o2 + (ordering=$o2)*$o1 WHERE $k = $k1 OR $k = $k2";
			$database->doSQL($sql);
		}
	}
	/**
	* Compacts the ordering sequence of the selected records
	* @param string Additional conditions on WHERE clause to limit ordering to a particular subset of records
	*/
	function updateOrder( $where='', $cfid=null, $order=null ) {
		if ($this->lacks('ordering')) return false;
		$k = $this->_tbl_key;
		if ($this->_tbl == "#__content_frontpage") $order2 = ", content_id DESC";
		else $order2 = "";

		$database = isset($this->_db) ? $this->_db : mamboDatabase::getInstance();
		
		if (!is_null($cfid) AND !is_null($order)) {
			foreach ($cfid as $i=>$id) {
				$o = intval($order[$i]);
				$set[] = "(id=$id)*$o";
			}
			$sql = "UPDATE $this->_tbl SET ordering = ".implode(' + ', $set).' WHERE id IN ('.implode(',', $cfid).')';
			$database->doSQL($sql);
		}
		
		$sql = "SELECT $k, ordering FROM $this->_tbl "
		. ($where ? "\nWHERE $where" : '')
		. "\nORDER BY ordering$order2";
		$database->setQuery($sql);
		if (!$rows = $database->loadObjectList()) {
			$this->_error = $database->getErrorMsg();
			return false;
		}
		$i = 1;
		foreach ($rows as $row) {
			$sql = "UPDATE $this->_tbl SET ordering=$i WHERE $k = ".$row->$k;
			$database->doSQL($sql);
			$i++;
		}
		return true;
	}

}
	

/**
* mosDBTable Abstract Class.
* @abstract
* @package Mambo
* @subpackage Database
*
* Parent classes to all database derived objects.  Customisation will generally
* not involve tampering with this object.
* @package Mambo
* @author Andrew Eddie <eddieajau@users.sourceforge.net
*/
class mosDBTable extends mosDBAbstractRow {
	/** @var mosDatabase Database connector */
	var $_db = null;

	/**
	 *	@return bool True if DB query failed.  Sets the error message
	 */
	function queryTestFailure () {
		if ($this->_db->query()) return false;
		$this->_error = $this->_db->getErrorMsg();
		return true;
	}
	/**
	 * Filters public properties
	 * @access protected
	 * @param array List of fields to ignore
	 */
	function filter( $ignoreList=null ) {
		$callcheck = array('InputFilter', 'process');
		if (!is_callable($callcheck)) require_once(mamboCore::get('mosConfig_absolute_path').'/includes/phpInputFilter/class.inputfilter.php');
		// specific filters
		$iFilter =& new InputFilter();
		if (is_array($ignoreList)) foreach ($this->getPublicProperties() as $k) {
			if (!in_array($k, $ignoreList)) $this->$k = $iFilter->process($this->$k);
		}
		else foreach ($this->getPublicProperties() as $k) $this->$k = $iFilter->process($this->$k);
	}
	/**
	 *	@return string Returns the error message
	 */
	function getError() {
		return $this->_error;
	}
	/**
	* Gets the value of the class variable
	* @param string The name of the class variable
	* @return mixed The value of the class var (or null if no var of that name exists)
	*/
	function get( $_property ) {
		if(isset( $this->$_property )) return $this->$_property;
		else return null;
	}
	/**
	 * Returns an array of public properties
	 * @return array
	 */
	function getPublicProperties() {
		static $cache = null;
		if (is_null( $cache )) {
			$cache = array();
			foreach (get_class_vars( get_class( $this ) ) as $key=>$val) {
				if (substr( $key, 0, 1 ) != '_') {
					$cache[] = $key;
				}
			}
		}
		return $cache;
	}
	/**
	* Set the value of the class variable
	* @param string The name of the class variable
	* @param mixed The value to assign to the variable
	*/
	function set( $_property, $_value ) {
		$this->$_property = $_value;
	}
	/**
	*	binds a named array/hash to this object
	*
	*	can be overloaded/supplemented by the child class
	*	@param array $hash named array
	*	@return null|string	null is operation was satisfactory, otherwise returns an error
	*/
	function bind( $array, $ignore="" ) {
		$database =& mamboDatabase::getInstance();
		if (is_array($array)) return $database->mosBindArrayToObject($array, $this, $ignore);
		$this->_error = strtolower(get_class( $this ))."::bind failed.";
		return false;
	}

	/**
	*	binds an array/hash to this object
	*	@param int $oid optional argument, if not specifed then the value of current key is used
	*	@return any result from the database operation
	*/
	function load( $oid=null ) {
		$k = $this->_tbl_key;
		if ($oid !== null) $this->$k = $oid;
		if ($this->$k === null) return false;
		$this->_db->setQuery("SELECT * FROM $this->_tbl WHERE $this->_tbl_key='".$this->$k."'" );
		return $this->_db->loadObject($this);
	}

	/**
	* Inserts a new row if id is zero or updates an existing row in the database table
	*
	* Can be overloaded/supplemented by the child class
	* @param boolean If false, null object variables are not updated
	* @return null|string null if successful otherwise returns and error message
	*/
	function store( $updateNulls=false ) {
		$k = $this->_tbl_key;
		global $migrate;
		if( $this->$k && !$migrate) $ret = $this->_db->updateObject( $this->_tbl, $this, $this->_tbl_key, $updateNulls );
		else $ret = $this->_db->insertObject( $this->_tbl, $this, $this->_tbl_key );
		if( !$ret ) {
			$this->_error = strtolower(get_class( $this ))."::store failed <br />" . $this->_db->getErrorMsg();
			return false;
		} else return true;
	}

	/**
	*	Default delete method
	*
	*	can be overloaded/supplemented by the child class
	*	@return true if successful otherwise returns and error message
	*/
	function delete( $oid=null ) {
		$k = $this->_tbl_key;
		if ($oid) $this->$k = intval( $oid );
		$this->_db->setQuery( "DELETE FROM $this->_tbl WHERE $this->_tbl_key = '".$this->$k."'" );
		if ($this->queryTestFailure()) return false;
		return true;
	}

	function checkout( $who, $oid=null ) {
		if ($this->lacks('checked_out')) return false;
		$k = $this->_tbl_key;
		if ($oid !== null) $this->$k = $oid;
		$time = date( "%Y-%m-%d H:i:s" );
		if (intval( $who )) {
			// new way of storing editor, by id
			$this->_db->setQuery( "UPDATE $this->_tbl"
			. "\nSET checked_out='$who', checked_out_time='$time'"
			. "\nWHERE $this->_tbl_key='".$this->$k."'"
			);
		} else {
			// old way of storing editor, by name
			$this->_db->setQuery( "UPDATE $this->_tbl"
			. "\nSET checked_out='1', checked_out_time='$time', editor='".$who."' "
			. "\nWHERE $this->_tbl_key='".$this->$k."'"
			);
		}
		return $this->_db->query();
	}

	function checkin( $oid=null ) {
		if ($this->lacks('checked_out')) return false;
		$k = $this->_tbl_key;
		if ($oid !== null) $this->$k = $oid;
		$time = date("H:i:s");
		$this->_db->setQuery( "UPDATE $this->_tbl"
		. "\nSET checked_out='0', checked_out_time='0000-00-00 00:00:00'"
		. "\nWHERE $this->_tbl_key='".$this->$k."'"
		);
		return $this->_db->query();
	}

	function hit( $oid=null ) {
		$k = $this->_tbl_key;
		if ($oid !== null) $this->$k = intval( $oid );
		$key = $this->$k;
		$this->_db->setQuery( "UPDATE $this->_tbl SET hits=(hits+1) WHERE $this->_tbl_key='$key'" );
		$this->_db->query();

		if (mamboCore::get('mosConfig_enable_log_items')) {
			$now = date( "Y-m-d" );
			$this->_db->setQuery( "SELECT hits"
			. "\nFROM #__core_log_items"
			. "\nWHERE time_stamp='$now' AND item_table='$this->_tbl' AND item_id='$key'"
			);
			$hits = intval( $this->_db->loadResult() );
			if ($hits) $this->_db->setQuery( "UPDATE #__core_log_items SET hits=(hits+1)"
				. "\nWHERE time_stamp='$now' AND item_table='$this->_tbl' AND item_id='".$this->$k."'"
				);
			else $this->_db->setQuery( "INSERT INTO #__core_log_items VALUES"
				. "\n('$now','$this->_tbl','".$this->$k."','1')"
				);
			$this->_db->query();
		}
	}

	/**
	* Generic save function
	* @param array Source array for binding to class vars
	* @param string Filter for the order updating
	* @returns TRUE if completely successful, FALSE if partially or not succesful.
	*/
	function save( $source, $order_filter ) {
		if (!$this->bind($_POST) OR !$this->check() OR !$this->store()OR !$this->checkin()) return false;
		$filter_value = $this->$order_filter;
		$this->updateOrder( $order_filter ? "`$order_filter`='$filter_value'" : "" );
		$this->_error = '';
		return true;
	}

	/**
	* Generic Publish/Unpublish function
	* @param array An array of id numbers
	* @param integer 0 if unpublishing, 1 if publishing
	* @param integer The id of the user performnig the operation
	*/
	function publish_array( $cid=null, $publish=1, $myid=0 ) {
		if (!is_array( $cid ) OR count( $cid ) < 1) {
			$this->_error = "No items selected.";
			return false;
		}
		$cids = implode( ',', $cid );
		$this->_db->setQuery( "UPDATE $this->_tbl SET published='$publish'"
		. "\nWHERE $this->_tbl_key IN ($cids) AND (checked_out=0 OR checked_out='$myid')"
		);
		if ($this->queryTestFailure()) return false;
		if (count( $cid ) == 1) $this->checkin( $cid[0] );
		return true;
	}

	/**
	* Export item list to xml
	* @param boolean Map foreign keys to text values
	*/
	function toXML( $mapKeysToText=false ) {
		$xml = '<record table="' . $this->_tbl . '"';
		if ($mapKeysToText) $xml .= ' mapkeystotext="true"';
		$xml .= '>';
		foreach (get_object_vars($this) as $k => $v) {
			if ($v === null OR is_array($v) OR is_object($v)) continue;
			if ($k[0] == '_') continue; // internal field
			$xml .= '<' . $k . '><![CDATA[' . $v . ']]></' . $k . '>';
		}
		$xml .= '</record>';
		return $xml;
	}
}

/**
* Abstract class for classes where the objects of the class can be relatively easily
*  stored in a single database table.  Can usually be adapted to more complex cases.
* Requires child classes to implement: tableName(), notSQL().
* tableName() must return the name of the database table, using #__ in the usual Mambo way
* notSQL() must return an array of strings, where each string is the name of a
*  variable that is NOT in the database table, or is not written explicitly,
*  e.g. the auto-increment key.  If this is the ONLY non-SQL field, then the
*  child class need not implement it, as that it is already in the abstract class.
* Child classes may implement timeStampField, in which case it must return the name
*  of a field that will have a timestamp placed in it whenever the DB is written.
*/

class mosTableEntry extends mosDBAbstractRow {

	/* Stores all POST data where the name matches an object variable name */
	function addPostData () {
		foreach (get_class_vars(get_class($this)) as $field=>$value) {
			if ($field!='id' AND $field[1] != '_' AND isset($_POST[$field])) {
				$this->$field = trim($_POST[$field]);
			}
		}
		$this->forceBools();
  	}

	/* Provided in case child class does not implement it.  Can force any values */
	/* within some limited range.  In particular, can force bools to be 0 or 1 */
	function forceBools () {
		return;
	}

	/* Updates an existing DB entry with the object's current values */
	function updateObjectDB () {
		$this->prepareValues();
		$database = mamboDatabase::getInstance();
		$database->doSQL($this->updateSQL());
	}

	/* Deletes the current object from the DB */
	function delete () {
		$table = $this->tableName();
		$sql = "DELETE FROM $table WHERE id=$this->id";
		$database = mamboDatabase::getInstance();
		$database->doSQL($sql);
	}

	/* Provided in case the child class does not provide a method for timeStampField */
	function timeStampField () {
		return '';
	}

	/* Provides SQL for updating the DB with the contents of the current object */
	function updateSQL () {
		$tabname = $this->tableName();
		$sql = "UPDATE $tabname SET %s WHERE id=$this->id";
		$exclude = $this->notSQL();
		foreach (get_class_vars(get_class($this)) as $field=>$value) {
			if (!in_array($field,$exclude) AND $field[0] != '_') $setter[] = $field."='".$this->$field."'";
		}
		$timestamp = $this->timeStampField();
		if ($timestamp) $setter[] = $timestamp."='".date('Y-m-d H:i:s')."'";
		return sprintf($sql,implode(',', $setter));
	}

	/* Default method for identifying fields not to be written to the DB */
	/* The child classes may override this and return more items in the array */
	function notSQL () {
		return array ('id');
	}

	/* Provides SQL to insert the current object into the DB */
	function insertSQL () {
		$tabname = $this->tableName();
		$sql = "INSERT INTO $tabname (%s) VALUES (%s)";
		$exclude = $this->notSQL();
		foreach (get_class_vars(get_class($this)) as $field=>$value) {
			if (!in_array($field,$exclude) AND $field[0] != '_') {
				$infields[] = $field;
				$values[] = "'".$this->$field."'";
			}
		}
		$timestamp = $this->timeStampField();
		if ($timestamp) {
			$infields[] = $timestamp;
			$values[] = "'".date('Y-m-d H:i:s')."'";
		}
		return sprintf($sql, implode(',', $infields), implode(',', $values));
	}

	/* Copies any matching fields from some arbitrary object into the current object */
	function setValues (&$anObject) {
		foreach (get_class_vars(get_class($this)) as $field=>$value) {
			if ($field != 'id' AND isset($anObject->$field)) $this->$field = $anObject->$field;
		}
	}

	/* Ensures values can safely be written to DB; assumes magic quotes forced off */
	function prepareValues () {
		$database = mamboDatabase::getInstance();
		foreach (get_class_vars(get_class($this)) as $field=>$value) {
			if (!is_numeric($this->$field) AND is_string($this->$field)) $this->$field = $database->getEscaped($this->$field);
		}
	}

	/* Takes some arbitrary SELECT type SQL and places the first or only result into the current object */
	function readDataBase($sql) {
		$database = mamboDatabase::getInstance();
		$database->setQuery( $sql );
		if (!$database->loadObject($this)) $this->id = 0;
	}

}

?>
