diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index bc99de3a2f..d6c33e24ce 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/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"); diff --git a/application/iotask.class.inc.php b/application/iotask.class.inc.php index f1f62f8c79..e882a48ee2 100644 --- a/application/iotask.class.inc.php +++ b/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()))); diff --git a/core/action.class.inc.php b/core/action.class.inc.php index 68c12a7596..51cee9af5e 100644 --- a/core/action.class.inc.php +++ b/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(); } } ?> diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 6beed31b2c..145020461a 100644 --- a/core/attributedef.class.inc.php +++ b/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 + * @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 diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index ebbfc173bc..2835992edf 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/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 + * @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 = '
'.$this->GetAsHtml('prevdata').'
'; + + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = "$sAttName changed, previous value: $sTextView"; + } + return $sResult; + } +} ?> diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 997f605d4d..5f490e9e5d 100644 --- a/core/cmdbobject.class.inc.php +++ b/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 diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index b68474d3e4..20f3fed9e7 100644 --- a/core/cmdbsource.class.inc.php +++ b/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) { diff --git a/core/dbobject.class.php b/core/dbobject.class.php index d309ca4db1..e20943a7ab 100644 --- a/core/dbobject.class.php +++ b/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) diff --git a/core/event.class.inc.php b/core/event.class.inc.php index 833879e542..79b2efbe0c 100644 --- a/core/event.class.inc.php +++ b/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 diff --git a/core/metamodel.class.php b/core/metamodel.class.php index c54e828f98..6177cace23 100644 --- a/core/metamodel.class.php +++ b/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"; - } - elseif ($oAttDef->IsNullAllowed() != CMDBSource::IsNullAllowed($sTable, $sField)) - { - if ($oAttDef->IsNullAllowed()) + if ($oAttDef->IsExternalKey()) { - $aErrors[$sClass][] = "field '$sField' in table '$sTable' could be NULL"; - $aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs"; + $aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)"; } - else + } + else + { + if ($oAttDef->IsNullAllowed() != CMDBSource::IsNullAllowed($sTable, $sField)) { - $aErrors[$sClass][] = "field '$sField' in table '$sTable' could NOT 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"; + } + } + if ($oAttDef->IsExternalKey() && !CMDBSource::HasIndex($sTable, $sField)) + { + $aErrors[$sClass][] = "Foreign key '$sField' in table '$sTable' should have an index"; + $aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)"; } } } diff --git a/core/trigger.class.inc.php b/core/trigger.class.inc.php index ffc043a9f9..6d9d1a637b 100644 --- a/core/trigger.class.inc.php +++ b/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() diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 393a06baac..e84b0c2b5e 100644 --- a/core/valuesetdef.class.inc.php +++ b/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 = '') - { - // First, build it from the series of additional values - parent::__construct($sAdditionalValues); + protected $m_sCategories; - // Second: add the list of classes - foreach (MetaModel::GetClasses($sCategory) as $sClass) - { - $this->m_aValues[$sClass] = MetaModel::GetName($sClass); - } + public function __construct($sCategories = '', $sAdditionalValues = '') + { + $this->m_sCategories = $sCategories; + parent::__construct($sAdditionalValues); } protected function LoadValues($aArgs) - { + { + // First, get the additional values + parent::LoadValues($aArgs); + + // Then, add the classes from the category definition + foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass) + { + $this->m_aValues[$sClass] = MetaModel::GetName($sClass); + } + return true; } } diff --git a/pages/testlist.inc.php b/pages/testlist.inc.php index 862e41206e..42d1375281 100644 --- a/pages/testlist.inc.php +++ b/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,12 +1267,30 @@ $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 */ ), ), + 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, @@ -1267,12 +1304,31 @@ $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 */ ), ), + 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, @@ -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 "

Note: You may also want to try the sample SOAP client itopsoap.examples.php

\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 "

SOAP call #$iPos ".$aWebService['explain result']."

\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 ""; print "Response in HTML:

".$this->m_SoapClient->__getLastResponse()."

"; - return false; + throw $e; } echo "
\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 "
"; - } - 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'"); + } + } } } diff --git a/webservices/itop.wsdl.tpl b/webservices/itop.wsdl.tpl index 32f6edf19e..c3e4289e36 100644 --- a/webservices/itop.wsdl.tpl +++ b/webservices/itop.wsdl.tpl @@ -1,6 +1,6 @@ - + @@ -8,9 +8,13 @@ + - + @@ -21,14 +25,23 @@ + + - + @@ -39,6 +52,9 @@ + @@ -53,6 +69,9 @@ + @@ -65,6 +84,9 @@ + @@ -72,7 +94,7 @@ - + @@ -83,6 +105,10 @@ + @@ -96,6 +122,12 @@ + @@ -129,17 +161,17 @@ - + Get the current version of Itop As this service is very simple, it is a test to get trained for more complex operations - + --> - + Create a ticket, return information about reconciliation on external keys and the created ticket - + --> @@ -166,6 +198,9 @@ + + ITop is the central solution for managing your IT infrastructure + diff --git a/webservices/itopsoap.examples.php b/webservices/itopsoap.examples.php new file mode 100644 index 0000000000..c93be4333d --- /dev/null +++ b/webservices/itopsoap.examples.php @@ -0,0 +1,69 @@ + 1, + 'classmap' => $aSOAPMapping, // defined in itopsoaptypes.class.inc.php + ) +); + +try +{ + // The most simple service, returning a string + // + $sServerVersion = $oSoapClient->GetVersion(); + echo "

GetVersion() returned $sServerVersion

"; + + // 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 "

CreateIncidentTicket() returned:\n"; + echo "

\n";
+	print_r($oRes);
+	echo "
\n"; + echo "

\n"; +} +catch(SoapFault $e) +{ + echo "

SoapFault Exception: {$e->getMessage()}

\n"; + echo "

Request

\n"; + echo "
\n"; 
+	echo htmlspecialchars($oSoapClient->__getLastRequest())."\n"; 
+	echo "
"; + echo "

Response

"; + echo $oSoapClient->__getLastResponse()."\n"; +} +?> diff --git a/webservices/webservices.class.inc.php b/webservices/webservices.class.inc.php index e17163ed0d..5595f7113a 100644 --- a/webservices/webservices.class.inc.php +++ b/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()); @@ -233,6 +241,29 @@ class WebServices $oLog->DBInsertNoReload(); } + /** + * 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 * @@ -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) {