Ver Fonte

New type of attribute: Blob, allowing documents in the application. Was fully developed in the core, but roughly integrated in the application... todo: file upload for edition, file download for viewing

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@196 a333f486-631f-4898-b8df-5754b55c2be0
romainq há 15 anos atrás
pai
commit
1c5be06a13

+ 3 - 1
business/itop.business.class.inc.php

@@ -399,11 +399,13 @@ class bizDocument extends logRealObject
 		MetaModel::Init_AddAttribute(new AttributeEnum("scope", array("label"=>"scope", "description"=>"Scope of this document", "allowed_values"=>new ValueSetEnum("organization,hardware support"), "sql"=>"scope", "default_value"=>"organization", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("description", array("label"=>"Description", "description"=>"Service Description", "allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
 
+		MetaModel::Init_AddAttribute(new AttributeBlob("contents", array("label"=>"Contents", "description"=>"File content", "depends_on"=>array())));
+
 		MetaModel::Init_InheritFilters();
 		MetaModel::Init_AddFilterFromAttribute("scope");
 		MetaModel::Init_AddFilterFromAttribute("description");
 
-		MetaModel::Init_SetZListItems('details', array('name', 'status', 'org_id', 'scope','description')); // Attributes to be displayed for the complete details
+		MetaModel::Init_SetZListItems('details', array('name', 'status', 'org_id', 'scope', 'description', 'contents')); // Attributes to be displayed for the complete details
 		MetaModel::Init_SetZListItems('list', array('name', 'status', 'org_id', 'scope')); // Attributes to be displayed for a list
 		// Search criteria
 		MetaModel::Init_SetZListItems('standard_search', array('name', 'status', 'scope')); // Criteria of the std search form

+ 209 - 49
core/attributedef.class.inc.php

@@ -1,7 +1,15 @@
 <?php
 
 require_once('MyHelpers.class.inc.php');
+require_once('ormdocument.class.inc.php');
 
+/**
+ * MissingColumnException - sent if an attribute is being created but the column is missing in the row 
+ *
+ * @package     iTopORM
+ */
+class MissingColumnException extends Exception
+{}
 
 /**
  * add some description here... 
@@ -48,7 +56,6 @@ abstract class AttributeDefinition
 	abstract public function GetType();
 	abstract public function GetTypeDesc();
 	abstract public function GetEditClass();
-	abstract public function GetDBFieldType();
 
 	protected $m_sCode;
 	private $m_aParams = array();
@@ -88,7 +95,7 @@ abstract class AttributeDefinition
 	// to be overloaded
 	static protected function ListExpectedParams()
 	{
-		return array("label", "description", "allowed_values");
+		return array("label", "description");
 	}
 
 	private function ConsistencyCheck()
@@ -125,8 +132,8 @@ abstract class AttributeDefinition
 	public function GetCode() {return $this->m_sCode;} 
 	public function GetLabel() {return $this->Get("label");} 
 	public function GetDescription() {return $this->Get("description");} 
-	public function GetValuesDef() {return $this->Get("allowed_values");} 
-	public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} 
+	public function GetValuesDef() {return null;} 
+	public function GetPrerequisiteAttributes() {return array();} 
 	//public function IsSearchableStd() {return $this->Get("search_std");} 
 	//public function IsSearchableGlobal() {return $this->Get("search_global");} 
 	//public function IsMandatory() {return $this->Get("is_mandatory");} 
@@ -136,13 +143,12 @@ abstract class AttributeDefinition
 	//public function GetCheckRegExp() {return $this->Get("regexp");} 
 	//public function GetCheckFunc() {return $this->Get("checkfunc");} 
 
-	// Definition: real value is what will be stored in memory and maintained by MetaModel
-	// DBObject::Set()        relies on MakeRealValue()
-	// MetaModel::MakeQuery()  relies on RealValueToSQLValue()
-	// DBObject::FromRow()    relies on SQLToRealValue()
 	public function MakeRealValue($proposedValue) {return $proposedValue;} // force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!)
-	public function RealValueToSQLValue($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
-	public function SQLValueToRealValue($value) {return $value;} // take the result of a fetch... and make it a PHP variable
+
+	public function GetSQLExpressions() {return array();} // returns suffix/expression pairs (1 in most of the cases), for READING (Select)
+	public function FromSQLToValue($aCols, $sPrefix = '') {return null;} // returns a value out of suffix/value pairs, for SELECT result interpretation
+	public function GetSQLColumns() {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
+	public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
 
 	public function GetJSCheckFunc()
 	{
@@ -167,7 +173,6 @@ abstract class AttributeDefinition
 		return call_user_func($sComputeFunc);
 	}
 	
-	abstract public function DBGetUsedFields();
 	abstract public function GetDefaultValue();
 
 	//
@@ -181,17 +186,17 @@ abstract class AttributeDefinition
 
 	public function GetAsHTML($sValue)
 	{
-		return Str::pure2html($sValue);
+		return Str::pure2html((string)$sValue);
 	}
 
 	public function GetAsXML($sValue)
 	{
-		return Str::pure2xml($sValue);
+		return Str::pure2xml((string)$sValue);
 	}
 
 	public function GetAsCSV($sValue, $sSeparator = ';', $sSepEscape = ',')
 	{
-		return str_replace($sSeparator, $sSepEscape, $sValue);
+		return str_replace($sSeparator, $sSepEscape, (string)$sValue);
 	}
 
 	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
@@ -216,23 +221,23 @@ class AttributeLinkedSet extends AttributeDefinition
 {
 	static protected function ListExpectedParams()
 	{
-		return array_merge(parent::ListExpectedParams(), array("depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
+		return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
 	}
 
 	public function GetType() {return "Array of objects";}
 	public function GetTypeDesc() {return "Any kind of objects [subclass] of the same class";}
 	public function GetEditClass() {return "List";}
-	public function GetDBFieldType() {return "N/A";} // should be moved out of the AttributeDef root class
 
 	public function IsWritable() {return true;} 
 	public function IsLinkSet() {return true;} 
 
+	public function GetValuesDef() {return $this->Get("allowed_values");} 
+	public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} 
 	public function GetDefaultValue() {return DBObjectSet::FromScratch($this->Get('linked_class'));}
 
 	public function GetLinkedClass() {return $this->Get('linked_class');}
 	public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');}
 
-	public function DBGetUsedFields() {return array();}
 	public function GetBasicFilterOperators() {return array();}
 	public function GetBasicFilterLooseOperator() {return '';}
 	public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';}
@@ -286,25 +291,55 @@ class AttributeDBFieldVoid extends AttributeDefinition
 {
 	static protected function ListExpectedParams()
 	{
-		return array_merge(parent::ListExpectedParams(), array("depends_on", "sql"));
+		return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql"));
 	}
 
+	// To be overriden, used in GetSQLColumns
+	protected function GetSQLCol() {return "VARCHAR(255)";}
+
 	public function GetType() {return "Void";}
 	public function GetTypeDesc() {return "Any kind of value, from the DB";}
 	public function GetEditClass() {return "String";}
-	public function GetDBFieldType() {return "VARCHAR(255)";}
 	
+	public function GetValuesDef() {return $this->Get("allowed_values");} 
+	public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} 
+
 	public function IsDirectField() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return true;} 
 	public function GetSQLExpr()    {return $this->Get("sql");}
 	public function GetDefaultValue() {return "";}
 	public function IsNullAllowed() {return false;}
-	public function DBGetUsedFields()
+
+	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
+	protected function SQLToScalar($value) {return $value;} // take the result of a fetch... and make it a PHP variable
+
+	public function GetSQLExpressions()
 	{
-		// #@# bugge a mort... a suivre...
-		return array($this->Get("sql"));
-	} 
+		$aColumns = array();
+		// Note: to optimize things, the existence of the attribute is determine by the existence of one column with an empty suffix
+		$aColumns[''] = $this->Get("sql");
+		return $aColumns;
+	}
+
+	public function FromSQLToValue($aCols, $sPrefix = '')
+	{
+		$value = $this->MakeRealValue($aCols[$sPrefix.'']);
+		return $value;
+	}
+	public function GetSQLValues($value)
+	{
+		$aValues = array();
+		$aValues[$this->Get("sql")] = $this->ScalarToSQL($value);
+		return $aValues;
+	}
+
+	public function GetSQLColumns()
+	{
+		$aColumns = array();
+		$aColumns[$this->Get("sql")] = $this->GetSQLCol();
+		return $aColumns;
+	}
 
 	public function GetBasicFilterOperators()
 	{
@@ -371,7 +406,7 @@ class AttributeInteger extends AttributeDBField
 	public function GetType() {return "Integer";}
 	public function GetTypeDesc() {return "Numeric value (could be negative)";}
 	public function GetEditClass() {return "String";}
-	public function GetDBFieldType() {return "INT";}
+	protected function GetSQLCol() {return "INT";}
 	
 	public function GetBasicFilterOperators()
 	{
@@ -427,12 +462,12 @@ class AttributeInteger extends AttributeDBField
 		//return intval($proposedValue); could work as well
 		return (int)$proposedValue;
 	}
-	public function RealValueToSQLValue($value)
+	public function ScalarToSQL($value)
 	{
 		assert(is_numeric($value));
 		return $value; // supposed to be an int
 	}
-	public function SQLValueToRealValue($value)
+	public function SQLToScalar($value)
 	{
 		// Use cast (int) or intval() ?
 		return (int)$value;
@@ -461,7 +496,7 @@ class AttributeString extends AttributeDBField
 	public function GetType() {return "String";}
 	public function GetTypeDesc() {return "Alphanumeric string";}
 	public function GetEditClass() {return "String";}
-	public function GetDBFieldType() {return "VARCHAR(255)";}
+	protected function GetSQLCol() {return "VARCHAR(255)";}
 
 	public function GetBasicFilterOperators()
 	{
@@ -510,7 +545,7 @@ class AttributeString extends AttributeDBField
 		// 	throw new CoreException("Failed to change the type of '$proposedValue' to a string");
 		// }
 	}
-	public function RealValueToSQLValue($value)
+	public function ScalarToSQL($value)
 	{
 		if (!is_string($value))
 		{
@@ -518,7 +553,7 @@ class AttributeString extends AttributeDBField
 		}
 		return $value;
 	}
-	public function SQLValueToRealValue($value)
+	public function SQLToScalar($value)
 	{
 		return $value;
 	}
@@ -544,7 +579,7 @@ class AttributePassword extends AttributeString
 	}
 
 	public function GetEditClass() {return "Password";}
-	public function GetDBFieldType() {return "VARCHAR(64)";}
+	protected function GetSQLCol() {return "VARCHAR(64)";}
 }
 
 /**
@@ -562,7 +597,7 @@ class AttributeText extends AttributeString
 	public function GetType() {return "Text";}
 	public function GetTypeDesc() {return "Multiline character string";}
 	public function GetEditClass() {return "Text";}
-	public function GetDBFieldType() {return "TEXT";}
+	protected function GetSQLCol() {return "TEXT";}
 
 	public function GetAsHTML($sValue)
 	{
@@ -601,7 +636,7 @@ class AttributeEnum extends AttributeString
 	public function GetType() {return "Enum";}
 	public function GetTypeDesc() {return "List of predefined alphanumeric strings";}
 	public function GetEditClass() {return "String";}
-	public function GetDBFieldType()
+	protected function GetSQLCol()
 	{
 		$oValDef = $this->GetValuesDef();
 		if ($oValDef)
@@ -672,7 +707,7 @@ class AttributeDate extends AttributeDBField
 	public function GetType() {return "Date";}
 	public function GetTypeDesc() {return "Date and time";}
 	public function GetEditClass() {return "Date";}
-	public function GetDBFieldType() {return "TIMESTAMP";}
+	protected function GetSQLCol() {return "TIMESTAMP";}
 
 	// #@# THIS HAS TO REVISED
 	// Having null not allowed was interpreted by mySQL
@@ -768,7 +803,7 @@ class AttributeDate extends AttributeDBField
 		throw new CoreException("Invalid type for a date (found ".gettype($proposedValue)." and accepting string/int/DateTime)");
 		return null;
 	}
-	public function RealValueToSQLValue($value)
+	public function ScalarToSQL($value)
 	{
 		if (empty($value))
 		{
@@ -777,7 +812,7 @@ class AttributeDate extends AttributeDBField
 		}
 		return $value;
 	}
-	public function SQLValueToRealValue($value)
+	public function SQLToScalar($value)
 	{
 		return $value;
 	}
@@ -825,7 +860,7 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 	public function GetType() {return "Extkey";}
 	public function GetTypeDesc() {return "Link to another object";}
 	public function GetEditClass() {return "ExtKey";}
-	public function GetDBFieldType() {return "INT";}
+	protected function GetSQLCol() {return "INT";}
 
 	public function IsExternalKey($iType = EXTKEY_RELATIVE) {return true;}
 	public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->Get("targetclass");}
@@ -904,11 +939,11 @@ class AttributeExternalField extends AttributeDefinition
 	public function GetType() {return "ExtkeyField";}
 	public function GetTypeDesc() {return "Field of an object pointed to by the current object";}
 	public function GetEditClass() {return "ExtField";}
-	public function GetDBFieldType()
+	protected function GetSQLCol()
 	{
 		// throw new CoreException("external attribute: does it make any sense to request its type ?");  
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->GetDBFieldType(); 
+		return $oExtAttDef->GetSQLCol(); 
 	}
 
 	public function IsExternalKey($iType = EXTKEY_RELATIVE)
@@ -974,12 +1009,6 @@ class AttributeExternalField extends AttributeDefinition
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->GetSQLExpr(); 
 	} 
-	public function DBGetUsedFields()
-	{
-		// No field is used but the one defined in the field of the external class
-		// #@# so what ?
-		return array();
-	} 
 
 	public function GetDefaultValue()
 	{
@@ -1006,7 +1035,7 @@ class AttributeExternalField extends AttributeDefinition
 	public function GetBasicFilterSQLExpr($sOpCode, $value)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value); 
+		return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value);
 	} 
 
 	public function MakeRealValue($proposedValue)
@@ -1014,17 +1043,29 @@ class AttributeExternalField extends AttributeDefinition
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->MakeRealValue($proposedValue);
 	}
-	public function RealValueToSQLValue($value)
+	public function ScalarToSQL($value)
 	{
 		// This one could be used in case of filtering only
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->RealValueToSQLValue($value);
+		return $oExtAttDef->ScalarToSQL($value);
 	}
-	public function SQLValueToRealValue($value)
+	public function SQLToScalar($value)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->SQLValueToRealValue($value);
+		return $oExtAttDef->SQLToScalar($value);
 	}
+
+
+	// Do not overload GetSQLExpression here because this is handled in the joins
+	//public function GetSQLExpressions() {return array();}
+
+	// Here, we get the data...
+	public function FromSQLToValue($aCols, $sPrefix = '')
+	{
+		$oExtAttDef = $this->GetExtAttDef();
+		return $oExtAttDef->FromSQLToValue($aCols, $sPrefix);
+	}
+
 	public function GetAsHTML($value)
 	{
 		$oExtAttDef = $this->GetExtAttDef();
@@ -1078,4 +1119,123 @@ class AttributeURL extends AttributeString
 	}
 }
 
+/**
+ * Data column, consisting in TWO columns in the DB  
+ *
+ * @package     iTopORM
+ * @author      Romain Quetiez <romainquetiez@yahoo.fr>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.itop.com
+ * @since       1.0
+ * @version     $itopversion$
+ */
+class AttributeBlob extends AttributeDefinition
+{
+	static protected function ListExpectedParams()
+	{
+		return array_merge(parent::ListExpectedParams(), array("depends_on"));
+	}
+
+	public function GetType() {return "Blob";}
+	public function GetTypeDesc() {return "Document";}
+	public function GetEditClass() {return "Document";}
+	
+	public function IsDirectField() {return true;} 
+	public function IsScalar() {return true;} 
+	public function IsWritable() {return true;} 
+	public function GetDefaultValue() {return "";}
+	public function IsNullAllowed() {return false;}
+
+	// Facilitate things: allow the user to Set the value from a string
+	public function MakeRealValue($proposedValue)
+	{
+		if (!is_object($proposedValue))
+		{
+			return new ormDocument($proposedValue, 'text/plain');
+		}
+		return $proposedValue;
+	}
+
+	public function GetSQLExpressions()
+	{
+		$aColumns = array();
+		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
+		$aColumns[''] = $this->GetCode().'_mimetype';
+		$aColumns['_data'] = $this->GetCode().'_data';
+		$aColumns['_filename'] = $this->GetCode().'_filename';
+		return $aColumns;
+	}
+
+	public function FromSQLToValue($aCols, $sPrefix = '')
+	{
+		if (!isset($aCols[$sPrefix]))
+		{
+			$sAvailable = implode(', ', array_keys($aCols));
+			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
+		} 
+		$sMimeType = $aCols[$sPrefix];
+
+		if (!isset($aCols[$sPrefix.'_data'])) 
+		{
+			$sAvailable = implode(', ', array_keys($aCols));
+			throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}");
+		} 
+		$data = $aCols[$sPrefix.'_data'];
+
+		if (!isset($aCols[$sPrefix.'_filename'])) 
+		{
+			$sAvailable = implode(', ', array_keys($aCols));
+			throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}");
+		} 
+		$sFileName = $aCols[$sPrefix.'_filename'];
+
+		$value = new ormDocument($data, $sMimeType, $sFileName);
+		return $value;
+	}
+
+	public function GetSQLValues($value)
+	{
+		// #@# Optimization: do not load blobs anytime
+		//     As per mySQL doc, selecting blob columns will prevent mySQL from
+		//     using memory in case a temporary table has to be created
+		//     (temporary tables created on disk)
+		//     We will have to remove the blobs from the list of attributes when doing the select
+		//     then the use of Get() should finalize the load
+		$aValues = array();
+		$aValues[$this->GetCode().'_data'] = $value->GetData();
+		$aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType();
+		$aValues[$this->GetCode().'_filename'] = $value->GetFileName();
+		return $aValues;
+	}
+
+	public function GetSQLColumns()
+	{
+		$aColumns = array();
+		$aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb)
+		$aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)';
+		$aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)';
+		return $aColumns;
+	}
+
+	public function GetBasicFilterOperators()
+	{
+		return array();
+	}
+	public function GetBasicFilterLooseOperator()
+	{
+		return '=';
+	}
+
+	public function GetBasicFilterSQLExpr($sOpCode, $value)
+	{
+		return 'true';
+	} 
+
+	public function GetAsHTML($value)
+	{
+		return $value->GetAsHTML();
+	}
+}
+
+
 ?>

+ 113 - 3
core/cmdbchangeop.class.inc.php

@@ -145,7 +145,7 @@ class CMDBChangeOpDelete extends CMDBChangeOp
 
 
 /**
- * Record the modification of an attribute 
+ * Record the modification of an attribute (abstract)
  *
  * @package     iTopORM
  * @author      Romain Quetiez <romainquetiez@yahoo.fr>
@@ -175,11 +175,50 @@ class CMDBChangeOpSetAttribute extends CMDBChangeOp
 		MetaModel::Init_Params($aParams);
 		MetaModel::Init_InheritAttributes();
 		MetaModel::Init_AddAttribute(new AttributeString("attcode", array("label"=>"Attribute", "description"=>"code of the modified property", "allowed_values"=>null, "sql"=>"attcode", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
+
+		MetaModel::Init_InheritFilters();
+		MetaModel::Init_AddFilterFromAttribute("attcode");
+		
+		// Display lists
+		MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+		MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+	}
+}
+
+/**
+ * Record the modification of a scalar attribute 
+ *
+ * @package     iTopORM
+ * @author      Romain Quetiez <romainquetiez@yahoo.fr>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.itop.com
+ * @since       1.0
+ * @version     $itopversion$
+ */
+class CMDBChangeOpSetAttributeScalar extends CMDBChangeOpSetAttribute
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "core/cmdb",
+			"name" => "property change",
+			"description" => "Object scalar properties change tracking",
+			"key_type" => "",
+			"key_label" => "",
+			"name_attcode" => "change",
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "priv_changeop_setatt_scalar",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "",
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_InheritAttributes();
 		MetaModel::Init_AddAttribute(new AttributeString("oldvalue", array("label"=>"Previous value", "description"=>"previous value of the attribute", "allowed_values"=>null, "sql"=>"oldvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("newvalue", array("label"=>"New value", "description"=>"new value of the attribute", "allowed_values"=>null, "sql"=>"newvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
 
 		MetaModel::Init_InheritFilters();
-		MetaModel::Init_AddFilterFromAttribute("attcode");
 		MetaModel::Init_AddFilterFromAttribute("oldvalue");
 		MetaModel::Init_AddFilterFromAttribute("newvalue");
 		
@@ -235,13 +274,84 @@ class CMDBChangeOpSetAttribute extends CMDBChangeOp
 				$sTo = MetaModel::GetHyperLink($sTargetClass, $sNewValue);
 				$sResult = "$sAttName set to $sTo (previous: $sFrom)";
 			}
+			elseif ($oAttDef instanceOf AttributeBlob)
+			{
+				$sResult = "#@# Issue... found an attribute for which other type of tracking should be made";
+			}
 			else
 			{
-				$sResult = "$sAttName set too $sNewValue (previous value: $sOldValue)";
+				$sResult = "$sAttName set to $sNewValue (previous value: $sOldValue)";
 			}
 		}
 		return $sResult;
 	}
 }
 
+/**
+ * Record the modification of a blob
+ *
+ * @package     iTopORM
+ * @author      Romain Quetiez <romainquetiez@yahoo.fr>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.itop.com
+ * @since       1.0
+ * @version     $itopversion$
+ */
+class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "core/cmdb",
+			"name" => "object data change",
+			"description" => "Object data change tracking",
+			"key_type" => "",
+			"key_label" => "",
+			"name_attcode" => "change",
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "priv_changeop_setatt_data",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "",
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_InheritAttributes();
+		MetaModel::Init_AddAttribute(new AttributeBlob("prevdata", array("label"=>"Previous data", "description"=>"previous contents of the attribute", "depends_on"=>array())));
+
+		MetaModel::Init_InheritFilters();
+		
+		// Display lists
+		MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+		MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+	}
+	
+	/**
+	 * Describe (as a text string) the modifications corresponding to this change
+	 */	 
+	public function GetDescription()
+	{
+		// Temporary, until we change the options of GetDescription() -needs a more global revision
+		$bIsHtml = true;
+		
+		$sResult = '';
+		$oTargetObjectClass = $this->Get('objclass');
+		$oTargetObjectKey = $this->Get('objkey');
+		$oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+		$oTargetSearch->AddCondition('id', $oTargetObjectKey);
+
+		$oMonoObjectSet = new DBObjectSet($oTargetSearch);
+		if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+			$sAttName = $oAttDef->GetLabel();
+			$oPrevDoc = $this->Get('prevdata');
+			$sDocView = $oPrevDoc->GetAsHtml();
+			$sResult = "$sAttName changed (previous value: $sDocView)";
+		}
+		return $sResult;
+	}
+}
+
+
 ?>

+ 38 - 11
core/cmdbobject.class.inc.php

@@ -162,22 +162,49 @@ abstract class CMDBObject extends DBObject
 		{
 			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 			if ($oAttDef->IsLinkSet()) continue; // #@# temporary
-			$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttribute");
-			$oMyChangeOp->Set("change", $oChange->GetKey());
-			$oMyChangeOp->Set("objclass", get_class($this));
-			$oMyChangeOp->Set("objkey", $this->GetKey());
-			$oMyChangeOp->Set("attcode", $sAttCode);
-			if (array_key_exists($sAttCode, $aOrigValues))
+
+			if ($oAttDef instanceOf AttributeBlob)
 			{
-				$sOriginalValue = $aOrigValues[$sAttCode];
+				// Data blobs
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob");
+				$oMyChangeOp->Set("change", $oChange->GetKey());
+				$oMyChangeOp->Set("objclass", get_class($this));
+				$oMyChangeOp->Set("objkey", $this->GetKey());
+				$oMyChangeOp->Set("attcode", $sAttCode);
+
+				if (array_key_exists($sAttCode, $aOrigValues))
+				{
+					$original = $aOrigValues[$sAttCode];
+				}
+				else
+				{
+					$original = new ormDocument();
+				}
+				$oMyChangeOp->Set("prevdata", $original);
+				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
 			else
 			{
-				$sOriginalValue = 'undefined';
+				// Scalars
+				//
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+				$oMyChangeOp->Set("change", $oChange->GetKey());
+				$oMyChangeOp->Set("objclass", get_class($this));
+				$oMyChangeOp->Set("objkey", $this->GetKey());
+				$oMyChangeOp->Set("attcode", $sAttCode);
+
+				if (array_key_exists($sAttCode, $aOrigValues))
+				{
+					$sOriginalValue = $aOrigValues[$sAttCode];
+				}
+				else
+				{
+					$sOriginalValue = 'undefined';
+				}
+				$oMyChangeOp->Set("oldvalue", $sOriginalValue);
+				$oMyChangeOp->Set("newvalue", $value);
+				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
-			$oMyChangeOp->Set("oldvalue", $sOriginalValue);
-			$oMyChangeOp->Set("newvalue", $value);
-			$iId = $oMyChangeOp->DBInsertNoReload();
 		}
 	}
 

+ 12 - 8
core/dbobject.class.php

@@ -207,11 +207,14 @@ abstract class DBObject
 			// Skip links (could not be loaded by the mean of this query)
 			if ($oAttDef->IsLinkSet()) continue;
 
-			if (array_key_exists($sAttCode, $aRow))
+			// Note: we assume that, for a given attribute, if it can be loaded,
+			// then one column will be found with an empty suffix, the others have a suffix
+			if (isset($aRow[$sAttCode]))
 			{
-				$sValue = $oAttDef->SQLValueToRealValue($aRow[$sAttCode]);
-				$this->m_aCurrValues[$sAttCode] = $sValue;
-				$this->m_aOrigValues[$sAttCode] = $sValue;
+				$value = $oAttDef->FromSQLToValue($aRow, $sAttCode);
+
+				$this->m_aCurrValues[$sAttCode] = $value;
+				$this->m_aOrigValues[$sAttCode] = $value;
 				$this->m_aLoadedAtt[$sAttCode] = true;
 			}
 			else
@@ -613,11 +616,12 @@ abstract class DBObject
 		{
 			// Skip this attribute if not defined in this table
 			if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue;
-			if ($oAttDef->IsDirectField())
+			$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
+			foreach($aAttColumns as $sColumn => $sValue)
 			{
-				$aFieldsToWrite[] = $oAttDef->GetSQLExpr(); 
-				$aValuesToWrite[] = CMDBSource::Quote($oAttDef->RealValueToSQLValue($this->m_aCurrValues[$sAttCode]));
-			} 
+				$aFieldsToWrite[] = $sColumn; 
+				$aValuesToWrite[] = CMDBSource::Quote($sValue);
+			}
 		}
 
 		if (count($aValuesToWrite) == 0) return false;

+ 7 - 35
core/filterdef.class.inc.php

@@ -77,8 +77,7 @@ abstract class FilterDefinition
 	abstract public function GetOperators();
 	// returns an opcode
 	abstract public function GetLooseOperator();
-	abstract public function GetFilterSQLExpr($sOpCode, $value);
-	abstract public function TemporaryGetSQLCol();
+	abstract public function GetSQLExpressions();
 
 	// Wrapper - no need for overloading this one
 	public function GetOpDescription($sOpCode)
@@ -137,32 +136,11 @@ class FilterPrivateKey extends FilterDefinition
 		return "IN";
 	}
 
-	public function GetFilterSQLExpr($sOpCode, $value)
+	public function GetSQLExpressions()
 	{
-		$sFieldName = $this->Get("id_field");
-		// #@# not obliged to quote... these are numbers !!!
-		$sQValue = CMDBSource::Quote($value);
-		switch($sOpCode)
-		{
-			case "IN":
-				if (!is_array($sQValue)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')");
-				return "$sFieldName IN (".implode(", ", $sQValue).")"; 
-
-			case "NOTIN":
-				if (!is_array($sQValue)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')");
-				return "$sFieldName NOT IN (".implode(", ", $sQValue).")"; 
-
-			case "!=":
-				return $sFieldName." != ".$sQValue;
-
-			case "=":
-			default:
-				return $sFieldName." = ".$sQValue;
-		}
-	}
-	public function TemporaryGetSQLCol()
-	{
-		return $this->Get("id_field");
+		return array(
+			'' => $this->Get("id_field"),
+		);
 	}
 }
 
@@ -228,16 +206,10 @@ class FilterFromAttribute extends FilterDefinition
 		return $oAttDef->GetBasicFilterLooseOperator();
 	}
 
-	public function GetFilterSQLExpr($sOpCode, $value)
-	{
-		$oAttDef = $this->Get("refattribute");
-		return $oAttDef->GetBasicFilterSQLExpr($sOpCode, $value);
-	}
-
-	public function TemporaryGetSQLCol()
+	public function GetSQLExpressions()
 	{
 		$oAttDef = $this->Get("refattribute");
-		return $oAttDef->GetSQLExpr();
+		return $oAttDef->GetSQLExpressions();
 	}
 }
 

+ 23 - 13
core/metamodel.class.php

@@ -1560,15 +1560,17 @@ abstract class MetaModel
 			if (self::$m_aAttribOrigins[$sTargetClass][$sAttCode] != $sTableClass) continue;
 
 			// Skip this attribute if not writable (means that it does not correspond 
-			if (count($oAttDef->DBGetUsedFields()) == 0) continue;
+			if (count($oAttDef->GetSQLExpressions()) == 0) continue;
 
 			// Update...
 			//
 			if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
 			{
 				assert ($oAttDef->IsDirectField());
-				// Later, we'll have to use $oAttDef->GetDBField();
-				$aUpdateValues[$oAttDef->GetSQLExpr()] = $oAttDef->RealValueToSQLValue($aValues[$sAttCode]);
+				foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
+				{
+					$aUpdateValues[$sColumn] = $sValue;
+				}
 			}
 
 			// Select...
@@ -1585,7 +1587,10 @@ abstract class MetaModel
 			{
 				// standard field, or external key
 				// add it to the output
-				$aSelect[$sAttAlias] = new FieldExpression($oAttDef->GetSQLExpr(), $sTableAlias);
+				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
+				{
+					$aSelect[$sAttAlias.$sColId] = new FieldExpression($sSQLExpr, $sTableAlias); 
+				}
 			}
 		}
 
@@ -1597,8 +1602,12 @@ abstract class MetaModel
 			if (self::$m_aFilterOrigins[$sTargetClass][$sFltCode] != $sTableClass) continue;
 
 			// #@# todo - aller plus loin... a savoir que la table de translation doit contenir une "Expression"
-			// non-sens: $aTranslation[$sTargetAlias][$sFltCode] = array($sTableAlias, $oFltAtt->GetFilterSQLExpr(opcode, operand));
-			$aTranslation[$sTargetAlias][$sFltCode] = array($sTableAlias, $oFltAtt->TemporaryGetSQLCol());
+			foreach($oFltAtt->GetSQLExpressions() as $sColID => $sFltExpr)
+			{
+				// Note: I did not test it with filters relying on several expressions...
+				//       as long as sColdID is empty, this is working, otherwise... ?
+				$aTranslation[$sTargetAlias][$sFltCode.$sColID] = array($sTableAlias, $sFltExpr);
+			}
 		}
 
 		// #@# todo - See what a full text search condition should be
@@ -1660,8 +1669,10 @@ abstract class MetaModel
 					}
 					// Translate mainclass.extfield => remoteclassalias.remotefieldcode
 					$oRemoteAttDef = self::GetAttributeDef($sKeyClass, $sExtAttCode);
-					$sRemoteAttExpr = $oRemoteAttDef->GetSQLExpr(); 
-					$aIntermediateTranslation[$sTargetAlias][$sAttCode] = array($sKeyClassAlias, $sRemoteAttExpr);
+					foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr)
+					{
+						$aIntermediateTranslation[$sTargetAlias.$sColID][$sAttCode] = array($sKeyClassAlias, $sRemoteAttExpr);
+					}
 					//#@# debug - echo "<p>$sTargetAlias.$sAttCode to $sKeyClassAlias.$sRemoteAttExpr (class: $sKeyClass)</p>\n";
 				}
 				$oConditionTree = $oConditionTree->Translate($aIntermediateTranslation, false);
@@ -1669,7 +1680,7 @@ abstract class MetaModel
 				self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()");
 				$oSelectExtKey = self::MakeQuery($sGlobalTargetAlias, $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oExtFilter, $aExpAtts);
 
-				$sLocalKeyField = $oKeyAttDef->GetSQLExpr();
+				$sLocalKeyField = current($oKeyAttDef->GetSQLExpressions()); // get the first column for an external key
 				$sExternalKeyField = self::DBGetKey($sKeyClass);
 				self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
 				if ($oKeyAttDef->IsNullAllowed())
@@ -2081,9 +2092,8 @@ abstract class MetaModel
 				// Skip this attribute if not originaly defined in this class
 				if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue;
 
-				foreach($oAttDef->DBGetUsedFields() as $sField)
+				foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
 				{
-					$sDBFieldType = $oAttDef->GetDBFieldType();
 					$sFieldSpecs = $oAttDef->IsNullAllowed() ? "$sDBFieldType NULL" : "$sDBFieldType NOT NULL";
 					if (!CMDBSource::IsField($sTable, $sField))
 					{
@@ -2298,7 +2308,7 @@ abstract class MetaModel
 					$sRemoteTable = self::DBGetTable($sRemoteClass);
 					$sRemoteKey = self::DBGetKey($sRemoteClass);
 
-					$sExtKeyField = $oAttDef->GetSQLExpr();
+					$sExtKeyField = current($oAttDef->GetSQLExpressions()); // get the first column for an external key
 
 					// Note: a class/table may have an external key on itself
 					$sSelBase = "SELECT DISTINCT maintable.`$sKeyField` AS id, maintable.`$sExtKeyField` AS extkey FROM `$sTable` AS maintable LEFT JOIN `$sRemoteTable` ON maintable.`$sExtKeyField` = `$sRemoteTable`.`$sRemoteKey`";
@@ -2344,7 +2354,7 @@ abstract class MetaModel
 					{
 						$sExpectedValues = implode(",", CMDBSource::Quote(array_keys($aAllowedValues), true));
 	
-						$sMyAttributeField = $oAttDef->GetSQLExpr();
+						$sMyAttributeField = current($oAttDef->GetSQLExpressions()); // get the first column for the moment
 						$sDefaultValue = $oAttDef->GetDefaultValue();
 						$sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE maintable.`$sMyAttributeField` NOT IN ($sExpectedValues)";
 						self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record having a column ('<em>$sAttCode</em>') with an unexpected value", $sMyAttributeField, CMDBSource::Quote($sDefaultValue), $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);

+ 78 - 0
core/ormdocument.class.inc.php

@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * ormDocument
+ * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob 
+ *
+ * @package     tbd
+ * @author      Romain Quetiez <romainquetiez@yahoo.fr>
+ * @author      Denis Flaven <denisflave@free.fr>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.itop.com
+ * @since       1.0
+ */
+
+class ormDocument
+{
+	protected $m_data;
+	protected $m_sMimeType;
+	protected $m_sFileName;
+	
+	/**
+	 * Constructor
+	 */
+	public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '')
+	{
+		$this->m_data = $data;
+		$this->m_sMimeType = $sMimeType;
+		$this->m_sFileName = $sFileName;
+	}
+
+	public function __toString()
+	{
+		return MyHelpers::beautifulstr($this->m_data, 100, true);
+	}
+
+	public function GetMimeType()
+	{
+		return $this->m_sMimeType;
+	}
+	public function GetMainMimeType()
+	{
+		$iSeparatorPos = strpos($this->m_sMimeType, '/');
+		if ($iSeparatorPos > 0)
+		{
+			return substr($this->m_sMimeType, 0, $iSeparatorPos);
+		}
+		return $this->m_sMimeType;
+	}
+
+	public function GetData()
+	{
+		return $this->m_data;
+	}
+
+	public function GetFileName()
+	{
+		return $this->m_sFileName;
+	}
+
+	public function GetAsHTML()
+	{
+		$data = $this->GetData();
+
+		switch ($this->GetMainMimeType())
+		{
+		case 'text':
+			return "<pre>".htmlentities(MyHelpers::beautifulstr($data, 1000, true))."</pre>\n";
+
+		case 'application':
+			return "binary data for ".$this->GetMimeType().', size: '.strlen($data).' byte(s).';
+
+		case 'html':
+		default:
+			return "<div>".htmlentities(MyHelpers::beautifulstr($data, 1000, true))."</div>\n";
+		}
+	}
+}
+?>

+ 10 - 3
pages/schema.php

@@ -338,13 +338,20 @@ function DisplayClassDetails($oPage, $sClass)
 		$sAllowedValues = "";
 		$oAllowedValuesDef = $oAttDef->GetValuesDef();
 		$sMoreInfo = "";
-		if (is_subclass_of($oAttDef, 'AttributeDBFieldVoid'))
+
+		$aCols = array();
+		foreach($oAttDef->GetSQLColumns() as $sCol => $sFieldDesc)
+		{
+			$aCols[] = "$sCol: $sFieldDesc";
+		}
+		if (count($aCols) > 0)
 		{
+			$sCols = implode(', ', $aCols);
+	
 			$aMoreInfo = array();
-			$aMoreInfo[] = "Column: <em>".$oAttDef->GetSQLExpr()."</em>";
+			$aMoreInfo[] = "Column(s): <em>$sCols</em>";
 			$aMoreInfo[] = "Default: '".$oAttDef->GetDefaultValue()."'";
 			$aMoreInfo[] = $oAttDef->IsNullAllowed() ? "Null allowed" : "Null NOT allowed";
-			//$aMoreInfo[] = $oAttDef->DBGetUsedFields();
 			$sMoreInfo .= implode(', ', $aMoreInfo);
 		}