Просмотр исходного кода

UserMsgs added reply, thread, updated view; added DB getDataSource

Piotr Labudda 10 лет назад
Родитель
Сommit
ef98bd3907

+ 59 - 0
SE/se-lib/Core/DataSource.php

@@ -0,0 +1,59 @@
+<?php
+
+
+class Core_DataSource {
+
+	var $_conn;
+	var $_errors;
+
+	public function __construct($host, $user, $password, $database, $names = '', $params = array()) {
+		$this->_conn = null;
+		$this->_errors = array();
+		$this->_database_name = $database;
+		if(!empty($params['zasob_id'])) $this->_zasob_id=$params['zasob_id'];
+	}
+
+	public function getDatabaseName() {
+		return $this->_database_name;
+	}
+
+	/**
+	 * Wykonuje podane zapytanie i zwraca wynik mysql_query().
+	 */
+	public function query($query) {
+		return null;
+	}
+
+	function fetch($res) {
+		return null;
+	}
+
+	function _($str) {
+		return $str;
+	}
+
+	function error() {
+		return "#unknown";
+	}
+
+	function _set_error($err) {
+		$this->_errors[] = $err;
+	}
+
+	function get_errors() {
+		return $this->_errors;
+	}
+
+	function has_errors() {
+		return !empty($this->_errors);
+	}
+
+	function get_last_error() {
+		return end($this->_errors);
+	}
+
+	function get_by_id($table, $id) {
+		return null;
+	}
+
+}

+ 262 - 0
SE/se-lib/Core/DataSource/Mssql.php

@@ -0,0 +1,262 @@
+<?php
+
+Lib::loadClass('Core_DataSource');
+
+class Core_DataSource_Mssql extends Core_DataSource {
+
+	function __construct($host, $user, $password, $database, $names = '', $params = array()) {
+		if ($names != '') {
+			if (strtolower($names) == 'utf8') {
+				$names = 'UTF-8';// @see http://stackoverflow.com/questions/1322421/php-sql-server-how-to-set-charset-for-connection
+			}
+			ini_set('mssql.charset', $names);
+		}
+
+		if (!empty($params['tdsver'])) {
+			$tdsver = V::get('tdsver', '', $params);
+			putenv("TDSVER={$tdsver}");
+		}
+
+		$this->_database_name = $database;
+		$this->_conn = @mssql_connect($host, $user, $password);
+		if (!is_resource($this->_conn)) {
+			$this->_set_error('CREATE CONNECTION FAILED');
+			return;
+		}
+		if (false === mssql_select_db($database, $this->_conn)) {
+			$this->_set_error('SELECT DATABASE FAILED');
+			return;
+		}
+
+
+		if ($names != '') {
+			//$this->query(" SET NAMES '$names' ");
+		}
+	}
+
+	function getConnection() {
+		return $this->_conn;
+	}
+
+	function getVersion($version) {
+		if (!$this->_version) {
+			// TODO: get version sql
+		}
+		return $this->_version;
+	}
+
+	/**
+	 * Wykonuje podane zapytanie i zwraca wynik mssql_query().
+	 */
+	function query($query, $msg = 'Query ERROR.') {
+		$null = null;
+		if (!$this->_conn) { return $null; }
+		$res = mssql_query($query, $this->_conn);
+		if (!$res) {
+			$this->_set_error('SQL QUERY FAILED: ' . mssql_get_last_message());
+			return $null;
+		}
+		return $res;
+	}
+
+	function fetch($res) {
+		if (!is_resource($res)) return null;
+		return mssql_fetch_object($res);
+	}
+
+	function fetch_row($res) {
+		if (!is_resource($res)) return null;
+		return mssql_fetch_row($res);
+	}
+
+	/**
+	 * Returns an associative array that corresponds to the fetched row and moves
+	 * the internal data pointer ahead. mysql_fetch_assoc() is equivalent to calling
+	 * mysql_fetch_array() with MYSQL_ASSOC for the optional second parameter.
+	 * It only returns an associative array.
+	 */
+	function fetch_assoc($res) {
+		$ret = null;
+		if (!is_resource($res)) {
+			return null;
+		} else {
+			$ret = mssql_fetch_assoc($res);
+		}
+		return $ret;
+	}
+
+	function fetch_array($res, $result_type = null) {
+		if (!is_resource($res)) return null;
+		return ($result_type)? mssql_fetch_array($res, $result_type) : mssql_fetch_array($res);
+	}
+
+	function count($res) {
+		if (!is_resource($res)) return null;
+		return mssql_num_rows($res);
+	}
+
+	function num_rows($res) {
+		if (!is_resource($res)) return null;
+		return mssql_num_rows($res);
+	}
+
+	function insert_id() {
+		$id = 0;
+		$res = $this->query("SELECT @@identity AS id");
+		if ($row = $this->fetch_assoc($res)) {
+			$id = $row["id"];
+		}
+		return $id;
+	}
+
+	function affected_rows() {
+		return mssql_rows_affected($this->_conn);
+	}
+
+	function _($str) {
+		//TODO: return mssql_real_escape_string($str, $this->_conn);
+		return $str;
+	}
+
+	function error() {
+		return "#".mssql_errno($this->_conn).": ".mssql_error($this->_conn);
+	}
+
+	function get_by_id( $table, $id ) {
+		$null = null;
+		$sql = "select p.*
+			from `".$table."` as p
+			where p.`ID`='".$id."'
+		";
+		$res = $this->query( $sql );
+		if ($r = $this->fetch( $res )) {
+			return $r;
+		}
+		return $null;
+	}
+
+	/**
+	 * @returns int
+	 *   1 - changed but without add hist
+	 *   2 - changed and add hist
+	 *   0 - nothing to change
+	 *   -1 - error ID not set
+	 *   -2 - error id not exists in DB
+	 *
+	 * TODO: sprawdzac czy w hist mozna odczytac aktualny stan, jesli nie to dodac caly rekord do HIST, jako 'procesy-fix-hist-data'
+	 */
+	function UPDATE_OBJ( $table, &$sql_obj ) {
+		if (!isset($sql_obj->ID) || $sql_obj->ID <= 0) {
+			return -1;
+		}
+		$id = $sql_obj->ID;
+
+		// check id record $id exists
+		if (($curr_obj = $this->get_by_id( $table, $sql_obj->ID )) == null) {
+			return -2;
+		}
+
+		// check if enything changed
+		$changed = false;
+		$fields_to_change = get_object_vars($sql_obj);
+		foreach ($fields_to_change as $k => $v) {
+			if ($k == 'ID') continue;
+			if ($v == $curr_obj->$k) {// === ?
+				unset($sql_obj->$k);
+			} else {
+				$changed = true;
+			}
+		}
+		if ($changed == false) {
+			return 0;// record not changed
+		}
+
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col []= 'A_RECORD_CREATE_DATE';
+		$admin_col []= 'A_RECORD_CREATE_AUTHOR';
+		// ...
+		$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d-H:i');
+		$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+			if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL') {
+				$v = 'NULL';
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr [] = "`{$k}`={$v}";
+		}
+		$sql = "update `{$table}` set ".implode(",", $sql_arr)." where `ID`='{$id}' limit 1; ";
+		$this->query($sql);
+
+		if ($this->has_errors()) {
+			//echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">db errors: (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($this->get_errors());echo'</pre>';
+		}
+
+		$ret = $this->affected_rows();
+		if ($ret) {
+			$sql_obj->ID_USERS2 = $sql_obj->ID;
+			unset($sql_obj->ID);
+			$new_id = $this->ADD_NEW_OBJ("{$table}_HIST", $sql_obj);
+			if ($new_id) {
+				$ret += 1;
+			}
+		}
+		return $ret;
+	}
+
+	function ADD_NEW_OBJ( $table, $sql_obj ) {
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col []= 'ID';
+		$admin_col []= 'A_RECORD_CREATE_DATE';
+		$admin_col []= 'A_RECORD_CREATE_AUTHOR';
+		$admin_col []= 'A_RECORD_UPDATE_DATE';
+		$admin_col []= 'A_RECORD_UPDATE_AUTHOR';
+		// ...
+		$sql_arr["`ID`"] = "NULL";// add default value for ID, NULL in all inserts
+		if (substr($table, 0, -5) == '_HIST') {
+			$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d-H:i');
+			$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		} else {
+			$sql_obj->A_RECORD_CREATE_DATE = date('Y-m-d-H:i');
+			$sql_obj->A_RECORD_CREATE_AUTHOR = User::getName();
+		}
+
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+			if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL' && substr($table, -5) != '_HIST') {
+				$v = 'NULL';
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr ["`{$k}`"] = $v;
+		}
+		$sql = "insert into `{$table}` (".implode(",", array_keys($sql_arr)).") values (".implode(",", array_values($sql_arr))."); ";
+		$this->query($sql);
+
+		if ($this->has_errors()) {
+			//echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">db errors: (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($this->get_errors());echo'</pre>';
+		}
+
+		$ret_id = $this->insert_id();
+		if (substr($table, -5) == '_HIST') {
+			return $ret_id;
+		}
+		if ($ret_id) {
+			$sql_obj->ID_USERS2 = $ret_id;
+			unset($sql_obj->ID);
+			$new_id_hist = $this->ADD_NEW_OBJ($table . '_HIST', $sql_obj);
+			// error jesli nie udalo sie dodac rekordu do tabeli _HIST
+		}
+		return $ret_id;
+	}
+
+}

+ 350 - 0
SE/se-lib/Core/DataSource/Mysql.php

@@ -0,0 +1,350 @@
+<?php
+
+Lib::loadClass('Core_DataSource');
+Lib::loadClass('DataSourceException');
+
+class Core_DataSource_Mysql extends Core_DataSource {
+
+	function __construct($host, $user, $password, $database, $names = '', $params = array()) {
+		parent::__construct($host, $user, $password, $database, $names, $params);
+
+		$this->_conn = @mysql_pconnect($host, $user, $password);
+		if (!is_resource($this->_conn)) throw new Exception("Create connection failed!");
+		if (false === mysql_select_db($database, $this->_conn)) throw new Exception("Select database failed!");
+
+		if ($names != '') {
+			$this->query(" SET NAMES '$names' ");
+		}
+	}
+
+	function getConnection() {
+		return $this->_conn;
+	}
+
+	function getVersion($version) {
+		if (!$this->_version) {
+			$sql = "SHOW VARIABLES LIKE 'version';";
+			$res = $this->query($sql);
+			if ($r = $this->fetch($res)) {
+				// [Variable_name] => version, [Value] => 4.0.26-log
+				$this->_version = $r->Value;
+			}
+		}
+		return $this->_version;
+	}
+
+	function insert($table, $data) {
+		if (is_object($data)) $data = (array)$data;
+		else if (!is_array($data)) throw new Exception("Wrong data type to insert.");
+		$sqlFields = array();
+		$sqlValues = array();
+		foreach ($data as $fldName => $fldValue) {
+			$sqlFields[] = "`{$fldName}`";
+			$sqlValues[] = $this->parseValue($fldValue);
+		}
+		$sqlFields = implode(", ", $sqlFields);
+		$sqlValues = implode(", ", $sqlValues);
+		$sql = "insert into `{$table}` ({$sqlFields})
+			values ({$sqlValues})
+		";
+		$this->query($sql);
+		return mysql_insert_id($this->_conn);
+	}
+
+	function getById($tableName, $id) {
+		$sqlTableName = $this->_($tableName);
+		$sqlId = (int)$this->_($id);
+		if (!$sqlTableName) throw new Exception("Wrong table name!");
+		if ($sqlId <= 0) throw new Exception("Wrong record id!");
+		$sql = "select t.*
+			from `{$sqlTableName}` as t
+			where t.`ID`='{$sqlId}'
+		";
+		$res = $this->query($sql);
+		if ($r = $this->fetch($res)) {
+			return $r;
+		} else throw new Exception("Nie naleziono rekordu nr '{$sqlId}'");
+	}
+
+	function getListByQuery($sql) {
+		$list = array();
+		if (!$sql) throw new Exception("Empty query!");
+		$res = $this->query($sql);
+		while ($r = $this->fetch($res)) {
+			$list[] = $r;
+		}
+		return $list;
+	}
+
+	public function parseValue($value) {
+		$parsedValue = 'NULL';
+		if ('NOW()' == strtoupper($value)) {
+			$parsedValue = 'NOW()';
+		} else if ('GeomFromText' == substr($value, 0, strlen('GeomFromText'))) {
+		} else {
+			$parsedValue = "'" . $this->_($value) . "'";
+		}
+		return $parsedValue;
+	}
+
+	function query($query) {
+		$null = null;
+		if (!$this->_conn) throw new Exception("Connection not exists!");
+		$res = mysql_query($query, $this->_conn);
+		if (!$res) {
+			$exception = new DataSourceException("Query error: " . mysql_error($this->_conn), mysql_errno($this->_conn));
+			$exception->setQuery($query);
+			throw $exception;
+		}
+		return $res;
+	}
+
+	function fetch( $res ) {
+		$ret = null;
+		if ($res) $ret = mysql_fetch_object( $res );
+		return $ret;
+	}
+
+	function fetch_row( $res ) {
+		$ret = null;
+		if ($res) $ret = mysql_fetch_row( $res );
+		return $ret;
+	}
+
+	function fetch_assoc( $res ) {
+		$ret = null;
+		if ($res) $ret = mysql_fetch_assoc( $res );
+		return $ret;
+	}
+
+	function fetch_array($res) {
+		$ret = null;
+		if ($res) $ret = mysql_fetch_array($res);
+		return $ret;
+	}
+
+	function count( $res ) {
+		return mysql_num_rows( $res );
+	}
+
+	function num_rows( $res ) {
+		return mysql_num_rows( $res );
+	}
+
+	function insert_id() {
+		return mysql_insert_id( $this->_conn );
+	}
+	function show_tables($table=null) {
+		if(!empty($table)) $sql="show tables like '".$table."'";
+		else $sql="show tables";
+		$res = $this->query($sql);
+		return $res;
+	}
+
+	function describe_table($table) {
+		$sql='SHOW FIELDS FROM `'.$table.'`';
+		$res = $this->query($sql);
+		return $res;
+	}
+
+	function describe_table_value($table) {
+
+		$res = self::describe_table($table);
+		while($h=self::fetch($res)) {
+			$result[$h->Field]=$h;
+		}
+		return $result;
+	}
+
+	function show_index($table,$only_primary_flag=false) {
+		$sql="show index from `".$table."`";
+		if($only_primary_flag) $sql.=" WHERE  `Key_name` =  'PRIMARY' ";
+
+		$res=self::query($sql);
+		return $res;
+
+	}
+
+	function show_index_value($table) {
+		$res=self::show_index($table,true);
+		while($h=self::fetch($res)) {
+			return $h->Column_name;
+		}
+	}
+
+	function affected_rows($needed_in_psql_only=null) {
+		return mysql_affected_rows( $this->_conn );
+	}
+
+	function _( $str ) {
+		return mysql_real_escape_string( $str, $this->_conn );
+	}
+
+	function error() {
+		return "#".mysql_errno($this->_conn).": ".mysql_error($this->_conn);
+	}
+
+	function errno() {
+		return mysql_errno($this->_conn);
+	}
+
+	/**
+	 * @returns int
+	 *   1 - changed but without add hist
+	 *   2 - changed and add hist
+	 *   0 - nothing to change
+	 *   -1 - sql errors
+	 *   -2 - error id not exists in DB
+	 *   -3 - error ID not set
+	 *
+	 * TODO: sprawdzac czy w hist mozna odczytac aktualny stan, jesli nie to dodac caly rekord do HIST, jako 'procesy-fix-hist-data'
+	 */
+	public function UPDATE_OBJ($table, $sql_obj,$timestamp=null,$skip_author=null) {
+		$structure=self::describe_table_value($table); //todo to cache optimize
+		$primary=self::show_index_value($table); //todo to cache optimize
+
+		if (!isset($sql_obj->$primary) || $sql_obj->$primary <= 0) {
+			return -3;
+		}
+		$id = $sql_obj->$primary;
+
+		// check id record $id exists
+		if (($curr_obj = $this->get_by_id( $table, $sql_obj->$primary )) == null) {
+			return -2;
+		}
+
+		// check if enything changed
+		$changed = false;
+		$fields_to_change = get_object_vars($sql_obj);
+		foreach ($fields_to_change as $k => $v) {
+			if ($k == $primary) continue;
+			if ($v == $curr_obj->$k) {// === ?
+				unset($sql_obj->$k);
+			} else {
+				$changed = true;
+			}
+		}
+		if ($changed == false) {
+			return 0;// record not changed
+		}
+
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col[] = 'A_RECORD_CREATE_DATE';
+		$admin_col[] = 'A_RECORD_CREATE_AUTHOR';
+		// ...
+		$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d-H:i');
+// OFF - BUG w _HIST		$sql_obj->A_RECORD_UPDATE_DATE = "FROM_UNIXTIME(".$this->get_current_time().")";
+
+		$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+		   if(!empty($skip_author)) {
+			   if($k=='A_RECORD_UPDATE_AUTHOR') continue;
+			   if($k=='A_RECORD_UPDATE_DATE') continue;
+		   }
+
+
+			if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL') {
+				$v = 'NULL';
+			} else if (substr($v, 0, strlen('GeomFromText')) == 'GeomFromText') {
+
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr [] = "`{$k}`={$v}";
+		}
+		$sql = "update `{$table}` set ".implode(",", $sql_arr)." where `ID`='{$id}' limit 1; ";
+		$this->query($sql);
+
+		$returnError = false;
+		$skipDbErrorAddHist = false;
+		if ($this->has_errors()) {
+			$returnError = true;
+			if (1146 == $this->errno()) {
+				$skipDbErrorAddHist = true;
+			}
+		}
+
+		$returnCode = 0;
+		$affected = $this->affected_rows();
+		if ($affected || $skipDbErrorAddHist) {
+			$returnCode = 1;
+			$sql_obj->ID_USERS2 = $sql_obj->$primary;
+			unset($sql_obj->$primary);
+			$new_id = $this->ADD_NEW_OBJ("{$table}_HIST", $sql_obj);
+			if ($new_id) {
+				$returnCode += 1;
+			}
+		}
+		if ($returnError) {
+			return -1;
+		}
+		return $returnCode;
+	}
+
+	function ADD_NEW_OBJ( $table, $sql_obj,$dieonerror=null ) {
+
+		//TODO to optimize:
+		$structure=self::describe_table_value($table);
+		$primary=self::show_index_value($table);
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col []= 'ID';
+		$admin_col []= 'A_RECORD_CREATE_DATE';
+		$admin_col []= 'A_RECORD_CREATE_AUTHOR';
+		$admin_col []= 'A_RECORD_UPDATE_DATE';
+		$admin_col []= 'A_RECORD_UPDATE_AUTHOR';
+		// ...
+		$sql_arr["`ID`"] = "NULL";// add default value for ID, NULL in all inserts
+		if (substr($table, 0, -5) == '_HIST') {
+			$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d-H:i');
+			$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		} else {
+			$sql_obj->A_RECORD_CREATE_DATE = date('Y-m-d-H:i');
+			$sql_obj->A_RECORD_CREATE_AUTHOR = User::getName();
+		}
+
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+			if($k==$primary) $v='0';
+			else if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL' && substr($table, -5) != '_HIST') {
+				$v = 'NULL';
+			} else if (substr($v, 0, strlen('GeomFromText')) == 'GeomFromText') {
+
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr ["`{$k}`"] = $v;
+		}
+		$sql = "insert into `{$table}` (".implode(",", array_keys($sql_arr)).") values (".implode(",", array_values($sql_arr))."); ";
+		//error_log($sql);
+		$this->query($sql);
+
+		if ($this->has_errors()) {
+			if(!empty($dieonerror)) {
+			  echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">db errors: (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($this->get_errors());echo'</pre>';
+			  DEBUG_S(-3,'Bledne zapytanie sql',$sql,__FILE__,__FUNCTION__,__LINE__);
+			}
+		}
+
+		$ret_id = $this->insert_id();
+		if (substr($table, -5) == '_HIST') {
+			return $ret_id;
+		}
+		if ($ret_id) {
+			$sql_obj->ID_USERS2 = $ret_id;
+			unset($sql_obj->ID);
+			$new_id_hist = $this->ADD_NEW_OBJ($table . '_HIST', $sql_obj);
+			// error jesli nie udalo sie dodac rekordu do tabeli _HIST
+		}
+		return $ret_id;
+	}
+
+}

+ 307 - 0
SE/se-lib/Core/DataSource/Pgsql.php

@@ -0,0 +1,307 @@
+<?php
+
+Lib::loadClass('Core_DataSource');
+
+class Core_DataSource_Pgsql extends Core_DataSource {
+
+	function __construct($host, $user, $password, $database, $names = '', $params = array()) {
+		parent::__construct($host, $user, $password, $database, $names, $params);
+
+		list($host,$port)=explode(":", $host);
+		$this->_conn = @pg_connect("host=".$host." port=".$port." dbname=".$database." user=".$user." password=".$password);
+
+		//DEBUG_S(-3,'conn'.$names." ",$params);
+		if (!is_resource($this->_conn)) {
+			$this->_set_error('CREATE CONNECTION FAILED');
+			return;
+		}
+		//if (false === mysql_select_db($database, $this->_conn)) {
+		//	$this->_set_error('SELECT DATABASE FAILED');
+		//	return;
+		//}
+
+		if ($names != '') {
+			$this->query("SET CLIENT_ENCODING TO '".$names."';");
+		}
+	}
+
+	function getConnection() {
+		return $this->_conn;
+	}
+
+	function getVersion($version) {
+		if (!$this->_version) {
+			//$sql = "SHOW VARIABLES LIKE 'version';";
+			$this->_version=pg_version($this->_conn);
+			//if ($r = $this->fetch($res)) {
+			//	// [Variable_name] => version, [Value] => 4.0.26-log
+			//	$this->_version = $r->Value;
+			//}
+		}
+		return $this->_version;
+	}
+
+	/**
+	 * Wykonuje podane zapytanie i zwraca wynik mysql_query().
+	 */
+	function query( $query, $msg = 'Query ERROR.' ) {
+		$null = null;
+		if (!$this->_conn) { return $null; }
+		$res = pg_query($this->_conn,$query);
+		if (!$res) {
+			DEBUG_S(-3,'error ',array($query),__FILE__,__FUNCTION__,__LINE__);
+			$this->_set_error('SQL QUERY FAILED: '.pg_result_error($this->_conn)."(".$query.")".pg_last_error($this->_conn));
+			return $null;
+		}
+		return $res;
+	}
+
+	function fetch( $res ) {
+		$ret = null;
+		if ($res) $ret = pg_fetch_object( $res );
+		return $ret;
+	}
+
+	function fetch_row( $res ) {
+		$ret = null;
+		if ($res) $ret = pg_fetch_row( $res );
+		return $ret;
+	}
+
+	function fetch_assoc( $res ) {
+		$ret = null;
+		if ($res) $ret = pg_fetch_assoc( $res );
+		return $ret;
+	}
+
+	function fetch_array($res) {
+		$ret = null;
+		if ($res) $ret = pg_fetch_array($res);
+		return $ret;
+	}
+
+	function count( $res ) {
+		return pg_num_rows( $res );
+	}
+
+	function num_rows( $res ) {
+		return pg_num_rows( $res );
+	}
+
+	function insert_id() {
+		$sql="select lastval();";
+		$res = $this->query($sql);
+		$last=$this->fetch_array($res);
+		return $last[0];
+		//return fetch_row( $res ); //TODO TESTING!!!
+		//die('TEST THIS FUNCTION!!!');
+		//return mysql_insert_id( $this->_conn );
+	}
+
+	function affected_rows($res) {
+		return pg_affected_rows( $res );
+	}
+
+	function _( $str ) {
+		return pg_escape_string( $str);
+	}
+
+	function error() {
+		return "#".pg_result_error_field($this->_conn).": ".pg_result_error($this->_conn);
+	}
+
+	function get_by_id( $table, $id ) {
+		$primary=self::show_index_value($table); //TODO to optimalize cache
+
+		$null = null;
+		$sql = "select p.*
+			from \"".$table."\" as p
+			where p.\"".$primary."\"='".$id."'
+		";
+		$res = $this->query( $sql );
+		if ($r = $this->fetch( $res )) {
+			return $r;
+		}
+		return $null;
+	}
+
+	/**
+	 * @returns int
+	 *   1 - changed but without add hist
+	 *   2 - changed and add hist
+	 *   0 - nothing to change
+	 *   -1 - sql errors
+	 *   -2 - error id not exists in DB
+	 *   -3 - error ID not set
+	 *
+	 * TODO: sprawdzac czy w hist mozna odczytac aktualny stan, jesli nie to dodac caly rekord do HIST, jako 'procesy-fix-hist-data'
+	 */
+	public function UPDATE_OBJ($table, $sql_obj,$timestamp=null,$skip_author=null) {
+		$structure=self::describe_table_value($table);
+		$primary=self::show_index_value($table);
+
+		if (!isset($sql_obj->$primary) || $sql_obj->$primary <= 0) {
+			return -3;
+		}
+		$id = $sql_obj->$primary;
+
+		// check id record $id exists
+		if (($curr_obj = $this->get_by_id( $table, $sql_obj->$primary )) == null) {
+			return -2;
+		}
+
+		// check if enything changed
+		$changed = false;
+		$fields_to_change = get_object_vars($sql_obj);
+		//DEBUG_S(-3,'chk',array($fields_to_change),__FILE__,__FUNCTION__,__LINE__);
+
+		foreach ($fields_to_change as $k => $v) {
+			if ($k == $primary) continue;
+			if ($v == $curr_obj->$k) {// === ?
+				unset($sql_obj->$k);
+			} else {
+				$changed = true;
+			}
+		}
+		if ($changed == false) {
+			return 0;// record not changed
+		}
+
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col []= 'A_RECORD_CREATE_DATE';
+		$admin_col []= 'A_RECORD_CREATE_AUTHOR';
+		// ...
+//		$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d H:i');
+		if(!empty($timestamp))  //fixed timestamp option TODO @2015-01-not working due to trigger?
+			$sql_obj->A_RECORD_UPDATE_DATE = $timestamp;
+		else
+		$sql_obj->A_RECORD_UPDATE_DATE = self::get_current_timestamp();
+
+		$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+
+			if($k=='A_RECORD_CREATE_DATE'&& $v=='0000-00-00 00:00:00') {
+				unset($k);
+				unset($v);
+				continue;
+
+			} else if(strstr(trim($structure[$k]->Type),'date') && (strstr($v,'-00'))) {
+				$v="'".str_replace("-00","-01",$v)."'" ;
+			} else if(strstr(trim($structure[$k]->Type),'date') && (strstr($v,'0000'))) {
+				$v="'".str_replace("0000","1970",$v)."'" ;
+			} else if (substr($v, 0, strlen('ST_GeomFromText')) == 'ST_GeomFromText') {
+
+			} else if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL') {
+				$v = 'NULL';
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr [] = '"'.$k.'"='.$v;
+		}
+
+		$sql = "update \"{$table}\" set ".implode(",", $sql_arr)." where \"".$primary."\"='{$id}'  ; ";
+	//	DEBUG_S(-3,'update',array($sql),__FILE__,__FUNCTION__,__LINE__);
+	//	die();
+		$res=$this->query($sql);
+
+		if ($this->has_errors()) {
+			//DEBUG_S(-3,'errors',$this->has_errors,__FILE__,__FUNCTION__,__LINE__);
+			return -1;
+		}
+
+		$ret = $this->affected_rows($res);
+				//	DEBUG_S(-3,'affected',$ret,__FILE__,__FUNCTION__,__LINE__);
+
+		if ($ret) {
+			$sql_obj->ID_USERS2 = $sql_obj->$primary;
+			unset($sql_obj->$primary);
+			$new_id = $this->ADD_NEW_OBJ("{$table}_HIST", $sql_obj);
+			if ($new_id) {
+				$ret += 1;
+			}
+		}
+		return $ret;
+	}
+
+	function ADD_NEW_OBJ( $table, $sql_obj,$dieonerror=null ) {
+		$structure=self::describe_table_value($table);
+		$primary=self::show_index_value($table);
+		$sql_arr = array();
+		// TODO: add admin columns if exists in table - search in session
+		$admin_col = array();
+		$admin_col []= 'A_RECORD_CREATE_DATE';
+		$admin_col []= 'A_RECORD_CREATE_AUTHOR';
+		$admin_col []= 'A_RECORD_UPDATE_DATE';
+		$admin_col []= 'A_RECORD_UPDATE_AUTHOR';
+		// ...
+		if (substr($table, 0, -5) == '_HIST') {
+			$sql_obj->A_RECORD_UPDATE_DATE = date('Y-m-d H:i');
+			$sql_obj->A_RECORD_UPDATE_AUTHOR = User::getName();
+		} else {
+			$sql_obj->A_RECORD_CREATE_DATE = date('Y-m-d H:i');
+			$sql_obj->A_RECORD_CREATE_AUTHOR = User::getName();
+		}
+
+		foreach (get_object_vars($sql_obj) as $k => $v) {
+			if($k==$primary) {
+				unset($k);
+				unset($v);
+				continue;
+			}  else if (substr($v, 0, strlen('ST_GeomFromText')) == 'ST_GeomFromText') {
+
+			} else if(strstr(trim($structure[$k]->Type),'int')) {
+				if(strlen($v)>0) {
+					$v=$v;
+				} else if(empty($v)) $v='null';
+				else $v = $v;
+			} else if(trim($structure[$k]->Type)=='datetime') {
+				if(empty($v)) $v='null';
+				else if(strstr($v,'-00')) $v="'".str_replace("-00","-01",$v)."'" ;
+				else $v = "'".$v."'::timestamp";
+			} else if(trim($structure[$k]->Type)=='date') {
+				if($v=='0000-00-00') $v='null';
+				else if(strstr($v,'-00')) $v="'".str_replace("-00","-01",$v)."'" ;
+				else $v = "'".$v."'";
+			} else if (strtoupper($v) == 'NOW()') {
+				$v = 'NOW()';
+			} else if (strtoupper($v) == 'NULL' && substr($table, -5) != '_HIST') {
+				$v = 'NULL';
+			} else if(strstr($structure[$k]->Type,'int(')) {
+				$v = $v ;
+			} else {
+				$v = $this->_($v);
+				$v = "'{$v}'";
+			}
+			$sql_arr ['"'.$k.'"'] = $v;
+		}
+		$sql = "insert into \"{$table}\" (".implode(",", array_keys($sql_arr)).") values (".implode(",", array_values($sql_arr))."); ";
+		//DEBUG_S(-3,' insert sql ',$sql,__FILE__,__FUNCTION__,__LINE__);
+		$this->query($sql);
+
+		if ($this->has_errors()) {
+			if($dieonerror) {
+				DEBUG_S(-3,'Has errors died',$this->get_errors(),__FILE__,__FUNCTION__,__LINE__);
+				die();
+			}
+		//	echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">db errors: (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($this->get_errors());print_r($sql);echo'</pre>';
+		}
+
+		$ret_id = $this->insert_id();
+		if (substr($table, -5) == '_HIST') {
+			return $ret_id;
+		}
+		if ($ret_id) {
+			$sql_obj->ID_USERS2 = $ret_id;
+			unset($sql_obj->ID);
+			$new_id_hist = $this->ADD_NEW_OBJ($table . '_HIST', $sql_obj);
+			// error jesli nie udalo sie dodac rekordu do tabeli _HIST
+		}
+		return $ret_id;
+	}
+
+}

+ 52 - 0
SE/se-lib/DB.php

@@ -1,7 +1,59 @@
 <?php
 
+Lib::loadClass('Config');
+Lib::loadClass('DataSourceException');
+
 class DB {
 
+	public static function getDataSource($db = null) {
+		static $_instanceList;
+		if (!is_array($_instanceList)) {
+			$_instanceList = array();
+		}
+
+		if (null === $db) {
+			$configName = 'default_db';
+		} else if (is_numeric($db) && $db > 0) {
+			$configName = "zasob_{$db}";
+		} else if ($db == 'import_db') {
+			$configName = "import_db";
+		} else if ($db == 'test_db') {
+			$configName = "test_db";
+		}  else if ($db == 'billing_db') {
+			$configName = "billing_db";
+		} else {// TODO: check by name from zasoby
+			throw new Exception("Unknown data source name!");
+		}
+
+		if (array_key_exists($configName, $_instanceList)) {
+			return $_instanceList[$configName];
+		}
+		$_instanceList[$configName] = null;
+
+		$conf = Config::getConfFile($configName);
+		if (!$conf) throw new Exception("Config for data source '{$configName}' not found!");
+		$type = V::get('type', 'mysql', $conf);
+		$host = V::get('host', '', $conf);
+		$port = V::get('port', '', $conf);
+		$user = V::get('user', '', $conf);
+		$pass = V::get('pass', '', $conf);
+		$zasob_id = V::get('zasob_id', '', $conf);
+
+		$database = V::get('database', '', $conf);
+		if ($port && $host) $host .= ":{$port}";
+		$names = 'utf8';
+		$db_class = 'Core_DataSource_' . ucfirst($type);
+		Lib::loadClass($db_class);
+		if (!class_exists($db_class)) throw new Exception("Data source class for type '{$type}' not found!");
+		$params = array();
+		$tdsver = V::get('tdsver', '', $conf);
+		if (!empty($tdsver)) $params['tdsver'] = $tdsver;
+		if (!empty($zasob_id)) $params['zasob_id'] = $zasob_id;
+
+		$_instanceList[$configName] = new $db_class($host, $user, $pass, $database, $names, $params);
+		return $_instanceList[$configName];
+	}
+
 	/**
 	 * Get database object.
 	 * 

+ 15 - 0
SE/se-lib/DataSourceException.php

@@ -0,0 +1,15 @@
+<?php
+
+class DataSourceException extends Exception {
+
+	public $_query = null;
+
+	public function setQuery($query) {
+		$this->_query = $query;
+	}
+
+	public function getQuery() {
+		return $this->_query;
+	}
+
+}

+ 2 - 0
SE/se-lib/Route/Msgs.php

@@ -244,6 +244,8 @@ SQL_QUERY;
 		$sqlList['InstallTable'] = "
 			CREATE TABLE IF NOT EXISTS `CRM_UI_MSGS` (
 				`ID` int(11) NOT NULL AUTO_INCREMENT
+				, `idReplyTo` int(11) NOT NULL DEFAULT 0
+				, `idThread` int(11) NOT NULL DEFAULT 0
 				-- app_className - for automatic msgs to search for msg text
 				, `app_className` varchar(255) DEFAULT NULL
 				-- msg - msg to show or to parse by app_className

+ 146 - 57
SE/se-lib/Route/UserMsgs.php

@@ -707,38 +707,10 @@ function tblMsgsLoadMoreRows(n) {
 	}
 
 	public function _testViewMsg($msg) {
-		/* $msg = {_raw: {A_RECORD_CREATE_AUTHOR: "plabudda",
-											A_RECORD_CREATE_DATE: "2015-10-26 12:20:05",
-											A_RECORD_DELETE_AUTHOR: "",
-											A_RECORD_DELETE_DATE: null,
-											A_RECORD_UPDATE_AUTHOR: "plabudda",
-											A_RECORD_UPDATE_DATE: "2015-11-02 12:44:59",
-											A_STATUS: "NORMAL",
-											ID: "67",
-											actionExecutedTime: "2015-11-02 12:44:59",
-											actionNotes: "",
-											app_className: "TableMsgs",
-											msg: "test Y",
-											msgType: "info",
-											uiTargetName: "TEST_PERMS.31",
-											uiTargetType: "default_db_table_record",
-											userTargetName: "plabudda",
-											userTargetType: "user"}
-							 _read: true,
-							 idRow: "31",
-							 message: "test Y",
-							 tblName: "TEST_PERMS",
-							 type: "info",
-							 usrLogin: "plabudda"} */
-		$message = new stdClass();
-		$message->id = $msg['_raw']->ID;
-		$message->idThread = $msg['_raw']->ID;// TODO: ID_THREAD
-		$message->idReplyTo = $msg['_raw']->ID;// TODO: ID_REPLY_TO
-		$message->message = $msg['_raw']->msg;
-		$message->type = $msg['_raw']->msgType;
-		$message->author = $msg['_raw']->A_RECORD_CREATE_AUTHOR;
-		$message->created = $msg['_raw']->A_RECORD_CREATE_DATE;
-		$message->_read = $msg['_read'];
+		$uiTargetName = $msg['_raw']->uiTargetName;
+		$uiTargetType = $msg['_raw']->uiTargetType;
+		$replyLink = "index.php?_route=UserMsgs&_task=reply&uiTargetName={$uiTargetName}&uiTargetType={$uiTargetType}";
+		$message = $this->_convertMessageToJson($msg['_raw']);
 		$messageList = array($message);
 		?>
 <link rel="stylesheet" href="./stuff/widget-select.css">
@@ -778,31 +750,28 @@ function frmTestSubmit(frm) {
 		msgs: <?php echo json_encode($messageList); ?>,
 		fetchMessages: (function() {
 			var _msgsXhr = null;
-			return function(input, callback) {
+			return function(reqData, callback) {
 				if (_msgsXhr && _msgsXhr.state() === 'pending') {
 					_msgsXhr.abort();
 					_msgsXhr = null;
 				}
 				_msgsXhr = $.ajax({
 					url: 'index.php?_route=UserMsgs&_task=getMessagesById&id=<?php echo $message->id; ?>',
+					data: reqData,
 					dataType: 'json'
 				});
 				_msgsXhr.done(function(data, textStatus, jqXHR) {
-					if (data && data.length > 0) {
-						var options = [];
-						data.forEach(function(item) {
-							options.push({value: item.id, label: item.name});
-						});
-						callback(null, {options: options});
+					if (data && data.msgs && data.msgs.length > 0) {
+						callback(null, {msgs: data.msgs});
 					} else {
-						callback(null, {options: []});//"Error no data!");
+						callback(null, {msgs: []});//"Error no data!");
 					}
 				});
 				_msgsXhr.fail(function() {
 					callback(null, {options: []});//"Error no data!");
 				});
 				_msgsXhr.always(function() {
-					_userXhr = null;
+					_msgsXhr = null;
 				});
 			};
 		})(),
@@ -868,22 +837,31 @@ function frmTestSubmit(frm) {
 		})(),
 		saveReply: function(data, callback) {
 			console.log('#widget-msg-tree/MsgThread::saveReply: data:', data, 'callback', callback);
-			// TODO: send data
-			setTimeout(function() {
-				console.log('#widget-msg-tree/MsgThread::saveReply: after 500ms data:', data, 'callback', callback);
-				var newId = 1000 + (testNewRecordCounter++),
-						newRecord = {
-							id: newId,
-							author: 'test13',
-							created: '2015-10-22 10:36:52',
-							to: 'plabudda',
-							metaInfo: 'do plabudda',
-							message: 'test added newId('+newId+')'
-						}
-				;
-				//callback(null, {message: 'Nie udało się zapisać wiadomości', type: 'danger'});
-				callback(null, {message: 'Saved', type: 'success', record: newRecord});
-			}, 500);
+			$.ajax({
+				url: '<?php echo $replyLink; ?>',
+				method: 'POST',
+				data: data,
+				dataType: 'json'
+			})
+			.done(function(data, textStatus, jqXHR) {
+				var returnData = {message: '', type: 'danger'};
+				if (data && data.record) {
+					returnData.msg = data.msg || 'Wysłano wiadomość';
+					returnData.record = data.record;
+					returnData.type = 'success';
+				} else if (data.validateErrors) {
+					returnData.msg = data.msg || 'Wystąpiły błędy w formularzu';
+					returnData.type = 'warning';
+					returnData.validateErrors = data.validateErrors;
+				} else {
+					returnData.msg = data.msg || 'Nie udało się wysłać wiadomości!';
+					returnData.type = 'danger';
+				}
+				callback(null, returnData);
+			})
+			.fail(function() {
+				callback(null, {message: 'Nie udało się wysłać wiadomości!', type: 'danger'});
+			});
 		}
 	});
 //	jQuery("#widget-msg-tree").on('change', function(e, data) {
@@ -893,8 +871,119 @@ function frmTestSubmit(frm) {
 <?php
 	}
 
+	public function _convertMessageToJson($rawMsg) {
+		/* $msg = {_raw: {A_RECORD_CREATE_AUTHOR: "plabudda",
+											A_RECORD_CREATE_DATE: "2015-10-26 12:20:05",
+											A_RECORD_DELETE_AUTHOR: "",
+											A_RECORD_DELETE_DATE: null,
+											A_RECORD_UPDATE_AUTHOR: "plabudda",
+											A_RECORD_UPDATE_DATE: "2015-11-02 12:44:59",
+											A_STATUS: "NORMAL",
+											ID: "67",
+											actionExecutedTime: "2015-11-02 12:44:59",
+											actionNotes: "",
+											app_className: "TableMsgs",
+											msg: "test Y",
+											msgType: "info",
+											uiTargetName: "TEST_PERMS.31",
+											uiTargetType: "default_db_table_record",
+											userTargetName: "plabudda",
+											userTargetType: "user"}
+							 _read: true,
+							 idRow: "31",
+							 message: "test Y",
+							 tblName: "TEST_PERMS",
+							 type: "info",
+							 usrLogin: "plabudda"} */
+		$message = new stdClass();
+		$message->id = $rawMsg->ID;
+		$message->idThread = $rawMsg->idThread;// TODO: ID_THREAD
+		$message->idReplyTo = $rawMsg->idReplyTo;// TODO: ID_REPLY_TO
+		$message->message = $rawMsg->msg;
+		$message->type = $rawMsg->msgType;
+		$message->to = $rawMsg->userTargetName;
+		$message->toType = $rawMsg->userTargetType;
+		$message->author = $rawMsg->A_RECORD_CREATE_AUTHOR;
+		$message->created = $rawMsg->A_RECORD_CREATE_DATE;
+		$message->_read = false;//TODO: $msg['_read'];
+		return $message;
+	}
+
 	public function getMessagesByIdAction() {
+		try {
+			$id = V::get('id', '', $_GET, 'int');
+			$idLastMsg = V::get('idLastMsg', '', $_GET, 'int');
+			if ($id <= 0) throw new Exception("Wrong param id!");
+
+			$sqlLimit = 10;// TODO: 100?
+			$ds = DB::getDataSource();
+			$sql = "
+				select m.*
+					from `CRM_UI_MSGS` m
+					where m.`idThread` = {$id}
+						and m.`ID` > {$idLastMsg}
+					limit {$sqlLimit}
+			";
+			$moreMsgs = $ds->getListByQuery($sql);
+
+			$response = new stdClass();
+			$response->msg = "Nowe wiadomości";
+			$response->type = 'success';
+			$response->msgs = array();
+			foreach ($moreMsgs as $msg) {
+				$response->msgs[] = $this->_convertMessageToJson($msg);
+			}
+		} catch (Exception $e) {
+			$response = new stdClass();
+			$response->msg = "Wystąpiły błędy: " . $e->getMessage();
+			$response->type = 'danger';
+		}
+		echo json_encode($response);
+	}
+
+	public function replyAction() {
+		try {
+			$uiTargetType = V::get('uiTargetType', '', $_GET);
+			$uiTargetName = V::get('uiTargetName', '', $_GET);
+			$response = $this->_reply($uiTargetType, $uiTargetName, $_POST);
+		} catch (Exception $e) {
+			$response = new stdClass();
+			$response->msg = "Wystąpiły błędy: " . $e->getMessage();
+			$response->type = 'danger';
+		}
+		echo json_encode($response);
+	}
 
+	public function _reply($uiTargetType, $uiTargetName, $args) {
+		$ds = DB::getDataSource();
+		$newMsg = array();
+		$newMsg['idReplyTo'] = V::get('idReplyTo', '', $args, 'int');
+		$newMsg['msg'] = V::get('message', '', $args);
+		$newMsg['msgType'] = V::get('msgType', 'info', $args);
+		$newMsg['userTargetType'] = V::get('toType', '', $args);
+		$newMsg['userTargetName'] = V::get('to', '', $args);
+		$newMsg['A_RECORD_CREATE_DATE'] = 'NOW()';
+		$newMsg['A_RECORD_CREATE_AUTHOR'] = User::getLogin();
+		$newMsg['app_className'] = 'TableMsgs';
+		//DBG::_(true, true, "newMsg", $newMsg, __CLASS__, __FUNCTION__, __LINE__);
+
+		if ($newMsg['idReplyTo'] <= 0) throw new Exception("Wrong id reply to msg");
+		$parentMsg = $ds->getById('CRM_UI_MSGS', $newMsg['idReplyTo']);
+		if (!$parentMsg) throw new Exception("Nie znaleziono wiadomości");
+
+		$newMsg['idThread'] = ($parentMsg->idThread > 0)? $parentMsg->idThread : $parentMsg->ID;
+		$newMsg['uiTargetType'] = $uiTargetType;// TODO:? $parentMsg->uiTargetType
+		$newMsg['uiTargetName'] = $uiTargetName;// TODO:? $parentMsg->uiTargetName
+
+		$insertedId = $ds->insert('CRM_UI_MSGS', $newMsg);
+		if (!$insertedId) throw new Exception("Nie udało się utworzyć rekordu");
+		$msgAdded = $ds->getById('CRM_UI_MSGS', $insertedId);
+
+		$response = new stdClass();
+		$response->msg = "Wysłano wiadomość";
+		$response->type = 'success';
+		$response->record = $msgAdded;
+		return $response;
 	}
 
 	public function removeMsgAction() {

+ 1 - 0
SE/se-lib/bootstrap.php

@@ -12,6 +12,7 @@ Lib::loadClass('DBG');
 Lib::loadClass('V');
 Lib::loadClass('Config');
 Lib::loadClass('DB');
+Lib::loadClass('DataSourceException');
 Lib::loadClass('ACL');
 Lib::loadClass('User');
 Lib::loadClass('SE_Layout');

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
SE/stuff/bundle.se_route_user_msgs.js


Некоторые файлы не были показаны из-за большого количества измененных файлов