Explorar o código

- Fixed bug with change tracking of TEXT attributes
- Log any email notification (successful or not)
- Class selection always made through a Combo box
- Automatic creation of indexes for external keys
- New trigger: on object creation
- Application log: added a status string
- Added documentation to the WSDL (+ anyType replaced by string)
- SOAP: improved handling of enumeration attributes
- SOAP: returned logs to mention the name of the parameter as advertised in the WSDL file (different than the name of the attribute in Itop)
- Finalized SOAP tests
- Added a SOAP client example

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@246 a333f486-631f-4898-b8df-5754b55c2be0

romainq %!s(int64=15) %!d(string=hai) anos
pai
achega
87047aabf3

+ 1 - 1
addons/userrights/userrightsprofile.class.inc.php

@@ -307,7 +307,7 @@ class URP_Dimensions extends UserRightsBaseClass
 		//MetaModel::Init_InheritAttributes();
 		MetaModel::Init_AddAttribute(new AttributeString("name", array("label"=>"Name", "description"=>"label", "allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("description", array("label"=>"Description", "description"=>"one line description", "allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeString("type", array("label"=>"Type", "description"=>"class name or data type (projection unit)", "allowed_values"=>new ValueSetEnumClasses('bizmodel', 'String,Integer'), "sql"=>"type", "default_value"=>'String', "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeClass("type", array("label"=>"Type", "description"=>"class name or data type (projection unit)", "class_category"=>"bizmodel", "more_values"=>"String,Integer", "sql"=>"type", "default_value"=>'String', "is_null_allowed"=>false, "depends_on"=>array())));
 
 		//MetaModel::Init_InheritFilters();
 		MetaModel::Init_AddFilterFromAttribute("name");

+ 1 - 1
application/iotask.class.inc.php

@@ -31,7 +31,7 @@ class InputOutputTask extends cmdbAbstractObject
 		MetaModel::Init_AddAttribute(new AttributeEnum("source_type", array("label"=>"Source Type", "description"=>"Type of data source", "allowed_values"=>new ValueSetEnum('File, Database, Web Service'), "sql"=>"source_type", "default_value"=>"File", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("source_subtype", array("label"=>"Source Subtype", "description"=>"Subtype of Data Source", "allowed_values"=>new ValueSetEnum('Oracle, MySQL, Postgress, MSSQL, SOAP, HTTP-Get, HTTP-Post, XML/RPC, CSV, XML, Excel'), "sql"=>"source_subtype", "default_value"=>"CSV", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("source_path", array("label"=>"Source Path", "description"=>"Path to the icon o the menu", "allowed_values"=>null, "sql"=>"source_path", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeEnum("objects_class", array("label"=>"Objects Class", "description"=>"Class of the objects processed by this task", "allowed_values"=>new ValueSetEnumClasses(), "sql"=>"objects_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeClass("objects_class", array("label"=>"Objects Class", "description"=>"Class of the objects processed by this task", "class_category"=>"", "more_values"=>"", "sql"=>"objects_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("test_mode", array("label"=>"Test Mode", "description"=>"If set to 'Yes' the modifications are not applied", "allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"test_mode", "default_value"=>'No', "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("verbose_mode", array("label"=>"Verbose Mode", "description"=>"If set to 'Yes' extra debug information is added to the log", "allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"verbose_mode", "default_value" => 'No', "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("options", array("label"=>"Options", "description"=>"Reconciliation options", "allowed_values"=>new ValueSetEnum('Full, Update Only, Creation Only'), "sql"=>"options", "default_value"=> 'Full', "is_null_allowed"=>true, "depends_on"=>array())));

+ 16 - 13
core/action.class.inc.php

@@ -215,25 +215,28 @@ class ActionEmail extends ActionNotification
 			$sHeaders .= "Bcc: $sBCC\r\n";
 		}
 
-		// Mail it
+		$oLog = new EventNotificationEmail();
 		if (mail($sTo, $sSubject, $sBody, $sHeaders))
 		{
-			$oLog = new EventNotificationEmail();
-			$oLog->Set('userinfo', UserRights::GetUser());
-			$oLog->Set('trigger_id', $oTrigger->GetKey());
-			$oLog->Set('action_id', $this->GetKey());
-			$oLog->Set('object_id', $aContextArgs['this->id']);
-			$oLog->Set('to', $sTo);
-			$oLog->Set('cc', $sCC);
-			$oLog->Set('bcc', $sBCC);
-			$oLog->Set('subject', $sSubject);
-			$oLog->Set('body', $sBody);
-			$oLog->DBInsertNoReload();
+			$oLog->Set('message', 'Notification sent');
 		}
 		else
 		{
-			throw new CoreException('mail not sent', array('action'=>$this->GetKey(), 'to'=>$sTo, 'subject'=>$sSubject, 'headers'=>$sHeaders));
+			$aLastError = error_get_last();
+			$oLog->Set('message', 'Mail could not be sent: '.$aLastError['message']);
+			//throw new CoreException('mail not sent', array('action'=>$this->GetKey(), 'to'=>$sTo, 'subject'=>$sSubject, 'headers'=>$sHeaders));
 		}
+
+		$oLog->Set('userinfo', UserRights::GetUser());
+		$oLog->Set('trigger_id', $oTrigger->GetKey());
+		$oLog->Set('action_id', $this->GetKey());
+		$oLog->Set('object_id', $aContextArgs['this->id']);
+		$oLog->Set('to', $sTo);
+		$oLog->Set('cc', $sCC);
+		$oLog->Set('bcc', $sBCC);
+		$oLog->Set('subject', $sSubject);
+		$oLog->Set('body', $sBody);
+		$oLog->DBInsertNoReload();
 	}
 }
 ?>

+ 24 - 0
core/attributedef.class.inc.php

@@ -600,6 +600,30 @@ class AttributeString extends AttributeDBField
 	}
 }
 
+/**
+ * An attibute that matches an object class 
+ *
+ * @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 AttributeClass extends AttributeString
+{
+	static protected function ListExpectedParams()
+	{
+		return array_merge(parent::ListExpectedParams(), array("class_category", "more_values"));
+	}
+
+	public function __construct($sCode, $aParams)
+	{
+		$this->m_sCode = $sCode;
+		$aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']);
+		parent::__construct($sCode, $aParams);
+	}
+}
 
 /**
  * Map a varchar column (size < ?) to an attribute that must never be shown to the user 

+ 68 - 2
core/cmdbchangeop.class.inc.php

@@ -304,8 +304,8 @@ class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute
 		$aParams = array
 		(
 			"category" => "core/cmdb",
-			"name" => "object data change",
-			"description" => "Object data change tracking",
+			"name" => "data change",
+			"description" => "data change tracking",
 			"key_type" => "",
 			"key_label" => "",
 			"name_attcode" => "change",
@@ -357,5 +357,71 @@ class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute
 	}
 }
 
+/**
+ * Record the modification of a multiline string (text)
+ *
+ * @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 CMDBChangeOpSetAttributeText extends CMDBChangeOpSetAttribute
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "core/cmdb",
+			"name" => "text change",
+			"description" => "text change tracking",
+			"key_type" => "",
+			"key_label" => "",
+			"name_attcode" => "change",
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "priv_changeop_setatt_text",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "",
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_InheritAttributes();
+		MetaModel::Init_AddAttribute(new AttributeText("prevdata", array("label"=>"Previous data", "description"=>"previous contents of the attribute", "allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "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();
+			$sTextView = '<div>'.$this->GetAsHtml('prevdata').'</div>';
+
+			//$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
+			$sResult = "$sAttName changed, previous value: $sTextView";
+		}
+		return $sResult;
+	}
+}
 
 ?>

+ 20 - 0
core/cmdbobject.class.inc.php

@@ -193,6 +193,26 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("prevdata", $original);
 				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
+			elseif ($oAttDef instanceOf AttributeText)
+			{
+				// Data blobs
+				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText");
+				$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 = null;
+				}
+				$oMyChangeOp->Set("prevdata", $original);
+				$iId = $oMyChangeOp->DBInsertNoReload();
+			}
 			else
 			{
 				// Scalars

+ 10 - 0
core/cmdbsource.class.inc.php

@@ -342,6 +342,16 @@ class CMDBSource
 		return (strtolower($aFieldData["Null"]) == "yes");
 	}
 
+	public static function HasIndex($sTable, $sField)
+	{
+		$aTableInfo = self::GetTableInfo($sTable);
+		if (empty($aTableInfo)) return false;
+		if (!array_key_exists($sField, $aTableInfo["Fields"])) return false;
+		$aFieldData = $aTableInfo["Fields"][$sField];
+		// $aFieldData could be 'PRI' for the primary key, or 'MUL', or ?
+		return (strlen($aFieldData["Key"]) > 0);
+	}
+
 	// Returns an array of (fieldname => array of field info)
 	public static function GetTableFieldsList($sTable)
 	{

+ 25 - 2
core/dbobject.class.php

@@ -497,6 +497,17 @@ abstract class DBObject
 				}
 			}
 		}
+		elseif ($oAtt->IsWritable() && $oAtt->IsScalar())
+		{
+			$aValues = $oAtt->GetAllowedValues();
+			if (count($aValues) > 0)
+			{
+				if (!array_key_exists($toCheck, $aValues))
+				{
+					return false;
+				}
+			}
+		}
 		return true;
 	}
 	
@@ -690,9 +701,19 @@ abstract class DBObject
 		}
 
 		$this->DBWriteLinks();
-
-		// Reload to update the external attributes
 		$this->m_bIsInDB = true;
+
+		// Activate any existing trigger 
+		$sClass = get_class($this);
+		$oSet = new DBObjectSet(new DBObjectSearch('TriggerOnObjectCreate'));
+		while ($oTrigger = $oSet->Fetch())
+		{
+			if (MetaModel::IsParentClass($oTrigger->Get('target_class'), $sClass))
+			{
+				$oTrigger->DoActivate($this->ToArgs('this'));
+			}
+		}
+
 		return $this->m_iKey;
 	}
 
@@ -849,6 +870,8 @@ abstract class DBObject
 		$aScalarArgs = array();
 		$aScalarArgs[$sArgName] = $this->GetKey();
 		$aScalarArgs[$sArgName.'->id'] = $this->GetKey();
+		$aScalarArgs[$sArgName.'->hyperlink()'] = $this->GetHyperlink();
+		$aScalarArgs[$sArgName.'->name()'] = $this->GetName();
 	
 		$sClass = get_class($this);
 		foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)

+ 4 - 2
core/event.class.inc.php

@@ -32,15 +32,17 @@ class Event extends cmdbAbstractObject
 		);
 		MetaModel::Init_Params($aParams);
 		//MetaModel::Init_InheritAttributes();
+		MetaModel::Init_AddAttribute(new AttributeString("message", array("label"=>"message", "description"=>"short description of the event", "allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeDate("date", array("label"=>"date", "description"=>"date and time at which the changes have been recorded", "allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("label"=>"user info", "description"=>"identification of the user that was doing the action that triggered this event", "allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
 
 		//MetaModel::Init_InheritFilters();
+		MetaModel::Init_AddFilterFromAttribute("message");
 		MetaModel::Init_AddFilterFromAttribute("date");
 
 		// Display lists
-		MetaModel::Init_SetZListItems('details', array('finalclass', 'date', 'userinfo')); // Attributes to be displayed for the complete details
-		MetaModel::Init_SetZListItems('list', array('finalclass', 'date')); // Attributes to be displayed for a list
+		MetaModel::Init_SetZListItems('details', array('message', 'date', 'userinfo')); // Attributes to be displayed for the complete details
+		MetaModel::Init_SetZListItems('list', array('date', 'finalclass', 'message')); // Attributes to be displayed for a list
 		// Search criteria
 //		MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
 //		MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form

+ 19 - 7
core/metamodel.class.php

@@ -2092,18 +2092,30 @@ abstract class MetaModel
 					{
 						$aErrors[$sClass][] = "field '$sField' could not be found in table '$sTable'";
 						$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD `$sField` $sFieldSpecs";
+						if ($oAttDef->IsExternalKey())
+						{
+							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
+						}
 					}
-					elseif ($oAttDef->IsNullAllowed() != CMDBSource::IsNullAllowed($sTable, $sField))
+					else
 					{
-						if ($oAttDef->IsNullAllowed())
+						if ($oAttDef->IsNullAllowed() != CMDBSource::IsNullAllowed($sTable, $sField))
 						{
-							$aErrors[$sClass][] = "field '$sField' in table '$sTable' could be NULL";
-							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
+							if ($oAttDef->IsNullAllowed())
+							{
+								$aErrors[$sClass][] = "field '$sField' in table '$sTable' could be NULL";
+								$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
+							}
+							else
+							{
+								$aErrors[$sClass][] = "field '$sField' in table '$sTable' could NOT be NULL";
+								$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
+							}
 						}
-						else
+						if ($oAttDef->IsExternalKey() && !CMDBSource::HasIndex($sTable, $sField))
 						{
-							$aErrors[$sClass][] = "field '$sField' in table '$sTable' could NOT be NULL";
-							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
+							$aErrors[$sClass][] = "Foreign key '$sField' in table '$sTable' should have an index";
+							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
 						}
 					}
 				}

+ 36 - 2
core/trigger.class.inc.php

@@ -82,7 +82,7 @@ class TriggerOnStateChange extends Trigger
 		);
 		MetaModel::Init_Params($aParams);
 		MetaModel::Init_InheritAttributes();
-		MetaModel::Init_AddAttribute(new AttributeString("target_class", array("label"=>"Target class", "description"=>"label", "allowed_values"=>null, "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("label"=>"Target class", "description"=>"label", "class_category"=>"bizmodel", "more_values"=>null, "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeString("state", array("label"=>"State", "description"=>"label", "allowed_values"=>null, "sql"=>"state", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));	
 
 		MetaModel::Init_InheritFilters();
@@ -96,7 +96,6 @@ class TriggerOnStateChange extends Trigger
 //		MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
 //		MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
 	}
-
 }
 
 class TriggerOnStateEnter extends TriggerOnStateChange
@@ -165,6 +164,41 @@ class TriggerOnStateLeave extends TriggerOnStateChange
 	}
 }
 
+class TriggerOnObjectCreate extends Trigger
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "core/cmdb",
+			"name" => "Trigger on object creation",
+			"description" => "Trigger on object creation of [a child class of] the given class",
+			"key_type" => "autoincrement",
+			"key_label" => "",
+			"name_attcode" => "",
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "priv_trigger_onobjcreate",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "",
+			"display_template" => "",
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_InheritAttributes();
+		MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("label"=>"Target class", "description"=>"label", "class_category"=>"bizmodel", "more_values"=>null, "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
+
+		MetaModel::Init_InheritFilters();
+		MetaModel::Init_AddFilterFromAttribute("target_class");
+
+		// Display lists
+		MetaModel::Init_SetZListItems('details', array('description', 'target_class')); // Attributes to be displayed for the complete details
+		MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
+		// Search criteria
+//		MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
+//		MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
+	}
+}
+
 class lnkTriggerAction extends cmdbAbstractObject
 {
 	public static function Init()

+ 22 - 14
core/valuesetdef.class.inc.php

@@ -128,16 +128,23 @@ class ValueSetObjects extends ValueSetDefinition
  */
 class ValueSetEnum extends ValueSetDefinition
 {
+	protected $m_values;
+
 	public function __construct($Values)
 	{
-		if (is_array($Values))
+		$this->m_values = $Values;
+	}
+
+	protected function LoadValues($aArgs)
+	{
+		if (is_array($this->m_values))
 		{
-			$aValues = $Values;
+			$aValues = $this->m_values;
 		}
 		else
 		{
 			$aValues = array();
-			foreach (explode(",", $Values) as $sVal)
+			foreach (explode(",", $this->m_values) as $sVal)
 			{
 				$sVal = trim($sVal);
 				$sKey = $sVal; 
@@ -145,10 +152,6 @@ class ValueSetEnum extends ValueSetDefinition
 			}
 		}
 		$this->m_aValues = $aValues;
-	}
-
-	protected function LoadValues($aArgs)
-	{
 		return true;
 	}
 }
@@ -166,20 +169,25 @@ class ValueSetEnum extends ValueSetDefinition
  */
 class ValueSetEnumClasses extends ValueSetEnum
 {
-	public function __construct($sCategory = '', $sAdditionalValues = '')
+	protected $m_sCategories;
+
+	public function __construct($sCategories = '', $sAdditionalValues = '')
 	{
-		// First, build it from the series of additional values
+		$this->m_sCategories = $sCategories;
 		parent::__construct($sAdditionalValues);
+	}
+
+	protected function LoadValues($aArgs)
+	{	
+		// First, get the additional values
+		parent::LoadValues($aArgs);
 
-		// Second: add the list of classes
-		foreach (MetaModel::GetClasses($sCategory) as $sClass)
+		// Then, add the classes from the category definition
+		foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass)
 		{
 			$this->m_aValues[$sClass] = MetaModel::GetName($sClass);
 		}
-	}
 
-	protected function LoadValues($aArgs)
-	{
 		return true;
 	}
 }

+ 92 - 23
pages/testlist.inc.php

@@ -582,7 +582,6 @@ class TestMyBizModel extends TestBizModel
 		$this->test_relations();
 		$this->test_linkedset();
 		$this->test_object_lifecycle();
-		return true;
 	}
 }
 
@@ -929,7 +928,7 @@ class TestBulkChangeOnFarm extends TestBizModel
 		$aRes = $oBulk->Process($oMyChange);
 		print_r($aRes);
 
-		return true;
+		return;
 
 		$oRawData = array(
 			'Mammal',
@@ -978,7 +977,6 @@ class TestFullTextSearchOnFarm extends MyFarm
 		$oSearch->AddCondition_FullText('manof');
 		//$oResultSet = new DBObjectSet($oSearch);
 		$this->search_and_show_list($oSearch);
-		return true;
 	}
 }
 
@@ -1103,7 +1101,6 @@ class TestItopEfficiency extends TestBizModel
 			$aData[] = $aValues;
 		}
 		echo MyHelpers::make_table_from_assoc_array($aData);
-		return true;
 	}
 }
 
@@ -1169,8 +1166,6 @@ class TestItopWebServices extends TestWebServices
 		{
 			$this->DoExecSingleLoad($aLoadSpec);
 		}
-
-		return true;
 	}
 }
 
@@ -1178,14 +1173,14 @@ class TestItopWebServices extends TestWebServices
 $aWebServices = array(
 	array(
 		'verb' => 'GetVersion',
-		'expected result' => true,
+		'expected result' => '0.8',
 		'explain result' => 'n/a',
 		'args' => array(),
 	),
 	array(
 		'verb' => 'CreateIncidentTicket',
 		'expected result' => true,
-		'explain result' => 'ok, but link attribute unknown',
+		'explain result' => 'link attribute unknown + a CI not found',
 		'args' => array(
 			'admin', /* sLogin */
 			'admin', /* sPassword */
@@ -1195,7 +1190,7 @@ $aWebServices = array(
 			'very grave', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 				new SOAPLinkCreationSpec(
 					'logInfra',
@@ -1207,14 +1202,38 @@ $aWebServices = array(
 					array(new SOAPSearchCondition('name', 'Router03')),
 					array(new SOAPAttributeValue('impact', 'who cares'))
 				),
+				new SOAPLinkCreationSpec(
+					'bizDevice',
+					array(new SOAPSearchCondition('name', 'thisone')),
+					array(new SOAPAttributeValue('impact', 'our lives'))
+				),
 			), /* aImpact */
 			'low' /* sSeverity */
 		),
 	),
 	array(
 		'verb' => 'CreateIncidentTicket',
-		'expected result' => true,
-		'explain result' => 'ok, but CI unknown',
+		'expected result' => false,
+		'explain result' => 'caller not specified',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Desktop', /* sType */
+			'PC burning', /* sDescription */
+			'The power supply suddenly started to warm up', /* sInitialSituation */
+			'The agent could not do his job', /* sImpact */
+			null, /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'wrong condition on CI to attach',
 		'args' => array(
 			'admin', /* sLogin */
 			'admin', /* sPassword */
@@ -1224,11 +1243,11 @@ $aWebServices = array(
 			'The agent could not do his job', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 				new SOAPLinkCreationSpec(
 					'logInfra',
-					array(new SOAPSearchCondition('id', 99999)),
+					array(new SOAPSearchCondition('dummyfiltercode', 2)),
 					array(new SOAPAttributeValue('impact', 'very critical'))
 				),
 			), /* aImpact */
@@ -1237,8 +1256,8 @@ $aWebServices = array(
 	),
 	array(
 		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'ok, no CI to attach',
+		'expected result' => true,
+		'explain result' => 'no CI to attach (empty array)',
 		'args' => array(
 			'admin', /* sLogin */
 			'admin', /* sPassword */
@@ -1248,7 +1267,7 @@ $aWebServices = array(
 			'Could not talk to my wife', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 			), /* aImpact */
 			'low' /* sSeverity */
@@ -1256,6 +1275,24 @@ $aWebServices = array(
 	),
 	array(
 		'verb' => 'CreateIncidentTicket',
+		'expected result' => true,
+		'explain result' => 'no CI to attach (null)',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			null, /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
 		'expected result' => false,
 		'explain result' => 'caller unknown',
 		'args' => array(
@@ -1267,7 +1304,7 @@ $aWebServices = array(
 			'Could not talk to my wife', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1000))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 			), /* aImpact */
 			'low' /* sSeverity */
@@ -1276,6 +1313,25 @@ $aWebServices = array(
 	array(
 		'verb' => 'CreateIncidentTicket',
 		'expected result' => false,
+		'explain result' => 'wrong values for type and severity',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'my type', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'my severity' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
 		'explain result' => 'wrong password',
 		'args' => array(
 			'admin', /* sLogin */
@@ -1286,7 +1342,7 @@ $aWebServices = array(
 			'Could not talk to my wife', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 			), /* aImpact */
 			'low' /* sSeverity */
@@ -1305,7 +1361,7 @@ $aWebServices = array(
 			'Could not talk to my wife', /* sImpact */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
 			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aWorkgroupDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
 			array(
 			), /* aImpact */
 			'low' /* sSeverity */
@@ -1321,6 +1377,8 @@ class TestSoap extends TestSoapWebService
 
 	protected function DoExecute()
 	{
+		echo "<p>Note: You may also want to try the sample SOAP client <a href=\"../webservices/itopsoap.examples.php\">itopsoap.examples.php</a></p>\n";
+
 		global $aSOAPMapping;
 
 		// this file is generated dynamically with location = here
@@ -1331,7 +1389,6 @@ class TestSoap extends TestSoapWebService
 		(
 			$sWsdlUri,
 			array(
-				//'uri' => 'http://soap-itop/',
 				'classmap' => $aSOAPMapping,
 				'trace' => 1,
 			)
@@ -1348,6 +1405,7 @@ class TestSoap extends TestSoapWebService
 		foreach ($aWebServices as $iPos => $aWebService)
 		{
 			echo "<h4>SOAP call #$iPos ".$aWebService['explain result']."</h4>\n";
+
 			try
 			{
 				$oRes = call_user_func_array(array($this->m_SoapClient, $aWebService['verb']), $aWebService['args']);
@@ -1359,7 +1417,7 @@ class TestSoap extends TestSoapWebService
 				print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
 				print "</pre>";
 				print "Response in HTML: <p>".$this->m_SoapClient->__getLastResponse()."</p>"; 
-				return false;
+				throw $e;
 			}
 
 			echo "<pre>\n";
@@ -1370,9 +1428,20 @@ class TestSoap extends TestSoapWebService
 			print "Request: \n".htmlspecialchars($this->m_SoapClient->__getLastRequest()) ."\n"; 
 			print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
 			print "</pre>";
-		} 
 
-		return true;
+			if ($oRes instanceof SOAPResult)
+			{
+				$res = $oRes->status;
+			}
+			else
+			{
+				$res = $oRes;
+			}
+			if ($res != $aWebService['expected result'])
+			{
+				throw new UnitTestException("Expecting result '{$aWebService['expected result']}', but got '$res'");
+			}
+		} 
 	}
 }
 

+ 43 - 8
webservices/itop.wsdl.tpl

@@ -1,6 +1,6 @@
 <?xml version='1.0' encoding='UTF-8'?>
 
-<!-- WSDL file originally generated by Zend Studio, then reworked manually -->
+<!-- WSDL file generated manually -->
 
 <definitions name="ITop" targetNamespace="urn:ITop" xmlns:typens="urn:ITop" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/">
 	<types>
@@ -8,9 +8,13 @@
 			<!-- Added the following import tag to pass the Eclipse validation -->
 			<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
 			<xsd:complexType name="SearchCondition">
+				<!-- <wsdl:documentation>
+					A criteria to restrict a search (strict search is performed)
+					Example: name = 'myserver.combodo.fr'
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="attcode" type="xsd:string"/>
-					<xsd:element name="value" type="xsd:anyType"/>
+					<xsd:element name="value" type="xsd:string"/> <!-- should be anyType but this one is not well supported by Eclipse -->
 				</xsd:all>
 			</xsd:complexType>
 			<xsd:complexType name="ArrayOfSearchCondition">
@@ -21,14 +25,23 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="ExternalKeySearch">
+				<!-- <wsdl:documentation>
+					Specifies [how to find] a value for an external key.
+					the class of object to search for will depend on the usage that is being made, therefore the search conditions that may be used will vary depending on the parameter that is concerned.
+					If one criteria is not relevant, then the match will not be attempted and warning will be logged (or an error if the target external key is mandatory)
+					Example: match on customer = 'Demo' and type = 'Router'
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="conditions" type="typens:ArrayOfSearchCondition"/>
 				</xsd:all>
 			</xsd:complexType>
 			<xsd:complexType name="AttributeValue">
+				<!-- <wsdl:documentation>
+					Specifies a value to set
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="attcode" type="xsd:string"/>
-					<xsd:element name="value" type="xsd:anyType"/>
+					<xsd:element name="value" type="xsd:string"/> <!-- should be anyType but this one is not well supported by Eclipse -->
 				</xsd:all>
 			</xsd:complexType>
 			<xsd:complexType name="ArrayOfAttributeValue">
@@ -39,6 +52,9 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="LinkCreationSpec">
+				<!-- <wsdl:documentation>
+					Specifies [how to match] one item to attach and what values should be set on the newly created link.
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="class" type="xsd:string"/>
 					<xsd:element name="conditions" type="typens:ArrayOfSearchCondition"/>
@@ -53,6 +69,9 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="LogMessage">
+				<!-- <wsdl:documentation>
+					An event that happened during the execution of the web service
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="text" type="xsd:string"/>
 				</xsd:all>
@@ -65,6 +84,9 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="ResultLog">
+				<!-- <wsdl:documentation>
+					A Log of events of the same category
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="messages" type="typens:ArrayOfLogMessage"/>
 				</xsd:all>
@@ -72,7 +94,7 @@
 			<xsd:complexType name="ResultData">
 				<xsd:all>
 					<xsd:element name="key" type="xsd:string"/>
-					<xsd:element name="value" type="xsd:anyType"/>
+					<xsd:element name="value" type="xsd:string"/> <!-- should be anyType but this one is not well supported by Eclipse -->
 				</xsd:all>
 			</xsd:complexType>
 			<xsd:complexType name="ArrayOfResultData">
@@ -83,6 +105,10 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="ResultMessage">
+				<!-- <wsdl:documentation>
+					Output expected, depending on the operation invoked.
+					Example: CreateIncidentTicket will return 'created' => basic information on the created ticket
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="label" type="xsd:string"/>
 					<xsd:element name="values" type="typens:ArrayOfResultData"/>
@@ -96,6 +122,12 @@
 				</xsd:complexContent>
 			</xsd:complexType>
 			<xsd:complexType name="Result">
+				<!-- <wsdl:documentation>
+					Standard result structure returned by all of the operations, excepted GetVersion (returning a string)
+					result holds returned data if the status is set to true
+					errors, warnings and infos will help in understanding what happened (unknown identifiers, object matching issues/results)
+					This resulting structure is being tracked into the application log as well.
+				</wsdl:documentation> -->
 				<xsd:all>
 					<xsd:element name="status" type="xsd:boolean"/>
 					<xsd:element name="result" type="typens:ArrayOfResultMessage"/>
@@ -129,17 +161,17 @@
 	</message>
 	<portType name="WebServicePortType">
 		<operation name="GetVersion">
-			<documentation>
+			<wsdl:documentation>
 				Get the current version of Itop
 				As this service is very simple, it is a test to get trained for more complex operations 
-			</documentation>
+			</wsdl:documentation> -->
 			<input message="typens:GetVersion"/>
 			<output message="typens:GetVersionResponse"/>
 		</operation>
 		<operation name="CreateIncidentTicket">
-			<documentation>
+			<wsdl:documentation>
 				Create a ticket, return information about reconciliation on external keys and the created ticket
-			</documentation>
+			</wsdl:documentation> -->
 			<input message="typens:CreateIncidentTicket"/>
 			<output message="typens:CreateIncidentTicketResponse"/>
 		</operation>
@@ -166,6 +198,9 @@
 		</operation>
 	</binding>
 	<service name="ITopService">
+		<wsdl:documentation>
+			ITop is the central solution for managing your IT infrastructure
+		</wsdl:documentation>
 		<port name="WebServicePort" binding="typens:WebServiceBinding">
 			<soap:address location="___SOAP_SERVER_URI___"/>
 		</port>

+ 69 - 0
webservices/itopsoap.examples.php

@@ -0,0 +1,69 @@
+<?php
+
+require_once('itopsoaptypes.class.inc.php');
+
+$sItopRoot = 'http'.(empty($_SERVER['HTTPS']) ? '' : 's').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/..';
+$sWsdlUri = $sItopRoot.'/webservices/itop.wsdl.php';
+
+ini_set("soap.wsdl_cache_enabled","0");
+$oSoapClient = new SoapClient(
+	$sWsdlUri,
+	array(
+		'trace' => 1,
+		'classmap' => $aSOAPMapping, // defined in itopsoaptypes.class.inc.php
+	)
+);
+
+try
+{
+	// The most simple service, returning a string
+	//
+	$sServerVersion = $oSoapClient->GetVersion();
+	echo "<p>GetVersion() returned <em>$sServerVersion</em></p>";
+
+	// More complex ones, returning a SOAPResult structure
+	// (run the page to know more about the returned data)
+	//
+	$oRes = $oSoapClient->CreateIncidentTicket
+	(
+		'admin', /* login */
+		'admin', /* password */
+		'Server', /* type */
+		'Email server down', /* description */
+		'HW found shutdown', /* initial situation */
+		'Email not working', /* impact */
+		null, /* caller */
+		new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'Demo'))), /* customer */
+		new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* workgroup */
+		array(
+			new SOAPLinkCreationSpec(
+				'bizDevice',
+				array(new SOAPSearchCondition('name', 'Router03')),
+				array(new SOAPAttributeValue('impact', 'root cause'))
+			),
+			new SOAPLinkCreationSpec(
+				'bizServer',
+				array(new SOAPSearchCondition('name', 'Server01')),
+				array(new SOAPAttributeValue('impact', ''))
+			),
+		), /* impact */
+		'high' /* severity */
+	);
+
+	echo "<p>CreateIncidentTicket() returned:\n";
+	echo "<pre>\n";
+	print_r($oRes);
+	echo "</pre>\n";
+	echo "</p>\n";
+}
+catch(SoapFault $e)
+{
+	echo "<h1>SoapFault Exception: {$e->getMessage()}</h1>\n"; 
+	echo "<h2>Request</h2>\n"; 
+	echo "<pre>\n"; 
+	echo htmlspecialchars($oSoapClient->__getLastRequest())."\n"; 
+	echo "</pre>"; 
+	echo "<h2>Response</h2>";
+	echo $oSoapClient->__getLastResponse()."\n";
+}
+?>

+ 99 - 37
webservices/webservices.class.inc.php

@@ -223,6 +223,14 @@ class WebServices
 	protected function LogUsage($sVerb, $oRes)
 	{
 		$oLog = new EventWebService();
+		if ($oRes->IsOk())
+		{
+			$oLog->Set('message', $sVerb.' was successfully invoked');
+		}
+		else
+		{
+			$oLog->Set('message', $sVerb.' returned errors');
+		}
 		$oLog->Set('userinfo', UserRights::GetUser());
 		$oLog->Set('verb', $sVerb);
 		$oLog->Set('result', $oRes->IsOk());
@@ -234,6 +242,29 @@ class WebServices
 	}
 
 	/**
+	 * Helper to set a scalar attribute
+	 *
+	 * @param string sAttCode
+	 * @param scalar value
+	 * @param DBObject oTargetObj
+	 * @param WebServiceResult oRes
+	 *
+	 */
+	protected function MyObjectSetScalar($sAttCode, $sParamName, $value, &$oTargetObj, &$oRes)
+	{
+		if ($oTargetObj->CheckValue($sAttCode, $value))
+		{
+			$oTargetObj->Set($sAttCode, $value);
+		}
+		else
+		{
+			$aAllowedValues = MetaModel::GetAllowedValues_att(get_class($oTargetObj), $sAttCode);
+			$sValues = implode(', ', $aAllowedValues);
+			$oRes->LogError("Parameter $sParamName: found '$value' while expecting a value in {".$sValues."}");
+		}
+	}
+
+	/**
 	 * Helper to set an external key
 	 *
 	 * @param string sAttCode
@@ -242,14 +273,28 @@ class WebServices
 	 * @param WebServiceResult oRes
 	 *
 	 */
-	protected function SetExternalKey($sAttCode, $aExtKeyDesc, &$oTargetObj, &$oRes)
+	protected function MyObjectSetExternalKey($sAttCode, $sParamName, $aExtKeyDesc, &$oTargetObj, &$oRes)
 	{
 		$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
 
 		$bIsMandatory = !$oExtKey->IsNullAllowed();
+
+		if (is_null($aExtKeyDesc))
+		{
+			if ($bIsMandatory)
+			{
+				$oRes->LogError("Parameter $sParamName: found null for a mandatory key");
+			}
+			else
+			{
+				// skip silently
+				return;
+			}
+		}
+
 		if (count($aExtKeyDesc) == 0)
 		{
-			$oRes->LogIssue("Ext key $sAttCode: no data was given to give a value to the key", $bIsMandatory);
+			$oRes->LogIssue("Parameter $sParamName: no search condition has been specified", $bIsMandatory);
 			return;
 		}
 
@@ -259,7 +304,8 @@ class WebServices
 		{
 			if (!MetaModel::IsValidFilterCode($sKeyClass, $sForeignAttCode))
 			{
-				$sMsg = "Ext key $sAttCode: '$sForeignAttCode' is not a valid filter code for class '$sKeyClass'";
+				$aCodes = array_keys(MetaModel::GetClassFilterDefs($sKeyClass));
+				$sMsg = "Parameter $sParamName: '$sForeignAttCode' is not a valid filter code for class '$sKeyClass', expecting a value in {".implode(', ', $aCodes)."}";
 				$oRes->LogIssue($sMsg, $bIsMandatory);
 			}
 			// The foreign attribute is one of our reconciliation key
@@ -269,7 +315,7 @@ class WebServices
 		switch($oExtObjects->Count())
 		{
 		case 0:
-			$sMsg = "External key $sAttCode could not be found (searched: '".$oReconFilter->ToOQL()."')";
+			$sMsg = "Parameter $sParamName: no match (searched: '".$oReconFilter->ToOQL()."')";
 			$oRes->LogIssue($sMsg, $bIsMandatory);
 			break;
 		case 1:
@@ -280,11 +326,11 @@ class WebServices
 			// Report it (no need to report if the object already had this value
 			if (array_key_exists($sAttCode, $oTargetObj->ListChanges()))
 			{
-				$oRes->LogInfo("$sAttCode has been set to ".$oForeignObj->GetKey());
+				$oRes->LogInfo("Parameter $sParamName: found match ".get_class($oForeignObj)."::".$oForeignObj->GetKey()." '".$oForeignObj->GetName()."'");
 			}
 			break;
 		default:
-			$sMsg = "Found ".$oExtObjects->Count()." matches for external key $sAttCode (searched: '".$oReconFilter->ToOQL()."')";
+			$sMsg = "Parameter $sParamName: Found ".$oExtObjects->Count()." matches (searched: '".$oReconFilter->ToOQL()."')";
 			$oRes->LogIssue($sMsg, $bIsMandatory);
 		}
 	}
@@ -300,7 +346,7 @@ class WebServices
 	 *
 	 * @return array List of objects that could not be found
 	 */
-	protected function AddLinkedObjects($sLinkAttCode, $sLinkedClass, $aLinkList, &$oTargetObj, &$oRes)
+	protected function AddLinkedObjects($sLinkAttCode, $sParamName, $sLinkedClass, $aLinkList, &$oTargetObj, &$oRes)
 	{
 		$oLinkAtt = MetaModel::GetAttributeDef(get_class($oTargetObj), $sLinkAttCode);
 		$sLinkClass = $oLinkAtt->GetLinkedClass();
@@ -308,22 +354,28 @@ class WebServices
 
 		$aItemsFound = array();
 		$aItemsNotFound = array();
+		
+		if (is_null($aLinkList))
+		{
+			return $aItemsNotFound;
+		}
+
 		foreach ($aLinkList as $aItemData)
 		{
 			if (!array_key_exists('class', $aItemData))
 			{
-				$oRes->LogWarning("Linked object descriptor: missing 'class' specification");
+				$oRes->LogWarning("Parameter $sParamName: missing 'class' specification");
 				continue; // skip
 			}
 			$sTargetClass = $aItemData['class'];
 			if (!MetaModel::IsValidClass($sTargetClass))
 			{
-				$oRes->LogError("Invalid class $sTargetClass for impacted item");
+				$oRes->LogError("Parameter $sParamName: invalid class '$sTargetClass'");
 				continue; // skip
 			}
 			if (!MetaModel::IsParentClass($sLinkedClass, $sTargetClass))
 			{
-				$oRes->LogError("$sTargetClass is not a child class of $sLinkedClass");
+				$oRes->LogError("Parameter $sParamName: '$sTargetClass' is not a child class of '$sLinkedClass'");
 				continue; // skip
 			}
 			$oReconFilter = new CMDBSearchFilter($sTargetClass);
@@ -332,8 +384,9 @@ class WebServices
 			{
 				if (!MetaModel::IsValidFilterCode($sTargetClass, $sAttCode))
 				{
-					$oRes->LogError("Invalid filter code $sAttCode for class $sTargetClass");
-					continue; // skip
+					$aCodes = array_keys(MetaModel::GetClassFilterDefs($sTargetClass));
+					$oRes->LogError("Parameter $sParamName: '$sAttCode' is not a valid filter code for class '$sTargetClass', expecting a value in {".implode(', ', $aCodes)."}");
+					continue 2; // skip the entire item
 				}
 				$aCIStringDesc[] = "$sAttCode: $value";
 
@@ -355,7 +408,7 @@ class WebServices
 			switch($oExtObjects->Count())
 			{
 			case 0:
-				$oRes->LogWarning("Object to link $sLinkedClass / $sItemDesc could not be found (searched: '".$oReconFilter->ToOQL()."')");
+				$oRes->LogWarning("Parameter $sParamName: object to link $sLinkedClass / $sItemDesc could not be found (searched: '".$oReconFilter->ToOQL()."')");
 				$aItemsNotFound[] = $sItemDesc;
 				break;
 			case 1:
@@ -366,7 +419,7 @@ class WebServices
 				);
 				break;
 			default:
-				$oRes->LogWarning("Found ".$oExtObjects->Count()." matches for external key $sAttCode (searched: '".$oReconFilter->ToOQL()."')");
+				$oRes->LogWarning("Parameter $sParamName: Found ".$oExtObjects->Count()." matches for item '$sItemDesc' (searched: '".$oReconFilter->ToOQL()."')");
 				$aItemsNotFound[] = $sItemDesc;
 			}
 		}
@@ -382,7 +435,7 @@ class WebServices
 				{
 					if(!MetaModel::IsValidAttCode($sLinkClass, $sKey))
 					{
-						$oRes->LogWarning("Attaching item '".$aItemData['desc']."', the attribute code '$sKey' is not valid ; check the class '$sLinkClass'");
+						$oRes->LogWarning("Parameter $sParamName: Attaching item '".$aItemData['desc']."', the attribute code '$sKey' is not valid ; check the class '$sLinkClass'");
 					}
 					else
 					{
@@ -398,9 +451,28 @@ class WebServices
 		return $aItemsNotFound;
 	}
 
+	protected function MyObjectInsert($oTargetObj, $sResultLabel, $oChange, &$oRes)
+	{
+		if ($oRes->IsOk())
+		{
+			if ($oTargetObj->CheckToInsert())
+			{
+				$iId = $oTargetObj->DBInsertTrackedNoReload($oChange);
+				$oRes->LogInfo("Created object ".get_class($$oTargetObj)."::$iId");
+				$oRes->AddResultObject($sResultLabel, $oTargetObj);
+			}
+			else
+			{
+				$oRes->LogError("The ticket could not be created due to forbidden values (or inconsistent values)");
+			}
+		}
+	}
+
 
 	static protected function SoapStructToExternalKeySearch(SoapExternalKeySearch $oExternalKeySearch)
 	{
+		if (is_null($oExternalKeySearch)) return null;
+
 		$aRes = array();
 		foreach($oExternalKeySearch->conditions as $oSearchCondition)
 		{
@@ -504,36 +576,26 @@ class WebServices
 			$iChangeId = $oMyChange->DBInsertNoReload();
 	
 			$oNewTicket = MetaModel::NewObject('bizIncidentTicket');
-			$oNewTicket->Set('type', $sType);
-			$oNewTicket->Set('title', $sDescription);
-			$oNewTicket->Set('initial_situation', $sInitialSituation);
-			$oNewTicket->Set('severity', $sSeverity);
+			$this->MyObjectSetScalar('type', 'type', $sType, $oNewTicket, $oRes);
+			$this->MyObjectSetScalar('title', 'title', $sDescription, $oNewTicket, $oRes);
+			$this->MyObjectSetScalar('initial_situation', 'initialsituation', $sInitialSituation, $oNewTicket, $oRes);
+			$this->MyObjectSetScalar('severity', 'severity', $sSeverity, $oNewTicket, $oRes);
 	
-			$this->SetExternalKey('org_id', $aCustomerDesc, $oNewTicket, $oRes);
-			$this->SetExternalKey('caller_id', $aCallerDesc, $oNewTicket, $oRes);
-			$this->SetExternalKey('workgroup_id', $aWorkgroupDesc, $oNewTicket, $oRes);
+			$this->MyObjectSetExternalKey('org_id', 'customer', $aCustomerDesc, $oNewTicket, $oRes);
+			$this->MyObjectSetExternalKey('caller_id', 'caller', $aCallerDesc, $oNewTicket, $oRes);
+			$this->MyObjectSetExternalKey('workgroup_id', 'workgroup', $aWorkgroupDesc, $oNewTicket, $oRes);
 	
-			$aDevicesNotFound = $this->AddLinkedObjects('impacted_infra_manual', 'logInfra', $aImpactedCIs, $oNewTicket, $oRes);
+			$aDevicesNotFound = $this->AddLinkedObjects('impacted_infra_manual', 'impacted_cis', 'logInfra', $aImpactedCIs, $oNewTicket, $oRes);
 			if (count($aDevicesNotFound) > 0)
 			{
-				$oNewTicket->Set('impact', $sImpact.' - Related CIs: '.implode(', ', $aDevicesNotFound));
+				$this->MyObjectSetScalar('impact', 'n/a', $sImpact.' - Related CIs: '.implode(', ', $aDevicesNotFound), $oNewTicket, $oRes);
 			}
 			else
 			{
-				$oNewTicket->Set('impact', $sImpact);
-			}
-	
-			if (!$oNewTicket->CheckToInsert())
-			{
-				$oRes->LogError("The ticket could not be created due to forbidden values (or inconsistent values)");
-			}
-	
-			if ($oRes->IsOk())
-			{
-				$iId = $oNewTicket->DBInsertTrackedNoReload($oMyChange);
-				$oRes->LogInfo("Created ticket #$iId");
-				$oRes->AddResultObject('created', $oNewTicket);
+				$this->MyObjectSetScalar('impact', 'n/a', $sImpact, $oNewTicket, $oRes);
 			}
+
+			$this->MyObjectInsert($oNewTicket, 'created', $oMyChange, $oRes);
 		}
 		catch (CoreException $e)
 		{