diff --git a/addons/userrights/userrightsmatrix.class.inc.php b/addons/userrights/userrightsmatrix.class.inc.php new file mode 100644 index 0000000000..3f4f9369b7 --- /dev/null +++ b/addons/userrights/userrightsmatrix.class.inc.php @@ -0,0 +1,368 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +class UserRightsMatrixClassGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixclasses", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +class UserRightsMatrixClassStimulusGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixclassesstimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +class UserRightsMatrixAttributeGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixattributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + + + + +class UserRightsMatrix extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Maybe we should check that no other user with userid == 0 exists + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + $oUser->Set('contactid', 1); // one is for root ! + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + // Now record the admin user object + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + $this->SetupUser($iUserId, true); + return true; + } + + public function IsAdministrator($oUser) + { + return ($oUser->GetKey() == 1); + } + + public function IsPortalUser($oUser) + { + return ($oUser->GetKey() == 1); + } + + // Deprecated - create a new module ! + public function Setup() + { + // Users must be added manually + // This procedure will then update the matrix when a new user is found or a new class/attribute appears + $oUserSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT User")); + while ($oUser = $oUserSet->Fetch()) + { + $this->SetupUser($oUser->GetKey()); + } + return true; + } + + protected function SetupUser($iUserId, $bNewUser = false) + { + foreach(array('bizmodel', 'application', 'gui', 'core/cmdb') as $sCategory) + { + foreach (MetaModel::GetClasses($sCategory) as $sClass) + { + foreach (self::$m_aActionCodes as $iActionCode => $sAction) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + // Create a new entry + $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassGrant"); + $oMyClassGrant->Set("userid", $iUserId); + $oMyClassGrant->Set("class", $sClass); + $oMyClassGrant->Set("action", $sAction); + $oMyClassGrant->Set("permission", "yes"); + $iId = $oMyClassGrant->DBInsertNoReload(); + } + } + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + // Create a new entry + $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassStimulusGrant"); + $oMyClassGrant->Set("userid", $iUserId); + $oMyClassGrant->Set("class", $sClass); + $oMyClassGrant->Set("stimulus", $sStimulusCode); + $oMyClassGrant->Set("permission", "yes"); + $iId = $oMyClassGrant->DBInsertNoReload(); + } + } + foreach (MetaModel::GetAttributesList($sClass) as $sAttCode) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + foreach (array('read', 'modify') as $sAction) + { + // Create a new entry + $oMyAttGrant = MetaModel::NewObject("UserRightsMatrixAttributeGrant"); + $oMyAttGrant->Set("userid", $iUserId); + $oMyAttGrant->Set("class", $sClass); + $oMyAttGrant->Set("attcode", $sAttCode); + $oMyAttGrant->Set("action", $sAction); + $oMyAttGrant->Set("permission", "yes"); + $iId = $oMyAttGrant->DBInsertNoReload(); + } + } + } + } + } + /* + // Create the "My Bookmarks" menu item (parent_id = 0, rank = 6) + if ($bNewUser) + { + $bAddMenu = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT menuNode WHERE type = 'user' AND parent_id = 0 AND user_id = $iUserId")); + $bAddMenu = ($oSet->Count() < 1); + } + if ($bAddMenu) + { + $oMenu = MetaModel::NewObject('menuNode'); + $oMenu->Set('type', 'user'); + $oMenu->Set('parent_id', 0); // It's a toplevel entry + $oMenu->Set('rank', 6); // Located just above the Admin Tools section (=7) + $oMenu->Set('name', 'My Bookmarks'); + $oMenu->Set('label', 'My Favorite Items'); + $oMenu->Set('hyperlink', 'UI.php'); + $oMenu->Set('template', '

My bookmarks

This section contains my most favorite search results

'); + $oMenu->Set('user_id', $iUserId); + $oMenu->DBInsert(); + } + */ + } + + + public function Init() + { + // Could be loaded in a shared memory (?) + return true; + } + + public function GetSelectFilter($oUser, $sClass) + { + $oNullFilter = new DBObjectSearch($sClass); + return $oNullFilter; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + if (!array_key_exists($iActionCode, self::$m_aActionCodes)) + { + return UR_ALLOWED_NO; + } + $sAction = self::$m_aActionCodes[$iActionCode]; + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + if (!array_key_exists($iActionCode, self::$m_aActionCodes)) + { + return UR_ALLOWED_NO; + } + $sAction = self::$m_aActionCodes[$iActionCode]; + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function FlushPrivileges() + { + } +} + +UserRights::SelectModule('UserRightsMatrix'); + +?> diff --git a/addons/userrights/userrightsnull.class.inc.php b/addons/userrights/userrightsnull.class.inc.php new file mode 100644 index 0000000000..760b93ab7a --- /dev/null +++ b/addons/userrights/userrightsnull.class.inc.php @@ -0,0 +1,78 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class UserRightsNull extends UserRightsAddOnAPI +{ + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + return true; + } + + public function IsAdministrator($oUser) + { + return true; + } + + public function IsPortalUser($oUser) + { + return true; + } + + public function Init() + { + return true; + } + + public function GetSelectFilter($oUser, $sClass) + { + $oNullFilter = new DBObjectSearch($sClass); + return $oNullFilter; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function FlushPrivileges() + { + } +} + +UserRights::SelectModule('UserRightsNull'); + +?> diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php new file mode 100644 index 0000000000..e8a89b0bc3 --- /dev/null +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -0,0 +1,839 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +define('ADMIN_PROFILE_NAME', 'Administrator'); +define('PORTAL_PROFILE_NAME', 'Portal user'); + +class UserRightsBaseClass extends cmdbAbstractObject +{ + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + + + + +class URP_Profiles extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profiles", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // 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 + } + + protected $m_bCheckReservedNames = true; + protected function DisableCheckOnReservedNames() + { + $this->m_bCheckReservedNames = false; + } + + /* + * Create the built-in Administrator profile with its reserved name + */ + public static function DoCreateAdminProfile() + { + $oNewObj = MetaModel::NewObject("URP_Profiles"); + $oNewObj->Set('name', ADMIN_PROFILE_NAME); + $oNewObj->Set('description', 'Has the rights on everything (bypassing any control)'); + $oNewObj->DisableCheckOnReservedNames(); + $iNewId = $oNewObj->DBInsertNoReload(); + } + + /* + * Create the built-in User Portal profile with its reserved name + */ + public static function DoCreateUserPortalProfile() + { + $oNewObj = MetaModel::NewObject("URP_Profiles"); + $oNewObj->Set('name', PORTAL_PROFILE_NAME); + $oNewObj->Set('description', 'Has the rights to access to the user portal. People having this profile will not be allowed to access the standard application, they will be automatically redirected to the user portal.'); + $oNewObj->DisableCheckOnReservedNames(); + $iNewId = $oNewObj->DBInsertNoReload(); + } + + /* + * Overload the standard behavior to preserve reserved names + */ + public function DoCheckToWrite() + { + parent::DoCheckToWrite(); + + if ($this->m_bCheckReservedNames) + { + $aChanges = $this->ListChanges(); + if (array_key_exists('name', $aChanges)) + { + if ($this->GetOriginal('name') == ADMIN_PROFILE_NAME) + { + $this->m_aCheckIssues[] = "The name of the Administrator profile must not be changed"; + } + elseif ($this->Get('name') == ADMIN_PROFILE_NAME) + { + $this->m_aCheckIssues[] = ADMIN_PROFILE_NAME." is a reserved to the built-in Administrator profile"; + } + elseif ($this->GetOriginal('name') == PORTAL_PROFILE_NAME) + { + $this->m_aCheckIssues[] = "The name of the User Portal profile must not be changed"; + } + elseif ($this->Get('name') == PORTAL_PROFILE_NAME) + { + $this->m_aCheckIssues[] = PORTAL_PROFILE_NAME." is a reserved to the built-in User Portal profile"; + } + } + } + } + + function GetGrantAsHtml($oUserRights, $sClass, $sAction) + { + $iGrant = $oUserRights->GetProfileActionGrant($this->GetKey(), $sClass, $sAction); + if (!is_null($iGrant)) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage) + { + if ($this->GetName() == "Administrator") + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + // Note: for sure, we assume that the instance is derived from UserRightsProfile + $oUserRights = UserRights::GetModuleInstance(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses('bizmodel') as $sClass) + { + // Skip non instantiable classes + if (MetaModel::IsAbstract($sClass)) continue; + + $aStimuli = array(); + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel()).''; + } + } + $sStimuli = implode(', ', $aStimuli); + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), + 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), + 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), + 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), + 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), + 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), + 'stimuli' => $sStimuli, + ); + } + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage); + } + } +} + + + +class URP_UserProfile extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userprofile", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('userid', 'profileid', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); + } +} + +class URP_UserOrg extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userorg", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("allowed_org_id", array("targetclass"=>"Organization", "jointype"=> "", "allowed_values"=>null, "sql"=>"allowed_org_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("allowed_org_name", array("allowed_values"=>null, "extkey_attcode"=> 'allowed_org_id', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"reason", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'allowed_org_id', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('allowed_org_id', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'allowed_org_id')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'allowed_org_id')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Org', $this->Get('userlogin'), $this->Get('allowed_org_name')); + } +} + + +class URP_ActionGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_actions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form + } +} + + +class URP_StimulusGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_stimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form + } +} + + +class URP_AttributeGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "actiongrantid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_attributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form + } +} + + + + +class UserRightsProfile extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + // Support drastic data model changes: no organization class ! + if (MetaModel::IsValidClass('Organization')) + { + $oOrg = new Organization(); + $oOrg->Set('name', 'My Company/Department'); + $oOrg->Set('code', 'SOMECODE'); + $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip security */); + } + else + { + $iOrgId = 0; + } + + // Support drastic data model changes: no Person class ! + if (MetaModel::IsValidClass('Person')) + { + $oContact = new Person(); + $oContact->Set('name', 'My last name'); + $oContact->Set('first_name', 'My first name'); + if (MetaModel::IsValidAttCode('Person', 'org_id')) + { + $oContact->Set('org_id', $iOrgId); + } + $oContact->Set('email', 'my.email@foo.org'); + $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); + } + else + { + $iContactId = 0; + } + + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + if (MetaModel::IsValidAttCode('UserLocal', 'contactid')) + { + $oUser->Set('contactid', $iContactId); + } + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + + // Add this user to the very specific 'admin' profile + $oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => ADMIN_PROFILE_NAME), true /*all data*/); + if (is_object($oAdminProfile)) + { + $oUserProfile = new URP_UserProfile(); + //$oUserProfile->Set('userid', $iUserId); + $oUserProfile->Set('profileid', $oAdminProfile->GetKey()); + $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); + //$oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); + $oSet = DBObjectSet::FromObject($oUserProfile); + $oUser->Set('profile_list', $oSet); + } + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + return true; + } + + public function Init() + { + MetaModel::RegisterPlugin('userrights', 'ACbyProfile'); + } + + + protected $m_aAdmins; // id of users being linked to the well-known admin profile + protected $m_aPortalUsers; // id of users being linked to the well-known admin profile + + protected $m_aProfiles; // id -> object + protected $m_aUserProfiles; // userid,profileid -> object + protected $m_aUserOrgs; // userid,orgid -> object + + // Those arrays could be completed on demand (inheriting parent permissions) + protected $m_aClassActionGrants = null; // profile, class, action -> actiongrantid (or false if NO, or null/missing if undefined) + protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission + + // Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read) + protected $m_aObjectActionGrants = array(); + + public function ResetCache() + { + // Loaded by Load cache + $this->m_aProfiles = null; + $this->m_aUserProfiles = null; + $this->m_aUserOrgs = null; + + $this->m_aAdmins = null; + $this->m_aPortalUsers = null; + + // Loaded on demand (time consuming as compared to the others) + $this->m_aClassActionGrants = null; + $this->m_aClassStimulusGrants = null; + + $this->m_aObjectActionGrants = array(); + } + + // Separate load: this cache is much more time consuming while loading + // Thus it is loaded iif required + // Could be improved by specifying the profile id + public function LoadActionGrantCache() + { + if (!is_null($this->m_aClassActionGrants)) return; + + $oKPI = new ExecutionKPI(); + + $oFilter = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant AS p WHERE p.permission = 'yes'"); + $aGrants = $oFilter->ToDataArray(); + foreach($aGrants as $aGrant) + { + $this->m_aClassActionGrants[$aGrant['profileid']][$aGrant['class']][strtolower($aGrant['action'])] = $aGrant['id']; + } + + $oKPI->ComputeAndReport('Load of action grants'); + } + + public function LoadCache() + { + if (!is_null($this->m_aProfiles)) return; + // Could be loaded in a shared memory (?) + + $oKPI = new ExecutionKPI(); + + $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); + $this->m_aProfiles = array(); + while ($oProfile = $oProfileSet->Fetch()) + { + $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; + } + + $oUserProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserProfile")); + $this->m_aUserProfiles = array(); + $this->m_aAdmins = array(); + $this->m_aPortalUsers = array(); + while ($oUserProfile = $oUserProfileSet->Fetch()) + { + $this->m_aUserProfiles[$oUserProfile->Get('userid')][$oUserProfile->Get('profileid')] = $oUserProfile; + if ($oUserProfile->Get('profile') == ADMIN_PROFILE_NAME) + { + $this->m_aAdmins[] = $oUserProfile->Get('userid'); + } + elseif ($oUserProfile->Get('profile') == PORTAL_PROFILE_NAME) + { + $this->m_aPortalUsers[] = $oUserProfile->Get('userid'); + } + } + + $oUserOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserOrg")); + $this->m_aUserOrgs = array(); + while ($oUserOrg = $oUserOrgSet->Fetch()) + { + $this->m_aUserOrgs[$oUserOrg->Get('userid')][$oUserOrg->Get('allowed_org_id')] = $oUserOrg; + } + + $this->m_aClassStimulusGrants = array(); + $oStimGrantSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant")); + $this->m_aStimGrants = array(); + while ($oStimGrant = $oStimGrantSet->Fetch()) + { + $this->m_aClassStimulusGrants[$oStimGrant->Get('profileid')][$oStimGrant->Get('class')][$oStimGrant->Get('stimulus')] = $oStimGrant; + } + + $oKPI->ComputeAndReport('Load of user management cache (excepted Action Grants)'); + +/* + echo "
\n";
+		print_r($this->m_aProfiles);
+		print_r($this->m_aUserProfiles);
+		print_r($this->m_aUserOrgs);
+		print_r($this->m_aClassActionGrants);
+		print_r($this->m_aClassStimulusGrants);
+		echo "
\n"; +exit; +*/ + + return true; + } + + public function IsAdministrator($oUser) + { + $this->LoadCache(); + + if (in_array($oUser->GetKey(), $this->m_aAdmins)) + { + return true; + } + else + { + return false; + } + } + + public function IsPortalUser($oUser) + { + $this->LoadCache(); + + if (in_array($oUser->GetKey(), $this->m_aPortalUsers)) + { + return true; + } + else + { + return false; + } + } + + public function GetSelectFilter($oUser, $sClass) + { + $this->LoadCache(); + + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, UR_ACTION_READ); + if ($aObjectPermissions['permission'] == UR_ALLOWED_NO) + { + return false; + } + + // Determine how to position the objects of this class + // + if ($sClass == 'Organization') + { + $sAttCode = 'id'; + } + elseif(MetaModel::IsValidAttCode($sClass, 'org_id')) + { + $sAttCode = 'org_id'; + } + else + { + // The objects of this class are not positioned in this dimension + // All of them are visible + return true; + } + $oExpression = new FieldExpression($sAttCode, $sClass); + + // Position the user + // + @$aUserOrgs = $this->m_aUserOrgs[$oUser->GetKey()]; + if (!isset($aUserOrgs) || count($aUserOrgs) == 0) + { + // No position means 'Everywhere' + return true; + } + + $aIds = array_keys($aUserOrgs); + $oListExpr = ListExpression::FromScalars($aIds); + $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); + + $oFilter = new DBObjectSearch($sClass); + $oFilter->AddConditionExpression($oCondition); + return $oFilter; + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetProfileActionGrant($iProfile, $sClass, $sAction) + { + $this->LoadActionGrantCache(); + + // Note: action is forced lowercase to be more flexible (historical bug) + $sAction = strtolower($sAction); + if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) + { + return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; + } + + // Recursively look for the grant record in the class hierarchy + $sParentClass = MetaModel::GetParentPersistentClass($sClass); + if (empty($sParentClass)) + { + $iGrant = null; + } + else + { + // Recursively look for the grant record in the class hierarchy + $iGrant = $this->GetProfileActionGrant($iProfile, $sParentClass, $sAction); + } + + $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $iGrant; + return $iGrant; + } + + protected function GetUserActionGrant($oUser, $sClass, $iActionCode) + { + $this->LoadCache(); + + // load and cache permissions for the current user on the given class + // + $iUser = $oUser->GetKey(); + $aTest = @$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode]; + if (is_array($aTest)) return $aTest; + + $sAction = self::$m_aActionCodes[$iActionCode]; + + $iPermission = UR_ALLOWED_NO; + $aAttributes = array(); + if (isset($this->m_aUserProfiles[$iUser])) + { + foreach($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + $iGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction); + if (is_null($iGrant) || !$iGrant) + { + continue; // loop to the next profile + } + else + { + $iPermission = UR_ALLOWED_YES; + + // update the list of attributes with those allowed for this profile + // + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); + $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $iGrant)); + $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); + if (count($aProfileAttributes) == 0) + { + $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); + $aAttributes = array_merge($aAttributes, $aAllAttributes); + } + else + { + $aAttributes = array_merge($aAttributes, $aProfileAttributes); + } + } + } + } + + $aRes = array( + 'permission' => $iPermission, + 'attributes' => $aAttributes, + ); + $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes; + return $aRes; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + return $aObjectPermissions['permission']; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + return $aObjectPermissions['permission']; + } + else + { + return UR_ALLOWED_NO; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) + { + $this->LoadCache(); + + if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) + { + return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; + } + else + { + return null; + } + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + $this->LoadCache(); + // Note: this code is VERY close to the code of IsActionAllowed() + $iUser = $oUser->GetKey(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $iPermission = UR_ALLOWED_NO; + if (isset($this->m_aUserProfiles[$iUser])) + { + foreach($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iPermission = UR_ALLOWED_YES; + } + } + } + return $iPermission; + } + + public function FlushPrivileges() + { + $this->ResetCache(); + } +} + + +UserRights::SelectModule('UserRightsProfile'); + +?> diff --git a/addons/userrights/userrightsprojection.class.inc.php b/addons/userrights/userrightsprojection.class.inc.php new file mode 100644 index 0000000000..4e771051e8 --- /dev/null +++ b/addons/userrights/userrightsprojection.class.inc.php @@ -0,0 +1,1252 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +define('ADMIN_PROFILE_ID', 1); + +class UserRightsBaseClass extends cmdbAbstractObject +{ + // Whenever something changes, reload the privileges + + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + + + + +class URP_Profiles extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profiles", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // 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 + } + + function GetGrantAsHtml($oUserRights, $sClass, $sAction) + { + $oGrant = $oUserRights->GetClassActionGrant($this->GetKey(), $sClass, $sAction); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage) + { + if ($this->GetName() == "Administrator") + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + // Note: for sure, we assume that the instance is derived from UserRightsProjection + $oUserRights = UserRights::GetModuleInstance(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses('bizmodel') as $sClass) + { + // Skip non instantiable classes + if (MetaModel::IsAbstract($sClass)) continue; + + $aStimuli = array(); + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel()).''; + } + } + $sStimuli = implode(', ', $aStimuli); + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), + 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), + 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), + 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), + 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), + 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), + 'stimuli' => $sStimuli, + ); + } + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage); + } + } +} + + +class URP_Dimensions extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_dimensions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeClass("type", array("class_category"=>"bizmodel", "more_values"=>"String,Integer", "sql"=>"type", "default_value"=>'String', "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'type')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // 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 + } + + public function CheckProjectionSpec($oProjectionSpec, $sProjectedClass) + { + $sExpression = $oProjectionSpec->Get('value'); + $sAttribute = $oProjectionSpec->Get('attribute'); + + // Shortcut: "any value" or "no value" means no projection + if (empty($sExpression)) return; + if ($sExpression == '') return; + + // 1st - compute the data type for the dimension + // + $sType = $this->Get('type'); + if (MetaModel::IsValidClass($sType)) + { + $sExpectedType = $sType; + } + else + { + $sExpectedType = '_scalar_'; + } + + // 2nd - compute the data type for the projection + // + $sTargetClass = ''; + if (($sExpression == '') || ($sExpression == '')) + { + $sTargetClass = $sProjectedClass; + } + elseif ($sExpression == '') + { + $sTargetClass = ''; + } + else + { + // Evaluate wether it is a constant or not + try + { + $oObjectSearch = DBObjectSearch::FromOQL_AllData($sExpression); + + $sTargetClass = $oObjectSearch->GetClass(); + } + catch (OqlException $e) + { + } + } + + if (empty($sTargetClass)) + { + $sFoundType = '_void_'; + } + else + { + if (empty($sAttribute)) + { + $sFoundType = $sTargetClass; + } + else + { + if (!MetaModel::IsValidAttCode($sTargetClass, $sAttribute)) + { + throw new CoreException('Unkown attribute code in projection specification', array('found' => $sAttribute, 'expecting' => MetaModel::GetAttributesList($sTargetClass), 'class' => $sTargetClass, 'projection' => $oProjectionSpec)); + } + $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttribute); + if ($oAttDef->IsExternalKey()) + { + $sFoundType = $oAttDef->GetTargetClass(); + } + else + { + $sFoundType = '_scalar_'; + } + } + } + + // Compare the dimension type and projection type + if (($sFoundType != '_void_') && ($sFoundType != $sExpectedType)) + { + throw new CoreException('Wrong type in projection specification', array('found' => $sFoundType, 'expecting' => $sExpectedType, 'expression' => $sExpression, 'attribute' => $sAttribute, 'projection' => $oProjectionSpec)); + } + } +} + + +class URP_UserProfile extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userprofile", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('profileid', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); + } +} + + +class URP_ProfileProjection extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profileprojection", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('dimensionid', 'profileid', 'value', 'attribute')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('profileid', 'value', 'attribute')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'profileid')); // Criteria of the advanced search form + } + + protected $m_aUserProjections; // cache + + public function ProjectUser(User $oUser) + { + if (is_array($this->m_aUserProjections)) + { + // Hit! + return $this->m_aUserProjections; + } + + $sExpr = $this->Get('value'); + if ($sExpr == '') + { + $sColumn = $this->Get('attribute'); + if (empty($sColumn)) + { + $aRes = array($oUser->GetKey()); + } + else + { + $aRes = array($oUser->Get($sColumn)); + } + + } + elseif (($sExpr == '') || ($sExpr == '')) + { + $aRes = null; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + $sColumn = $this->Get('attribute'); + // SELECT... + $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); + $aRes = $oValueSetDef->GetValues(array('user' => $oUser), ''); + } + else + { + // Constant value(s) + $aRes = explode(';', trim($sExpr)); + } + $this->m_aUserProjections = $aRes; + return $aRes; + } +} + + +class URP_ClassProjection extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "dimensionid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_classprojection", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('dimensionid', 'class', 'value', 'attribute')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'value', 'attribute')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'class')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'class')); // Criteria of the advanced search form + } + + public function ProjectObject($oObject) + { + $sExpr = $this->Get('value'); + if ($sExpr == '') + { + $sColumn = $this->Get('attribute'); + if (empty($sColumn)) + { + $aRes = array($oObject->GetKey()); + } + else + { + $aRes = array($oObject->Get($sColumn)); + } + + } + elseif (($sExpr == '') || ($sExpr == '')) + { + $aRes = null; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + $sColumn = $this->Get('attribute'); + // SELECT... + $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); + $aRes = $oValueSetDef->GetValues(array('this' => $oObject), ''); + } + else + { + // Constant value(s) + $aRes = explode(';', trim($sExpr)); + } + return $aRes; + } + +} + + +class URP_ActionGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_actions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form + } +} + + +class URP_StimulusGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_stimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form + } +} + + +class URP_AttributeGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "actiongrantid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_attributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form + } +} + + + + +class UserRightsProjection extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + $oOrg = new Organization(); + $oOrg->Set('name', 'My Company/Department'); + $oOrg->Set('code', 'SOMECODE'); +// $oOrg->Set('status', 'implementation'); + //$oOrg->Set('parent_id', xxx); + $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip strong security */); + + // Location : optional + //$oLocation = new bizLocation(); + //$oLocation->Set('name', 'MyOffice'); + //$oLocation->Set('status', 'implementation'); + //$oLocation->Set('org_id', $iOrgId); + //$oLocation->Set('severity', 'high'); + //$oLocation->Set('address', 'my building in my city'); + //$oLocation->Set('country', 'my country'); + //$oLocation->Set('parent_location_id', xxx); + //$iLocationId = $oLocation->DBInsertNoReload(); + + $oContact = new Person(); + $oContact->Set('name', 'My last name'); + $oContact->Set('first_name', 'My first name'); + //$oContact->Set('status', 'available'); + $oContact->Set('org_id', $iOrgId); + $oContact->Set('email', 'my.email@foo.org'); + //$oContact->Set('phone', ''); + //$oContact->Set('location_id', $iLocationId); + //$oContact->Set('employee_number', ''); + $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); + + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + $oUser->Set('contactid', $iContactId); + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + + // Add this user to the very specific 'admin' profile + $oUserProfile = new URP_UserProfile(); + $oUserProfile->Set('userid', $iUserId); + $oUserProfile->Set('profileid', ADMIN_PROFILE_ID); + $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); + $oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); + return true; + } + + public function IsAdministrator($oUser) + { + if (in_array($oUser->GetKey(), $this->m_aAdmins)) + { + return true; + } + else + { + return false; + } + } + + public function IsPortalUser($oUser) + { + return true; + // See implementation of userrightsprofile + } + + public function Init() + { + MetaModel::RegisterPlugin('userrights', 'ACbyProfile', array($this, 'CacheData')); + } + + protected $m_aDimensions = array(); // id -> object + protected $m_aClassProj = array(); // class,dimensionid -> object + protected $m_aProfiles = array(); // id -> object + protected $m_aUserProfiles = array(); // userid,profileid -> object + protected $m_aProPro = array(); // profileid,dimensionid -> object + + protected $m_aAdmins = array(); // id of users being linked to the well-known admin profile + + protected $m_aClassActionGrants = array(); // profile, class, action -> permission + protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission + protected $m_aObjectActionGrants = array(); // userid, class, id, action -> permission, list of attributes + + public function CacheData() + { + // Could be loaded in a shared memory (?) + + $oDimensionSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Dimensions")); + $this->m_aDimensions = array(); + while ($oDimension = $oDimensionSet->Fetch()) + { + $this->m_aDimensions[$oDimension->GetKey()] = $oDimension; + } + + $oClassProjSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ClassProjection")); + $this->m_aClassProjs = array(); + while ($oClassProj = $oClassProjSet->Fetch()) + { + $this->m_aClassProjs[$oClassProj->Get('class')][$oClassProj->Get('dimensionid')] = $oClassProj; + } + + $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); + $this->m_aProfiles = array(); + while ($oProfile = $oProfileSet->Fetch()) + { + $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; + } + + $oUserProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserProfile")); + $this->m_aUserProfiles = array(); + $this->m_aAdmins = array(); + while ($oUserProfile = $oUserProfileSet->Fetch()) + { + $this->m_aUserProfiles[$oUserProfile->Get('userid')][$oUserProfile->Get('profileid')] = $oUserProfile; + if ($oUserProfile->Get('profileid') == ADMIN_PROFILE_ID) + { + $this->m_aAdmins[] = $oUserProfile->Get('userid'); + } + } + + $oProProSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ProfileProjection")); + $this->m_aProPros = array(); + while ($oProPro = $oProProSet->Fetch()) + { + $this->m_aProPros[$oProPro->Get('profileid')][$oProPro->Get('dimensionid')] = $oProPro; + } + +/* + echo "
\n";
+		print_r($this->m_aDimensions);
+		print_r($this->m_aClassProjs);
+		print_r($this->m_aProfiles);
+		print_r($this->m_aUserProfiles);
+		print_r($this->m_aProPros);
+		echo "
\n"; +exit; +*/ + + return true; + } + + public function GetSelectFilter($oUser, $sClass) + { + $aConditions = array(); + foreach ($this->m_aDimensions as $iDimension => $oDimension) + { + $oClassProj = @$this->m_aClassProjs[$sClass][$iDimension]; + if (is_null($oClassProj)) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + + // 1 - Get class projection info + // + $oExpression = null; + $sExpr = $oClassProj->Get('value'); + if ($sExpr == '') + { + $sColumn = $oClassProj->Get('attribute'); + if (empty($sColumn)) + { + $oExpression = new FieldExpression('id', $sClass); + } + else + { + $oExpression = new FieldExpression($sColumn, $sClass); + } + } + elseif (($sExpr == '') || ($sExpr == '')) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + throw new CoreException('Sorry, projections by the mean of OQL are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); + } + else + { + // Constant value(s) + // unsupported + throw new CoreException('Sorry, constant projections are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); +// $aRes = explode(';', trim($sExpr)); + } + + // 2 - Get profile projection info and use it if needed + // + $aProjections = self::GetReadableProjectionsByDim($oUser, $sClass, $oDimension); + if (is_null($aProjections)) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + elseif (count($aProjections) == 0) + { + // Authorize none, then exit as quickly as possible + return false; + } + else + { + // Authorize the given set of values + $oListExpr = ListExpression::FromScalars($aProjections); + $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); + $aConditions[] = $oCondition; + } + } + + if (count($aConditions) == 0) + { + // allow all + return true; + } + else + { + $oFilter = new DBObjectSearch($sClass); + foreach($aConditions as $oCondition) + { + $oFilter->AddConditionExpression($oCondition); + } + //return true; + return $oFilter; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassActionGrant($iProfile, $sClass, $sAction) + { + if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) + { + return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; + } + + // Get the permission for this profile/class/action + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant WHERE class = :class AND action = :action AND profileid = :profile AND permission = 'yes'"); + $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'action'=>$sAction, 'profile'=>$iProfile)); + if ($oSet->Count() >= 1) + { + $oGrantRecord = $oSet->Fetch(); + } + else + { + $sParentClass = MetaModel::GetParentPersistentClass($sClass); + if (empty($sParentClass)) + { + $oGrantRecord = null; + } + else + { + $oGrantRecord = $this->GetClassActionGrant($iProfile, $sParentClass, $sAction); + } + } + + $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $oGrantRecord; + return $oGrantRecord; + } + + protected function GetObjectActionGrant($oUser, $sClass, $iActionCode, /*DBObject*/ $oObject = null) + { + if(is_null($oObject)) + { + $iObjectRef = -999; + } + else + { + $iObjectRef = $oObject->GetKey(); + } + // load and cache permissions for the current user on the given object + // + $aTest = @$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode]; + if (is_array($aTest)) return $aTest; + + $sAction = self::$m_aActionCodes[$iActionCode]; + + $iInstancePermission = UR_ALLOWED_NO; + $aAttributes = array(); + foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) + { + $oGrantRecord = $this->GetClassActionGrant($iProfile, $sClass, $sAction); + if (is_null($oGrantRecord)) + { + continue; // loop to the next profile + } + else + { + $iInstancePermission = UR_ALLOWED_YES; + + // update the list of attributes with those allowed for this profile + // + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); + $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $oGrantRecord->GetKey())); + $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); + if (count($aProfileAttributes) == 0) + { + $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); + $aAttributes = array_merge($aAttributes, $aAllAttributes); + } + else + { + $aAttributes = array_merge($aAttributes, $aProfileAttributes); + } + } + } + + $aRes = array( + 'permission' => $iInstancePermission, + 'attributes' => $aAttributes, + ); + $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode] = $aRes; + return $aRes; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + if (is_null($oInstanceSet)) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); + return $aObjectPermissions['permission']; + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); + + $iInstancePermission = $aObjectPermissions['permission']; + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + break; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + if (is_null($oInstanceSet)) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + return $aObjectPermissions['permission']; + } + else + { + return UR_ALLOWED_NO; + } + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); + + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + $iInstancePermission = $aObjectPermissions['permission']; + } + else + { + $iInstancePermission = UR_ALLOWED_NO; + } + + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) + { + if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) + { + return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; + } + + // Get the permission for this profile/class/stimulus + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant WHERE class = :class AND stimulus = :stimulus AND profileid = :profile AND permission = 'yes'"); + $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'stimulus'=>$sStimulusCode, 'profile'=>$iProfile)); + if ($oSet->Count() >= 1) + { + $oGrantRecord = $oSet->Fetch(); + } + else + { + $oGrantRecord = null; + } + + $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode] = $oGrantRecord; + return $oGrantRecord; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + // Note: this code is VERY close to the code of IsActionAllowed() + + if (is_null($oInstanceSet)) + { + $iInstancePermission = UR_ALLOWED_NO; + foreach($this->GetMatchingProfiles($oUser, $sClass) as $iProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iInstancePermission = UR_ALLOWED_YES; + } + } + return $iInstancePermission; + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $iInstancePermission = UR_ALLOWED_NO; + foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iInstancePermission = UR_ALLOWED_YES; + } + } + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + // Copied from GetMatchingProfilesByDim() + // adapted to the optimized implementation of GetSelectFilter() + // Note: shares the cache m_aProPros with GetMatchingProfilesByDim() + // Returns null if any object is readable + // an array of allowed projections otherwise (could be an empty array if none is allowed) + protected function GetReadableProjectionsByDim($oUser, $sClass, $oDimension) + { + // + // Given a dimension, lists the values for which the user will be allowed to read the objects + // + $iUser = $oUser->GetKey(); + $iDimension = $oDimension->GetKey(); + + $aRes = array(); + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + // user projection to be cached on a given page ! + if (!isset($this->m_aProPros[$iProfile][$iDimension])) + { + // No projection for a given profile: default to 'any' + return null; + } + + $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); + if (is_null($aUserProjection)) + { + // No projection for a given profile: default to 'any' + return null; + } + $aRes = array_unique(array_merge($aRes, $aUserProjection)); + } + } + return $aRes; + } + + // Note: shares the cache m_aProPros with GetReadableProjectionsByDim() + protected function GetMatchingProfilesByDim($oUser, $oObject, $oDimension) + { + // + // List profiles for which the user projection overlaps the object projection in the given dimension + // + $iUser = $oUser->GetKey(); + $sClass = get_class($oObject); + $iPKey = $oObject->GetKey(); + $iDimension = $oDimension->GetKey(); + + if (isset($this->m_aClassProjs[$sClass][$iDimension])) + { + $aObjectProjection = $this->m_aClassProjs[$sClass][$iDimension]->ProjectObject($oObject); + } + else + { + // No projection for a given class: default to 'any' + $aObjectProjection = null; + } + + $aRes = array(); + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + if (is_null($aObjectProjection)) + { + $aRes[] = $iProfile; + } + else + { + // user projection to be cached on a given page ! + if (isset($this->m_aProPros[$iProfile][$iDimension])) + { + $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); + } + else + { + // No projection for a given profile: default to 'any' + $aUserProjection = null; + } + + if (is_null($aUserProjection)) + { + $aRes[] = $iProfile; + } + else + { + $aMatchingValues = array_intersect($aObjectProjection, $aUserProjection); + if (count($aMatchingValues) > 0) + { + $aRes[] = $iProfile; + } + } + } + } + } + return $aRes; + } + + protected $m_aMatchingProfiles = array(); // cache of the matching profiles for a given user/object + + protected function GetMatchingProfiles($oUser, $sClass, /*DBObject*/ $oObject = null) + { + $iUser = $oUser->GetKey(); + + if(is_null($oObject)) + { + $iObjectRef = -999; + } + else + { + $iObjectRef = $oObject->GetKey(); + } + + // + // List profiles for which the user projection overlaps the object projection in each and every dimension + // Caches the result + // + $aTest = @$this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef]; + if (is_array($aTest)) + { + return $aTest; + } + + if (is_null($oObject)) + { + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + $aRes = array_keys($this->m_aUserProfiles[$iUser]); + } + else + { + // no profile has been defined for this user + $aRes = array(); + } + } + else + { + $aProfileRes = array(); + foreach ($this->m_aDimensions as $iDimension => $oDimension) + { + foreach ($this->GetMatchingProfilesByDim($oUser, $oObject, $oDimension) as $iProfile) + { + @$aProfileRes[$iProfile] += 1; + } + } + + $aRes = array(); + $iDimCount = count($this->m_aDimensions); + foreach ($aProfileRes as $iProfile => $iMatches) + { + if ($iMatches == $iDimCount) + { + $aRes[] = $iProfile; + } + } + } + + // store into the cache + $this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef] = $aRes; + return $aRes; + } + + public function FlushPrivileges() + { + $this->CacheData(); + } +} + + +UserRights::SelectModule('UserRightsProjection'); + +?> diff --git a/application/ajaxwebpage.class.inc.php b/application/ajaxwebpage.class.inc.php new file mode 100644 index 0000000000..0f81c70ac8 --- /dev/null +++ b/application/ajaxwebpage.class.inc.php @@ -0,0 +1,235 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); + +class ajax_page extends WebPage +{ + /** + * Jquery style ready script + * @var Hash + */ + protected $m_sReadyScript; + protected $m_sCurrentTab; + protected $m_sCurrentTabContainer; + protected $m_aTabs; + + /** + * constructor for the web page + * @param string $s_title Not used + */ + function __construct($s_title) + { + parent::__construct($s_title); + $this->m_sReadyScript = ""; + $this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->m_sCurrentTabContainer = ''; + $this->m_sCurrentTab = ''; + $this->m_aTabs = array(); + } + + public function AddTabContainer($sTabContainer, $sPrefix = '') + { + $this->m_aTabs[$sTabContainer] = array('content' =>'', 'prefix' => $sPrefix); + $this->add("\$Tabs:$sTabContainer\$"); + } + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml) + { + if (!isset($this->m_aTabs[$sTabContainer]['content'][$sTabLabel])) + { + // Set the content of the tab + $this->m_aTabs[$sTabContainer]['content'][$sTabLabel] = $sHtml; + } + else + { + // Append to the content of the tab + $this->m_aTabs[$sTabContainer]['content'][$sTabLabel] .= $sHtml; + } + } + + public function SetCurrentTabContainer($sTabContainer = '') + { + $sPreviousTabContainer = $this->m_sCurrentTabContainer; + $this->m_sCurrentTabContainer = $sTabContainer; + return $sPreviousTabContainer; + } + + public function SetCurrentTab($sTabLabel = '') + { + $sPreviousTab = $this->m_sCurrentTab; + $this->m_sCurrentTab = $sTabLabel; + return $sPreviousTab; + } + + /** + * Echoes the content of the whole page + * @return void + */ + public function output() + { + foreach($this->a_headers as $s_header) + { + header($s_header); + } + + if (count($this->m_aTabs) > 0) + { + $this->add_ready_script( +<<m_aTabs as $sTabContainerName => $aTabContainer) + { + $sTabs = ''; + $m_aTabs = $aTabContainer['content']; + $sPrefix = $aTabContainer['prefix']; + $container_index = 0; + if (count($m_aTabs) > 0) + { + $sTabs = "\n
\n"; + $sTabs .= "\n"; + // Now add the content of the tabs themselves + $i = 0; + foreach($m_aTabs as $sTabName => $sTabContent) + { + $sTabs .= "
".$sTabContent."
\n"; + $i++; + } + $sTabs .= "
\n\n"; + } + $this->s_content = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $this->s_content); + $container_index++; + } + + $s_captured_output = ob_get_contents(); + ob_end_clean(); + echo $this->s_content; + echo $this->s_deferred_content; + if (count($this->a_scripts) > 0) + { + echo "\n"; + } + if (!empty($this->m_sReadyScript)) + { + echo "\n"; + } + if (trim($s_captured_output) != "") + { + echo $s_captured_output; + } + } + + /** + * Adds a paragraph with a smaller font into the page + * NOT implemented (i.e does nothing) + * @param string $sText Content of the (small) paragraph + * @return void + */ + public function small_p($sText) + { + } + + public function add($sHtml) + { + if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab)) + { + $this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml); + } + else + { + parent::add($sHtml); + } + } + + /** + * Adds a script to be executed when the DOM is ready (typical JQuery use) + * NOT implemented in this version of the class. + * @return void + */ + public function add_ready_script($sScript) + { + // Does nothing in ajax rendered content.. for now... + // Maybe we should add this as a simple '); // TO DO: add support for $aExtraParams in asynchronous/Ajax mode + } + */ + } + + public function GetDisplay(WebPage $oPage, $sId, $aExtraParams = array()) + { + $sHtml = ''; + $aExtraParams = array_merge($aExtraParams, $this->m_aParams); + $aExtraParams['currentId'] = $sId; + $sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them + + $bAutoReload = false; + if (isset($aExtraParams['auto_reload'])) + { + switch($aExtraParams['auto_reload']) + { + case 'fast': + $bAutoReload = true; + $iReloadInterval = MetaModel::GetConfig()->GetFastReloadInterval()*1000; + break; + + case 'standard': + case 'true': + case true: + $bAutoReload = true; + $iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000; + break; + + default: + if (is_numeric($aExtraParams['auto_reload'])) + { + $bAutoReload = true; + $iReloadInterval = $aExtraParams['auto_reload']*1000; + } + else + { + // incorrect config, ignore it + $bAutoReload = false; + } + } + } + + $sFilter = $this->m_oFilter->serialize(); // Used either for asynchronous or auto_reload + if (!$this->m_bAsynchronous) + { + // render now + $sHtml .= "
\n"; + $sHtml .= $this->GetRenderContent($oPage, $aExtraParams, $sId); + $sHtml .= "
\n"; + } + else + { + // render it as an Ajax (asynchronous) call + $sHtml .= "
\n"; + $sHtml .= $oPage->GetP(" ".Dict::S('UI:Loading')); + $sHtml .= "
\n"; + $sHtml .= ' + '; + } + if ($bAutoReload) + { + $sHtml .= ' + '; + } + return $sHtml; + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + if (empty($aExtraParams['currentId'])) + { + $sId = $oPage->GetUniqueId(); // Works only if the page is not an Ajax one ! + } + else + { + $sId = $aExtraParams['currentId']; + } + $oPage->add($this->GetRenderContent($oPage, $aExtraParams, $sId)); + } + + public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId) + { + $sHtml = ''; + // Add the extra params into the filter if they make sense for such a filter + $bDoSearch = utils::ReadParam('dosearch', false); + if ($this->m_oSet == null) + { + $aQueryParams = array(); + if (isset($aExtraParams['query_params'])) + { + $aQueryParams = $aExtraParams['query_params']; + } + if ($this->m_sStyle != 'links') + { + $oAppContext = new ApplicationContext(); + $sClass = $this->m_oFilter->GetClass(); + $aFilterCodes = array_keys(MetaModel::GetClassFilterDefs($sClass)); + foreach($oAppContext->GetNames() as $sContextParam) + { + eval("\$sParamCode = $sClass::MapContextParam('$sContextParam');"); //Map context parameter to the value/filter code depending on the class + if (!is_null($sParamCode)) + { + $sParamValue = $oAppContext->GetCurrentValue($sContextParam, null); + if (!is_null($sParamValue)) + { + $aExtraParams[$sParamCode] = $sParamValue; + } + } + } + foreach($aFilterCodes as $sFilterCode) + { + $sExternalFilterValue = utils::ReadParam($sFilterCode, ''); + $condition = null; + if (isset($aExtraParams[$sFilterCode])) + { + $condition = $aExtraParams[$sFilterCode]; + } +// else if ($bDoSearch && $sExternalFilterValue != "") + if ($bDoSearch && $sExternalFilterValue != "") + { + // Search takes precedence over context params... + unset($aExtraParams[$sFilterCode]); + $condition = trim($sExternalFilterValue); + } + + if (!is_null($condition)) + { + $this->m_oFilter->AddCondition($sFilterCode, $condition); // Use the default 'loose' operator + } + } + } + $this->m_oSet = new CMDBObjectSet($this->m_oFilter, array(), $aQueryParams); + } + switch($this->m_sStyle) + { + case 'count': + if (isset($aExtraParams['group_by'])) + { + $sGroupByField = $aExtraParams['group_by']; + $aGroupBy = array(); + $sLabels = array(); + while($oObj = $this->m_oSet->Fetch()) + { + $sValue = $oObj->Get($sGroupByField); + $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + $sLabels[$sValue] = $oObj->GetAsHtml($sGroupByField); + } + $sFilter = urlencode($this->m_oFilter->serialize()); + $aData = array(); + $oAppContext = new ApplicationContext(); + $sParams = $oAppContext->GetForLink(); + foreach($aGroupBy as $sValue => $iCount) + { + $aData[] = array ( 'group' => $sLabels[$sValue], + 'value' => "$iCount"); // TO DO: add the context information + } + $aAttribs =array( + 'group' => array('label' => MetaModel::GetLabel($this->m_oFilter->GetClass(), $sGroupByField), 'description' => ''), + 'value' => array('label'=> Dict::S('UI:GroupBy:Count'), 'description' => Dict::S('UI:GroupBy:Count+')) + ); + $sHtml .= $oPage->GetTable($aAttribs, $aData); + } + else + { + // Simply count the number of elements in the set + $iCount = $this->m_oSet->Count(); + $sFormat = 'UI:CountOfObjects'; + if (isset($aExtraParams['format'])) + { + $sFormat = $aExtraParams['format']; + } + $sHtml .= $oPage->GetP(Dict::Format($sFormat, $iCount)); + } + + break; + + case 'join': + $aDisplayAliases = isset($aExtraParams['display_aliases']) ? explode(',', $aExtraParams['display_aliases']): array(); + if (!isset($aExtraParams['group_by'])) + { + $sHtml .= $oPage->GetP(Dict::S('UI:Error:MandatoryTemplateParameter_group_by')); + } + else + { + $aGroupByFields = array(); + $aGroupBy = explode(',', $aExtraParams['group_by']); + foreach($aGroupBy as $sGroupBy) + { + $aMatches = array(); + if (preg_match('/^(.+)\.(.+)$/', $sGroupBy, $aMatches) > 0) + { + $aGroupByFields[] = array('alias' => $aMatches[1], 'att_code' => $aMatches[2]); + } + } + if (count($aGroupByFields) == 0) + { + $sHtml .= $oPage->GetP(Dict::Format('UI:Error:InvalidGroupByFields', $aExtraParams['group_by'])); + } + else + { + $aResults = array(); + $aCriteria = array(); + while($aObjects = $this->m_oSet->FetchAssoc()) + { + $aKeys = array(); + foreach($aGroupByFields as $aField) + { + $aKeys[$aField['alias'].'.'.$aField['att_code']] = $aObjects[$aField['alias']]->Get($aField['att_code']); + } + $sCategory = implode($aKeys, ' '); + $aResults[$sCategory][] = $aObjects; + $aCriteria[$sCategory] = $aKeys; + } + + $sHtml .= "\n"; + // Construct a new (parametric) query that will return the content of this block + $oBlockFilter = clone $this->m_oFilter; + $aExpressions = array(); + $index = 0; + foreach($aGroupByFields as $aField) + { + $aExpressions[] = '`'.$aField['alias'].'`.`'.$aField['att_code'].'` = :param'.$index++; + } + $sExpression = implode(' AND ', $aExpressions); + $oExpression = Expression::FromOQL($sExpression); + $oBlockFilter->AddConditionExpression($oExpression); + $aExtraParams['menu'] = false; + foreach($aResults as $sCategory => $aObjects) + { + $sHtml .= "\n"; + if (count($aDisplayAliases) == 1) + { + $aSimpleArray = array(); + foreach($aObjects as $aRow) + { + $aSimpleArray[] = $aRow[$aDisplayAliases[0]]; + } + $oSet = CMDBObjectSet::FromArray($this->m_oFilter->GetClass(), $aSimpleArray); + $sHtml .= "\n"; + } + else + { + $index = 0; + $aArgs = array(); + foreach($aGroupByFields as $aField) + { + $aArgs['param'.$index] = $aCriteria[$sCategory][$aField['alias'].'.'.$aField['att_code']]; + $index++; + } + $oSet = new CMDBObjectSet($oBlockFilter, array(), $aArgs); + $sHtml .= "\n"; + } + } + $sHtml .= "

$sCategory

".cmdbAbstractObject::GetDisplaySet($oPage, $oSet, $aExtraParams)."
".cmdbAbstractObject::GetDisplayExtendedSet($oPage, $oSet, $aExtraParams)."
\n"; + } + } + break; + + case 'list': + $aClasses = $this->m_oSet->GetSelectedClasses(); + $aAuthorizedClasses = array(); + if (count($aClasses) > 1) + { + // Check the classes that can be read (i.e authorized) by this user... + foreach($aClasses as $sAlias => $sClassName) + { + if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $this->m_oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS)) + { + $aAuthorizedClasses[$sAlias] = $sClassName; + } + } + if (count($aAuthorizedClasses) > 0) + { + if($this->m_oSet->Count() > 0) + { + $sHtml .= cmdbAbstractObject::GetDisplayExtendedSet($oPage, $this->m_oSet, $aExtraParams); + } + else + { + // Empty set + $sHtml .= $oPage->GetP(Dict::S('UI:NoObjectToDisplay')); + } + } + else + { + // Not authorized + $sHtml .= $oPage->GetP(Dict::S('UI:NoObjectToDisplay')); + } + } + else + { + // The list is made of only 1 class of objects, actions on the list are possible + if ( ($this->m_oSet->Count()> 0) && (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES) ) + { + $sHtml .= cmdbAbstractObject::GetDisplaySet($oPage, $this->m_oSet, $aExtraParams); + } + else + { + $sHtml .= $oPage->GetP(Dict::S('UI:NoObjectToDisplay')); + $sClass = $this->m_oFilter->GetClass(); + $bDisplayMenu = isset($aExtraParams['menu']) ? $aExtraParams['menu'] == true : true; + if ($bDisplayMenu) + { + if ((UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) + { + $oAppContext = new ApplicationContext(); + $sParams = $oAppContext->GetForLink(); + // 1:n links, populate the target object as a default value when creating a new linked object + if (isset($aExtraParams['target_attr'])) + { + $aExtraParams['default'][$aExtraParams['target_attr']] = $aExtraParams['object_id']; + } + $sDefault = ''; + if (!empty($aExtraParams['default'])) + { + foreach($aExtraParams['default'] as $sKey => $sValue) + { + $sDefault.= "&default[$sKey]=$sValue"; + } + } + + $sHtml .= $oPage->GetP("".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sClass))."\n"); + } + } + } + } + break; + + case 'links': + //$bDashboardMode = isset($aExtraParams['dashboard']) ? ($aExtraParams['dashboard'] == 'true') : false; + //$bSelectMode = isset($aExtraParams['select']) ? ($aExtraParams['select'] == 'true') : false; + if ( ($this->m_oSet->Count()> 0) && (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES) ) + { + //$sLinkage = isset($aExtraParams['linkage']) ? $aExtraParams['linkage'] : ''; + $sHtml .= cmdbAbstractObject::GetDisplaySet($oPage, $this->m_oSet, $aExtraParams); + } + else + { + $sClass = $this->m_oFilter->GetClass(); + $oAttDef = MetaModel::GetAttributeDef($sClass, $this->m_aParams['target_attr']); + $sTargetClass = $oAttDef->GetTargetClass(); + $sHtml .= $oPage->GetP(Dict::Format('UI:NoObject_Class_ToDisplay', MetaModel::GetName($sTargetClass))); + $bDisplayMenu = isset($this->m_aParams['menu']) ? $this->m_aParams['menu'] == true : true; + if ($bDisplayMenu) + { + if ((UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) + { + $oAppContext = new ApplicationContext(); + $sParams = $oAppContext->GetForLink(); + $sDefaults = ''; + if (isset($this->m_aParams['default'])) + { + foreach($this->m_aParams['default'] as $sName => $sValue) + { + $sDefaults .= '&'.urlencode($sName).'='.urlencode($sValue); + } + } + $sHtml .= $oPage->GetP("".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sClass))."\n"); + } + } + } + break; + + case 'details': + while($oObj = $this->m_oSet->Fetch()) + { + $sHtml .= $oObj->GetDetails($oPage); // Still used ??? + } + break; + + case 'actions': + $sClass = $this->m_oFilter->GetClass(); + $oAppContext = new ApplicationContext(); + $bContextFilter = isset($aExtraParams['context_filter']) ? isset($aExtraParams['context_filter']) != 0 : false; + if ($bContextFilter) + { + $aFilterCodes = array_keys(MetaModel::GetClassFilterDefs($this->m_oFilter->GetClass())); + foreach($oAppContext->GetNames() as $sFilterCode) + { + $sContextParamValue = $oAppContext->GetCurrentValue($sFilterCode, null); + if (!is_null($sContextParamValue) && ! empty($sContextParamValue) && MetaModel::IsValidFilterCode($sClass, $sFilterCode)) + { + $this->m_oFilter->AddCondition($sFilterCode, $sContextParamValue); // Use the default 'loose' operator + } + } + $aQueryParams = array(); + if (isset($aExtraParams['query_params'])) + { + $aQueryParams = $aExtraParams['query_params']; + } + $this->m_oSet = new CMDBObjectSet($this->m_oFilter, array(), $aQueryParams); + } + $iCount = $this->m_oSet->Count(); + $sHyperlink = '../pages/UI.php?operation=search&'.$oAppContext->GetForLink().'&filter='.$this->m_oFilter->serialize(); + $sHtml .= '

'; + $sHtml .= MetaModel::GetClassIcon($sClass, true, 'float;left;margin-right:10px;'); + $sHtml .= MetaModel::GetName($sClass).': '.$iCount.'

'; + $sParams = $oAppContext->GetForLink(); + $sHtml .= '

'; + if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY)) + { + $sHtml .= "".Dict::Format('UI:ClickToCreateNew', MetaModel::GetName($sClass))."
\n"; + } + $sHtml .= "".Dict::Format('UI:SearchFor_Class', MetaModel::GetName($sClass))."\n"; + $sHtml .= '

'; + break; + + case 'summary': + $sClass = $this->m_oFilter->GetClass(); + $oAppContext = new ApplicationContext(); + $sTitle = isset($aExtraParams['title[block]']) ? $aExtraParams['title[block]'] : ''; + $sLabel = isset($aExtraParams['label[block]']) ? $aExtraParams['label[block]'] : ''; + $sStateAttrCode = isset($aExtraParams['status[block]']) ? $aExtraParams['status[block]'] : 'status'; + $sStatesList = isset($aExtraParams['status_codes[block]']) ? $aExtraParams['status_codes[block]'] : ''; + + $bContextFilter = isset($aExtraParams['context_filter']) ? isset($aExtraParams['context_filter']) != 0 : false; + if ($bContextFilter) + { + $aFilterCodes = array_keys(MetaModel::GetClassFilterDefs($this->m_oFilter->GetClass())); + foreach($oAppContext->GetNames() as $sFilterCode) + { + $sContextParamValue = $oAppContext->GetCurrentValue($sFilterCode, null); + if (!is_null($sContextParamValue) && ! empty($sContextParamValue) && MetaModel::IsValidFilterCode($sClass, $sFilterCode)) + { + $this->m_oFilter->AddCondition($sFilterCode, $sContextParamValue); // Use the default 'loose' operator + } + } + $aQueryParams = array(); + if (isset($aExtraParams['query_params'])) + { + $aQueryParams = $aExtraParams['query_params']; + } + $this->m_oSet = new CMDBObjectSet($this->m_oFilter, array(), $aQueryParams); + } + // Summary details + $aCounts = array(); + $aStateLabels = array(); + if (!empty($sStateAttrCode) && !empty($sStatesList)) + { + $aStates = explode(',', $sStatesList); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sStateAttrCode); + foreach($aStates as $sStateValue) + { + $oFilter = clone($this->m_oFilter); + $oFilter->AddCondition($sStateAttrCode, $sStateValue, '='); + $oSet = new DBObjectSet($oFilter); + $aCounts[$sStateValue] = $oSet->Count(); + $aStateLabels[$sStateValue] = Dict::S("Class:".$oAttDef->GetHostClass()."/Attribute:$sStateAttrCode/Value:$sStateValue"); + if ($aCounts[$sStateValue] == 0) + { + $aCounts[$sStateValue] = '-'; + } + else + { + $sHyperlink = '../pages/UI.php?operation=search&'.$oAppContext->GetForLink().'&filter='.$oFilter->serialize(); + $aCounts[$sStateValue] = "{$aCounts[$sStateValue]}"; + } + } + } + $sHtml .= '
'; + $sHtml .= '
'.implode('', $aStateLabels).'
'.implode('', $aCounts).'
'; + // Title & summary + $iCount = $this->m_oSet->Count(); + $sHyperlink = '../pages/UI.php?operation=search&'.$oAppContext->GetForLink().'&filter='.$this->m_oFilter->serialize(); + $sHtml .= '

'.Dict::S(str_replace('_', ':', $sTitle)).'

'; + $sHtml .= ''.Dict::Format(str_replace('_', ':', $sLabel), $iCount).''; + break; + + case 'bare_details': + while($oObj = $this->m_oSet->Fetch()) + { + $sHtml .= $oObj->GetBareProperties($oPage); + } + break; + + case 'csv': + $sHtml .= "\n"; + break; + + case 'modify': + if ((UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_MODIFY, $this->m_oSet) == UR_ALLOWED_YES)) + { + while($oObj = $this->m_oSet->Fetch()) + { + $sHtml .= $oObj->GetModifyForm($oPage); + } + } + break; + + case 'search': + $sStyle = (isset($aExtraParams['open']) && ($aExtraParams['open'] == 'true')) ? 'SearchDrawer' : 'SearchDrawer DrawerClosed'; + $sHtml .= "
\n"; + $oPage->add_ready_script( +<<m_oSet, $aExtraParams); + $sHtml .= "
\n"; + $sHtml .= "
\n"; + $sHtml .= "
".Dict::S('UI:SearchToggle')."
\n"; + break; + + case 'open_flash_chart': + static $iChartCounter = 0; + $sChartType = isset($aExtraParams['chart_type']) ? $aExtraParams['chart_type'] : 'pie'; + $sTitle = isset($aExtraParams['chart_title']) ? $aExtraParams['chart_title'] : ''; + $sGroupBy = isset($aExtraParams['group_by']) ? $aExtraParams['group_by'] : ''; + $sFilter = $this->m_oFilter->serialize(); + $sHtml .= "
If the chart does not display, install Flash
\n"; + $oPage->add_script("function ofc_resize(left, width, top, height) { /* do nothing special */ }"); + $oPage->add_ready_script("swfobject.embedSWF(\"../images/open-flash-chart.swf\", \"my_chart_{$iChartCounter}\", \"100%\", \"300\",\"9.0.0\", \"expressInstall.swf\", + {\"data-file\":\"".urlencode("../pages/ajax.render.php?operation=open_flash_chart¶ms[group_by]=$sGroupBy¶ms[chart_type]=$sChartType¶ms[chart_title]=$sTitle&filter=".$sFilter)."\"}, {wmode: 'transparent'} );\n"); + $iChartCounter++; + break; + + case 'open_flash_chart_ajax': + require_once(APPROOT.'/pages/php-ofc-library/open-flash-chart.php'); + $sChartType = isset($aExtraParams['chart_type']) ? $aExtraParams['chart_type'] : 'pie'; + + $oChart = new open_flash_chart(); + switch($sChartType) + { + case 'bars': + $oChartElement = new bar_glass(); + + if (isset($aExtraParams['group_by'])) + { + $sGroupByField = $aExtraParams['group_by']; + $aGroupBy = array(); + while($oObj = $this->m_oSet->Fetch()) + { + $sValue = $oObj->Get($sGroupByField); + $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + } + $sFilter = urlencode($this->m_oFilter->serialize()); + $aData = array(); + $aLabels = array(); + foreach($aGroupBy as $sValue => $iValue) + { + $aData[] = $iValue; + $aLabels[] = $sValue; + } + $maxValue = max($aData); + $oYAxis = new y_axis(); + $aMagicValues = array(1,2,5,10); + $iMultiplier = 1; + $index = 0; + $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier; + while($maxValue > $iTop) + { + $index++; + $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier; + if (($index % count($aMagicValues)) == 0) + { + $iMultiplier = $iMultiplier * 10; + } + } + //echo "oYAxis->set_range(0, $iTop, $iMultiplier);\n"; + $oYAxis->set_range(0, $iTop, $iMultiplier); + $oChart->set_y_axis( $oYAxis ); + + $oChartElement->set_values( $aData ); + $oXAxis = new x_axis(); + $oXLabels = new x_axis_labels(); + // set them vertical + $oXLabels->set_vertical(); + // set the label text + $oXLabels->set_labels($aLabels); + // Add the X Axis Labels to the X Axis + $oXAxis->set_labels( $oXLabels ); + $oChart->set_x_axis( $oXAxis ); + } + break; + + case 'pie': + default: + $oChartElement = new pie(); + $oChartElement->set_start_angle( 35 ); + $oChartElement->set_animate( true ); + $oChartElement->set_tooltip( '#label# - #val# (#percent#)' ); + $oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') ); + if (isset($aExtraParams['group_by'])) + { + $sGroupByField = $aExtraParams['group_by']; + $aGroupBy = array(); + while($oObj = $this->m_oSet->Fetch()) + { + $sValue = $oObj->Get($sGroupByField); + $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + } + $sFilter = urlencode($this->m_oFilter->serialize()); + $aData = array(); + foreach($aGroupBy as $sValue => $iValue) + { + $aData[] = new pie_value($iValue, $sValue); //@@ BUG: not passed via ajax !!! + } + + + $oChartElement->set_values( $aData ); + $oChart->x_axis = null; + } + } + if (isset($aExtraParams['chart_title'])) + { + $oTitle = new title( Dict::S($aExtraParams['chart_title']) ); + $oChart->set_title( $oTitle ); + } + $oChart->set_bg_colour('#FFFFFF'); + $oChart->add_element( $oChartElement ); + + $sHtml = $oChart->toPrettyString(); + break; + + default: + // Unsupported style, do nothing. + $sHtml .= Dict::format('UI:Error:UnsupportedStyleOfBlock', $this->m_sStyle); + } + return $sHtml; + } +} + +/** + * Helper class to manage 'blocks' of HTML pieces that are parts of a page and contain some list of cmdb objects + * + * Each block is actually rendered as a
tag that can be rendered synchronously + * or as a piece of Javascript/JQuery/Ajax that will get its content from another page (ajax.render.php). + * The list of cmdbObjects to be displayed into the block is defined by a filter + * Right now the type of display is either: list, count or details + * - list produces a table listing the objects + * - count produces a paragraphs with a sentence saying 'cont' objects found + * - details display (as table) the details of each object found (best if only one) + */ +class HistoryBlock extends DisplayBlock +{ + public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId) + { + $sHtml = ''; + $oSet = new CMDBObjectSet($this->m_oFilter, array('date'=>false)); + $sHtml .= "\n"; + switch($this->m_sStyle) + { + case 'toggle': + // First the latest change that the user is allowed to see + do + { + $oLatestChangeOp = $oSet->Fetch(); + } + while(is_object($oLatestChangeOp) && ($oLatestChangeOp->GetDescription() == '')); + + if (is_object($oLatestChangeOp)) + { + // There is one change in the list... only when the object has been created ! + $sDate = $oLatestChangeOp->GetAsHTML('date'); + $oChange = MetaModel::GetObject('CMDBChange', $oLatestChangeOp->Get('change')); + $sUserInfo = $oChange->GetAsHTML('userinfo'); + $sHtml .= $oPage->GetStartCollapsibleSection(Dict::Format('UI:History:LastModified_On_By', $sDate, $sUserInfo)); + $sHtml .= $this->GetHistoryTable($oPage, $oSet); + $sHtml .= $oPage->GetEndCollapsibleSection(); + } + break; + + case 'table': + default: + $sHtml .= $this->GetHistoryTable($oPage, $oSet); + + } + return $sHtml; + } + + protected function GetHistoryTable(WebPage $oPage, DBObjectSet $oSet) + { + $sHtml = ''; + // First the latest change that the user is allowed to see + $oSet->Rewind(); // Reset the pointer to the beginning of the set + $aChanges = array(); + while($oChangeOp = $oSet->Fetch()) + { + $sChangeDescription = $oChangeOp->GetDescription(); + if ($sChangeDescription != '') + { + // The change is visible for the current user + $changeId = $oChangeOp->Get('change'); + $aChanges[$changeId]['date'] = $oChangeOp->Get('date'); + $aChanges[$changeId]['userinfo'] = $oChangeOp->Get('userinfo'); + if (!isset($aChanges[$changeId]['log'])) + { + $aChanges[$changeId]['log'] = array(); + } + $aChanges[$changeId]['log'][] = $sChangeDescription; + } + } + $aAttribs = array('date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), + 'userinfo' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), + 'log' => array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+')), + ); + $aValues = array(); + foreach($aChanges as $aChange) + { + $aValues[] = array('date' => $aChange['date'], 'userinfo' => $aChange['userinfo'], 'log' => "
  • ".implode('
  • ', $aChange['log'])."
"); + } + $sHtml .= $oPage->GetTable($aAttribs, $aValues); + return $sHtml; + } +} + +class MenuBlock extends DisplayBlock +{ + /** + * Renders the "Actions" popup menu for the given set of objects + * + * Note that the menu links containing (or ending) with a hash (#) will have their fragment + * part (whatever is after the hash) dynamically replaced (by javascript) when the menu is + * displayed, to correspond to the current hash/fragment in the page. This allows modifying + * an object in with the same tab active by default as the tab that was active when selecting + * the "Modify..." action. + */ + public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId) + { + $sHtml = ''; + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + $sClass = $this->m_oFilter->GetClass(); + $oSet = new CMDBObjectSet($this->m_oFilter); + $sFilter = $this->m_oFilter->serialize(); + $aActions = array(); + $sUIPage = cmdbAbstractObject::ComputeUIPage($sClass); + // 1:n links, populate the target object as a default value when creating a new linked object + if (isset($aExtraParams['target_attr'])) + { + $aExtraParams['default'][$aExtraParams['target_attr']] = $aExtraParams['object_id']; + } + $sDefault = ''; + if (!empty($aExtraParams['default'])) + { + foreach($aExtraParams['default'] as $sKey => $sValue) + { + $sDefault.= "&default[$sKey]=$sValue"; + } + } + switch($oSet->Count()) + { + case 0: + // No object in the set, the only possible action is "new" + $bIsModifyAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES); + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:New'), 'url' => "../page/$sUIPage?operation=new&class=$sClass&$sContext{$sDefault}"); } + break; + + case 1: + $oObj = $oSet->Fetch(); + $id = $oObj->GetKey(); + $bIsModifyAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES); + $bIsDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet); + $bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sClass)) && UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet); + $bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet); + // Just one object in the set, possible actions are "new / clone / modify and delete" + if (!isset($aExtraParams['link_attr'])) + { + $sUrl = utils::GetAbsoluteUrl(false); + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Modify'), 'url' => "../pages/$sUIPage?operation=modify&class=$sClass&id=$id&$sContext#"); } + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:New'), 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext{$sDefault}"); } + if ($bIsDeleteAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Delete'), 'url' => "../pages/$sUIPage?operation=delete&class=$sClass&id=$id&$sContext"); } + // Transitions / Stimuli + $aTransitions = $oObj->EnumTransitions(); + if (count($aTransitions)) + { + $this->AddMenuSeparator($aActions); + $aStimuli = Metamodel::EnumStimuli($sClass); + foreach($aTransitions as $sStimulusCode => $aTransitionDef) + { + $iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass, $sStimulusCode, $oSet) : UR_ALLOWED_NO; + switch($iActionAllowed) + { + case UR_ALLOWED_YES: + $aActions[] = array('label' => $aStimuli[$sStimulusCode]->GetLabel(), 'url' => "../pages/UI.php?operation=stimulus&stimulus=$sStimulusCode&class=$sClass&id=$id&$sContext"); + break; + + default: + // Do nothing + } + } + } + // Relations... + $aRelations = MetaModel::EnumRelations($sClass); + if (count($aRelations)) + { + $this->AddMenuSeparator($aActions); + foreach($aRelations as $sRelationCode) + { + $aActions[] = array ('label' => MetaModel::GetRelationVerbUp($sRelationCode), 'url' => "../pages/$sUIPage?operation=swf_navigator&relation=$sRelationCode&class=$sClass&id=$id&$sContext"); + } + } + $this->AddMenuSeparator($aActions); + // Static menus: Email this page & CSV Export + $aActions[] = array ('label' => Dict::S('UI:Menu:EMail'), 'url' => "mailto:?subject=".$oObj->GetName()."&body=".urlencode("$sUrl?operation=details&class=$sClass&id=$id&$sContext")); + $aActions[] = array ('label' => Dict::S('UI:Menu:CSVExport'), 'url' => "../pages/$sUIPage?operation=search&filter=$sFilter&format=csv&$sContext"); + } + else + { + // List of links, the only actions are 'Add...' and 'Manage...' + $id = $aExtraParams['object_id']; + $sTargetAttr = $aExtraParams['target_attr']; + $oAttDef = MetaModel::GetAttributeDef($sClass, $sTargetAttr); + $sTargetClass = $oAttDef->GetTargetClass(); + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Add'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&addObjects=true&$sContext"); } + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Manage'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&sContext"); } + } + break; + + default: + // Check rights + // New / Modify + $bIsModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet); + $bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sClass)) && UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet); + $bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet); + if (isset($aExtraParams['link_attr'])) + { + $id = $aExtraParams['object_id']; + $sTargetAttr = $aExtraParams['target_attr']; + $oAttDef = MetaModel::GetAttributeDef($sClass, $sTargetAttr); + $sTargetClass = $oAttDef->GetTargetClass(); + $bIsDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet); + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Add'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&addObjects=true&$sContext"); } + if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Manage'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&sContext"); } + //if ($bIsBulkDeleteAllowed) { $aActions[] = array ('label' => 'Remove All...', 'url' => "#"); } + } + else + { + // many objects in the set, possible actions are: new / modify all / delete all + $sUrl = utils::GetAbsoluteUrl(); + if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:New'), 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext{$sDefault}"); } + //if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => 'Modify All...', 'url' => "../pages/$sUIPage?operation=modify_all&filter=$sFilter&$sContext"); } + if ($bIsBulkDeleteAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:BulkDelete'), 'url' => "../pages/$sUIPage?operation=select_for_deletion&filter=$sFilter&$sContext"); } + $this->AddMenuSeparator($aActions); + $aActions[] = array ('label' => Dict::S('UI:Menu:EMail'), 'url' => "mailto:?subject=".$oSet->GetFilter()->__DescribeHTML()."&body=".urlencode("$sUrl?operation=search&filter=$sFilter&$sContext")); + $aActions[] = array ('label' => Dict::S('UI:Menu:CSVExport'), 'url' => "../pages/$sUIPage?operation=search&filter=$sFilter&format=csv&$sContext"); + } + } + $sHtml .= "
    \n
  • ".Dict::S('UI:Menu:Actions')."\n
      \n"; + foreach ($aActions as $aAction) + { + $sClass = isset($aAction['class']) ? " class=\"{$aAction['class']}\"" : ""; + if (empty($aAction['url'])) + { + $sHtml .= "
    • {$aAction['label']}
    • \n"; + } + else + { + $sHtml .= "
    • {$aAction['label']}
    • \n"; + } + } + $sHtml .= "
    \n
  • \n
\n"; + static $bPopupScript = false; + if (!$bPopupScript) + { + // Output this once per page... + $oPage->add_ready_script("$(\"div.itop_popup>ul\").popupmenu();\n"); + $bPopupScript = true; + } + return $sHtml; + } + + /** + * Appends a menu separator to the current list of actions + * @param Hash $aActions The current actions list + * @return void + */ + protected function AddMenuSeparator(&$aActions) + { + $sSeparator = ''; + if (count($aActions) > 0) // Make sure that the separator is not the first item in the menu + { + if ($aActions[count($aActions)-1]['label'] != $sSeparator) // Make sure there are no 2 consecutive separators + { + $aActions[] = array('label' => $sSeparator, 'url' => ''); + } + } + } +} +?> diff --git a/application/iotask.class.inc.php b/application/iotask.class.inc.php new file mode 100644 index 0000000000..21f002d83c --- /dev/null +++ b/application/iotask.class.inc.php @@ -0,0 +1,68 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); + +/** + * This class manages the input/output tasks + * for synchronizing information with external data sources + */ +class InputOutputTask extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "application", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_iotask", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("category", array("allowed_values"=>new ValueSetEnum('Input, Ouput'), "sql"=>"category", "default_value"=>"Input", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("source_type", array("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("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("allowed_values"=>null, "sql"=>"source_path", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeClass("objects_class", array("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("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("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("allowed_values"=>new ValueSetEnum('Full, Update Only, Creation Only'), "sql"=>"options", "default_value"=> 'Full', "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype', 'source_path' , 'options', 'test_mode', 'verbose_mode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description', 'category', 'objects_class', 'source_type', 'source_subtype', 'options')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the advanced search form + } +} +?> diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php new file mode 100644 index 0000000000..ae5b2fa95c --- /dev/null +++ b/application/itopwebpage.class.inc.php @@ -0,0 +1,908 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/nicewebpage.class.inc.php"); +require_once(APPROOT."/application/applicationcontext.class.inc.php"); +require_once(APPROOT."/application/user.preferences.class.inc.php"); +/** + * Web page with some associated CSS and scripts (jquery) for a fancier display + */ +class iTopWebPage extends NiceWebPage +{ + private $m_sMenu; +// private $m_currentOrganization; + private $m_aTabs; + private $m_sCurrentTabContainer; + private $m_sCurrentTab; + + public function __construct($sTitle) + { + parent::__construct($sTitle); + $this->m_sCurrentTabContainer = ''; + $this->m_sCurrentTab = ''; + $this->m_aTabs = array(); + $this->m_sMenu = ""; + $oAppContext = new ApplicationContext(); + $sExtraParams = $oAppContext->GetForLink(); +// $this->m_currentOrganization = $currentOrganization; + $this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->add_linked_stylesheet("../css/jquery.treeview.css"); + $this->add_linked_stylesheet("../css/jquery.autocomplete.css"); +// $this->add_linked_stylesheet("../css/date.picker.css"); + $this->add_linked_script('../js/jquery.layout.min.js'); + $this->add_linked_script('../js/jquery.ba-bbq.min.js'); +// $this->add_linked_script("../js/jquery.dimensions.js"); + $this->add_linked_script("../js/jquery.tablehover.js"); + $this->add_linked_script("../js/jquery.treeview.js"); + $this->add_linked_script("../js/jquery.autocomplete.js"); + $this->add_linked_script("../js/jquery.bgiframe.js"); + $this->add_linked_script("../js/jquery.positionBy.js"); + $this->add_linked_script("../js/jquery.popupmenu.js"); + $this->add_linked_script("../js/date.js"); +// $this->add_linked_script("../js/jquery.date.picker.js"); + $this->add_linked_script("../js/jquery.tablesorter.min.js"); + $this->add_linked_script("../js/jquery.blockUI.js"); + $this->add_linked_script("../js/utils.js"); + $this->add_linked_script("../js/swfobject.js"); + $this->add_linked_script("../js/ckeditor/ckeditor.js"); + $this->add_linked_script("../js/ckeditor/adapters/jquery.js"); + $this->add_ready_script( +<<'); + if (GetUserPreference('menu_pane', 'open') == 'closed') + { + myLayout.close('west'); + } + + // Accordion Menu + $("#accordion").accordion({ header: "h3", navigation: true, autoHeight: false, collapsible: false, icons: false }); // collapsible will be enabled once the item will be selected + }); + //add new widget called TruncatedList to properly display truncated lists when they are sorted + $.tablesorter.addWidget({ + // give the widget a id + id: "truncatedList", + // format is called when the on init and when a sorting has finished + format: function(table) + { + // Check if there is a "truncated" line + this.truncatedList = false; + if ($("tr td.truncated",table).length > 0) + { + this.truncatedList = true; + } + if (this.truncatedList) + { + $("tr td",table).removeClass('truncated'); + $("tr:last td",table).addClass('truncated'); + } + } + }); + + + $.tablesorter.addWidget({ + // give the widget a id + id: "myZebra", + // format is called when the on init and when a sorting has finished + format: function(table) + { + // Replace the 'red even' lines by 'red_even' since most browser do not support 2 classes selector in CSS, etc.. + $("tbody tr:even",table).addClass('even'); + $("tbody tr.red:even",table).removeClass('red').removeClass('even').addClass('red_even'); + $("tbody tr.orange:even",table).removeClass('orange').removeClass('even').addClass('orange_even'); + $("tbody tr.green:even",table).removeClass('green').removeClass('even').addClass('green_even'); + // In case we sort again the table, we need to remove the added 'even' classes on odd rows + $("tbody tr:odd",table).removeClass('even'); + $("tbody tr.red_even:odd",table).removeClass('even').removeClass('red_even').addClass('red'); + $("tbody tr.orange_even:odd",table).removeClass('even').removeClass('orange_even').addClass('orange'); + $("tbody tr.green_even:odd",table).removeClass('even').removeClass('green_even').addClass('green'); + } + }); + + $('.resizable').resizable(); // Make resizable everything that claims to be resizable ! + // Adjust initial size + $('.v-resizable').each( function() + { + var parent_id = $(this).parent().id; + // Restore the saved height + var iHeight = GetUserPreference(parent_id+'_'+this.id+'_height', undefined); + if (iHeight != undefined) + { + $(this).height(parseInt(iHeight, 10)); // Parse in base 10 !); + } + // Adjust the child 'item''s height and width to fit + var container = $(this); + var fixedWidth = container.parent().innerWidth() - 6; + // Set the width to fit the parent + $(this).width(fixedWidth); + var headerHeight = $(this).find('.drag_handle').height(); + // Now adjust the width and height of the child 'item' + container.find('.item').height(container.innerHeight() - headerHeight - 12).width(fixedWidth - 10); + } + ); + // Make resizable, vertically only everything that claims to be v-resizable ! + $('.v-resizable').resizable( { handles: 's', minHeight: $(this).find('.drag_handle').height(), minWidth: $(this).parent().innerWidth() - 6, maxWidth: $(this).parent().innerWidth() - 6, stop: function() + { + // Adjust the content + var container = $(this); + var headerHeight = $(this).find('.drag_handle').height(); + container.find('.item').height(container.innerHeight() - headerHeight - 12);//.width(container.innerWidth()); + var parent_id = $(this).parent().id; + SetUserPreference(parent_id+'_'+this.id+'_height', $(this).height(), true); // true => persistent + } + } ); + + // Tabs, using JQuery BBQ to store the history + // The "tab widgets" to handle. + var tabs = $('div[id^=tabbedContent]'); + + // This selector will be reused when selecting actual tab widget A elements. + var tab_a_selector = 'ul.ui-tabs-nav a'; + + // Enable tabs on all tab widgets. The `event` property must be overridden so + // that the tabs aren't changed on click, and any custom event name can be + // specified. Note that if you define a callback for the 'select' event, it + // will be executed for the selected tab whenever the hash changes. + tabs.tabs({ event: 'change' }); + + // Define our own click handler for the tabs, overriding the default. + tabs.find( tab_a_selector ).click(function() + { + var state = {}; + + // Get the id of this tab widget. + var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); + + // Get the index of this tab. + var idx = $(this).parent().prevAll().length; + + // Set the state! + state[ id ] = idx; + $.bbq.pushState( state ); + }); + + // Bind an event to window.onhashchange that, when the history state changes, + // iterates over all tab widgets, changing the current tab as necessary. + $(window).bind( 'hashchange', function(e) + { + // Iterate over all tab widgets. + tabs.each(function() + { + // Get the index for this tab widget from the hash, based on the + // appropriate id property. In jQuery 1.4, you should use e.getState() + // instead of $.bbq.getState(). The second, 'true' argument coerces the + // string value to a number. + var idx = $.bbq.getState( this.id, true ) || 0; + + // Select the appropriate tab for this tab widget by triggering the custom + // event specified in the .tabs() init above (you could keep track of what + // tab each widget is on using .data, and only select a tab if it has + // changed). + $(this).find( tab_a_selector ).eq( idx ).triggerHandler( 'change' ); + }); + + // Iterate over all truncated lists to find whether they are expanded or not + $('a.truncated').each(function() + { + var state = $.bbq.getState( this.id, true ) || 'close'; + if (state == 'open') + { + $(this).trigger('open'); + } + else + { + $(this).trigger('close'); + } + }); + }); + + // End of Tabs handling + $("table.listResults").tableHover(); // hover tables + // Check each 'listResults' table for a checkbox in the first column and make the first column sortable only if it does not contain a checkbox in the header + $(".listResults").each( function() + { + var table = $(this); + var id = $(this).parent(); + var checkbox = (table.find('th:first :checkbox').length > 0); + if (checkbox) + { + // There is a checkbox in the first column, don't make it sortable + table.tablesorter( { headers: { 0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables + } + else + { + // There is NO checkbox in the first column, all columns are considered sortable + table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables + } + }); + $(".date-pick").datepicker({ + showOn: 'button', + buttonImage: '../images/calendar.png', + buttonImageOnly: true, + dateFormat: 'yy-mm-dd', + constrainInput: false, + changeMonth: true, + changeYear: true + }); + // Restore the persisted sortable order, for all sortable lists... if any + $('.sortable').each(function() + { + var sTemp = GetUserPreference(this.id+'_order', undefined); + if (sTemp != undefined) + { + var aSerialized = sTemp.split(','); + var sortable = $(this); + $.each(aSerialized, function(i,v) { + var item = $('#menu_'+v); + if (item.length > 0) // Check that the menu exists + { + sortable.append(item); + } + }); + } + }); + + // Make sortable, everything that claims to be sortable + $('.sortable').sortable( {axis: 'y', cursor: 'move', handle: '.drag_handle', stop: function() + { + if ($(this).hasClass('persistent')) + { + // remember the sort order for next time the page is loaded... + sSerialized = $(this).sortable('serialize', {key: 'menu'}); + var sTemp = sSerialized.replace(/menu=/g, ''); + SetUserPreference(this.id+'_order', sTemp.replace(/&/g, ','), true); // true => persistent ! + } + } + }); + docWidth = $(document).width(); + $('#ModalDlg').dialog({ autoOpen: false, modal: true, width: 0.8*docWidth }); // JQuery UI dialogs + ShowDebug(); + $('#logOffBtn>ul').popupmenu(); +// $.history.init(history_callback); +// $("a[rel='history']").click(function() +// { +// $.history.load(this.href.replace(/^.*#/, '')); +// return false; +// }); + } + catch(err) + { + // Do something with the error ! + alert(err); + } + + //$('.display_block').draggable(); // make the blocks draggable +EOF +); + $sUserPrefs = appUserPreferences::GetAsJSON(); + $this->add_script( +<< 0) + { + window.location.href = './UI.php?operation=details&class='+sClass+'&id='+id; + } + else + { + window.location.href = sDefaultUrl; + } + } + + + function BackToList(sClass) + { + window.location.href = './UI.php?operation=search_oql&oql_class='+sClass+'&oql_clause=WHERE id=0'; + } + + function ShowDebug() + { + if ($('#rawOutput > div').html() != '') + { + $('#rawOutput').dialog( {autoOpen: true, modal:false}); + } + } + + var oUserPreferences = $sUserPrefs; +EOF +); + + // Build menus from module handlers + // + foreach(get_declared_classes() as $sPHPClass) + { + if (is_subclass_of($sPHPClass, 'ModuleHandlerAPI')) + { + $aCallSpec = array($sPHPClass, 'OnMenuCreation'); + call_user_func($aCallSpec); + } + } + } + + public function AddToMenu($sHtml) + { + $this->m_sMenu .= $sHtml; + } + + public function GetSiloSelectionForm() + { + // List of visible Organizations + $iCount = 0; + if (MetaModel::IsValidClass('Organization')) + { + $oSearchFilter = new DBObjectSearch('Organization'); + $oSet = new CMDBObjectSet($oSearchFilter); + $iCount = $oSet->Count(); + } + switch($iCount) + { + case 0: + // No such dimension/silo => nothing to select + $sHtml = '
'; + break; + + case 1: + // Only one possible choice... no selection, but display the value + $oOrg = $oSet->Fetch(); + $sHtml = '
'.$oOrg->GetName().'
'; + $sHtml .= ''; + break; + + default: + $oAppContext = new ApplicationContext(); + $iCurrentOrganization = $oAppContext->GetCurrentValue('org_id'); + $sHtml = '
'; + $sHtml .= '
'; + // Add other dimensions/context information to this form +// $oAppContext = new ApplicationContext(); + $oAppContext->Reset('org_id'); // org_id is handled above and we want to be able to change it here ! + $sHtml .= $oAppContext->GetForForm(); + $sHtml .= '
'; + $sHtml .= '
'; + } + return $sHtml; + } + + public function DisplayMenu() + { + // Display the menu + $oAppContext = new ApplicationContext(); + $iAccordionIndex = 0; + + ApplicationMenu::DisplayMenu($this, $oAppContext->GetAsHash()); + } + + /** + * Outputs (via some echo) the complete HTML page by assembling all its elements + */ + public function output() + { + $this->DisplayMenu(); // Compute the menu + + // Put here the 'ready scripts' that must be executed after all others + $this->add_ready_script( +<<a_headers as $s_header) + { + header($s_header); + } + $s_captured_output = ob_get_contents(); + ob_end_clean(); + echo "\n"; + echo "\n"; + echo "\n"; + // Make sure that Internet Explorer renders the page using its latest/highest/greatest standards ! + echo "\n"; + echo "\n"; + echo "{$this->s_title}\n"; + echo $this->get_base_tag(); + // Stylesheets MUST be loaded before any scripts otherwise + // jQuery scripts may face some spurious problems (like failing on a 'reload') + foreach($this->a_linked_stylesheets as $a_stylesheet) + { + if ($a_stylesheet['condition'] != "") + { + echo "\n"; + } + } + foreach($this->a_linked_scripts as $s_script) + { + echo "\n"; + } + if (count($this->m_aReadyScripts)>0) + { + $this->add_script("\$(document).ready(function() {\n".implode("\n", $this->m_aReadyScripts)."\n});"); + } + if (count($this->a_scripts)>0) + { + echo "\n"; + } + + if (count($this->a_styles)>0) + { + echo "\n"; + } + echo "\n"; + echo "\n"; + echo "\n"; + + + + + + + + + // Render the revision number + if (ITOP_REVISION == '$WCREV$') + { + // This is NOT a version built using the buil system, just display the main version + $sVersionString = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION); + } + else + { + // This is a build made from SVN, let display the full information + $sVersionString = Dict::Format('UI:iTopVersion:Long', ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE); + } + + // Render the text of the global search form + $sText = Utils::ReadParam('text', ''); + $sOnClick = ""; + if (empty($sText)) + { + // if no search text is supplied then + // 1) the search text is filled with "your search" + // 2) clicking on it will erase it + $sText = Dict::S("UI:YourSearch"); + $sOnClick = " onclick=\"this.value='';this.onclick=null;\""; + } + + $sForm = $this->GetSiloSelectionForm(); + + // Render the tabs in the page (if any) + foreach($this->m_aTabs as $sTabContainerName => $m_aTabs) + { + $sTabs = ''; + $container_index = 0; + if (count($m_aTabs) > 0) + { + $sTabs = "\n
\n"; + $sTabs .= "\n"; + // Now add the content of the tabs themselves + $i = 0; + foreach($m_aTabs as $sTabName => $sTabContent) + { + $sTabs .= "
".$sTabContent."
\n"; + $i++; + } + $sTabs .= "
\n\n"; + } + $this->s_content = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $this->s_content); + $container_index++; + } + $sUserName = UserRights::GetUser(); + $sIsAdmin = UserRights::IsAdministrator() ? '(Administrator)' : ''; + if (UserRights::IsAdministrator()) + { + $sLogonMessage = Dict::Format('UI:LoggedAsMessage+Admin', $sUserName); + } + else + { + $sLogonMessage = Dict::Format('UI:LoggedAsMessage', $sUserName); + } + $sLogOffMenu = "\n"; + + $sRestrictions = ''; + if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE)) + { + if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE)) + { + $sRestrictions = Dict::S('UI:AccessRO-All'); + } + } + elseif (!MetaModel::DBHasAccess(ACCESS_USER_WRITE)) + { + $sRestrictions = Dict::S('UI:AccessRO-Users'); + } + + if (strlen($sRestrictions) > 0) + { + $sAdminMessage = trim(MetaModel::GetConfig()->Get('access_message')); + $sApplicationBanner = '
'; + $sApplicationBanner .= ''; + $sApplicationBanner .= ' '.$sRestrictions.''; + if (strlen($sAdminMessage) > 0) + { + $sApplicationBanner .= ' '.$sAdminMessage.''; + } + $sApplicationBanner .= '
'; + } + else + { + $sApplicationBanner = ''; + } + + $sOnlineHelpUrl = MetaModel::GetConfig()->Get('online_help'); + //$sLogOffMenu = ""; + + echo '
'; + echo ''; + echo ' '; + echo '
'; + echo '
pin
'; + echo '
'.$sForm.'
'; + echo '
'; + echo ' '; + echo ' '; + echo ''; + echo '
'; + + echo '
'; + echo '
'; + echo $sApplicationBanner; + echo ' '; + //echo '        
'; + echo '
'; + echo '
'; + echo ' '; + echo $this->s_content; + echo ' '; + echo '
'; + echo ''; +/* + echo "
iTop
\n"; + //echo "
\n"; + $sText = Utils::ReadParam('text', ''); + $sOnClick = ""; + if (empty($sText)) + { + // if no search text is supplied then + // 1) the search text is filled with "your search" + // 2) clicking on it will erase it + $sText = Dict::S("UI:YourSearch"); + $sOnClick = " onclick=\"this.value='';this.onclick=null;\""; + } + $sUserName = UserRights::GetUser(); + $sIsAdmin = UserRights::IsAdministrator() ? '(Administrator)' : ''; + if (UserRights::IsAdministrator()) + { + $sLogonMessage = Dict::Format('UI:LoggedAsMessage+Admin', $sUserName); + } + else + { + $sLogonMessage = Dict::Format('UI:LoggedAsMessage', $sUserName); + } + $sLogOffBtn = Dict::S('UI:Button:Logoff'); + $sSearchBtn = Dict::S('UI:Button:GlobalSearch'); + echo "
{$sLogonMessage}  "; + echo "
\n"; + echo "\n"; + echo "\n"; + echo "
\n"; + echo "
+
\n"; + echo "
\n"; + + echo "\n"; + + // Display the menu + echo "
\n"; + echo "
\n"; + echo $this->m_sMenu; + echo "
\n"; + + echo "
\n"; + + + // Display the page's content + echo $this->s_content; + +*/ + // Add the captured output + if (trim($s_captured_output) != "") + { + echo "
$s_captured_output
\n"; + } + echo $this->s_deferred_content; + echo "
Please wait...
\n"; // jqModal Window + echo "
"; + echo "
"; + + echo "\n"; + echo "\n"; + } + + public function AddTabContainer($sTabContainer) + { + $this->m_aTabs[$sTabContainer] = array(); + $this->add("\$Tabs:$sTabContainer\$"); + } + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml) + { + if (!isset($this->m_aTabs[$sTabContainer][$sTabLabel])) + { + // Set the content of the tab + $this->m_aTabs[$sTabContainer][$sTabLabel] = $sHtml; + } + else + { + // Append to the content of the tab + $this->m_aTabs[$sTabContainer][$sTabLabel] .= $sHtml; + } + } + + public function SetCurrentTabContainer($sTabContainer = '') + { + $sPreviousTabContainer = $this->m_sCurrentTabContainer; + $this->m_sCurrentTabContainer = $sTabContainer; + return $sPreviousTabContainer; + } + + public function SetCurrentTab($sTabLabel = '') + { + $sPreviousTab = $this->m_sCurrentTab; + $this->m_sCurrentTab = $sTabLabel; + return $sPreviousTab; + } + + /** + * Make the given tab the active one, as if it were clicked + * DOES NOT WORK: apparently in the *old* version of jquery + * that we are using this is not supported... TO DO upgrade + * the whole jquery bundle... + */ + public function SelectTab($sTabContainer, $sTabLabel) + { + $container_index = 0; + $tab_index = 0; + foreach($this->m_aTabs as $sCurrentTabContainerName => $aTabs) + { + if ($sTabContainer == $sCurrentTabContainerName) + { + foreach($aTabs as $sCurrentTabLabel => $void) + { + if ($sCurrentTabLabel == $sTabLabel) + { + break; + } + $tab_index++; + } + break; + } + $container_index++; + } + $sSelector = '#tabbedContent_'.$container_index.' > ul'; + $this->add_ready_script("$('$sSelector').tabs('select', $tab_index);"); + } + + public function StartCollapsibleSection($sSectionLabel, $bOpen = false) + { + $this->add($this->GetStartCollapsibleSection($sSectionLabel, $bOpen)); + } + + public function GetStartCollapsibleSection($sSectionLabel, $bOpen = false) + { + $sHtml = ''; + static $iSectionId = 0; + $sImgStyle = $bOpen ? ' open' : ''; + $sHtml .= "$sSectionLabel
\n"; + $sStyle = $bOpen ? '' : 'style="display:none" '; + $sHtml .= "
"; + $this->add_ready_script("\$(\"#LnkCollapse_$iSectionId\").click(function() {\$(\"#Collapse_$iSectionId\").slideToggle('normal'); $(\"#LnkCollapse_$iSectionId\").toggleClass('open');});"); + //$this->add_ready_script("$('#LnkCollapse_$iSectionId').hide();"); + $iSectionId++; + return $sHtml; + } + + public function EndCollapsibleSection() + { + $this->add($this->GetEndCollapsibleSection()); + } + + public function GetEndCollapsibleSection() + { + return "
"; + } + + public function add($sHtml) + { + if (!empty($this->m_sCurrentTabContainer) && !empty($this->m_sCurrentTab)) + { + $this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml); + } + else + { + parent::add($sHtml); + } + } + + /* + public function AddSearchForm($sClassName, $bOpen = false) + { + $iSearchSectionId = 0; + + $sStyle = $bOpen ? 'SearchDrawer' : 'SearchDrawer DrawerClosed'; + $this->add("
\n"); + $this->add("

Search form for ".Metamodel::GetName($sClassName)."

\n"); + $this->add_ready_script("\$(\"#LnkSearch_$iSearchSectionId\").click(function() {\$(\"#Search_$iSearchSectionId\").slideToggle('normal'); $(\"#LnkSearch_$iSearchSectionId\").toggleClass('open');});"); + $oFilter = new DBObjectSearch($sClassName); + $sFilter = $oFilter->serialize(); + $oSet = new CMDBObjectSet($oFilter); + cmdbAbstractObject::DisplaySearchForm($this, $oSet, array('operation' => 'search', 'filter' => $sFilter, 'search_form' => true)); + $this->add("
\n"); + $this->add("
\n"); + $this->add("
Search
\n"); + + + $iSearchSectionId++; + } + */ +} + +?> diff --git a/application/itopwizardwebpage.class.inc.php b/application/itopwizardwebpage.class.inc.php new file mode 100644 index 0000000000..3b03af1b6b --- /dev/null +++ b/application/itopwizardwebpage.class.inc.php @@ -0,0 +1,56 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once('itopwebpage.class.inc.php'); +/** + * Web page to display a wizard in the iTop framework + */ +class iTopWizardWebPage extends iTopWebPage +{ + var $m_iCurrentStep; + var $m_aSteps; + public function __construct($sTitle, $currentOrganization, $iCurrentStep, $aSteps) + { + parent::__construct($sTitle." - step $iCurrentStep of ".count($aSteps)." - ".$aSteps[$iCurrentStep - 1], $currentOrganization); + $this->m_iCurrentStep = $iCurrentStep; + $this->m_aSteps = $aSteps; + } + + public function output() + { + $aSteps = array(); + $iIndex = 0; + foreach($this->m_aSteps as $sStepTitle) + { + $iIndex++; + $sStyle = ($iIndex == $this->m_iCurrentStep) ? 'wizActiveStep' : 'wizStep'; + $aSteps[] = "
$sStepTitle
"; + } + $sWizardHeader = "

{$this->s_title}

\n".implode("
", $aSteps)."
\n"; + $this->s_content = "$sWizardHeader
".$this->s_content."
"; + parent::output(); + } +} +?> diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php new file mode 100644 index 0000000000..320a77073f --- /dev/null +++ b/application/loginwebpage.class.inc.php @@ -0,0 +1,415 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/nicewebpage.class.inc.php"); +/** + * Web page used for displaying the login form + */ +class LoginWebPage extends NiceWebPage +{ + public function __construct() + { + parent::__construct("iTop Login"); + $this->add_style(<<add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION)); + $this->add_header('HTTP/1.0 401 Unauthorized'); + // Note: displayed when the user will click on Cancel + $this->add('

'.Dict::S('UI:Login:Error:AccessRestricted').'

'); + break; + + case 'external': + case 'form': + default: // In case the settings get messed up... + $sAuthUser = utils::ReadParam('auth_user', ''); + $sAuthPwd = utils::ReadParam('suggest_pwd', ''); + + $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION); + $this->add("
\n"); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:Welcome')."

\n"); + if ($bFailedLogin) + { + $this->add("

".Dict::S('UI:Login:IncorrectLoginPassword')."

\n"); + } + else + { + $this->add("

".Dict::S('UI:Login:IdentifyYourself')."

\n"); + } + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("
\n"); + break; + } + } + + public function DisplayChangePwdForm($bFailedLogin = false) + { + $sAuthUser = utils::ReadParam('auth_user', ''); + $sAuthPwd = utils::ReadParam('suggest_pwd', ''); + + $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION); + $sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch'); + $this->add_script(<<add("
\n"); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:ChangeYourPassword')."

\n"); + if ($bFailedLogin) + { + $this->add("

".Dict::S('UI:Login:IncorrectOldPassword')."

\n"); + } + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
  
\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("
\n"); + } + + static function ResetSession() + { + if (isset($_SESSION['login_mode'])) + { + $sPreviousLoginMode = $_SESSION['login_mode']; + } + else + { + $sPreviousLoginMode = ''; + } + // Unset all of the session variables. + $_SESSION = array(); + // If it's desired to kill the session, also delete the session cookie. + // Note: This will destroy the session, and not just the session data! + if (isset($_COOKIE[session_name()])) + { + setcookie(session_name(), '', time()-3600, '/'); + } + // Finally, destroy the session. + session_destroy(); + } + + static function SecureConnectionRequired() + { + return MetaModel::GetConfig()->GetSecureConnectionRequired(); + } + + static function IsConnectionSecure() + { + $bSecured = false; + + if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off')) + { + $bSecured = true; + } + return $bSecured; + } + + protected static function Login() + { + if (self::SecureConnectionRequired() && !self::IsConnectionSecure()) + { + // Non secured URL... redirect to a secured one + $sUrl = Utils::GetAbsoluteUrl(true /* query string */, true /* force HTTPS */); + header("Location: $sUrl"); + exit; + } + + $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); + + if (isset($_SESSION['auth_user'])) + { + //echo "User: ".$_SESSION['auth_user']."\n"; + // Already authentified + UserRights::Login($_SESSION['auth_user']); // Login & set the user's language + return true; + } + else + { + $index = 0; + $sLoginMode = ''; + $sAuthentication = 'internal'; + while(($sLoginMode == '') && ($index < count($aAllowedLoginTypes))) + { + $sLoginType = $aAllowedLoginTypes[$index]; + switch($sLoginType) + { + case 'form': + // iTop standard mode: form based authentication + $sAuthUser = utils::ReadPostedParam('auth_user', ''); + $sAuthPwd = utils::ReadPostedParam('auth_pwd', ''); + if ($sAuthUser != '') + { + $sLoginMode = 'form'; + } + break; + + case 'basic': + // Standard PHP authentication method, works with Apache... + // Case 1) Apache running in CGI mode + rewrite rules in .htaccess + if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) + { + list($sAuthUser, $sAuthPwd) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + $sLoginMode = 'basic'; + } + else if (isset($_SERVER['PHP_AUTH_USER'])) + { + $sAuthUser = $_SERVER['PHP_AUTH_USER']; + $sAuthPwd = $_SERVER['PHP_AUTH_PW']; + $sLoginMode = 'basic'; + } + break; + + case 'external': + // Web server supplied authentication + $bExternalAuth = false; + $sExtAuthVar = MetaModel::GetConfig()->GetExternalAuthenticationVariable(); // In which variable is the info passed ? + $sEval = '$bExternalAuth = isset('.$sExtAuthVar.');'; + eval($sEval); + if ($bExternalAuth) + { + eval('$sAuthUser = '.$sExtAuthVar.';'); // Retrieve the value + $sAuthPwd = ''; // No password in this case the web server already authentified the user... + $sLoginMode = 'external'; + $sAuthentication = 'external'; + } + break; + + case 'url': + // Credentials passed directly in the url + $sAuthUser = utils::ReadParam('auth_user', ''); + if ($sAuthUser != '') + { + $sAuthPwd = utils::ReadParam('auth_pwd', ''); + $sLoginMode = 'url'; + } + break; + } + $index++; + } + //echo "\nsLoginMode: $sLoginMode (user: $sAuthUser / pwd: $sAuthPwd\n)"; + if ($sLoginMode == '') + { + // First connection + $sDesiredLoginMode = utils::ReadParam('login_mode'); + if (in_array($sDesiredLoginMode, $aAllowedLoginTypes)) + { + $sLoginMode = $sDesiredLoginMode; + } + else + { + $sLoginMode = $aAllowedLoginTypes[0]; // First in the list... + } + $oPage = new LoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */); + $oPage->output(); + exit; + } + else + { + if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $sAuthentication)) + { + self::ResetSession(); + $oPage = new LoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */); + $oPage->output(); + exit; + } + else + { + // User is Ok, let's save it in the session and proceed with normal login + UserRights::Login($sAuthUser, $sAuthentication); // Login & set the user's language + $_SESSION['auth_user'] = $sAuthUser; + $_SESSION['login_mode'] = $sLoginMode; + } + } + } + } + + /** + * Check if the user is already authentified, if yes, then performs some additional validations: + * - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed + * - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected to the portal + * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page + * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal + */ + static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false) + { + $operation = utils::ReadParam('loginop', ''); + session_name(MetaModel::GetConfig()->Get('session_name')); + session_start(); + + if ($operation == 'logoff') + { + if (isset($_SESSION['login_mode'])) + { + $sLoginMode = $_SESSION['login_mode']; + } + else + { + $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); + if (count($aAllowedLoginTypes) > 0) + { + $sLoginMode = $aAllowedLoginTypes[0]; + } + else + { + $sLoginMode = 'form'; + } + } + self::ResetSession(); + $oPage = new LoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */); + $oPage->output(); + exit; + } + else if ($operation == 'change_pwd') + { + $sAuthUser = $_SESSION['auth_user']; + UserRights::Login($sAuthUser); // Set the user's language + $oPage = new LoginWebPage(); + $oPage->DisplayChangePwdForm(); + $oPage->output(); + exit; + } + if ($operation == 'do_change_pwd') + { + $sAuthUser = $_SESSION['auth_user']; + UserRights::Login($sAuthUser); // Set the user's language + $sOldPwd = utils::ReadPostedParam('old_pwd'); + $sNewPwd = utils::ReadPostedParam('new_pwd'); + if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd)))) + { + $oPage = new LoginWebPage(); + $oPage->DisplayChangePwdForm(true); // old pwd was wrong + $oPage->output(); + } + } + + self::Login(); + + if ($bMustBeAdmin && !UserRights::IsAdministrator()) + { + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); + $oP->p("".Dict::S('UI:LogOffMenu').""); + $oP->output(); + exit; + } + elseif ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser())) + { + // No rights to be here, redirect to the portal + header('Location: ../portal/index.php'); + } + } + +} // End of class +?> diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php new file mode 100644 index 0000000000..2e2d596120 --- /dev/null +++ b/application/menunode.class.inc.php @@ -0,0 +1,687 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/utils.inc.php'); +require_once(APPROOT.'/application/template.class.inc.php'); + + +/** + * This class manipulates, stores and displays the navigation menu used in the application + * In order to improve the modularity of the data model and to ease the update/migration + * between evolving data models, the menus are no longer stored in the database, but are instead + * built on the fly each time a page is loaded. + * The application's menu is organized into top-level groups with, inside each group, a tree of menu items. + * Top level groups do not display any content, they just expand/collapse. + * Sub-items drive the actual content of the page, they are based either on templates, OQL queries or full (external?) web pages. + * + * Example: + * Here is how to insert the following items in the application's menu: + * +----------------------------------------+ + * | Configuration Management Group | >> Top level group + * +----------------------------------------+ + * + Configuration Management Overview >> Template based menu item + * + Contacts >> Template based menu item + * + Persons >> Plain list (OQL based) + * + Teams >> Plain list (OQL based) + * + * // Create the top-level group. fRank = 1, means it will be inserted after the group '0', which is usually 'Welcome' + * $oConfigMgmtMenu = new MenuGroup('ConfigurationManagementMenu', 1); + * // Create an entry, based on a custom template, for the Configuration management overview, under the top-level group + * new TemplateMenuNode('ConfigurationManagementMenu', '../somedirectory/configuration_management_menu.html', $oConfigMgmtMenu->GetIndex(), 0); + * // Create an entry (template based) for the overview of contacts + * $oContactsMenu = new TemplateMenuNode('ContactsMenu', '../somedirectory/configuration_management_menu.html',$oConfigMgmtMenu->GetIndex(), 1); + * // Plain list of persons + * new OQLMenuNode('PersonsMenu', 'SELECT bizPerson', $oContactsMenu->GetIndex(), 0); + * + */ + +class ApplicationMenu +{ + static $aRootMenus = array(); + static $aMenusIndex = array(); + + /** + * Main function to add a menu entry into the application, can be called during the definition + * of the data model objects + */ + static public function InsertMenu(MenuNode $oMenuNode, $iParentIndex = -1, $fRank) + { + $index = self::GetMenuIndexById($oMenuNode->GetMenuId()); + if ($index == -1) + { + // The menu does not already exist, insert it + $index = count(self::$aMenusIndex); + self::$aMenusIndex[$index] = array( 'node' => $oMenuNode, 'children' => array()); + if ($iParentIndex == -1) + { + self::$aRootMenus[] = array ('rank' => $fRank, 'index' => $index); + } + else + { + self::$aMenusIndex[$iParentIndex]['children'][] = array ('rank' => $fRank, 'index' => $index); + } + } + return $index; + } + + /** + * Entry point to display the whole menu into the web page, used by iTopWebPage + */ + static public function DisplayMenu(iTopWebPage $oPage, $aExtraParams) + { + // Sort the root menu based on the rank + usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); + $iAccordion = 0; + $iActiveMenu = ApplicationMenu::GetActiveNodeId(); + foreach(self::$aRootMenus as $aMenu) + { + $oMenuNode = self::GetMenuNode($aMenu['index']); + if (!$oMenuNode->IsEnabled()) continue; // Don't display a non-enabled menu + $oPage->AddToMenu('

'.$oMenuNode->GetTitle().'

'); + $oPage->AddToMenu('
'); + $aChildren = self::GetChildren($aMenu['index']); + if (count($aChildren) > 0) + { + $oPage->AddToMenu('
    '); + $bActive = self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); + $oPage->AddToMenu('
'); + if ($bActive) + { + $oPage->add_ready_script("$('#accordion').accordion('activate', $iAccordion);"); + $oPage->add_ready_script("$('#accordion').accordion('option', {collapsible: true});"); // Make it auto-collapsible once it has been opened properly + } + } + $oPage->AddToMenu('
'); + $iAccordion++; + } + } + + /** + * Handles the display of the sub-menus (called recursively if necessary) + * @return true if the currently selected menu is one of the submenus + */ + static protected function DisplaySubMenu($oPage, $aMenus, $aExtraParams, $iActiveMenu = -1) + { + // Sort the menu based on the rank + $bActive = false; + usort($aMenus, array('ApplicationMenu', 'CompareOnRank')); + foreach($aMenus as $aMenu) + { + $index = $aMenu['index']; + $oMenu = self::GetMenuNode($index); + if ($oMenu->IsEnabled()) + { + $aChildren = self::GetChildren($index); + $sCSSClass = (count($aChildren) > 0) ? ' class="submenu"' : ''; + $sHyperlink = $oMenu->GetHyperlink($aExtraParams); + if ($sHyperlink != '') + { + $oPage->AddToMenu(''.$oMenu->GetTitle().''); + } + else + { + $oPage->AddToMenu(''.$oMenu->GetTitle().''); + } + $aCurrentMenu = self::$aMenusIndex[$index]; + if ($iActiveMenu == $index) + { + $bActive = true; + } + if (count($aChildren) > 0) + { + $oPage->AddToMenu('
    '); + $bActive |= self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); + $oPage->AddToMenu('
'); + } + } + } + return $bActive; + } + /** + * Helper function to sort the menus based on their rank + */ + static public function CompareOnRank($a, $b) + { + $result = 1; + if ($a['rank'] == $b['rank']) + { + $result = 0; + } + if ($a['rank'] < $b['rank']) + { + $result = -1; + } + return $result; + } + + /** + * Helper function to retrieve the MenuNodeObject based on its ID + */ + static public function GetMenuNode($index) + { + return isset(self::$aMenusIndex[$index]) ? self::$aMenusIndex[$index]['node'] : null; + } + + /** + * Helper function to get the list of child(ren) of a menu + */ + static protected function GetChildren($index) + { + return self::$aMenusIndex[$index]['children']; + } + + /** + * Helper function to get the ID of a menu based on its name + * @param string $sTitle Title of the menu (as passed when creating the menu) + * @return integer ID of the menu, or -1 if not found + */ + static public function GetMenuIndexById($sTitle) + { + $index = -1; + foreach(self::$aMenusIndex as $aMenu) + { + if ($aMenu['node']->GetMenuId() == $sTitle) + { + $index = $aMenu['node']->GetIndex(); + break; + } + } + return $index; + } + + /** + * Retrieves the currently active menu (if any, otherwise the first menu is the default) + * @return MenuNode or null if there is no menu at all ! + */ + static public function GetActiveNodeId() + { + $oAppContext = new ApplicationContext(); + $iMenuIndex = $oAppContext->GetCurrentValue('menu', -1); + + if ($iMenuIndex == -1) + { + // Make sure the root menu is sorted on 'rank' + usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); + $oFirstGroup = self::GetMenuNode(self::$aRootMenus[0]['index']); + $oMenuNode = self::GetMenuNode(self::$aMenusIndex[$oFirstGroup->GetIndex()]['children'][0]['index']); + $iMenuIndex = $oMenuNode->GetIndex(); + } + return $iMenuIndex; + } +} + +/** + * Root class for all the kind of node in the menu tree, data model providers are responsible for instantiating + * MenuNodes (i.e instances from derived classes) in order to populate the application's menu. Creating an objet + * derived from MenuNode is enough to have it inserted in the application's main menu. + * The class iTopWebPage, takes care of 3 items: + * +--------------------+ + * | Welcome | + * +--------------------+ + * Welcome To iTop + * +--------------------+ + * | Tools | + * +--------------------+ + * CSV Import + * +--------------------+ + * | Admin Tools | + * +--------------------+ + * User Accounts + * Profiles + * Notifications + * Run Queries + * Export + * Data Model + * Universal Search + * + * All the other menu items must constructed along with the various data model modules + */ +abstract class MenuNode +{ + protected $sMenuId; + protected $index; + + /** + * Class of objects to check if the menu is enabled, null if none + */ + protected $m_sEnableClass; + + /** + * User Rights Action code to check if the menu is enabled, null if none + */ + protected $m_iEnableAction; + + /** + * User Rights allowed results (actually a bitmask) to check if the menu is enabled, null if none + */ + protected $m_iEnableActionResults; + + /** + * Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu + */ + protected $m_sEnableStimulus; + + /** + * Create a menu item, sets the condition to have it displayed and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param integer $iParentIndex ID of the parent menu, pass -1 for top level (group) items + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param mixed $iActionCode UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus The user can see this menu if she/he has enough rights to apply this stimulus + * @return MenuNode + */ + public function __construct($sMenuId, $iParentIndex = -1, $fRank = 0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + $this->sMenuId = $sMenuId; + $this->m_sEnableClass = $sEnableClass; + $this->m_iEnableAction = $iActionCode; + $this->m_iEnableActionResults = $iAllowedResults; + $this->m_sEnableStimulus = $sEnableStimulus; + $this->index = ApplicationMenu::InsertMenu($this, $iParentIndex, $fRank); + } + + public function GetMenuId() + { + return $this->sMenuId; + } + + public function GetTitle() + { + return Dict::S("Menu:$this->sMenuId"); + } + + public function GetLabel() + { + return Dict::S("Menu:$this->sMenuId+"); + } + + public function GetIndex() + { + return $this->index; + } + + public function GetHyperlink($aExtraParams) + { + $aExtraParams['c[menu]'] = $this->GetIndex(); + return $this->AddParams('../pages/UI.php', $aExtraParams); + } + + /** + * Tells whether the menu is enabled (i.e. displayed) for the current user + * @return bool True if enabled, false otherwise + */ + public function IsEnabled() + { + if ($this->m_sEnableClass != null) + { + if (MetaModel::IsValidClass($this->m_sEnableClass)) + { + if ($this->m_sEnableStimulus != null) + { + if (!UserRights::IsStimulusAllowed($this->m_sEnableClass, $this->m_sEnableStimulus)) + { + return false; + } + } + if ($this->m_iEnableAction != null) + { + $iResult = UserRights::IsActionAllowed($this->m_sEnableClass, $this->m_iEnableAction); + if (($iResult & $this->m_iEnableActionResults)) + { + return true; + } + else + { + return false; + } + } + return true; + } + return false; + } + return true; + } + + public abstract function RenderContent(WebPage $oPage, $aExtraParams = array()); + + protected function AddParams($sHyperlink, $aExtraParams) + { + if (count($aExtraParams) > 0) + { + $aQuery = array(); + $sSeparator = '?'; + if (strpos($sHyperlink, '?') !== false) + { + $sSeparator = '&'; + } + foreach($aExtraParams as $sName => $sValue) + { + $aQuery[] = urlencode($sName).'='.urlencode($sValue); + } + $sHyperlink .= $sSeparator.implode('&', $aQuery); + } + return $sHyperlink; + } +} + +/** + * This class implements a top-level menu group. A group is just a container for sub-items + * it does not display a page by itself + */ +class MenuGroup extends MenuNode +{ + /** + * Create a top-level menu group and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param float $fRank Number used to order the list, the groups are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @return MenuGroup + */ + public function __construct($sMenuId, $fRank, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, -1 /* no parent, groups are at root level */, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, groups do not display any content + } +} + +/** + * This class defines a menu item which content is based on a custom template. + * Note the template can be either a local file or an URL ! + */ +class TemplateMenuNode extends MenuNode +{ + protected $sTemplateFile; + + /** + * Create a menu item based on a custom template and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sTemplateFile Path (or URL) to the file that will be used as a template for displaying the page's content + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @return MenuNode + */ + public function __construct($sMenuId, $sTemplateFile, $iParentIndex, $fRank = 0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sTemplateFile = $sTemplateFile; + } + + public function GetHyperlink($aExtraParams) + { + if ($this->sTemplateFile == '') return ''; + return parent::GetHyperlink($aExtraParams); + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + $sTemplate = @file_get_contents($this->sTemplateFile); + if ($sTemplate !== false) + { + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, $aExtraParams); + } + else + { + $oPage->p("Error: failed to load template file: '{$this->sTemplateFile}'"); // No need to translate ? + } + } +} + +/** + * This class defines a menu item that uses a standard template to display a list of items therefore it allows + * only two parameters: the page's title and the OQL expression defining the list of items to be displayed + */ +class OQLMenuNode extends MenuNode +{ + protected $sPageTitle; + protected $sOQL; + protected $bSearch; + + /** + * Extra parameters to be passed to the display block to fine tune its appearence + */ + protected $m_aParams; + + + /** + * Create a menu item based on an OQL query and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sOQL OQL query defining the set of objects to be displayed + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param bool $bSearch Whether or not to display a (collapsed) search frame at the top of the page + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @return MenuNode + */ + public function __construct($sMenuId, $sOQL, $iParentIndex, $fRank = 0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sPageTitle = "Menu:$sMenuId+"; + $this->sOQL = $sOQL; + $this->bSearch = $bSearch; + $this->m_aParams = array(); + // Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects + // of the class specified by the OQL... + } + + /** + * Set some extra parameters to be passed to the display block to fine tune its appearence + * @param Hash $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters + */ + public function SetParameters($aParams) + { + $this->m_aParams = $aParams; + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + $aExtraParams = array_merge($aExtraParams, $this->m_aParams); + try + { + $oSearch = DBObjectSearch::FromOQL($this->sOQL); + $sIcon = MetaModel::GetClassIcon($oSearch->GetClass()); + } + catch(Exception $e) + { + $sIcon = ''; + } + // The standard template used for all such pages: a (closed) search form at the top and a list of results at the bottom + $sTemplate = ''; + + if ($this->bSearch) + { + $sTemplate .= <<$this->sOQL +EOF; + } + $sParams = ''; + if (!empty($this->m_aParams)) + { + $sParams = 'parameters="'; + foreach($this->m_aParams as $sName => $sValue) + { + $sParams .= $sName.':'.$sValue.';'; + } + $sParams .= '"'; + } + $sTemplate .= <<$sIcon$this->sPageTitle

+$this->sOQL +EOF; + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, $aExtraParams); + } +} +/** + * This class defines a menu item that displays a search form for the given class of objects + */ +class SearchMenuNode extends MenuNode +{ + protected $sPageTitle; + protected $sClass; + + /** + * Create a menu item based on an OQL query and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sClass The class of objects to search for + * @param string $sPageTitle Title displayed into the page's content (will be looked-up in the dictionnary for translation) + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @return MenuNode + */ + public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sPageTitle = "Menu:$sMenuId+"; + $this->sClass = $sClass; + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + // The standard template used for all such pages: an open search form at the top + $sTemplate = <<SELECT $this->sClass +EOF; + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, $aExtraParams); + } +} + +/** + * This class defines a menu that points to any web page. It takes only two parameters: + * - The hyperlink to point to + * - The name of the menu + * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink + * in order to make it the active one, if the target page is based on iTopWebPage and therefore displays the menu + */ +class WebPageMenuNode extends MenuNode +{ + protected $sHyperlink; + + /** + * Create a menu item that points to any web page (not only UI.php) + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sHyperlink URL to the page to load. Use relative URL if you want to keep the application portable ! + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @return MenuNode + */ + public function __construct($sMenuId, $sHyperlink, $iParentIndex, $fRank = 0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sHyperlink = $sHyperlink; + } + + public function GetHyperlink($aExtraParams) + { + $aExtraParams['c[menu]'] = $this->GetIndex(); + return $this->AddParams( $this->sHyperlink, $aExtraParams); + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, the external web page will handle the display by itself + } +} + +/** + * This class defines a menu that points to the page for creating a new object of the specified class. + * It take only one parameter: the name of the class + * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink + * in order to make it the active one + */ +class NewObjectMenuNode extends MenuNode +{ + protected $sClass; + + /** + * Create a menu item that points to the URL for creating a new object, the menu will be added only if the current user has enough + * rights to create such an object (or an object of a child class) + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sClass URL to the page to load. Use relative URL if you want to keep the application portable ! + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @return MenuNode + */ + public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0) + { + parent::__construct($sMenuId, $iParentIndex, $fRank); + $this->sClass = $sClass; + } + + public function GetHyperlink($aExtraParams) + { + $sHyperlink = '../pages/UI.php?operation=new&class='.$this->sClass; + $aExtraParams['c[menu]'] = $this->GetIndex(); + return $this->AddParams($sHyperlink, $aExtraParams); + } + + /** + * Overload the check of the "enable" state of this menu to take into account + * derived classes of objects + */ + public function IsEnabled() + { + // Enable this menu, only if the current user has enough rights to create such an object, or an object of + // any child class + + $aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself + $bActionIsAllowed = false; + + foreach($aSubClasses as $sCandidateClass) + { + if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) + { + $bActionIsAllowed = true; + break; // Enough for now + } + } + return $bActionIsAllowed; + } + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, the external web page will handle the display by itself + } +} +?> diff --git a/application/nicewebpage.class.inc.php b/application/nicewebpage.class.inc.php new file mode 100644 index 0000000000..fd9f9795a9 --- /dev/null +++ b/application/nicewebpage.class.inc.php @@ -0,0 +1,111 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); +/** + * Web page with some associated CSS and scripts (jquery) for a fancier display + */ +class NiceWebPage extends WebPage +{ + var $m_aReadyScripts; + + public function __construct($s_title) + { + parent::__construct($s_title); + $this->m_aReadyScripts = array(); + $this->add_linked_script("../js/jquery-1.4.2.min.js"); + //$this->add_linked_script("../js/jquery.history_remote.pack.js"); + $this->add_linked_stylesheet('../css/ui-lightness/jquery-ui-1.8.2.custom.css'); + $this->add_linked_script('../js/jquery-ui-1.8.2.custom.min.js'); + //$this->add_linked_script("../js/ui.resizable.js"); +// $this->add_linked_script("../js/ui.tabs.js"); + $this->add_linked_script("../js/hovertip.js"); +// $this->add_linked_script("../js/jqModal.js"); + $this->add_linked_stylesheet("../css/light-grey.css"); +// $this->add_linked_stylesheet("../js/themes/light/light.tabs.css"); + //$this->add_linked_stylesheet("../css/jquery.tabs-ie.css", "lte IE 7"); +// $this->add_linked_stylesheet("../css/jqModal.css"); + $this->add_ready_script(' window.setTimeout(hovertipInit, 1);'); + } + + public function small_p($sText) + { + $this->add("

$sText

\n"); + } + + // By Rom, used by CSVImport and Advanced search + public function MakeClassesSelect($sName, $sDefaultValue, $iWidthPx, $iActionCode = null) + { + // $aTopLevelClasses = array('bizService', 'bizContact', 'logInfra', 'bizDocument'); + // These are classes wich root class is cmdbAbstractObject ! + $this->add(""); + } + + // By Rom, used by Advanced search + public function add_select($aChoices, $sName, $sDefaultValue, $iWidthPx) + { + $this->add(""); + } + + public function add_ready_script($sScript) + { + $this->m_aReadyScripts[] = $sScript; + } + + /** + * Outputs (via some echo) the complete HTML page by assembling all its elements + */ + public function output() + { + if (count($this->m_aReadyScripts)>0) + { + $this->add_script("\$(document).ready(function() {\n".implode("\n", $this->m_aReadyScripts)."\n});"); + } + parent::output(); + } +} + +?> diff --git a/application/portalwebpage.class.inc.php b/application/portalwebpage.class.inc.php new file mode 100644 index 0000000000..4fbee95451 --- /dev/null +++ b/application/portalwebpage.class.inc.php @@ -0,0 +1,177 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/nicewebpage.class.inc.php"); +require_once(APPROOT."/application/applicationcontext.class.inc.php"); +require_once(APPROOT."/application/user.preferences.class.inc.php"); +/** + * Web page with some associated CSS and scripts (jquery) for a fancier display + * of the Portal web page + */ +class PortalWebPage extends NiceWebPage +{ + /** + * Portal menu + */ + protected $m_aMenuButtons; + + public function __construct($sTitle, $sAlternateStyleSheet = '') + { + $this->m_aMenuButtons = array(); + parent::__construct($sTitle); + $this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->add_linked_stylesheet("../css/jquery.treeview.css"); + $this->add_linked_stylesheet("../css/jquery.autocomplete.css"); + if ($sAlternateStyleSheet != '') + { + $this->add_linked_stylesheet("../portal/$sAlternateStyleSheet/portal.css"); + } + else + { + $this->add_linked_stylesheet("../portal/portal.css"); + } + $this->add_linked_script('../js/jquery.layout.min.js'); + $this->add_linked_script('../js/jquery.ba-bbq.min.js'); + $this->add_linked_script("../js/jquery.tablehover.js"); + $this->add_linked_script("../js/jquery.treeview.js"); + $this->add_linked_script("../js/jquery.autocomplete.js"); + $this->add_linked_script("../js/jquery.bgiframe.js"); + $this->add_linked_script("../js/jquery.positionBy.js"); + $this->add_linked_script("../js/jquery.popupmenu.js"); + $this->add_linked_script("../js/date.js"); + $this->add_linked_script("../js/jquery.tablesorter.min.js"); + $this->add_linked_script("../js/jquery.blockUI.js"); + $this->add_linked_script("../js/utils.js"); + $this->add_linked_script("../js/forms-json-utils.js"); + $this->add_linked_script("../js/swfobject.js"); + $this->add_ready_script( +<< 0) + { + this.truncatedList = true; + } + if (this.truncatedList) + { + $("tr td",table).removeClass('truncated'); + $("tr:last td",table).addClass('truncated'); + } + } + }); + + + $.tablesorter.addWidget({ + // give the widget a id + id: "myZebra", + // format is called when the on init and when a sorting has finished + format: function(table) + { + // Replace the 'red even' lines by 'red_even' since most browser do not support 2 classes selector in CSS, etc.. + $("tbody tr:even",table).addClass('even'); + $("tbody tr.red:even",table).removeClass('red').removeClass('even').addClass('red_even'); + $("tbody tr.orange:even",table).removeClass('orange').removeClass('even').addClass('orange_even'); + $("tbody tr.green:even",table).removeClass('green').removeClass('even').addClass('green_even'); + } + }); + + $("table.listResults").tableHover(); // hover tables + $(".listResults").tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables + $(".date-pick").datepicker({ + showOn: 'button', + buttonImage: '../images/calendar.png', + buttonImageOnly: true, + dateFormat: 'yy-mm-dd', + constrainInput: false, + changeMonth: true, + changeYear: true + }); + $('.resizable').resizable(); // Make resizable everything that claims to be resizable ! +} +catch(err) +{ + // Do something with the error ! + alert(err); +} +EOF +); + + $this->add_script( +<< 0); + if (!bResult) + { + alert(sMessage); + } + return bResult; + } + + function GoBack() + { + var form = $('#request_form'); + var step = $('input[name=step]'); + + form.unbind('submit'); // De-activate validation + step.val(step.val() -2); // To go Back one step: next step is x, current step is x-1, previous step is x-2 + form.submit(); // Go + } +EOF +); + + } + + /** + * Add a button to the portal's main menu + */ + public function AddMenuButton($sId, $sLabel, $sHyperlink) + { + $this->m_aMenuButtons[] = array('id' => $sId, 'label' => $sLabel, 'hyperlink' => $sHyperlink); + } + + public function output() + { + $sMenu = ''; + $this->AddMenuButton('logoff', 'Portal:Disconnect', '../pages/logoff.php?portal=1'); // This menu is always present and is the last one + foreach($this->m_aMenuButtons as $aMenuItem) + { + $sMenu .= "".Dict::S($aMenuItem['label']).""; + } + $this->s_content = '
'.$this->s_content.'
'; + parent::output(); + } +} +?> diff --git a/application/startup.inc.php b/application/startup.inc.php new file mode 100644 index 0000000000..9c71599b02 --- /dev/null +++ b/application/startup.inc.php @@ -0,0 +1,31 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/core/cmdbobject.class.inc.php'); +require_once(APPROOT.'/application/utils.inc.php'); + +MetaModel::Startup(ITOP_CONFIG_FILE); + +?> diff --git a/application/template.class.inc.php b/application/template.class.inc.php new file mode 100644 index 0000000000..b7caf07c96 --- /dev/null +++ b/application/template.class.inc.php @@ -0,0 +1,253 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/displayblock.class.inc.php'); +/** + * This class manages the special template format used internally to build the iTop web pages + */ +class DisplayTemplate +{ + protected $m_sTemplate; + protected $m_aTags; + static protected $iBlockCount = 0; + + public function __construct($sTemplate) + { + $this->m_aTags = array('itopblock', 'itopcheck', 'itoptabs', 'itoptab', 'itoptoggle', 'itopstring'); + $this->m_sTemplate = $sTemplate; + } + + public function Render(WebPage $oPage, $aParams = array()) + { + $this->m_sTemplate = MetaModel::ApplyParams($this->m_sTemplate, $aParams); + $iStart = 0; + $iEnd = strlen($this->m_sTemplate); + $iCount = 0; + $iBeforeTagPos = $iStart; + $iAfterTagPos = $iStart; + while($sTag = $this->GetNextTag($iStart, $iEnd)) + { + $sContent = $this->GetTagContent($sTag, $iStart, $iEnd); + $iAfterTagPos = $iEnd + strlen(''); + $sOuterTag = substr($this->m_sTemplate, $iStart, $iAfterTagPos - $iStart); + $oPage->add(substr($this->m_sTemplate, $iBeforeTagPos, $iStart - $iBeforeTagPos)); + if ($sTag == DisplayBlock::TAG_BLOCK) + { + try + { + $oBlock = DisplayBlock::FromTemplate($sOuterTag); + if (is_object($oBlock)) + { + $oBlock->Display($oPage, 'block_'.self::$iBlockCount, $aParams); + } + } + catch(OQLException $e) + { + $oPage->p('Error in template (please contact your administrator) - Invalid query'); + } + catch(Exception $e) + { + $oPage->p('Error in template (please contact your administrator)'); + } + + self::$iBlockCount++; + } + else + { + $aAttributes = $this->GetTagAttributes($sTag, $iStart, $iEnd); + //$oPage->p("Tag: $sTag - ($iStart, $iEnd)"); + $this->RenderTag($oPage, $sTag, $aAttributes, $sContent); + + } + $iAfterTagPos = $iEnd + strlen(''); + $iBeforeTagPos = $iAfterTagPos; + $iStart = $iEnd; + $iEnd = strlen($this->m_sTemplate); + $iCount++; + } + $oPage->add(substr($this->m_sTemplate, $iAfterTagPos)); + } + + public function GetNextTag(&$iStartPos, &$iEndPos) + { + $iChunkStartPos = $iStartPos; + $sNextTag = null; + $iStartPos = $iEndPos; + foreach($this->m_aTags as $sTag) + { + // Search for the opening tag + $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.' ', $iChunkStartPos); + if ($iOpeningPos === false) + { + $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.'>', $iChunkStartPos); + } + if ($iOpeningPos !== false) + { + $iClosingPos = stripos($this->m_sTemplate, '', $iOpeningPos); + } + if ( ($iOpeningPos !== false) && ($iClosingPos !== false)) + { + if ($iOpeningPos < $iStartPos) + { + // This is the next tag + $iStartPos = $iOpeningPos; + $iEndPos = $iClosingPos; + $sNextTag = $sTag; + } + } + } + return $sNextTag; + } + + public function GetTagContent($sTag, $iStartPos, $iEndPos) + { + $sContent = ""; + $iContentStart = strpos($this->m_sTemplate, '>', $iStartPos); // Content of tag start immediatly after the first closing bracket + if ($iContentStart !== false) + { + $sContent = substr($this->m_sTemplate, 1+$iContentStart, $iEndPos - $iContentStart - 1); + } + return $sContent; + } + + public function GetTagAttributes($sTag, $iStartPos, $iEndPos) + { + $aAttr = array(); + $iAttrStart = strpos($this->m_sTemplate, ' ', $iStartPos); // Attributes start just after the first space + $iAttrEnd = strpos($this->m_sTemplate, '>', $iStartPos); // Attributes end just before the first closing bracket + if ( ($iAttrStart !== false) && ($iAttrEnd !== false) && ($iAttrEnd > $iAttrStart)) + { + $sAttributes = substr($this->m_sTemplate, 1+$iAttrStart, $iAttrEnd - $iAttrStart - 1); + $aAttributes = explode(' ', $sAttributes); + foreach($aAttributes as $sAttr) + { + if ( preg_match('/(.+) *= *"(.+)"$/', $sAttr, $aMatches) ) + { + $aAttr[strtolower($aMatches[1])] = $aMatches[2]; + } + } + } + return $aAttr; + } + + protected function RenderTag($oPage, $sTag, $aAttributes, $sContent) + { + static $iTabContainerCount = 0; + switch($sTag) + { + case 'itoptabs': + $oPage->AddTabContainer('Tabs_'.$iTabContainerCount); + $oPage->SetCurrentTabContainer('Tabs_'.$iTabContainerCount); + $iTabContainerCount++; + //$oPage->p('Content:
'.htmlentities($sContent).'
'); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + $oPage->SetCurrentTabContainer(''); + break; + + case 'itopcheck': + $sClassName = $aAttributes['class']; + if (MetaModel::IsValidClass($sClassName) && UserRights::IsActionAllowed($sClassName, UR_ACTION_READ)) + { + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + } + else + { + // Leave a trace for those who'd like to understand why nothing is displayed + $oPage->add("\n"); + } + break; + + case 'itoptab': + $oPage->SetCurrentTab(Dict::S(str_replace('_', ' ', $aAttributes['name']))); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + //$oPage->p('iTop Tab Content:
'.htmlentities($sContent).'
'); + $oPage->SetCurrentTab(''); + break; + + case 'itoptoggle': + $sName = isset($aAttributes['name']) ? $aAttributes['name'] : 'Tagada'; + $bOpen = isset($aAttributes['open']) ? $aAttributes['open'] : true; + $oPage->StartCollapsibleSection(Dict::S($sName), $bOpen); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + //$oPage->p('iTop Tab Content:
'.htmlentities($sContent).'
'); + $oPage->EndCollapsibleSection(); + break; + + case 'itopstring': + $oPage->add(Dict::S($sContent)); + break; + + case 'itopblock': // No longer used, handled by DisplayBlock::FromTemplate see above + $oPage->add(""); + break; + + default: + // Unknown tag, just ignore it or now -- output an HTML comment + $oPage->add(""); + } + } + + /** + * Unit test + */ + static public function UnitTest() + { + require_once(APPROOT.'/application/startup.inc.php'); + require_once(APPROOT."/application/itopwebpage.class.inc.php"); + + $sTemplate = ' + + SELECT NetworkDevice AS d WHERE d.id = $id$ + + + SELECT Interface AS i WHERE i.device_id = $id$ + + + SELECT Contact AS c JOIN lnkContactToCI AS l ON l.contact_id = c.id WHERE l.ci_id = $id$ + + + SELECT Document AS d JOIN lnkDocumentToCI as l ON l.document_id = d.id WHERE l.ci_id = $id$) + + '; + + $oPage = new iTopWebPage('Unit Test'); + //$oPage->add("Template content:
".htmlentities($sTemplate)."
\n"); + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, array('class'=>'Network device','pkey'=> 271, 'name' => 'deliversw01.mecanorama.fr', 'org_id' => 3)); + $oPage->output(); + } +} + +//DisplayTemplate::UnitTest(); + +?> diff --git a/application/templates/audit_category.html b/application/templates/audit_category.html new file mode 100644 index 0000000000..4155554023 --- /dev/null +++ b/application/templates/audit_category.html @@ -0,0 +1,12 @@ + + +SELECT $class$ WHERE id = $id$ + + + SELECT AuditRule WHERE category_id = $id$ + + diff --git a/application/templates/notifications_menu.html b/application/templates/notifications_menu.html new file mode 100644 index 0000000000..76f8f5ac41 --- /dev/null +++ b/application/templates/notifications_menu.html @@ -0,0 +1,20 @@ + + +
+ +UI:NotificationsMenu:HelpContent +
+
+

 

+ + +

UI:NotificationsMenu:AvailableTriggers

+ SELECT Trigger +
+ +

UI:NotificationsMenu:AvailableActions

+ SELECT ActionEmail +
+
diff --git a/application/templates/welcome_popup.html b/application/templates/welcome_popup.html new file mode 100644 index 0000000000..0b3634702e --- /dev/null +++ b/application/templates/welcome_popup.html @@ -0,0 +1,46 @@ +
+ +

+

+

UI:WelcomeMenu:Title

+

+ + + + + +
+UI:WelcomeMenu:LeftBlock + +UI:WelcomeMenu:RightBlock +
+
diff --git a/application/transaction.class.inc.php b/application/transaction.class.inc.php new file mode 100644 index 0000000000..3f7ae058bf --- /dev/null +++ b/application/transaction.class.inc.php @@ -0,0 +1,97 @@ + diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php new file mode 100644 index 0000000000..6ebe4684c0 --- /dev/null +++ b/application/ui.extkeywidget.class.inc.php @@ -0,0 +1,271 @@ + (input)-------+ +-----------+ + * | | | Browse... | + * +-----------------------------+ +-----------+ + * + * And the popup dialog has the following layout: + * + * +------------------- ac_dlg_ (div)-----------+ + * + +--- ds_ (div)---------------------------+ | + * | | +------------- fs_ (form)------------+ | | + * | | | +--------+---+ | | | + * | | | | Class | V | | | | + * | | | +--------+---+ | | | + * | | | | | | + * | | | S e a r c h F o r m | | | + * | | | +--------+ | | | + * | | | | Search | | | | + * | | | +--------+ | | | + * | | +----------------------------------------+ | | + * | +--------------+-dh_-+--------------------+ | + * | \ Search / | + * | +------+ | + * | +--- fr_ (form)--------------------------+ | + * | | +------------ dr_ (div)--------------+ | | + * | | | | | | + * | | | S e a r c h R e s u l t s | | | + * | | | | | | + * | | +----------------------------------------+ | | + * | | +--------+ +-----+ | | + * | | | Cancel | | Add | | | + * | | +--------+ +-----+ | | + * | +--------------------------------------------+ | + * +------------------------------------------------+ + * @author Erwan Taloc + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/displayblock.class.inc.php'); + +class UIExtKeyWidget +{ + protected static $iWidgetIndex = 0; + protected $sAttCode; + protected $sNameSuffix; + protected $iId; + protected $sTitle; + + public function __construct($sAttCode, $sClass, $sTitle, $aAllowedValues, $value, $iInputId, $bMandatory, $sNameSuffix = '', $sFieldPrefix = '', $sFormPrefix = '') + { + self::$iWidgetIndex++; + $this->sAttCode = $sAttCode; + $this->sClass = $sClass; + $this->oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $this->sNameSuffix = $sNameSuffix; + $this->iId = $iInputId; + $this->aAllowedValues = $aAllowedValues; + $this->value = $value; + $this->sFieldPrefix = $sFieldPrefix; + $this->sTargetClass = $this->oAttDef->GetTargetClass(); + $this->sTitle = $sTitle; + $this->sFormPrefix = $sFormPrefix; + $this->bMandatory = $bMandatory; + } + + /** + * Get the HTML fragment corresponding to the linkset editing widget + * @param WebPage $oP The web page used for all the output + * @param Hash $aArgs Extra context arguments + * @return string The HTML fragment to be inserted into the page + */ + public function Display(WebPage $oPage, $aArgs = array()) + { + $bCreate = (!MetaModel::IsAbstract($this->sTargetClass)) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_BULK_MODIFY) && $this->oAttDef->AllowTargetCreation()); + $sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm'); + + $sHTMLValue = ""; // no wrap + if (count($this->aAllowedValues) < $this->oAttDef->GetMaximumComboLength()) + { + // Few choices, use a normal 'select' + $sSelectMode = 'true'; + + $sHelpText = $this->oAttDef->GetHelpOnEdition(); + + // In case there are no valid values, the select will be empty, thus blocking the user from validating the form + $sHTMLValue = "\n"; + $oPage->add_ready_script( +<<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sClass}', '{$this->sAttCode}', '{$this->sNameSuffix}', $sSelectMode, oWizardHelper{$this->sFormPrefix}); + oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; + +EOF +); + } + else + { + // Too many choices, use an autocomplete + $sSelectMode = 'false'; + + if ($this->oAttDef->IsNull($this->value)) // Null values are displayed as '' + { + $sDisplayValue = ''; + } + else + { + $sDisplayValue = $this->GetObjectName($this->value); + } + $sFormPrefix = $this->sFormPrefix; + $iMinChars = $this->oAttDef->GetMinAutoCompleteChars(); + $iFieldSize = $this->oAttDef->GetMaxSize(); + + // the input for the auto-complete + $sHTMLValue = "aAllowedValues)."\" type=\"text\" id=\"label_$this->iId\" size=\"30\" maxlength=\"$iFieldSize\" value=\"$sDisplayValue\"/> "; + $sHTMLValue .= "iId}.Search();\"> "; + + // another hidden input to store & pass the object's Id + $sHTMLValue .= "iId\" name=\"attr_{$this->sFieldPrefix}{$this->sAttCode}{$this->sNameSuffix}\" value=\"$this->value\" />\n"; + + // Scripts to start the autocomplete and bind some events to it + $oPage->add_ready_script( +<<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sClass}', '{$this->sAttCode}', '{$this->sNameSuffix}', $sSelectMode, oWizardHelper{$this->sFormPrefix}); + oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; + $('#label_$this->iId').autocomplete('./ajax.render.php', { scroll:true, minChars:{$iMinChars}, formatItem:formatItem, autoFill:false, matchContains:true, keyHolder:'#{$this->iId}', extraParams:{operation:'autocomplete', sclass:'{$this->sClass}',attCode:'{$this->sAttCode}'}}); + $('#label_$this->iId').blur(function() { $(this).search(); } ); + $('#label_$this->iId').result( function(event, data, formatted) { OnAutoComplete('{$this->iId}', event, data, formatted); } ); + $('#ac_dlg_$this->iId').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, title: '{$this->sTitle}', resizeStop: oACWidget_{$this->iId}.UpdateSizes, close: oACWidget_{$this->iId}.OnClose }); + +EOF +); + $oPage->add_at_the_end($this->GetSearchDialog($oPage)); // To prevent adding forms inside the main form + + } + if ($bCreate) + { + $sHTMLValue .= "iId}.CreateObject();\"> "; + $oPage->add_at_the_end('
'); + } + $sHTMLValue .= "iId}\">"; + $sHTMLValue .= "
"; // end of no wrap + return $sHTMLValue; + } + + protected function GetSearchDialog(WebPage $oPage) + { + $sHTML = '
'; + + $oFilter = new DBObjectSearch($this->sTargetClass); + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'search', false); + $sHTML .= $oBlock->GetDisplay($oPage, $this->iId, array('open' => true, 'currentId' => $this->iId)); + $sHTML .= "
iId}\" OnSubmit=\"return oACWidget_{$this->iId}.DoOk();\">\n"; + $sHTML .= "
iId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; + $sHTML .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; + $sHTML .= "
\n"; + $sHTML .= "iId}\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#ac_dlg_{$this->iId}').dialog('close');\">  "; + $sHTML .= "iId}\" value=\"".Dict::S('UI:Button:Ok')."\" onClick=\"oACWidget_{$this->iId}.DoOk();\">"; + $sHTML .= "
\n"; + $sHTML .= '
'; + + $oPage->add_ready_script("$('#fs_{$this->iId}').bind('submit.uiAutocomplete', oACWidget_{$this->iId}.DoSearchObjects);"); + $oPage->add_ready_script("$('#dc_{$this->iId}').resize(oACWidget_{$this->iId}.UpdateSizes);"); + + return $sHTML; + } + + /** + * Search for objects to be selected + * @param WebPage $oP The page used for the output (usually an AjaxWebPage) + * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass + * @param Array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of the search + */ + public function SearchObjectsToSelect(WebPage $oP, $sTargetClass = '') + { + if ($sTargetClass != '') + { + // assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass)); + $oFilter = new DBObjectSearch($sTargetClass); + } + else + { + // No remote class specified use the one defined in the linkedset + $oFilter = new DBObjectSearch($this->sTargetClass); + } + $oFilter->AddCondition('id', array_keys($this->aAllowedValues), 'IN'); + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, $this->iId, array('menu' => false, 'selection_mode' => true, 'selection_type' => 'single', 'display_limit' => false)); // Don't display the 'Actions' menu on the results + } + + /** + * Get the display name of the selected object, to fill back the autocomplete + */ + public function GetObjectName($iObjId) + { + $oObj = MetaModel::GetObject($this->sTargetClass, $iObjId); + return $oObj->GetName(); + } + + /** + * Get the form to create a new object of the 'target' class + */ + public function GetObjectCreationForm(WebPage $oPage) + { + $oPage->add('
'); + $oPage->add("

".MetaModel::GetClassIcon($this->sTargetClass)." ".Dict::Format('UI:CreationTitle_Class', MetaModel::GetName($this->sTargetClass))."

\n"); + cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, null, array(), array('formPrefix' => $this->iId, 'noRelations' => true)); + $oPage->add('
'); + $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$this->sTitle'});\n"); + $oPage->add_ready_script("$('#dcr_{$this->iId} form').removeAttr('onsubmit');"); + $oPage->add_ready_script("$('#dcr_{$this->iId} form').bind('submit.uilinksWizard', oACWidget_{$this->iId}.DoCreateObject);"); + } + + /** + * Get the form to create a new object of the 'target' class + */ + public function DoCreateObject($oPage) + { + $oObj = MetaModel::NewObject($this->sTargetClass); + $oObj->UpdateObject($this->sFormPrefix.$this->iId); + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $sUserString = CMDBChange::GetCurrentUserName(); + $oMyChange->Set("userinfo", $sUserString); + $iChangeId = $oMyChange->DBInsert(); + $oObj->DBInsertTracked($oMyChange); + + return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey()); + + //return array('name' => 'test', 'id' => '42'); + } +} +?> diff --git a/application/ui.htmleditorwidget.class.inc.php b/application/ui.htmleditorwidget.class.inc.php new file mode 100644 index 0000000000..0aa15786e8 --- /dev/null +++ b/application/ui.htmleditorwidget.class.inc.php @@ -0,0 +1,86 @@ + + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class UIHTMLEditorWidget +{ + protected $m_iId; + protected $m_sAttCode; + protected $m_sNameSuffix; + protected $m_sFieldPrefix; + protected $m_sHelpText; + protected $m_sValidationField; + protected $m_sValue; + protected $m_sMandatory; + + public function __construct($iInputId, $sAttCode, $sNameSuffix, $sFieldPrefix, $sHelpText, $sValidationField, $sValue, $sMandatory) + { + $this->m_iId = $iInputId; + $this->m_sAttCode = $sAttCode; + $this->m_sNameSuffix = $sNameSuffix; + $this->m_sHelpText = $sHelpText; + $this->m_sValidationField = $sValidationField; + $this->m_sValue = $sValue; + $this->m_sMandatory = $sMandatory; + $this->m_sFieldPrefix = $sFieldPrefix; + } + + /** + * Get the HTML fragment corresponding to the HTML editor widget + * @param WebPage $oP The web page used for all the output + * @param Hash $aArgs Extra context arguments + * @return string The HTML fragment to be inserted into the page + */ + public function Display(WebPage $oPage, $aArgs = array()) + { + $iId = $this->m_iId; + $sCode = $this->m_sAttCode.$this->m_sNameSuffix; + $sValue = $this->m_sValue; + $sHelpText = $this->m_sHelpText; + $sValidationField = $this->m_sValidationField; + + $sHtmlValue = "
$sValidationField
"; + + // Replace the text area with CKEditor + // To change the default settings of the editor, + // a) edit the file /js/ckeditor/config.js + // b) or override some of the configuration settings, using the second parameter of ckeditor() + $sLanguage = strtolower(trim(UserRights::GetUserLanguage())); + $oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, { language : '$sLanguage' , contentsLanguage : '$sLanguage' });"); // Transform $iId into a CKEdit + + // Please read... + // ValidateCKEditField triggers a timer... calling itself indefinitely + // This design was the quickest way to achieve the field validation (only checking if the field is blank) + // because the ckeditor does not fire events like "change" or "keyup", etc. + // See http://dev.ckeditor.com/ticket/900 => won't fix + // The most relevant solution would be to implement a plugin to CKEdit, and handle the internal events like: setData, insertHtml, insertElement, loadSnapshot, key, afterUndo, afterRedo + + // Could also be bound to 'instanceReady.ckeditor' + $oPage->add_ready_script("$('#$iId').bind('validate', function(evt, sFormId) { return ValidateCKEditField('$iId', '', {$this->m_sMandatory}, sFormId, '') } );"); + + return $sHtmlValue; + } +} +?> diff --git a/application/ui.linkswidget.class.inc.php b/application/ui.linkswidget.class.inc.php new file mode 100644 index 0000000000..49e2b7e57f --- /dev/null +++ b/application/ui.linkswidget.class.inc.php @@ -0,0 +1,443 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/displayblock.class.inc.php'); + +class UILinksWidget +{ + protected $m_sClass; + protected $m_sAttCode; + protected $m_sNameSuffix; + protected $m_iInputId; + protected $m_aAttributes; + protected $m_sExtKeyToRemote; + protected $m_sLinkedClass; + protected $m_sRemoteClass; + protected $m_bDuplicatesAllowed; + + public function __construct($sClass, $sAttCode, $iInputId, $sNameSuffix = '', $bDuplicatesAllowed = false) + { + $this->m_sClass = $sClass; + $this->m_sAttCode = $sAttCode; + $this->m_sNameSuffix = $sNameSuffix; + $this->m_iInputId = $iInputId; + $this->m_bDuplicatesAllowed = $bDuplicatesAllowed; + $this->m_aEditableFields = array(); + + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sAttCode); + $this->m_sLinkedClass = $oAttDef->GetLinkedClass(); + $this->m_sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); + $oLinkingAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $this->m_sExtKeyToRemote); + $this->m_sRemoteClass = $oLinkingAttDef->GetTargetClass(); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); + $sDefaultState = MetaModel::GetDefaultState($this->m_sClass); + + $this->m_aEditableFields = array(); + $this->m_aTableConfig = array(); + $this->m_aTableConfig['form::checkbox'] = array( 'label' => "m_sAttCode}{$this->m_sNameSuffix} .selection', this.checked); oWidget".$this->m_iInputId.".OnSelectChange();\">", 'description' => Dict::S('UI:SelectAllToggle+')); + + foreach(MetaModel::ListAttributeDefs($this->m_sLinkedClass) as $sAttCode=>$oAttDef) + { + if ($sStateAttCode == $sAttCode) + { + // State attribute is always hidden from the UI + } + else if (!$oAttDef->IsExternalField() && ($sAttCode != $sExtKeyToMe) && ($sAttCode != $this->m_sExtKeyToRemote) && ($sAttCode != 'finalclass')) + { + $iFlags = MetaModel::GetAttributeFlags($this->m_sLinkedClass, $sDefaultState, $sAttCode); + if ( !($iFlags & OPT_ATT_HIDDEN) && !($iFlags & OPT_ATT_READONLY) ) + { + $this->m_aEditableFields[] = $sAttCode; + $this->m_aTableConfig[$sAttCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + } + + $this->m_aTableConfig['static::key'] = array( 'label' => MetaModel::GetName($this->m_sRemoteClass), 'description' => MetaModel::GetClassDescription($this->m_sRemoteClass)); + foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) + { + // TO DO: check the state of the attribute: hidden or visible ? + if ($sFieldCode != 'finalclass') + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sRemoteClass, $sFieldCode); + $this->m_aTableConfig['static::'.$sFieldCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + } + + /** + * A one-row form for editing a link record + * @param WebPage $oP Web page used for the ouput + * @param DBObject $oLinkedObj The object to which all the elements of the linked set refer to + * @param mixed $linkObjOrId Either the object linked or a unique number for new link records to add + * @param Hash $aArgs Extra context arguments + * @return string The HTML fragment of the one-row form + */ + protected function GetFormRow(WebPage $oP, DBObject $oLinkedObj, $linkObjOrId = null, $aArgs = array() ) + { + $sPrefix = "$this->m_sAttCode{$this->m_sNameSuffix}"; + $aRow = array(); + if(is_object($linkObjOrId)) + { + $key = $linkObjOrId->GetKey(); + $sPrefix .= "[$key]["; + $sNameSuffix = "]"; // To make a tabular form + $aArgs['prefix'] = $sPrefix; + $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"$key\">"; + $aRow['form::checkbox'] .= ""; + foreach($this->m_aEditableFields as $sFieldCode) + { + $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.$linkObjOrId->GetKey().']'; + $sSafeId = str_replace(array('[',']','-'), '_', $sFieldId); + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); + $aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, $linkObjOrId->Get($sFieldCode), '' /* DisplayValue */, $sSafeId, $sNameSuffix, 0, $aArgs); + } + } + else + { + // form for creating a new record + $sPrefix .= "[$linkObjOrId]["; + $sNameSuffix = "]"; // To make a tabular form + $aArgs['prefix'] = $sPrefix; + $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"$linkObjOrId\">"; + $aRow['form::checkbox'] .= ""; + foreach($this->m_aEditableFields as $sFieldCode) + { + $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.$linkObjOrId.']'; + $sSafeId = str_replace(array('[',']','-'), '_', $sFieldId); + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); + $aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, '' /* TO DO/ call GetDefaultValue($oObject->ToArgs()) */, '' /* DisplayValue */, $sSafeId /* id */, $sNameSuffix, 0, $aArgs); + } + } + + $aRow['static::key'] = $oLinkedObj->GetHyperLink(); + foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) + { + $aRow['static::'.$sFieldCode] = $oLinkedObj->GetAsHTML($sFieldCode); + } + return $aRow; + } + + /** + * Display one row of the whole form + * @return none + */ + protected function DisplayFormRow(WebPage $oP, $aConfig, $aRow, $iRowId) + { + $sHtml = ''; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_row_$iRowId\">\n"; + foreach($aConfig as $sName=>$void) + { + $sHtml .= "".$aRow[$sName]."\n"; + } + $sHtml .= "\n"; + + return $sHtml; + } + + /** + * Display the table with the form for editing all the links at once + * @param WebPage $oP The web page used for the output + * @param Hash $aConfig The table's header configuration + * @param Hash $aData The tabular data to be displayed + * @return string Html fragment representing the form table + */ + protected function DisplayFormTable(WebPage $oP, $aConfig, $aData) + { + $sHtml = ''; + $sHtml .= "\n"; + // Header + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach($aConfig as $sName=>$aDef) + { + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "\n"; + + // Content + $sHtml .= "\n"; + if (count($aData) == 0) + { + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_empty_row\">"; + } + else + { + foreach($aData as $iRowId => $aRow) + { + $sHtml .= $this->DisplayFormRow($oP, $aConfig, $aRow, $iRowId); + } + } + $sHtml .= "\n"; + + // Footer + $sHtml .= "
".$aDef['label']."
".Dict::S('UI:Message:EmptyList:UseAdd')."m_sAttCode}{$this->m_sNameSuffix}\" value=\"\">
\n"; + + return $sHtml; + } + + + /** + * Get the HTML fragment corresponding to the linkset editing widget + * @param WebPage $oP The web page used for all the output + * @param DBObjectSet The initial value of the linked set + * @param Hash $aArgs Extra context arguments + * @return string The HTML fragment to be inserted into the page + */ + public function Display(WebPage $oPage, DBObjectSet $oValue, $aArgs = array()) + { + $sHtmlValue = ''; + $sTargetClass = self::GetTargetClass($this->m_sClass, $this->m_sAttCode); + $sHtmlValue .= "
m_sAttCode}{$this->m_sNameSuffix}\">\n"; + $oValue->Rewind(); + $aForm = array(); + while($oCurrentLink = $oValue->Fetch()) + { + $aRow = array(); + $key = $oCurrentLink->GetKey(); + $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote)); + + $aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs); + } + $sHtmlValue .= $this->DisplayFormTable($oPage, $this->m_aTableConfig, $aForm); + $sDuplicates = ($this->m_bDuplicatesAllowed) ? 'true' : 'false'; + $oPage->add_ready_script(<<m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates); + oWidget{$this->m_iInputId}.Init(); +EOF +); + $sHtmlValue .= "     m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.RemoveSelected();\" >"; + $sHtmlValue .= "   m_sAttCode}{$this->m_sNameSuffix}_btnAdd\" type=\"button\" value=\"".Dict::Format('UI:AddLinkedObjectsOf_Class', MetaModel::GetName($this->m_sRemoteClass))."\" onClick=\"oWidget{$this->m_iInputId}.AddObjects();\">\n"; + $sHtmlValue .= "

 

\n"; + $sHtmlValue .= "
\n"; + $oPage->add_at_the_end($this->GetObjectPickerDialog($oPage)); // To prevent adding forms inside the main form + return $sHtmlValue; + } + + /** + * This static function is called by the Ajax Page when there is a need to fill an autocomplete combo + * @param $oPage WebPage The ajax page used for the output (sent back to the browser) + * @param $sClass string The name of the class of the current object being edited + * @param $sAttCode string The name of the attribute being edited + * @param $sName string The partial name that was typed by the user + * @param $iMaxCount integer The maximum number of items to return + * @return void + */ + static public function Autocomplete(WebPage $oPage, $sClass, $sAttCode, $sName, $iMaxCount) + { + // #@# todo - add context information, otherwise any value will be authorized for external keys + $aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, array() /* $aArgs */, $sName); + if ($aAllowedValues != null) + { + $iCount = $iMaxCount; + foreach($aAllowedValues as $key => $value) + { + $oPage->add($value."|".$key."\n"); + $iCount--; + if ($iCount == 0) break; + } + } + else // No limitation to the allowed values + { + // Search for all the object of the linked class + $oAttDef = $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $sLinkedClass = $oAttDef->GetLinkedClass(); + $sSearchClass = self::GetTargetClass($sClass, $sAttCode); + $oFilter = new DBObjectSearch($sSearchClass); + $sSearchAttCode = MetaModel::GetNameAttributeCode($sSearchClass); + $oFilter->AddCondition($sSearchAttCode, $sName, 'Begins with'); + $oSet = new CMDBObjectSet($oFilter, array($sSearchAttCode => true)); + $iCount = 0; + while( ($iCount < $iMaxCount) && ($oObj = $oSet->fetch()) ) + { + $oPage->add($oObj->GetName()."|".$oObj->GetKey()."\n"); + $iCount++; + } + } + } + + /** + * This static function is called by the Ajax Page display a set of objects being linked + * to the object being created + * @param $oPage WebPage The ajax page used for the put^put (sent back to the browser + * @param $sClass string The name of the 'linking class' which is the class of the objects to display + * @param $sSet JSON serialized set of objects + * @param $sExtKeyToMe Name of the attribute in sClass that is pointing to a given object + * @param $iObjectId The id of the object $sExtKeyToMe is pointing to + * @return void + */ + static public function RenderSet($oPage, $sClass, $sJSONSet, $sExtKeyToMe, $sExtKeyToRemote, $iObjectId) + { + $aSet = json_decode($sJSONSet, true); // true means hash array instead of object + $oSet = CMDBObjectSet::FromScratch($sClass); + foreach($aSet as $aObject) + { + $oObj = MetaModel::NewObject($sClass); + foreach($aObject as $sAttCode => $value) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsExternalKey() && ($value != 0)) + { + $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value); // @@ optimization, don't do & query per object in the set ! + $oObj->Set($sAttCode, $oTargetObj); + } + else + { + $oObj->Set($sAttCode, $value); + } + + } + $oSet->AddObject($oObj); + } + $aExtraParams = array(); + $aExtraParams['link_attr'] = $sExtKeyToMe; + $aExtraParams['object_id'] = $iObjectId; + $aExtraParams['target_attr'] = $sExtKeyToRemote; + $aExtraParams['menu'] = false; + $aExtraParams['select'] = false; + $aExtraParams['view_link'] = false; + + cmdbAbstractObject::DisplaySet($oPage, $oSet, $aExtraParams); + } + + + protected static function GetTargetClass($sClass, $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $sLinkedClass = $oAttDef->GetLinkedClass(); + switch(get_class($oAttDef)) + { + case 'AttributeLinkedSetIndirect': + $oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote()); + $sTargetClass = $oLinkingAttDef->GetTargetClass(); + break; + + case 'AttributeLinkedSet': + $sTargetClass = $sLinkedClass; + break; + } + + return $sTargetClass; + } + + protected function GetObjectPickerDialog($oPage) + { + $sHtml = "
m_sAttCode}{$this->m_sNameSuffix}\">"; + $sHtml .= "
\n"; + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'search', false); + $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", array('open' => true)); + $sHtml .= "
m_sAttCode}{$this->m_sNameSuffix}\" OnSubmit=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\">\n"; + $sHtml .= "
m_sAttCode}{$this->m_sNameSuffix}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; + $sHtml .= "

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"; + $sHtml .= "
\n"; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">  "; + $sHtml .= "
\n"; + $sHtml .= "\n"; + $sHtml .= "
\n"; + $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, resizeStop: oWidget{$this->m_iInputId}.UpdateSizes });"); + $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog('option', {title:'".addslashes(Dict::Format('UI:AddObjectsOf_Class_LinkedWith_Class', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName($this->m_sClass)))."'});"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix} form').bind('submit.uilinksWizard', oWidget{$this->m_iInputId}.SearchObjectsToAdd);"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}').resize(oWidget{$this->m_iInputId}.UpdateSizes);"); + return $sHtml; + } + + /** + * Search for objects to be linked to the current object (i.e "remote" objects) + * @param WebPage $oP The page used for the output (usually an AjaxWebPage) + * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass + * @param Array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of the search + */ + public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array()) + { + if ($sRemoteClass != '') + { + // assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass)); + $oFilter = new DBObjectSearch($sRemoteClass); + } + else + { + // No remote class specified use the one defined in the linkedset + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + } + if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) + { + // Positive IDs correspond to existing link records + // negative IDs correspond to "remote" objects to be linked + $aLinkIds = array(); + $aRemoteObjIds = array(); + foreach($aAlreadyLinkedIds as $iId) + { + if ($iId > 0) + { + $aLinkIds[] = $iId; + } + else + { + $aRemoteObjIds[] = -$iId; + } + } + + if (count($aLinkIds) >0) + { + // Search for the links to find to which "remote" object they are linked + $oLinkFilter = new DBObjectSearch($this->m_sLinkedClass); + $oLinkFilter->AddCondition('id', $aLinkIds, 'IN'); + $oLinkSet = new CMDBObjectSet($oLinkFilter); + while($oLink = $oLinkSet->Fetch()) + { + $aRemoteObjIds[] = $oLink->Get($this->m_sExtKeyToRemote); + } + } + $oFilter->AddCondition('id', $aRemoteObjIds, 'NOTIN'); + } + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, 'ResultsToAdd', array('menu' => false, 'selection_mode' => true, 'display_limit' => false)); // Don't display the 'Actions' menu on the results + } + + public function DoAddObjects(WebPage $oP, $aLinkedObjectIds = array()) + { + $aTable = array(); + foreach($aLinkedObjectIds as $iObjectId) + { + $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $iObjectId); + if (is_object($oLinkedObj)) + { + $aRow = $this->GetFormRow($oP, $oLinkedObj, -$iObjectId ); // Not yet created link get negative Ids + $oP->add($this->DisplayFormRow($oP, $this->m_aTableConfig, $aRow, -$iObjectId)); + } + else + { + $oP->p(Dict::Format('UI:Error:Object_Class_Id_NotFound', $this->m_sLinkedClass, $iObjectId)); + } + } + } +} +?> diff --git a/application/ui.passwordwidget.class.inc.php b/application/ui.passwordwidget.class.inc.php new file mode 100644 index 0000000000..4bea0a5596 --- /dev/null +++ b/application/ui.passwordwidget.class.inc.php @@ -0,0 +1,70 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/displayblock.class.inc.php'); + +class UIPasswordWidget +{ + protected static $iWidgetIndex = 0; + protected $sAttCode; + protected $sNameSuffix; + protected $iId; + + public function __construct($sAttCode, $iInputId, $sNameSuffix = '') + { + self::$iWidgetIndex++; + $this->sAttCode = $sAttCode; + $this->sNameSuffix = $sNameSuffix; + $this->iId = $iInputId; + } + + /** + * Get the HTML fragment corresponding to the linkset editing widget + * @param WebPage $oP The web page used for all the output + * @param Hash $aArgs Extra context arguments + * @return string The HTML fragment to be inserted into the page + */ + public function Display(WebPage $oPage, $aArgs = array()) + { + $sCode = $this->sAttCode.$this->sNameSuffix; + $iWidgetIndex = self::$iWidgetIndex; + $sPasswordValue = utils::ReadPostedParam("attr_$sCode", '*****'); + $sConfirmPasswordValue = utils::ReadPostedParam("attr_{$sCode}_confirmed", '*****'); + $sChangedValue = (($sPasswordValue != '*****') || ($sConfirmPasswordValue != '*****')) ? 1 : 0; + $sHtmlValue = ''; + $sHtmlValue = ' 
'; + $sHtmlValue .= ' '.Dict::S('UI:PasswordConfirm').' '; + $sHtmlValue .= ''; + + $oPage->add_ready_script("$('#$this->iId').bind('keyup change', function(evt) { return PasswordFieldChanged('$this->iId') } );"); // Bind to a custom event: validate + $oPage->add_ready_script("$('#$this->iId').bind('keyup change validate', function(evt, sFormId) { return ValidatePasswordField('$this->iId', sFormId) } );"); // Bind to a custom event: validate + $oPage->add_ready_script("$('#{$this->iId}_confirm').bind('keyup change', function(evt, sFormId) { return ValidatePasswordField('$this->iId', sFormId) } );"); // Bind to a custom event: validate + + return $sHtmlValue; + } +} +?> diff --git a/application/uilinkswizard.class.inc.php b/application/uilinkswizard.class.inc.php new file mode 100644 index 0000000000..f443788308 --- /dev/null +++ b/application/uilinkswizard.class.inc.php @@ -0,0 +1,424 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class UILinksWizard +{ + protected $m_sClass; + protected $m_sLinkageAttr; + protected $m_iObjectId; + protected $m_sLinkedClass; + protected $m_sLinkingAttCode; + protected $m_aEditableFields; + protected $m_aTableConfig; + + public function __construct($sClass, $sLinkageAttr, $iObjectId, $sLinkedClass = '') + { + $this->m_sClass = $sClass; + $this->m_sLinkageAttr = $sLinkageAttr; + $this->m_iObjectId = $iObjectId; + $this->m_sLinkedClass = $sLinkedClass; // Will try to guess below, if it's empty + $this->m_sLinkingAttCode = ''; // Will be filled once we've found the attribute corresponding to the linked class + + $this->m_aEditableFields = array(); + $this->m_aTableConfig = array(); + $this->m_aTableConfig['form::checkbox'] = array( 'label' => "", 'description' => Dict::S('UI:SelectAllToggle+')); + foreach(MetaModel::GetAttributesList($this->m_sClass) as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oAttDef->IsExternalKey() && ($sAttCode != $this->m_sLinkageAttr)) + { + if (empty($this->m_sLinkedClass)) + { + // This is a class of objects we can manage ! + // Since nothing was specify, any class will do ! + $this->m_sLinkedClass = $oAttDef->GetTargetClass(); + $this->m_sLinkingAttCode = $sAttCode; + } + else if ($this->m_sLinkedClass == $oAttDef->GetTargetClass()) + { + // This is the class of objects we want to manage ! + $this->m_sLinkingAttCode = $sAttCode; + } + } + else if ( (!$oAttDef->IsExternalKey()) && (!$oAttDef->IsExternalField())) + { + $this->m_aEditableFields[] = $sAttCode; + $this->m_aTableConfig[$sAttCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + if (empty($this->m_sLinkedClass)) + { + throw( new Exception(Dict::Format('UI:Error:IncorrectLinkDefinition_LinkedClass_Class', $sLinkedClass, $sClass))); + } + foreach(MetaModel::GetZListItems($this->m_sLinkedClass, 'list') as $sFieldCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); + $this->m_aTableConfig['static::'.$sFieldCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + + public function Display(WebPage $oP, $aExtraParams = array()) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr); + $sTargetClass = $oAttDef->GetTargetClass(); + $oTargetObj = MetaModel::GetObject($sTargetClass, $this->m_iObjectId); + + $oP->set_title("iTop - ".MetaModel::GetName($this->m_sLinkedClass)." objects linked with ".MetaModel::GetName(get_class($oTargetObj)).": ".$oTargetObj->GetName()); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("m_sClass}\">\n"); + $oP->add("m_sLinkageAttr}\">\n"); + $oP->add("m_iObjectId}\">\n"); + $oP->add("m_sLinkingAttCode}\">\n"); + $oP->add("

".Dict::Format('UI:ManageObjectsOf_Class_LinkedWith_Class_Instance', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName(get_class($oTargetObj)), "".$oTargetObj->GetHyperlink()."")."

\n"); + $oP->add("
\n"); + $oP->add("\n"); + $oP->add_ready_script("InitForm();"); + $oFilter = new DBObjectSearch($this->m_sClass); + $oFilter->AddCondition($this->m_sLinkageAttr, $this->m_iObjectId, '='); + $oSet = new DBObjectSet($oFilter); + $aForm = array(); + while($oCurrentLink = $oSet->Fetch()) + { + $aRow = array(); + $key = $oCurrentLink->GetKey(); + $oLinkedObj = MetaModel::GetObject($this->m_sLinkedClass, $oCurrentLink->Get($this->m_sLinkingAttCode)); + + $aForm[$key] = $this->GetFormRow($oP, $oLinkedObj, $oCurrentLink); + } + //var_dump($aTableLabels); + //var_dump($aForm); + $this->DisplayFormTable($oP, $this->m_aTableConfig, $aForm); + $oP->add("     "); + $oP->add("   m_sLinkedClass))."\" onClick=\"AddObjects();\">\n"); + $oP->add("m_iObjectId.");\">"); + $oP->add("   \n"); + $oP->add("

 

\n"); + $oP->add("
\n"); + $oP->add("\n"); + if (isset($aExtraParams['StartWithAdd']) && ($aExtraParams['StartWithAdd'])) + { + $oP->add_ready_script("AddObjects();"); + } + } + + protected function GetFormRow($oP, $oLinkedObj, $currentLink = null ) + { + $aRow = array(); + if(is_object($currentLink)) + { + $key = $currentLink->GetKey(); + $sNameSuffix = "[$key]"; // To make a tabular form + $aRow['form::checkbox'] = ""; + $aRow['form::checkbox'] .= ""; + foreach($this->m_aEditableFields as $sFieldCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sFieldCode); + $aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sClass, $sFieldCode, $oAttDef, $currentLink->Get($sFieldCode), '' /* DisplayValue */, $key, $sNameSuffix); + } + } + else + { + // form for creating a new record + $sNameSuffix = "[$currentLink]"; // To make a tabular form + $aRow['form::checkbox'] = ""; + $aRow['form::checkbox'] .= ""; + foreach($this->m_aEditableFields as $sFieldCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sFieldCode); + $aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sClass, $sFieldCode, $oAttDef, '' /* TO DO/ call GetDefaultValue($oObject->ToArgs()) */, '' /* DisplayValue */, '' /* id */, $sNameSuffix); + } + } + foreach(MetaModel::GetZListItems($this->m_sLinkedClass, 'list') as $sFieldCode) + { + $aRow['static::'.$sFieldCode] = $oLinkedObj->GetAsHTML($sFieldCode); + } + return $aRow; + } + + protected function DisplayFormTable(WebPage $oP, $aConfig, $aData) + { + $oP->add("\n"); + // Header + $oP->add("\n"); + $oP->add("\n"); + foreach($aConfig as $sName=>$aDef) + { + $oP->add("\n"); + } + $oP->add("\n"); + $oP->add("\n"); + + // Content + $oP->add("\n"); + if (count($aData) == 0) + { + $oP->add(""); + } + else + { + foreach($aData as $iRowId => $aRow) + { + $this->DisplayFormRow($oP, $aConfig, $aRow, $iRowId); + } + } + $oP->add("\n"); + + // Footer + $oP->add("
".$aDef['label']."
".Dict::S('UI:Message:EmptyList:UseAdd')."
\n"); + } + + protected function DisplayFormRow(WebPage $oP, $aConfig, $aRow, $iRowId) + { + $oP->add("\n"); + foreach($aConfig as $sName=>$void) + { + $oP->add("".$aRow[$sName]."\n"); + } + $oP->add("\n"); + } + + public function DisplayAddForm(WebPage $oP) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr); + $sTargetClass = $oAttDef->GetTargetClass(); + $oTargetObj = MetaModel::GetObject($sTargetClass, $this->m_iObjectId); + $oP->add("
\n"); + //$oP->add("
\n"); + //$oP->add("

".Dict::Format('UI:AddObjectsOf_Class_LinkedWith_Class_Instance', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName(get_class($oTargetObj)), "".$oTargetObj->GetHyperlink()."")."

\n"); + //$oP->add("
\n"); + + $oFilter = new DBObjectSearch($this->m_sLinkedClass); + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'search', false); + $oBlock->Display($oP, 'SearchFormToAdd', array('open' => true)); + $oP->Add("
\n"); + $oP->Add("
\n"); + $oP->Add("

".Dict::S('UI:Message:EmptyList:UseSearchForm')."

\n"); + $oP->Add("
\n"); + $oP->add("  "); + $oP->Add("
\n"); + $oP->Add("\n"); + $oP->add_ready_script("$('#ModalDlg').dialog('option', {title:'".Dict::Format('UI:AddObjectsOf_Class_LinkedWith_Class_Instance', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName(get_class($oTargetObj)), "".$oTargetObj->GetHyperlink()."")."'});"); + $oP->add_ready_script("$('div#SearchFormToAdd form').bind('submit.uilinksWizard', SubmitHook);"); + } + + public function SearchObjectsToAdd(WebPage $oP) + { + //$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr); + + $oFilter = new DBObjectSearch($this->m_sLinkedClass); + $oSet = new CMDBObjectSet($oFilter); + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, 'ResultsToAdd', array('menu' => false, 'selection_mode' => true, 'display_limit' => false)); // Don't display the 'Actions' menu on the results + } + + public function DoAddObjects(WebPage $oP, $aLinkedObjectIds = array()) + { + //$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr); + //$sTargetClass = $oAttDef->GetTargetClass(); + //$oP->Add("\n"); // Just to make sure it's not empty + $aTable = array(); + foreach($aLinkedObjectIds as $iObjectId) + { + $oLinkedObj = MetaModel::GetObject($this->m_sLinkedClass, $iObjectId); + if (is_object($oLinkedObj)) + { + $aRow = $this->GetFormRow($oP, $oLinkedObj, -$iObjectId ); // Not yet created link get negative Ids + $this->DisplayFormRow($oP, $this->m_aTableConfig, $aRow, -$iObjectId); + } + else + { + echo Dict::Format('UI:Error:Object_Class_Id_NotFound', $this->m_sLinkedClass, $iObjectId); + } + } + //var_dump($aTable); + //$oP->Add("\n"); // Just to make sure it's not empty + } +} +?> diff --git a/application/uiwizard.class.inc.php b/application/uiwizard.class.inc.php new file mode 100644 index 0000000000..6fd612bf04 --- /dev/null +++ b/application/uiwizard.class.inc.php @@ -0,0 +1,332 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class UIWizard +{ + protected $m_oPage; + protected $m_sClass; + protected $m_sTargetState; + protected $m_aWizardSteps; + + public function __construct($oPage, $sClass, $sTargetState = '') + { + $this->m_oPage = $oPage; + $this->m_sClass = $sClass; + if (empty($sTargetState)) + { + $sTargetState = MetaModel::GetDefaultState($sClass); + } + $this->m_sTargetState = $sTargetState; + $this->m_aWizardSteps = $this->ComputeWizardStructure(); + } + + public function GetObjectClass() { return $this->m_sClass; } + public function GetTargetState() { return $this->m_sTargetState; } + public function GetWizardStructure() { return $this->m_aWizardSteps; } + + /** + * Displays one step of the wizard + */ + public function DisplayWizardStep($aStep, $iStepIndex, &$iMaxInputId, &$aFieldsMap, $bFinishEnabled = false, $aArgs = array()) + { + if ($iStepIndex == 1) // one big form that contains everything, to make sure that the uploaded files are posted too + { + $this->m_oPage->add("
\n"); + } + $this->m_oPage->add("
\n"); + $this->m_oPage->add("\n"); + $aStates = MetaModel::EnumStates($this->m_sClass); + $aDetails = array(); + $sJSHandlerCode = ''; // Javascript code to be executed each time this step of the wizard is entered + foreach($aStep as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oAttDef->IsWritable()) + { + $sAttLabel = $oAttDef->GetLabel(); + $iOptions = isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode]) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; + + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + if ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) + { + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + $aFields[$sAttCode][$sCode] = ''; + } + } + if (count($aPrerequisites) > 0) + { + $aOptions[] = 'Prerequisites: '.implode(', ', $aPrerequisites); + } + + $sFieldFlag = (($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE)) || (!$oAttDef->IsNullAllowed()) )? ' *' : ''; + $oDefaultValuesSet = $oAttDef->GetDefaultValue(/* $oObject->ToArgs() */); // @@@ TO DO: get the object's current value if the object exists + $sHTMLValue = cmdbAbstractObject::GetFormElementForField($this->m_oPage, $this->m_sClass, $sAttCode, $oAttDef, $oDefaultValuesSet, '', "att_$iMaxInputId", '', $iOptions, $aArgs); + $aFieldsMap["att_$iMaxInputId"] = $sAttCode; + $aDetails[] = array('label' => ''.$oAttDef->GetLabel().$sFieldFlag.'', 'value' => "$sHTMLValue"); + if ($oAttDef->GetValuesDef() != null) + { + $sJSHandlerCode .= "\toWizardHelper.RequestAllowedValues('$sAttCode');\n"; + } + if ($oAttDef->GetDefaultValue() != null) + { + $sJSHandlerCode .= "\toWizardHelper.RequestDefaultValue('$sAttCode');\n"; + } + if ($oAttDef->IsLinkSet()) + { + $sJSHandlerCode .= "\toLinkWidgetatt_$iMaxInputId.Init();"; + } + $iMaxInputId++; + } + } + //$aDetails[] = array('label' => '', 'value' => ''); + $this->m_oPage->details($aDetails); + $sBackButtonDisabled = ($iStepIndex <= 1) ? 'disabled' : ''; + $sDisabled = $bFinishEnabled ? '' : 'disabled'; + $nbSteps = count($this->m_aWizardSteps['mandatory']) + count($this->m_aWizardSteps['optional']); + $this->m_oPage->add("
+ + + +
\n"); + $this->m_oPage->add(" +\n"); + $this->m_oPage->add("
\n\n"); + } + + /** + * Display the final step of the wizard: a confirmation screen + */ + public function DisplayFinalStep($iStepIndex, $aFieldsMap) + { + $oAppContext = new ApplicationContext(); + $this->m_oPage->add("\n"); + $this->m_oPage->add("\n"); + } + /** + * Compute the order of the fields & pages in the wizard + * @param $oPage iTopWebPage The current page (used to display error messages) + * @param $sClass string Name of the class + * @param $sStateCode string Code of the target state of the object + * @return hash Two dimensional array: each element represents the list of fields for a given page + */ + protected function ComputeWizardStructure() + { + $aWizardSteps = array( 'mandatory' => array(), 'optional' => array()); + $aFieldsDone = array(); // Store all the fields that are already covered by a previous step of the wizard + + $aStates = MetaModel::EnumStates($this->m_sClass); + $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); + + $aMandatoryAttributes = array(); + // Some attributes are always mandatory independently of the state machine (if any) + foreach(MetaModel::GetAttributesList($this->m_sClass) as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if (!$oAttDef->IsExternalField() && !$oAttDef->IsNullAllowed() && + $oAttDef->IsWritable() && ($sAttCode != $sStateAttCode) ) + { + $aMandatoryAttributes[$sAttCode] = OPT_ATT_MANDATORY; + } + } + + // Now check the attributes that are mandatory in the specified state + if ( (!empty($this->m_sTargetState)) && (count($aStates[$this->m_sTargetState]['attribute_list']) > 0) ) + { + // Check all the fields that *must* be included in the wizard for this + // particular target state + $aFields = array(); + foreach($aStates[$this->m_sTargetState]['attribute_list'] as $sAttCode => $iOptions) + { + if ( (isset($aMandatoryAttributes[$sAttCode])) && + ($aMandatoryAttributes[$sAttCode] & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) ) + { + $aMandatoryAttributes[$sAttCode] |= $iOptions; + } + else + { + $aMandatoryAttributes[$sAttCode] = $iOptions; + } + } + } + + // Check all the fields that *must* be included in the wizard + // i.e. all mandatory, must-change or must-prompt fields that are + // not also read-only or hidden. + // Some fields may be required (null not allowed) from the database + // perspective, but hidden or read-only from the user interface perspective + $aFields = array(); + foreach($aMandatoryAttributes as $sAttCode => $iOptions) + { + if ( ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) && + !($iOptions & (OPT_ATT_READONLY | OPT_ATT_HIDDEN)) ) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + $aFields[$sAttCode][$sCode] = ''; + } + } + } + + // Now use the dependencies between the fields to order them + // Start from the order of the 'details' + $aList = MetaModel::GetZListItems($this->m_sClass, 'details'); + $index = 0; + $aOrder = array(); + foreach($aFields as $sAttCode => $void) + { + $aOrder[$sAttCode] = 999; // At the end of the list... + } + foreach($aList as $sAttCode) + { + if (array_key_exists($sAttCode, $aFields)) + { + $aOrder[$sAttCode] = $index; + } + $index++; + } + foreach($aFields as $sAttCode => $aDependencies) + { + // All fields with no remaining dependencies can be entered at this + // step of the wizard + if (count($aDependencies) > 0) + { + $iMaxPos = 0; + // Remove this field from the dependencies of the other fields + foreach($aDependencies as $sDependentAttCode => $void) + { + // position the current field after the ones it depends on + $iMaxPos = max($iMaxPos, 1+$aOrder[$sDependentAttCode]); + } + } + } + asort($aOrder); + $aCurrentStep = array(); + foreach($aOrder as $sAttCode => $rank) + { + $aCurrentStep[] = $sAttCode; + $aFieldsDone[$sAttCode] = ''; + } + $aWizardSteps['mandatory'][] = $aCurrentStep; + + + // Now computes the steps to fill the optional fields + $aFields = array(); // reset + foreach(MetaModel::ListAttributeDefs($this->m_sClass) as $sAttCode=>$oAttDef) + { + $iOptions = (isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode])) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; + if ( ($sStateAttCode != $sAttCode) && + (!$oAttDef->IsExternalField()) && + (($iOptions & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)) == 0) && + (!isset($aFieldsDone[$sAttCode])) ) + + { + // 'State', external fields, read-only and hidden fields + // and fields that are already listed in the wizard + // are removed from the 'optional' part of the wizard + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + if (!isset($aFieldsDone[$sCode])) + { + // retain only the dependencies that were not covered + // in the 'mandatory' part of the wizard + $aFields[$sAttCode][$sCode] = ''; + } + } + } + } + // Now use the dependencies between the fields to order them + while(count($aFields) > 0) + { + $aCurrentStep = array(); + foreach($aFields as $sAttCode => $aDependencies) + { + // All fields with no remaining dependencies can be entered at this + // step of the wizard + if (count($aDependencies) == 0) + { + $aCurrentStep[] = $sAttCode; + $aFieldsDone[$sAttCode] = ''; + unset($aFields[$sAttCode]); + // Remove this field from the dependencies of the other fields + foreach($aFields as $sUpdatedCode => $aDummy) + { + // remove the dependency + unset($aFields[$sUpdatedCode][$sAttCode]); + } + } + } + if (count($aCurrentStep) == 0) + { + // This step of the wizard would contain NO field ! + $this->m_oPage->add(Dict::S('UI:Error:WizardCircularReferenceInDependencies')); + print_r($aFields); + break; + } + $aWizardSteps['optional'][] = $aCurrentStep; + } + return $aWizardSteps; + + } +} +?> diff --git a/application/user.preferences.class.inc.php b/application/user.preferences.class.inc.php new file mode 100644 index 0000000000..591e5e5632 --- /dev/null +++ b/application/user.preferences.class.inc.php @@ -0,0 +1,177 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ +require_once(APPROOT.'/core/dbobject.class.php'); +require_once(APPROOT.'/core/userrights.class.inc.php'); + +/** + * This class is used to store, in a persistent manner, user related settings (preferences) + * For each user, one record in the database will be created, making their preferences permanent and storing a list + * of properties (pairs of name/value strings) + * This overcomes some limitations of cookies: limited number of cookies, maximum size, depends on the browser, etc.. + * This class is used in conjunction with the GetUserPreferences/SetUserPreferences javascript functions (utils.js) + */ +class appUserPreferences extends DBObject +{ + static $oUserPrefs = null; // Local cache + + /** + * Get the value of the given property/preference + * If not set, the default value will be returned + * @param string $sCode Code/Name of the property to set + * @param string $sDefaultValue The default value + * @return string The value of the property for the current user + */ + static function GetPref($sCode, $sDefaultValue) + { + if (self::$oUserPrefs == null) + { + self::Load(); + } + $aPrefs = self::$oUserPrefs->Get('preferences'); + if (isset($aPrefs[$sCode])) + { + return $aPrefs[$sCode]; + } + else + { + return $sDefaultValue; + } + } + + /** + * Set the value for a given preference, and stores it into the database + * @param string $sCode Code/Name of the property/preference to set + * @param string $sValue Value to set + */ + static function SetPref($sCode, $sValue) + { + if (self::$oUserPrefs == null) + { + self::Load(); + } + $aPrefs = self::$oUserPrefs->Get('preferences'); + $aPrefs[$sCode] = $sValue; + self::$oUserPrefs->Set('preferences', $aPrefs); + self::Save(); + } + + /** + * Call this function to get all the preferences for the user, packed as a JSON object + * @return string JSON representation of the preferences + */ + static function GetAsJSON() + { + if (self::$oUserPrefs == null) + { + self::Load(); + } + $aPrefs = self::$oUserPrefs->Get('preferences'); + return json_encode($aPrefs); + } + + /** + * Call this function if the user has changed (like when doing a logoff...) + */ + static public function Reset() + { + self::$oUserPrefs = null; + } + /** + * Call this function to ERASE all the preferences from the current user + */ + static public function ClearPreferences() + { + self::$oUserPrefs = null; + } + + static protected function Save() + { + if (self::$oUserPrefs != null) + { + if (self::$oUserPrefs->IsModified()) + { + self::$oUserPrefs->DBUpdate(); + } + } + } + + /** + * Loads the preferences for the current user, creating the record in the database + * if needed + */ + static protected function Load() + { + if (self::$oUserPrefs != null) return; + $oSearch = new DBObjectSearch('appUserPreferences'); + $oSearch->AddCondition('userid', UserRights::GetUserId(), '='); + $oSet = new DBObjectSet($oSearch); + $oObj = $oSet->Fetch(); + if ($oObj == null) + { + // No prefs (yet) for this user, create the object + $oObj = new appUserPreferences(); + $oObj->Set('userid', UserRights::GetUserId()); + $oObj->Set('preferences', array()); // Default preferences: an empty array + try + { + $oObj->DBInsert(); + } + catch(Exception $e) + { + // Ignore errors + } + } + self::$oUserPrefs = $oObj; + } + + public static function Init() + { + $aParams = array + ( + "category" => "gui", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_app_preferences", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("preferences", array("allowed_values"=>null, "sql"=>"preferences", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + } + + /** + * Overloading this function here to secure a fix done right before the release + * The real fix should be to implement this verb in DBObject + */ + public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + $this->DBDelete(); + } +} +?> diff --git a/application/utils.inc.php b/application/utils.inc.php new file mode 100644 index 0000000000..84a4b5bbba --- /dev/null +++ b/application/utils.inc.php @@ -0,0 +1,309 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/core/config.class.inc.php'); +require_once(APPROOT.'/application/transaction.class.inc.php'); + +define('ITOP_CONFIG_FILE', APPROOT.'/config-itop.php'); + +class FileUploadException extends Exception +{ +} + + +/** + * Helper functions to interact with forms: read parameters, upload files... + * @package iTop + */ +class utils +{ + private static $m_sConfigFile = ITOP_CONFIG_FILE; + private static $m_oConfig = null; + + + public static function IsModeCLI() + { + $sSAPIName = php_sapi_name(); + $sCleanName = strtolower(trim($sSAPIName)); + if ($sCleanName == 'cli') + { + return true; + } + else + { + return false; + } + } + + + public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false) + { + global $argv; + $retValue = $defaultValue; + if (isset($_REQUEST[$sName])) + { + $retValue = $_REQUEST[$sName]; + } + elseif ($bAllowCLI && isset($argv)) + { + foreach($argv as $iArg => $sArg) + { + if (preg_match('/^--'.$sName.'=(.*)$/', $sArg, $aMatches)) + { + $retValue = $aMatches[1]; + } + } + } + return $retValue; + } + + public static function ReadPostedParam($sName, $defaultValue = "") + { + return isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue; + } + + /** + * Reads an uploaded file and turns it into an ormDocument object - Triggers an exception in case of error + * @param string $sName Name of the input used from uploading the file + * @return ormDocument The uploaded file (can be 'empty' if nothing was uploaded) + */ + public static function ReadPostedDocument($sName) + { + $oDocument = new ormDocument(); // an empty document + if(isset($_FILES[$sName])) + { + switch($_FILES[$sName]['error']) + { + case UPLOAD_ERR_OK: + $doc_content = file_get_contents($_FILES[$sName]['tmp_name']); + $sMimeType = $_FILES[$sName]['type']; + if (function_exists('finfo_file')) + { + // as of PHP 5.3 the fileinfo extension is bundled within PHP + // in which case we don't trust the mime type provided by the browser + $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension + if ($rInfo !== false) + { + $sType = @finfo_file($rInfo, $file); + if ( ($sType !== false) + && is_string($sType) + && (strlen($sType)>0)) + { + $sMimeType = $sType; + } + } + @finfo_close($rInfo); + } + $oDocument = new ormDocument($doc_content, $sMimeType, $_FILES[$sName]['name']); + break; + + case UPLOAD_ERR_NO_FILE: + // no file to load, it's a normal case, just return an empty document + break; + + case UPLOAD_ERR_FORM_SIZE: + case UPLOAD_ERR_INI_SIZE: + throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize'))); + break; + + case UPLOAD_ERR_PARTIAL: + throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.')); + break; + + case UPLOAD_ERR_NO_TMP_DIR: + throw new FileUploadException(Dict::S('UI:Error:NoTmpDir')); + break; + + case UPLOAD_ERR_CANT_WRITE: + throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir'))); + break; + + case UPLOAD_ERR_EXTENSION: + throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $_FILES[$sName]['name'])); + break; + + default: + throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $_FILES[$sName]['error'])); + break; + + } + } + return $oDocument; + } + + public static function GetNewTransactionId() + { + return privUITransaction::GetNewTransactionId(); + } + + public static function IsTransactionValid($sId, $bRemoveTransaction = true) + { + return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction); + } + + public static function RemoveTransaction($sId) + { + return privUITransaction::RemoveTransaction($sId); + } + + public static function ReadFromFile($sFileName) + { + if (!file_exists($sFileName)) return false; + return file_get_contents($sFileName); + } + + /** + * Helper function to convert a value expressed in a 'user friendly format' + * as in php.ini, e.g. 256k, 2M, 1G etc. Into a number of bytes + * @param mixed $value The value as read from php.ini + * @return number + */ + public static function ConvertToBytes( $value ) + { + $iReturn = $value; + if ( !is_numeric( $value ) ) + { + $iLength = strlen( $value ); + $iReturn = substr( $value, 0, $iLength - 1 ); + $sUnit = strtoupper( substr( $value, $iLength - 1 ) ); + switch ( $sUnit ) + { + case 'G': + $iReturn *= 1024; + case 'M': + $iReturn *= 1024; + case 'K': + $iReturn *= 1024; + } + } + return $iReturn; + } + + /** + * Returns an absolute URL to the current page + * @param $bQueryString bool True to also get the query string, false otherwise + * @param $bForceHTTPS bool True to force HTTPS, false otherwise + * @return string The absolute URL to the current page + */ + static public function GetAbsoluteUrl($bQueryString = true, $bForceHTTPS = false) + { + // Build an absolute URL to this page on this server/port + $sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; + if (MetaModel::GetConfig()->GetSecureConnectionRequired() || MetaModel::GetConfig()->GetHttpsHyperlinks()) + { + // If a secure connection is required, or if the URL is requested to start with HTTPS + // then any URL must start with https ! + $bForceHTTPS = true; + } + if ($bForceHTTPS) + { + $sProtocol = 'https'; + $sPort = ''; + } + else + { + $sProtocol = (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!="off")) ? 'https' : 'http'; + $iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; + if ($sProtocol == 'http') + { + $sPort = ($iPort == 80) ? '' : ':'.$iPort; + } + else + { + $sPort = ($iPort == 443) ? '' : ':'.$iPort; + } + } + // $_SERVER['REQUEST_URI'] is empty when running on IIS + // Let's use Ivan Tcholakov's fix (found on www.dokeos.com) + if (!empty($_SERVER['REQUEST_URI'])) + { + $sPath = $_SERVER['REQUEST_URI']; + } + else + { + $sPath = $_SERVER['SCRIPT_NAME']; + if (!empty($_SERVER['QUERY_STRING'])) + { + $sPath .= '?'.$_SERVER['QUERY_STRING']; + } + $_SERVER['REQUEST_URI'] = $sPath; + } + $sPath = $_SERVER['REQUEST_URI']; + if (!$bQueryString) + { + // remove all the parameters from the query string + $iQuestionMarkPos = strpos($sPath, '?'); + if ($iQuestionMarkPos !== false) + { + $sPath = substr($sPath, 0, $iQuestionMarkPos); + } + } + $sUrl = "$sProtocol://{$sServerName}{$sPort}{$sPath}"; + + return $sUrl; + } + + /** + * Returns the absolute URL PATH of the current page + * @param $bForceHTTPS bool True to force HTTPS, false otherwise + * @return string The absolute URL to the current page + */ + static public function GetAbsoluteUrlPath($bForceHTTPS = false) + { + $sAbsoluteUrl = self::GetAbsoluteUrl(false, $bForceHTTPS); // False => Don't get the query string + $sAbsoluteUrl = substr($sAbsoluteUrl, 0, 1+strrpos($sAbsoluteUrl, '/')); // remove the current page, keep just the path, up to the last / + return $sAbsoluteUrl; + } + + /** + * Returns the absolute URL to the server's root path + * @param $bForceHTTPS bool True to force HTTPS, false otherwise + * @return string The absolute URL to the server's root, without the first slash + */ + static public function GetAbsoluteUrlRoot($bForceHTTPS = false) + { + $sAbsoluteUrl = self::GetAbsoluteUrl(false, $bForceHTTPS); // False => Don't get the query string + $sServerPos = 3 + strpos($sAbsoluteUrl, '://'); + $iFirstSlashPos = strpos($sAbsoluteUrl, '/', $sServerPos); + if ($iFirstSlashPos !== false) + { + $sAbsoluteUrl = substr($sAbsoluteUrl, 0, $iFirstSlashPos); // remove the current page, keep just the path, without the first / + } + return $sAbsoluteUrl; + } + + /** + * Tells whether or not log off operation is supported. + * Actually in only one case: + * 1) iTop is using an internal authentication + * 2) the user did not log-in using the "basic" mode (i.e basic authentication) or by passing credentials in the URL + * @return boolean True if logoff is supported, false otherwise + */ + static function CanLogOff() + { + return (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'form'); + } +} +?> diff --git a/application/webpage.class.inc.php b/application/webpage.class.inc.php new file mode 100644 index 0000000000..caa30a2cee --- /dev/null +++ b/application/webpage.class.inc.php @@ -0,0 +1,373 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +/** + * Simple helper class to ease the production of HTML pages + * + * This class provide methods to add content, scripts, includes... to a web page + * and renders the full web page by putting the elements in the proper place & order + * when the output() method is called. + * Usage: + * $oPage = new WebPage("Title of my page"); + * $oPage->p("Hello World !"); + * $oPage->output(); + */ +class WebPage +{ + protected $s_title; + protected $s_content; + protected $s_deferred_content; + protected $a_scripts; + protected $a_styles; + protected $a_include_scripts; + protected $a_include_stylesheets; + protected $a_headers; + protected $a_base; + protected $iNextId; + + public function __construct($s_title) + { + $this->s_title = $s_title; + $this->s_content = ""; + $this->s_deferred_content = ''; + $this->a_scripts = array(); + $this->a_styles = array(); + $this->a_linked_scripts = array(); + $this->a_linked_stylesheets = array(); + $this->a_headers = array(); + $this->a_base = array( 'href' => '', 'target' => ''); + $this->iNextId = 0; + ob_start(); // Start capturing the output + } + + /** + * Change the title of the page after its creation + */ + public function set_title($s_title) + { + $this->s_title = $s_title; + } + + /** + * Specify a default URL and a default target for all links on a page + */ + public function set_base($s_href = '', $s_target = '') + { + $this->a_base['href'] = $s_href; + $this->a_base['target'] = $s_target; + } + + /** + * Add any text or HTML fragment to the body of the page + */ + public function add($s_html) + { + $this->s_content .= $s_html; + } + + /** + * Add any text or HTML fragment at the end of the body of the page + * This is useful to add hidden content, DIVs or FORMs that should not + * be embedded into each other. + */ + public function add_at_the_end($s_html) + { + $this->s_deferred_content .= $s_html; + } + + /** + * Add a paragraph to the body of the page + */ + public function p($s_html) + { + $this->add($this->GetP($s_html)); + } + + /** + * Add a pre-formatted text to the body of the page + */ + public function pre($s_html) + { + $this->add('
'.$s_html.'
'); + } + + /** + * Add a paragraph to the body of the page + */ + public function GetP($s_html) + { + return "

$s_html

\n"; + } + + /** + * Adds a tabular content to the web page + * @param Hash $aConfig Configuration of the table: hash array of 'column_id' => 'Column Label' + * @param Hash $aData Hash array. Data to display in the table: each row is made of 'column_id' => Data. A column 'pkey' is expected for each row + * @param Hash $aParams Hash array. Extra parameters for the table. + * @return void + */ + public function table($aConfig, $aData, $aParams = array()) + { + $this->add($this->GetTable($aConfig, $aData, $aParams)); + } + + public function GetTable($aConfig, $aData, $aParams = array()) + { + $oAppContext = new ApplicationContext(); + + static $iNbTables = 0; + $iNbTables++; + $sHtml = ""; + $sHtml .= "\n"; + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach($aConfig as $sName=>$aDef) + { + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach($aData as $aRow) + { + if (isset($aRow['@class'])) // Row specific class, for hilighting certain rows + { + $sHtml .= "\n"; + } + else + { + $sHtml .= "\n"; + } + foreach($aConfig as $sName=>$aAttribs) + { + $aMatches = array(); + $sClass = isset($aAttribs['class']) ? 'class="'.$aAttribs['class'].'"' : ''; + $sValue = ($aRow[$sName] === '') ? ' ' : $aRow[$sName]; + $sHtml .= "\n"; + } + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "
".$aDef['label']."
$sValue
\n"; + return $sHtml; + } + + /** + * Add some Javascript to the header of the page + */ + public function add_script($s_script) + { + $this->a_scripts[] = $s_script; + } + + /** + * Add some Javascript to the header of the page + */ + public function add_ready_script($s_script) + { + // Do nothing silently... this is not supported by this type of page... + } + /** + * Add some CSS definitions to the header of the page + */ + public function add_style($s_style) + { + $this->a_styles[] = $s_style; + } + + /** + * Add a script (as an include, i.e. link) to the header of the page + */ + public function add_linked_script($s_linked_script) + { + $this->a_linked_scripts[] = $s_linked_script; + } + + /** + * Add a CSS stylesheet (as an include, i.e. link) to the header of the page + */ + public function add_linked_stylesheet($s_linked_stylesheet, $s_condition = "") + { + $this->a_linked_stylesheets[] = array( 'link' => $s_linked_stylesheet, 'condition' => $s_condition); + } + + /** + * Add some custom header to the page + */ + public function add_header($s_header) + { + $this->a_headers[] = $s_header; + } + + /** + * Add needed headers to the page so that it will no be cached + */ + public function no_cache() + { + $this->add_header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 + $this->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past + } + + /** + * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data + */ + public function details($aFields) + { + + $this->add($this->GetDetails($aFields)); + } + + /** + * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data + */ + public function GetDetails($aFields) + { + $sHtml = "\n"; + $sHtml .= "\n"; + foreach($aFields as $aAttrib) + { + $sHtml .= "\n"; + // By Rom, for csv import, proposed to show several values for column selection + if (is_array($aAttrib['value'])) + { + $sHtml .= "\n"; + } + else + { + $sHtml .= "\n"; + } + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "
".$aAttrib['label']."".implode("", $aAttrib['value'])."".$aAttrib['label']."".$aAttrib['value']."
\n"; + return $sHtml; + } + + /** + * Outputs (via some echo) the complete HTML page by assembling all its elements + */ + public function output() + { + foreach($this->a_headers as $s_header) + { + header($s_header); + } + $s_captured_output = ob_get_contents(); + ob_end_clean(); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "{$this->s_title}\n"; + echo $this->get_base_tag(); + foreach($this->a_linked_scripts as $s_script) + { + echo "\n"; + } + if (count($this->a_scripts)>0) + { + echo "\n"; + } + foreach($this->a_linked_stylesheets as $a_stylesheet) + { + if ($a_stylesheet['condition'] != "") + { + echo "\n"; + } + } + + if (count($this->a_styles)>0) + { + echo "\n"; + } + echo "\n"; + echo "\n"; + echo $this->s_content; + if (trim($s_captured_output) != "") + { + echo "
$s_captured_output
\n"; + } + echo $this->s_deferred_content; + echo "\n"; + echo "\n"; + } + + /** + * Build a series of hidden field[s] from an array + */ + // By Rom - je verrais bien une serie d'outils pour gerer des parametres que l'on retransmet entre pages d'un wizard... + // ptet deriver webpage en webwizard + public function add_input_hidden($sLabel, $aData) + { + foreach($aData as $sKey=>$sValue) + { + $this->add(""); + } + } + + protected function get_base_tag() + { + $sTag = ''; + if (($this->a_base['href'] != '') || ($this->a_base['target'] != '')) + { + $sTag = 'a_base['href'] != '')) + { + $sTag .= "href =\"{$this->a_base['href']}\" "; + } + if (($this->a_base['target'] != '')) + { + $sTag .= "target =\"{$this->a_base['target']}\" "; + } + $sTag .= " />\n"; + } + return $sTag; + } + + /** + * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page + * @return int The unique ID (in this page) + */ + public function GetUniqueId() + { + return $this->iNextId++; + } +} +?> \ No newline at end of file diff --git a/application/wizardhelper.class.inc.php b/application/wizardhelper.class.inc.php new file mode 100644 index 0000000000..db231ca9a4 --- /dev/null +++ b/application/wizardhelper.class.inc.php @@ -0,0 +1,273 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT.'/application/uiwizard.class.inc.php'); + +class WizardHelper +{ + protected $m_aData; + + public function __construct() + { + } + /** + * Constructs the PHP target object from the parameters sent to the web page by the wizard + * @param boolean $bReadUploadedFiles True to also ready any uploaded file (for blob/document fields) + * @return object + */ + public function GetTargetObject($bReadUploadedFiles = false) + { + if (isset($this->m_aData['m_oCurrentValues']['id'])) + { + $oObj = MetaModel::GetObject($this->m_aData['m_sClass'], $this->m_aData['m_oCurrentValues']['id']); + } + else + { + $oObj = MetaModel::NewObject($this->m_aData['m_sClass']); + } + foreach($this->m_aData['m_oCurrentValues'] as $sAttCode => $value) + { + // Because this is stored in a Javascript array, unused indexes + // are filled with null values and unused keys (stored as strings) contain $$NULL$$ + if ( ($sAttCode !='id') && ($sAttCode !== false) && ($value !== null) && ($value !== '$$NULL$$')) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); + if (($oAttDef->IsLinkSet()) && ($value != '') ) + { + // special handling for lists + // assumes this is handled as an array of objects + // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] + $aData = json_decode($value, true); // true means decode as a hash array (not an object) + // Check what are the meaningful attributes + $aFields = $this->GetLinkedWizardStructure($oAttDef); + $sLinkedClass = $oAttDef->GetLinkedClass(); + $aLinkedObjectsArray = array(); + if (!is_array($aData)) + { + echo ("aData: '$aData' (value: '$value')\n"); + } + foreach($aData as $aLinkedObject) + { + $oLinkedObj = MetaModel::NewObject($sLinkedClass); + foreach($aFields as $sLinkedAttCode) + { + if ( isset($aLinkedObject[$sLinkedAttCode]) && ($aLinkedObject[$sLinkedAttCode] !== null) ) + { + $sLinkedAttDef = MetaModel::GetAttributeDef($sLinkedClass, $sLinkedAttCode); + if (($sLinkedAttDef->IsExternalKey()) && ($aLinkedObject[$sLinkedAttCode] != '') && ($aLinkedObject[$sLinkedAttCode] != 0) ) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($sLinkedAttDef->GetTargetClass(), $aLinkedObject[$sLinkedAttCode]); + $oLinkedObj->Set($sLinkedAttCode, $oTargetObj); + } + else + { + $oLinkedObj->Set($sLinkedAttCode, $aLinkedObject[$sLinkedAttCode]); + } + } + } + $aLinkedObjectsArray[] = $oLinkedObj; + } + $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); + $oObj->Set($sAttCode, $oSet); + } + else if ( $oAttDef->GetEditClass() == 'Document' ) + { + if ($bReadUploadedFiles) + { + $oDocument = utils::ReadPostedDocument('file_'.$sAttCode); + $oObj->Set($sAttCode, $oDocument); + } + else + { + // Create a new empty document, just for displaying the file name + $oDocument = new ormDocument(null, '', $value); + $oObj->Set($sAttCode, $oDocument); + } + } + else if (($oAttDef->IsExternalKey()) && (!empty($value)) ) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value); + $oObj->Set($sAttCode, $oTargetObj); + } + else + { + $oObj->Set($sAttCode, $value); + } + } + } + return $oObj; + } + + public function GetFieldsForDefaultValue() + { + return $this->m_aData['m_aDefaultValueRequested']; + } + + public function SetDefaultValue($sAttCode, $value) + { + // Protect against a request for a non existing field + if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); + if ($oAttDef->GetEditClass() == 'List') + { + // special handling for lists + // this as to be handled as an array of objects + // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] + // NOT YET IMPLEMENTED !! + $sLinkedClass = $oAttDef->GetLinkedClass(); + $oSet = $value; + $aData = array(); + $aFields = $this->GetLinkedWizardStructure($oAttDef); + while($oSet->fetch()) + { + foreach($aFields as $sLinkedAttCode) + { + $aRow[$sAttCode] = $oLinkedObj->Get($sLinkedAttCode); + } + $aData[] = $aRow; + } + $this->m_aData['m_oDefaultValue'][$sAttCode] = json_encode($aData); + + } + else + { + // Normal handling for all other scalar attributes + $this->m_aData['m_oDefaultValue'][$sAttCode] = $value; + } + } + } + + public function GetFieldsForAllowedValues() + { + return $this->m_aData['m_aAllowedValuesRequested']; + } + + public function SetAllowedValuesHtml($sAttCode, $sHtml) + { + // Protect against a request for a non existing field + if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) + { + $this->m_aData['m_oAllowedValues'][$sAttCode] = $sHtml; + } + } + + public function ToJSON() + { + return json_encode($this->m_aData); + } + + static public function FromJSON($sJSON) + { + $oWizHelper = new WizardHelper(); + if (get_magic_quotes_gpc()) + { + $sJSON = stripslashes($sJSON); + } + $aData = json_decode($sJSON, true); // true means hash array instead of object + $oWizHelper->m_aData = $aData; + return $oWizHelper; + } + + protected function GetLinkedWizardStructure($oAttDef) + { + $oWizard = new UIWizard(null, $oAttDef->GetLinkedClass()); + $aWizardSteps = $oWizard->GetWizardStructure(); + $aFields = array(); + $sExtKeyToMeCode = $oAttDef->GetExtKeyToMe(); + // Retrieve as a flat list, all the attributes that are needed to create + // an object of the linked class and put them into a flat array, except + // the attribute 'ext_key_to_me' which is a constant in our case + foreach($aWizardSteps as $sDummy => $aMainSteps) + { + // 2 entries: 'mandatory' and 'optional' + foreach($aMainSteps as $aSteps) + { + // One entry for each step of the wizard + foreach($aSteps as $sAttCode) + { + if ($sAttCode != $sExtKeyToMeCode) + { + $aFields[] = $sAttCode; + } + } + } + } + return $aFields; + } + + public function GetTargetClass() + { + return $this->m_aData['m_sClass']; + } + + public function GetFormPrefix() + { + return $this->m_aData['m_sFormPrefix']; + } + + public function GetIdForField($sFieldName) + { + $sResult = ''; + // It may happen that the field we'd like to update does not + // exist in the form. For example, if the field should be hidden/read-only + // in the current state of the object + if (isset($this->m_aData['m_oFieldsMap'][$sFieldName])) + { + $sResult = $this->m_aData['m_oFieldsMap'][$sFieldName]; + } + return $sResult; + } + + static function ParseJsonSet($oMe, $sLinkClass, $sExtKeyToMe, $sJsonSet) + { + $aSet = json_decode($sJsonSet, true); // true means hash array instead of object + $oSet = CMDBObjectSet::FromScratch($sLinkClass); + foreach($aSet as $aLinkObj) + { + $oLink = MetaModel::NewObject($sLinkClass); + foreach($aLinkObj as $sAttCode => $value) + { + $oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode); + if (($oAttDef->IsExternalKey()) && ($value != '') ) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value); + $oLink->Set($sAttCode, $oTargetObj); + } + $oLink->Set($sAttCode, $value); + } + $oLink->Set($sExtKeyToMe, $oMe->GetKey()); + $oSet->AddObject($oLink); + } + return $oSet; + } +} +?> diff --git a/application/xmlpage.class.inc.php b/application/xmlpage.class.inc.php new file mode 100644 index 0000000000..7ebc21aa23 --- /dev/null +++ b/application/xmlpage.class.inc.php @@ -0,0 +1,100 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); +/** + * Simple web page with no includes or fancy formatting, useful to generateXML documents + * The page adds the content-type text/XML and the encoding into the headers + */ +class XMLPage extends WebPage +{ + /** + * For big XML files, it's better NOT to store everything in memory and output the XML piece by piece + */ + var $m_bPassThrough; + var $m_bHeaderSent; + + function __construct($s_title, $bPassThrough = false) + { + parent::__construct($s_title); + $this->m_bPassThrough = $bPassThrough; + $this->m_bHeaderSent = false; + $this->add_header("Content-type: text/xml; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->add_header("Content-location: export.xml"); + } + + public function output() + { + if (!$this->m_bPassThrough) + { + $this->add("\n"); + $this->add_header("Content-Length: ".strlen(trim($this->s_content))); + foreach($this->a_headers as $s_header) + { + header($s_header); + } + echo trim($this->s_content); + } + } + + public function add($sText) + { + if (!$this->m_bPassThrough) + { + parent::add($sText); + } + else + { + if ($this->m_bHeaderSent) + { + echo $sText; + } + else + { + $s_captured_output = ob_get_contents(); + ob_end_clean(); + foreach($this->a_headers as $s_header) + { + header($s_header); + } + echo "\n"; + echo trim($s_captured_output); + echo trim($this->s_content); + echo $sText; + $this->m_bHeaderSent = true; + } + } + } + + public function small_p($sText) + { + } + + public function table($aConfig, $aData, $aParams = array()) + { + } +} +?> diff --git a/approot.inc.php b/approot.inc.php new file mode 100644 index 0000000000..6caae3533f --- /dev/null +++ b/approot.inc.php @@ -0,0 +1,3 @@ + diff --git a/core/MyHelpers.class.inc.php b/core/MyHelpers.class.inc.php new file mode 100644 index 0000000000..c0f392cf8e --- /dev/null +++ b/core/MyHelpers.class.inc.php @@ -0,0 +1,500 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * MyHelpers + * + * @package iTopORM + */ +class MyHelpers +{ + public static function CheckValueInArray($sDescription, $value, $aData) + { + if (!in_array($value, $aData)) + { + self::HandleWrongValue($sDescription, $value, $aData); + } + } + + public static function CheckKeyInArray($sDescription, $key, $aData) + { + if (!array_key_exists($key, $aData)) + { + self::HandleWrongValue($sDescription, $key, array_keys($aData)); + } + } + + public static function HandleWrongValue($sDescription, $value, $aData) + { + if (count($aData) == 0) + { + $sArrayDesc = "{}"; + } + else + { + $sArrayDesc = "{".implode(", ", $aData)."}"; + } + // exit! + throw new CoreException("Wrong value for $sDescription, found '$value' while expecting a value in $sArrayDesc"); + } + + // getmicrotime() + // format sss.mmmuuupppnnn + public static function getmicrotime() + { + list($usec, $sec) = explode(" ",microtime()); + return ((float)$usec + (float)$sec); + } + + /* + * MakeSQLComment + * converts hash into text comment which we can use in a (mySQL) query + */ + public static function MakeSQLComment ($aHash) + { + if (empty($aHash)) return ""; + $sComment = ""; + { + foreach($aHash as $sKey=>$sValue) + { + $sComment .= "\n-- ". $sKey ."=>" . $sValue; + } + } + return $sComment; + } + + public static function var_dump_html($aWords, $bFullDisplay = false) + { + echo "
\n";
+		if ($bFullDisplay)
+		{
+			print_r($aWords); // full dump!
+		}
+		else
+		{
+			var_dump($aWords); // truncate things when they are too big
+		}
+		echo "\n
\n"; + } + + public static function arg_dump_html() + { + echo "
\n";
+		echo "GET:\n";
+		var_dump($_GET);
+		echo "POST:\n";
+		var_dump($_POST);
+		echo "\n
\n"; + } + + public static function var_dump_string($var) + { + ob_start(); + print_r($var); + $sRet = ob_get_clean(); + return $sRet; + } + + protected static function first_diff_line($s1, $s2) + { + $aLines1 = explode("\n", $s1); + $aLines2 = explode("\n", $s2); + for ($i = 0 ; $i < min(count($aLines1), count($aLines2)) ; $i++) + { + if ($aLines1[$i] != $aLines2[$i]) return $i; + } + return false; + } + + protected static function highlight_line($sMultiline, $iLine, $sHighlightStart = '', $sHightlightEnd = '') + { + $aLines = explode("\n", $sMultiline); + $aLines[$iLine] = $sHighlightStart.$aLines[$iLine].$sHightlightEnd; + return implode("\n", $aLines); + } + + protected static function first_diff($s1, $s2) + { + // do not work fine with multiline strings + $iLen1 = strlen($s1); + $iLen2 = strlen($s2); + for ($i = 0 ; $i < min($iLen1, $iLen2) ; $i++) + { + if ($s1[$i] !== $s2[$i]) return $i; + } + return false; + } + + protected static function last_diff($s1, $s2) + { + // do not work fine with multiline strings + $iLen1 = strlen($s1); + $iLen2 = strlen($s2); + for ($i = 0 ; $i < min(strlen($s1), strlen($s2)) ; $i++) + { + if ($s1[$iLen1 - $i - 1] !== $s2[$iLen2 - $i - 1]) return array($iLen1 - $i, $iLen2 - $i); + } + return false; + } + + protected static function text_cmp_html($sText1, $sText2, $sHighlight) + { + $iDiffPos = self::first_diff_line($sText1, $sText2); + $sDisp1 = self::highlight_line($sText1, $iDiffPos, '
', '
'); + $sDisp2 = self::highlight_line($sText2, $iDiffPos, '
', '
'); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
$sDisp1
$sDisp2
\n"; + } + + protected static function string_cmp_html($s1, $s2, $sHighlight) + { + $iDiffPos = self::first_diff($s1, $s2); + if ($iDiffPos === false) + { + echo "strings are identical"; + return; + } + $sStart = substr($s1, 0, $iDiffPos); + + $aLastDiff = self::last_diff($s1, $s2); + $sEnd = substr($s1, $aLastDiff[0]); + + $sMiddle1 = substr($s1, $iDiffPos, $aLastDiff[0] - $iDiffPos); + $sMiddle2 = substr($s2, $iDiffPos, $aLastDiff[1] - $iDiffPos); + + echo "

$sStart$sMiddle1$sEnd

\n"; + echo "

$sStart$sMiddle2$sEnd

\n"; + } + + protected static function object_cmp_html($oObj1, $oObj2, $sHighlight) + { + $sObj1 = self::var_dump_string($oObj1); + $sObj2 = self::var_dump_string($oObj2); + return self::text_cmp_html($sObj1, $sObj2, $sHighlight); + } + + public static function var_cmp_html($var1, $var2, $sHighlight = 'color:red; font-weight:bold;') + { + if (is_object($var1)) + { + return self::object_cmp_html($var1, $var2, $sHighlight); + } + else if (count(explode("\n", $var1)) > 1) + { + // multiline string + return self::text_cmp_html($var1, $var2, $sHighlight); + } + else + { + return self::string_cmp_html($var1, $var2, $sHighlight); + } + } + + public static function get_callstack_html($iLevelsToIgnore = 0, $aCallStack = null) + { + if ($aCallStack == null) $aCallStack = debug_backtrace(); + + $aCallStack = array_slice($aCallStack, $iLevelsToIgnore); + + $aDigestCallStack = array(); + $bFirstLine = true; + foreach ($aCallStack as $aCallInfo) + { + $sLine = empty($aCallInfo['line']) ? "" : $aCallInfo['line']; + $sFile = empty($aCallInfo['file']) ? "" : $aCallInfo['file']; + $sClass = empty($aCallInfo['class']) ? "" : $aCallInfo['class']; + $sType = empty($aCallInfo['type']) ? "" : $aCallInfo['type']; + $sFunction = empty($aCallInfo['function']) ? "" : $aCallInfo['function']; + + if ($bFirstLine) + { + $bFirstLine = false; + // For this line do not display the "function name" because + // that will be the name of our error handler for sure ! + $sFunctionInfo = "N/A"; + } + else + { + $args = ''; + if (empty($aCallInfo['args'])) $aCallInfo['args'] = array(); + foreach ($aCallInfo['args'] as $a) + { + if (!empty($args)) + { + $args .= ', '; + } + switch (gettype($a)) + { + case 'integer': + case 'double': + $args .= $a; + break; + case 'string': + $a = Str::pure2html(self::beautifulstr($a, 1024, true, true)); + $args .= "\"$a\""; + break; + case 'array': + $args .= 'Array('.count($a).')'; + break; + case 'object': + $args .= 'Object('.get_class($a).')'; + break; + case 'resource': + $args .= 'Resource('.strstr($a, '#').')'; + break; + case 'boolean': + $args .= $a ? 'True' : 'False'; + break; + case 'NULL': + $args .= 'Null'; + break; + default: + $args .= 'Unknown'; + } + } + $sFunctionInfo = "$sClass $sType $sFunction($args)"; + } + $aDigestCallStack[] = array('File'=>$sFile, 'Line'=>$sLine, 'Function'=>$sFunctionInfo); + } + return self::make_table_from_assoc_array($aDigestCallStack); + } + + public static function dump_callstack($iLevelsToIgnore = 0, $aCallStack = null) + { + return self::get_callstack_html($iLevelsToIgnore, $aCallStack); + } + + /////////////////////////////////////////////////////////////////////////////// + // Source: New + // Last modif: 2004/12/20 RQU + /////////////////////////////////////////////////////////////////////////////// + public static function make_table_from_assoc_array(&$aData) + { + if (!is_array($aData)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not an array"); + $aFirstRow = reset($aData); + if (count($aData) == 0) return ''; + if (!is_array($aFirstRow)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not a bi-dimensional array"); + $sOutput = ""; + $sOutput .= "\n"; + + // Table header + // + $sOutput .= " \n"; + foreach ($aFirstRow as $fieldname=>$trash) { + $sOutput .= " \n"; + } + $sOutput .= " \n"; + + // Table contents + // + $iCount = 0; + foreach ($aData as $aRow) { + $sStyle = ($iCount++ % 2 ? "STYLE=\"background-color : #eeeeee\"" : ""); + $sOutput .= " \n"; + foreach ($aRow as $data) { + if (strlen($data) == 0) { + $data = " "; + } + $sOutput .= " \n"; + } + $sOutput .= " \n"; + } + + $sOutput .= "
".$fieldname."
".$data."
\n"; + return $sOutput; + } + + public static function debug_breakpoint($arg) + { + echo "

Debug breakpoint

\n"; + MyHelpers::var_dump_html($arg); + MyHelpers::dump_callstack(); + exit; + } + public static function debug_breakpoint_notempty($arg) + { + if (empty($arg)) return; + echo "

Debug breakpoint (triggered on non-empty value)

\n"; + MyHelpers::var_dump_html($arg); + MyHelpers::dump_callstack(); + exit; + } + + /** + * xmlentities() + * ... same as htmlentities, but designed for xml ! + */ + public static function xmlentities($string) + { + return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); + } + + /** + * xmlencode() + * Encodes a string so that for sure it can be output as an xml data string + */ + public static function xmlencode($string) + { + return xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); + } + + /////////////////////////////////////////////////////////////////////////////// + // Source: New - format strings for output + // Last modif: 2005/01/18 RQU + /////////////////////////////////////////////////////////////////////////////// + public static function beautifulstr($sLongString, $iMaxLen, $bShowLen=false, $bShowTooltip=true) + { + if (!is_string($sLongString)) throw new CoreException("beautifulstr: expect a string as 1st argument"); + + // Nothing to do if the string is short + if (strlen($sLongString) <= $iMaxLen) return $sLongString; + + // Truncate the string + $sSuffix = "..."; + if ($bShowLen) { + $sSuffix .= "(".strlen($sLongString)." chars)..."; + } + $sOutput = substr($sLongString, 0, $iMaxLen - strlen($sSuffix)).$sSuffix; + $sOutput = htmlspecialchars($sOutput); + + // Add tooltip if required + //if ($bShowTooltip) { + // $oTooltip = new gui_tooltip($sLongString); + // $sOutput = "get_mouseOver_code().">".$sOutput.""; + //} + return $sOutput; + } +} + +/** +Utility class: static methods for cleaning & escaping untrusted (i.e. +user-supplied) strings. +Any string can (usually) be thought of as being in one of these 'modes': +pure = what the user actually typed / what you want to see on the page / + what is actually stored in the DB +gpc = incoming GET, POST or COOKIE data +sql = escaped for passing safely to RDBMS via SQL (also, data from DB + queries and file reads if you have magic_quotes_runtime on--which + is rare) +html = safe for html display (htmlentities applied) +Always knowing what mode your string is in--using these methods to +convert between modes--will prevent SQL injection and cross-site scripting. +This class refers to its own namespace (so it can work in PHP 4--there is no +self keyword until PHP 5). Do not change the name of the class w/o changing +all the internal references. +Example usage: a POST value that you want to query with: +$username = Str::gpc2sql($_POST['username']); +*/ +//This sets SQL escaping to use slashes; for Sybase(/MSSQL)-style escaping +// ( ' --> '' ), set to true. +define('STR_SYBASE', false); +class Str +{ + public static function gpc2sql($gpc, $maxLength = false) + { + return self::pure2sql(self::gpc2pure($gpc), $maxLength); + } + public static function gpc2html($gpc, $maxLength = false) + { + return self::pure2html(self::gpc2pure($gpc), $maxLength); + } + public static function gpc2pure($gpc) + { + if (ini_get('magic_quotes_sybase')) $pure = str_replace("''", "'", $gpc); + else $pure = get_magic_quotes_gpc() ? stripslashes($gpc) : $gpc; + return $pure; + } + public static function html2pure($html) + { + return html_entity_decode($html); + } + public static function html2sql($html, $maxLength = false) + { + return self::pure2sql(self::html2pure($html), $maxLength); + } + public static function pure2html($pure, $maxLength = false) + { + // Check for HTML entities, but be careful the DB is in UTF-8 + return $maxLength + ? htmlentities(substr($pure, 0, $maxLength), ENT_COMPAT, 'UTF-8') + : htmlentities($pure, ENT_COMPAT, 'UTF-8'); + } + public static function pure2sql($pure, $maxLength = false) + { + if ($maxLength) $pure = substr($pure, 0, $maxLength); + return (STR_SYBASE) + ? str_replace("'", "''", $pure) + : addslashes($pure); + } + public static function sql2html($sql, $maxLength = false) + { + $pure = self::sql2pure($sql); + if ($maxLength) $pure = substr($pure, 0, $maxLength); + return self::pure2html($pure); + } + public static function sql2pure($sql) + { + return (STR_SYBASE) + ? str_replace("''", "'", $sql) + : stripslashes($sql); + } + + public static function xml2pure($xml) + { + // #@# - not implemented + return $xml; + } + public static function pure2xml($pure) + { + return self::xmlencode($pure); + } + + protected static function xmlentities($string) + { + return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); + } + + /** + * xmlencode() + * Encodes a string so that for sure it can be output as an xml data string + */ + protected static function xmlencode($string) + { + return self::xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); + } + + public static function islowcase($sString) + { + return (strtolower($sString) == $sString); + } +} + +?> diff --git a/core/action.class.inc.php b/core/action.class.inc.php new file mode 100644 index 0000000000..7dfa724d90 --- /dev/null +++ b/core/action.class.inc.php @@ -0,0 +1,349 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +require_once(APPROOT.'/core/email.class.inc.php'); + +/** + * A user defined action, to customize the application + * + * @package iTopORM + */ +abstract class Action extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_action", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum(array('test'=>'Being tested' ,'enabled'=>'In production', 'disabled'=>'Inactive')), "sql"=>"status", "default_value"=>"test", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("trigger_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"action_id", "ext_key_to_remote"=>"trigger_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // 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 + } + + abstract public function DoExecute($oTrigger, $aContextArgs); + + public function IsActive() + { + switch($this->Get('status')) + { + case 'enabled': + case 'test': + return true; + + default: + return false; + } + } + + public function IsBeingTested() + { + switch($this->Get('status')) + { + case 'test': + return true; + + default: + return false; + } + } +} + +/** + * A notification + * + * @package iTopORM + */ +abstract class ActionNotification extends Action +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_action_notification", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // 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 + } +} + +/** + * An email notification + * + * @package iTopORM + */ +class ActionEmail extends ActionNotification +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_action_email", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeEmailAddress("test_recipient", array("allowed_values"=>null, "sql"=>"test_recipient", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("reply_to", array("allowed_values"=>null, "sql"=>"reply_to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTemplateText("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values"=>new ValueSetEnum('low,normal,high'), "sql"=>"importance", "default_value"=>'normal', "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // 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 + } + + // count the recipients found + protected $m_iRecipients; + + // Errors management : not that simple because we need that function to be + // executed in the background, while making sure that any issue would be reported clearly + protected $m_aMailErrors; //array of strings explaining the issue + + // returns a the list of emails as a string, or a detailed error description + protected function FindRecipients($sRecipAttCode, $aArgs) + { + $sOQL = $this->Get($sRecipAttCode); + if (strlen($sOQL) == '') return ''; + + try + { + $oSearch = DBObjectSearch::FromOQL($sOQL); + } + catch (OQLException $e) + { + $this->m_aMailErrors[] = "query syntax error for recipient '$sRecipAttCode'"; + return $e->getMessage(); + } + + $sClass = $oSearch->GetClass(); + // Determine the email attribute (the first one will be our choice) + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeEmailAddress) + { + $sEmailAttCode = $sAttCode; + // we've got one, exit the loop + break; + } + } + if (!isset($sEmailAttCode)) + { + $this->m_aMailErrors[] = "wrong target for recipient '$sRecipAttCode'"; + return "The objects of the class '$sClass' do not have any email attribute"; + } + + $oSet = new DBObjectSet($oSearch, array() /* order */, $aArgs); + $aRecipients = array(); + while ($oObj = $oSet->Fetch()) + { + $aRecipients[] = $oObj->Get($sEmailAttCode); + $this->m_iRecipients++; + } + return implode(', ', $aRecipients); + } + + + public function DoExecute($oTrigger, $aContextArgs) + { + $this->m_iRecipients = 0; + $this->m_aMailErrors = array(); + $bRes = false; // until we do succeed in sending the email + try + { + // Determine recicipients + // + $sTo = $this->FindRecipients('to', $aContextArgs); + $sCC = $this->FindRecipients('cc', $aContextArgs); + $sBCC = $this->FindRecipients('bcc', $aContextArgs); + + $sFrom = $this->Get('from'); + $sReplyTo = $this->Get('reply_to'); + + $sSubject = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs); + $sBody = MetaModel::ApplyParams($this->Get('body'), $aContextArgs); + + $oObj = $aContextArgs['this->object()']; + $sServerIP = $_SERVER['SERVER_ADDR']; //gethostbyname(gethostname()); + $sReference = 'GetKey().'@'.$sServerIP.'>'; + + $oEmail = new EMail(); + + if ($this->IsBeingTested()) + { + $oEmail->SetSubject('TEST['.$sSubject.']'); + $sTestBody = $sBody; + $sTestBody .= "
\n"; + $sTestBody .= "

Testing email notification ".$this->GetHyperlink()."

\n"; + $sTestBody .= "

The email should be sent with the following properties\n"; + $sTestBody .= "

    \n"; + $sTestBody .= "
  • TO: $sTo
  • \n"; + $sTestBody .= "
  • CC: $sCC
  • \n"; + $sTestBody .= "
  • BCC: $sBCC
  • \n"; + $sTestBody .= "
  • From: $sFrom
  • \n"; + $sTestBody .= "
  • Reply-To: $sReplyTo
  • \n"; + $sTestBody .= "
  • References: $sReference
  • \n"; + $sTestBody .= "
\n"; + $sTestBody .= "

\n"; + $sTestBody .= "
\n"; + $oEmail->SetBody($sTestBody); + $oEmail->SetRecipientTO($this->Get('test_recipient')); + $oEmail->SetRecipientFrom($this->Get('test_recipient')); + $oEmail->SetReferences($sReference); + } + else + { + $oEmail->SetSubject($sSubject); + $oEmail->SetBody($sBody); + $oEmail->SetRecipientTO($sTo); + $oEmail->SetRecipientCC($sCC); + $oEmail->SetRecipientBCC($sBCC); + $oEmail->SetRecipientFrom($sFrom); + $oEmail->SetRecipientReplyTo($sReplyTo); + $oEmail->SetReferences($sReference); + } + + if (empty($this->m_aMailErrors)) + { + if ($this->m_iRecipients == 0) + { + $this->m_aMailErrors[] = 'No recipient'; + } + else + { + $oKPI = new ExecutionKPI(); + $this->m_aMailErrors = array_merge($this->m_aMailErrors, $oEmail->Send()); + $oKPI->ComputeStats('Send mail', $sTo); + } + } + } + catch (Exception $e) + { + $this->m_aMailErrors[] = $e->getMessage(); + } + + if (MetaModel::IsLogEnabledNotification()) + { + $oLog = new EventNotificationEmail(); + if (empty($this->m_aMailErrors)) + { + if ($this->IsBeingTested()) + { + $oLog->Set('message', 'TEST - Notification sent ('.$this->Get('test_recipient').')'); + } + else + { + $oLog->Set('message', 'Notification sent'); + } + } + else + { + if (is_array($this->m_aMailErrors) && count($this->m_aMailErrors) > 0) + { + $sError = implode(', ', $this->m_aMailErrors); + } + else + { + $sError = 'Unknown reason'; + } + if ($this->IsBeingTested()) + { + $oLog->Set('message', 'TEST - Notification was not sent: '.$sError); + } + else + { + $oLog->Set('message', 'Notification was not sent: '.$sError); + } + } + $oLog->Set('userinfo', UserRights::GetUser()); + $oLog->Set('trigger_id', $oTrigger->GetKey()); + $oLog->Set('action_id', $this->GetKey()); + $oLog->Set('object_id', $aContextArgs['this->object()']->GetKey()); + + // Note: we have to secure this because those values are calculated + // inside the try statement, and we would like to keep track of as + // many data as we could while some variables may still be undefined + if (isset($sTo)) $oLog->Set('to', $sTo); + if (isset($sCC)) $oLog->Set('cc', $sCC); + if (isset($sBCC)) $oLog->Set('bcc', $sBCC); + if (isset($sFrom)) $oLog->Set('from', $sFrom); + if (isset($sSubject)) $oLog->Set('subject', $sSubject); + if (isset($sBody)) $oLog->Set('body', $sBody); + $oLog->DBInsertNoReload(); + } + } +} +?> \ No newline at end of file diff --git a/core/archive.class.inc.php b/core/archive.class.inc.php new file mode 100644 index 0000000000..bb5f3b150a --- /dev/null +++ b/core/archive.class.inc.php @@ -0,0 +1,334 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * iTopArchive a class to manipulate (read/write) iTop archives with their catalog + * Each iTop archive is a zip file that contains (at the root of the archive) + * a file called catalog.xml holding the description of the archive + */ +class iTopArchive +{ + const read = 0; + const create = ZipArchive::CREATE; + + protected $m_sZipPath; + protected $m_oZip; + protected $m_sVersion; + protected $m_sTitle; + protected $m_sDescription; + protected $m_aPackages; + protected $m_aErrorMessages; + + /** + * Construct an iTopArchive object + * @param $sArchivePath string The full path the archive file + * @param $iMode integrer Either iTopArchive::read for reading an existing archive or iTopArchive::create for creating a new one. Updating is not supported (yet) + */ + public function __construct($sArchivePath, $iMode = iTopArchive::read) + { + $this->m_sZipPath = $sArchivePath; + $this->m_oZip = new ZipArchive(); + $this->m_oZip->open($this->m_sZipPath, $iMode); + $this->m_aErrorMessages = array(); + $this->m_sVersion = '1.0'; + $this->m_sTitle = ''; + $this->m_sDescription = ''; + $this->m_aPackages = array(); + } + + public function SetTitle($sTitle) + { + $this->m_sTitle = $sTitle; + } + + public function SetDescription($sDescription) + { + $this->m_sDescription = $sDescription; + } + + public function GetTitle() + { + return $this->m_sTitle; + } + + public function GetDescription() + { + return $this->m_sDescription; + } + + public function GetPackages() + { + return $this->m_aPackages; + } + + public function __destruct() + { + $this->m_oZip->close(); + } + + /** + * Get the error message explaining the latest error encountered + * @return array All the error messages encountered during the validation + */ + public function GetErrors() + { + return $this->m_aErrorMessages; + } + + /** + * Read the catalog from the archive (zip) file + * @param sPath string Path the the zip file + * @return boolean True in case of success, false otherwise + */ + public function ReadCatalog() + { + if ($this->IsValid()) + { + $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); + $oParser = xml_parser_create(); + xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); + xml_parser_free($oParser); + + $iIndex = $aIndexes['ARCHIVE'][0]; + $this->m_sVersion = $aValues[$iIndex]['attributes']['VERSION']; + $iIndex = $aIndexes['TITLE'][0]; + $this->m_sTitle = $aValues[$iIndex]['value']; + $iIndex = $aIndexes['DESCRIPTION'][0]; + if (array_key_exists('value', $aValues[$iIndex])) + { + // #@# implement a get_array_value(array, key, default) ? + $this->m_sDescription = $aValues[$iIndex]['value']; + } + + foreach($aIndexes['PACKAGE'] as $iIndex) + { + $this->m_aPackages[$aValues[$iIndex]['attributes']['HREF']] = array( 'type' => $aValues[$iIndex]['attributes']['TYPE'], 'title'=> $aValues[$iIndex]['attributes']['TITLE'], 'description' => $aValues[$iIndex]['value']); + } + + //echo "Archive path: {$this->m_sZipPath}
\n"; + //echo "Archive format version: {$this->m_sVersion}
\n"; + //echo "Title: {$this->m_sTitle}
\n"; + //echo "Description: {$this->m_sDescription}
\n"; + //foreach($this->m_aPackages as $aFile) + //{ + // echo "{$aFile['title']} ({$aFile['type']}): {$aFile['description']}
\n"; + //} + } + return true; + } + + public function WriteCatalog() + { + $sXml = "\n"; // split the XML closing tag that disturbs PSPad's syntax coloring + $sXml .= "\n"; + $sXml .= "{$this->m_sTitle}\n"; + $sXml .= "{$this->m_sDescription}\n"; + foreach($this->m_aPackages as $sFileName => $aFile) + { + $sXml .= "{$aFile['description']}\n"; + } + $sXml .= ""; + $this->m_oZip->addFromString('catalog.xml', $sXml); + } + + /** + * Add a package to the archive + * @param string $sExternalFilePath The path to the file to be added to the archive as a package (directories are not yet implemented) + * @param string $sFilePath The name of the file inside the archive + * @param string $sTitle A short title for this package + * @param string $sType Type of the package. SQL scripts must be of type 'text/sql' + * @param string $sDescription A longer description of the purpose of this package + * @return none + */ + public function AddPackage($sExternalFilePath, $sFilePath, $sTitle, $sType, $sDescription) + { + $this->m_aPackages[$sFilePath] = array('title' => $sTitle, 'type' => $sType, 'description' => $sDescription); + $this->m_oZip->addFile($sExternalFilePath, $sFilePath); + } + + /** + * Reads the contents of the given file from the archive + * @param string $sFileName The path to the file inside the archive + * @return string The content of the file read from the archive + */ + public function GetFileContents($sFileName) + { + return $this->m_oZip->getFromName($sFileName); + } + + /** + * Extracts the contents of the given file from the archive + * @param string $sFileName The path to the file inside the archive + * @param string $sDestinationFileName The path of the file to write + * @return none + */ + public function ExtractToFile($sFileName, $sDestinationFileName) + { + $iBufferSize = 64 * 1024; // Read 64K at a time + $oZipStream = $this->m_oZip->getStream($sFileName); + $oDestinationStream = fopen($sDestinationFileName, 'wb'); + while (!feof($oZipStream)) { + $sContents = fread($oZipStream, $iBufferSize); + fwrite($oDestinationStream, $sContents); + } + fclose($oZipStream); + fclose($oDestinationStream); + } + + /** + * Apply a SQL script taken from the archive. The package must be listed in the catalog and of type text/sql + * @param string $sFileName The path to the SQL package inside the archive + * @return boolean false in case of error, true otherwise + */ + public function ImportSql($sFileName, $sDatabase = 'itop') + { + if ( ($this->m_oZip->locateName($sFileName) == false) || (!isset($this->m_aPackages[$sFileName])) || ($this->m_aPackages[$sFileName]['type'] != 'text/sql')) + { + // invalid type or not listed in the catalog + return false; + } + $sTempName = tempnam("../tmp/", "sql"); + //echo "Extracting to: '$sTempName'
\n"; + $this->ExtractToFile($sFileName, $sTempName); + // Note: the command line below works on Windows with the right path to mysql !!! + $sCommandLine = 'type "'.$sTempName.'" | "/iTop/MySQL Server 5.0/bin/mysql.exe" -u root '.$sDatabase; + //echo "Executing: '$sCommandLine'
\n"; + exec($sCommandLine, $aOutput, $iRet); + //echo "Return code: $iRet
\n"; + //echo "Output:
\n";
+		//print_r($aOutput);
+		//echo "

\n"; + unlink($sTempName); + return ($iRet == 0); + } + + /** + * Dumps some part of the specified MySQL database into the archive as a text/sql package + * @param $sTitle string A short title for this SQL script + * @param $sDescription string A longer description of the purpose of this SQL script + * @param $sFileName string The name of the package inside the archive + * @param $sDatabase string name of the database + * @param $aTables array array or table names. If empty, all tables are dumped + * @param $bStructureOnly boolean Whether or not to dump the data or just the schema + * @return boolean False in case of error, true otherwise + */ + public function AddDatabaseDump($sTitle, $sDescription, $sFileName, $sDatabase = 'itop', $aTables = array(), $bStructureOnly = true) + { + $sTempName = tempnam("../tmp/", "sql"); + $sNoData = $bStructureOnly ? "--no-data" : ""; + $sCommandLine = "\"/iTop/MySQL Server 5.0/bin/mysqldump.exe\" --user=root --opt $sNoData --result-file=$sTempName $sDatabase ".implode(" ", $aTables); + //echo "Executing command: '$sCommandLine'
\n"; + exec($sCommandLine, $aOutput, $iRet); + //echo "Return code: $iRet
\n"; + //echo "Output:
\n";
+		//print_r($aOutput);
+		//echo "

\n"; + if ($iRet == 0) + { + $this->AddPackage($sTempName, $sFileName, $sTitle, 'text/sql', $sDescription); + } + //unlink($sTempName); + return ($iRet == 0); + } + + /** + * Check the consistency of the archive + * @return boolean True if the archive file is consistent + */ + public function IsValid() + { + // TO DO: use a DTD to validate the XML instead of this hand-made validation + $bResult = true; + $aMandatoryTags = array('ARCHIVE' => array('VERSION'), + 'TITLE' => array(), + 'DESCRIPTION' => array(), + 'PACKAGE' => array('TYPE', 'HREF', 'TITLE')); + + $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); + $oParser = xml_parser_create(); + xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); + xml_parser_free($oParser); + + foreach($aMandatoryTags as $sTag => $aAttributes) + { + // Check that all the required tags are present + if (!isset($aIndexes[$sTag])) + { + $this->m_aErrorMessages[] = "The XML catalog does not contain the mandatory tag $sTag."; + $bResult = false; + } + else + { + foreach($aIndexes[$sTag] as $iIndex) + { + switch($aValues[$iIndex]['type']) + { + case 'complete': + case 'open': + // Check that all the required attributes are present + foreach($aAttributes as $sAttribute) + { + if (!isset($aValues[$iIndex]['attributes'][$sAttribute])) + { + $this->m_aErrorMessages[] = "The tag $sTag ($iIndex) does not contain the required attribute $sAttribute."; + } + } + break; + + default: + // ignore other type of tags: close or cdata + } + } + } + } + return $bResult; + } +} +/* +// Unit test - reading an archive +$sArchivePath = '../tmp/archive.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::read); +$oArchive->ReadCatalog(); +$oArchive->ImportSql('full_backup.sql'); + +// Writing an archive -- + +$sArchivePath = '../tmp/archive2.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); +$oArchive->SetTitle('First Archive !'); +$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); +$oArchive->AddPackage('../tmp/schema.sql', 'test.sql', 'this is just a test', 'text/sql', 'My first attempt at creating an archive from PHP...'); +$oArchive->WriteCatalog(); + + +$sArchivePath = '../tmp/archive2.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); +$oArchive->SetTitle('First Archive !'); +$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); +$oArchive->AddDatabaseDump('Test', 'This is my first automatic dump', 'schema.sql', 'itop', array('objects')); +$oArchive->WriteCatalog(); +*/ +?> diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php new file mode 100644 index 0000000000..401f2f8e17 --- /dev/null +++ b/core/attributedef.class.inc.php @@ -0,0 +1,2380 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +require_once('MyHelpers.class.inc.php'); +require_once('ormdocument.class.inc.php'); +require_once('ormpassword.class.inc.php'); + +/** + * MissingColumnException - sent if an attribute is being created but the column is missing in the row + * + * @package iTopORM + */ +class MissingColumnException extends Exception +{} + +/** + * add some description here... + * + * @package iTopORM + */ +define('EXTKEY_RELATIVE', 1); + +/** + * add some description here... + * + * @package iTopORM + */ +define('EXTKEY_ABSOLUTE', 2); + +/** + * Propagation of the deletion through an external key - ask the user to delete the referencing object + * + * @package iTopORM + */ +define('DEL_MANUAL', 1); + +/** + * Propagation of the deletion through an external key - ask the user to delete the referencing object + * + * @package iTopORM + */ +define('DEL_AUTO', 2); + + +/** + * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) + * + * @package iTopORM + */ +abstract class AttributeDefinition +{ + public function GetType() + { + return Dict::S('Core:'.get_class($this)); + } + public function GetTypeDesc() + { + return Dict::S('Core:'.get_class($this).'+'); + } + + abstract public function GetEditClass(); + + protected $m_sCode; + private $m_aParams = array(); + protected $m_sHostClass = '!undefined!'; + protected function Get($sParamName) {return $this->m_aParams[$sParamName];} + protected function IsParam($sParamName) {return (array_key_exists($sParamName, $this->m_aParams));} + + protected function GetOptional($sParamName, $default) + { + if (array_key_exists($sParamName, $this->m_aParams)) + { + return $this->m_aParams[$sParamName]; + } + else + { + return $default; + } + } + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + public function OverloadParams($aParams) + { + foreach ($aParams as $sParam => $value) + { + if (!array_key_exists($sParam, $this->m_aParams)) + { + throw new CoreException("Unknown attribute definition parameter '$sParam', please select a value in {".implode(", ", array_keys($this->m_aParams))."}"); + } + else + { + $this->m_aParams[$sParam] = $value; + } + } + } + public function SetHostClass($sHostClass) + { + $this->m_sHostClass = $sHostClass; + } + public function GetHostClass() + { + return $this->m_sHostClass; + } + + // Note: I could factorize this code with the parameter management made for the AttributeDef class + // to be overloaded + static protected function ListExpectedParams() + { + return array(); + } + + private function ConsistencyCheck() + { + + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } + + // table, key field, name field + public function ListDBJoins() + { + return ""; + // e.g: return array("Site", "infrid", "name"); + } + public function IsDirectField() {return false;} + public function IsScalar() {return false;} + public function IsLinkSet() {return false;} + public function IsExternalKey($iType = EXTKEY_RELATIVE) {return false;} + public function IsExternalField() {return false;} + public function IsWritable() {return false;} + public function IsNullAllowed() {return true;} + public function GetCode() {return $this->m_sCode;} + public function GetLabel() {return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode, $this->m_sCode);} + public function GetLabel_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('label', $this->m_aParams)) + { + return $this->m_aParams['label']; + } + else + { + return $this->GetLabel(); + } + } + public function GetDescription() {return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'+', '');} + public function GetHelpOnEdition() {return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'?', '');} + public function GetDescription_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('description', $this->m_aParams)) + { + return $this->m_aParams['description']; + } + else + { + return $this->GetDescription(); + } + } + public function GetValuesDef() {return null;} + public function GetPrerequisiteAttributes() {return array();} + + public function GetNullValue() {return null;} + public function IsNull($proposedValue) {return is_null($proposedValue);} + + public function MakeRealValue($proposedValue) {return $proposedValue;} // force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!) + + public function GetSQLExpressions() {return array();} // returns suffix/expression pairs (1 in most of the cases), for READING (Select) + public function FromSQLToValue($aCols, $sPrefix = '') {return null;} // returns a value out of suffix/value pairs, for SELECT result interpretation + public function GetSQLColumns() {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) + public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update) + public function RequiresIndex() {return false;} + + public function GetValidationPattern() + { + return ''; + } + + public function CheckFormat($value) + { + return true; + } + + public function GetMaxSize() + { + return null; + } + + public function MakeValue() + { + $sComputeFunc = $this->Get("compute_func"); + if (empty($sComputeFunc)) return null; + + return call_user_func($sComputeFunc); + } + + abstract public function GetDefaultValue(); + + // + // To be overloaded in subclasses + // + + abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description" + abstract public function GetBasicFilterLooseOperator(); // returns an "opCode" + //abstract protected GetBasicFilterHTMLInput(); + abstract public function GetBasicFilterSQLExpr($sOpCode, $value); + + public function GetFilterDefinitions() + { + return array(); + } + + public function GetEditValue($sValue) + { + return (string)$sValue; + } + + public function GetAsHTML($sValue) + { + return Str::pure2html((string)$sValue); + } + + public function GetAsXML($sValue) + { + return Str::pure2xml((string)$sValue); + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + return (string)$sValue; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oValSetDef = $this->GetValuesDef(); + if (!$oValSetDef) return null; + return $oValSetDef->GetValues($aArgs, $sContains); + } + + /** + * Parses a string to find some smart search patterns and build the corresponding search/OQL condition + * Each derived class is reponsible for defining and processing their own smart patterns, the base class + * does nothing special, and just calls the default (loose) operator + * @param string $sSearchText The search string to analyze for smart patterns + * @param FieldExpression The FieldExpression representing the atttribute code in this OQL query + * @param Hash $aParams Values of the query parameters + * @return Expression The search condition to be added (AND) to the current search + */ + public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) + { + $sParamName = $oField->GetParent().'_'.$oField->GetName(); + $oRightExpr = new VariableExpression($sParamName); + $sOperator = $this->GetBasicFilterLooseOperator(); + switch ($sOperator) + { + case 'Contains': + $aParams[$sParamName] = "%$sSearchText%"; + $sSQLOperator = 'LIKE'; + break; + + default: + $sSQLOperator = $sOperator; + $aParams[$sParamName] = $sSearchText; + } + $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); + return $oNewCondition; + } +} + +/** + * Set of objects directly linked to an object, and being part of its definition + * + * @package iTopORM + */ +class AttributeLinkedSet extends AttributeDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max")); + } + + public function GetEditClass() {return "List";} + + public function IsWritable() {return true;} + public function IsLinkSet() {return true;} + public function IsIndirect() {return false;} + + public function GetValuesDef() {return $this->Get("allowed_values");} + public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} + public function GetDefaultValue($aArgs = array()) + { + // Note: so far, this feature is a prototype, + // later, the argument 'this' should always be present in the arguments + // + if (($this->IsParam('default_value')) && array_key_exists('this', $aArgs)) + { + $aValues = $this->Get('default_value')->GetValues($aArgs); + $oSet = DBObjectSet::FromArray($this->Get('linked_class'), $aValues); + return $oSet; + } + else + { + return DBObjectSet::FromScratch($this->Get('linked_class')); + } + } + + public function GetLinkedClass() {return $this->Get('linked_class');} + public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');} + + public function GetBasicFilterOperators() {return array();} + public function GetBasicFilterLooseOperator() {return '';} + public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} + + public function GetAsHTML($sValue) + { + return "ERROR: LIST OF OBJECTS"; + } + + public function GetAsXML($sValue) + { + return "ERROR: LIST OF OBJECTS"; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + return "ERROR: LIST OF OBJECTS"; + } + public function DuplicatesAllowed() {return false;} // No duplicates for 1:n links, never +} + +/** + * Set of objects linked to an object (n-n), and being part of its definition + * + * @package iTopORM + */ +class AttributeLinkedSetIndirect extends AttributeLinkedSet +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote")); + } + public function IsIndirect() {return true;} + public function GetExtKeyToRemote() { return $this->Get('ext_key_to_remote'); } + public function GetEditClass() {return "LinkedSet";} + public function DuplicatesAllowed() {return $this->GetOptional("duplicates", false);} // The same object may be linked several times... or not... +} + +/** + * Abstract class implementing default filters for a DB column + * + * @package iTopORM + */ +class AttributeDBFieldVoid extends AttributeDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql")); + } + + // To be overriden, used in GetSQLColumns + protected function GetSQLCol() {return "VARCHAR(255)";} + + public function GetEditClass() {return "String";} + + public function GetValuesDef() {return $this->Get("allowed_values");} + public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} + + public function IsDirectField() {return true;} + public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetSQLExpr() {return $this->Get("sql");} + public function GetDefaultValue() {return $this->MakeRealValue("");} + public function IsNullAllowed() {return false;} + + // + protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) + + public function GetSQLExpressions() + { + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determine by the existence of one column with an empty suffix + $aColumns[''] = $this->Get("sql"); + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + $value = $this->MakeRealValue($aCols[$sPrefix.'']); + return $value; + } + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); + return $aValues; + } + + public function GetSQLColumns() + { + $aColumns = array(); + $aColumns[$this->Get("sql")] = $this->GetSQLCol(); + return $aColumns; + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + return array("="=>"equals", "!="=>"differs from"); + } + public function GetBasicFilterLooseOperator() + { + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '=': + default: + return $this->GetSQLExpr()." = $sQValue"; + } + } +} + +/** + * Base class for all kind of DB attributes, with the exception of external keys + * + * @package iTopORM + */ +class AttributeDBField extends AttributeDBFieldVoid +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); + } + public function GetDefaultValue() {return $this->MakeRealValue($this->Get("default_value"));} + public function IsNullAllowed() {return $this->Get("is_null_allowed");} +} + +/** + * Map an integer column to an attribute + * + * @package iTopORM + */ +class AttributeInteger extends AttributeDBField +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol() {return "INT(11)";} + + public function GetValidationPattern() + { + return "^[0-9]+$"; + } + + public function GetBasicFilterOperators() + { + return array( + "!="=>"differs from", + "="=>"equals", + ">"=>"greater (strict) than", + ">="=>"greater than", + "<"=>"less (strict) than", + "<="=>"less than", + "in"=>"in" + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement an "equals approximately..." or "same order of magnitude" + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '>': + return $this->GetSQLExpr()." > $sQValue"; + break; + case '>=': + return $this->GetSQLExpr()." >= $sQValue"; + break; + case '<': + return $this->GetSQLExpr()." < $sQValue"; + break; + case '<=': + return $this->GetSQLExpr()." <= $sQValue"; + break; + case 'in': + if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); + return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; + break; + + case '=': + default: + return $this->GetSQLExpr()." = \"$value\""; + } + } + + public function GetNullValue() + { + return null; + } + public function IsNull($proposedValue) + { + return is_null($proposedValue); + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return null; + if ($proposedValue == '') return null; + return (int)$proposedValue; + } + + public function ScalarToSQL($value) + { + assert(is_numeric($value) || is_null($value)); + return $value; // supposed to be an int + } +} + +/** + * Map a decimal value column (suitable for financial computations) to an attribute + * internally in PHP such numbers are represented as string. Should you want to perform + * a calculation on them, it is recommended to use the BC Math functions in order to + * retain the precision + * + * @package iTopORM + */ +class AttributeDecimal extends AttributeDBField +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol() {return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")";} + + public function GetValidationPattern() + { + $iNbDigits = $this->Get('digits'); + $iPrecision = $this->Get('decimals'); + $iNbIntegerDigits = $iNbDigits - $iPrecision - 1; // -1 because the first digit is treated separately in the pattern below + return "^[-+]?[0-9]\d{0,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$"; + } + + public function GetBasicFilterOperators() + { + return array( + "!="=>"differs from", + "="=>"equals", + ">"=>"greater (strict) than", + ">="=>"greater than", + "<"=>"less (strict) than", + "<="=>"less than", + "in"=>"in" + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement an "equals approximately..." or "same order of magnitude" + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '>': + return $this->GetSQLExpr()." > $sQValue"; + break; + case '>=': + return $this->GetSQLExpr()." >= $sQValue"; + break; + case '<': + return $this->GetSQLExpr()." < $sQValue"; + break; + case '<=': + return $this->GetSQLExpr()." <= $sQValue"; + break; + case 'in': + if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); + return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; + break; + + case '=': + default: + return $this->GetSQLExpr()." = \"$value\""; + } + } + + public function GetNullValue() + { + return null; + } + public function IsNull($proposedValue) + { + return is_null($proposedValue); + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return null; + if ($proposedValue == '') return null; + return (string)$proposedValue; + } + + public function ScalarToSQL($value) + { + assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value)); + return (string)$value; // treated as a string + } +} + +/** + * Map a boolean column to an attribute + * + * @package iTopORM + */ +class AttributeBoolean extends AttributeInteger +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Integer";} + protected function GetSQLCol() {return "TINYINT(1)";} + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return null; + if ($proposedValue == '') return null; + if ((int)$proposedValue) return true; + return false; + } + + public function ScalarToSQL($value) + { + if ($value) return 1; + return 0; + } +} + +/** + * Map a varchar column (size < ?) to an attribute + * + * @package iTopORM + */ +class AttributeString extends AttributeDBField +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol() {return "VARCHAR(255)";} + + public function CheckFormat($value) + { + $sRegExp = $this->GetValidationPattern(); + if (empty($sRegExp)) + { + return true; + } + else + { + $sRegExp = str_replace('/', '\\/', $sRegExp); + return preg_match("/$sRegExp/", $value); + } + } + + public function GetMaxSize() + { + return 255; + } + + public function GetBasicFilterOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "Like"=>"equals (no case)", + "NotLike"=>"differs from (no case)", + "Contains"=>"contains", + "Begins with"=>"begins with", + "Finishes with"=>"finishes with" + ); + } + public function GetBasicFilterLooseOperator() + { + return "Contains"; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '=': + case '!=': + return $this->GetSQLExpr()." $sOpCode $sQValue"; + case 'Begins with': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%"); + case 'Finishes with': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value"); + case 'Contains': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); + case 'NotLike': + return $this->GetSQLExpr()." NOT LIKE $sQValue"; + case 'Like': + default: + return $this->GetSQLExpr()." LIKE $sQValue"; + } + } + + public function GetNullValue() + { + return ''; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == ''); + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return ''; + return (string)$proposedValue; + } + + public function ScalarToSQL($value) + { + if (!is_string($value) && !is_null($value)) + { + throw new CoreWarning('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetCode(), 'attribute' => $this->GetHostClass())); + } + return $value; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return '"'.$sEscaped.'"'; + } +} + +/** + * An attibute that matches an object class + * + * @package iTopORM + */ +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); + } + + public function GetDefaultValue() + { + $sDefault = parent::GetDefaultValue(); + if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) + { + // For this kind of attribute specifying null as default value + // is authorized even if null is not allowed + + // Pick the first one... + $aClasses = $this->GetAllowedValues(); + $sDefault = key($aClasses); + } + return $sDefault; + } + + public function GetAsHTML($sValue) + { + if (empty($sValue)) return ''; + return MetaModel::GetName($sValue); + } + + public function RequiresIndex() + { + return true; + } +} + +/** + * An attibute that matches one of the language codes availables in the dictionnary + * + * @package iTopORM + */ +class AttributeApplicationLanguage extends AttributeString +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + } + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $aAvailableLanguages = Dict::GetLanguages(); + $aLanguageCodes = array(); + foreach($aAvailableLanguages as $sLangCode => $aInfo) + { + $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')'; + } + $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes); + parent::__construct($sCode, $aParams); + } + + public function RequiresIndex() + { + return true; + } +} + +/** + * The attribute dedicated to the finalclass automatic attribute + * + * @package iTopORM + */ +class AttributeFinalClass extends AttributeString +{ + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $aParams["allowed_values"] = null; + parent::__construct($sCode, $aParams); + + $this->m_sValue = $this->Get("default_value"); + } + + public function IsWritable() + { + return false; + } + + public function RequiresIndex() + { + return true; + } + + public function SetFixedValue($sValue) + { + $this->m_sValue = $sValue; + } + public function GetDefaultValue() + { + return $this->m_sValue; + } + + public function GetAsHTML($sValue) + { + if (empty($sValue)) return ''; + return MetaModel::GetName($sValue); + } +} + +/** + * Map a varchar column (size < ?) to an attribute that must never be shown to the user + * + * @package iTopORM + */ +class AttributePassword extends AttributeString +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Password";} + protected function GetSQLCol() {return "VARCHAR(64)";} + + public function GetMaxSize() + { + return 64; + } + + public function GetFilterDefinitions() + { + // Note: due to this, you will get an error if a password is being declared as a search criteria (see ZLists) + // not allowed to search on passwords! + return array(); + } + + public function GetAsHTML($sValue) + { + if (strlen($sValue) == 0) + { + return ''; + } + else + { + return '******'; + } + } +} + +/** + * Map a text column (size < 255) to an attribute that is encrypted in the database + * The encryption is based on a key set per iTop instance. Thus if you export your + * database (in SQL) to someone else without providing the key at the same time + * the encrypted fields will remain encrypted + * + * @package iTopORM + */ +class AttributeEncryptedString extends AttributeString +{ + static $sKey = null; // Encryption key used for all encrypted fields + + public function __construct($sCode, $aParams) + { + parent::__construct($sCode, $aParams); + if (self::$sKey == null) + { + self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); + } + } + + protected function GetSQLCol() {return "TINYBLOB";} + + public function GetMaxSize() + { + return 255; + } + + public function GetFilterDefinitions() + { + // Note: due to this, you will get an error if a an encrypted field is declared as a search criteria (see ZLists) + // not allowed to search on encrypted fields ! + return array(); + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return null; + return (string)$proposedValue; + } + + /** + * Decrypt the value when reading from the database + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + $oSimpleCrypt = new SimpleCrypt(); + $sValue = $oSimpleCrypt->Decrypt(self::$sKey, $aCols[$sPrefix]); + return $sValue; + } + + /** + * Encrypt the value before storing it in the database + */ + public function GetSQLValues($value) + { + $oSimpleCrypt = new SimpleCrypt(); + $encryptedValue = $oSimpleCrypt->Encrypt(self::$sKey, $value); + + $aValues = array(); + $aValues[$this->Get("sql")] = $encryptedValue; + return $aValues; + } +} + +/** + * Map a text column (size > ?) to an attribute + * + * @package iTopORM + */ +class AttributeText extends AttributeString +{ + public function GetEditClass() {return "Text";} + protected function GetSQLCol() {return "TEXT";} + + public function GetMaxSize() + { + // Is there a way to know the current limitation for mysql? + // See mysql_field_len() + return 65535; + } + + public function GetAsHTML($sValue) + { + return str_replace("\n", "
\n", parent::GetAsHTML($sValue)); + } + + public function GetAsXML($value) + { + return Str::pure2xml($value); + } +} + +/** + * Map a text column (size > ?), containing HTML code, to an attribute + * + * @package iTopORM + */ +class AttributeHTML extends AttributeText +{ + public function GetEditClass() {return "HTML";} + + public function GetAsHTML($sValue) + { + return $sValue; + } +} + +/** + * Specialization of a string: email + * + * @package iTopORM + */ +class AttributeEmailAddress extends AttributeString +{ + public function GetValidationPattern() + { + // return "^([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})$"; + return "^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$"; + } + + public function GetAsHTML($sValue) + { + if (empty($sValue)) return ''; + return '
'.parent::GetAsHTML($sValue).''; + } +} + +/** + * Specialization of a string: IP address + * + * @package iTopORM + */ +class AttributeIPAddress extends AttributeString +{ + public function GetValidationPattern() + { + $sNum = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])'; + return "^($sNum\\.$sNum\\.$sNum\\.$sNum)$"; + } +} + +/** + * Specialization of a string: OQL expression + * + * @package iTopORM + */ +class AttributeOQL extends AttributeText +{ +} + +/** + * Specialization of a string: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateString extends AttributeString +{ +} + +/** + * Specialization of a text: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateText extends AttributeText +{ +} + +/** + * Specialization of a HTML: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateHTML extends AttributeText +{ + public function GetEditClass() {return "HTML";} + + public function GetAsHTML($sValue) + { + return $sValue; + } +} + + +/** + * Specialization of a text: wiki formatting + * + * @package iTopORM + */ +class AttributeWikiText extends AttributeText +{ + public function GetAsHTML($value) + { + // [SELECT xxxx.... [label]] => hyperlink to a result list + // {SELECT xxxx.... [label]} => result list displayed inline + // [myclass/nnn [label]] => hyperlink to an object + // {myclass/nnn/attcode} => attribute displayed inline + // etc. + return parent::GetAsHTML($value); + } +} + +/** + * Map a enum column to an attribute + * + * @package iTopORM + */ +class AttributeEnum extends AttributeString +{ + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol() + { + $oValDef = $this->GetValuesDef(); + if ($oValDef) + { + $aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true); + } + else + { + $aValues = array(); + } + if (count($aValues) > 0) + { + // The syntax used here do matters + // In particular, I had to remove unnecessary spaces to + // make sure that this string will match the field type returned by the DB + // (used to perform a comparison between the current DB format and the data model) + return "ENUM(".implode(",", $aValues).")"; + } + else + { + return "VARCHAR(255)"; // ENUM() is not an allowed syntax! + } + } + + public function ScalarToSQL($value) + { + // Note: for strings, the null value is an empty string and it is recorded as such in the DB + // but that wasn't working for enums, because '' is NOT one of the allowed values + // that's why a null value must be forced to a real null + $value = parent::ScalarToSQL($value); + if ($this->IsNull($value)) + { + return null; + } + else + { + return $value; + } + } + + public function RequiresIndex() + { + return false; + } + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return parent::GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetAsHTML($sValue) + { + if (is_null($sValue)) + { + // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label + $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, Dict::S('Enum:Undefined')); + } + else + { + $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, $sValue); + } + $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', $sValue); + // later, we could imagine a detailed description in the title + return "".parent::GetAsHtml($sLabel).""; + } + + public function GetEditValue($sValue) + { + $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, $sValue); + return $sLabel; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $aRawValues = parent::GetAllowedValues($aArgs, $sContains); + if (is_null($aRawValues)) return null; + $aLocalizedValues = array(); + foreach ($aRawValues as $sKey => $sValue) + { + $aLocalizedValues[$sKey] = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sKey, $sKey); + } + return $aLocalizedValues; + } + + /** + * Processes the input value to align it with the values supported + * by this type of attribute. In this case: turns empty strings into nulls + * @param mixed $proposedValue The value to be set for the attribute + * @return mixed The actual value that will be set + */ + public function MakeRealValue($proposedValue) + { + if ($proposedValue == '') return null; + return parent::MakeRealValue($proposedValue); + } +} + +/** + * Map a date+time column to an attribute + * + * @package iTopORM + */ +class AttributeDateTime extends AttributeDBField +{ + //const MYDATETIMEZONE = "UTC"; + const MYDATETIMEZONE = "Europe/Paris"; + static protected $const_TIMEZONE = null; // set once for all upon object construct + + static public function InitStatics() + { + // Init static constant once for all (remove when PHP allows real static const) + self::$const_TIMEZONE = new DateTimeZone(self::MYDATETIMEZONE); + + // #@# Init default timezone -> do not get a notice... to be improved !!! + // duplicated in the email test page (the mail function does trigger a notice as well) + date_default_timezone_set(self::MYDATETIMEZONE); + } + + static protected function GetDateFormat() + { + return "Y-m-d H:i:s"; + } + + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "DateTime";} + protected function GetSQLCol() {return "TIMESTAMP";} + public static function GetAsUnixSeconds($value) + { + $oDeadlineDateTime = new DateTime($value, self::$const_TIMEZONE); + $iUnixSeconds = $oDeadlineDateTime->format('U'); + return $iUnixSeconds; + + } + + // #@# THIS HAS TO REVISED + // Having null not allowed was interpreted by mySQL + // which was creating the field with the flag 'ON UPDATE CURRENT_TIMESTAMP' + // Then, on each update of the record, the field was modified. + // We will have to specify the default value if we want to restore this option + // In fact, we could also have more verbs dedicated to the DB: + // GetDBDefaultValue()... or GetDBFieldCreationStatement().... + public function IsNullAllowed() {return true;} + public function GetDefaultValue() + { + $default = parent::GetDefaultValue(); + + if (!parent::IsNullAllowed()) + { + if (empty($default)) + { + $default = date(self::GetDateFormat()); + } + } + + return $default; + } + // END OF THE WORKAROUND + /////////////////////////////////////////////////////////////// + + public function GetValidationPattern() + { + return "^([0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30))))( (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])){0,1}|0000-00-00 00:00:00|0000-00-00$"; + } + + public function GetBasicFilterOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "<"=>"before", + "<="=>"before", + ">"=>"after (strictly)", + ">="=>"after", + "SameDay"=>"same day (strip time)", + "SameMonth"=>"same year/month", + "SameYear"=>"same year", + "Today"=>"today", + ">|"=>"after today + N days", + "<|"=>"before today + N days", + "=|"=>"equals today + N days", + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement a "same xxx, depending on given precision" ! + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + + switch ($sOpCode) + { + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + return $this->GetSQLExpr()." $sOpCode $sQValue"; + case 'SameDay': + return "DATE(".$this->GetSQLExpr().") = DATE($sQValue)"; + case 'SameMonth': + return "DATE_FORMAT(".$this->GetSQLExpr().", '%Y-%m') = DATE_FORMAT($sQValue, '%Y-%m')"; + case 'SameYear': + return "MONTH(".$this->GetSQLExpr().") = MONTH($sQValue)"; + case 'Today': + return "DATE(".$this->GetSQLExpr().") = CURRENT_DATE()"; + case '>|': + return "DATE(".$this->GetSQLExpr().") > DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + case '<|': + return "DATE(".$this->GetSQLExpr().") < DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + case '=|': + return "DATE(".$this->GetSQLExpr().") = DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + default: + return $this->GetSQLExpr()." = $sQValue"; + } + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) + { + return null; + } + if (is_string($proposedValue) && ($proposedValue == "") && $this->IsNullAllowed()) + { + return null; + } + if (!is_numeric($proposedValue)) + { + return $proposedValue; + } + + return date(self::GetDateFormat(), $proposedValue); + } + + public function ScalarToSQL($value) + { + if (is_null($value)) + { + return null; + } + elseif (empty($value)) + { + // Make a valid date for MySQL. TO DO: support NULL as a literal value for fields that can be null. + return '0000-00-00 00:00:00'; + } + return $value; + } + + public function GetAsHTML($value) + { + return Str::pure2html($value); + } + + public function GetAsXML($value) + { + return Str::pure2xml($value); + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return '"'.$sEscaped.'"'; + } + + /** + * Parses a string to find some smart search patterns and build the corresponding search/OQL condition + * Each derived class is reponsible for defining and processing their own smart patterns, the base class + * does nothing special, and just calls the default (loose) operator + * @param string $sSearchText The search string to analyze for smart patterns + * @param FieldExpression The FieldExpression representing the atttribute code in this OQL query + * @param Hash $aParams Values of the query parameters + * @return Expression The search condition to be added (AND) to the current search + */ + public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) + { + // Possible smart patterns + $aPatterns = array( + 'between' => array('pattern' => '/^\[(.*),(.*)\]$/', 'operator' => 'n/a'), + 'greater than or equal' => array('pattern' => '/^>=(.*)$/', 'operator' => '>='), + 'greater than' => array('pattern' => '/^>(.*)$/', 'operator' => '>'), + 'less than or equal' => array('pattern' => '/^<=(.*)$/', 'operator' => '<='), + 'less than' => array('pattern' => '/^<(.*)$/', 'operator' => '<'), + ); + + $sPatternFound = ''; + $aMatches = array(); + foreach($aPatterns as $sPatName => $sPattern) + { + if (preg_match($sPattern['pattern'], $sSearchText, $aMatches)) + { + $sPatternFound = $sPatName; + break; + } + } + + switch($sPatternFound) + { + case 'between': + + $sParamName1 = $oField->GetParent().'_'.$oField->GetName().'_1'; + $oRightExpr = new VariableExpression($sParamName1); + $aParams[$sParamName1] = $aMatches[1]; + $oCondition1 = new BinaryExpression($oField, '>=', $oRightExpr); + + $sParamName2 = $oField->GetParent().'_'.$oField->GetName().'_2'; + $oRightExpr = new VariableExpression($sParamName2); + $sOperator = $this->GetBasicFilterLooseOperator(); + $aParams[$sParamName2] = $aMatches[2]; + $oCondition2 = new BinaryExpression($oField, '<=', $oRightExpr); + + $oNewCondition = new BinaryExpression($oCondition1, 'AND', $oCondition2); + break; + + case 'greater than': + case 'greater than or equal': + case 'less than': + case 'less than or equal': + $sSQLOperator = $aPatterns[$sPatternFound]['operator']; + $sParamName = $oField->GetParent().'_'.$oField->GetName(); + $oRightExpr = new VariableExpression($sParamName); + $aParams[$sParamName] = $aMatches[1]; + $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); + + break; + + default: + $oNewCondition = parent::GetSmartConditionExpression($sSearchText, $oField, $aParams); + } + + return $oNewCondition; + } +} + +/** + * Map a date+time column to an attribute + * + * @package iTopORM + */ +class AttributeDate extends AttributeDateTime +{ + const MYDATEFORMAT = "Y-m-d"; + + static protected function GetDateFormat() + { + return "Y-m-d"; + } + + static public function InitStatics() + { + // Nothing to do... + } + + static protected function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Date";} + protected function GetSQLCol() {return "DATE";} + + public function GetValidationPattern() + { + return "^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30)))$"; + } +} + +// Init static constant once for all (remove when PHP allows real static const) +AttributeDate::InitStatics(); + +/** + * A dead line stored as a date & time + * The only difference with the DateTime attribute is the display: + * relative to the current time + */ +class AttributeDeadline extends AttributeDateTime +{ + public function GetAsHTML($value) + { + $sResult = ''; + if ($value !== null) + { + $value = AttributeDateTime::GetAsUnixSeconds($value); + $difference = $value - time(); + + if ($difference >= 0) + { + $sResult = self::FormatDuration($difference); + } + else + { + $sResult = Dict::Format('UI:DeadlineMissedBy_duration', self::FormatDuration(-$difference)); + } + } + return $sResult; + } + + static function FormatDuration($duration) + { + $days = floor($duration / 86400); + $hours = floor(($duration - (86400*$days)) / 3600); + $minutes = floor(($duration - (86400*$days + 3600*$hours)) / 60); + $sResult = ''; + + if ($duration < 60) + { + // Less than 1 min + $sResult =Dict::S('UI:Deadline_LessThan1Min'); + } + else if ($duration < 3600) + { + // less than 1 hour, display it in minutes + $sResult =Dict::Format('UI:Deadline_Minutes', $minutes); + } + else if ($duration < 86400) + { + // Less that 1 day, display it in hours/minutes + $sResult =Dict::Format('UI:Deadline_Hours_Minutes', $hours, $minutes); + } + else + { + // Less that 1 day, display it in hours/minutes + $sResult =Dict::Format('UI:Deadline_Days_Hours_Minutes', $days, $hours, $minutes); + } + return $sResult; + } +} +// Init static constant once for all (remove when PHP allows real static const) +AttributeDateTime::InitStatics(); + + +/** + * Map a foreign key to an attribute + * AttributeExternalKey and AttributeExternalField may be an external key + * the difference is that AttributeExternalKey corresponds to a column into the defined table + * where an AttributeExternalField corresponds to a column into another table (class) + * + * @package iTopORM + */ +class AttributeExternalKey extends AttributeDBFieldVoid +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete")); + } + + public function GetEditClass() {return "ExtKey";} + protected function GetSQLCol() {return "INT(11)";} + public function RequiresIndex() + { + return true; + } + + public function IsExternalKey($iType = EXTKEY_RELATIVE) {return true;} + public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->Get("targetclass");} + public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;} + public function GetKeyAttCode() {return $this->GetCode();} + + + public function GetDefaultValue() {return 0;} + public function IsNullAllowed() {return $this->Get("is_null_allowed");} + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return parent::GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + // overloaded here so that an ext key always have the answer to + // "what are your possible values?" + public function GetValuesDef() + { + $oValSetDef = $this->Get("allowed_values"); + if (!$oValSetDef) + { + // Let's propose every existing value + $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); + } + return $oValSetDef; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + try + { + return parent::GetAllowedValues($aArgs, $sContains); + } + catch (MissingQueryArgument $e) + { + // Some required arguments could not be found, enlarge to any existing value + $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); + return $oValSetDef->GetValues($aArgs, $sContains); + } + } + + public function GetDeletionPropagationOption() + { + return $this->Get("on_target_delete"); + } + + public function GetNullValue() + { + return 0; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == 0); + } + + public function MakeRealValue($proposedValue) + { + if (is_null($proposedValue)) return 0; + if ($proposedValue === '') return 0; + if (MetaModel::IsValidObject($proposedValue)) return $proposedValue->GetKey(); + return (int)$proposedValue; + } + + public function GetMaximumComboLength() + { + return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length')); + } + + public function GetMinAutoCompleteChars() + { + return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars')); + } + + public function AllowTargetCreation() + { + return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation')); + } + +} + +/** + * An attribute which corresponds to an external key (direct or indirect) + * + * @package iTopORM + */ +class AttributeExternalField extends AttributeDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode")); + } + + public function GetEditClass() {return "ExtField";} + protected function GetSQLCol() + { + // throw new CoreException("external attribute: does it make any sense to request its type ?"); + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetSQLCol(); + } + + public function GetLabel() + { + $oRemoteAtt = $this->GetExtAttDef(); + $sDefault = $oRemoteAtt->GetLabel(); + return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode, $sDefault); + } + public function GetDescription() + { + $oRemoteAtt = $this->GetExtAttDef(); + $sDefault = $oRemoteAtt->GetDescription(); + return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'+', $sDefault); + } + public function GetHelpOnEdition() + { + $oRemoteAtt = $this->GetExtAttDef(); + $sDefault = $oRemoteAtt->GetHelpOnEdition(); + return Dict::S('Class:'.$this->m_sHostClass.'/Attribute:'.$this->m_sCode.'?', $sDefault); + } + + public function IsExternalKey($iType = EXTKEY_RELATIVE) + { + switch($iType) + { + case EXTKEY_ABSOLUTE: + // see further + $oRemoteAtt = $this->GetExtAttDef(); + return $oRemoteAtt->IsExternalKey($iType); + + case EXTKEY_RELATIVE: + return false; + + default: + throw new CoreException("Unexpected value for argument iType: '$iType'"); + } + } + + public function GetTargetClass($iType = EXTKEY_RELATIVE) + { + return $this->GetKeyAttDef($iType)->GetTargetClass(); + } + + public function IsExternalField() {return true;} + public function GetKeyAttCode() {return $this->Get("extkey_attcode");} + public function GetExtAttCode() {return $this->Get("target_attcode");} + + public function GetKeyAttDef($iType = EXTKEY_RELATIVE) + { + switch($iType) + { + case EXTKEY_ABSOLUTE: + // see further + $oRemoteAtt = $this->GetExtAttDef(); + if ($oRemoteAtt->IsExternalField()) + { + return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE); + } + else if ($oRemoteAtt->IsExternalKey()) + { + return $oRemoteAtt; + } + return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter ! + + case EXTKEY_RELATIVE: + return MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode")); + + default: + throw new CoreException("Unexpected value for argument iType: '$iType'"); + } + } + + public function GetExtAttDef() + { + $oKeyAttDef = $this->GetKeyAttDef(); + $oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->Get("targetclass"), $this->Get("target_attcode")); + if (!is_object($oExtAttDef)) throw new CoreException("Invalid external field ".$this->GetCode()." in class ".$this->GetHostClass().". The class ".$oKeyAttDef->Get("targetclass")." has no attribute ".$this->Get("target_attcode")); + return $oExtAttDef; + } + + public function GetSQLExpr() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetSQLExpr(); + } + + public function GetDefaultValue() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetDefaultValue(); + } + public function IsNullAllowed() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->IsNullAllowed(); + } + + public function IsScalar() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->IsScalar(); + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetNullValue() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetNullValue(); + } + + public function IsNull($proposedValue) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->IsNull($proposedValue); + } + + public function MakeRealValue($proposedValue) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->MakeRealValue($proposedValue); + } + + public function ScalarToSQL($value) + { + // This one could be used in case of filtering only + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->ScalarToSQL($value); + } + + + // Do not overload GetSQLExpression here because this is handled in the joins + //public function GetSQLExpressions() {return array();} + + // Here, we get the data... + public function FromSQLToValue($aCols, $sPrefix = '') + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->FromSQLToValue($aCols, $sPrefix); + } + + public function GetAsHTML($value) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsHTML($value); + } + public function GetAsXML($value) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsXML($value); + } + public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"') + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier); + } +} + +/** + * Map a varchar column to an URL (formats the ouput in HMTL) + * + * @package iTopORM + */ +class AttributeURL extends AttributeString +{ + static protected function ListExpectedParams() + { + //return parent::ListExpectedParams(); + return array_merge(parent::ListExpectedParams(), array("target")); + } + + public function GetEditClass() {return "String";} + + public function GetAsHTML($sValue) + { + $sTarget = $this->Get("target"); + if (empty($sTarget)) $sTarget = "_blank"; + $sLabel = Str::pure2html($sValue); + if (strlen($sLabel) > 40) + { + // Truncate the length to about 40 characters, by removing the middle + $sLabel = substr($sLabel, 0, 25).'...'.substr($sLabel, -15); + } + return "$sLabel"; + } + + public function GetValidationPattern() + { + return "^(http|https|ftp)\://[a-zA-Z0-9\-\.]+(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9\-\._\?\,\'/\\\+&%\$#\=~])*$"; + } +} + +/** + * A blob is an ormDocument, it is stored as several columns in the database + * + * @package iTopORM + */ +class AttributeBlob extends AttributeDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("depends_on")); + } + + public function GetEditClass() {return "Document";} + + public function IsDirectField() {return true;} + public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetDefaultValue() {return "";} + public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} + + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue) + { + if (!is_object($proposedValue)) + { + return new ormDocument($proposedValue, 'text/plain'); + } + return $proposedValue; + } + + public function GetSQLExpressions() + { + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $this->GetCode().'_mimetype'; + $aColumns['_data'] = $this->GetCode().'_data'; + $aColumns['_filename'] = $this->GetCode().'_filename'; + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + if (!isset($aCols[$sPrefix])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $sMimeType = $aCols[$sPrefix]; + + if (!isset($aCols[$sPrefix.'_data'])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}"); + } + $data = $aCols[$sPrefix.'_data']; + + if (!isset($aCols[$sPrefix.'_filename'])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}"); + } + $sFileName = $aCols[$sPrefix.'_filename']; + + $value = new ormDocument($data, $sMimeType, $sFileName); + return $value; + } + + public function GetSQLValues($value) + { + // #@# Optimization: do not load blobs anytime + // As per mySQL doc, selecting blob columns will prevent mySQL from + // using memory in case a temporary table has to be created + // (temporary tables created on disk) + // We will have to remove the blobs from the list of attributes when doing the select + // then the use of Get() should finalize the load + if ($value instanceOf ormDocument) + { + $aValues = array(); + $aValues[$this->GetCode().'_data'] = $value->GetData(); + $aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType(); + $aValues[$this->GetCode().'_filename'] = $value->GetFileName(); + } + else + { + $aValues = array(); + $aValues[$this->GetCode().'_data'] = ''; + $aValues[$this->GetCode().'_mimetype'] = ''; + $aValues[$this->GetCode().'_filename'] = ''; + } + return $aValues; + } + + public function GetSQLColumns() + { + $aColumns = array(); + $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) + $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'; + $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'; + return $aColumns; + } + + public function GetFilterDefinitions() + { + return array(); + // still not working... see later... + return array( + $this->GetCode().'->filename' => new FilterFromAttribute($this, '_filename'), + $this->GetCode().'_mimetype' => new FilterFromAttribute($this, '_mimetype'), + $this->GetCode().'_mimetype' => new FilterFromAttribute($this, '_mimetype') + ); + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return 'true'; + } + + public function GetAsHTML($value) + { + if (is_object($value)) + { + return $value->GetAsHTML(); + } + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + return ''; // Not exportable in CSV ! + } + + public function GetAsXML($value) + { + return ''; // Not exportable in XML, or as CDATA + some subtags ?? + } +} +/** + * One way encrypted (hashed) password + */ +class AttributeOneWayPassword extends AttributeDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("depends_on")); + } + + public function GetEditClass() {return "One Way Password";} + + public function IsDirectField() {return true;} + public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetDefaultValue() {return "";} + public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} + + // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted) + public function MakeRealValue($proposedValue) + { + $oPassword = $proposedValue; + if (!is_object($oPassword)) + { + $oPassword = new ormPassword('', ''); + $oPassword->SetPassword($proposedValue); + } + return $oPassword; + } + + public function GetSQLExpressions() + { + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $this->GetCode().'_hash'; + $aColumns['_salt'] = $this->GetCode().'_salt'; + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + if (!isset($aCols[$sPrefix])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $hashed = $aCols[$sPrefix]; + + if (!isset($aCols[$sPrefix.'_salt'])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}"); + } + $sSalt = $aCols[$sPrefix.'_salt']; + + $value = new ormPassword($hashed, $sSalt); + return $value; + } + + public function GetSQLValues($value) + { + // #@# Optimization: do not load blobs anytime + // As per mySQL doc, selecting blob columns will prevent mySQL from + // using memory in case a temporary table has to be created + // (temporary tables created on disk) + // We will have to remove the blobs from the list of attributes when doing the select + // then the use of Get() should finalize the load + if ($value instanceOf ormPassword) + { + $aValues = array(); + $aValues[$this->GetCode().'_hash'] = $value->GetHash(); + $aValues[$this->GetCode().'_salt'] = $value->GetSalt(); + } + else + { + $aValues = array(); + $aValues[$this->GetCode().'_hash'] = ''; + $aValues[$this->GetCode().'_salt'] = ''; + } + return $aValues; + } + + public function GetSQLColumns() + { + $aColumns = array(); + $aColumns[$this->GetCode().'_hash'] = 'TINYBLOB'; + $aColumns[$this->GetCode().'_salt'] = 'TINYBLOB'; + return $aColumns; + } + + public function GetFilterDefinitions() + { + return array(); + // still not working... see later... + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return 'true'; + } + + public function GetAsHTML($value) + { + if (is_object($value)) + { + return $value->GetAsHTML(); + } + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"') + { + return ''; // Not exportable in CSV + } + + public function GetAsXML($value) + { + return ''; // Not exportable in XML + } +} + +// Indexed array having two dimensions +class AttributeTable extends AttributeText +{ + public function GetEditClass() {return "Text";} + protected function GetSQLCol() {return "TEXT";} + + public function GetMaxSize() + { + return null; + } + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue) + { + if (!is_array($proposedValue)) + { + return array(0 => array(0 => $proposedValue)); + } + return $proposedValue; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + try + { + $value = @unserialize($aCols[$sPrefix.'']); + if ($value === false) + { + $value = $this->MakeRealValue($aCols[$sPrefix.'']); + } + } + catch(Exception $e) + { + $value = $this->MakeRealValue($aCols[$sPrefix.'']); + } + + return $value; + } + + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = serialize($value); + return $aValues; + } + + public function GetAsHTML($value) + { + if (!is_array($value)) + { + throw new CoreException('Expecting an array', array('found' => get_class($value))); + } + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + $sRes .= ""; + foreach($value as $iRow => $aRawData) + { + $sRes .= ""; + foreach ($aRawData as $iCol => $cell) + { + $sCell = str_replace("\n", "
\n", Str::pure2html((string)$cell)); + $sRes .= ""; + } + $sRes .= ""; + } + $sRes .= ""; + $sRes .= "
$sCell
"; + return $sRes; + } +} + +// The PHP value is a hash array, it is stored as a TEXT column +class AttributePropertySet extends AttributeTable +{ + public function GetEditClass() {return "Text";} + protected function GetSQLCol() {return "TEXT";} + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue) + { + if (!is_array($proposedValue)) + { + return array('?' => (string)$proposedValue); + } + return $proposedValue; + } + + public function GetAsHTML($value) + { + if (!is_array($value)) + { + throw new CoreException('Expecting an array', array('found' => get_class($value))); + } + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + $sRes .= ""; + foreach($value as $sProperty => $sValue) + { + $sRes .= ""; + $sCell = str_replace("\n", "
\n", Str::pure2html((string)$sValue)); + $sRes .= ""; + $sRes .= ""; + } + $sRes .= ""; + $sRes .= "
$sProperty$sCell
"; + return $sRes; + } +} + +?> diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php new file mode 100644 index 0000000000..1b38cef087 --- /dev/null +++ b/core/bulkchange.class.inc.php @@ -0,0 +1,1063 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * BulkChange + * Interpret a given data set and update the DB accordingly (fake mode avail.) + * + * @package iTopORM + */ + +class BulkChangeException extends CoreException +{ +} + +/** + * CellChangeSpec + * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? + * + * @package iTopORM + */ +abstract class CellChangeSpec +{ + protected $m_proposedValue; + protected $m_sOql; // in case of ambiguity + + public function __construct($proposedValue, $sOql = '') + { + $this->m_proposedValue = $proposedValue; + $this->m_sOql = $sOql; + } + + static protected function ValueAsHtml($value) + { + if (MetaModel::IsValidObject($value)) + { + return $value->GetHyperLink(); + } + else + { + return htmlentities($value, ENT_QUOTES, 'UTF-8'); + } + } + + public function GetValue() + { + return $this->m_proposedValue; + } + + public function GetOql() + { + return $this->m_sOql; + } + + abstract public function GetDescription(); +} + + +class CellStatus_Void extends CellChangeSpec +{ + public function GetDescription() + { + return ''; + } +} + +class CellStatus_Modify extends CellChangeSpec +{ + protected $m_previousValue; + + public function __construct($proposedValue, $previousValue) + { + $this->m_previousValue = $previousValue; + parent::__construct($proposedValue); + } + + public function GetDescription() + { + return 'Modified'; + } + + public function GetPreviousValue() + { + return $this->m_previousValue; + } +} + +class CellStatus_Issue extends CellStatus_Modify +{ + protected $m_sReason; + + public function __construct($proposedValue, $previousValue, $sReason) + { + $this->m_sReason = $sReason; + parent::__construct($proposedValue, $previousValue); + } + + public function GetDescription() + { + if (is_null($this->m_proposedValue)) + { + return 'Could not be changed - reason: '.$this->m_sReason; + } + return 'Could not be changed to '.$this->m_proposedValue.' - reason: '.$this->m_sReason; + } +} + +class CellStatus_SearchIssue extends CellStatus_Issue +{ + public function __construct() + { + parent::__construct(null, null, null); + } + + public function GetDescription() + { + return 'No match'; + } +} + +class CellStatus_NullIssue extends CellStatus_Issue +{ + public function __construct() + { + parent::__construct(null, null, null); + } + + public function GetDescription() + { + return 'Missing mandatory value'; + } +} + + +class CellStatus_Ambiguous extends CellStatus_Issue +{ + protected $m_iCount; + + public function __construct($previousValue, $iCount, $sOql) + { + $this->m_iCount = $iCount; + $this->m_sQuery = $sOql; + parent::__construct(null, $previousValue, ''); + } + + public function GetDescription() + { + $sCount = $this->m_iCount; + return "Ambiguous: found $sCount objects"; + } +} + + +/** + * RowStatus + * A series of classes, keeping the information about a given row: could it be changed or not (and why)? + * + * @package iTopORM + */ +abstract class RowStatus +{ + public function __construct() + { + } + + abstract public function GetDescription(); +} + +class RowStatus_NoChange extends RowStatus +{ + public function GetDescription() + { + return "unchanged"; + } +} + +class RowStatus_NewObj extends RowStatus +{ + public function GetDescription() + { + return "created"; + } +} + +class RowStatus_Modify extends RowStatus +{ + protected $m_iChanged; + + public function __construct($iChanged) + { + $this->m_iChanged = $iChanged; + } + + public function GetDescription() + { + return "updated ".$this->m_iChanged." cols"; + } +} + +class RowStatus_Disappeared extends RowStatus_Modify +{ + public function GetDescription() + { + return "disappeared, changed ".$this->m_iChanged." cols"; + } +} + +class RowStatus_Issue extends RowStatus +{ + protected $m_sReason; + + public function __construct($sReason) + { + $this->m_sReason = $sReason; + } + + public function GetDescription() + { + return 'Issue: '.$this->m_sReason; + } +} + + +/** + * BulkChange + * + * @package iTopORM + */ +class BulkChange +{ + protected $m_sClass; + protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string) + // #@# todo: rename the variables to sColIndex + protected $m_aAttList; // attcode => iCol + protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol; + protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey) + protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported + protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined) + + public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null) + { + $this->m_sClass = $sClass; + $this->m_aData = $aData; + $this->m_aAttList = $aAttList; + $this->m_aReconcilKeys = $aReconcilKeys; + $this->m_aExtKeys = $aExtKeys; + $this->m_sSynchroScope = $sSynchroScope; + $this->m_aOnDisappear = $aOnDisappear; + } + + protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) + { + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass()); + foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '='); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + + $oExtObjects = new CMDBObjectSet($oReconFilter); + $aKeys = $oExtObjects->ToArray(); + return array($oReconFilter->ToOql(), $aKeys); + } + + // Returns true if the CSV data specifies that the external key must be left undefined + protected function IsNullExternalKeySpec($aRowData, $sAttCode) + { + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + if (strlen($aRowData[$iCol]) > 0) + { + return false; + } + } + return true; + } + + protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors) + { + $aResults = array(); + $aErrors = array(); + + // External keys reconciliation + // + foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + { + // Skip external keys used for the reconciliation process + // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue; + + $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + + if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) + { + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + if ($oExtKey->IsNullAllowed()) + { + $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue()); + $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue()); + } + else + { + $aErrors[$sAttCode] = "Null not allowed"; + $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), 'Null not allowed'); + } + } + else + { + $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass()); + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '='); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + $oExtObjects = new CMDBObjectSet($oReconFilter); + switch($oExtObjects->Count()) + { + case 0: + $aErrors[$sAttCode] = "Object not found"; + $aResults[$sAttCode]= new CellStatus_SearchIssue(); + break; + case 1: + // Do change the external key attribute + $oForeignObj = $oExtObjects->Fetch(); + $oTargetObj->Set($sAttCode, $oForeignObj->GetKey()); + break; + default: + $aErrors[$sAttCode] = "Found ".$oExtObjects->Count()." matches"; + $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $oExtObjects->Count(), $oReconFilter->ToOql()); + } + } + + // Report + if (!array_key_exists($sAttCode, $aResults)) + { + $iForeignObj = $oTargetObj->Get($sAttCode); + if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) + { + if ($oTargetObj->IsNew()) + { + $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); + } + else + { + $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); + } + } + else + { + $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); + } + } + } + + // Set the object attributes + // + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + // skip the private key, if any + if ($sAttCode == 'id') continue; + + $res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]); + if ($res === true) + { + $oTargetObj->Set($sAttCode, $aRowData[$iCol]); + } + else + { + // $res is a string with the error description + $aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res"; + } + } + + // Reporting on fields + // + $aChangedFields = $oTargetObj->ListChanges(); + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') + { + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); + } + if (isset($aErrors[$sAttCode])) + { + $aResults[$iCol]= new CellStatus_Issue($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode), $aErrors[$sAttCode]); + } + elseif (array_key_exists($sAttCode, $aChangedFields)) + { + if ($oTargetObj->IsNew()) + { + $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); + } + else + { + $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode)); + } + } + else + { + // By default... nothing happens + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); + } + } + + // Checks + // + $res = $oTargetObj->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res"; + } + return $aResults; + } + + protected function PrepareMissingObject(&$oTargetObj, &$aErrors) + { + $aResults = array(); + $aErrors = array(); + + // External keys + // + foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + { + //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode)); + + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + $aResults[$iCol] = new CellStatus_Void('?'); + } + } + + // Update attributes + // + foreach($this->m_aOnDisappear as $sAttCode => $value) + { + if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode)) + { + throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode)); + } + $oTargetObj->Set($sAttCode, $value); + if (!array_key_exists($sAttCode, $this->m_aAttList)) + { + // #@# will be out of the reporting... (counted anyway) + } + } + + // Reporting on fields + // + $aChangedFields = $oTargetObj->ListChanges(); + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') + { + $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey()); + } + if (array_key_exists($sAttCode, $aChangedFields)) + { + $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode)); + } + else + { + // By default... nothing happens + $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); + } + } + + // Checks + // + $res = $oTargetObj->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res"; + } + return $aResults; + } + + + protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null) + { + $oTargetObj = MetaModel::NewObject($this->m_sClass); + $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + return $oTargetObj; + } + + // Check that any external key will have a value proposed + $aMissingKeys = array(); + foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) + { + if (!$oExtKey->IsNullAllowed()) + { + if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) + { + $aMissingKeys[] = $oExtKey->GetLabel(); + } + } + } + if (count($aMissingKeys) > 0) + { + $sMissingKeys = implode(', ', $aMissingKeys); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys"); + return $oTargetObj; + } + + // Optionaly record the results + // + if ($oChange) + { + $newID = $oTargetObj->DBInsertTrackedNoReload($oChange); + $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj($this->m_sClass, $newID); + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($newID); + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void(0); + } + return $oTargetObj; + } + + protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null) + { + $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); + + // Reporting + // + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + return; + } + + $aChangedFields = $oTargetObj->ListChanges(); + if (count($aChangedFields) > 0) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields)); + + // Optionaly record the results + // + if ($oChange) + { + $oTargetObj->DBUpdateTracked($oChange); + } + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange(); + } + } + + protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null) + { + $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors); + + // Reporting + // + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)"); + return; + } + + $aChangedFields = $oTargetObj->ListChanges(); + if (count($aChangedFields) > 0) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields)); + + // Optionaly record the results + // + if ($oChange) + { + $oTargetObj->DBUpdateTracked($oChange); + } + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0); + } + } + + public function Process(CMDBChange $oChange = null) + { + // Note: $oChange can be null, in which case the aim is to check what would be done + + // Debug... + // + if (false) + { + echo "
\n";
+			echo "Attributes:\n";
+			print_r($this->m_aAttList);
+			echo "ExtKeys:\n";
+			print_r($this->m_aExtKeys);
+			echo "Reconciliation:\n";
+			print_r($this->m_aReconcilKeys);
+			echo "Synchro scope:\n";
+			print_r($this->m_sSynchroScope);
+			echo "Synchro changes:\n";
+			print_r($this->m_aOnDisappear);
+			//echo "Data:\n";
+			//print_r($this->m_aData);
+			echo "
\n"; + exit; + } + + + // Compute the results + // + if (!is_null($this->m_sSynchroScope)) + { + $aVisited = array(); + } + $aResult = array(); + foreach($this->m_aData as $iRow => $aRowData) + { + $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass); + $bSkipQuery = false; + foreach($this->m_aReconcilKeys as $sAttCode) + { + $valuecondition = null; + if (array_key_exists($sAttCode, $this->m_aExtKeys)) + { + if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) + { + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oExtKey->IsNullAllowed()) + { + $valuecondition = $oExtKey->GetNullValue(); + $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); + } + } + else + { + // The value has to be found or verified + list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); + + if (count($aMatches) == 1) + { + $oRemoteObj = reset($aMatches); // first item + $valuecondition = $oRemoteObj->GetKey(); + $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey()); + } + elseif (count($aMatches) == 0) + { + $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue(); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); + } + } + } + else + { + // The value is given in the data row + $iCol = $this->m_aAttList[$sAttCode]; + $valuecondition = $aRowData[$iCol]; + } + if (is_null($valuecondition)) + { + $bSkipQuery = true; + } + else + { + $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '='); + } + } + if ($bSkipQuery) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile"); + } + else + { + $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); + switch($oReconciliationSet->Count()) + { + case 0: + $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in CreateObject + $aVisited[] = $oTargetObj->GetKey(); + break; + case 1: + $oTargetObj = $oReconciliationSet->Fetch(); + $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject + if (!is_null($this->m_sSynchroScope)) + { + $aVisited[] = $oTargetObj->GetKey(); + } + break; + default: + // Found several matches, ambiguous + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation"); + $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql()); + $aResult[$iRow]["finalclass"]= 'n/a'; + } + } + + // Whatever happened, do report the reconciliation values + foreach($this->m_aAttList as $iCol) + { + if (!array_key_exists($iCol, $aResult[$iRow])) + { + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + } + foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts) + { + if (!array_key_exists($sAttCode, $aResult[$iRow])) + { + $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a'); + } + foreach ($aForeignAtts as $sForeignAttCode => $iCol) + { + if (!array_key_exists($iCol, $aResult[$iRow])) + { + // The foreign attribute is one of our reconciliation key + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + } + } + } + + if (!is_null($this->m_sSynchroScope)) + { + // Compute the delta between the scope and visited objects + $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope); + $oScopeSet = new DBObjectSet($oScopeSearch); + while ($oObj = $oScopeSet->Fetch()) + { + $iObj = $oObj->GetKey(); + if (!in_array($iObj, $aVisited)) + { + $iRow++; + $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange); + } + } + } + + return $aResult; + } + + /** + * Display the history of bulk imports + */ + static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false) + { + $sAjaxDivId = "CSVImportHistory"; + if (!$bFromAjax) + { + $oPage->add('
'); + } + + $oPage->p(Dict::S('UI:History:BulkImports+')); + + $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'"); + + $iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1; + $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), $iQueryLimit); + + $oAppContext = new ApplicationContext(); + + $bLimitExceeded = false; + if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit()) + { + $bLimitExceeded = true; + if (!$bShowAll) + { + $iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit(); + $oBulkChanges->SetLimit($iMaxObjects); + } + } + $oBulkChanges->Seek(0); + + $aDetails = array(); + while ($oChange = $oBulkChanges->Fetch()) + { + $sDate = ''.$oChange->Get('date').''; + $sUser = $oChange->GetUserName(); + if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches)) + { + $sUser = $aMatches[1]; + } + else + { + $sUser = $oChange->Get('userinfo'); + } + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); + $iCreated = $oOpSet->Count(); + + // Get the class from the first item found (assumption: a CSV load is done for a single class) + if ($oCreateOp = $oOpSet->Fetch()) + { + $sClass = $oCreateOp->Get('objclass'); + } + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); + + $aModified = array(); + $aAttList = array(); + while ($oModified = $oOpSet->Fetch()) + { + // Get the class (if not done earlier on object creation) + $sClass = $oModified->Get('objclass'); + $iKey = $oModified->Get('objkey'); + $sAttCode = $oModified->Get('attcode'); + + $aAttList[$sClass][$sAttCode] = true; + $aModified["$sClass::$iKey"] = true; + } + $iModified = count($aModified); + + // Assumption: there is only one class of objects being loaded + // Then the last class found gives us the class for every object + + $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified); + + } + + $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), + 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), + 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')), + 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')), + 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')), + ); + + if ($bLimitExceeded) + { + if ($bShowAll) + { + // Collapsible list + $oPage->add('

'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'  '.Dict::S('UI:CollapseList').'

'); + } + else + { + // Truncated list + $iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit(); + $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count()); + $sLinkLabel = Dict::S('UI:DisplayAll'); + $oPage->add('

'.$sCollapsedLabel.'  '.$sLinkLabel.'

'); + + $oPage->add_ready_script( +<<GetForLink(); + $oPage->add_script( +<<table($aConfig, $aDetails); + + if (!$bFromAjax) + { + $oPage->add('
'); + } + } + + /** + * Display the details of an import + */ + static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange) + { + if ($iChange == 0) + { + throw new Exception("Missing parameter changeid"); + } + $oChange = MetaModel::GetObject('CMDBChange', $iChange, false); + if (is_null($oChange)) + { + throw new Exception("Unknown change: $iChange"); + } + $oPage->add("

".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."

\n"); + + // Assumption : change made one single class of objects + $aObjects = array(); + $aAttributes = array(); // array of attcode => occurences + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange)); + while ($oOperation = $oOpSet->Fetch()) + { + $sClass = $oOperation->Get('objclass'); + $iKey = $oOperation->Get('objkey'); + $iObjId = "$sClass::$iKey"; + if (!isset($aObjects[$iObjId])) + { + $aObjects[$iObjId] = array(); + $aObjects[$iObjId]['__class__'] = $sClass; + $aObjects[$iObjId]['__id__'] = $iKey; + } + if (get_class($oOperation) == 'CMDBChangeOpCreate') + { + $aObjects[$iObjId]['__created__'] = true; + } + elseif (is_subclass_of($oOperation, 'CMDBChangeOpSetAttribute')) + { + $sAttCode = $oOperation->Get('attcode'); + + if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar') + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsExternalKey()) + { + $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue')); + $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue')); + $sOldValue = $oOldTarget->GetHyperlink(); + $sNewValue = $oNewTarget->GetHyperlink(); + } + else + { + $sOldValue = $oOperation->GetAsHTML('oldvalue'); + $sNewValue = $oOperation->GetAsHTML('newvalue'); + } + $aObjects[$iObjId][$sAttCode] = $sOldValue.' -> '.$sNewValue; + } + else + { + $aObjects[$iObjId][$sAttCode] = 'n/a'; + } + + if (isset($aAttributes[$sAttCode])) + { + $aAttributes[$sAttCode]++; + } + else + { + $aAttributes[$sAttCode] = 1; + } + } + } + + $aDetails = array(); + foreach($aObjects as $iUId => $aObjData) + { + $aRow = array(); + $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false); + if (is_null($oObject)) + { + $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)'; + } + else + { + $aRow['object'] = $oObject->GetHyperlink(); + } + if (isset($aObjData['__created__'])) + { + $aRow['operation'] = Dict::S('Change:ObjectCreated'); + } + else + { + $aRow['operation'] = Dict::S('Change:ObjectModified'); + } + foreach ($aAttributes as $sAttCode => $iOccurences) + { + if (isset($aObjData[$sAttCode])) + { + $aRow[$sAttCode] = $aObjData[$sAttCode]; + } + elseif (!is_null($oObject)) + { + // This is the current vaslue: $oObject->GetAsHtml($sAttCode) + // whereas we are displaying the value that was set at the time + // the object was created + // This requires addtional coding...let's do that later + $aRow[$sAttCode] = ''; + } + else + { + $aRow[$sAttCode] = ''; + } + } + $aDetails[] = $aRow; + } + + $aConfig = array(); + $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass)); + $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+')); + foreach ($aAttributes as $sAttCode => $iOccurences) + { + $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode)); + } + $oPage->table($aConfig, $aDetails); + } +} + + +?> diff --git a/core/cmdbchange.class.inc.php b/core/cmdbchange.class.inc.php new file mode 100644 index 0000000000..db2a9f84b4 --- /dev/null +++ b/core/cmdbchange.class.inc.php @@ -0,0 +1,82 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * A change as requested/validated at once by user, may groups many atomic changes + * + * @package iTopORM + */ +class CMDBChange extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "date", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_change", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + } + + // Helper to keep track of the author of a given change, + // taking into account a variety of cases (contact attached or not, impersonation) + static public function GetCurrentUserName() + { + if (UserRights::IsImpersonated()) + { + $sUserString = Dict::Format('UI:Archive_User_OnBehalfOf_User', UserRights::GetRealUserFriendlyName(), UserRights::GetUserFriendlyName()); + } + else + { + $sUserString = UserRights::GetUserFriendlyName(); + } + return $sUserString; + } + + public function GetUserName() + { + if (preg_match('/^(.*)\\(CSV\\)$/i', $this->Get('userinfo'), $aMatches)) + { + $sUser = $aMatches[1]; + } + else + { + $sUser = $this->Get('userinfo'); + } + return $sUser; + } +} + +?> diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php new file mode 100644 index 0000000000..d78870295c --- /dev/null +++ b/core/cmdbchangeop.class.inc.php @@ -0,0 +1,478 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * Various atomic change operations, to be tracked + * + * @package iTopORM + */ + +class CMDBChangeOp extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop", + "db_key_field" => "id", + "db_finalclass_field" => "optype", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("change", array("allowed_values"=>null, "sql"=>"changeid", "targetclass"=>"CMDBChange", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("date", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"date"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userinfo", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"userinfo"))); + MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values"=>null, "sql"=>"objclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("objkey", array("allowed_values"=>null, "sql"=>"objkey", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_SetZListItems('details', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return ''; + } +} + + + +/** + * Record the creation of an object + * + * @package iTopORM + */ +class CMDBChangeOpCreate extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_create", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return Dict::S('Change:ObjectCreated'); + } +} + + +/** + * Record the deletion of an object + * + * @package iTopORM + */ +class CMDBChangeOpDelete extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_delete", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + } + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return Dict::S('Change:ObjectDeleted'); + } +} + + +/** + * Record the modification of an attribute (abstract) + * + * @package iTopORM + */ +class CMDBChangeOpSetAttribute extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } +} + +/** + * Record the modification of a scalar attribute + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeScalar extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_scalar", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("oldvalue", array("allowed_values"=>null, "sql"=>"oldvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("newvalue", array("allowed_values"=>null, "sql"=>"newvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // 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(); + $sNewValue = $this->Get('newvalue'); + $sOldValue = $this->Get('oldvalue'); + if ( (($oAttDef->GetType() == 'String') || ($oAttDef->GetType() == 'Text')) && + (strlen($sNewValue) > strlen($sOldValue)) ) + { + // Check if some text was not appended to the field + if (substr($sNewValue,0, strlen($sOldValue)) == $sOldValue) // Text added at the end + { + $sDelta = substr($sNewValue, strlen($sOldValue)); + $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sAttName); + } + else if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning + { + $sDelta = substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue)); + $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sAttName); + } + else + { + $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sAttName, $sNewValue, $sOldValue); + } + } + elseif($bIsHtml && $oAttDef->IsExternalKey()) + { + $sTargetClass = $oAttDef->GetTargetClass(); + $sFrom = MetaModel::GetHyperLink($sTargetClass, $sOldValue); + $sTo = MetaModel::GetHyperLink($sTargetClass, $sNewValue); + $sResult = "$sAttName set to $sTo (previous: $sFrom)"; + } + elseif ($oAttDef instanceOf AttributeBlob) + { + $sResult = "#@# Issue... found an attribute for which other type of tracking should be made"; + } + else + { + $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sAttName, $sNewValue, $sOldValue); + } + } + return $sResult; + } +} + +/** + * Record the modification of a blob + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_data", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeBlob("prevdata", array("depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + $oPrevDoc = $this->Get('prevdata'); + $sDocView = $oPrevDoc->GetAsHtml(); + $sDocView .= "
".Dict::Format('UI:OpenDocumentInNewWindow_',$oPrevDoc->GetDisplayLink(get_class($this), $this->GetKey(), 'prevdata')).", \n"; + $sDocView .= Dict::Format('UI:DownloadDocument_', $oPrevDoc->GetDownloadLink(get_class($this), $this->GetKey(), 'prevdata'))."\n"; + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView); + } + return $sResult; + } +} +/** + * Safely record the modification of one way encrypted password + */ +class CMDBChangeOpSetAttributeOneWayPassword extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_pwd", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeOneWayPassword("prev_pwd", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); + + // 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(); + $sResult = Dict::Format('Change:AttName_Changed', $sAttName); + } + return $sResult; + } +} + +/** + * Safely record the modification of an encrypted field + */ +class CMDBChangeOpSetAttributeEncrypted extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_encrypted", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeEncryptedString("prevstring", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); + + // 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(); + $sPrevString = $this->Get('prevstring'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevString); + } + return $sResult; + } +} + +/** + * Record the modification of a multiline string (text) + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeText extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "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("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + // 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 = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + return $sResult; + } +} + +?> diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php new file mode 100644 index 0000000000..2e6e370073 --- /dev/null +++ b/core/cmdbobject.class.inc.php @@ -0,0 +1,533 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * cmdbObjectClass + * the file to include, then the core is yours + * + * @package iTopORM + */ + +require_once('coreexception.class.inc.php'); + +require_once('config.class.inc.php'); +require_once('log.class.inc.php'); +require_once('kpi.class.inc.php'); + +require_once('dict.class.inc.php'); + +require_once('attributedef.class.inc.php'); +require_once('filterdef.class.inc.php'); +require_once('stimulus.class.inc.php'); +require_once('valuesetdef.class.inc.php'); +require_once('MyHelpers.class.inc.php'); + +require_once('expression.class.inc.php'); + +require_once('cmdbsource.class.inc.php'); +require_once('sqlquery.class.inc.php'); +require_once('oql/oqlquery.class.inc.php'); +require_once('oql/oqlexception.class.inc.php'); +require_once('oql/oql-parser.php'); +require_once('oql/oql-lexer.php'); +require_once('oql/oqlinterpreter.class.inc.php'); + +require_once('dbobject.class.php'); +require_once('dbobjectsearch.class.php'); +require_once('dbobjectset.class.php'); + +require_once('dbproperty.class.inc.php'); + +// db change tracking data model +require_once('cmdbchange.class.inc.php'); +require_once('cmdbchangeop.class.inc.php'); + +// customization data model +// Romain: temporary moved into application.inc.php (see explanations there) +//require_once('trigger.class.inc.php'); +//require_once('action.class.inc.php'); + +// application log +// Romain: temporary moved into application.inc.php (see explanations there) +//require_once('event.class.inc.php'); + +require_once('csvparser.class.inc.php'); +require_once('bulkchange.class.inc.php'); + +/** + * A persistent object, which changes are accurately recorded + * + * @package iTopORM + */ +abstract class CMDBObject extends DBObject +{ + protected $m_datCreated; + protected $m_datUpdated; + // Note: this value is static, but that could be changed because it is sometimes a real issue (see update of interfaces / connected_to + protected static $m_oCurrChange = null; + + + private function RecordObjCreation(CMDBChange $oChange) + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate"); + $oMyChangeOp->Set("change", $oChange->GetKey()); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + private function RecordObjDeletion(CMDBChange $oChange, $objkey) + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete"); + $oMyChangeOp->Set("change", $oChange->GetKey()); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $objkey); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + private function RecordAttChanges(CMDBChange $oChange, array $aValues, array $aOrigValues) + { + // $aValues is an array of $sAttCode => $value + // + foreach ($aValues as $sAttCode=> $value) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsLinkSet()) continue; // #@# temporary + + if ($oAttDef instanceOf AttributeOneWayPassword) + { + // One Way encrypted passwords' history is stored -one way- encrypted + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword"); + $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 = ''; + } + $oMyChangeOp->Set("prev_pwd", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeEncryptedString) + { + // Encrypted string history is stored encrypted + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted"); + $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 = ''; + } + $oMyChangeOp->Set("prevstring", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeBlob) + { + // Data blobs + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob"); + $oMyChangeOp->Set("change", $oChange->GetKey()); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (array_key_exists($sAttCode, $aOrigValues)) + { + $original = $aOrigValues[$sAttCode]; + } + else + { + $original = new ormDocument(); + } + $oMyChangeOp->Set("prevdata", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + 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 + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); + $oMyChangeOp->Set("change", $oChange->GetKey()); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (array_key_exists($sAttCode, $aOrigValues)) + { + $sOriginalValue = $aOrigValues[$sAttCode]; + } + else + { + $sOriginalValue = 'undefined'; + } + $oMyChangeOp->Set("oldvalue", $sOriginalValue); + $oMyChangeOp->Set("newvalue", $value); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + + /** + * Helper to ultimately check user rights before writing (Insert, Update or Delete) + * The check should never fail, because the UI should prevent from such a usage + * Anyhow, if the user has found a workaround... the security gets enforced here + */ + protected function CheckUserRights($bSkipStrongSecurity, $iActionCode) + { + if (is_null($bSkipStrongSecurity)) + { + // This is temporary + // We have implemented this safety net right before releasing iTop 1.0 + // and we decided that it was too risky to activate it + // Anyhow, users willing to have a very strong security could set + // skip_strong_security = 0, in the config file + $bSkipStrongSecurity = MetaModel::GetConfig()->Get('skip_strong_security'); + } + if (!$bSkipStrongSecurity) + { + $sClass = get_class($this); + $oSet = DBObjectSet::FromObject($this); + if (!UserRights::IsActionAllowed($sClass, $iActionCode, $oSet)) + { + // Intrusion detected + throw new SecurityException('You are not allowed to modify objects of class: '.$sClass); + } + } + } + + + public function DBInsert() + { + if(!is_object(self::$m_oCurrChange)) + { + throw new CoreException("DBInsert() could not be used here, please use DBInsertTracked() instead"); + } + return $this->DBInsertTracked_Internal(); + } + + public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + + self::$m_oCurrChange = $oChange; + $ret = $this->DBInsertTracked_Internal(); + self::$m_oCurrChange = null; + return $ret; + } + + public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + + self::$m_oCurrChange = $oChange; + $ret = $this->DBInsertTracked_Internal(true); + self::$m_oCurrChange = null; + return $ret; + } + + protected function DBInsertTracked_Internal($bDoNotReload = false) + { + if ($bDoNotReload) + { + $ret = parent::DBInsertNoReload(); + } + else + { + $ret = parent::DBInsert(); + } + $this->RecordObjCreation(self::$m_oCurrChange); + return $ret; + } + + public function DBClone($newKey = null) + { + if(!self::$m_oCurrChange) + { + throw new CoreException("DBClone() could not be used here, please use DBCloneTracked() instead"); + } + return $this->DBCloneTracked_Internal(); + } + + public function DBCloneTracked(CMDBChange $oChange, $newKey = null) + { + self::$m_oCurrChange = $oChange; + $this->DBCloneTracked_Internal($newKey); + self::$m_oCurrChange = null; + } + + protected function DBCloneTracked_Internal($newKey = null) + { + $newKey = parent::DBClone($newKey); + $oClone = MetaModel::GetObject(get_class($this), $newKey); + + $oClone->RecordObjCreation(self::$m_oCurrChange); + return $newKey; + } + + public function DBUpdate() + { + if(!self::$m_oCurrChange) + { + throw new CoreException("DBUpdate() could not be used here, please use DBUpdateTracked() instead"); + } + return $this->DBUpdateTracked_internal(); + } + + public function DBUpdateTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + + self::$m_oCurrChange = $oChange; + $this->DBUpdateTracked_Internal(); + self::$m_oCurrChange = null; + } + + protected function DBUpdateTracked_Internal() + { + // Copy the changes list before the update (the list should be reset afterwards) + $aChanges = $this->ListChanges(); + if (count($aChanges) == 0) + { + //throw new CoreWarning("Attempting to update an unchanged object"); + return; + } + + // Save the original values (will be reset to the new values when the object get written to the DB) + $aOriginalValues = $this->m_aOrigValues; + $ret = parent::DBUpdate(); + $this->RecordAttChanges(self::$m_oCurrChange, $aChanges, $aOriginalValues); + return $ret; + } + + public function DBDelete() + { + if(!self::$m_oCurrChange) + { + throw new CoreException("DBDelete() could not be used here, please use DBDeleteTracked() instead"); + } + return $this->DBDeleteTracked_Internal(); + } + + public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE); + + self::$m_oCurrChange = $oChange; + $this->DBDeleteTracked_Internal(); + self::$m_oCurrChange = null; + } + + protected function DBDeleteTracked_Internal() + { + $prevkey = $this->GetKey(); + $ret = parent::DBDelete(); + $this->RecordObjDeletion(self::$m_oCurrChange, $prevkey); + return $ret; + } + + public static function BulkDelete(DBObjectSearch $oFilter) + { + if(!self::$m_oCurrChange) + { + throw new CoreException("BulkDelete() could not be used here, please use BulkDeleteTracked() instead"); + } + return $this->BulkDeleteTracked_Internal($oFilter); + } + + public static function BulkDeleteTracked(CMDBChange $oChange, DBObjectSearch $oFilter) + { + self::$m_oCurrChange = $oChange; + $this->BulkDeleteTracked_Internal($oFilter); + self::$m_oCurrChange = null; + } + + protected static function BulkDeleteTracked_Internal(DBObjectSearch $oFilter) + { + throw new CoreWarning("Change tracking not tested for bulk operations"); + + // Get the list of objects to delete (and record data before deleting the DB records) + $oObjSet = new CMDBObjectSet($oFilter); + $aObjAndKeys = array(); // array of id=>object + while ($oItem = $oObjSet->Fetch()) + { + $aObjAndKeys[$oItem->GetKey()] = $oItem; + } + $oObjSet->FreeResult(); + + // Delete in one single efficient query + $ret = parent::BulkDelete($oFilter); + // Record... in many queries !!! + foreach($aObjAndKeys as $prevkey=>$oItem) + { + $oItem->RecordObjDeletion(self::$m_oCurrChange, $prevkey); + } + return $ret; + } + + public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues) + { + if(!self::$m_oCurrChange) + { + throw new CoreException("BulkUpdate() could not be used here, please use BulkUpdateTracked() instead"); + } + return $this->BulkUpdateTracked_Internal($oFilter, $aValues); + } + + public static function BulkUpdateTracked(CMDBChange $oChange, DBObjectSearch $oFilter, array $aValues) + { + self::$m_oCurrChange = $oChange; + $this->BulkUpdateTracked_Internal($oFilter, $aValues); + self::$m_oCurrChange = null; + } + + protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues) + { + // $aValues is an array of $sAttCode => $value + + // Get the list of objects to update (and load it before doing the change) + $oObjSet = new CMDBObjectSet($oFilter); + $oObjSet->Load(); + + // Keep track of the previous values (will be overwritten when the objects are synchronized with the DB) + $aOriginalValues = array(); + $oObjSet->Rewind(); + while ($oItem = $oObjSet->Fetch()) + { + $aOriginalValues[$oItem->GetKey()] = $oItem->m_aOrigValues; + } + + // Update in one single efficient query + $ret = parent::BulkUpdate($oFilter, $aValues); + + // Record... in many queries !!! + $oObjSet->Rewind(); + while ($oItem = $oObjSet->Fetch()) + { + $aChangedValues = $oItem->ListChangedValues($aValues); + $oItem->RecordAttChanges(self::$m_oCurrChange, $aChangedValues, $aOriginalValues[$oItem->GetKey()]); + } + return $ret; + } +} + + + +/** + * TODO: investigate how to get rid of this class that was made to workaround some language limitation... or a poor design! + * + * @package iTopORM + */ +class CMDBObjectSet extends DBObjectSet +{ + // this is the public interface (?) + + // I have to define those constructors here... :-( + // just to get the right object class in return. + // I have to think again to those things: maybe it will work fine if a have a constructor define here (?) + + static public function FromScratch($sClass) + { + $oFilter = new CMDBSearchFilter($sClass); + $oRetSet = new CMDBObjectSet($oFilter); // THE ONLY DIFF IS HERE + // NOTE: THIS DOES NOT WORK IF m_bLoaded is private... + // BUT IT THAT CASE YOU DO NOT GET ANY ERROR !!!!! + $oRetSet->m_bLoaded = true; // no DB load + return $oRetSet; + } + + static public function FromArray($sClass, $aObjects) + { + $oFilter = new CMDBSearchFilter($sClass); + $oRetSet = new CMDBObjectSet($oFilter); // THE ONLY DIFF IS HERE + // NOTE: THIS DOES NOT WORK IF m_bLoaded is private... + // BUT IT THAT CASE YOU DO NOT GET ANY ERROR !!!!! + $oRetSet->m_bLoaded = true; // no DB load + $oRetSet->AddObjectArray($aObjects); + return $oRetSet; + } + + static public function FromArrayAssoc($aClasses, $aObjects) + { + // In a perfect world, we should create a complete tree of DBObjectSearch, + // but as we lack most of the information related to the objects, + // let's create one search definition + $sClass = reset($aClasses); + $sAlias = key($aClasses); + $oFilter = new CMDBSearchFilter($sClass, $sAlias); + + $oRetSet = new CMDBObjectSet($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + + foreach($aObjects as $rowIndex => $aObjectsByClassAlias) + { + $oRetSet->AddObjectExtended($aObjectsByClassAlias); + } + return $oRetSet; + } +} + +/** + * TODO: investigate how to get rid of this class that was made to workaround some language limitation... or a poor design! + * + * @package iTopORM + */ +class CMDBSearchFilter extends DBObjectSearch +{ + // this is the public interface (?) +} + + +?> diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php new file mode 100644 index 0000000000..cf91820f62 --- /dev/null +++ b/core/cmdbsource.class.inc.php @@ -0,0 +1,584 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once('MyHelpers.class.inc.php'); + +class MySQLException extends CoreException +{ + public function __construct($sIssue, $aContext) + { + $aContext['mysql_error'] = mysql_error(); + $aContext['mysql_errno'] = mysql_errno(); + parent::__construct($sIssue, $aContext); + } +} + + +/** + * CMDBSource + * database access wrapper + * + * @package iTopORM + */ +class CMDBSource +{ + protected static $m_sDBHost; + protected static $m_sDBUser; + protected static $m_sDBPwd; + protected static $m_sDBName; + protected static $m_resDBLink; + + public static function Init($sServer, $sUser, $sPwd, $sSource = '') + { + self::$m_sDBHost = $sServer; + self::$m_sDBUser = $sUser; + self::$m_sDBPwd = $sPwd; + self::$m_sDBName = $sSource; + if (!self::$m_resDBLink = @mysql_pconnect($sServer, $sUser, $sPwd)) + { + throw new MySQLException('Could not connect to the DB server', array('host'=>$sServer, 'user'=>$sUser)); + } + if (!empty($sSource)) + { + if (!mysql_select_db($sSource, self::$m_resDBLink)) + { + throw new MySQLException('Could not select DB', array('host'=>$sServer, 'user'=>$sUser, 'db_name'=>$sSource)); + } + } + } + + public static function SetCharacterSet($sCharset = 'utf8', $sCollation = 'utf8_general_ci') + { + if (strlen($sCharset) > 0) + { + if (strlen($sCollation) > 0) + { + self::Query("SET NAMES '$sCharset' COLLATE '$sCollation'"); + } + else + { + self::Query("SET NAMES '$sCharset'"); + } + } + } + + public static function ListDB() + { + $aDBs = self::QueryToCol('SHOW DATABASES', 'Database'); + // Show Database does return the DB names in lower case + return $aDBs; + } + + public static function IsDB($sSource) + { + try + { + $aDBs = self::ListDB(); + foreach($aDBs as $sDBName) + { + // perform a case insensitive test because on Windows the table names become lowercase :-( + if (strtolower($sDBName) == strtolower($sSource)) return true; + } + return false; + } + catch(Exception $e) + { + // In case we don't have rights to enumerate the databases + // Let's try to connect directly + return @mysql_select_db($sSource, self::$m_resDBLink); + } + + } + + public static function GetDBVersion() + { + $aVersions = self::QueryToCol('SELECT Version() as version', 'version'); + return $aVersions[0]; + } + + public static function SelectDB($sSource) + { + if (!mysql_select_db($sSource, self::$m_resDBLink)) + { + throw new MySQLException('Could not select DB', array('db_name'=>$sSource)); + } + self::$m_sDBName = $sSource; + } + + public static function CreateDB($sSource) + { + self::Query("CREATE DATABASE `$sSource` CHARACTER SET utf8 COLLATE utf8_unicode_ci"); + self::SelectDB($sSource); + } + + public static function DropDB($sDBToDrop = '') + { + if (empty($sDBToDrop)) + { + $sDBToDrop = self::$m_sDBName; + } + self::Query("DROP DATABASE `$sDBToDrop`"); + if ($sDBToDrop == self::$m_sDBName) + { + self::$m_sDBName = ''; + } + } + + public static function CreateTable($sQuery) + { + $res = self::Query($sQuery); + self::_TablesInfoCacheReset(); // reset the table info cache! + return $res; + } + + public static function DropTable($sTable) + { + $res = self::Query("DROP TABLE `$sTable`"); + self::_TablesInfoCacheReset(true); // reset the table info cache! + return $res; + } + + public static function DBHost() {return self::$m_sDBHost;} + public static function DBUser() {return self::$m_sDBUser;} + public static function DBPwd() {return self::$m_sDBPwd;} + public static function DBName() {return self::$m_sDBName;} + + public static function Quote($value, $bAlways = false, $cQuoteStyle = "'") + { + // Quote variable and protect against SQL injection attacks + // Code found in the PHP documentation: quote_smart($value) + + // bAlways should be set to true when the purpose is to create a IN clause, + // otherwise and if there is a mix of strings and numbers, the clause + // would always be false + + if (is_null($value)) + { + return 'NULL'; + } + + if (is_array($value)) + { + $aRes = array(); + foreach ($value as $key => $itemvalue) + { + $aRes[$key] = self::Quote($itemvalue, $bAlways, $cQuoteStyle); + } + return $aRes; + } + + // Stripslashes + if (get_magic_quotes_gpc()) + { + $value = stripslashes($value); + } + // Quote if not a number or a numeric string + if ($bAlways || is_string($value)) + { + $value = $cQuoteStyle . mysql_real_escape_string($value, self::$m_resDBLink) . $cQuoteStyle; + } + return $value; + } + + public static function Query($sSQLQuery) + { + // Add info into the query as a comment, for easier error tracking + // disabled until we need it really! + // + //$aTraceInf['file'] = __FILE__; + // $sSQLQuery .= MyHelpers::MakeSQLComment($aTraceInf); + + $oKPI = new ExecutionKPI(); + $result = mysql_query($sSQLQuery, self::$m_resDBLink); + if (!$result) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery)); + } + $oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery); + + return $result; + } + + public static function GetNextInsertId($sTable) + { + $sSQL = "SHOW TABLE STATUS LIKE '$sTable'"; + $result = self::Query($sSQL); + $aRow = mysql_fetch_assoc($result); + $iNextInsertId = $aRow['Auto_increment']; + return $iNextInsertId; + } + + public static function GetInsertId() + { + return mysql_insert_id(self::$m_resDBLink); + } + public static function InsertInto($sSQLQuery) + { + if (self::Query($sSQLQuery)) + { + return self::GetInsertId(); + } + return false; + } + + public static function QueryToArray($sSql) + { + $aData = array(); + $result = mysql_query($sSql, self::$m_resDBLink); + if (!$result) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + while ($aRow = mysql_fetch_array($result, MYSQL_BOTH)) + { + $aData[] = $aRow; + } + mysql_free_result($result); + return $aData; + } + + public static function QueryToCol($sSql, $col) + { + $aColumn = array(); + $aData = self::QueryToArray($sSql); + foreach($aData as $aRow) + { + @$aColumn[] = $aRow[$col]; + } + return $aColumn; + } + + public static function ExplainQuery($sSql) + { + $aData = array(); + $result = mysql_query("EXPLAIN $sSql", self::$m_resDBLink); + if (!$result) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + $aNames = self::GetColumns($result); + + $aData[] = $aNames; + while ($aRow = mysql_fetch_array($result, MYSQL_ASSOC)) + { + $aData[] = $aRow; + } + mysql_free_result($result); + return $aData; + } + + public static function TestQuery($sSql) + { + $result = mysql_query("EXPLAIN $sSql", self::$m_resDBLink); + if (!$result) + { + return mysql_error(); + } + + mysql_free_result($result); + return ''; + } + + public static function NbRows($result) + { + return mysql_num_rows($result); + } + + public static function FetchArray($result) + { + return mysql_fetch_array($result, MYSQL_ASSOC); + } + + public static function GetColumns($result) + { + $aNames = array(); + for ($i = 0; $i < mysql_num_fields($result) ; $i++) + { + $meta = mysql_fetch_field($result, $i); + if (!$meta) + { + throw new MySQLException('mysql_fetch_field: No information available', array('query'=>$sSql, 'i'=>$i)); + } + else + { + $aNames[] = $meta->name; + } + } + return $aNames; + } + + public static function Seek($result, $iRow) + { + return mysql_data_seek($result, $iRow); + } + + public static function FreeResult($result) + { + return mysql_free_result($result); + } + + public static function IsTable($sTable) + { + $aTableInfo = self::GetTableInfo($sTable); + return (!empty($aTableInfo)); + } + + public static function IsKey($sTable, $iKey) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($iKey, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$iKey]; + if (!array_key_exists("Key", $aFieldData)) return false; + return ($aFieldData["Key"] == "PRI"); + } + + public static function IsAutoIncrement($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + if (!array_key_exists("Extra", $aFieldData)) return false; + //MyHelpers::debug_breakpoint($aFieldData); + return (strstr($aFieldData["Extra"], "auto_increment")); + } + + public static function IsField($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + return true; + } + + public static function IsNullAllowed($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + return (strtolower($aFieldData["Null"]) == "yes"); + } + + public static function GetFieldType($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + return ($aFieldData["Type"]); + } + + 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) + { + assert(!empty($sTable)); + + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return array(); // #@# or an error ? + + return array_keys($aTableInfo["Fields"]); + } + + // Cache the information about existing tables, and their fields + private static $m_aTablesInfo = array(); + private static function _TablesInfoCacheReset() + { + self::$m_aTablesInfo = array(); + } + private static function _TableInfoCacheInit($sTableName) + { + if (isset(self::$m_aTablesInfo[strtolower($sTableName)]) + && (self::$m_aTablesInfo[strtolower($sTableName)] != null)) return; + + try + { + // Check if the table exists + $aFields = self::QueryToArray("SHOW COLUMNS FROM `$sTableName`"); + // Note: without backticks, you get an error with some table names (e.g. "group") + foreach ($aFields as $aFieldData) + { + $sFieldName = $aFieldData["Field"]; + self::$m_aTablesInfo[strtolower($sTableName)]["Fields"][$sFieldName] = + array + ( + "Name"=>$aFieldData["Field"], + "Type"=>$aFieldData["Type"], + "Null"=>$aFieldData["Null"], + "Key"=>$aFieldData["Key"], + "Default"=>$aFieldData["Default"], + "Extra"=>$aFieldData["Extra"] + ); + } + } + catch(MySQLException $e) + { + // Table does not exist + self::$m_aTablesInfo[strtolower($sTableName)] = null; + } + } + //public static function EnumTables() + //{ + // self::_TablesInfoCacheInit(); + // return array_keys(self::$m_aTablesInfo); + //} + public static function GetTableInfo($sTable) + { + self::_TableInfoCacheInit($sTable); + + // perform a case insensitive match because on Windows the table names become lowercase :-( + //foreach(self::$m_aTablesInfo as $sTableName => $aInfo) + //{ + // if (strtolower($sTableName) == strtolower($sTable)) + // { + // return $aInfo; + // } + //} + return self::$m_aTablesInfo[strtolower($sTable)]; + //return null; + } + + public static function DumpTable($sTable) + { + $sSql = "SELECT * FROM `$sTable`"; + $result = mysql_query($sSql, self::$m_resDBLink); + if (!$result) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + $aRows = array(); + while ($aRow = mysql_fetch_array($result, MYSQL_ASSOC)) + { + $aRows[] = $aRow; + } + mysql_free_result($result); + return $aRows; + } + + /** + * Returns the value of the specified server variable + * @param string $sVarName Name of the server variable + * @return mixed Current value of the variable + */ + public static function GetServerVariable($sVarName) + { + $result = ''; + $sSql = "SELECT @@$sVarName as theVar"; + $aRows = self::QueryToArray($sSql); + if (count($aRows) > 0) + { + $result = $aRows[0]['theVar']; + } + return $result; + } + + + /** + * Returns the privileges of the current user + * @return string privileges in a raw format + */ + public static function GetRawPrivileges() + { + try + { + $result = self::Query('SHOW GRANTS'); // [ FOR CURRENT_USER()] + } + catch(MySQLException $e) + { + return "Current user not allowed to see his own privileges (could not access to the database 'mysql' - $iCode)"; + } + + $aRes = array(); + while ($aRow = mysql_fetch_array($result, MYSQL_NUM)) + { + // so far, only one column... + $aRes[] = implode('/', $aRow); + } + mysql_free_result($result); + // so far, only one line... + return implode(', ', $aRes); + } + + /** + * Determine the slave status of the server + * @return bool true if the server is slave + */ + public static function IsSlaveServer() + { + try + { + $result = self::Query('SHOW SLAVE STATUS'); + } + catch(MySQLException $e) + { + throw new CoreException("Current user not allowed to check the status", array('mysql_error' => $e->getMessage())); + } + + if (mysql_num_rows($result) == 0) + { + return false; + } + + // Returns one single row anytime + $aRow = mysql_fetch_array($result, MYSQL_ASSOC); + mysql_free_result($result); + + if (!isset($aRow['Slave_IO_Running'])) + { + return false; + } + if (!isset($aRow['Slave_SQL_Running'])) + { + return false; + } + + // If at least one slave thread is running, then we consider that the slave is enabled + if ($aRow['Slave_IO_Running'] == 'Yes') + { + return true; + } + if ($aRow['Slave_SQL_Running'] == 'Yes') + { + return true; + } + return false; + } +} + + +?> diff --git a/core/config.class.inc.php b/core/config.class.inc.php new file mode 100644 index 0000000000..4896ce0bc4 --- /dev/null +++ b/core/config.class.inc.php @@ -0,0 +1,1037 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once('coreexception.class.inc.php'); + +class ConfigException extends CoreException +{ +} + +define ('DEFAULT_CHARACTER_SET', 'utf8'); +define ('DEFAULT_COLLATION', 'utf8_general_ci'); + +define ('DEFAULT_LOG_GLOBAL', true); +define ('DEFAULT_LOG_NOTIFICATION', true); +define ('DEFAULT_LOG_ISSUE', true); +define ('DEFAULT_LOG_WEB_SERVICE', true); +define ('DEFAULT_LOG_KPI_DURATION', false); +define ('DEFAULT_LOG_KPI_MEMORY', false); +define ('DEFAULT_DEBUG_QUERIES', false); + +define ('DEFAULT_QUERY_CACHE_ENABLED', true); + + +define ('DEFAULT_MIN_DISPLAY_LIMIT', 10); +define ('DEFAULT_MAX_DISPLAY_LIMIT', 15); +define ('DEFAULT_STANDARD_RELOAD_INTERVAL', 5*60); +define ('DEFAULT_FAST_RELOAD_INTERVAL', 1*60); +define ('DEFAULT_SECURE_CONNECTION_REQUIRED', false); +define ('DEFAULT_HTTPS_HYPERLINKS', false); +define ('DEFAULT_ALLOWED_LOGIN_TYPES', 'form|basic|external'); +define ('DEFAULT_EXT_AUTH_VARIABLE', '$_SERVER[\'REMOTE_USER\']'); +define ('DEFAULT_ENCRYPTION_KEY', '@iT0pEncr1pti0n!'); // We'll use a random value, later... + +/** + * Config + * configuration data (this class cannot not be localized, because it is responsible for loading the dictionaries) + * + * @package iTopORM + */ +class Config +{ + //protected $m_bIsLoaded = false; + protected $m_sFile = ''; + + protected $m_aAppModules; + protected $m_aDataModels; + protected $m_aWebServiceCategories; + protected $m_aAddons; + protected $m_aDictionaries; + + protected $m_aModuleSettings; + + // New way to store the settings ! + // + protected $m_aSettings = array( + 'skip_check_to_write' => array( + 'type' => 'bool', + 'description' => 'Disable data format and integrity checks to boost up data load (insert or update)', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'skip_check_ext_keys' => array( + 'type' => 'bool', + 'description' => 'Disable external key check when checking the value of attribtutes', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'skip_strong_security' => array( + 'type' => 'bool', + 'description' => 'Disable strong security - TEMPORY: this flag should be removed when we are more confident in the recent change in security', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'graphviz_path' => array( + 'type' => 'string', + 'description' => 'Path to the Graphviz "dot" executable for graphing objects lifecycle', + 'default' => '/usr/bin/dot', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'session_name' => array( + 'type' => 'string', + 'description' => 'The name of the cookie used to store the PHP session id', + 'default' => 'iTop', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'max_combo_length' => array( + 'type' => 'int', + 'description' => 'The maximum number of elements in a drop-down list. If more then an autocomplete will be used', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'min_autocomplete_chars' => array( + 'type' => 'int', + 'description' => 'The minimum number of characters to type in order to trigger the "autocomplete" behavior', + 'default' => 3, + 'value' => 3, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'allow_target_creation' => array( + 'type' => 'bool', + 'description' => 'Displays the + button on external keys to create target objects', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + // Levels that trigger a confirmation in the CSV import/synchro wizard + 'csv_import_min_object_confirmation' => array( + 'type' => 'integer', + 'description' => 'Minimum number of objects to check for the confirmation percentages', + 'default' => 3, + 'value' => 3, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_errors_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of errors that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_modifications_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of modifications that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_creations_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of creations that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'access_mode' => array( + 'type' => 'integer', + 'description' => 'Combination of flags (ACCESS_USER_WRITE | ACCESS_ADMIN_WRITE, or ACCESS_FULL)', + 'default' => ACCESS_FULL, + 'value' => ACCESS_FULL, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'access_message' => array( + 'type' => 'string', + 'description' => 'Message displayed to the users when there is any access restriction', + 'default' => 'iTop is temporarily frozen, please wait... (the admin team)', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'online_help' => array( + 'type' => 'string', + 'description' => 'Hyperlink to the online-help web page', + 'default' => 'http://www.combodo.com/itop-help', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + ); + + public function IsProperty($sPropCode) + { + return (array_key_exists($sPropCode, $this->m_aSettings)); + } + + public function Set($sPropCode, $value, $sSourceDesc = 'unknown') + { + $sType = $this->m_aSettings[$sPropCode]['type']; + switch($sType) + { + case 'bool': + $value = (bool) $value; + break; + case 'string': + $value = (string) $value; + break; + case 'integer': + $value = (integer) $value; + break; + case 'float': + $value = (float) $value; + break; + default: + throw new CoreException('Unknown type for setting', array('property' => $sPropCode, 'type' => $sType)); + } + $this->m_aSettings[$sPropCode]['value'] = $value; + $this->m_aSettings[$sPropCode]['source_of_value'] = $sSourceDesc; + + } + + public function Get($sPropCode) + { + return $this->m_aSettings[$sPropCode]['value']; + } + + // Those variables will be deprecated later, when the transition to ...Get('my_setting') will be done + protected $m_sDBHost; + protected $m_sDBUser; + protected $m_sDBPwd; + protected $m_sDBName; + protected $m_sDBSubname; + protected $m_sDBCharacterSet; + protected $m_sDBCollation; + + /** + * Event log options (see LOG_... definition) + */ + // Those variables will be deprecated later, when the transition to ...Get('my_setting') will be done + protected $m_bLogGlobal; + protected $m_bLogNotification; + protected $m_bLogIssue; + protected $m_bLogWebService; + protected $m_bLogKpiDuration; // private setting + protected $m_bLogKpiMemory; // private setting + protected $m_bDebugQueries; // private setting + protected $m_bQueryCacheEnabled; // private setting + + /** + * @var integer Number of elements to be displayed when there are more than m_iMaxDisplayLimit elements + */ + protected $m_iMinDisplayLimit; + /** + * @var integer Max number of elements before truncating the display + */ + protected $m_iMaxDisplayLimit; + + /** + * @var integer Number of seconds between two reloads of the display (standard) + */ + protected $m_iStandardReloadInterval; + /** + * @var integer Number of seconds between two reloads of the display (fast) + */ + protected $m_iFastReloadInterval; + + /** + * @var boolean Whether or not a secure connection is required for using the application. + * If set, any attempt to connect to an iTop page with http:// will be redirected + * to https:// + */ + protected $m_bSecureConnectionRequired; + + /** + * @var boolean Forces iTop to output hyperlinks starting with https:// even + * if the current page is not using https. This can be useful when + * the application runs behind a SSL gateway + */ + protected $m_bHttpsHyperlinks; + + /** + * @var string Langage code, default if the user language is undefined + */ + protected $m_sDefaultLanguage; + + /** + * @var string Type of login process allowed: form|basic|url|external + */ + protected $m_sAllowedLoginTypes; + + /** + * @var string Name of the PHP variable in which external authentication information is passed by the web server + */ + protected $m_sExtAuthVariable; + + /** + * @var string Encryption key used for all attributes of type "encrypted string". Can be set to a random value + * unless you want to import a database from another iTop instance, in which case you must use + * the same encryption key in order to properly decode the encrypted fields + */ + protected $m_sEncryptionKey; + + /** + * @var array Additional character sets to be supported by the interactive CSV import + * 'iconv_code' => 'display name' + */ + protected $m_aCharsets; + + public function __construct($sConfigFile, $bLoadConfig = true) + { + $this->m_sFile = $sConfigFile; + $this->m_aAppModules = array( + // Some default modules, always present can be move to an official iTop Module later if needed + 'application/transaction.class.inc.php', + 'application/menunode.class.inc.php', + 'application/user.preferences.class.inc.php', + 'application/audit.rule.class.inc.php', +// Romain - That's dirty, because those 3 classes are in fact part of the core +// but I needed those classes to be derived from cmdbAbstractObject +// (to be managed via the GUI) and this class in not really known from +// the core, PLUS I needed the includes to be there also for the setup +// to create the tables. + 'core/event.class.inc.php', + 'core/action.class.inc.php', + 'core/trigger.class.inc.php', + ); + $this->m_aDataModels = array(); + $this->m_aWebServiceCategories = array( + 'webservices/webservices.basic.php', + ); + $this->m_aAddons = array( + // Default AddOn, always present can be moved to an official iTop Module later if needed + 'user rights' => 'addons/userrights/userrightsprofile.class.inc.php', + ); + $this->m_aDictionaries = array( + // Default dictionaries, always present can be moved to an official iTop Module later if needed + 'dictionaries/dictionary.itop.core.php', + 'dictionaries/dictionary.itop.ui.php', // Support for English + 'dictionaries/fr.dictionary.itop.ui.php', // Support for French + 'dictionaries/fr.dictionary.itop.core.php', // Support for French + 'dictionaries/es_cr.dictionary.itop.ui.php', // Support for Spanish (from Costa Rica) + 'dictionaries/es_cr.dictionary.itop.core.php', // Support for Spanish (from Costa Rica) + 'dictionaries/de.dictionary.itop.ui.php', // Support for German + 'dictionaries/de.dictionary.itop.core.php', // Support for German + 'dictionaries/pt_br.dictionary.itop.ui.php', // Support for Brazilian Portuguese + 'dictionaries/pt_br.dictionary.itop.core.php', // Support for Brazilian Portuguese + 'dictionaries/ru.dictionary.itop.ui.php', // Support for Russian + 'dictionaries/ru.dictionary.itop.core.php', // Support for Russian + 'dictionaries/tr.dictionary.itop.ui.php', // Support for Turkish + 'dictionaries/tr.dictionary.itop.core.php', // Support for Turkish + 'dictionaries/zh.dictionary.itop.ui.php', // Support for Chinese + 'dictionaries/zh.dictionary.itop.core.php', // Support for Chinese + ); + foreach($this->m_aSettings as $sPropCode => $aSettingInfo) + { + $this->m_aSettings[$sPropCode]['value'] = $aSettingInfo['default']; + } + + $this->m_sDBHost = ''; + $this->m_sDBUser = ''; + $this->m_sDBPwd = ''; + $this->m_sDBName = ''; + $this->m_sDBSubname = ''; + $this->m_sDBCharacterSet = DEFAULT_CHARACTER_SET; + $this->m_sDBCollation = DEFAULT_COLLATION; + $this->m_bLogGlobal = DEFAULT_LOG_GLOBAL; + $this->m_bLogNotification = DEFAULT_LOG_NOTIFICATION; + $this->m_bLogIssue = DEFAULT_LOG_ISSUE; + $this->m_bLogWebService = DEFAULT_LOG_WEB_SERVICE; + $this->m_bLogKPIDuration = DEFAULT_LOG_KPI_DURATION; + $this->m_bLogKPIDuration = DEFAULT_LOG_KPI_DURATION; + $this->m_iMinDisplayLimit = DEFAULT_MIN_DISPLAY_LIMIT; + $this->m_iMaxDisplayLimit = DEFAULT_MAX_DISPLAY_LIMIT; + $this->m_iStandardReloadInterval = DEFAULT_STANDARD_RELOAD_INTERVAL; + $this->m_iFastReloadInterval = DEFAULT_FAST_RELOAD_INTERVAL; + $this->m_bSecureConnectionRequired = DEFAULT_SECURE_CONNECTION_REQUIRED; + $this->m_bHttpsHyperlinks = DEFAULT_HTTPS_HYPERLINKS; + $this->m_sDefaultLanguage = 'EN US'; + $this->m_sAllowedLoginTypes = DEFAULT_ALLOWED_LOGIN_TYPES; + $this->m_sExtAuthVariable = DEFAULT_EXT_AUTH_VARIABLE; + $this->m_sEncryptionKey = DEFAULT_ENCRYPTION_KEY; + $this->m_aCharsets = array(); + + $this->m_aModuleSettings = array(); + + if ($bLoadConfig) + { + $this->Load($sConfigFile); + $this->Verify(); + } + } + + protected function CheckFile($sPurpose, $sFileName) + { + if (!file_exists($sFileName)) + { + throw new ConfigException("Could not find $sPurpose file", array('file' => $sFileName)); + } + } + + protected function Load($sConfigFile) + { + $this->CheckFile('configuration', $sConfigFile); + + $sConfigCode = trim(file_get_contents($sConfigFile)); + + // This does not work on several lines + // preg_match('/^<\\?php(.*)\\?'.'>$/', $sConfigCode, $aMatches)... + // So, I've implemented a solution suggested in the PHP doc (search for phpWrapper) + try + { + ob_start(); + eval('?'.'>'.trim($sConfigCode)); + $sNoise = trim(ob_get_contents()); + ob_end_clean(); + } + catch (Exception $e) + { + // well, never reach in case of parsing error :-( + // will be improved in PHP 6 ? + throw new ConfigException('Error in configuration file', array('file' => $sConfigFile, 'error' => $e->getMessage())); + } + if (strlen($sNoise) > 0) + { + // Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack) + throw new ConfigException('Syntax error in configuration file', array('file' => $sConfigFile, 'error' => ''.htmlentities($sNoise).'')); + } + + if (!isset($MySettings) || !is_array($MySettings)) + { + throw new ConfigException('Missing array in configuration file', array('file' => $sConfigFile, 'expected' => '$MySettings')); + } + if (!isset($MyModules) || !is_array($MyModules)) + { + throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules')); + } + if (!array_key_exists('application', $MyModules)) + { + throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules[\'application\']')); + } + if (!array_key_exists('business', $MyModules)) + { + throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules[\'business\']')); + } + if (!array_key_exists('addons', $MyModules)) + { + throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules[\'addons\']')); + } + if (!array_key_exists('user rights', $MyModules['addons'])) + { + // Add one, by default + $MyModules['addons']['user rights'] = '/addons/userrights/userrightsnull.class.inc.php'; + } + if (!array_key_exists('dictionaries', $MyModules)) + { + throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules[\'dictionaries\']')); + } + $this->m_aAppModules = $MyModules['application']; + $this->m_aDataModels = $MyModules['business']; + if (isset($MyModules['webservices'])) + { + $this->m_aWebServiceCategories = $MyModules['webservices']; + } + $this->m_aAddons = $MyModules['addons']; + $this->m_aDictionaries = $MyModules['dictionaries']; + + foreach($MySettings as $sPropCode => $rawvalue) + { + if ($this->IsProperty($sPropCode)) + { + $value = trim($rawvalue); + $this->Set($sPropCode, $value, $sConfigFile); + } + } + + $this->m_sDBHost = trim($MySettings['db_host']); + $this->m_sDBUser = trim($MySettings['db_user']); + $this->m_sDBPwd = trim($MySettings['db_pwd']); + $this->m_sDBName = trim($MySettings['db_name']); + $this->m_sDBSubname = trim($MySettings['db_subname']); + + $this->m_sDBCharacterSet = isset($MySettings['db_character_set']) ? trim($MySettings['db_character_set']) : DEFAULT_CHARACTER_SET; + $this->m_sDBCollation = isset($MySettings['db_collation']) ? trim($MySettings['db_collation']) : DEFAULT_COLLATION; + + $this->m_bLogGlobal = isset($MySettings['log_global']) ? (bool) trim($MySettings['log_global']) : DEFAULT_LOG_GLOBAL; + $this->m_bLogNotification = isset($MySettings['log_notification']) ? (bool) trim($MySettings['log_notification']) : DEFAULT_LOG_NOTIFICATION; + $this->m_bLogIssue = isset($MySettings['log_issue']) ? (bool) trim($MySettings['log_issue']) : DEFAULT_LOG_ISSUE; + $this->m_bLogWebService = isset($MySettings['log_web_service']) ? (bool) trim($MySettings['log_web_service']) : DEFAULT_LOG_WEB_SERVICE; + $this->m_bLogKPIDuration = isset($MySettings['log_kpi_duration']) ? (bool) trim($MySettings['log_kpi_duration']) : DEFAULT_LOG_KPI_DURATION; + $this->m_bLogKPIMemory = isset($MySettings['log_kpi_memory']) ? (bool) trim($MySettings['log_kpi_memory']) : DEFAULT_LOG_KPI_MEMORY; + $this->m_bDebugQueries = isset($MySettings['debug_queries']) ? (bool) trim($MySettings['debug_queries']) : DEFAULT_DEBUG_QUERIES; + $this->m_bQueryCacheEnabled = isset($MySettings['query_cache_enabled']) ? (bool) trim($MySettings['query_cache_enabled']) : DEFAULT_QUERY_CACHE_ENABLED; + + $this->m_iMinDisplayLimit = isset($MySettings['min_display_limit']) ? trim($MySettings['min_display_limit']) : DEFAULT_MIN_DISPLAY_LIMIT; + $this->m_iMaxDisplayLimit = isset($MySettings['max_display_limit']) ? trim($MySettings['max_display_limit']) : DEFAULT_MAX_DISPLAY_LIMIT; + $this->m_iStandardReloadInterval = isset($MySettings['standard_reload_interval']) ? trim($MySettings['standard_reload_interval']) : DEFAULT_STANDARD_RELOAD_INTERVAL; + $this->m_iFastReloadInterval = isset($MySettings['fast_reload_interval']) ? trim($MySettings['fast_reload_interval']) : DEFAULT_FAST_RELOAD_INTERVAL; + $this->m_bSecureConnectionRequired = isset($MySettings['secure_connection_required']) ? (bool) trim($MySettings['secure_connection_required']) : DEFAULT_SECURE_CONNECTION_REQUIRED; + $this->m_bHttpsHyperlinks = isset($MySettings['https_hyperlinks']) ? (bool) trim($MySettings['https_hyperlinks']) : DEFAULT_HTTPS_HYPERLINKS; + + $this->m_aModuleSettings = isset($MyModuleSettings) ? $MyModuleSettings : array(); + + $this->m_sDefaultLanguage = isset($MySettings['default_language']) ? trim($MySettings['default_language']) : 'EN US'; + $this->m_sAllowedLoginTypes = isset($MySettings['allowed_login_types']) ? trim($MySettings['allowed_login_types']) : DEFAULT_ALLOWED_LOGIN_TYPES; + $this->m_sExtAuthVariable = isset($MySettings['ext_auth_variable']) ? trim($MySettings['ext_auth_variable']) : DEFAULT_EXT_AUTH_VARIABLE; + $this->m_sEncryptionKey = isset($MySettings['encryption_key']) ? trim($MySettings['encryption_key']) : DEFAULT_ENCRYPTION_KEY; + $this->m_aCharsets = isset($MySettings['csv_import_charsets']) ? $MySettings['csv_import_charsets'] : array(); + } + + protected function Verify() + { + // Files are verified later on, just before using them -see MetaModel::Plugin() + // (we have their final path at that point) + } + + public function GetModuleSetting($sModule, $sProperty, $defaultvalue = null) + { + if (isset($this->m_aModuleSettings[$sModule][$sProperty])) + { + return $this->m_aModuleSettings[$sModule][$sProperty]; + } + return $defaultvalue; + } + + public function SetModuleSetting($sModule, $sProperty, $value) + { + $this->m_aModuleSettings[$sModule][$sProperty] = $value; + } + + public function GetAppModules() + { + return $this->m_aAppModules; + } + public function SetAppModules($aAppModules) + { + $this->m_aAppModules = $aAppModules; + } + + public function GetDataModels() + { + return $this->m_aDataModels; + } + public function SetDataModels($aDataModels) + { + $this->m_aDataModels = $aDataModels; + } + + public function GetWebServiceCategories() + { + return $this->m_aWebServiceCategories; + } + public function SetWebServiceCategories($aWebServiceCategories) + { + $this->m_aWebServiceCategories = $aWebServiceCategories; + } + + public function GetAddons() + { + return $this->m_aAddons; + } + public function SetAddons($aAddons) + { + $this->m_aAddons = $aAddons; + } + + public function GetDictionaries() + { + return $this->m_aDictionaries; + } + public function SetDictionaries($aDictionaries) + { + $this->m_aDictionaries = $aDictionaries; + } + + public function GetDBHost() + { + return $this->m_sDBHost; + } + + public function GetDBName() + { + return $this->m_sDBName; + } + + public function GetDBSubname() + { + return $this->m_sDBSubname; + } + + public function GetDBCharacterSet() + { + return $this->m_sDBCharacterSet; + } + + public function GetDBCollation() + { + return $this->m_sDBCollation; + } + + public function GetDBUser() + { + return $this->m_sDBUser; + } + + public function GetDBPwd() + { + return $this->m_sDBPwd; + } + + public function GetLogGlobal() + { + return $this->m_bLogGlobal; + } + + public function GetLogNotification() + { + return $this->m_bLogNotification; + } + + public function GetLogIssue() + { + return $this->m_bLogIssue; + } + + public function GetLogWebService() + { + return $this->m_bLogWebService; + } + + public function GetLogKPIDuration() + { + return $this->m_bLogKPIDuration; + } + + public function GetLogKPIMemory() + { + return $this->m_bLogKPIMemory; + } + + public function GetDebugQueries() + { + return $this->m_bDebugQueries; + } + + public function GetQueryCacheEnabled() + { + return $this->m_bQueryCacheEnabled; + } + + public function GetMinDisplayLimit() + { + return $this->m_iMinDisplayLimit; + } + + public function GetMaxDisplayLimit() + { + return $this->m_iMaxDisplayLimit; + } + + public function GetStandardReloadInterval() + { + return $this->m_iStandardReloadInterval; + } + + public function GetFastReloadInterval() + { + return $this->m_iFastReloadInterval; + } + + public function GetSecureConnectionRequired() + { + return $this->m_bSecureConnectionRequired; + } + + public function GetHttpsHyperlinks() + { + return $this->m_bHttpsHyperlinks; + } + + public function GetDefaultLanguage() + { + return $this->m_sDefaultLanguage; + } + + public function GetEncryptionKey() + { + return $this->m_sEncryptionKey; + } + + public function GetAllowedLoginTypes() + { + return explode('|', $this->m_sAllowedLoginTypes); + } + + public function GetExternalAuthenticationVariable() + { + return $this->m_sExtAuthVariable; + } + + public function GetCSVImportCharsets() + { + return $this->m_aCharsets; + } + + public function SetDBHost($sDBHost) + { + $this->m_sDBHost = $sDBHost; + } + + public function SetDBName($sDBName) + { + $this->m_sDBName = $sDBName; + } + + public function SetDBSubname($sDBSubName) + { + $this->m_sDBSubname = $sDBSubName; + } + + public function SetDBCharacterSet($sDBCharacterSet) + { + $this->m_sDBCharacterSet = $sDBCharacterSet; + } + + public function SetDBCollation($sDBCollation) + { + $this->m_sDBCollation = $sDBCollation; + } + + public function SetDBUser($sUser) + { + $this->m_sDBUser = $sUser; + } + + public function SetDBPwd($sPwd) + { + $this->m_sDBPwd = $sPwd; + } + + public function SetLogGlobal($iLogGlobal) + { + $this->m_iLogGlobal = $iLogGlobal; + } + + public function SetLogNotification($iLogNotification) + { + $this->m_iLogNotification = $iLogNotification; + } + + public function SetLogIssue($iLogIssue) + { + $this->m_iLogIssue = $iLogIssue; + } + + public function SetLogWebService($iLogWebService) + { + $this->m_iLogWebService = $iLogWebService; + } + + public function SetMinDisplayLimit($iMinDisplayLimit) + { + $this->m_iMinDisplayLimit = $iMinDisplayLimit; + } + + public function SetMaxDisplayLimit($iMaxDisplayLimit) + { + $this->m_iMaxDisplayLimit = $iMaxDisplayLimit; + } + + public function SetStandardReloadInterval($iStandardReloadInterval) + { + $this->m_iStandardReloadInterval = $iStandardReloadInterval; + } + + public function SetFastReloadInterval($iFastReloadInterval) + { + $this->m_iFastReloadInterval = $iFastReloadInterval; + } + + public function SetSecureConnectionRequired($bSecureConnectionRequired) + { + $this->m_bSecureConnectionRequired = $bSecureConnectionRequired; + } + + public function SetHttpsHyperlinks($bHttpsHyperlinks) + { + $this->m_bHttpsHyperlinks = $bHttpsHyperlinks; + } + + public function SetDefaultLanguage($sLanguageCode) + { + $this->m_sDefaultLanguage = $sLanguageCode; + } + + public function SetAllowedLoginTypes($aAllowedLoginTypes) + { + $this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes); + } + + public function SetExternalAuthenticationVariable($sExtAuthVariable) + { + $this->m_sExtAuthVariable = $sExtAuthVariable; + } + + public function SetEncryptionKey($sKey) + { + $this->m_sEncryptionKey = $sKey; + } + + public function SetCSVImportCharsets($aCharsets) + { + $this->m_aCharsets = $aCharsets; + } + + public function AddCSVImportCharset($sIconvCode, $sDisplayName) + { + $this->m_aCharsets[$sIconvCode] = $sDisplayName; + } + public function FileIsWritable() + { + return is_writable($this->m_sFile); + } + public function GetLoadedFile() + { + return $this->m_sFile; + } + + /** + * Render the configuration as an associative array + * @return boolean True otherwise throws an Exception + */ + public function ToArray() + { + $aSettings = array(); + foreach($this->m_aSettings as $sPropCode => $aSettingInfo) + { + $aSettings[$sPropCode] = $aSettingInfo['value']; + } + $aSettings['db_host'] = $this->m_sDBHost; + $aSettings['db_user'] = $this->m_sDBUser; + $aSettings['db_pwd'] = $this->m_sDBPwd; + $aSettings['db_name'] = $this->m_sDBName; + $aSettings['db_subname'] = $this->m_sDBSubname; + $aSettings['db_character_set'] = $this->m_sDBCharacterSet; + $aSettings['db_collation'] = $this->m_sDBCollation; + $aSettings['log_global'] = $this->m_bLogGlobal; + $aSettings['log_notification'] = $this->m_bLogNotification; + $aSettings['log_issue'] = $this->m_bLogIssue; + $aSettings['log_web_service'] = $this->m_bLogWebService; + $aSettings['min_display_limit'] = $this->m_iMinDisplayLimit; + $aSettings['max_display_limit'] = $this->m_iMaxDisplayLimit; + $aSettings['standard_reload_interval'] = $this->m_iStandardReloadInterval; + $aSettings['fast_reload_interval'] = $this->m_iFastReloadInterval; + $aSettings['secure_connection_required'] = $this->m_bSecureConnectionRequired; + $aSettings['https_hyperlinks'] = $this->m_bHttpsHyperlinks; + $aSettings['default_language'] = $this->m_sDefaultLanguage; + $aSettings['allowed_login_types'] = $this->m_sAllowedLoginTypes; + $aSettings['encryption_key'] = $this->m_sEncryptionKey; + $aSettings['csv_import_charsets'] = $this->m_aCharsets; + + foreach ($this->m_aModuleSettings as $sModule => $aProperties) + { + foreach ($aProperties as $sProperty => $value) + { + $aSettings['module_settings'][$sModule][$sProperty] = $value; + } + } + foreach($this->m_aAppModules as $sFile) + { + $aSettings['application_list'][] = $sFile; + } + foreach($this->m_aDataModels as $sFile) + { + $aSettings['datamodel_list'][] = $sFile; + } + foreach($this->m_aWebServiceCategories as $sFile) + { + $aSettings['webservice_list'][] = $sFile; + } + foreach($this->m_aAddons as $sKey => $sFile) + { + $aSettings['addon_list'][] = $sFile; + } + foreach($this->m_aDictionaries as $sFile) + { + $aSettings['dictionary_list'][] = $sFile; + } + return $aSettings; + } + + /** + * Write the configuration to a file (php format) that can be reloaded later + * By default write to the same file that was specified when constructing the object + * @param $sFileName string Name of the file to write to (emtpy to write to the same file) + * @return boolean True otherwise throws an Exception + */ + public function WriteToFile($sFileName = '') + { + if (empty($sFileName)) + { + $sFileName = $this->m_sFile; + } + $hFile = @fopen($sFileName, 'w'); + if ($hFile !== false) + { + fwrite($hFile, "m_aSettings as $sPropCode => $aSettingInfo) + { + if ($aSettingInfo['show_in_conf_sample']) + { + $sType = $this->m_aSettings[$sPropCode]['type']; + switch($sType) + { + case 'bool': + $sSeenAs = $aSettingInfo['value'] ? '1' : '0'; + break; + default: + $sSeenAs = "'".$aSettingInfo['value']."'"; + } + fwrite($hFile, "\t'$sPropCode' => $sSeenAs,\n"); + } + } + fwrite($hFile, "\t'db_host' => '{$this->m_sDBHost}',\n"); + fwrite($hFile, "\t'db_user' => '{$this->m_sDBUser}',\n"); + fwrite($hFile, "\t'db_pwd' => '".addslashes($this->m_sDBPwd)."',\n"); + fwrite($hFile, "\t'db_name' => '{$this->m_sDBName}',\n"); + fwrite($hFile, "\t'db_subname' => '{$this->m_sDBSubname}',\n"); + fwrite($hFile, "\t'db_character_set' => '{$this->m_sDBCharacterSet}',\n"); + fwrite($hFile, "\t'db_collation' => '{$this->m_sDBCollation}',\n"); + fwrite($hFile, "\n"); + fwrite($hFile, "\t'log_global' => {$this->m_bLogGlobal},\n"); + fwrite($hFile, "\t'log_notification' => {$this->m_bLogNotification},\n"); + fwrite($hFile, "\t'log_issue' => {$this->m_bLogIssue},\n"); + fwrite($hFile, "\t'log_web_service' => {$this->m_bLogWebService},\n"); + fwrite($hFile, "\t'min_display_limit' => {$this->m_iMinDisplayLimit},\n"); + fwrite($hFile, "\t'max_display_limit' => {$this->m_iMaxDisplayLimit},\n"); + fwrite($hFile, "\t'standard_reload_interval' => {$this->m_iStandardReloadInterval},\n"); + fwrite($hFile, "\t'fast_reload_interval' => {$this->m_iFastReloadInterval},\n"); + fwrite($hFile, "\t'secure_connection_required' => ".($this->m_bSecureConnectionRequired ? 'true' : 'false').",\n"); + fwrite($hFile, "\t'https_hyperlinks' => ".($this->m_bHttpsHyperlinks ? 'true' : 'false').",\n"); + fwrite($hFile, "\t'default_language' => '{$this->m_sDefaultLanguage}',\n"); + fwrite($hFile, "\t'allowed_login_types' => '{$this->m_sAllowedLoginTypes}',\n"); + fwrite($hFile, "\t'encryption_key' => '{$this->m_sEncryptionKey}',\n"); + $sExport = var_export($this->m_aCharsets, true); + fwrite($hFile, "\t'csv_import_charsets' => $sExport,\n"); + + fwrite($hFile, ");\n"); + + fwrite($hFile, "\n"); + fwrite($hFile, "\$MyModuleSettings = array(\n"); + foreach ($this->m_aModuleSettings as $sModule => $aProperties) + { + fwrite($hFile, "\t'$sModule' => array (\n"); + foreach ($aProperties as $sProperty => $value) + { + $sExport = var_export($value, true); + fwrite($hFile, "\t\t'$sProperty' => $sExport,\n"); + } + fwrite($hFile, "\t),\n"); + } + fwrite($hFile, ");\n"); + + fwrite($hFile, "\n/**\n"); + fwrite($hFile, " *\n"); + fwrite($hFile, " * Data model modules to be loaded. Names should be specified as absolute paths\n"); + fwrite($hFile, " *\n"); + fwrite($hFile, " */\n"); + fwrite($hFile, "\$MyModules = array(\n"); + fwrite($hFile, "\t'application' => array (\n"); + foreach($this->m_aAppModules as $sFile) + { + fwrite($hFile, "\t\t'$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, "\t'business' => array (\n"); + foreach($this->m_aDataModels as $sFile) + { + fwrite($hFile, "\t\t'$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, "\t'webservices' => array (\n"); + foreach($this->m_aWebServiceCategories as $sFile) + { + fwrite($hFile, "\t\t'$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, "\t'addons' => array (\n"); + foreach($this->m_aAddons as $sKey => $sFile) + { + fwrite($hFile, "\t\t'$sKey' => '$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, "\t'dictionaries' => array (\n"); + foreach($this->m_aDictionaries as $sFile) + { + fwrite($hFile, "\t\t'$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, ");\n"); + fwrite($hFile, '?'.'>'); // Avoid perturbing the syntax highlighting ! + return fclose($hFile); + } + else + { + throw new ConfigException("Could not write to configuration file", array('file' => $sFileName)); + } + } +} +?> diff --git a/core/coreexception.class.inc.php b/core/coreexception.class.inc.php new file mode 100644 index 0000000000..039a388141 --- /dev/null +++ b/core/coreexception.class.inc.php @@ -0,0 +1,114 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + + +class CoreException extends Exception +{ + public function __construct($sIssue, $aContextData = null, $sImpact = '') + { + $this->m_sIssue = $sIssue; + $this->m_sImpact = $sImpact; + $this->m_aContextData = $aContextData ? $aContextData : array(); + + $sMessage = $sIssue; + if (!empty($sImpact)) $sMessage .= "($sImpact)"; + if (count($this->m_aContextData) > 0) + { + $sMessage .= ": "; + $aContextItems = array(); + foreach($this->m_aContextData as $sKey => $value) + { + if (is_array($value)) + { + $aPairs = array(); + foreach($value as $key => $val) + { + if (is_array($val)) + { + $aPairs[] = $key.'=>('.implode(', ', $val).')'; + } + else + { + $aPairs[] = $key.'=>'.$val; + } + } + $sValue = '{'.implode('; ', $aPairs).'}'; + } + else + { + $sValue = $value; + } + $aContextItems[] = "$sKey = $sValue"; + } + $sMessage .= implode(', ', $aContextItems); + } + parent::__construct($sMessage, 0); + } + + public function getHtmlDesc($sHighlightHtmlBegin = '', $sHighlightHtmlEnd = '') + { + return $this->getMessage(); + } + + public function getTraceAsHtml() + { + $aBackTrace = $this->getTrace(); + return MyHelpers::get_callstack_html(0, $this->getTrace()); + // return "
\n".$this->getTraceAsString()."
\n"; + } + + public function addInfo($sKey, $value) + { + $this->m_aContextData[$sKey] = $value; + } + + public function getIssue() + { + return $this->m_sIssue; + } + public function getImpact() + { + return $this->m_sImpact; + } + public function getContextData() + { + return $this->m_aContextData; + } +} + +class CoreWarning extends CoreException +{ +} + +class CoreUnexpectedValue extends CoreException +{ +} + +class SecurityException extends CoreException +{ +} + +?> diff --git a/core/csvparser.class.inc.php b/core/csvparser.class.inc.php new file mode 100644 index 0000000000..75b7b16a99 --- /dev/null +++ b/core/csvparser.class.inc.php @@ -0,0 +1,243 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +class CSVParserException extends CoreException +{ +} + +define('stSTARTING', 1); //grey zone: the type is undetermined +define('stRAW', 2); //building a non-qualified string +define('stQUALIFIED', 3); //building qualified string +define('stESCAPED', 4); //just encountered an escape char + +define('evBLANK', 0); +define('evSEPARATOR', 1); +define('evNEWLINE', 2); +define('evTEXTQUAL', 3); // used for escaping as well +define('evOTHERCHAR', 4); +define('evEND', 5); + + +/** + * CSVParser + * + * @package iTopORM + */ +class CSVParser +{ + private $m_sCSVData; + private $m_sSep; + private $m_sTextQualifier; + + public function __construct($sTxt, $sSep = ',', $sTextQualifier = '"') + { + $this->m_sCSVData = str_replace("\r\n", "\n", $sTxt); + $this->m_sSep = $sSep; + $this->m_sTextQualifier = $sTextQualifier; + } + + protected $m_sCurrCell = ''; + protected $m_aCurrRow = array(); + protected $m_iToSkip = 0; + protected $m_aDataSet = array(); + + protected function __AddChar($c) + { + $this->m_sCurrCell .= $c; + } + protected function __ClearCell() + { + $this->m_sCurrCell = ''; + } + protected function __AddCell($c = null, $aFieldMap = null, $bTrimSpaces = false) + { + if ($bTrimSpaces) + { + $sCell = trim($this->m_sCurrCell); + } + else + { + $sCell = $this->m_sCurrCell; + } + + if (!is_null($aFieldMap)) + { + $iNextCol = count($this->m_aCurrRow); + $iNextName = $aFieldMap[$iNextCol]; + $this->m_aCurrRow[$iNextName] = $sCell; + } + else + { + $this->m_aCurrRow[] = $sCell; + } + $this->m_sCurrCell = ''; + } + protected function __AddRow($c = null, $aFieldMap = null, $bTrimSpaces = false) + { + $this->__AddCell($c, $aFieldMap, $bTrimSpaces); + + if ($this->m_iToSkip > 0) + { + $this->m_iToSkip--; + } + elseif (count($this->m_aCurrRow) > 1) + { + $this->m_aDataSet[] = $this->m_aCurrRow; + } + elseif (count($this->m_aCurrRow) == 1) + { + // Get the unique value + $aValues = array_values($this->m_aCurrRow); + $sValue = $aValues[0]; + if (strlen($sValue) > 0) + { + $this->m_aDataSet[] = $this->m_aCurrRow; + } + } + else + { + // blank line, skip silently + } + $this->m_aCurrRow = array(); + } + protected function __AddCellTrimmed($c = null, $aFieldMap = null) + { + $this->__AddCell($c, $aFieldMap, true); + } + + protected function __AddRowTrimmed($c = null, $aFieldMap = null) + { + $this->__AddRow($c, $aFieldMap, true); + } + + function ToArray($iToSkip = 1, $aFieldMap = null, $iMax = 0) + { + $aTransitions = array(); + + $aTransitions[stSTARTING][evBLANK] = array('', stSTARTING); + $aTransitions[stSTARTING][evSEPARATOR] = array('__AddCell', stSTARTING); + $aTransitions[stSTARTING][evNEWLINE] = array('__AddRow', stSTARTING); + $aTransitions[stSTARTING][evTEXTQUAL] = array('', stQUALIFIED); + $aTransitions[stSTARTING][evOTHERCHAR] = array('__AddChar', stRAW); + $aTransitions[stSTARTING][evEND] = array('__AddRow', stSTARTING); + + $aTransitions[stRAW][evBLANK] = array('__AddChar', stRAW); + $aTransitions[stRAW][evSEPARATOR] = array('__AddCellTrimmed', stSTARTING); + $aTransitions[stRAW][evNEWLINE] = array('__AddRowTrimmed', stSTARTING); + $aTransitions[stRAW][evTEXTQUAL] = array('__AddChar', stRAW); + $aTransitions[stRAW][evOTHERCHAR] = array('__AddChar', stRAW); + $aTransitions[stRAW][evEND] = array('__AddRowTrimmed', stSTARTING); + + $aTransitions[stQUALIFIED][evBLANK] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evSEPARATOR] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evNEWLINE] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evTEXTQUAL] = array('', stESCAPED); + $aTransitions[stQUALIFIED][evOTHERCHAR] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evEND] = array('__AddRow', stSTARTING); + + $aTransitions[stESCAPED][evBLANK] = array('', stESCAPED); + $aTransitions[stESCAPED][evSEPARATOR] = array('__AddCell', stSTARTING); + $aTransitions[stESCAPED][evNEWLINE] = array('__AddRow', stSTARTING); + $aTransitions[stESCAPED][evTEXTQUAL] = array('__AddChar', stQUALIFIED); + $aTransitions[stESCAPED][evOTHERCHAR] = array('__AddChar', stSTARTING); + $aTransitions[stESCAPED][evEND] = array('__AddRow', stSTARTING); + + // Reset parser variables + $this->m_sCurrCell = ''; + $this->m_aCurrRow = array(); + $this->m_iToSkip = $iToSkip; + $this->m_aDataSet = array(); + + $iDataLength = strlen($this->m_sCSVData); + + $iState = stSTARTING; + for($i = 0; $i <= $iDataLength ; $i++) + { + if ($i == $iDataLength) + { + $iEvent = evEND; + } + else + { + $c = $this->m_sCSVData[$i]; + + if ($c == $this->m_sSep) + { + $iEvent = evSEPARATOR; + } + elseif ($c == ' ') + { + $iEvent = evBLANK; + } + elseif ($c == "\t") + { + $iEvent = evBLANK; + } + elseif ($c == "\n") + { + $iEvent = evNEWLINE; + } + elseif ($c == $this->m_sTextQualifier) + { + $iEvent = evTEXTQUAL; + } + else + { + $iEvent = evOTHERCHAR; + } + } + + $sAction = $aTransitions[$iState][$iEvent][0]; + $iState = $aTransitions[$iState][$iEvent][1]; + + if (!empty($sAction)) + { + $aCallSpec = array($this, $sAction); + if (is_callable($aCallSpec)) + { + call_user_func($aCallSpec, $c, $aFieldMap); + } + else + { + throw new CSVParserException("CSVParser: unknown verb '$sAction'"); + } + } + + $iLineCount = count($this->m_aDataSet); + if (($iMax > 0) && ($iLineCount >= $iMax)) break; + } + return $this->m_aDataSet; + } + + public function ListFields() + { + $aHeader = $this->ToArray(0, null, 1); + return $aHeader[0]; + } +} + + +?> diff --git a/core/data.generator.class.inc.php b/core/data.generator.class.inc.php new file mode 100644 index 0000000000..417ba58aa6 --- /dev/null +++ b/core/data.generator.class.inc.php @@ -0,0 +1,373 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +/** + * Data Generator helper class + * + * This class is useful to generate a lot of sample data that look consistent + * for a given organization in order to simulate a real CMDB + */ +class cmdbDataGenerator +{ + protected $m_sOrganizationKey; + protected $m_sOrganizationCode; + protected $m_sOrganizationName; + protected $m_OrganizationDomains; + + /** + * Constructor + */ + public function __construct($sOrganizationId = "") + { + global $aCompanies, $aCompaniesCode; + if ($sOrganizationId == '') + { + // No organization provided, pick a random and unused one from our predefined list + $retries = 5*count($aCompanies); + while ( ($retries > 0) && !isset($this->m_sOrganizationCode)) // Stupid algorithm, but I'm too lazy to do something bulletproof tonight + { + $index = rand(0, count($aCompanies) - 1); + if (!$this->OrganizationExists($aCompanies[$index]['code'])) + { + $this->m_sOrganizationCode = $aCompanies[$index]['code']; + $this->m_sOrganizationName = $aCompanies[$index]['name']; + $this->m_OrganizationDomains = $aCompanies[$index]['domain']; + } + $retries--; + } + } + else + { + // A code has been provided, let's take the information we need from the organization itself + $this->m_sOrganizationId = $sOrganizationId; + $oOrg = $this->GetOrganization($sOrganizationId); + if ($oOrg == null) + { + echo "Unable to find the organization '$sOrganisationCode' in the database... can not add objects into this organization.
\n"; + exit(); + } + $this->m_sOrganizationCode = $oOrg->Get('code'); + $this->m_sOrganizationName = $oOrg->Get('name'); + if (!isset($aCompaniesCode[$this->m_sOrganizationCode]['domain'])) + { + // Generate some probable domain names for this organization + $this->m_OrganizationDomains = array(strtolower($this->m_sOrganizationCode).".com", strtolower($this->m_sOrganizationCode).".org", strtolower($this->m_sOrganizationCode)."corp.net",); + } + else + { + // Pick the domain names for this organization from the predefined list + $this->m_OrganizationDomains = $aCompaniesCode[$this->m_sOrganizationCode]['domain']; + } + } + + if (!isset($this->m_sOrganizationCode)) + { + echo "Unable to find an organization code which is not already used... can not create a new organization. Enhance the list of fake organizations (\$aCompanies in data_sample.inc.php).
\n"; + exit(); + } + } + + /** + * Get the current organization id used by the generator + * + * @return string The organization id + */ + public function GetOrganizationId() + { + return $this->m_sOrganizationId; + } + + /** + * Get the current organization id used by the generator + * + * @param string The organization id + * @return none + */ + public function SetOrganizationId($sId) + { + $this->m_sOrganizationId = $sId; + } + + /** + * Get the current organization code used by the generator + * + * @return string The organization code + */ + public function GetOrganizationCode() + { + return $this->m_sOrganizationCode; + } + + /** + * Get the current organization name used by the generator + * + * @return string The organization name + */ + function GetOrganizationName() + { + return $this->m_sOrganizationName; + } + + /** + * Get a pseudo random first name taken from a (big) prefedined list + * + * @return string A random first name + */ + function GenerateFirstName() + { + global $aFirstNames; + return $aFirstNames[rand(0, count($aFirstNames) - 1)]; + } + + /** + * Get a pseudo random last name taken from a (big) prefedined list + * + * @return string A random last name + */ + function GenerateLastName() + { + global $aNames; + return $aNames[rand(0, count($aNames) - 1)]; + } + + /** + * Get a pseudo random country name taken from a prefedined list + * + * @return string A random city name + */ + function GenerateCountryName() + { + global $aCountries; + return $aCountries[rand(0, count($aCountries) - 1)]; + } + + /** + * Get a pseudo random city name taken from a (big) prefedined list + * + * @return string A random city name + */ + function GenerateCityName() + { + global $aCities; + return $aCities[rand(0, count($aCities) - 1)]; + } + + /** + * Get a pseudo random email address made of the first name, last name and organization's domain + * + * @return string A random email address + */ + function GenerateEmail($sFirstName, $sLastName) + { + if (rand(1, 20) > 18) + { + // some people (let's say 5~10%) have an irregular email address + $sEmail = strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); + } + else + { + $sEmail = strtolower($this->CleanForEmail($sFirstName)).".".strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); + } + return $sEmail; + } + + /** + * Generate (pseudo) random strings that follow a given pattern + * + * The template is made of any number of 'parts' separated by pipes '|' + * Each part is either: + * - domain() => returns a domain name for the current organization + * - enum(aaa,bb,c,dddd) => returns randomly one of aaa,bb,c or dddd with the same + * probability of occurence. If you want to change the probability you can repeat some values + * i.e enum(most probable,most probable,most probable,most probable,most probable,rare) + * - number(xxx-yyy) => a random number between xxx and yyy (bounds included) + * note that if the first number (xxx) begins with a zero, then the result will zero padded + * to the same number of digits as xxx. + * All other 'part' that does not follow one of the above mentioned pattern is returned as is + * + * Example: GenerateString("enum(sw,rtr,gw)|number(00-99)|.|domain()") + * will produce strings like "sw01.netcmdb.com" or "rtr45.itop.org" + * + * @param string $sTemplate The template used for generating the string + * @return string The generated pseudo random the string + */ + function GenerateString($sTemplate) + { + $sResult = ""; + $aParts = explode("\|", $sTemplate); + foreach($aParts as $sPart) + { + if (preg_match("/domain\(\)/", $sPart, $aMatches)) + { + $sResult .= strtolower($this->GenerateDomain()); + } + elseif (preg_match("/enum\((.+)\)/", $sPart, $aMatches)) + { + $sEnumValues = $aMatches[1]; + $aEnumValues = explode(",", $sEnumValues); + $sResult .= $aEnumValues[rand(0, count($aEnumValues) - 1)]; + } + elseif (preg_match("/number\((\d+)-(\d+)\)/", $sPart, $aMatches)) + { + $sStartNumber = $aMatches[1]; + if ($sStartNumber[0] == '0') + { + // number must be zero padded + $sFormat = "%0".strlen($sStartNumber)."d"; + } + else + { + $sFormat = "%d"; + } + $sEndNumber = $aMatches[2]; + $sResult .= sprintf($sFormat, rand($sStartNumber, $sEndNumber)); + } + else + { + $sResult .= $sPart; + } + } + return $sResult; + } + + /** + * Generate a foreign key by picking a random element of the given class in a set limited by the given search criteria + * + * Example: GenerateKey("bizLocation", array('org_id', $oGenerator->GetOrganizationId()); + * will produce the foreign key of a Location object picked at random in the same organization + * + * @param string $sClass The name of the class to search for + * @param string $aFilterCriteria A hash array of filterCOde => FilterValue (the strict operator '=' is used ) + * @return mixed The key to an object of the given class, or null if none are found + */ + function GenerateKey($sClass, $aFilterCriteria) + { + $retKey = null; + $oFilter = new CMDBSearchFilter($sClass); + foreach($aFilterCriteria as $sFilterCode => $filterValue) + { + $oFilter->AddCondition($sFilterCode, $filterValue, '='); + } + $oSet = new CMDBObjectSet($oFilter); + if ($oSet->Count() > 0) + { + $max_count = $index = rand(1, $oSet->Count()); + do + { + $oObj = $oSet->Fetch(); + $index--; + } + while($index > 0); + + if (!is_object($oObj)) + { + echo "
";
+				echo "ERROR: non empty set, but invalid object picked! class='$sClass'\n";
+				echo "Index chosen: $max_count\n";
+				echo "The set is supposed to contain ".$oSet->Count()." object(s)\n";
+				echo "Filter criteria:\n";
+				print_r($aFilterCriteria);
+				echo "
"; + } + else + { + $retKey = $oObj->GetKey(); + } + } + return $retKey; + } + /////////////////////////////////////////////////////////////////////////////// + // + // Protected methods + // + /////////////////////////////////////////////////////////////////////////////// + + /** + * Generate a (random) domain name consistent with the organization name & code + * + * The values are pulled from a (limited) predefined list. Note that a given + * organization may have several domain names, so the result may be random + * + * @return string A domain name (like netcnmdb.com) + */ + protected function GenerateDomain() + { + if (is_array($this->m_OrganizationDomains)) + { + $sDomain = $this->m_OrganizationDomains[rand(0, count($this->m_OrganizationDomains)-1)]; + } + else + { + $sDomain = $this->m_OrganizationDomains; + } + return $sDomain; + } + + /** + * Strips accented characters from a string in order to produce a suitable email address + * + * @param string The text string to clean + * @return string The cleanified text string + */ + protected function CleanForEmail($sText) + { + return str_replace(array("'", "é", "è", "ê", "ç", "à", "â", "ñ", "ö", "ä"), array("", "e", "e", "e", "c", "a", "a", "n", "oe", "ae"), $sText); + } + + /** + * Check if an organization with the given code already exists in the database + * + * @param string $sCode The code to look for + * @return boolean true if the given organization exists, false otherwise + */ + protected function OrganizationExists($sCode) + { + $oFilter = new CMDBSearchFilter('bizOrganization'); + $oFilter->AddCondition('code', $sCode, '='); + $oSet = new CMDBObjectSet($oFilter); + return ($oSet->Count() > 0); + } + + /** + * Search for an organization with the given code in the database + * + * @param string $Id The organization Id to look for + * @return cmdbOrganization the organization if it exists, null otherwise + */ + protected function GetOrganization($sId) + { + $oOrg = null; + $oFilter = new CMDBSearchFilter('bizOrganization'); + $oFilter->AddCondition('id', $sId, '='); + $oSet = new CMDBObjectSet($oFilter); + if ($oSet->Count() > 0) + { + $oOrg = $oSet->Fetch(); // Let's take the first one found + } + return $oOrg; + } +} +?> diff --git a/core/dbobject.class.php b/core/dbobject.class.php new file mode 100644 index 0000000000..4bc995740c --- /dev/null +++ b/core/dbobject.class.php @@ -0,0 +1,1352 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once('metamodel.class.php'); + +/** + * A persistent object, as defined by the metamodel + * + * @package iTopORM + */ +abstract class DBObject +{ + private static $m_aMemoryObjectsByClass = array(); + + private $m_bIsInDB = false; // true IIF the object is mapped to a DB record + private $m_iKey = null; + private $m_aCurrValues = array(); + protected $m_aOrigValues = array(); + + private $m_bDirty = false; // Means: "a modification is ongoing" + // The object may have incorrect external keys, then any attempt of reload must be avoided + private $m_bCheckStatus = null; // Means: the object has been verified and is consistent with integrity rules + // if null, then the check has to be performed again to know the status + protected $m_aCheckIssues = null; + protected $m_aAsArgs = null; // The current object as a standard argument (cache) + + private $m_bFullyLoaded = false; // Compound objects can be partially loaded + private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode + + // Use the MetaModel::NewObject to build an object (do we have to force it?) + public function __construct($aRow = null, $sClassAlias = '') + { + if (!empty($aRow)) + { + $this->FromRow($aRow, $sClassAlias); + $this->m_bFullyLoaded = $this->IsFullyLoaded(); + return; + } + // Creation of brand new object + // + + $this->m_iKey = self::GetNextTempId(get_class($this)); + + // set default values + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue(); + $this->m_aOrigValues[$sAttCode] = null; + if ($oAttDef->IsExternalField()) + { + // This field has to be read from the DB + $this->m_aLoadedAtt[$sAttCode] = false; + } + else + { + // No need to trigger a reload for that attribute + // Let's consider it as being already fully loaded + $this->m_aLoadedAtt[$sAttCode] = true; + } + } + } + + // Read-only <=> Written once (archive) + public function RegisterAsDirty() + { + // While the object may be written to the DB, it is NOT possible to reload it + // or at least not possible to reload it the same way + $this->m_bDirty = true; + } + + public function IsNew() + { + return (!$this->m_bIsInDB); + } + + // Returns an Id for memory objects + static protected function GetNextTempId($sClass) + { + if (!array_key_exists($sClass, self::$m_aMemoryObjectsByClass)) + { + self::$m_aMemoryObjectsByClass[$sClass] = 0; + } + self::$m_aMemoryObjectsByClass[$sClass]++; + return (- self::$m_aMemoryObjectsByClass[$sClass]); + } + + public function __toString() + { + $sRet = ''; + $sClass = get_class($this); + $sRootClass = MetaModel::GetRootClass($sClass); + $iPKey = $this->GetKey(); + $sRet .= "$sClass::$iPKey
\n"; + $sRet .= "
    \n"; + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + $sRet .= "
  • ".$oAttDef->GetLabel()." = ".$this->GetAsHtml($sAttCode)."
  • \n"; + } + $sRet .= "
"; + return $sRet; + } + + // Restore initial values... mmmm, to be discussed + public function DBRevert() + { + $this->Reload(); + } + + protected function IsFullyLoaded() + { + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + @$bIsLoaded = $this->m_aLoadedAtt[$sAttCode]; + if ($bIsLoaded !== true) + { + return false; + } + } + return true; + } + + protected function Reload() + { + assert($this->m_bIsInDB); + $aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey); + if (empty($aRow)) + { + throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey); + } + $this->FromRow($aRow); + + // Process linked set attributes + // + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + if (!$oAttDef->IsLinkSet()) continue; + + // Load the link information + $sLinkClass = $oAttDef->GetLinkedClass(); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + + // The class to target is not the current class, because if this is a derived class, + // it may differ from the target class, then things start to become confusing + $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe); + $sMyClass = $oRemoteExtKeyAtt->GetTargetClass(); + + $oMyselfSearch = new DBObjectSearch($sMyClass); + $oMyselfSearch->AddCondition('id', $this->m_iKey, '='); + + $oLinkSearch = new DBObjectSearch($sLinkClass); + $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe); + $oLinks = new DBObjectSet($oLinkSearch); + + $this->m_aCurrValues[$sAttCode] = $oLinks; + $this->m_aOrigValues[$sAttCode] = clone $this->m_aCurrValues[$sAttCode]; + $this->m_aLoadedAtt[$sAttCode] = true; + } + + $this->m_bFullyLoaded = true; + } + + protected function FromRow($aRow, $sClassAlias = '') + { + if (strlen($sClassAlias) == 0) + { + // Default to the current class + $sClassAlias = get_class($this); + } + + $this->m_iKey = null; + $this->m_bIsInDB = true; + $this->m_aCurrValues = array(); + $this->m_aOrigValues = array(); + $this->m_aLoadedAtt = array(); + $this->m_bCheckStatus = true; + + // Get the key + // + $sKeyField = $sClassAlias."id"; + if (!array_key_exists($sKeyField, $aRow)) + { + // #@# Bug ? + throw new CoreException("Missing key for class '".get_class($this)."'"); + } + else + { + $iPKey = $aRow[$sKeyField]; + if (!self::IsValidPKey($iPKey)) + { + throw new CoreWarning("An object id must be an integer value ($iPKey)"); + } + $this->m_iKey = $iPKey; + } + + // Build the object from an array of "attCode"=>"value") + // + $bFullyLoaded = true; // ... set to false if any attribute is not found + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + // Say something, whatever the type of attribute + $this->m_aLoadedAtt[$sAttCode] = false; + + // Skip links (could not be loaded by the mean of this query) + if ($oAttDef->IsLinkSet()) continue; + + // Note: we assume that, for a given attribute, if it can be loaded, + // then one column will be found with an empty suffix, the others have a suffix + // Take care: the function isset will return false in case the value is null, + // which is something that could happen on open joins + $sAttRef = $sClassAlias.$sAttCode; + if (array_key_exists($sAttRef, $aRow)) + { + $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); + + $this->m_aCurrValues[$sAttCode] = $value; + $this->m_aOrigValues[$sAttCode] = $value; + $this->m_aLoadedAtt[$sAttCode] = true; + } + else + { + // This attribute was expected and not found in the query columns + $bFullyLoaded = false; + } + } + return $bFullyLoaded; + } + + public function Set($sAttCode, $value) + { + if ($sAttCode == 'finalclass') + { + // Ignore it - this attribute is set upon object creation and that's it + return; + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty) + { + // First time Set is called... ensure that the object gets fully loaded + // Otherwise we would lose the values on a further Reload + // + consistency does not make sense ! + $this->Reload(); + } + + if ($oAttDef->IsExternalKey() && is_object($value)) + { + // Setting an external key with a whole object (instead of just an ID) + // let's initialize also the external fields that depend on it + // (useful when building objects in memory and not from a query) + if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass()))) + { + throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored"); + } + else + { + // The object has changed, reset caches + $this->m_bCheckStatus = null; + $this->m_aAsArgs = null; + + $this->m_aCurrValues[$sAttCode] = $value->GetKey(); + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) + { + if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) + { + $this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode()); + } + } + } + return; + } + if(!$oAttDef->IsScalar() && !is_object($value)) + { + throw new CoreUnexpectedValue("scalar not allowed for attribute '$sAttCode', setting default value (empty list)"); + } + if($oAttDef->IsLinkSet()) + { + if((get_class($value) != 'DBObjectSet') && !is_subclass_of($value, 'DBObjectSet')) + { + throw new CoreUnexpectedValue("expecting a set of persistent objects (found a '".get_class($value)."'), setting default value (empty list)"); + } + + $oObjectSet = $value; + $sSetClass = $oObjectSet->GetClass(); + $sLinkClass = $oAttDef->GetLinkedClass(); + // not working fine :-( if (!is_subclass_of($sSetClass, $sLinkClass)) + if ($sSetClass != $sLinkClass) + { + throw new CoreUnexpectedValue("expecting a set of '$sLinkClass' objects (found a set of '$sSetClass'), setting default value (empty list)"); + } + } + + $realvalue = $oAttDef->MakeRealValue($value); + $this->m_aCurrValues[$sAttCode] = $realvalue; + + // The object has changed, reset caches + $this->m_bCheckStatus = null; + $this->m_aAsArgs = null; + + // Make sure we do not reload it anymore... before saving it + $this->RegisterAsDirty(); + } + + public function Get($sAttCode) + { + if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this)))) + { + throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); + } + if ($this->m_bIsInDB && !$this->m_aLoadedAtt[$sAttCode] && !$this->m_bDirty) + { + // #@# non-scalar attributes.... handle that differently + $this->Reload(); + } + return $this->m_aCurrValues[$sAttCode]; + } + + public function GetOriginal($sAttCode) + { + if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this)))) + { + throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); + } + return $this->m_aOrigValues[$sAttCode]; + } + + /** + * Updates the value of an external field by (re)loading the object + * corresponding to the external key and getting the value from it + * @param string $sAttCode Attribute code of the external field to update + * @return void + */ + protected function UpdateExternalField($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsExternalField()) + { + $sTargetClass = $oAttDef->GetTargetClass(); + $objkey = $this->Get($oAttDef->GetKeyAttCode()); + $oObj = MetaModel::GetObject($sTargetClass, $objkey); + if (is_object($oObj)) + { + $value = $oObj->Get($oAttDef->GetExtAttCode()); + $this->Set($sAttCode, $value); + } + } + } + + // Compute scalar attributes that depend on any other type of attribute + public function DoComputeValues() + { + if (is_callable(array($this, 'ComputeValues'))) + { + // First check that we are not currently computing the fields + // (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...) + foreach (debug_backtrace() as $aCallInfo) + { + if (!array_key_exists("class", $aCallInfo)) continue; + if ($aCallInfo["class"] != get_class($this)) continue; + if ($aCallInfo["function"] != "ComputeValues") continue; + return; //skip! + } + + $this->ComputeValues(); + } + } + + public function GetAsHTML($sAttCode) + { + $sClass = get_class($this); + $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); + + $aExtKeyFriends = MetaModel::GetExtKeyFriends($sClass, $sAttCode); + if (count($aExtKeyFriends) > 0) + { + // This attribute is an ext key (in this class or in another class) + // The corresponding value is an id of the remote object + // Let's try to use the corresponding external fields for a sexy display + + $aAvailableFields = array(); + foreach ($aExtKeyFriends as $sDispAttCode => $oExtField) + { +// $aAvailableFields[$oExtField->GetExtAttCode()] = $oExtField->GetAsHTML($this->Get($oExtField->GetCode())); + $aAvailableFields[$oExtField->GetExtAttCode()] = $this->Get($oExtField->GetCode()); + } + + $sTargetClass = $oAtt->GetTargetClass(EXTKEY_ABSOLUTE); + return $this->MakeHyperLink($sTargetClass, $this->Get($sAttCode), $aAvailableFields); + } + + // That's a standard attribute (might be an ext field or a direct field, etc.) + return $oAtt->GetAsHTML($this->Get($sAttCode)); + } + + public function GetEditValue($sAttCode) + { + $sClass = get_class($this); + $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); + + if ($oAtt->IsExternalKey()) + { + $sTargetClass = $oAtt->GetTargetClass(); + if ($this->IsNew()) + { + // The current object exists only in memory, don't try to query it in the DB ! + // instead let's query for the object pointed by the external key, and get its name + $targetObjId = $this->Get($sAttCode); + $oTargetObj = MetaModel::GetObject($sTargetClass, $targetObjId, false); // false => not sure it exists + if (is_object($oTargetObj)) + { + $sEditValue = $oTargetObj->GetName(); + } + else + { + $sEditValue = 0; + } + } + else + { + $aAvailableFields = array(); + // retrieve the "external fields" linked to this external key + foreach (MetaModel::GetExternalFields(get_class($this), $sAttCode) as $oExtField) + { + $aAvailableFields[$oExtField->GetExtAttCode()] = $oExtField->GetAsHTML($this->Get($oExtField->GetCode())); + } + // Use the "name" of the target class as the label of the hyperlink + // unless it's not available in the external fields... + $sExtClassNameAtt = MetaModel::GetNameAttributeCode($sTargetClass); + if (isset($aAvailableFields[$sExtClassNameAtt])) + { + $sEditValue = $aAvailableFields[$sExtClassNameAtt]; + } + else + { + $sEditValue = implode(' / ', $aAvailableFields); + } + } + } + else + { + $sEditValue = $oAtt->GetEditValue($this->Get($sAttCode)); + } + return $sEditValue; + } + + public function GetAsXML($sAttCode) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsXML($this->Get($sAttCode)); + } + + public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"') + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier); + } + + protected static function MakeHyperLink($sObjClass, $sObjKey, $aAvailableFields) + { + if ($sObjKey == 0) return 'undefined'; + + return MetaModel::GetName($sObjClass)."::$sObjKey"; + } + + public function GetHyperlink() + { + $aAvailableFields[MetaModel::GetNameAttributeCode(get_class($this))] = $this->GetName(); + return $this->MakeHyperLink(get_class($this), $this->GetKey(), $aAvailableFields); + } + + + // could be in the metamodel ? + public static function IsValidPKey($value) + { + return ((string)$value === (string)(int)$value); + } + + public function GetKey() + { + return $this->m_iKey; + } + public function SetKey($iNewKey) + { + if (!self::IsValidPKey($iNewKey)) + { + throw new CoreException("An object id must be an integer value ($iNewKey)"); + } + + if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey)) + { + throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database"); + } + $this->m_iKey = $iNewKey; + } + /** + * Get the icon representing this object + * @param boolean $bImgTag If true the result is a full IMG tag (or an emtpy string if no icon is defined) + * @return string Either the full IMG tag ($bImgTag == true) or just the path to the icon file + */ + public function GetIcon($bImgTag = true) + { + return MetaModel::GetClassIcon(get_class($this), $bImgTag); + } + + public function GetName() + { + $sNameAttCode = MetaModel::GetNameAttributeCode(get_class($this)); + if (empty($sNameAttCode)) + { + return $this->m_iKey; + } + else + { + return $this->Get($sNameAttCode); + } + } + + public function GetState() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + return $this->Get($sStateAttCode); + } + } + + public function GetStateLabel() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + $sStateValue = $this->Get($sStateAttCode); + return MetaModel::GetStateLabel(get_class($this), $sStateValue); + } + } + + public function GetStateDescription() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + $sStateValue = $this->Get($sStateAttCode); + return MetaModel::GetStateDescription(get_class($this), $sStateValue); + } + } + /** + * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) + * for the given attribute in the current state of the object + * @param string $sAttCode The code of the attribute + * @return integer Flags: the binary combination of the flags applicable to this attribute + */ + public function GetAttributeFlags($sAttCode) + { + $iFlags = 0; // By default (if no life cycle) no flag at all + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (!empty($sStateAttCode)) + { + $iFlags = MetaModel::GetAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); + } + return $iFlags; + } + + // check if the given (or current) value is suitable for the attribute + // return true if successfull + // return the error desciption otherwise + public function CheckValue($sAttCode, $value = null) + { + if (!is_null($value)) + { + $toCheck = $value; + } + else + { + $toCheck = $this->Get($sAttCode); + } + + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if (!$oAtt->IsWritable()) + { + return true; + } + elseif ($oAtt->IsNull($toCheck)) + { + if ($oAtt->IsNullAllowed()) + { + return true; + } + else + { + return "Null not allowed"; + } + } + elseif ($oAtt->IsExternalKey()) + { + if (!MetaModel::SkipCheckExtKeys()) + { + $sTargetClass = $oAtt->GetTargetClass(); + $oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/); + if (is_null($oTargetObj)) + { + return "Target object not found ($sTargetClass::$toCheck)"; + } + } + } + elseif ($oAtt->IsScalar()) + { + $aValues = $oAtt->GetAllowedValues($this->ToArgs()); + if (count($aValues) > 0) + { + if (!array_key_exists($toCheck, $aValues)) + { + return "Value not allowed [$toCheck]"; + } + } + if (!is_null($iMaxSize = $oAtt->GetMaxSize())) + { + $iLen = strlen($toCheck); + if ($iLen > $iMaxSize) + { + return "String too long (found $iLen, limited to $iMaxSize)"; + } + } + if (!$oAtt->CheckFormat($toCheck)) + { + return "Wrong format [$toCheck]"; + } + } + return true; + } + + // check attributes together + public function CheckConsistency() + { + return true; + } + + // check integrity rules (before inserting or updating the object) + // a displayable error is returned + public function DoCheckToWrite() + { + $this->DoComputeValues(); + + $this->m_aCheckIssues = array(); + + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + $res = $this->CheckValue($sAttCode); + if ($res !== true) + { + // $res contains the error description + $this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res"; + } + } + if (count($this->m_aCheckIssues) > 0) + { + // No need to check consistency between attributes if any of them has + // an unexpected value + return; + } + $res = $this->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $this->m_aCheckIssues[] = "Consistency rules not followed: $res"; + } + } + + final public function CheckToWrite() + { + if (MetaModel::SkipCheckToWrite()) + { + return array(true, array()); + } + if (is_null($this->m_bCheckStatus)) + { + $oKPI = new ExecutionKPI(); + $this->DoCheckToWrite(); + $oKPI->ComputeStats('CheckToWrite', get_class($this)); + if (count($this->m_aCheckIssues) == 0) + { + $this->m_bCheckStatus = true; + } + else + { + $this->m_bCheckStatus = false; + } + } + return array($this->m_bCheckStatus, $this->m_aCheckIssues); + } + + // check if it is allowed to delete the existing object from the database + // a displayable error is returned + public function CheckToDelete() + { + return true; + } + + protected function ListChangedValues(array $aProposal) + { + $aDelta = array(); + foreach ($aProposal as $sAtt => $proposedValue) + { + if (!array_key_exists($sAtt, $this->m_aOrigValues)) + { + // The value was not set + $aDelta[$sAtt] = $proposedValue; + } + elseif(is_object($proposedValue)) + { + // The value is an object, the comparison is not strict + // #@# todo - should be even less strict => add verb on AttributeDefinition: Compare($a, $b) + if ($this->m_aOrigValues[$sAtt] != $proposedValue) + { + $aDelta[$sAtt] = $proposedValue; + } + } + else + { + // The value is a scalar, the comparison must be 100% strict + if($this->m_aOrigValues[$sAtt] !== $proposedValue) + { + //echo "$sAtt:
\n";
+					//var_dump($this->m_aOrigValues[$sAtt]);
+					//var_dump($proposedValue);
+					//echo "
\n"; + $aDelta[$sAtt] = $proposedValue; + } + } + } + return $aDelta; + } + + // List the attributes that have been changed + // Returns an array of attname => currentvalue + public function ListChanges() + { + return $this->ListChangedValues($this->m_aCurrValues); + } + + // Tells whether or not an object was modified + public function IsModified() + { + $aChanges = $this->ListChanges(); + return (count($aChanges) != 0); + } + + // used both by insert/update + private function DBWriteLinks() + { + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + if (!$oAttDef->IsLinkSet()) continue; + + $oLinks = $this->Get($sAttCode); + $oLinks->Rewind(); + while ($oLinkedObject = $oLinks->Fetch()) + { + $oLinkedObject->Set($oAttDef->GetExtKeyToMe(), $this->m_iKey); + if ($oLinkedObject->IsModified()) + { + $oLinkedObject->DBWrite(); + } + } + + // Delete the objects that were initialy present and disappeared from the list + // (if any) + $oOriginalSet = $this->m_aOrigValues[$sAttCode]; + if ($oOriginalSet != null) + { + $aOriginalList = $oOriginalSet->ToArray(); + $aNewSet = $oLinks->ToArray(); + + foreach($aOriginalList as $iId => $oObject) + { + if (!array_key_exists($iId, $aNewSet)) + { + // It disappeared from the list + $oObject->DBDelete(); + } + } + } + } + } + + private function DBInsertSingleTable($sTableClass) + { + $sTable = MetaModel::DBGetTable($sTableClass); + // Abstract classes or classes having no specific attribute do not have an associated table + if ($sTable == '') return; + + $sClass = get_class($this); + + // fields in first array, values in the second + $aFieldsToWrite = array(); + $aValuesToWrite = array(); + + if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) + { + // Add it to the list of fields to write + $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; + $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); + } + + foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; + $aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]); + foreach($aAttColumns as $sColumn => $sValue) + { + $aFieldsToWrite[] = "`$sColumn`"; + $aValuesToWrite[] = CMDBSource::Quote($sValue); + } + } + + if (count($aValuesToWrite) == 0) return false; + + $sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")"; + + if (MetaModel::DBIsReadOnly()) + { + $iNewKey = -1; + } + else + { + $iNewKey = CMDBSource::InsertInto($sInsertSQL); + } + // Note that it is possible to have a key defined here, and the autoincrement expected, this is acceptable in a non root class + if (empty($this->m_iKey)) + { + // Take the autonumber + $this->m_iKey = $iNewKey; + } + return $this->m_iKey; + } + + // Insert of record for the new object into the database + // Returns the key of the newly created object + public function DBInsertNoReload() + { + if ($this->m_bIsInDB) + { + throw new CoreException("The object already exists into the Database, you may want to use the clone function"); + } + + $sClass = get_class($this); + $sRootClass = MetaModel::GetRootClass($sClass); + + // Ensure the update of the values (we are accessing the data directly) + $this->DoComputeValues(); + $this->OnInsert(); + + if ($this->m_iKey < 0) + { + // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it! + $this->m_iKey = null; + } + + // If not automatically computed, then check that the key is given by the caller + if (!MetaModel::IsAutoIncrementKey($sRootClass)) + { + if (empty($this->m_iKey)) + { + throw new CoreWarning("Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber", array('class' => $sRootClass)); + } + } + + // Ultimate check - ensure DB integrity + list($bRes, $aIssues) = $this->CheckToWrite(); + if (!$bRes) + { + throw new CoreException("Object not following integrity rules - it will not be written into the DB", array('class' => $sClass, 'id' => $this->GetKey(), 'issues' => $aIssues)); + } + + // First query built upon on the root class, because the ID must be created first + $this->m_iKey = $this->DBInsertSingleTable($sRootClass); + + // Then do the leaf class, if different from the root class + if ($sClass != $sRootClass) + { + $this->DBInsertSingleTable($sClass); + } + + // Then do the other classes + foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) + { + if ($sParentClass == $sRootClass) continue; + $this->DBInsertSingleTable($sParentClass); + } + + $this->DBWriteLinks(); + $this->m_bIsInDB = true; + $this->m_bDirty = false; + + // Arg cache invalidated (in particular, it needs the object key -could be improved later) + $this->m_aAsArgs = null; + + $this->AfterInsert(); + + // 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; + } + + public function DBInsert() + { + $this->DBInsertNoReload(); + $this->Reload(); + return $this->m_iKey; + } + + public function DBInsertTracked(CMDBChange $oVoid) + { + return $this->DBInsert(); + } + + // Creates a copy of the current object into the database + // Returns the id of the newly created object + public function DBClone($iNewKey = null) + { + $this->m_bIsInDB = false; + $this->m_iKey = $iNewKey; + return $this->DBInsert(); + } + + /** + * This function is automatically called after cloning an object with the "clone" PHP language construct + * The purpose of this method is to reset the appropriate attributes of the object in + * order to make sure that the newly cloned object is really distinct from its clone + */ + public function __clone() + { + $this->m_bIsInDB = false; + $this->m_bDirty = true; + $this->m_iKey = self::GetNextTempId(get_class($this)); + } + + // Update a record + public function DBUpdate() + { + if (!$this->m_bIsInDB) + { + throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead"); + } + + $this->DoComputeValues(); + $this->OnUpdate(); + + $aChanges = $this->ListChanges(); + if (count($aChanges) == 0) + { + //throw new CoreWarning("Attempting to update an unchanged object"); + return; + } + + // Ultimate check - ensure DB integrity + list($bRes, $aIssues) = $this->CheckToWrite(); + if (!$bRes) + { + throw new CoreException("Object not following integrity rules - it will not be written into the DB", array('class' => get_class($this), 'id' => $this->GetKey(), 'issues' => $aIssues)); + } + + $bHasANewExternalKeyValue = false; + foreach($aChanges as $sAttCode => $valuecurr) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true; + if (!$oAttDef->IsDirectField()) unset($aChanges[$sAttCode]); + } + + // Update scalar attributes + if (count($aChanges) != 0) + { + $oFilter = new DBObjectSearch(get_class($this)); + $oFilter->AddCondition('id', $this->m_iKey, '='); + + $sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges); + if (!MetaModel::DBIsReadOnly()) + { + CMDBSource::Query($sSQL); + } + } + + $this->DBWriteLinks(); + $this->m_bDirty = false; + + $this->AfterUpdate(); + + // Reload to get the external attributes + if ($bHasANewExternalKeyValue) + { + $this->Reload(); + } + + return $this->m_iKey; + } + + public function DBUpdateTracked(CMDBChange $oVoid) + { + return $this->DBUpdate(); + } + + // Make the current changes persistent - clever wrapper for Insert or Update + public function DBWrite() + { + if ($this->m_bIsInDB) + { + return $this->DBUpdate(); + } + else + { + return $this->DBInsert(); + } + } + + // Delete a record + public function DBDelete() + { + $oFilter = new DBObjectSearch(get_class($this)); + $oFilter->AddCondition('id', $this->m_iKey, '='); + + $this->OnDelete(); + + $sSQL = MetaModel::MakeDeleteQuery($oFilter); + if (!MetaModel::DBIsReadOnly()) + { + CMDBSource::Query($sSQL); + } + + $this->AfterDelete(); + + $this->m_bIsInDB = false; + $this->m_iKey = null; + } + + public function DBDeleteTracked(CMDBChange $oVoid) + { + $this->DBDelete(); + } + + public function EnumTransitions() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) return array(); + + $sState = $this->Get(MetaModel::GetStateAttributeCode(get_class($this))); + return MetaModel::EnumTransitions(get_class($this), $sState); + } + + public function ApplyStimulus($sStimulusCode) + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) return false; + + MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this))); + + $aStateTransitions = $this->EnumTransitions(); + $aTransitionDef = $aStateTransitions[$sStimulusCode]; + + // Change the state before proceeding to the actions, this is necessary because an action might + // trigger another stimuli (alternative: push the stimuli into a queue) + $sPreviousState = $this->Get($sStateAttCode); + $sNewState = $aTransitionDef['target_state']; + $this->Set($sStateAttCode, $sNewState); + + // $aTransitionDef is an + // array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD + + $bSuccess = true; + foreach ($aTransitionDef['actions'] as $sActionHandler) + { + // std PHP spec + $aActionCallSpec = array($this, $sActionHandler); + + if (!is_callable($aActionCallSpec)) + { + throw new CoreException("Unable to call action: ".get_class($this)."::$sActionHandler"); + return; + } + $bRet = call_user_func($aActionCallSpec, $sStimulusCode); + // if one call fails, the whole is considered as failed + if (!$bRet) $bSuccess = false; + } + + // Change state triggers... + $sClass = get_class($this); + $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sPreviousState'")); + while ($oTrigger = $oSet->Fetch()) + { + $oTrigger->DoActivate($this->ToArgs('this')); + } + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sNewState'")); + while ($oTrigger = $oSet->Fetch()) + { + $oTrigger->DoActivate($this->ToArgs('this')); + } + + return $bSuccess; + } + + // Make standard context arguments + // Note: Needs to be reviewed because it is currently called once per attribute when an object is written (CheckToWrite / CheckValue) + // Several options here: + // 1) cache the result + // 2) set only the object ref and resolve the values iif needed from contextual templates and queries (easy for the queries, not for the templates) + public function ToArgs($sArgName = 'this') + { + if (is_null($this->m_aAsArgs)) + { + $oKPI = new ExecutionKPI(); + $aScalarArgs = array(); + $aScalarArgs[$sArgName] = $this->GetKey(); + $aScalarArgs[$sArgName.'->id'] = $this->GetKey(); + $aScalarArgs[$sArgName.'->object()'] = $this; + $aScalarArgs[$sArgName.'->hyperlink()'] = $this->GetHyperlink(); + // #@# Prototype for a user portal - to be dehardcoded later + $sToPortal = utils::GetAbsoluteUrlPath().'../portal/index.php?operation=details&id='.$this->GetKey(); + $aScalarArgs[$sArgName.'->hyperlink(portal)'] = '
'.$this->GetName().''; + $aScalarArgs[$sArgName.'->name()'] = $this->GetName(); + + $sClass = get_class($this); + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + $aScalarArgs[$sArgName.'->'.$sAttCode] = $this->Get($sAttCode); + if ($oAttDef->IsScalar()) + { + // #@# Note: This has been proven to be quite slow, this can slow down bulk load + $sAsHtml = $this->GetAsHtml($sAttCode); + $aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = $sAsHtml; + $aScalarArgs[$sArgName.'->label('.$sAttCode.')'] = strip_tags($sAsHtml); + } + } + $this->m_aAsArgs = $aScalarArgs; + $oKPI->ComputeStats('ToArgs', get_class($this)); + } + return $this->m_aAsArgs; + } + + // To be optionaly overloaded + protected function OnInsert() + { + } + + // To be optionaly overloaded + protected function AfterInsert() + { + } + + // To be optionaly overloaded + protected function OnUpdate() + { + } + + // To be optionaly overloaded + protected function AfterUpdate() + { + } + + // To be optionaly overloaded + protected function OnDelete() + { + } + + // To be optionaly overloaded + protected function AfterDelete() + { + } + + // Return an empty set for the parent of all + public static function GetRelationQueries($sRelCode) + { + return array(); + } + + public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array()) + { + foreach (MetaModel::EnumRelationQueries(get_class($this), $sRelCode) as $sDummy => $aQueryInfo) + { + MetaModel::DbgTrace("object=".$this->GetKey().", depth=$iMaxDepth, rel=".$aQueryInfo["sQuery"]); + $sQuery = $aQueryInfo["sQuery"]; + $bPropagate = $aQueryInfo["bPropagate"]; + $iDistance = $aQueryInfo["iDistance"]; + + $iDepth = $bPropagate ? $iMaxDepth - 1 : 0; + + $oFlt = DBObjectSearch::FromOQL($sQuery); + $oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgs()); + while ($oObj = $oObjSet->Fetch()) + { + $sRootClass = MetaModel::GetRootClass(get_class($oObj)); + $sObjKey = $oObj->GetKey(); + if (array_key_exists($sRootClass, $aResults)) + { + if (array_key_exists($sObjKey, $aResults[$sRootClass])) + { + continue; // already visited, skip + } + } + + $aResults[$sRootClass][$sObjKey] = $oObj; + if ($iDepth > 0) + { + $oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults); + } + } + } + return $aResults; + } + + public function GetReferencingObjects() + { + $aDependentObjects = array(); + $aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this)); + foreach($aRererencingMe as $sRemoteClass => $aExtKeys) + { + foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) + { + // skip if this external key is behind an external field + if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue; + + $oSearch = new DBObjectSearch($sRemoteClass); + $oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '='); + $oSet = new CMDBObjectSet($oSearch); + if ($oSet->Count() > 0) + { + $aDependentObjects[$sRemoteClass][$sExtKeyAttCode] = array( + 'attribute' => $oExtKeyAttDef, + 'objects' => $oSet, + ); + } + } + } + return $aDependentObjects; + } + + /** + * $aDeletedObjs = array(); // [class][key] => structure + * $aResetedObjs = array(); // [class][key] => object + */ + public function GetDeletionScheme(&$aDeletedObjs, &$aResetedObjs, $aVisited = array()) + { + if (array_key_exists(get_class($this), $aVisited)) + { + if (in_array($this->GetKey(), $aVisited[get_class($this)])) + { + return; + } + } + $aVisited[get_class($this)] = $this->GetKey(); + + $aDependentObjects = $this->GetReferencingObjects(); + foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes) + { + foreach ($aPotentialDeletes as $sRemoteExtKey => $aData) + { + $oAttDef = $aData['attribute']; + $iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption(); + $oDepSet = $aData['objects']; + $oDepSet->Rewind(); + while ($oDependentObj = $oDepSet->fetch()) + { + $iId = $oDependentObj->GetKey(); + if ($oAttDef->IsNullAllowed()) + { + // Optional external key, list to reset + if (!array_key_exists($sRemoteClass, $aResetedObjs) || !array_key_exists($iId, $aResetedObjs[$sRemoteClass])) + { + $aResetedObjs[$sRemoteClass][$iId]['to_reset'] = $oDependentObj; + } + $aResetedObjs[$sRemoteClass][$iId]['attributes'][$sRemoteExtKey] = $oAttDef; + } + else + { + // Mandatory external key, list to delete + if (array_key_exists($sRemoteClass, $aDeletedObjs) && array_key_exists($iId, $aDeletedObjs[$sRemoteClass])) + { + $iCurrentOption = $aDeletedObjs[$sRemoteClass][$iId]; + if ($iCurrentOption == DEL_AUTO) + { + // be conservative, take the new option + // (DEL_MANUAL has precedence over DEL_AUTO) + $aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = ($iDeletePropagationOption == DEL_AUTO); + } + else + { + // DEL_MANUAL... leave it as is, it HAS to be verified anyway + } + } + else + { + // First time we find the given object in the list + // (and most likely case is that no other occurence will be found) + $aDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oDependentObj; + $aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = ($iDeletePropagationOption == DEL_AUTO); + // Recursively inspect this object + if ($iDeletePropagationOption == DEL_AUTO) + { + $oDependentObj->GetDeletionScheme($aDeletedObjs, $aResetedObjs, $aVisited); + } + } + } + } + } + } + } +} + + +?> diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php new file mode 100644 index 0000000000..b8d3707462 --- /dev/null +++ b/core/dbobjectsearch.class.php @@ -0,0 +1,910 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class DBObjectSearch +{ + private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters) + private $m_aSelectedClasses; // selected for the output (alias => class name) + private $m_oSearchCondition; + private $m_aParams; + private $m_aFullText; + private $m_aPointingTo; + private $m_aReferencedBy; + private $m_aRelatedTo; + + // By default, some information may be hidden to the current user + // But it may happen that we need to disable that feature + private $m_bAllowAllData = false; + + public function __construct($sClass, $sClassAlias = null) + { + if (is_null($sClassAlias)) $sClassAlias = $sClass; + assert('is_string($sClass)'); + assert('MetaModel::IsValidClass($sClass)'); // #@# could do better than an assert, or at least give the caller's reference + // => idee d'un assert avec call stack (autre utilisation = echec sur query SQL) + + $this->m_aSelectedClasses = array($sClassAlias => $sClass); + $this->m_aClasses = array($sClassAlias => $sClass); + $this->m_oSearchCondition = new TrueExpression; + $this->m_aParams = array(); + $this->m_aFullText = array(); + $this->m_aPointingTo = array(); + $this->m_aReferencedBy = array(); + $this->m_aRelatedTo = array(); + } + + public function AllowAllData() {$this->m_bAllowAllData = true;} + public function IsAllDataAllowed() {return $this->m_bAllowAllData;} + + public function GetClassName($sAlias) {return $this->m_aClasses[$sAlias];} + public function GetJoinedClasses() {return $this->m_aClasses;} + + public function GetClass() + { + return reset($this->m_aSelectedClasses); + } + public function GetClassAlias() + { + reset($this->m_aSelectedClasses); + return key($this->m_aSelectedClasses); + } + + public function GetFirstJoinedClass() + { + return reset($this->m_aClasses); + } + public function GetFirstJoinedClassAlias() + { + reset($this->m_aClasses); + return key($this->m_aClasses); + } + + public function SetSelectedClasses($aNewSet) + { + $this->m_aSelectedClasses = array(); + foreach ($aNewSet as $sAlias => $sClass) + { + if (!array_key_exists($sAlias, $this->m_aClasses)) + { + throw new CoreException('Unexpected class alias', array('alias'=>$sAlias, 'expected'=>$this->m_aClasses)); + } + $this->m_aSelectedClasses[$sAlias] = $sClass; + } + } + + public function GetSelectedClasses() + { + return $this->m_aSelectedClasses; + } + + + public function IsAny() + { + // #@# todo - if (!$this->m_oSearchCondition->IsTrue()) return false; + if (count($this->m_aFullText) > 0) return false; + if (count($this->m_aPointingTo) > 0) return false; + if (count($this->m_aReferencedBy) > 0) return false; + if (count($this->m_aRelatedTo) > 0) return false; + return true; + } + + public function Describe() + { + // To replace __Describe + } + + public function DescribeConditionPointTo($sExtKeyAttCode) + { + if (!isset($this->m_aPointingTo[$sExtKeyAttCode])) return ""; + $oFilter = $this->m_aPointingTo[$sExtKeyAttCode]; + if ($oFilter->IsAny()) return ""; + $oAtt = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode); + return $oAtt->GetLabel()." having ({$oFilter->DescribeConditions()})"; + } + + public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode) + { + if (!isset($this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode])) return ""; + $oFilter = $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]; + if ($oFilter->IsAny()) return ""; + $oAtt = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); + return "being ".$oAtt->GetLabel()." for ".$sForeignClass."s in ({$oFilter->DescribeConditions()})"; + } + + public function DescribeConditionRelTo($aRelInfo) + { + $oFilter = $aRelInfo['flt']; + $sRelCode = $aRelInfo['relcode']; + $iMaxDepth = $aRelInfo['maxdepth']; + return "related ($sRelCode... peut mieux faire !, $iMaxDepth dig depth) to a {$oFilter->GetClass()} ({$oFilter->DescribeConditions()})"; + } + + public function DescribeConditions() + { + $aConditions = array(); + + $aCondFT = array(); + foreach($this->m_aFullText as $sFullText) + { + $aCondFT[] = " contain word(s) '$sFullText'"; + } + if (count($aCondFT) > 0) + { + $aConditions[] = "which ".implode(" and ", $aCondFT); + } + + // #@# todo - review textual description of the JOIN and search condition (is that still feasible?) + $aConditions[] = $this->RenderCondition(); + + $aCondPoint = array(); + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$oFilter) + { + if ($oFilter->IsAny()) continue; + $aCondPoint[] = $this->DescribeConditionPointTo($sExtKeyAttCode); + } + if (count($aCondPoint) > 0) + { + $aConditions[] = implode(" and ", $aCondPoint); + } + + $aCondReferred= array(); + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter) + { + if ($oForeignFilter->IsAny()) continue; + $aCondReferred[] = $this->DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode); + } + } + foreach ($this->m_aRelatedTo as $aRelInfo) + { + $aCondReferred[] = $this->DescribeConditionRelTo($aRelInfo); + } + if (count($aCondReferred) > 0) + { + $aConditions[] = implode(" and ", $aCondReferred); + } + + return implode(" and ", $aConditions); + } + + public function __DescribeHTML() + { + try + { + $sConditionDesc = $this->DescribeConditions(); + } + catch (MissingQueryArgument $e) + { + $sConditionDesc = '?missing query argument?'; + } + if (!empty($sConditionDesc)) + { + return "Objects of class '".$this->GetClass()."', $sConditionDesc"; + } + return "Any object of class '".$this->GetClass()."'"; + } + + protected function TransferConditionExpression($oFilter, $aTranslation) + { + $oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false); + $this->AddConditionExpression($oTranslated); + // #@# what about collisions in parameter names ??? + $this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams); + } + + public function ResetCondition() + { + $this->m_oSearchCondition = new TrueExpression(); + // ? is that usefull/enough, do I need to rebuild the list after the subqueries ? + } + + public function AddConditionExpression($oExpression) + { + $this->m_oSearchCondition = $this->m_oSearchCondition->LogAnd($oExpression); + } + + public function AddCondition($sFilterCode, $value, $sOpCode = null) + { + MyHelpers::CheckKeyInArray('filter code', $sFilterCode, MetaModel::GetClassFilterDefs($this->GetClass())); + $oFilterDef = MetaModel::GetClassFilterDef($this->GetClass(), $sFilterCode); + + $oField = new FieldExpression($sFilterCode, $this->GetClassAlias()); + if (empty($sOpCode)) + { + if ($sFilterCode == 'id') + { + $sOpCode = '='; + } + else + { + $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); + $oNewCondition = $oAttDef->GetSmartConditionExpression($value, $oField, $this->m_aParams); + $this->AddConditionExpression($oNewCondition); + return; + } + } + MyHelpers::CheckKeyInArray('operator', $sOpCode, $oFilterDef->GetOperators()); + + // Preserve backward compatibility - quick n'dirty way to change that API semantic + // + switch($sOpCode) + { + case 'SameDay': + case 'SameMonth': + case 'SameYear': + case 'Today': + case '>|': + case '<|': + case '=|': + throw new CoreException('Deprecated operator, please consider using OQL (SQL) expressions like "(TO_DAYS(NOW()) - TO_DAYS(x)) AS AgeDays"', array('operator' => $sOpCode)); + break; + + case "IN": + if (!is_array($value)) $value = array($value); + $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; + $sOQLCondition = $oField->Render()." IN $sListExpr"; + break; + + case "NOTIN": + if (!is_array($value)) $value = array($value); + $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; + $sOQLCondition = $oField->Render()." NOT IN $sListExpr"; + break; + + case 'Contains': + $this->m_aParams[$sFilterCode] = "%$value%"; + $sOperator = 'LIKE'; + break; + + case 'Begins with': + $this->m_aParams[$sFilterCode] = "$value%"; + $sOperator = 'LIKE'; + break; + + case 'Finishes with': + $this->m_aParams[$sFilterCode] = "%$value"; + $sOperator = 'LIKE'; + break; + + default: + $this->m_aParams[$sFilterCode] = $value; + $sOperator = $sOpCode; + } + + switch($sOpCode) + { + case "IN": + case "NOTIN": + $oNewCondition = Expression::FromOQL($sOQLCondition); + break; + + case 'Contains': + case 'Begins with': + case 'Finishes with': + default: + $oRightExpr = new VariableExpression($sFilterCode); + $oNewCondition = new BinaryExpression($oField, $sOperator, $oRightExpr); + } + + $this->AddConditionExpression($oNewCondition); + } + + public function AddCondition_FullText($sFullText) + { + $this->m_aFullText[] = $sFullText; + } + + protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation) + { + $sOrigAlias = $this->GetClassAlias(); + if (array_key_exists($sOrigAlias, $aClassAliases)) + { + $sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetClass()); + $this->m_aSelectedClasses[$sNewAlias] = $this->GetClass(); + unset($this->m_aSelectedClasses[$sOrigAlias]); + + // Translate the condition expression with the new alias + $aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias; + } + + // add the alias into the filter aliases list + $aClassAliases[$this->GetClassAlias()] = $this->GetClass(); + + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$oFilter) + { + $oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); + } + + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter) + { + $oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); + } + } + } + + public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode) + { + $aAliasTranslation = array(); + $res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation); + $this->TransferConditionExpression($oFilter, $aAliasTranslation); + return $res; + } + + protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation) + { + if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode)) + { + throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}' - the condition will be ignored"); + } + $oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode); + if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass())) + { + throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); + } + + if (array_key_exists($sExtKeyAttCode, $this->m_aPointingTo)) + { + $this->m_aPointingTo[$sExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation); + } + else + { + $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); + + // #@# The condition expression found in that filter should not be used - could be another kind of structure like a join spec tree !!!! + // $oNewFilter = clone $oFilter; + // $oNewFilter->ResetCondition(); + + $this->m_aPointingTo[$sExtKeyAttCode] = $oFilter; + } + } + + public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode) + { + $aAliasTranslation = array(); + $res = $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation); + $this->TransferConditionExpression($oFilter, $aAliasTranslation); + return $res; + } + + protected function AddCondition_ReferencedBy_InNameSpace(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation) + { + $sForeignClass = $oFilter->GetClass(); + $sForeignClassAlias = $oFilter->GetClassAlias(); + if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode)) + { + throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}' - the condition will be ignored"); + } + $oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); + if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass())) + { + throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); + } + if (array_key_exists($sForeignClass, $this->m_aReferencedBy) && array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sForeignClass])) + { + $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation); + } + else + { + $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); + + // #@# The condition expression found in that filter should not be used - could be another kind of structure like a join spec tree !!!! + //$oNewFilter = clone $oFilter; + //$oNewFilter->ResetCondition(); + + $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]= $oFilter; + } + } + + public function AddCondition_LinkedTo(DBObjectSearch $oLinkFilter, $sExtKeyAttCodeToMe, $sExtKeyAttCodeTarget, DBObjectSearch $oFilterTarget) + { + $oLinkFilterFinal = clone $oLinkFilter; + $oLinkFilterFinal->AddCondition_PointingTo($sExtKeyAttCodeToMe); + + $this->AddCondition_ReferencedBy($oLinkFilterFinal, $sExtKeyAttCodeToMe); + } + + public function AddCondition_RelatedTo(DBObjectSearch $oFilter, $sRelCode, $iMaxDepth) + { + MyHelpers::CheckValueInArray('relation code', $sRelCode, MetaModel::EnumRelations()); + $this->m_aRelatedTo[] = array('flt'=>$oFilter, 'relcode'=>$sRelCode, 'maxdepth'=>$iMaxDepth); + } + + public function MergeWith($oFilter) + { + $aAliasTranslation = array(); + $res = $this->MergeWith_InNamespace($oFilter, $this->m_aClasses, $aAliasTranslation); + $this->TransferConditionExpression($oFilter, $aAliasTranslation); + return $res; + } + + protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation) + { + if ($this->GetClass() != $oFilter->GetClass()) + { + throw new CoreException("Attempting to merge a filter of class '{$this->GetClass()}' with a filter of class '{$oFilter->GetClass()}'"); + } + + // Translate search condition into our aliasing scheme + $aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); + + $this->m_aFullText = array_merge($this->m_aFullText, $oFilter->m_aFullText); + $this->m_aRelatedTo = array_merge($this->m_aRelatedTo, $oFilter->m_aRelatedTo); + + foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$oExtFilter) + { + $this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation); + } + foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $oForeignFilter) + { + $this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation); + } + } + } + + public function GetCriteria() {return $this->m_oSearchCondition;} + public function GetCriteria_FullText() {return $this->m_aFullText;} + public function GetCriteria_PointingTo($sKeyAttCode = "") + { + if (empty($sKeyAttCode)) + { + return $this->m_aPointingTo; + } + if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return null; + return $this->m_aPointingTo[$sKeyAttCode]; + } + public function GetCriteria_ReferencedBy($sRemoteClass = "", $sForeignExtKeyAttCode = "") + { + if (empty($sRemoteClass)) + { + return $this->m_aReferencedBy; + } + if (!array_key_exists($sRemoteClass, $this->m_aReferencedBy)) return null; + if (empty($sForeignExtKeyAttCode)) + { + return $this->m_aReferencedBy[$sRemoteClass]; + } + if (!array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sRemoteClass])) return null; + return $this->m_aReferencedBy[$sRemoteClass][$sForeignExtKeyAttCode]; + } + public function GetCriteria_RelatedTo() + { + return $this->m_aRelatedTo; + } + public function GetInternalParams() + { + return $this->m_aParams; + } + + public function RenderCondition() + { + return $this->m_oSearchCondition->Render($this->m_aParams, false); + } + + public function serialize($bDevelopParams = false, $aContextParams = null) + { + $sOql = $this->ToOql($bDevelopParams, $aContextParams); + return base64_encode(serialize(array($sOql, $this->m_aParams))); + } + + static public function unserialize($sValue) + { + $aData = unserialize(base64_decode($sValue)); + $sOql = $aData[0]; + $aParams = $aData[1]; + // We've tried to use gzcompress/gzuncompress, but for some specific queries + // it was not working at all (See Trac #193) + // gzuncompress was issuing a warning "data error" and the return object was null + return self::FromOQL($sOql, $aParams); + } + + // SImple BUt Structured Query Languag - SubuSQL + // + static private function Value2Expression($value) + { + $sRet = $value; + if (is_array($value)) + { + $sRet = VS_START.implode(', ', $value).VS_END; + } + else if (!is_numeric($value)) + { + $sRet = "'".addslashes($value)."'"; + } + return $sRet; + } + static private function Expression2Value($sExpr) + { + $retValue = $sExpr; + if ((substr($sExpr, 0, 1) == "'") && (substr($sExpr, -1, 1) == "'")) + { + $sNoQuotes = substr($sExpr, 1, -1); + return stripslashes($sNoQuotes); + } + if ((substr($sExpr, 0, 1) == VS_START) && (substr($sExpr, -1, 1) == VS_END)) + { + $sNoBracket = substr($sExpr, 1, -1); + $aRetValue = array(); + foreach (explode(",", $sNoBracket) as $sItem) + { + $aRetValue[] = self::Expression2Value(trim($sItem)); + } + return $aRetValue; + } + return $retValue; + } + + // Alternative to object mapping: the data are transfered directly into an array + // This is 10 times faster than creating a set of objects, and makes sense when optimization is required + public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array()) + { + $sSQL = MetaModel::MakeSelectQuery($this, $aOrderBy, $aArgs); + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return; + + if (count($aColumns) == 0) + { + $aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass())); + // Add the standard id (as first column) + array_unshift($aColumns, 'id'); + } + + $aQueryCols = CMDBSource::GetColumns($resQuery); + + $sClassAlias = $this->GetClassAlias(); + $aColMap = array(); + foreach ($aColumns as $sAttCode) + { + $sColName = $sClassAlias.$sAttCode; + if (in_array($sColName, $aQueryCols)) + { + $aColMap[$sAttCode] = $sColName; + } + } + + $aRes = array(); + while ($aRow = CMDBSource::FetchArray($resQuery)) + { + $aMappedRow = array(); + foreach ($aColMap as $sAttCode => $sColName) + { + $aMappedRow[$sAttCode] = $aRow[$sColName]; + } + $aRes[] = $aMappedRow; + } + CMDBSource::FreeResult($resQuery); + return $aRes; + } + + public function ToOQL($bDevelopParams = false, $aContextParams = null) + { + // Currently unused, but could be useful later + $bRetrofitParams = false; + + if ($bDevelopParams) + { + if (is_null($aContextParams)) + { + $aParams = array_merge($this->m_aParams); + } + else + { + $aParams = array_merge($aContextParams, $this->m_aParams); + } + } + else + { + // Leave it as is, the rendering will be made with parameters in clear + $aParams = null; + } + + $sSelectedClasses = implode(', ', array_keys($this->m_aSelectedClasses)); + $sRes = 'SELECT '.$sSelectedClasses.' FROM'; + + $sRes .= ' '.$this->GetClass().' AS '.$this->GetClassAlias(); + $sRes .= $this->ToOQL_Joins(); + $sRes .= " WHERE ".$this->m_oSearchCondition->Render($aParams, $bRetrofitParams); + + // Temporary: add more info about other conditions, necessary to avoid strange behaviors with the cache + foreach($this->m_aFullText as $sFullText) + { + $sRes .= " AND MATCHES '$sFullText'"; + } + return $sRes; + } + + protected function ToOQL_Joins() + { + $sRes = ''; + foreach($this->m_aPointingTo as $sExtKey=>$oFilter) + { + $sRes .= ' JOIN '.$oFilter->GetClass().' AS '.$oFilter->GetClassAlias().' ON '.$this->GetClassAlias().'.'.$sExtKey.' = '.$oFilter->GetClassAlias().'.id'; + $sRes .= $oFilter->ToOQL_Joins(); + } + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter) + { + $sRes .= ' JOIN '.$oForeignFilter->GetClass().' AS '.$oForeignFilter->GetClassAlias().' ON '.$oForeignFilter->GetClassAlias().'.'.$sForeignExtKeyAttCode.' = '.$this->GetClassAlias().'.id'; + $sRes .= $oForeignFilter->ToOQL_Joins(); + } + } + return $sRes; + } + + protected function OQLExpressionToCondition($sQuery, $oExpression, $aClassAliases) + { + if ($oExpression instanceof BinaryOqlExpression) + { + $sOperator = $oExpression->GetOperator(); + $oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases); + $oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases); + return new BinaryExpression($oLeft, $sOperator, $oRight); + } + elseif ($oExpression instanceof FieldOqlExpression) + { + $sClassAlias = $oExpression->GetParent(); + $sFltCode = $oExpression->GetName(); + if (empty($sClassAlias)) + { + // Try to find an alias + // Build an array of field => array of aliases + $aFieldClasses = array(); + foreach($aClassAliases as $sAlias => $sReal) + { + foreach(MetaModel::GetFiltersList($sReal) as $sAnFltCode) + { + $aFieldClasses[$sAnFltCode][] = $sAlias; + } + } + if (!array_key_exists($sFltCode, $aFieldClasses)) + { + throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), array_keys($aFieldClasses)); + } + if (count($aFieldClasses[$sFltCode]) > 1) + { + throw new OqlNormalizeException('Ambiguous filter code', $sQuery, $oExpression->GetNameDetails()); + } + $sClassAlias = $aFieldClasses[$sFltCode][0]; + } + else + { + if (!array_key_exists($sClassAlias, $aClassAliases)) + { + throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oExpression->GetParentDetails(), array_keys($aClassAliases)); + } + $sClass = $aClassAliases[$sClassAlias]; + if (!MetaModel::IsValidFilterCode($sClass, $sFltCode)) + { + throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), MetaModel::GetFiltersList($sClass)); + } + } + + return new FieldExpression($sFltCode, $sClassAlias); + } + elseif ($oExpression instanceof VariableOqlExpression) + { + return new VariableExpression($oExpression->GetName()); + } + elseif ($oExpression instanceof TrueOqlExpression) + { + return new TrueExpression; + } + elseif ($oExpression instanceof ScalarOqlExpression) + { + return new ScalarExpression($oExpression->GetValue()); + } + elseif ($oExpression instanceof ListOqlExpression) + { + $aItems = array(); + foreach ($oExpression->GetItems() as $oItemExpression) + { + $aItems[] = $this->OQLExpressionToCondition($sQuery, $oItemExpression, $aClassAliases); + } + return new ListExpression($aItems); + } + elseif ($oExpression instanceof FunctionOqlExpression) + { + $aArgs = array(); + foreach ($oExpression->GetArgs() as $oArgExpression) + { + $aArgs[] = $this->OQLExpressionToCondition($sQuery, $oArgExpression, $aClassAliases); + } + return new FunctionExpression($oExpression->GetVerb(), $aArgs); + } + elseif ($oExpression instanceof IntervalOqlExpression) + { + return new IntervalExpression($oExpression->GetValue(), $oExpression->GetUnit()); + } + else + { + throw new CoreException('Unknown expression type', array('class'=>get_class($oExpression), 'query'=>$sQuery)); + } + } + + // Create a search definition that leads to 0 result, still a valid search object + static public function FromEmptySet($sClass) + { + $oResultFilter = new DBObjectSearch($sClass); + $oResultFilter->m_oSearchCondition = new FalseExpression; + return $oResultFilter; + } + + static protected $m_aOQLQueries = array(); + + // Do not filter out depending on user rights + // In particular when we are currently in the process of evaluating the user rights... + static public function FromOQL_AllData($sQuery, $aParams = null) + { + $oRes = self::FromOQL($sQuery, $aParams); + $oRes->AllowAllData(); + return $oRes; + } + + static public function FromOQL($sQuery, $aParams = null) + { + if (empty($sQuery)) return null; + + // Query caching + $bOQLCacheEnabled = true; + if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries)) + { + // hit! + return clone self::$m_aOQLQueries[$sQuery]; + } + + $oOql = new OqlInterpreter($sQuery); + $oOqlQuery = $oOql->ParseObjectQuery(); + + $sClass = $oOqlQuery->GetClass(); + $sClassAlias = $oOqlQuery->GetClassAlias(); + + if (!MetaModel::IsValidClass($sClass)) + { + throw new OqlNormalizeException('Unknown class', $sQuery, $oOqlQuery->GetClassDetails(), MetaModel::GetClasses()); + } + + $oResultFilter = new DBObjectSearch($sClass, $sClassAlias); + $aAliases = array($sClassAlias => $sClass); + + // Maintain an array of filters, because the flat list is in fact referring to a tree + // And this will be an easy way to dispatch the conditions + // $oResultFilter will be referenced by the other filters, or the other way around... + $aJoinItems = array($sClassAlias => $oResultFilter); + + $aJoinSpecs = $oOqlQuery->GetJoins(); + if (is_array($aJoinSpecs)) + { + foreach ($aJoinSpecs as $oJoinSpec) + { + $sJoinClass = $oJoinSpec->GetClass(); + $sJoinClassAlias = $oJoinSpec->GetClassAlias(); + if (!MetaModel::IsValidClass($sJoinClass)) + { + throw new OqlNormalizeException('Unknown class', $sQuery, $oJoinSpec->GetClassDetails(), MetaModel::GetClasses()); + } + if (array_key_exists($sJoinClassAlias, $aAliases)) + { + if ($sJoinClassAlias != $sJoinClass) + { + throw new OqlNormalizeException('Duplicate class alias', $sQuery, $oJoinSpec->GetClassAliasDetails()); + } + else + { + throw new OqlNormalizeException('Duplicate class name', $sQuery, $oJoinSpec->GetClassDetails()); + } + } + + // Assumption: ext key on the left only !!! + // normalization should take care of this + $oLeftField = $oJoinSpec->GetLeftField(); + $sFromClass = $oLeftField->GetParent(); + $sExtKeyAttCode = $oLeftField->GetName(); + + $oRightField = $oJoinSpec->GetRightField(); + $sToClass = $oRightField->GetParent(); + $sPKeyDescriptor = $oRightField->GetName(); + if ($sPKeyDescriptor != 'id') + { + throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sQuery, $oRightField->GetNameDetails(), array('id')); + } + + $aAliases[$sJoinClassAlias] = $sJoinClass; + $aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias); + + if (!array_key_exists($sFromClass, $aJoinItems)) + { + throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sQuery, $oLeftField->GetParentDetails(), array_keys($aJoinItems)); + } + if (!array_key_exists($sToClass, $aJoinItems)) + { + throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sQuery, $oRightField->GetParentDetails(), array_keys($aJoinItems)); + } + $aExtKeys = array_keys(MetaModel::GetExternalKeys($aAliases[$sFromClass])); + if (!in_array($sExtKeyAttCode, $aExtKeys)) + { + throw new OqlNormalizeException('Unknown external key in join condition (left expression)', $sQuery, $oLeftField->GetNameDetails(), $aExtKeys); + } + + if ($sFromClass == $sJoinClassAlias) + { + $aJoinItems[$sToClass]->AddCondition_ReferencedBy($aJoinItems[$sFromClass], $sExtKeyAttCode); + } + else + { + $aJoinItems[$sFromClass]->AddCondition_PointingTo($aJoinItems[$sToClass], $sExtKeyAttCode); + } + } + } + + // Check and prepare the select information + $aSelected = array(); + foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails) + { + $sClassToSelect = $oClassDetails->GetValue(); + if (!array_key_exists($sClassToSelect, $aAliases)) + { + throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oClassDetails, array_keys($aAliases)); + } + $aSelected[$sClassToSelect] = $aAliases[$sClassToSelect]; + } + $oResultFilter->m_aClasses = $aAliases; + $oResultFilter->SetSelectedClasses($aSelected); + + $oConditionTree = $oOqlQuery->GetCondition(); + if ($oConditionTree instanceof Expression) + { + $oResultFilter->m_oSearchCondition = $oResultFilter->OQLExpressionToCondition($sQuery, $oConditionTree, $aAliases); + } + + if (!is_null($aParams)) + { + $oResultFilter->m_aParams = $aParams; + } + + if ($bOQLCacheEnabled) + { + self::$m_aOQLQueries[$sQuery] = clone $oResultFilter; + } + + return $oResultFilter; + } + + public function toxpath() + { + // #@# a voir... + } + static public function fromxpath() + { + // #@# a voir... + } +} + + +?> diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php new file mode 100644 index 0000000000..a55e1172f2 --- /dev/null +++ b/core/dbobjectset.class.php @@ -0,0 +1,442 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * A set of persistent objects, could be heterogeneous + * + * @package iTopORM + */ +class DBObjectSet +{ + private $m_oFilter; + private $m_aOrderBy; + public $m_bLoaded; + private $m_aData; + private $m_aId2Row; + private $m_iCurrRow; + + public function __construct(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0) + { + $this->m_oFilter = $oFilter; + $this->m_aOrderBy = $aOrderBy; + $this->m_aArgs = $aArgs; + $this->m_iLimitCount = $iLimitCount; + $this->m_iLimitStart = $iLimitStart; + + $this->m_bLoaded = false; + $this->m_aData = array(); // array of (row => array of (classalias) => object) + $this->m_aId2Row = array(); + $this->m_iCurrRow = 0; + } + + public function __destruct() + { + } + + public function __toString() + { + $sRet = ''; + $this->Rewind(); + $sRet .= "Set (".$this->m_oFilter->ToOQL().")
\n"; + $sRet .= "Query:
".MetaModel::MakeSelectQuery($this->m_oFilter, array()).")
\n"; + + $sRet .= $this->Count()." records
\n"; + if ($this->Count() > 0) + { + $sRet .= "
    \n"; + while ($oObj = $this->Fetch()) + { + $sRet .= "
  • ".$oObj->__toString()."
  • \n"; + } + $sRet .= "
\n"; + } + return $sRet; + } + + static public function FromObject($oObject) + { + $oRetSet = self::FromScratch(get_class($oObject)); + $oRetSet->AddObject($oObject); + return $oRetSet; + } + + static public function FromScratch($sClass) + { + $oFilter = new CMDBSearchFilter($sClass); + $oRetSet = new self($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + return $oRetSet; + } + + // create an object set ex nihilo + // input = array of objects + static public function FromArray($sClass, $aObjects) + { + $oFilter = new CMDBSearchFilter($sClass); + $oRetSet = new self($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + $oRetSet->AddObjectArray($aObjects, $sClass); + return $oRetSet; + } + + // create an object set ex nihilo + // aClasses = array of (alias => class) + // input = array of (array of (classalias => object)) + static public function FromArrayAssoc($aClasses, $aObjects) + { + // In a perfect world, we should create a complete tree of DBObjectSearch, + // but as we lack most of the information related to the objects, + // let's create one search definition + $sClass = reset($aClasses); + $sAlias = key($aClasses); + $oFilter = new CMDBSearchFilter($sClass, $sAlias); + + $oRetSet = new self($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + + foreach($aObjects as $rowIndex => $aObjectsByClassAlias) + { + $oRetSet->AddObjectExtended($aObjectsByClassAlias); + } + return $oRetSet; + } + + static public function FromLinkSet($oObject, $sLinkSetAttCode, $sExtKeyToRemote) + { + $oLinkAttCode = MetaModel::GetAttributeDef(get_class($oObject), $sLinkSetAttCode); + $oExtKeyAttDef = MetaModel::GetAttributeDef($oLinkAttCode->GetLinkedClass(), $sExtKeyToRemote); + $sTargetClass = $oExtKeyAttDef->GetTargetClass(); + + $oLinkSet = $oObject->Get($sLinkSetAttCode); + $aTargets = array(); + while ($oLink = $oLinkSet->Fetch()) + { + $aTargets[] = MetaModel::GetObject($sTargetClass, $oLink->Get($sExtKeyToRemote)); + } + + return self::FromArray($sTargetClass, $aTargets); + } + + public function ToArray($bWithId = true) + { + $aRet = array(); + $this->Rewind(); + while ($oObject = $this->Fetch()) + { + if ($bWithId) + { + $aRet[$oObject->GetKey()] = $oObject; + } + else + { + $aRet[] = $oObject; + } + } + return $aRet; + } + + public function ToArrayOfValues() + { + if (!$this->m_bLoaded) $this->Load(); + + $aRet = array(); + foreach($this->m_aData as $iRow => $aObjects) + { + foreach($aObjects as $sClassAlias => $oObject) + { + $aRet[$iRow][$sClassAlias.'.'.'id'] = $oObject->GetKey(); + $sClass = get_class($oObject); + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar()) + { + $sAttName = $sClassAlias.'.'.$sAttCode; + $aRet[$iRow][$sAttName] = $oObject->Get($sAttCode); + } + } + } + } + return $aRet; + } + + public function GetColumnAsArray($sAttCode, $bWithId = true) + { + $aRet = array(); + $this->Rewind(); + while ($oObject = $this->Fetch()) + { + if ($bWithId) + { + $aRet[$oObject->GetKey()] = $oObject->Get($sAttCode); + } + else + { + $aRet[] = $oObject->Get($sAttCode); + } + } + return $aRet; + } + + public function GetFilter() + { + return $this->m_oFilter; + } + + public function GetClass() + { + return $this->m_oFilter->GetClass(); + } + + public function GetSelectedClasses() + { + return $this->m_oFilter->GetSelectedClasses(); + } + + public function GetRootClass() + { + return MetaModel::GetRootClass($this->GetClass()); + } + + public function SetLimit($iLimitCount, $iLimitStart = 0) + { + $this->m_iLimitCount = $iLimitCount; + $this->m_iLimitStart = $iLimitStart; + } + + public function GetLimitCount() + { + return $this->m_iLimitCount; + } + + public function GetLimitStart() + { + return $this->m_iLimitStart; + } + + public function Load() + { + if ($this->m_bLoaded) return; + if ($this->m_iLimitCount > 0) + { + $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->m_aOrderBy, $this->m_aArgs, $this->m_iLimitCount, $this->m_iLimitStart); + } + else + { + $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->m_aOrderBy, $this->m_aArgs); + } + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return; + + $sClass = $this->m_oFilter->GetClass(); + while ($aRow = CMDBSource::FetchArray($resQuery)) + { + $aObjects = array(); + foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + $oObject = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias); + $aObjects[$sClassAlias] = $oObject; + } + $this->AddObjectExtended($aObjects); + } + CMDBSource::FreeResult($resQuery); + + $this->m_bLoaded = true; + } + + public function Count() + { + $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->m_aOrderBy, $this->m_aArgs, 0, 0, true); + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return 0; + + $aRow = CMDBSource::FetchArray($resQuery); + CMDBSource::FreeResult($resQuery); + return $aRow['COUNT']; + } + + public function Fetch($sClassAlias = '') + { + if (!$this->m_bLoaded) $this->Load(); + + if ($this->m_iCurrRow >= count($this->m_aData)) + { + return null; + } + + if (strlen($sClassAlias) == 0) + { + $sClassAlias = $this->m_oFilter->GetClassAlias(); + } + $oRetObj = $this->m_aData[$this->m_iCurrRow][$sClassAlias]; + $this->m_iCurrRow++; + return $oRetObj; + } + + // Return the whole line if several classes have been specified in the query + // + public function FetchAssoc() + { + if (!$this->m_bLoaded) $this->Load(); + + if ($this->m_iCurrRow >= count($this->m_aData)) + { + return null; + } + + $aRetObjects = $this->m_aData[$this->m_iCurrRow]; + $this->m_iCurrRow++; + return $aRetObjects; + } + + public function Rewind() + { + $this->Seek(0); + } + + public function Seek($iRow) + { + if (!$this->m_bLoaded) $this->Load(); + + $this->m_iCurrRow = min($iRow, count($this->m_aData)); + return $this->m_iCurrRow; + } + + public function AddObject($oObject, $sClassAlias = '') + { + if (strlen($sClassAlias) == 0) + { + $sClassAlias = $this->m_oFilter->GetClassAlias(); + } + + $iNextPos = count($this->m_aData); + $this->m_aData[$iNextPos][$sClassAlias] = $oObject; + $this->m_aId2Row[$sClassAlias][$oObject->GetKey()] = $iNextPos; + } + + protected function AddObjectExtended($aObjectArray) + { + $iNextPos = count($this->m_aData); + + foreach ($aObjectArray as $sClassAlias => $oObject) + { + $this->m_aData[$iNextPos][$sClassAlias] = $oObject; + $this->m_aId2Row[$sClassAlias][$oObject->GetKey()] = $iNextPos; + } + } + + public function AddObjectArray($aObjects, $sClassAlias = '') + { + // #@# todo - add a check on the object class ? + foreach ($aObjects as $oObj) + { + $this->AddObject($oObj, $sClassAlias); + } + } + + public function Merge($oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not merge two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + $this->AddObject($oObject); + } + } + + public function CreateIntersect($oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not 'intersect' two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $oNewSet = DBObjectSet::FromScratch($this->GetClass()); + + $sClassAlias = $this->m_oFilter->GetClassAlias(); + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + if (array_key_exists($oObject->GetKey(), $this->m_aId2Row[$sClassAlias])) + { + $oNewSet->AddObject($oObject); + } + } + return $oNewSet; + } + + public function CreateDelta($oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not 'delta' two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $oNewSet = DBObjectSet::FromScratch($this->GetClass()); + + $sClassAlias = $this->m_oFilter->GetClassAlias(); + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + if (!array_key_exists($oObject->GetKey(), $this->m_aId2Row[$sClassAlias])) + { + $oNewSet->AddObject($oObject); + } + } + return $oNewSet; + } + + public function GetRelatedObjects($sRelCode, $iMaxDepth = 99) + { + $aRelatedObjs = array(); + + $aVisited = array(); // optimization for consecutive calls of MetaModel::GetRelatedObjects + $this->Seek(0); + while ($oObject = $this->Fetch()) + { + $aMore = $oObject->GetRelatedObjects($sRelCode, $iMaxDepth, $aVisited); + foreach ($aMore as $sClass => $aRelated) + { + foreach ($aRelated as $iObj => $oObj) + { + if (!isset($aRelatedObjs[$sClass][$iObj])) + { + $aRelatedObjs[$sClass][$iObj] = $oObj; + } + } + } + } + return $aRelatedObjs; + } +} + +?> diff --git a/core/dbproperty.class.inc.php b/core/dbproperty.class.inc.php new file mode 100644 index 0000000000..994de667db --- /dev/null +++ b/core/dbproperty.class.inc.php @@ -0,0 +1,159 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * A database property + * + * @package iTopORM + */ +class DBProperty extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "cloud", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_db_properties", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeDateTime("change_date", array("allowed_values"=>null, "sql"=>"change_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("change_comment", array("allowed_values"=>null, "sql"=>"change_comment", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + } + + /** + * Helper to check wether the table has been created into the DB + * (this table did not exist in 1.0.1 and older versions) + */ + public static function IsInstalled() + { + $sTable = MetaModel::DBGetTable(__CLASS__); + if (CMDBSource::IsTable($sTable)) + { + return true; + } + else + { + return false; + } + return false; + } + + public static function SetProperty($sName, $sValue, $sComment = '', $sDescription = null) + { + try + { + $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); + $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); + if ($oSet->Count() == 0) + { + $oProp = new DBProperty(); + $oProp->Set('name', $sName); + $oProp->Set('description', $sDescription); + $oProp->Set('value', $sValue); + $oProp->Set('change_date', time()); + $oProp->Set('change_comment', $sComment); + $oProp->DBInsert(); + } + elseif ($oSet->Count() == 1) + { + $oProp = $oSet->fetch(); + if (!is_null($sDescription)) + { + $oProp->Set('description', $sDescription); + } + $oProp->Set('value', $sValue); + $oProp->Set('change_date', time()); + $oProp->Set('change_comment', $sComment); + $oProp->DBUpdate(); + } + else + { + // Houston... + throw new CoreException('duplicate db property'); + } + } + catch (MySQLException $e) + { + // This might be because the table could not be found, + // let's check it and discard silently if this is really the case + if (self::IsInstalled()) + { + throw $e; + } + IssueLog::Error('Attempting to write a DBProperty while the module has not been installed'); + } + } + + public static function GetProperty($sName, $default = null) + { + try + { + $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); + $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + //throw new CoreException('unknown db property', array('name' => $sName)); + $sValue = $default; + } + elseif ($iCount == 1) + { + $oProp = $oSet->fetch(); + $sValue = $oProp->Get('value'); + } + else + { + // $iCount > 1 + // Houston... + throw new CoreException('duplicate db property', array('name' => $sName, 'count' => $iCount)); + } + } + catch (MySQLException $e) + { + // This might be because the table could not be found, + // let's check it and discard silently if this is really the case + if (self::IsInstalled()) + { + throw $e; + } + $sValue = $default; + } + return $sValue; + } +} + +?> diff --git a/core/dict.class.inc.php b/core/dict.class.inc.php new file mode 100644 index 0000000000..1fcaceedfa --- /dev/null +++ b/core/dict.class.inc.php @@ -0,0 +1,235 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class DictException extends CoreException +{ +} + +class DictExceptionUnknownLanguage extends DictException +{ + public function __construct($sLanguageCode) + { + $aContext = array(); + $aContext['language_code'] = $sLanguageCode; + parent::__construct('Unknown localization language', $aContext); + } +} + +class DictExceptionMissingString extends DictException +{ + public function __construct($sLanguageCode, $sStringCode) + { + $aContext = array(); + $aContext['language_code'] = $sLanguageCode; + $aContext['string_code'] = $sStringCode; + parent::__construct('Missing localized string', $aContext); + } +} + + +define('DICT_ERR_STRING', 1); // when a string is missing, return the identifier +define('DICT_ERR_EXCEPTION', 2); // when a string is missing, throw an exception +//define('DICT_ERR_LOG', 3); // when a string is missing, log an error + + +class Dict +{ + protected static $m_iErrorMode = DICT_ERR_STRING; + protected static $m_sDefaultLanguage = 'EN US'; + protected static $m_sCurrentLanguage = null; // No language selected by default + + protected static $m_aLanguages = array(); // array( code => array( 'description' => '...', 'localized_description' => '...') ...) + protected static $m_aData = array(); + + + public static function SetDefaultLanguage($sLanguageCode) + { + if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) + { + throw new DictExceptionUnknownLanguage($sLanguageCode); + } + self::$m_sDefaultLanguage = $sLanguageCode; + } + + public static function SetUserLanguage($sLanguageCode) + { + if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) + { + throw new DictExceptionUnknownLanguage($sLanguageCode); + } + self::$m_sCurrentLanguage = $sLanguageCode; + } + + + public static function GetCurrentLanguage() + { + if (self::$m_sCurrentLanguage == null) // May happen when no user is logged in (i.e login screen, non authentifed page) + { + // In which case let's use the default language + return self::$m_sDefaultLanguage; + } + return self::$m_sCurrentLanguage; + } + + //returns a hash array( code => array( 'description' => '...', 'localized_description' => '...') ...) + public static function GetLanguages() + { + return self::$m_aLanguages; + } + + // iErrorMode from {DICT_ERR_STRING, DICT_ERR_EXCEPTION} + public static function SetErrorMode($iErrorMode) + { + self::$m_iErrorMode = $iErrorMode; + } + + + public static function S($sStringCode, $sDefault = null) + { + // Attempt to find the string in the user language + // + if (!array_key_exists(self::GetCurrentLanguage(), self::$m_aData)) + { + // It may happen, when something happens before the dictionnaries get loaded + return $sStringCode; + } + $aCurrentDictionary = self::$m_aData[self::GetCurrentLanguage()]; + if (array_key_exists($sStringCode, $aCurrentDictionary)) + { + return $aCurrentDictionary[$sStringCode]; + } + // Attempt to find the string in the default language + // + $aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage]; + if (array_key_exists($sStringCode, $aDefaultDictionary)) + { + return $aDefaultDictionary[$sStringCode]; + } + // Attempt to find the string in english + // + $aDefaultDictionary = self::$m_aData['EN US']; + if (array_key_exists($sStringCode, $aDefaultDictionary)) + { + return $aDefaultDictionary[$sStringCode]; + } + // Could not find the string... + // + switch (self::$m_iErrorMode) + { + case DICT_ERR_STRING: + if (is_null($sDefault)) + { + return $sStringCode; + } + else + { + return $sDefault; + } + break; + + case DICT_ERR_EXCEPTION: + default: + throw new DictExceptionMissingString(self::$m_sCurrentLanguage, $sStringCode); + break; + } + return 'bug!'; + } + + + public static function Format($sFormatCode /*, ... arguments ....*/) + { + $sLocalizedFormat = self::S($sFormatCode); + $aArguments = func_get_args(); + + if ($sLocalizedFormat == $sFormatCode) + { + // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) + return $sFormatCode.' - '.implode(', ', $aArguments); + } + + array_shift($aArguments); + return vsprintf($sLocalizedFormat, $aArguments); + } + + + // sLanguageCode: Code identifying the language i.e. FR-FR + // sEnglishLanguageDesc: Description of the language code, in English. i.e. French (France) + // sLocalizedLanguageDesc: Description of the language code, in its own language. i.e. Français (France) + // aEntries: Hash array of dictionnary entries + public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries) + { + if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) + { + self::$m_aLanguages[$sLanguageCode] = array('description' => $sEnglishLanguageDesc, 'localized_description' => $sLocalizedLanguageDesc); + self::$m_aData[$sLanguageCode] = array(); + } + self::$m_aData[$sLanguageCode] = array_merge(self::$m_aData[$sLanguageCode], $aEntries); + } + + public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US') + { + $aMissing = array(); // Strings missing for the target language + $aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary + $aNotTranslated = array(); // Strings having the same value in both dictionaries + $aOK = array(); // Strings having different values in both dictionaries + + foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue) + { + if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode])) + { + $aMissing[$sStringCode] = $sValue; + } + } + + foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue) + { + if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef])) + { + $aUnexpected[$sStringCode] = $sValue; + } + else + { + // The value exists in the reference + $sRefValue = self::$m_aData[$sLanguageRef][$sStringCode]; + if ($sValue == $sRefValue) + { + $aNotTranslated[$sStringCode] = $sValue; + } + else + { + $aOK[$sStringCode] = $sValue; + } + } + } + return array($aMissing, $aUnexpected, $aNotTranslated, $aOK); + } + + public static function Dump() + { + MyHelpers::var_dump_html(self::$m_aData); + } +} +?> diff --git a/core/email.class.inc.php b/core/email.class.inc.php new file mode 100644 index 0000000000..d120095b00 --- /dev/null +++ b/core/email.class.inc.php @@ -0,0 +1,140 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class EMail +{ + protected $m_sBody; + protected $m_sSubject; + protected $m_sTo; + protected $m_aHeaders; // array of key=>value + + public function __construct() + { + $this->m_sTo = ''; + $this->m_sSubject = ''; + $this->m_sBody = ''; + $this->m_aHeaders = array(); + } + + + // Errors management : not that simple because we need that function to be + // executed in the background, while making sure that any issue would be reported clearly + protected $m_aMailErrors; //array of strings explaining the issues + + public function mail_error_handler($errno, $errstr, $errfile, $errline) + { + $sCleanMessage= str_replace("mail() [function.mail]: ", "", $errstr); + $this->m_aMailErrors[] = $sCleanMessage; + } + + + // returns a list of issues if any + public function Send() + { + $sHeaders = 'MIME-Version: 1.0' . "\r\n"; + // ! the case is important for MS-Outlook + $sHeaders .= 'Content-Type: text/html; charset=UTF-8' . "\r\n"; + $sHeaders .= 'Content-Transfer-Encoding: 8bit' . "\r\n"; + foreach ($this->m_aHeaders as $sKey => $sValue) + { + $sHeaders .= "$sKey: $sValue\r\n"; + } + + // Under Windows (not yet proven for Linux/PHP) mail may issue a warning + // that I could not mask (tried error_reporting(), etc.) + $this->m_aMailErrors = array(); + set_error_handler(array($this, 'mail_error_handler')); + $bRes = mail + ( + $this->m_sTo, + $this->m_sSubject, + $this->m_sBody, + $sHeaders + ); + restore_error_handler(); + if (!$bRes && empty($this->m_aMailErrors)) + { + $this->m_aMailErrors[] = 'Unknown reason'; + } + return $this->m_aMailErrors; + } + + protected function AddToHeader($sKey, $sValue) + { + if (strlen($sValue) > 0) + { + $this->m_aHeaders[$sKey] = $sValue; + } + } + + public function SetReferences($sReferences) + { + $this->AddToHeader('References', $sReferences); + } + + public function SetBody($sBody) + { + $this->m_sBody = $sBody; + } + + public function SetSubject($aSubject) + { + $this->m_sSubject = $aSubject; + } + + public function SetRecipientTO($sAddress) + { + $this->m_sTo = $sAddress; + } + + public function SetRecipientCC($sAddress) + { + $this->AddToHeader('Cc', $sAddress); + } + + public function SetRecipientBCC($sAddress) + { + $this->AddToHeader('Bcc', $sAddress); + } + + public function SetRecipientFrom($sAddress) + { + $this->AddToHeader('From', $sAddress); + + // This is required on Windows because otherwise I would get the error + // "sendmail_from" not set in php.ini" even if it is correctly working + // (apparently, once it worked the SMTP server won't claim anymore for it) + ini_set("sendmail_from", $sAddress); + } + + public function SetRecipientReplyTo($sAddress) + { + $this->AddToHeader('Reply-To', $sAddress); + } + +} + +?> \ No newline at end of file diff --git a/core/event.class.inc.php b/core/event.class.inc.php new file mode 100644 index 0000000000..f49ef6bcea --- /dev/null +++ b/core/event.class.inc.php @@ -0,0 +1,261 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class Event extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + 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 + } +} + +class EventNotification extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_notification", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> "", "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> "", "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'trigger_id', 'action_id', 'object_id')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo')); // 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 EventNotificationEmail extends EventNotification +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_email", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'message', 'trigger_id', 'action_id', 'object_id', 'to', 'cc', 'bcc', 'from', 'subject', 'body')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'message', 'to', 'subject')); // 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 EventIssue extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_issue", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("issue", array("allowed_values"=>null, "sql"=>"issue", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("impact", array("allowed_values"=>null, "sql"=>"impact", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("page", array("allowed_values"=>null, "sql"=>"page", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_post", array("allowed_values"=>null, "sql"=>"arguments_post", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_get", array("allowed_values"=>null, "sql"=>"arguments_get", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTable("callstack", array("allowed_values"=>null, "sql"=>"callstack", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'issue', 'impact', 'page', 'arguments_post', 'arguments_get', 'callstack', 'data')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'issue', 'impact')); // 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 + } + + protected function OnInsert() + { + // Init page information: name, arguments + // + $this->Set('page', @$GLOBALS['_SERVER']['SCRIPT_NAME']); + + if (array_key_exists('_GET', $GLOBALS) && is_array($GLOBALS['_GET'])) + { + $this->Set('arguments_get', $GLOBALS['_GET']); + } + else + { + $this->Set('arguments_get', array()); + } + + if (array_key_exists('_POST', $GLOBALS) && is_array($GLOBALS['_POST'])) + { + $aPost = array(); + foreach($GLOBALS['_POST'] as $sKey => $sValue) + { + if (is_string($sValue)) + { + if (strlen($sValue) < 256) + { + $aPost[$sKey] = $sValue; + } + else + { + $aPost[$sKey] = "!long string: ".strlen($sValue). " chars"; + } + } + else + { + // Not a string + $aPost[$sKey] = (string) $sValue; + } + } + $this->Set('arguments_post', $aPost); + } + else + { + $this->Set('arguments_post', array()); + } + + $sLength = strlen($this->Get('issue')); + if ($sLength > 255) + { + $this->Set('issue', substr($this->Get('issue'), 0, 200)." -truncated ($sLength chars)"); + } + + $sLength = strlen($this->Get('impact')); + if ($sLength > 255) + { + $this->Set('impact', substr($this->Get('impact'), 0, 200)." -truncated ($sLength chars)"); + } + + $sLength = strlen($this->Get('page')); + if ($sLength > 255) + { + $this->Set('page', substr($this->Get('page'), 0, 200)." -truncated ($sLength chars)"); + } + } +} + + +class EventWebService extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_webservice", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("verb", array("allowed_values"=>null, "sql"=>"verb", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + //MetaModel::Init_AddAttribute(new AttributeStructure("arguments", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeBoolean("result", array("allowed_values"=>null, "sql"=>"result", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_info", array("allowed_values"=>null, "sql"=>"log_info", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_warning", array("allowed_values"=>null, "sql"=>"log_warning", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_error", array("allowed_values"=>null, "sql"=>"log_error", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'verb', 'result', 'log_info', 'log_warning', 'log_error', 'data')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'verb', 'result')); // 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/expression.class.inc.php b/core/expression.class.inc.php new file mode 100644 index 0000000000..dc16464f23 --- /dev/null +++ b/core/expression.class.inc.php @@ -0,0 +1,575 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class MissingQueryArgument extends CoreException +{ +} + +abstract class Expression +{ + // recursive translation of identifiers + abstract public function Translate($aTranslationData, $bMatchAll = true); + + // recursive rendering (aArgs used as input by default, or used as output if bRetrofitParams set to True + abstract public function Render(&$aArgs = null, $bRetrofitParams = false); + + // recursively builds an array of class => fieldname + abstract public function ListRequiredFields(); + + abstract public function IsTrue(); + + public function RequiresField($sClass, $sFieldName) + { + // #@# todo - optimize : this is called quite often when building a single query ! + $aRequired = $this->ListRequiredFields(); + if (!in_array($sClass.'.'.$sFieldName, $aRequired)) return false; + return true; + } + + public function serialize() + { + return base64_encode($this->Render()); + } + + static public function unserialize($sValue) + { + return self::FromOQL(base64_decode($sValue)); + } + + static public function FromOQL($sConditionExpr) + { + $oOql = new OqlInterpreter($sConditionExpr); + $oExpression = $oOql->ParseExpression(); + + return $oExpression; + } + + public function LogAnd($oExpr) + { + if ($this->IsTrue()) return clone $oExpr; + if ($oExpr->IsTrue()) return clone $this; + return new BinaryExpression($this, 'AND', $oExpr); + } + + public function LogOr($oExpr) + { + return new BinaryExpression($this, 'OR', $oExpr); + } +} + + +class BinaryExpression extends Expression +{ + protected $m_oLeftExpr; // filter code or an SQL expression (later?) + protected $m_oRightExpr; + protected $m_sOperator; + + public function __construct($oLeftExpr, $sOperator, $oRightExpr) + { + if (!is_object($oLeftExpr)) + { + throw new CoreException('Expecting an Expression object on the left hand', array('found_type' => gettype($oLeftExpr))); + } + if (!is_object($oRightExpr)) + { + throw new CoreException('Expecting an Expression object on the right hand', array('found_type' => gettype($oRightExpr))); + } + if (!$oLeftExpr instanceof Expression) + { + throw new CoreException('Expecting an Expression object on the left hand', array('found_class' => get_class($oLeftExpr))); + } + if (!$oRightExpr instanceof Expression) + { + throw new CoreException('Expecting an Expression object on the right hand', array('found_class' => get_class($oRightExpr))); + } + $this->m_oLeftExpr = $oLeftExpr; + $this->m_oRightExpr = $oRightExpr; + $this->m_sOperator = $sOperator; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + if ($this->m_sOperator == 'AND') + { + if ($this->m_oLeftExpr->IsTrue() && $this->m_oLeftExpr->IsTrue()) return true; + } + return false; + } + + public function GetLeftExpr() + { + return $this->m_oLeftExpr; + } + + public function GetRightExpr() + { + return $this->m_oRightExpr; + } + + public function GetOperator() + { + return $this->m_sOperator; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + $sOperator = $this->GetOperator(); + $sLeft = $this->GetLeftExpr()->Render($aArgs, $bRetrofitParams); + $sRight = $this->GetRightExpr()->Render($aArgs, $bRetrofitParams); + return "($sLeft $sOperator $sRight)"; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + $oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll); + $oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll); + return new BinaryExpression($oLeft, $this->GetOperator(), $oRight); + } + + public function ListRequiredFields() + { + $aLeft = $this->GetLeftExpr()->ListRequiredFields(); + $aRight = $this->GetRightExpr()->ListRequiredFields(); + return array_merge($aLeft, $aRight); + } +} + + +class UnaryExpression extends Expression +{ + protected $m_value; + + public function __construct($value) + { + $this->m_value = $value; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return ($this->m_value == 1); + } + + public function GetValue() + { + return $this->m_value; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + if ($bRetrofitParams) + { + $iParamIndex = count($aArgs) + 1; // 1-based indexation + $aArgs['param'.$iParamIndex] = $this->m_value; + return ':param'.$iParamIndex; + } + else + { + return CMDBSource::Quote($this->m_value); + } + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + return clone $this; + } + + public function ListRequiredFields() + { + return array(); + } +} + +class ScalarExpression extends UnaryExpression +{ + public function __construct($value) + { + if (!is_scalar($value)) + { + throw new CoreException('Attempt to create a scalar expression from a non scalar', array('var_type'=>gettype($value))); + } + parent::__construct($value); + } +} + +class TrueExpression extends ScalarExpression +{ + public function __construct() + { + parent::__construct(1); + } + + public function IsTrue() + { + return true; + } +} + +class FalseExpression extends ScalarExpression +{ + public function __construct() + { + parent::__construct(0); + } + + public function IsTrue() + { + return false; + } +} + +class FieldExpression extends UnaryExpression +{ + protected $m_sParent; + protected $m_sName; + + public function __construct($sName, $sParent = '') + { + parent::__construct("$sParent.$sName"); + + $this->m_sParent = $sParent; + $this->m_sName = $sName; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetParent() {return $this->m_sParent;} + public function GetName() {return $this->m_sName;} + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + if (empty($this->m_sParent)) + { + return "`{$this->m_sName}`"; + } + return "`{$this->m_sParent}`.`{$this->m_sName}`"; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + if (!array_key_exists($this->m_sParent, $aTranslationData)) + { + if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData))); + return clone $this; + } + if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent])) + { + if (!array_key_exists('*', $aTranslationData[$this->m_sParent])) + { + // #@# debug - if ($bMatchAll) MyHelpers::var_dump_html($aTranslationData, true); + if ($bMatchAll) throw new CoreException('Unknown name in translation table', array('name' => $this->m_sName, 'parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData[$this->m_sParent]))); + return clone $this; + } + $sNewParent = $aTranslationData[$this->m_sParent]['*']; + $sNewName = $this->m_sName; + } + else + { + $sNewParent = $aTranslationData[$this->m_sParent][$this->m_sName][0]; + $sNewName = $aTranslationData[$this->m_sParent][$this->m_sName][1]; + } + return new FieldExpression($sNewName, $sNewParent); + } + + public function ListRequiredFields() + { + return array($this->m_sParent.'.'.$this->m_sName); + } +} + + +class VariableExpression extends UnaryExpression +{ + protected $m_sName; + + public function __construct($sName) + { + parent::__construct($sName); + + $this->m_sName = $sName; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetName() {return $this->m_sName;} + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + if (is_null($aArgs)) + { + return ':'.$this->m_sName; + } + elseif (array_key_exists($this->m_sName, $aArgs)) + { + return CMDBSource::Quote($aArgs[$this->m_sName]); + } + elseif ($bRetrofitParams) + { + //$aArgs[$this->m_sName] = null; + return ':'.$this->m_sName; + } + else + { + throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>$aArgs)); + } + } +} + +// Temporary, until we implement functions and expression casting! +// ... or until we implement a real full text search based in the MATCH() expression +class ListExpression extends Expression +{ + protected $m_aExpressions; + + public function __construct($aExpressions) + { + $this->m_aExpressions = $aExpressions; + } + + public static function FromScalars($aScalars) + { + $aExpressions = array(); + foreach($aScalars as $value) + { + $aExpressions[] = new ScalarExpression($value); + } + return new ListExpression($aExpressions); + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetItems() + { + return $this->m_aExpressions; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Render($aArgs, $bRetrofitParams); + } + return '('.implode(', ', $aRes).')'; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll); + } + return new ListExpression($aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } +} + + +class FunctionExpression extends Expression +{ + protected $m_sVerb; + protected $m_aArgs; // array of expressions + + public function __construct($sVerb, $aArgExpressions) + { + $this->m_sVerb = $sVerb; + $this->m_aArgs = $aArgExpressions; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetVerb() + { + return $this->m_sVerb; + } + + public function GetArgs() + { + return $this->m_aArgs; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes[] = $oExpr->Render($aArgs, $bRetrofitParams); + } + return $this->m_sVerb.'('.implode(', ', $aRes).')'; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll); + } + return new FunctionExpression($this->m_sVerb, $aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } +} + +class IntervalExpression extends Expression +{ + protected $m_oValue; // expression + protected $m_sUnit; + + public function __construct($oValue, $sUnit) + { + $this->m_oValue = $oValue; + $this->m_sUnit = $sUnit; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetValue() + { + return $this->m_oValue; + } + + public function GetUnit() + { + return $this->m_sUnit; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + return 'INTERVAL '.$this->m_oValue->Render($aArgs, $bRetrofitParams).' '.$this->m_sUnit; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + return new IntervalExpression($this->m_oValue->Translate($aTranslationData, $bMatchAll), $this->m_sUnit); + } + + public function ListRequiredFields() + { + return array(); + } +} + +class CharConcatExpression extends Expression +{ + protected $m_aExpressions; + + public function __construct($aExpressions) + { + $this->m_aExpressions = $aExpressions; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetItems() + { + return $this->m_aExpressions; + } + + // recursive rendering + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $sCol = $oExpr->Render($aArgs, $bRetrofitParams); + // Concat will be globally NULL if one single argument is null ! + $aRes[] = "COALESCE($sCol, '')"; + } + return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)"; + } + + public function Translate($aTranslationData, $bMatchAll = true) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll); + } + return new CharConcatExpression($aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } +} + +?> diff --git a/core/filterdef.class.inc.php b/core/filterdef.class.inc.php new file mode 100644 index 0000000000..a56752299c --- /dev/null +++ b/core/filterdef.class.inc.php @@ -0,0 +1,225 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + + +require_once('MyHelpers.class.inc.php'); + + +/** + * Definition of a filter (could be made out of an existing attribute, or from an expression) + * + * @package iTopORM + */ +abstract class FilterDefinition +{ + abstract public function GetType(); + abstract public function GetTypeDesc(); + + protected $m_sCode; + private $m_aParams = array(); + protected function Get($sParamName) {return $this->m_aParams[$sParamName];} + + public function __construct($sCode, $aParams = array()) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + + public function OverloadParams($aParams) + { + foreach ($aParams as $sParam => $value) + { + if (!array_key_exists($sParam, $this->m_aParams)) + { + throw new CoreException("Unknown attribute definition parameter '$sParam', please select a value in {".implode(", ", $this->m_aParams)."}"); + } + else + { + $this->m_aParams[$sParam] = $value; + } + } + } + + // to be overloaded + static protected function ListExpectedParams() + { + return array(); + } + + private function ConsistencyCheck() + { + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new CoreException("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } + + public function GetCode() {return $this->m_sCode;} + abstract public function GetLabel(); + abstract public function GetValuesDef(); + + // returns an array of opcode=>oplabel (e.g. "differs from") + abstract public function GetOperators(); + // returns an opcode + abstract public function GetLooseOperator(); + abstract public function GetSQLExpressions(); + + // Wrapper - no need for overloading this one + public function GetOpDescription($sOpCode) + { + $aOperators = $this->GetOperators(); + if (!array_key_exists($sOpCode, $aOperators)) + { + throw new CoreException("Unknown operator '$sOpCode'"); + } + + return $aOperators[$sOpCode]; + } +} + +/** + * Match against the object unique identifier + * + * @package iTopORM + */ +class FilterPrivateKey extends FilterDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("id_field")); + } + + public function GetType() {return "PrivateKey";} + public function GetTypeDesc() {return "Match against object identifier";} + + public function GetLabel() + { + return "Object Private Key"; + } + + public function GetValuesDef() + { + return null; + } + + public function GetOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "IN"=>"in", + "NOTIN"=>"not in" + ); + } + public function GetLooseOperator() + { + return "IN"; + } + + public function GetSQLExpressions() + { + return array( + '' => $this->Get("id_field"), + ); + } +} + +/** + * Match against an existing attribute (the attribute type will determine the available operators) + * + * @package iTopORM + */ +class FilterFromAttribute extends FilterDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("refattribute")); + } + + public function __construct($oRefAttribute, $aParam = array()) + { + // In this very specific case, the code is the one of the attribute + // (this to get a very very simple syntax upon declaration) + $aParam["refattribute"] = $oRefAttribute; + parent::__construct($oRefAttribute->GetCode(), $aParam); + } + + public function GetType() {return "Basic";} + public function GetTypeDesc() {return "Match against field contents";} + + public function __GetRefAttribute() // for checking purposes only !!! + { + return $oAttDef = $this->Get("refattribute"); + } + + public function GetLabel() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetLabel(); + } + + public function GetValuesDef() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetValuesDef(); + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetAllowedValues($aArgs, $sContains); + } + + public function GetOperators() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetBasicFilterOperators(); + } + public function GetLooseOperator() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetBasicFilterLooseOperator(); + } + + public function GetSQLExpressions() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetSQLExpressions(); + } +} + +?> diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php new file mode 100644 index 0000000000..64aa10b289 --- /dev/null +++ b/core/kpi.class.inc.php @@ -0,0 +1,196 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class ExecutionKPI +{ + static protected $m_bEnabled_Duration = false; + static protected $m_bEnabled_Memory = false; + + static protected $m_aStats = array(); + + protected $m_fStarted = null; + protected $m_iInitialMemory = null; + + static public function EnableDuration() + { + self::$m_bEnabled_Duration = true; + } + + static public function EnableMemory() + { + self::$m_bEnabled_Memory = true; + } + + static public function ReportStats() + { + foreach (self::$m_aStats as $sOperation => $aOpStats) + { + echo "

KPIs for $sOperation

\n"; + $fTotalOp = 0; + $iTotalOp = 0; + $fMinOp = null; + $fMaxOp = 0; + echo "
    \n"; + foreach ($aOpStats as $sArguments => $aEvents) + { + $fTotalInter = 0; + $fMinInter = null; + $fMaxInter = 0; + foreach ($aEvents as $fDuration) + { + $fTotalInter += $fDuration; + $fMinInter = is_null($fMinInter) ? $fDuration : min($fMinInter, $fDuration); + $fMaxInter = max($fMaxInter, $fDuration); + + $fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration); + $fMaxOp = max($fMaxOp, $fDuration); + } + $fTotalOp += $fTotalInter; + $iTotalOp++; + + $iCountInter = count($aEvents); + $sTotalInter = round($fTotalInter, 3)."s"; + if ($iCountInter > 1) + { + $sMinInter = round($fMinInter, 3)."s"; + $sMaxInter = round($fMaxInter, 3)."s"; + $sTimeDesc = "$sTotalInter (from $sMinInter to $sMaxInter) in $iCountInter times"; + } + else + { + $sTimeDesc = "$sTotalInter"; + } + echo "
  • Spent $sTimeDesc, on: $sArguments
  • \n"; + } + echo "
\n"; + echo "
    Sumary for $sOperation\n"; + echo "
  • Total: $iTotalOp (".round($fTotalOp, 3).")
  • \n"; + echo "
  • Min: ".round($fMinOp, 3)."
  • \n"; + echo "
  • Max: ".round($fMaxOp, 3)."
  • \n"; + echo "
  • Avg: ".round($fTotalOp / $iTotalOp, 3)."
  • \n"; + echo "
\n"; + } + } + + + public function __construct() + { + $this->ResetCounters(); + } + + // Get the duration since startup, and reset the counter for the next measure + // + public function ComputeAndReport($sOperationDesc) + { + if (self::$m_bEnabled_Duration) + { + $fStopped = MyHelpers::getmicrotime(); + $fDuration = $fStopped - $this->m_fStarted; + $this->Report($sOperationDesc.' / duration: '.round($fDuration, 3)); + } + + if (self::$m_bEnabled_Memory) + { + $iMemory = self::memory_get_usage(); + $iMemoryUsed = $iMemory - $this->m_iInitialMemory; + $this->Report($sOperationDesc.' / memory: '.self::MemStr($iMemoryUsed).' (Total: '.self::MemStr($iMemory).')'); + if (function_exists('memory_get_peak_usage')) + { + $iMemoryPeak = memory_get_peak_usage(); + $this->Report($sOperationDesc.' / memory peak: '.self::MemStr($iMemoryPeak)); + } + } + + $this->ResetCounters(); + } + + public function ComputeStats($sOperation, $sArguments) + { + if (self::$m_bEnabled_Duration) + { + $fStopped = MyHelpers::getmicrotime(); + $fDuration = $fStopped - $this->m_fStarted; + self::$m_aStats[$sOperation][$sArguments][] = $fDuration; + } + } + + protected function ResetCounters() + { + if (self::$m_bEnabled_Duration) + { + $this->m_fStarted = MyHelpers::getmicrotime(); + } + + if (self::$m_bEnabled_Memory) + { + $this->m_iInitialMemory = self::memory_get_usage(); + } + } + + protected function Report($sText) + { + echo "$sText
\n"; + } + + static protected function MemStr($iMemory) + { + return round($iMemory / 1024).' Kb'; + } + + static protected function memory_get_usage() + { + if (function_exists('memory_get_usage')) + { + return memory_get_usage(true); + } + + // Copied from the PHP manual + // + //If its Windows + //Tested on Win XP Pro SP2. Should work on Win 2003 Server too + //Doesn't work for 2000 + //If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642 + if (substr(PHP_OS,0,3) == 'WIN') + { + $output = array(); + exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output); + + return preg_replace( '/[\D]/', '', $output[5] ) * 1024; + } + else + { + //We now assume the OS is UNIX + //Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4 + //This should work on most UNIX systems + $pid = getmypid(); + exec("ps -eo%mem,rss,pid | grep $pid", $output); + $output = explode(" ", $output[0]); + //rss is given in 1024 byte units + return $output[1] * 1024; + } + } +} + +?> diff --git a/core/log.class.inc.php b/core/log.class.inc.php new file mode 100644 index 0000000000..f6d72e8440 --- /dev/null +++ b/core/log.class.inc.php @@ -0,0 +1,146 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +class FileLog +{ + protected $m_sFile = ''; // log is disabled if this is empty + + public function __construct($sFileName = '') + { + $this->m_sFile = $sFileName; + } + + public function Error($sText) + { + self::Write("Error | ".$sText); + } + + public function Warning($sText) + { + self::Write("Warning | ".$sText); + } + + public function Info($sText) + { + self::Write("Info | ".$sText); + } + + public function Ok($sText) + { + self::Write("Ok | ".$sText); + } + + protected function Write($sText) + { + if (strlen($this->m_sFile) == 0) return; + + $hLogFile = @fopen($this->m_sFile, 'a'); + if ($hLogFile !== false) + { + $sDate = date('Y-m-d H:i:s'); + fwrite($hLogFile, "$sDate | $sText\n"); + fclose($hLogFile); + } + } +} + +class SetupLog +{ + protected static $m_oFileLog; + + public static function Enable($sTargetFile) + { + self::$m_oFileLog = new FileLog($sTargetFile); + } + public static function Error($sText) + { + self::$m_oFileLog->Error($sText); + } + public static function Warning($sText) + { + self::$m_oFileLog->Warning($sText); + } + public static function Info($sText) + { + self::$m_oFileLog->Info($sText); + } + public static function Ok($sText) + { + self::$m_oFileLog->Ok($sText); + } +} + +class IssueLog +{ + protected static $m_oFileLog; + + public static function Enable($sTargetFile) + { + self::$m_oFileLog = new FileLog($sTargetFile); + } + public static function Error($sText) + { + self::$m_oFileLog->Error($sText); + } + public static function Warning($sText) + { + self::$m_oFileLog->Warning($sText); + } + public static function Info($sText) + { + self::$m_oFileLog->Info($sText); + } + public static function Ok($sText) + { + self::$m_oFileLog->Ok($sText); + } +} + +class ToolsLog +{ + protected static $m_oFileLog; + + public static function Enable($sTargetFile) + { + self::$m_oFileLog = new FileLog($sTargetFile); + } + public static function Error($sText) + { + self::$m_oFileLog->Error($sText); + } + public static function Warning($sText) + { + self::$m_oFileLog->Warning($sText); + } + public static function Info($sText) + { + self::$m_oFileLog->Info($sText); + } + public static function Ok($sText) + { + self::$m_oFileLog->Ok($sText); + } +} +?> diff --git a/core/metamodel.class.php b/core/metamodel.class.php new file mode 100644 index 0000000000..90de8317c9 --- /dev/null +++ b/core/metamodel.class.php @@ -0,0 +1,3766 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + + +// #@# todo: change into class const (see Doctrine) +// Doctrine example +// class toto +// { +// /** +// * VERSION +// */ +// const VERSION = '1.0.0'; +// } + +/** + * add some description here... + * + * @package iTopORM + */ +define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1); +/** + * add some description here... + * + * @package iTopORM + */ +define('ENUM_CHILD_CLASSES_ALL', 2); +/** + * add some description here... + * + * @package iTopORM + */ +define('ENUM_PARENT_CLASSES_EXCLUDELEAF', 1); +/** + * add some description here... + * + * @package iTopORM + */ +define('ENUM_PARENT_CLASSES_ALL', 2); + +/** + * Specifies that this attribute is visible/editable.... normal (default config) + * + * @package iTopORM + */ +define('OPT_ATT_NORMAL', 0); +/** + * Specifies that this attribute is hidden in that state + * + * @package iTopORM + */ +define('OPT_ATT_HIDDEN', 1); +/** + * Specifies that this attribute is not editable in that state + * + * @package iTopORM + */ +define('OPT_ATT_READONLY', 2); +/** + * Specifieds that the attribute must be set (different than default value?) when arriving into that state + * + * @package iTopORM + */ +define('OPT_ATT_MANDATORY', 4); +/** + * Specifies that the attribute must change when arriving into that state + * + * @package iTopORM + */ +define('OPT_ATT_MUSTCHANGE', 8); +/** + * Specifies that the attribute must be proposed when arriving into that state + * + * @package iTopORM + */ +define('OPT_ATT_MUSTPROMPT', 16); + +/** + * DB Engine -should be moved into CMDBSource + * + * @package iTopORM + */ +define('MYSQL_ENGINE', 'innodb'); +//define('MYSQL_ENGINE', 'myisam'); + + + +/** + * (API) The objects definitions as well as their mapping to the database + * + * @package iTopORM + */ +abstract class MetaModel +{ + /////////////////////////////////////////////////////////////////////////// + // + // STATIC Members + // + /////////////////////////////////////////////////////////////////////////// + + // Purpose: workaround the following limitation = PHP5 does not allow to know the class (derived from the current one) + // from which a static function is called (__CLASS__ and self are interpreted during parsing) + private static function GetCallersPHPClass($sExpectedFunctionName = null) + { + //var_dump(debug_backtrace()); + $aBacktrace = debug_backtrace(); + // $aBacktrace[0] is where we are + // $aBacktrace[1] is the caller of GetCallersPHPClass + // $aBacktrace[1] is the info we want + if (!empty($sExpectedFunctionName)) + { + assert('$aBacktrace[2]["function"] == $sExpectedFunctionName'); + } + return $aBacktrace[2]["class"]; + } + + // Static init -why and how it works + // + // We found the following limitations: + //- it is not possible to define non scalar constants + //- it is not possible to declare a static variable as '= new myclass()' + // Then we had do propose this model, in which a derived (non abstract) + // class should implement Init(), to call InheritAttributes or AddAttribute. + + private static function _check_subclass($sClass) + { + // See also IsValidClass()... ???? #@# + // class is mandatory + // (it is not possible to guess it when called as myderived::...) + if (!array_key_exists($sClass, self::$m_aClassParams)) + { + throw new CoreException("Unknown class '$sClass', expected a value in {".implode(', ', array_keys(self::$m_aClassParams))."}"); + } + } + + public static function static_var_dump() + { + var_dump(get_class_vars(__CLASS__)); + } + + private static $m_bDebugQuery = false; + private static $m_iStackDepthRef = 0; + + public static function StartDebugQuery() + { + $aBacktrace = debug_backtrace(); + self::$m_iStackDepthRef = count($aBacktrace); + self::$m_bDebugQuery = true; + } + public static function StopDebugQuery() + { + self::$m_bDebugQuery = false; + } + public static function DbgTrace($value) + { + if (!self::$m_bDebugQuery) return; + $aBacktrace = debug_backtrace(); + $iCallStackPos = count($aBacktrace) - self::$m_bDebugQuery; + $sIndent = ""; + for ($i = 0 ; $i < $iCallStackPos ; $i++) + { + $sIndent .= " .-=^=-. "; + } + $aCallers = array(); + foreach($aBacktrace as $aStackInfo) + { + $aCallers[] = $aStackInfo["function"]; + } + $sCallers = "Callstack: ".implode(', ', $aCallers); + $sFunction = "".$aBacktrace[1]["function"].""; + + if (is_string($value)) + { + echo "$sIndent$sFunction: $value
\n"; + } + else if (is_object($value)) + { + echo "$sIndent$sFunction:\n
\n";
+			print_r($value);
+			echo "
\n"; + } + else + { + echo "$sIndent$sFunction: $value
\n"; + } + } + + private static $m_oConfig = null; + + private static $m_bSkipCheckToWrite = false; + private static $m_bSkipCheckExtKeys = false; + + private static $m_bQueryCacheEnabled = false; + private static $m_bTraceQueries = false; + private static $m_aQueriesLog = array(); + + private static $m_bLogIssue = false; + private static $m_bLogNotification = false; + private static $m_bLogWebService = false; + + public static function SkipCheckToWrite() + { + return self::$m_bSkipCheckToWrite; + } + + public static function SkipCheckExtKeys() + { + return self::$m_bSkipCheckExtKeys; + } + + public static function IsLogEnabledIssue() + { + return self::$m_bLogIssue; + } + public static function IsLogEnabledNotification() + { + return self::$m_bLogNotification; + } + public static function IsLogEnabledWebService() + { + return self::$m_bLogWebService; + } + + private static $m_sDBName = ""; + private static $m_sTablePrefix = ""; // table prefix for the current application instance (allow several applications on the same DB) + private static $m_Category2Class = array(); + private static $m_aRootClasses = array(); // array of "classname" => "rootclass" + private static $m_aParentClasses = array(); // array of ("classname" => array of "parentclass") + private static $m_aChildClasses = array(); // array of ("classname" => array of "childclass") + + private static $m_aClassParams = array(); // array of ("classname" => array of class information) + + static public function GetParentPersistentClass($sRefClass) + { + $sClass = get_parent_class($sRefClass); + if (!$sClass) return ''; + + if ($sClass == 'DBObject') return ''; // Warning: __CLASS__ is lower case in my version of PHP + + // Note: the UI/business model may implement pure PHP classes (intermediate layers) + if (array_key_exists($sClass, self::$m_aClassParams)) + { + return $sClass; + } + return self::GetParentPersistentClass($sClass); + } + + final static public function GetName($sClass) + { + self::_check_subclass($sClass); + $sStringCode = 'Class:'.$sClass; + return Dict::S($sStringCode, $sClass); + } + final static public function GetName_Obsolete($sClass) + { + // Written for compatibility with a data model written prior to version 0.9.1 + self::_check_subclass($sClass); + if (array_key_exists('name', self::$m_aClassParams[$sClass])) + { + return self::$m_aClassParams[$sClass]['name']; + } + else + { + return self::GetName($sClass); + } + } + final static public function GetCategory($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["category"]; + } + final static public function HasCategory($sClass, $sCategory) + { + self::_check_subclass($sClass); + return (strpos(self::$m_aClassParams[$sClass]["category"], $sCategory) !== false); + } + final static public function GetClassDescription($sClass) + { + self::_check_subclass($sClass); + $sStringCode = 'Class:'.$sClass.'+'; + return Dict::S($sStringCode, ''); + } + final static public function GetClassDescription_Obsolete($sClass) + { + // Written for compatibility with a data model written prior to version 0.9.1 + self::_check_subclass($sClass); + if (array_key_exists('description', self::$m_aClassParams[$sClass])) + { + return self::$m_aClassParams[$sClass]['description']; + } + else + { + return self::GetClassDescription($sClass); + } + } + final static public function GetClassIcon($sClass, $bImgTag = true, $sMoreStyles = '') + { + self::_check_subclass($sClass); + + $sIcon = ''; + if (array_key_exists('icon', self::$m_aClassParams[$sClass])) + { + $sIcon = self::$m_aClassParams[$sClass]['icon']; + } + if (strlen($sIcon) == 0) + { + $sParentClass = self::GetParentPersistentClass($sClass); + if (strlen($sParentClass) > 0) + { + return self::GetClassIcon($sParentClass); + } + } + if ($bImgTag && ($sIcon != '')) + { + $sIcon = ""; + } + return $sIcon; + } + final static public function IsAutoIncrementKey($sClass) + { + self::_check_subclass($sClass); + return (self::$m_aClassParams[$sClass]["key_type"] == "autoincrement"); + } + final static public function GetNameAttributeCode($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["name_attcode"]; + } + final static public function GetStateAttributeCode($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["state_attcode"]; + } + final static public function GetDefaultState($sClass) + { + $sDefaultState = ''; + $sStateAttrCode = self::GetStateAttributeCode($sClass); + if (!empty($sStateAttrCode)) + { + $oStateAttrDef = self::GetAttributeDef($sClass, $sStateAttrCode); + $sDefaultState = $oStateAttrDef->GetDefaultValue(); + } + return $sDefaultState; + } + final static public function GetReconcKeys($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["reconc_keys"]; + } + final static public function GetDisplayTemplate($sClass) + { + self::_check_subclass($sClass); + return array_key_exists("display_template", self::$m_aClassParams[$sClass]) ? self::$m_aClassParams[$sClass]["display_template"]: ''; + } + final static public function GetAttributeOrigin($sClass, $sAttCode) + { + self::_check_subclass($sClass); + return self::$m_aAttribOrigins[$sClass][$sAttCode]; + } + final static public function GetPrequisiteAttributes($sClass, $sAttCode) + { + self::_check_subclass($sClass); + $oAtt = self::GetAttributeDef($sClass, $sAttCode); + // Temporary implementation: later, we might be able to compute + // the dependencies, based on the attributes definition + // (allowed values and default values) + if ($oAtt->IsWritable()) + { + return $oAtt->GetPrerequisiteAttributes(); + } + else + { + return array(); + } + } + /** + * Find all attributes that depend on the specified one (reverse of GetPrequisiteAttributes) + * @param string $sClass Name of the class + * @param string $sAttCode Code of the attributes + * @return Array List of attribute codes that depend on the given attribute, empty array if none. + */ + final static public function GetDependentAttributes($sClass, $sAttCode) + { + $aResults = array(); + self::_check_subclass($sClass); + foreach (self::ListAttributeDefs($sClass) as $sDependentAttCode=>$void) + { + $aPrerequisites = self::GetPrequisiteAttributes($sClass, $sDependentAttCode); + if (in_array($sAttCode, $aPrerequisites)) + { + $aResults[] = $sDependentAttCode; + } + } + return $aResults; + } + // #@# restore to private ? + final static public function DBGetTable($sClass, $sAttCode = null) + { + self::_check_subclass($sClass); + if (empty($sAttCode) || ($sAttCode == "id")) + { + $sTableRaw = self::$m_aClassParams[$sClass]["db_table"]; + if (empty($sTableRaw)) + { + // return an empty string whenever the table is undefined, meaning that there is no table associated to this 'abstract' class + return ''; + } + else + { + return self::$m_sTablePrefix.$sTableRaw; + } + } + // This attribute has been inherited (compound objects) + return self::DBGetTable(self::$m_aAttribOrigins[$sClass][$sAttCode]); + } + + final static public function DBGetView($sClass) + { + return self::$m_sTablePrefix."view_".$sClass; + } + + final static protected function DBEnumTables() + { + // This API do not rely on our capability to query the DB and retrieve + // the list of existing tables + // Rather, it uses the list of expected tables, corresponding to the data model + $aTables = array(); + foreach (self::GetClasses() as $sClass) + { + if (!self::HasTable($sClass)) continue; + $sTable = self::DBGetTable($sClass); + + // Could be completed later with all the classes that are using a given table + if (!array_key_exists($sTable, $aTables)) + { + $aTables[$sTable] = array(); + } + $aTables[$sTable][] = $sClass; + } + return $aTables; + } + + final static public function DBGetKey($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["db_key_field"]; + } + final static public function DBGetClassField($sClass) + { + self::_check_subclass($sClass); + return self::$m_aClassParams[$sClass]["db_finalclass_field"]; + } + final static public function IsStandaloneClass($sClass) + { + self::_check_subclass($sClass); + + if (count(self::$m_aChildClasses[$sClass]) == 0) + { + if (count(self::$m_aParentClasses[$sClass]) == 0) + { + return true; + } + } + return false; + } + final static public function IsParentClass($sParentClass, $sChildClass) + { + self::_check_subclass($sChildClass); + self::_check_subclass($sParentClass); + if (in_array($sParentClass, self::$m_aParentClasses[$sChildClass])) return true; + if ($sChildClass == $sParentClass) return true; + return false; + } + final static public function IsSameFamilyBranch($sClassA, $sClassB) + { + self::_check_subclass($sClassA); + self::_check_subclass($sClassB); + if (in_array($sClassA, self::$m_aParentClasses[$sClassB])) return true; + if (in_array($sClassB, self::$m_aParentClasses[$sClassA])) return true; + if ($sClassA == $sClassB) return true; + return false; + } + final static public function IsSameFamily($sClassA, $sClassB) + { + self::_check_subclass($sClassA); + self::_check_subclass($sClassB); + return (self::GetRootClass($sClassA) == self::GetRootClass($sClassB)); + } + + // Attributes of a given class may contain attributes defined in a parent class + // - Some attributes are a copy of the definition + // - Some attributes correspond to the upper class table definition (compound objects) + // (see also filters definition) + private static $m_aAttribDefs = array(); // array of ("classname" => array of attributes) + private static $m_aAttribOrigins = array(); // array of ("classname" => array of ("attcode"=>"sourceclass")) + private static $m_aExtKeyFriends = array(); // array of ("classname" => array of ("indirect ext key attcode"=> array of ("relative ext field"))) + private static $m_aIgnoredAttributes = array(); //array of ("classname" => array of ("attcode") + + final static public function ListAttributeDefs($sClass) + { + self::_check_subclass($sClass); + return self::$m_aAttribDefs[$sClass]; + } + + final public static function GetAttributesList($sClass) + { + self::_check_subclass($sClass); + return array_keys(self::$m_aAttribDefs[$sClass]); + } + + final public static function GetFiltersList($sClass) + { + self::_check_subclass($sClass); + return array_keys(self::$m_aFilterDefs[$sClass]); + } + + final public static function GetKeysList($sClass) + { + self::_check_subclass($sClass); + $aExtKeys = array(); + foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef) + { + if ($oAttDef->IsExternalKey()) + { + $aExtKeys[] = $sAttCode; + } + } + return $aExtKeys; + } + + final static public function IsValidKeyAttCode($sClass, $sAttCode) + { + if (!array_key_exists($sClass, self::$m_aAttribDefs)) return false; + if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass])) return false; + return (self::$m_aAttribDefs[$sClass][$sAttCode]->IsExternalKey()); + } + final static public function IsValidAttCode($sClass, $sAttCode) + { + if (!array_key_exists($sClass, self::$m_aAttribDefs)) return false; + return (array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass])); + } + final static public function IsAttributeOrigin($sClass, $sAttCode) + { + return (self::$m_aAttribOrigins[$sClass][$sAttCode] == $sClass); + } + + final static public function IsValidFilterCode($sClass, $sFilterCode) + { + if (!array_key_exists($sClass, self::$m_aFilterDefs)) return false; + return (array_key_exists($sFilterCode, self::$m_aFilterDefs[$sClass])); + } + public static function IsValidClass($sClass) + { + return (array_key_exists($sClass, self::$m_aAttribDefs)); + } + + public static function IsValidObject($oObject) + { + if (!is_object($oObject)) return false; + return (self::IsValidClass(get_class($oObject))); + } + + public static function IsReconcKey($sClass, $sAttCode) + { + return (in_array($sAttCode, self::GetReconcKeys($sClass))); + } + + final static public function GetAttributeDef($sClass, $sAttCode) + { + self::_check_subclass($sClass); + return self::$m_aAttribDefs[$sClass][$sAttCode]; + } + + final static public function GetExternalKeys($sClass) + { + $aExtKeys = array(); + foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) + { + if ($oAtt->IsExternalKey()) + { + $aExtKeys[$sAttCode] = $oAtt; + } + } + return $aExtKeys; + } + + final static public function GetLinkedSets($sClass) + { + $aLinkedSets = array(); + foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) + { + if (is_subclass_of($oAtt, 'AttributeLinkedSet')) + { + $aLinkedSets[$sAttCode] = $oAtt; + } + } + return $aLinkedSets; + } + + final static public function GetExternalFields($sClass, $sKeyAttCode) + { + $aExtFields = array(); + foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) + { + if ($oAtt->IsExternalField() && ($oAtt->GetKeyAttCode() == $sKeyAttCode)) + { + $aExtFields[] = $oAtt; + } + } + return $aExtFields; + } + + final static public function GetExtKeyFriends($sClass, $sExtKeyAttCode) + { + if (array_key_exists($sExtKeyAttCode, self::$m_aExtKeyFriends[$sClass])) + { + return self::$m_aExtKeyFriends[$sClass][$sExtKeyAttCode]; + } + else + { + return array(); + } + } + + public static function GetLabel($sClass, $sAttCode) + { + $oAttDef = self::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef) return $oAttDef->GetLabel(); + return ""; + } + + public static function GetDescription($sClass, $sAttCode) + { + $oAttDef = self::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef) return $oAttDef->GetDescription(); + return ""; + } + + // Filters of a given class may contain filters defined in a parent class + // - Some filters are a copy of the definition + // - Some filters correspond to the upper class table definition (compound objects) + // (see also attributes definition) + private static $m_aFilterDefs = array(); // array of ("classname" => array filterdef) + private static $m_aFilterOrigins = array(); // array of ("classname" => array of ("attcode"=>"sourceclass")) + + public static function GetClassFilterDefs($sClass) + { + self::_check_subclass($sClass); + return self::$m_aFilterDefs[$sClass]; + } + + final static public function GetClassFilterDef($sClass, $sFilterCode) + { + self::_check_subclass($sClass); + return self::$m_aFilterDefs[$sClass][$sFilterCode]; + } + + public static function GetFilterLabel($sClass, $sFilterCode) + { + $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); + if ($oFilter) return $oFilter->GetLabel(); + return ""; + } + + public static function GetFilterDescription($sClass, $sFilterCode) + { + $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); + if ($oFilter) return $oFilter->GetDescription(); + return ""; + } + + // returns an array of opcode=>oplabel (e.g. "differs from") + public static function GetFilterOperators($sClass, $sFilterCode) + { + $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); + if ($oFilter) return $oFilter->GetOperators(); + return array(); + } + + // returns an opcode + public static function GetFilterLooseOperator($sClass, $sFilterCode) + { + $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); + if ($oFilter) return $oFilter->GetLooseOperator(); + return array(); + } + + public static function GetFilterOpDescription($sClass, $sFilterCode, $sOpCode) + { + $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); + if ($oFilter) return $oFilter->GetOpDescription($sOpCode); + return ""; + } + + public static function GetFilterHTMLInput($sFilterCode) + { + return ""; + } + + // Lists of attributes/search filters + // + private static $m_aListInfos = array(); // array of ("listcode" => various info on the list, common to every classes) + private static $m_aListData = array(); // array of ("classname" => array of "listcode" => list) + // list may be an array of attcode / fltcode + // list may be an array of "groupname" => (array of attcode / fltcode) + + public static function EnumZLists() + { + return array_keys(self::$m_aListInfos); + } + + final static public function GetZListInfo($sListCode) + { + return self::$m_aListInfos[$sListCode]; + } + + public static function GetZListItems($sClass, $sListCode) + { + if (array_key_exists($sClass, self::$m_aListData)) + { + if (array_key_exists($sListCode, self::$m_aListData[$sClass])) + { + return self::$m_aListData[$sClass][$sListCode]; + } + } + $sParentClass = self::GetParentPersistentClass($sClass); + if (empty($sParentClass)) return array(); // nothing for the mother of all classes + // Dig recursively + return self::GetZListItems($sParentClass, $sListCode); + } + + public static function IsAttributeInZList($sClass, $sListCode, $sAttCodeOrFltCode, $sGroup = null) + { + $aZList = self::GetZListItems($sClass, $sListCode); + if (!$sGroup) + { + return (in_array($sAttCodeOrFltCode, $aZList)); + } + return (in_array($sAttCodeOrFltCode, $aZList[$sGroup])); + } + + // + // Relations + // + private static $m_aRelationInfos = array(); // array of ("relcode" => various info on the list, common to every classes) + + public static function EnumRelations($sClass = '') + { + $aResult = array_keys(self::$m_aRelationInfos); + if (!empty($sClass)) + { + // Return only the relations that have a meaning (i.e. for which at least one query is defined) + // for the specified class + $aClassRelations = array(); + foreach($aResult as $sRelCode) + { + $aQueries = self::EnumRelationQueries($sClass, $sRelCode); + if (count($aQueries) > 0) + { + $aClassRelations[] = $sRelCode; + } + } + return $aClassRelations; + } + + return $aResult; + } + + public static function EnumRelationProperties($sRelCode) + { + MyHelpers::CheckKeyInArray('relation code', $sRelCode, self::$m_aRelationInfos); + return self::$m_aRelationInfos[$sRelCode]; + } + + final static public function GetRelationDescription($sRelCode) + { + return Dict::S("Relation:$sRelCode/Description"); + } + + final static public function GetRelationVerbUp($sRelCode) + { + return Dict::S("Relation:$sRelCode/VerbUp"); + } + + final static public function GetRelationVerbDown($sRelCode) + { + return Dict::S("Relation:$sRelCode/VerbDown"); + } + + public static function EnumRelationQueries($sClass, $sRelCode) + { + MyHelpers::CheckKeyInArray('relation code', $sRelCode, self::$m_aRelationInfos); + return call_user_func_array(array($sClass, 'GetRelationQueries'), array($sRelCode)); + } + + // + // Object lifecycle model + // + private static $m_aStates = array(); // array of ("classname" => array of "statecode"=>array('label'=>..., attribute_inherit=> attribute_list=>...)) + private static $m_aStimuli = array(); // array of ("classname" => array of ("stimuluscode"=>array('label'=>...))) + private static $m_aTransitions = array(); // array of ("classname" => array of ("statcode_from"=>array of ("stimuluscode" => array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD))) + + public static function EnumStates($sClass) + { + if (array_key_exists($sClass, self::$m_aStates)) + { + return self::$m_aStates[$sClass]; + } + else + { + return array(); + } + } + + public static function EnumStimuli($sClass) + { + if (array_key_exists($sClass, self::$m_aStimuli)) + { + return self::$m_aStimuli[$sClass]; + } + else + { + return array(); + } + } + + public static function GetStateLabel($sClass, $sStateValue) + { + $sStateAttrCode = self::GetStateAttributeCode($sClass); + $oAttDef = self::GetAttributeDef($sClass, $sStateAttrCode); + // Be consistent with what is done for enums, since states are defined as enums... + return Dict::S("Class:".$oAttDef->GetHostClass()."/Attribute:$sStateAttrCode/Value:$sStateValue"); + + // I've decided the current implementation, because I need + // to get the description as well -GetAllowedValues does not render the description, + // so far... + // Could have been implemented the following way (not tested + // $oStateAttrDef = self::GetAttributeDef($sClass, $sStateAttrCode); + // $aAllowedValues = $oStateAttrDef->GetAllowedValues(); + // return $aAllowedValues[$sStateValue]; + } + public static function GetStateDescription($sClass, $sStateValue) + { + $sStateAttrCode = self::GetStateAttributeCode($sClass); + return Dict::S("Class:$sClass/Attribute:$sStateAttrCode/Value:$sStateValue+", ''); + } + + public static function EnumTransitions($sClass, $sStateCode) + { + if (array_key_exists($sClass, self::$m_aTransitions)) + { + if (array_key_exists($sStateCode, self::$m_aTransitions[$sClass])) + { + return self::$m_aTransitions[$sClass][$sStateCode]; + } + } + return array(); + } + public static function GetAttributeFlags($sClass, $sState, $sAttCode) + { + $iFlags = 0; // By default (if no life cycle) no flag at all + $sStateAttCode = self::GetStateAttributeCode($sClass); + if (!empty($sStateAttCode)) + { + $aStates = MetaModel::EnumStates($sClass); + if (!array_key_exists($sState, $aStates)) + { + throw new CoreException("Invalid state '$sState' for class '$sClass', expecting a value in {".implode(', ', array_keys($aStates))."}"); + } + $aCurrentState = $aStates[$sState]; + if ( (array_key_exists('attribute_list', $aCurrentState)) && (array_key_exists($sAttCode, $aCurrentState['attribute_list'])) ) + { + $iFlags = $aCurrentState['attribute_list'][$sAttCode]; + } + } + return $iFlags; + } + + // + // Allowed values + // + + public static function GetAllowedValues_att($sClass, $sAttCode, $aArgs = array(), $sContains = '') + { + $oAttDef = self::GetAttributeDef($sClass, $sAttCode); + return $oAttDef->GetAllowedValues($aArgs, $sContains); + } + + public static function GetAllowedValues_flt($sClass, $sFltCode, $aArgs = array(), $sContains = '') + { + $oFltDef = self::GetClassFilterDef($sClass, $sFltCode); + return $oFltDef->GetAllowedValues($aArgs, $sContains); + } + + // + // Businezz model declaration verbs (should be static) + // + + public static function RegisterZList($sListCode, $aListInfo) + { + // Check mandatory params + $aMandatParams = array( + "description" => "detailed (though one line) description of the list", + "type" => "attributes | filters", + ); + foreach($aMandatParams as $sParamName=>$sParamDesc) + { + if (!array_key_exists($sParamName, $aListInfo)) + { + throw new CoreException("Declaration of list $sListCode - missing parameter $sParamName"); + } + } + + self::$m_aListInfos[$sListCode] = $aListInfo; + } + + public static function RegisterRelation($sRelCode) + { + // Each item used to be an array of properties... + self::$m_aRelationInfos[$sRelCode] = $sRelCode; + } + + // Must be called once and only once... + public static function InitClasses($sTablePrefix) + { + if (count(self::GetClasses()) > 0) + { + throw new CoreException("InitClasses should not be called more than once -skipped"); + return; + } + + self::$m_sTablePrefix = $sTablePrefix; + + foreach(get_declared_classes() as $sPHPClass) { + if (is_subclass_of($sPHPClass, 'DBObject')) + { + $sParent = get_parent_class($sPHPClass); + if (array_key_exists($sParent, self::$m_aIgnoredAttributes)) + { + // Inherit info about attributes to ignore + self::$m_aIgnoredAttributes[$sPHPClass] = self::$m_aIgnoredAttributes[$sParent]; + } + if (method_exists($sPHPClass, 'Init')) + { + call_user_func(array($sPHPClass, 'Init')); + } + } + } + + // Add a 'class' attribute/filter to the root classes and their children + // + foreach(self::EnumRootClasses() as $sRootClass) + { + if (self::IsStandaloneClass($sRootClass)) continue; + + $sDbFinalClassField = self::DBGetClassField($sRootClass); + if (strlen($sDbFinalClassField) == 0) + { + $sDbFinalClassField = 'finalclass'; + self::$m_aClassParams[$sRootClass]["db_finalclass_field"] = 'finalclass'; + } + $oClassAtt = new AttributeFinalClass('finalclass', array( + "sql"=>$sDbFinalClassField, + "default_value"=>$sRootClass, + "is_null_allowed"=>false, + "depends_on"=>array() + )); + $oClassAtt->SetHostClass($sRootClass); + self::$m_aAttribDefs[$sRootClass]['finalclass'] = $oClassAtt; + self::$m_aAttribOrigins[$sRootClass]['finalclass'] = $sRootClass; + + $oClassFlt = new FilterFromAttribute($oClassAtt); + self::$m_aFilterDefs[$sRootClass]['finalclass'] = $oClassFlt; + self::$m_aFilterOrigins[$sRootClass]['finalclass'] = $sRootClass; + + foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass) + { + if (array_key_exists('finalclass', self::$m_aAttribDefs[$sChildClass])) + { + throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as an attribute code"); + } + if (array_key_exists('finalclass', self::$m_aFilterDefs[$sChildClass])) + { + throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as a filter code"); + } + $oCloned = clone $oClassAtt; + $oCloned->SetFixedValue($sChildClass); + self::$m_aAttribDefs[$sChildClass]['finalclass'] = $oCloned; + self::$m_aAttribOrigins[$sChildClass]['finalclass'] = $sRootClass; + + $oClassFlt = new FilterFromAttribute($oClassAtt); + self::$m_aFilterDefs[$sChildClass]['finalclass'] = $oClassFlt; + self::$m_aFilterOrigins[$sChildClass]['finalclass'] = self::GetRootClass($sChildClass); + } + } + + // Prepare external fields and filters + // Add final class to external keys + // + foreach (self::GetClasses() as $sClass) + { + self::$m_aExtKeyFriends[$sClass] = array(); + foreach (self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef) + { + // Compute the filter codes + // + foreach ($oAttDef->GetFilterDefinitions() as $sFilterCode => $oFilterDef) + { + self::$m_aFilterDefs[$sClass][$sFilterCode] = $oFilterDef; + + if ($oAttDef->IsExternalField()) + { + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + $oKeyDef = self::GetAttributeDef($sClass, $sKeyAttCode); + self::$m_aFilterOrigins[$sClass][$sFilterCode] = $oKeyDef->GetTargetClass(); + } + else + { + self::$m_aFilterOrigins[$sClass][$sFilterCode] = self::$m_aAttribOrigins[$sClass][$sAttCode]; + } + } + + // Compute the fields that will be used to display a pointer to another object + // + if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) + { + // oAttDef is either + // - an external KEY / FIELD (direct), + // - an external field pointing to an external KEY / FIELD + // - an external field pointing to an external field pointing to.... + + if ($oAttDef->IsExternalKey()) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + if (self::HasChildrenClasses($sRemoteClass)) + { + // First, create an external field attribute, that gets the final class + $sClassRecallAttCode = $sAttCode.'_finalclass_recall'; + $oClassRecall = new AttributeExternalField($sClassRecallAttCode, array( + "allowed_values"=>null, + "extkey_attcode"=>$sAttCode, + "target_attcode"=>"finalclass", + "is_null_allowed"=>true, + "depends_on"=>array() + )); + $oClassRecall->SetHostClass($sClass); + self::$m_aAttribDefs[$sClass][$sClassRecallAttCode] = $oClassRecall; + self::$m_aAttribOrigins[$sClass][$sClassRecallAttCode] = $sRemoteClass; + + $oClassFlt = new FilterFromAttribute($oClassRecall); + self::$m_aFilterDefs[$sClass][$sClassRecallAttCode] = $oClassFlt; + self::$m_aFilterOrigins[$sClass][$sClassRecallAttCode] = $sRemoteClass; + + // Add it to the ZLists where the external key is present + //foreach(self::$m_aListData[$sClass] as $sListCode => $aAttributes) + $sListCode = 'list'; + if (isset(self::$m_aListData[$sClass][$sListCode])) + { + $aAttributes = self::$m_aListData[$sClass][$sListCode]; + // temporary.... no loop + { + if (in_array($sAttCode, $aAttributes)) + { + $aNewList = array(); + foreach($aAttributes as $iPos => $sAttToDisplay) + { + if (is_string($sAttToDisplay) && ($sAttToDisplay == $sAttCode)) + { + // Insert the final class right before + $aNewList[] = $sClassRecallAttCode; + } + $aNewList[] = $sAttToDisplay; + } + self::$m_aListData[$sClass][$sListCode] = $aNewList; + } + } + } + } + } + + // Get the real external key attribute + // It will be our reference to determine the other ext fields related to the same ext key + $oFinalKeyAttDef = $oAttDef->GetKeyAttDef(EXTKEY_ABSOLUTE); + + self::$m_aExtKeyFriends[$sClass][$sAttCode] = array(); + foreach (self::GetExternalFields($sClass, $oAttDef->GetKeyAttCode($sAttCode)) as $oExtField) + { + // skip : those extfields will be processed as external keys + if ($oExtField->IsExternalKey(EXTKEY_ABSOLUTE)) continue; + + // Note: I could not compare the objects by the mean of '===' + // because they are copied for the inheritance, and the internal references are NOT updated + if ($oExtField->GetKeyAttDef(EXTKEY_ABSOLUTE) == $oFinalKeyAttDef) + { + self::$m_aExtKeyFriends[$sClass][$sAttCode][$oExtField->GetCode()] = $oExtField; + } + } + } + } + + // Add a 'id' filter + // + if (array_key_exists('id', self::$m_aAttribDefs[$sClass])) + { + throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as an attribute code"); + } + if (array_key_exists('id', self::$m_aFilterDefs[$sClass])) + { + throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as a filter code"); + } + $oFilter = new FilterPrivateKey('id', array('id_field' => self::DBGetKey($sClass))); + self::$m_aFilterDefs[$sClass]['id'] = $oFilter; + self::$m_aFilterOrigins[$sClass]['id'] = $sClass; + + // Define defaults values for the standard ZLists + // + //foreach (self::$m_aListInfos as $sListCode => $aListConfig) + //{ + // if (!isset(self::$m_aListData[$sClass][$sListCode])) + // { + // $aAllAttributes = array_keys(self::$m_aAttribDefs[$sClass]); + // self::$m_aListData[$sClass][$sListCode] = $aAllAttributes; + // //echo "

$sClass: $sListCode (".count($aAllAttributes)." attributes)

\n"; + // } + //} + } + } + + // To be overriden, must be called for any object class (optimization) + public static function Init() + { + // In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation) + } + // To be overloaded by biz model declarations + public static function GetRelationQueries($sRelCode) + { + // In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation) + return array(); + } + + public static function Init_Params($aParams) + { + // Check mandatory params + $aMandatParams = array( + "category" => "group classes by modules defining their visibility in the UI", + "key_type" => "autoincrement | string", + "name_attcode" => "define wich attribute is the class name, may be an inherited attribute", + "state_attcode" => "define wich attribute is representing the state (object lifecycle)", + "reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes", + "db_table" => "database table", + "db_key_field" => "database field which is the key", + "db_finalclass_field" => "database field wich is the reference to the actual class of the object, considering that this will be a compound class", + ); + + $sClass = self::GetCallersPHPClass("Init"); + + foreach($aMandatParams as $sParamName=>$sParamDesc) + { + if (!array_key_exists($sParamName, $aParams)) + { + throw new CoreException("Declaration of class $sClass - missing parameter $sParamName"); + } + } + + $aCategories = explode(',', $aParams['category']); + foreach ($aCategories as $sCategory) + { + self::$m_Category2Class[$sCategory][] = $sClass; + } + self::$m_Category2Class[''][] = $sClass; // all categories, include this one + + + self::$m_aRootClasses[$sClass] = $sClass; // first, let consider that I am the root... updated on inheritance + self::$m_aParentClasses[$sClass] = array(); + self::$m_aChildClasses[$sClass] = array(); + + self::$m_aClassParams[$sClass]= $aParams; + + self::$m_aAttribDefs[$sClass] = array(); + self::$m_aAttribOrigins[$sClass] = array(); + self::$m_aExtKeyFriends[$sClass] = array(); + self::$m_aFilterDefs[$sClass] = array(); + self::$m_aFilterOrigins[$sClass] = array(); + } + + protected static function object_array_mergeclone($aSource1, $aSource2) + { + $aRes = array(); + foreach ($aSource1 as $key=>$object) + { + $aRes[$key] = clone $object; + } + foreach ($aSource2 as $key=>$object) + { + $aRes[$key] = clone $object; + } + return $aRes; + } + + public static function Init_InheritAttributes($sSourceClass = null) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + if (empty($sSourceClass)) + { + // Default: inherit from parent class + $sSourceClass = self::GetParentPersistentClass($sTargetClass); + if (empty($sSourceClass)) return; // no attributes for the mother of all classes + } + if (isset(self::$m_aAttribDefs[$sSourceClass])) + { + if (!isset(self::$m_aAttribDefs[$sTargetClass])) + { + self::$m_aAttribDefs[$sTargetClass] = array(); + self::$m_aAttribOrigins[$sTargetClass] = array(); + } + self::$m_aAttribDefs[$sTargetClass] = self::object_array_mergeclone(self::$m_aAttribDefs[$sTargetClass], self::$m_aAttribDefs[$sSourceClass]); + // Note: while investigating on some issues related to attribute inheritance, + // I found out that the notion of "host class" is unclear + // For stability reasons, and also because a workaround has been found + // I leave it unchanged, but later it could be a good thing to force + // attribute host class to the new class (See code below) + // In that case, we will have to review the attribute labels + // (currently relying on host class => the original declaration + // of the attribute) + // See TRAC #148 + // foreach(self::$m_aAttribDefs[$sTargetClass] as $sAttCode => $oAttDef) + // { + // $oAttDef->SetHostClass($sTargetClass); + // } + self::$m_aAttribOrigins[$sTargetClass] = array_merge(self::$m_aAttribOrigins[$sTargetClass], self::$m_aAttribOrigins[$sSourceClass]); + } + // Build root class information + if (array_key_exists($sSourceClass, self::$m_aRootClasses)) + { + // Inherit... + self::$m_aRootClasses[$sTargetClass] = self::$m_aRootClasses[$sSourceClass]; + } + else + { + // This class will be the root class + self::$m_aRootClasses[$sSourceClass] = $sSourceClass; + self::$m_aRootClasses[$sTargetClass] = $sSourceClass; + } + self::$m_aParentClasses[$sTargetClass] += self::$m_aParentClasses[$sSourceClass]; + self::$m_aParentClasses[$sTargetClass][] = $sSourceClass; + // I am the child of each and every parent... + foreach(self::$m_aParentClasses[$sTargetClass] as $sAncestorClass) + { + self::$m_aChildClasses[$sAncestorClass][] = $sTargetClass; + } + } + public static function Init_OverloadAttributeParams($sAttCode, $aParams) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + + if (!self::IsValidAttCode($sTargetClass, $sAttCode)) + { + throw new CoreException("Could not overload '$sAttCode', expecting a code from {".implode(", ", self::GetAttributesList($sTargetClass))."}"); + } + self::$m_aAttribDefs[$sTargetClass][$sAttCode]->OverloadParams($aParams); + } + + protected static function Init_IsKnownClass($sClass) + { + // Differs from self::IsValidClass() + // because it is being called before all the classes have been initialized + if (!class_exists($sClass)) return false; + if (!is_subclass_of($sClass, 'DBObject')) return false; + return true; + } + + public static function Init_AddAttribute(AttributeDefinition $oAtt) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + + // Some attributes could refer to a class + // declared in a module which is currently not installed/active + // We simply discard those attributes + // + if ($oAtt->IsLinkSet()) + { + $sRemoteClass = $oAtt->GetLinkedClass(); + if (!self::Init_IsKnownClass($sRemoteClass)) + { + self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass; + return; + } + } + elseif($oAtt->IsExternalKey()) + { + $sRemoteClass = $oAtt->GetTargetClass(); + if (!self::Init_IsKnownClass($sRemoteClass)) + { + self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass; + return; + } + } + elseif($oAtt->IsExternalField()) + { + $sExtKeyAttCode = $oAtt->GetKeyAttCode(); + if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode])) + { + // The corresponding external key has already been ignored + self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode]; + return; + } + // #@# todo - Check if the target attribute is still there + // this is not simple to implement because is involves + // several passes (the load order has a significant influence on that) + } + + self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt; + self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass; + // Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used + + // Specific case of external fields: + // I wanted to simplify the syntax of the declaration of objects in the biz model + // Therefore, the reference to the host class is set there + $oAtt->SetHostClass($sTargetClass); + } + + public static function Init_SetZListItems($sListCode, $aItems) + { + MyHelpers::CheckKeyInArray('list code', $sListCode, self::$m_aListInfos); + + $sTargetClass = self::GetCallersPHPClass("Init"); + + // Discard attributes that do not make sense + // (missing classes in the current module combination, resulting in irrelevant ext key or link set) + // + self::Init_CheckZListItems($aItems, $sTargetClass); + self::$m_aListData[$sTargetClass][$sListCode] = $aItems; + } + + protected static function Init_CheckZListItems(&$aItems, $sTargetClass) + { + foreach($aItems as $iFoo => $attCode) + { + if (is_array($attCode)) + { + self::Init_CheckZListItems($attCode, $sTargetClass); + } + else if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$attCode])) + { + unset($aItems[$iFoo]); + } + } + } + + public static function FlattenZList($aList) + { + $aResult = array(); + foreach($aList as $value) + { + if (!is_array($value)) + { + $aResult[] = $value; + } + else + { + $aResult = array_merge($aResult, self::FlattenZList($value)); + } + } + return $aResult; + } + + public static function Init_DefineState($sStateCode, $aStateDef) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + if (is_null($aStateDef['attribute_list'])) $aStateDef['attribute_list'] = array(); + + $sParentState = $aStateDef['attribute_inherit']; + if (!empty($sParentState)) + { + // Inherit from the given state (must be defined !) + // + $aToInherit = self::$m_aStates[$sTargetClass][$sParentState]; + + // Reset the constraint when it was mandatory to set the value at the previous state + // + foreach ($aToInherit['attribute_list'] as $sState => $iFlags) + { + $iFlags = $iFlags & ~OPT_ATT_MUSTPROMPT; + $iFlags = $iFlags & ~OPT_ATT_MUSTCHANGE; + $aToInherit['attribute_list'][$sState] = $iFlags; + } + + // The inherited configuration could be overriden + $aStateDef['attribute_list'] = array_merge($aToInherit['attribute_list'], $aStateDef['attribute_list']); + } + + foreach($aStateDef['attribute_list'] as $sAttCode => $iFlags) + { + if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sAttCode])) + { + unset($aStateDef['attribute_list'][$sAttCode]); + } + } + + self::$m_aStates[$sTargetClass][$sStateCode] = $aStateDef; + + // by default, create an empty set of transitions associated to that state + self::$m_aTransitions[$sTargetClass][$sStateCode] = array(); + } + + public static function Init_OverloadStateAttribute($sStateCode, $sAttCode, $iFlags) + { + // Warning: this is not sufficient: the flags have to be copied to the states that are inheriting from this state + $sTargetClass = self::GetCallersPHPClass("Init"); + self::$m_aStates[$sTargetClass][$sStateCode]['attribute_list'][$sAttCode] = $iFlags; + } + + public static function Init_DefineStimulus($oStimulus) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus; + + // I wanted to simplify the syntax of the declaration of objects in the biz model + // Therefore, the reference to the host class is set there + $oStimulus->SetHostClass($sTargetClass); + } + + public static function Init_DefineTransition($sStateCode, $sStimulusCode, $aTransitionDef) + { + $sTargetClass = self::GetCallersPHPClass("Init"); + if (is_null($aTransitionDef['actions'])) $aTransitionDef['actions'] = array(); + self::$m_aTransitions[$sTargetClass][$sStateCode][$sStimulusCode] = $aTransitionDef; + } + + public static function Init_InheritLifecycle($sSourceClass = '') + { + $sTargetClass = self::GetCallersPHPClass("Init"); + if (empty($sSourceClass)) + { + // Default: inherit from parent class + $sSourceClass = self::GetParentPersistentClass($sTargetClass); + if (empty($sSourceClass)) return; // no attributes for the mother of all classes + } + + self::$m_aClassParams[$sTargetClass]["state_attcode"] = self::$m_aClassParams[$sSourceClass]["state_attcode"]; + self::$m_aStates[$sTargetClass] = self::$m_aStates[$sSourceClass]; + // #@# Note: the aim is to clone the data, could be an issue if the simuli objects are changed + self::$m_aStimuli[$sTargetClass] = self::$m_aStimuli[$sSourceClass]; + self::$m_aTransitions[$sTargetClass] = self::$m_aTransitions[$sSourceClass]; + } + + // + // Static API + // + + public static function GetRootClass($sClass = null) + { + self::_check_subclass($sClass); + return self::$m_aRootClasses[$sClass]; + } + public static function IsRootClass($sClass) + { + self::_check_subclass($sClass); + return (self::GetRootClass($sClass) == $sClass); + } + public static function EnumRootClasses() + { + return array_unique(self::$m_aRootClasses); + } + public static function EnumParentClasses($sClass, $iOption = ENUM_PARENT_CLASSES_EXCLUDELEAF) + { + self::_check_subclass($sClass); + if ($iOption == ENUM_PARENT_CLASSES_EXCLUDELEAF) + { + return self::$m_aParentClasses[$sClass]; + } + $aRes = self::$m_aParentClasses[$sClass]; + $aRes[] = $sClass; + return $aRes; + } + public static function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP) + { + self::_check_subclass($sClass); + + $aRes = self::$m_aChildClasses[$sClass]; + if ($iOption != ENUM_CHILD_CLASSES_EXCLUDETOP) + { + // Add it to the list + $aRes[] = $sClass; + } + return $aRes; + } + public static function HasChildrenClasses($sClass) + { + return (count(self::$m_aChildClasses[$sClass]) > 0); + } + + public static function EnumCategories() + { + return array_keys(self::$m_Category2Class); + } + + // Note: use EnumChildClasses to take the compound objects into account + public static function GetSubclasses($sClass) + { + self::_check_subclass($sClass); + $aSubClasses = array(); + foreach(get_declared_classes() as $sSubClass) { + if (is_subclass_of($sSubClass, $sClass)) + { + $aSubClasses[] = $sSubClass; + } + } + return $aSubClasses; + } + public static function GetClasses($sCategory = '') + { + if (array_key_exists($sCategory, self::$m_Category2Class)) + { + return self::$m_Category2Class[$sCategory]; + } + + //if (count(self::$m_Category2Class) > 0) + //{ + // throw new CoreException("unkown class category '$sCategory', expecting a value in {".implode(', ', array_keys(self::$m_Category2Class))."}"); + //} + return array(); + } + + public static function HasTable($sClass) + { + if (strlen(self::DBGetTable($sClass)) == 0) return false; + return true; + } + + public static function IsAbstract($sClass) + { + $oReflection = new ReflectionClass($sClass); + return $oReflection->isAbstract(); + } + + protected static $m_aQueryStructCache = array(); + + protected static function PrepareQueryArguments($aArgs) + { + // Translate any object into scalars + // + $aScalarArgs = array(); + foreach($aArgs as $sArgName => $value) + { + if (self::IsValidObject($value)) + { + $aScalarArgs = array_merge($aScalarArgs, $value->ToArgs($sArgName)); + } + else + { + $aScalarArgs[$sArgName] = (string) $value; + } + } + // Add standard contextual arguments + // + $aScalarArgs['current_contact_id'] = UserRights::GetContactId(); + + return $aScalarArgs; + } + + public static function MakeSelectQuery(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false) + { + // Hide objects that are not visible to the current user + // + if (!$oFilter->IsAllDataAllowed()) + { + $oVisibleObjects = UserRights::GetSelectFilter($oFilter->GetClass()); + if ($oVisibleObjects === false) + { + // Make sure this is a valid search object, saying NO for all + $oVisibleObjects = DBObjectSearch::FromEmptySet($oFilter->GetClass()); + } + if (is_object($oVisibleObjects)) + { + $oFilter->MergeWith($oVisibleObjects); + } + else + { + // should be true at this point, meaning that no additional filtering + // is required + } + } + + if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries) + { + // Need to identify the query + $sOqlQuery = $oFilter->ToOql(); + $sOqlId = md5($sOqlQuery); + } + else + { + $sOqlQuery = "SELECTING... ".$oFilter->GetClass(); + $sOqlId = "query id ? n/a"; + } + + + // Query caching + // + if (self::$m_bQueryCacheEnabled) + { + // Warning: using directly the query string as the key to the hash array can FAIL if the string + // is long and the differences are only near the end... so it's safer (but not bullet proof?) + // to use a hash (like md5) of the string as the key ! + // + // Example of two queries that were found as similar by the hash array: + // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2 + // and + // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2 + // the only difference is R instead or O at position 285 (TTR instead of TTO)... + // + if (array_key_exists($sOqlId, self::$m_aQueryStructCache)) + { + // hit! + $oSelect = clone self::$m_aQueryStructCache[$sOqlId]; + } + } + + if (!isset($oSelect)) + { + $aTranslation = array(); + $aClassAliases = array(); + $aTableAliases = array(); + $oConditionTree = $oFilter->GetCriteria(); + + $oKPI = new ExecutionKPI(); + $oSelect = self::MakeQuery($oFilter->GetSelectedClasses(), $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oFilter, array(), array(), true /* main query */); + $oKPI->ComputeStats('MakeQuery (select)', $sOqlQuery); + + self::$m_aQueryStructCache[$sOqlId] = clone $oSelect; + } + + // Check the order by specification, and prefix with the class alias + // + $aOrderSpec = array(); + foreach ($aOrderBy as $sFieldAlias => $bAscending) + { + MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); + if (!is_bool($bAscending)) + { + throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); + } + $aOrderSpec[$oFilter->GetFirstJoinedClassAlias().$sFieldAlias] = $bAscending; + } + // By default, force the name attribute to be the ordering key + // + if (empty($aOrderSpec)) + { + foreach ($oFilter->GetSelectedClasses() as $sSelectedAlias => $sSelectedClass) + { + $sNameAttCode = self::GetNameAttributeCode($sSelectedClass); + if (!empty($sNameAttCode)) + { + // By default, simply order on the "name" attribute, ascending + $aOrderSpec[$sSelectedAlias.$sNameAttCode] = true; + } + } + } + + // Go + // + $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); + + try + { + $sRes = $oSelect->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount); + } + catch (MissingQueryArgument $e) + { + // Add some information... + $e->addInfo('OQL', $sOqlQuery); + throw $e; + } + + if (self::$m_bTraceQueries) + { + $sQueryId = md5($sRes); + if(!isset(self::$m_aQueriesLog[$sOqlId])) + { + self::$m_aQueriesLog[$sOqlId]['oql'] = $sOqlQuery; + self::$m_aQueriesLog[$sOqlId]['hits'] = 1; + } + else + { + self::$m_aQueriesLog[$sOqlId]['hits']++; + } + if(!isset(self::$m_aQueriesLog[$sOqlId]['queries'][$sQueryId])) + { + self::$m_aQueriesLog[$sOqlId]['queries'][$sQueryId]['sql'] = $sRes; + self::$m_aQueriesLog[$sOqlId]['queries'][$sQueryId]['count'] = 1; + } + else + { + self::$m_aQueriesLog[$sOqlId]['queries'][$sQueryId]['count']++; + } + } + + return $sRes; + } + + public static function ShowQueryTrace() + { + if (!self::$m_bTraceQueries) return; + + $iOqlCount = count(self::$m_aQueriesLog); + if ($iOqlCount == 0) + { + echo "

Trace activated, but no query found

\n"; + return; + } + + $iSqlCount = 0; + foreach (self::$m_aQueriesLog as $aOqlData) + { + $iSqlCount += $aOqlData['hits']; + } + echo "

Stats on SELECT queries: OQL=$iOqlCount, SQL=$iSqlCount

\n"; + + foreach (self::$m_aQueriesLog as $aOqlData) + { + $sOql = $aOqlData['oql']; + $sHits = $aOqlData['hits']; + + echo "

$sHits hits for OQL query: $sOql

\n"; + echo "
    \n"; + foreach($aOqlData['queries'] as $aSqlData) + { + $sQuery = $aSqlData['sql']; + $sSqlHits = $aSqlData['count']; + echo "
  • $sSqlHits hits for SQL: $sQuery
  • \n"; + } + echo "
\n"; + } + } + + public static function MakeDeleteQuery(DBObjectSearch $oFilter, $aArgs = array()) + { + $aTranslation = array(); + $aClassAliases = array(); + $aTableAliases = array(); + $oConditionTree = $oFilter->GetCriteria(); + $oSelect = self::MakeQuery($oFilter->GetSelectedClasses(), $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oFilter, array(), array(), true /* main query */); + $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); + return $oSelect->RenderDelete($aScalarArgs); + } + + public static function MakeUpdateQuery(DBObjectSearch $oFilter, $aValues, $aArgs = array()) + { + // $aValues is an array of $sAttCode => $value + $aTranslation = array(); + $aClassAliases = array(); + $aTableAliases = array(); + $oConditionTree = $oFilter->GetCriteria(); + $oSelect = self::MakeQuery($oFilter->GetSelectedClasses(), $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oFilter, array(), $aValues, true /* main query */); + $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); + return $oSelect->RenderUpdate($aScalarArgs); + } + + private static function MakeQuery($aSelectedClasses, &$oConditionTree, &$aClassAliases, &$aTableAliases, &$aTranslation, DBObjectSearch $oFilter, $aExpectedAtts = array(), $aValues = array(), $bIsMainQuery = false) + { + // Note: query class might be different than the class of the filter + // -> this occurs when we are linking our class to an external class (referenced by, or pointing to) + // $aExpectedAtts is an array of sAttCode=>array of columns + $sClass = $oFilter->GetFirstJoinedClass(); + $sClassAlias = $oFilter->GetFirstJoinedClassAlias(); + + $bIsOnQueriedClass = array_key_exists($sClassAlias, $aSelectedClasses); + if ($bIsOnQueriedClass) + { + $aClassAliases = array_merge($aClassAliases, $oFilter->GetJoinedClasses()); + } + + self::DbgTrace("Entering: ".$oFilter->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY").", expectedatts=".count($aExpectedAtts).": ".implode(",", array_keys($aExpectedAtts))); + + $sRootClass = self::GetRootClass($sClass); + $sKeyField = self::DBGetKey($sClass); + + if ($bIsOnQueriedClass) + { + // default to the whole list of attributes + the very std id/finalclass + $aExpectedAtts['id'][] = $sClassAlias.'id'; + foreach (self::GetAttributesList($sClass) as $sAttCode) + { + $aExpectedAtts[$sAttCode][] = $sClassAlias.$sAttCode; // alias == class and attcode + } + } + + // Compute a clear view of external keys, and external attributes + // Build the list of external keys: + // -> ext keys required by a closed join ??? + // -> ext keys mentionned in a 'pointing to' condition + // -> ext keys required for an external field + // + $aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef)) + // + // Optimization: could be computed once for all (cached) + // Could be done in MakeQuerySingleTable ??? + // + + if ($bIsOnQueriedClass) + { + // Get all Ext keys for the queried class (??) + foreach(self::GetKeysList($sClass) as $sKeyAttCode) + { + $sKeyTableClass = self::$m_aAttribOrigins[$sClass][$sKeyAttCode]; + $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); + } + } + // Get all Ext keys used by the filter + foreach ($oFilter->GetCriteria_PointingTo() as $sKeyAttCode => $trash) + { + $sKeyTableClass = self::$m_aAttribOrigins[$sClass][$sKeyAttCode]; + $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); + } + // Add the ext fields used in the select (eventually adds an external key) + foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + if ($oAttDef->IsExternalField()) + { + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + if (array_key_exists($sAttCode, $aExpectedAtts) || $oConditionTree->RequiresField($sClassAlias, $sAttCode)) + { + // Add the external attribute + $sKeyTableClass = self::$m_aAttribOrigins[$sClass][$sKeyAttCode]; + $aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef; + } + } + } + + // First query built upon on the leaf (ie current) class + // + self::DbgTrace("Main (=leaf) class, call MakeQuerySingleTable()"); + if (self::HasTable($sClass)) + { + $oSelectBase = self::MakeQuerySingleTable($aSelectedClasses, $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oFilter, $sClass, $aExpectedAtts, $aExtKeys, $aValues); + } + else + { + $oSelectBase = null; + + // As the join will not filter on the expected classes, we have to specify it explicitely + $sExpectedClasses = implode("', '", self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); + $oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')"); + $oConditionTree = $oConditionTree->LogAnd($oFinalClassRestriction); + } + + // Then we join the queries of the eventual parent classes (compound model) + foreach(self::EnumParentClasses($sClass) as $sParentClass) + { + if (!self::HasTable($sParentClass)) continue; + self::DbgTrace("Parent class: $sParentClass... let's call MakeQuerySingleTable()"); + $oSelectParentTable = self::MakeQuerySingleTable($aSelectedClasses, $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oFilter, $sParentClass, $aExpectedAtts, $aExtKeys, $aValues); + if (is_null($oSelectBase)) + { + $oSelectBase = $oSelectParentTable; + } + else + { + $oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, self::DBGetKey($sParentClass)); + } + } + + // Filter on objects referencing me + foreach ($oFilter->GetCriteria_ReferencedBy() as $sForeignClass => $aKeysAndFilters) + { + foreach ($aKeysAndFilters as $sForeignKeyAttCode => $oForeignFilter) + { + $oForeignKeyAttDef = self::GetAttributeDef($sForeignClass, $sForeignKeyAttCode); + + // We don't want any attribute from the foreign class, just filter on an inner join + $aExpAtts = array(); + + self::DbgTrace("Referenced by foreign key: $sForeignKeyAttCode... let's call MakeQuery()"); + //self::DbgTrace($oForeignFilter); + //self::DbgTrace($oForeignFilter->ToOQL()); + //self::DbgTrace($oSelectForeign); + //self::DbgTrace($oSelectForeign->RenderSelect(array())); + $oSelectForeign = self::MakeQuery($aSelectedClasses, $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oForeignFilter, $aExpAtts); + + $sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias(); + $sForeignKeyTable = $aTranslation[$sForeignClassAlias][$sForeignKeyAttCode][0]; + $sForeignKeyColumn = $aTranslation[$sForeignClassAlias][$sForeignKeyAttCode][1]; + $oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable); + } + } + + // Filter on related objects + // + foreach ($oFilter->GetCriteria_RelatedTo() as $aCritInfo) + { + $oSubFilter = $aCritInfo['flt']; + $sRelCode = $aCritInfo['relcode']; + $iMaxDepth = $aCritInfo['maxdepth']; + + // Get the starting point objects + $oStartSet = new CMDBObjectSet($oSubFilter); + + // Get the objects related to those objects... recursively... + $aRelatedObjs = $oStartSet->GetRelatedObjects($sRelCode, $iMaxDepth); + $aRestriction = array_key_exists($sRootClass, $aRelatedObjs) ? $aRelatedObjs[$sRootClass] : array(); + + // #@# todo - related objects and expressions... + // Create condition + if (count($aRestriction) > 0) + { + $oSelectBase->AddCondition($sKeyField.' IN ('.implode(', ', CMDBSource::Quote(array_keys($aRestriction), true)).')'); + } + else + { + // Quick N'dirty -> generate an empty set + $oSelectBase->AddCondition('false'); + } + } + + // Translate the conditions... and go + // + if ($bIsMainQuery) + { + $oConditionTranslated = $oConditionTree->Translate($aTranslation); + $oSelectBase->SetCondition($oConditionTranslated); + } + + // That's all... cross fingers and we'll get some working query + + //MyHelpers::var_dump_html($oSelectBase, true); + //MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true); + if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml(); + return $oSelectBase; + } + + protected static function MakeQuerySingleTable($aSelectedClasses, &$oConditionTree, &$aClassAliases, &$aTableAliases, &$aTranslation, $oFilter, $sTableClass, $aExpectedAtts, $aExtKeys, $aValues) + { + // $aExpectedAtts is an array of sAttCode=>sAlias + // $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields)) + + // Prepare the query for a single table (compound objects) + // Ignores the items (attributes/filters) that are not on the target table + // Perform an (inner or left) join for every external key (and specify the expected fields) + // + // Returns an SQLQuery + // + $sTargetClass = $oFilter->GetFirstJoinedClass(); + $sTargetAlias = $oFilter->GetFirstJoinedClassAlias(); + $sTable = self::DBGetTable($sTableClass); + $sTableAlias = self::GenerateUniqueAlias($aTableAliases, $sTargetAlias.'_'.$sTable, $sTable); + + $bIsOnQueriedClass = array_key_exists($sTargetAlias, $aSelectedClasses); + + self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$oFilter->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY").", expectedatts=".count($aExpectedAtts).": ".implode(",", array_keys($aExpectedAtts))); + + // 1 - SELECT and UPDATE + // + // Note: no need for any values nor fields for foreign Classes (ie not the queried Class) + // + $aSelect = array(); + $aUpdateValues = array(); + + // 1/a - Get the key + // + if ($bIsOnQueriedClass) + { + $aSelect[$aExpectedAtts['id'][0]] = new FieldExpression(self::DBGetKey($sTableClass), $sTableAlias); + } + // We need one pkey to be the key, let's take the one corresponding to the root class + // (used to be based on the leaf, but it may happen that this one has no table defined) + $sRootClass = self::GetRootClass($sTargetClass); + if ($sTableClass == $sRootClass) + { + $aTranslation[$sTargetAlias]['id'] = array($sTableAlias, self::DBGetKey($sTableClass)); + } + + // 1/b - Get the other attributes + // + foreach(self::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (self::$m_aAttribOrigins[$sTargetClass][$sAttCode] != $sTableClass) continue; + + // Skip this attribute if not writable (means that it does not correspond + if (count($oAttDef->GetSQLExpressions()) == 0) continue; + + // Update... + // + if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues)) + { + assert ($oAttDef->IsDirectField()); + foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue) + { + $aUpdateValues[$sColumn] = $sValue; + } + } + + // Select... + // + // Skip, if a list of fields has been specified and it is not there + if (!array_key_exists($sAttCode, $aExpectedAtts)) continue; + + if ($oAttDef->IsExternalField()) + { + // skip, this will be handled in the joined tables + } + else + { + // standard field, or external key + // add it to the output + foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) + { + foreach ($aExpectedAtts[$sAttCode] as $sAttAlias) + { + $aSelect[$sAttAlias.$sColId] = new FieldExpression($sSQLExpr, $sTableAlias); + } + } + } + } + + // 2 - WHERE + // + foreach(self::$m_aFilterDefs[$sTargetClass] as $sFltCode => $oFltAtt) + { + // Skip this filter if not defined in this table + if (self::$m_aFilterOrigins[$sTargetClass][$sFltCode] != $sTableClass) continue; + + // #@# todo - aller plus loin... a savoir que la table de translation doit contenir une "Expression" + foreach($oFltAtt->GetSQLExpressions() as $sColID => $sFltExpr) + { + // Note: I did not test it with filters relying on several expressions... + // as long as sColdID is empty, this is working, otherwise... ? + $aTranslation[$sTargetAlias][$sFltCode.$sColID] = array($sTableAlias, $sFltExpr); + } + } + + // #@# todo - See what a full text search condition should be + // 2' - WHERE / Full text search condition + // + if ($bIsOnQueriedClass) + { + $aFullText = $oFilter->GetCriteria_FullText(); + } + else + { + // Pourquoi ??? + $aFullText = array(); + } + + // 3 - The whole stuff, for this table only + // + $oSelectBase = new SQLQuery($sTable, $sTableAlias, $aSelect, null, $aFullText, $bIsOnQueriedClass, $aUpdateValues); + + // 4 - The external keys -> joins... + // + if (array_key_exists($sTableClass, $aExtKeys)) + { + foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields) + { + $oKeyAttDef = self::GetAttributeDef($sTargetClass, $sKeyAttCode); + + $oExtFilter = $oFilter->GetCriteria_PointingTo($sKeyAttCode); + + // In case the join was not explicitely defined in the filter, + // we need to do it now + if (empty($oExtFilter)) + { + $sKeyClass = $oKeyAttDef->GetTargetClass(); + $sKeyClassAlias = self::GenerateUniqueAlias($aClassAliases, $sKeyClass.'_'.$sKeyAttCode, $sKeyClass); + $oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias); + } + else + { + // The aliases should not conflict because normalization occured while building the filter + $sKeyClass = $oExtFilter->GetFirstJoinedClass(); + $sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias(); + + // Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree + } + + // Specify expected attributes for the target class query + // ... and use the current alias ! + $aExpAtts = array(); + $aIntermediateTranslation = array(); + foreach($aExtFields as $sAttCode => $oAtt) + { + + $sExtAttCode = $oAtt->GetExtAttCode(); + if (array_key_exists($sAttCode, $aExpectedAtts)) + { + // Request this attribute... transmit the alias ! + $aExpAtts[$sExtAttCode] = $aExpectedAtts[$sAttCode]; + } + // Translate mainclass.extfield => remoteclassalias.remotefieldcode + $oRemoteAttDef = self::GetAttributeDef($sKeyClass, $sExtAttCode); + foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr) + { + $aIntermediateTranslation[$sTargetAlias.$sColID][$sAttCode] = array($sKeyClassAlias, $sRemoteAttExpr); + } + //#@# debug - echo "

$sTargetAlias.$sAttCode to $sKeyClassAlias.$sRemoteAttExpr (class: $sKeyClass)

\n"; + } + $oConditionTree = $oConditionTree->Translate($aIntermediateTranslation, false); + + self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()"); + $oSelectExtKey = self::MakeQuery($aSelectedClasses, $oConditionTree, $aClassAliases, $aTableAliases, $aTranslation, $oExtFilter, $aExpAtts); + + $aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) + $sLocalKeyField = current($aCols); // get the first column for an external key + $sExternalKeyField = self::DBGetKey($sKeyClass); + self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField"); + if ($oKeyAttDef->IsNullAllowed()) + { + $oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField); + } + else + { + $oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField); + } + } + } + + //MyHelpers::var_dump_html($oSelectBase->RenderSelect()); + return $oSelectBase; + } + + public static function GenerateUniqueAlias(&$aAliases, $sNewName, $sRealName) + { + if (!array_key_exists($sNewName, $aAliases)) + { + $aAliases[$sNewName] = $sRealName; + return $sNewName; + } + + for ($i = 1 ; $i < 100 ; $i++) + { + $sAnAlias = $sNewName.$i; + if (!array_key_exists($sAnAlias, $aAliases)) + { + // Create that new alias + $aAliases[$sAnAlias] = $sRealName; + return $sAnAlias; + } + } + throw new CoreException('Failed to create an alias', array('aliases' => $aAliases, 'new'=>$sNewName)); + } + + public static function CheckDefinitions() + { + if (count(self::GetClasses()) == 0) + { + throw new CoreException("MetaModel::InitClasses() has not been called, or no class has been declared ?!?!"); + } + + $aErrors = array(); + $aSugFix = array(); + foreach (self::GetClasses() as $sClass) + { + $sNameAttCode = self::GetNameAttributeCode($sClass); + if (empty($sNameAttCode)) + { + // let's try this !!! + // $aErrors[$sClass][] = "Missing value for name definition: the framework will (should...) replace it by the id"; + // $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); + } + else if(!self::IsValidAttCode($sClass, $sNameAttCode)) + { + $aErrors[$sClass][] = "Unkown attribute code '".$sNameAttCode."' for the name definition"; + $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); + } + + foreach(self::GetReconcKeys($sClass) as $sReconcKeyAttCode) + { + if (!empty($sReconcKeyAttCode) && !self::IsValidAttCode($sClass, $sReconcKeyAttCode)) + { + $aErrors[$sClass][] = "Unkown attribute code '".$sReconcKeyAttCode."' in the list of reconciliation keys"; + $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); + } + } + + $bHasWritableAttribute = false; + foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + // It makes no sense to check the attributes again and again in the subclasses + if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue; + + if ($oAttDef->IsExternalKey()) + { + if (!self::IsValidClass($oAttDef->GetTargetClass())) + { + $aErrors[$sClass][] = "Unkown class '".$oAttDef->GetTargetClass()."' for the external key '$sAttCode'"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetClasses())."}"; + } + } + elseif ($oAttDef->IsExternalField()) + { + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + if (!self::IsValidAttCode($sClass, $sKeyAttCode) || !self::IsValidKeyAttCode($sClass, $sKeyAttCode)) + { + $aErrors[$sClass][] = "Unkown key attribute code '".$sKeyAttCode."' for the external field $sAttCode"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sClass))."}"; + } + else + { + $oKeyAttDef = self::GetAttributeDef($sClass, $sKeyAttCode); + $sTargetClass = $oKeyAttDef->GetTargetClass(); + $sExtAttCode = $oAttDef->GetExtAttCode(); + if (!self::IsValidAttCode($sTargetClass, $sExtAttCode)) + { + $aErrors[$sClass][] = "Unkown key attribute code '".$sExtAttCode."' for the external field $sAttCode"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sTargetClass))."}"; + } + } + } + else // standard attributes + { + // Check that the default values definition is a valid object! + $oValSetDef = $oAttDef->GetValuesDef(); + if (!is_null($oValSetDef) && !$oValSetDef instanceof ValueSetDefinition) + { + $aErrors[$sClass][] = "Allowed values for attribute $sAttCode is not of the relevant type"; + $aSugFix[$sClass][] = "Please set it as an instance of a ValueSetDefinition object."; + } + else + { + // Default value must be listed in the allowed values (if defined) + $aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode); + if (!is_null($aAllowedValues)) + { + $sDefaultValue = $oAttDef->GetDefaultValue(); + if (!array_key_exists($sDefaultValue, $aAllowedValues)) + { + $aErrors[$sClass][] = "Default value '".$sDefaultValue."' for attribute $sAttCode is not an allowed value"; + $aSugFix[$sClass][] = "Please pickup the default value out of {'".implode(", ", array_keys($aAllowedValues))."'}"; + } + } + } + } + // Check dependencies + if ($oAttDef->IsWritable()) + { + $bHasWritableAttribute = true; + foreach ($oAttDef->GetPrerequisiteAttributes() as $sDependOnAttCode) + { + if (!self::IsValidAttCode($sClass, $sDependOnAttCode)) + { + $aErrors[$sClass][] = "Unkown attribute code '".$sDependOnAttCode."' in the list of prerequisite attributes"; + $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); + } + } + } + } + foreach(self::GetClassFilterDefs($sClass) as $sFltCode=>$oFilterDef) + { + if (method_exists($oFilterDef, '__GetRefAttribute')) + { + $oAttDef = $oFilterDef->__GetRefAttribute(); + if (!self::IsValidAttCode($sClass, $oAttDef->GetCode())) + { + $aErrors[$sClass][] = "Wrong attribute code '".$oAttDef->GetCode()."' (wrong class) for the \"basic\" filter $sFltCode"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; + } + } + } + + // Lifecycle + // + $sStateAttCode = self::GetStateAttributeCode($sClass); + if (strlen($sStateAttCode) > 0) + { + // Lifecycle - check that the state attribute does exist as an attribute + if (!self::IsValidAttCode($sClass, $sStateAttCode)) + { + $aErrors[$sClass][] = "Unkown attribute code '".$sStateAttCode."' for the state definition"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; + } + else + { + // Lifecycle - check that there is a value set constraint on the state attribute + $aAllowedValuesRaw = self::GetAllowedValues_att($sClass, $sStateAttCode); + $aStates = array_keys(self::EnumStates($sClass)); + if (is_null($aAllowedValuesRaw)) + { + $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' will reflect the state of the object. It must be restricted to a set of values"; + $aSugFix[$sClass][] = "Please define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')]"; + } + else + { + $aAllowedValues = array_keys($aAllowedValuesRaw); + + // Lifecycle - check the the state attribute allowed values are defined states + foreach($aAllowedValues as $sValue) + { + if (!in_array($sValue, $aStates)) + { + $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has an allowed value ($sValue) which is not a known state"; + $aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states"; + } + } + + // Lifecycle - check that defined states are allowed values + foreach($aStates as $sStateValue) + { + if (!in_array($sStateValue, $aAllowedValues)) + { + $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has a state ($sStateValue) which is not an allowed value"; + $aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states"; + } + } + } + + // Lifcycle - check that the action handlers are defined + foreach (self::EnumStates($sClass) as $sStateCode => $aStateDef) + { + foreach(self::EnumTransitions($sClass, $sStateCode) as $sStimulusCode => $aTransitionDef) + { + foreach ($aTransitionDef['actions'] as $sActionHandler) + { + if (!method_exists($sClass, $sActionHandler)) + { + $aErrors[$sClass][] = "Unknown function '$sActionHandler' in transition [$sStateCode/$sStimulusCode] for state attribute '$sStateAttCode'"; + $aSugFix[$sClass][] = "Specify a function which prototype is in the form [public function $sActionHandler(\$sStimulusCode){return true;}]"; + } + } + } + } + } + } + + if ($bHasWritableAttribute) + { + if (!self::HasTable($sClass)) + { + $aErrors[$sClass][] = "No table has been defined for this class"; + $aSugFix[$sClass][] = "Either define a table name or move the attributes elsewhere"; + } + } + + + // ZList + // + foreach(self::EnumZLists() as $sListCode) + { + foreach (self::FlattenZList(self::GetZListItems($sClass, $sListCode)) as $sMyAttCode) + { + if (!self::IsValidAttCode($sClass, $sMyAttCode)) + { + $aErrors[$sClass][] = "Unkown attribute code '".$sMyAttCode."' from ZList '$sListCode'"; + $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; + } + } + } + } + + if (count($aErrors) > 0) + { + echo "
"; + echo "

Business model inconsistencies have been found

\n"; + // #@# later -> this is the responsibility of the caller to format the output + foreach ($aErrors as $sClass => $aMessages) + { + echo "

Wrong declaration for class $sClass

\n"; + echo "
    \n"; + $i = 0; + foreach ($aMessages as $sMsg) + { + echo "
  • $sMsg ({$aSugFix[$sClass][$i]})
  • \n"; + $i++; + } + echo "
\n"; + } + echo "

Aborting...

\n"; + echo "
\n"; + exit; + } + } + + public static function DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes) + { + if (empty($sRepairUrl)) return; + + // By design, some queries might be blank, we have to ignore them + $aCleanFixes = array(); + foreach($aSQLFixes as $sSQLFix) + { + if (!empty($sSQLFix)) + { + $aCleanFixes[] = $sSQLFix; + } + } + if (count($aCleanFixes) == 0) return; + + echo "
\n"; + echo " \n"; + echo " \n"; + echo "
\n"; + } + + public static function DBExists($bMustBeComplete = true) + { + // returns true if at least one table exists + // + + if (!CMDBSource::IsDB(self::$m_sDBName)) + { + return false; + } + CMDBSource::SelectDB(self::$m_sDBName); + + $aFound = array(); + $aMissing = array(); + foreach (self::DBEnumTables() as $sTable => $aClasses) + { + if (CMDBSource::IsTable($sTable)) + { + $aFound[] = $sTable; + } + else + { + $aMissing[] = $sTable; + } + } + + if (count($aFound) == 0) + { + // no expected table has been found + return false; + } + else + { + if (count($aMissing) == 0) + { + // the database is complete (still, could be some fields missing!) + return true; + } + else + { + // not all the tables, could be an older version + if ($bMustBeComplete) + { + return false; + } + else + { + return true; + } + } + } + } + + public static function DBDrop() + { + $bDropEntireDB = true; + + if (!empty(self::$m_sTablePrefix)) + { + // Do drop only tables corresponding to the sub-database (table prefix) + // then possibly drop the DB itself (if no table remain) + foreach (CMDBSource::EnumTables() as $sTable) + { + // perform a case insensitive test because on Windows the table names become lowercase :-( + if (strtolower(substr($sTable, 0, strlen(self::$m_sTablePrefix))) == strtolower(self::$m_sTablePrefix)) + { + CMDBSource::DropTable($sTable); + } + else + { + // There is at least one table which is out of the scope of the current application + $bDropEntireDB = false; + } + } + } + + if ($bDropEntireDB) + { + CMDBSource::DropDB(self::$m_sDBName); + } + } + + + public static function DBCreate() + { + // Note: we have to check if the DB does exist, because we may share the DB + // with other applications (in which case the DB does exist, not the tables with the given prefix) + if (!CMDBSource::IsDB(self::$m_sDBName)) + { + CMDBSource::CreateDB(self::$m_sDBName); + } + self::DBCreateTables(); + self::DBCreateViews(); + } + + protected static function DBCreateTables() + { + list($aErrors, $aSugFix) = self::DBCheckFormat(); + + $aSQL = array(); + foreach ($aSugFix as $sClass => $aTarget) + { + foreach ($aTarget as $aQueries) + { + foreach ($aQueries as $sQuery) + { + if (!empty($sQuery)) + { + //$aSQL[] = $sQuery; + // forces a refresh of cached information + CMDBSource::CreateTable($sQuery); + } + } + } + } + // does not work -how to have multiple statements in a single query? + // $sDoCreateAll = implode(" ; ", $aSQL); + } + + protected static function DBCreateViews() + { + list($aErrors, $aSugFix) = self::DBCheckViews(); + + $aSQL = array(); + foreach ($aSugFix as $sClass => $aTarget) + { + foreach ($aTarget as $aQueries) + { + foreach ($aQueries as $sQuery) + { + if (!empty($sQuery)) + { + //$aSQL[] = $sQuery; + // forces a refresh of cached information + CMDBSource::CreateTable($sQuery); + } + } + } + } + } + + public static function DBDump() + { + $aDataDump = array(); + foreach (self::DBEnumTables() as $sTable => $aClasses) + { + $aRows = CMDBSource::DumpTable($sTable); + $aDataDump[$sTable] = $aRows; + } + return $aDataDump; + } + + /* + * Determines wether the target DB is frozen or not + */ + public static function DBIsReadOnly() + { + // Improvement: check the mySQL variable -> Read-only + + if (UserRights::IsAdministrator()) + { + return (!self::DBHasAccess(ACCESS_ADMIN_WRITE)); + } + else + { + return (!self::DBHasAccess(ACCESS_USER_WRITE)); + } + } + + public static function DBHasAccess($iRequested = ACCESS_FULL) + { + $iMode = self::$m_oConfig->Get('access_mode'); + if (($iMode & $iRequested) == 0) return false; + return true; + } + + protected static function MakeDictEntry($sKey, $sValueFromOldSystem, $sDefaultValue, &$bNotInDico) + { + $sValue = Dict::S($sKey, 'x-no-nothing'); + if ($sValue == 'x-no-nothing') + { + $bNotInDico = true; + $sValue = $sValueFromOldSystem; + if (strlen($sValue) == 0) + { + $sValue = $sDefaultValue; + } + } + return " '$sKey' => '".str_replace("'", "\\'", $sValue)."',\n"; + } + + public static function MakeDictionaryTemplate($sModules = '', $sOutputFilter = 'NotInDictionary') + { + $sRes = ''; + + $sRes .= "// Dictionnay conventions\n"; + $sRes .= htmlentities("// Class:\n"); + $sRes .= htmlentities("// Class:+\n"); + $sRes .= htmlentities("// Class:/Attribute:\n"); + $sRes .= htmlentities("// Class:/Attribute:+\n"); + $sRes .= htmlentities("// Class:/Attribute:/Value:\n"); + $sRes .= htmlentities("// Class:/Attribute:/Value:+\n"); + $sRes .= htmlentities("// Class:/Stimulus:\n"); + $sRes .= htmlentities("// Class:/Stimulus:+\n"); + $sRes .= "\n"; + + // Note: I did not use EnumCategories(), because a given class maybe found in several categories + // Need to invent the "module", to characterize the origins of a class + if (strlen($sModules) == 0) + { + $aModules = array('bizmodel', 'core/cmdb', 'gui' , 'application', 'addon/userrights'); + } + else + { + $aModules = explode(', ', $sModules); + } + + $sRes .= "//////////////////////////////////////////////////////////////////////\n"; + $sRes .= "// Note: The classes have been grouped by categories: ".implode(', ', $aModules)."\n"; + $sRes .= "//////////////////////////////////////////////////////////////////////\n"; + + foreach ($aModules as $sCategory) + { + $sRes .= "//////////////////////////////////////////////////////////////////////\n"; + $sRes .= "// Classes in '$sCategory'\n"; + $sRes .= "//////////////////////////////////////////////////////////////////////\n"; + $sRes .= "//\n"; + $sRes .= "\n"; + foreach (self::GetClasses($sCategory) as $sClass) + { + if (!self::HasTable($sClass)) continue; + + $bNotInDico = false; + + $sClassRes = "//\n"; + $sClassRes .= "// Class: $sClass\n"; + $sClassRes .= "//\n"; + $sClassRes .= "\n"; + $sClassRes .= "Dict::Add('EN US', 'English', 'English', array(\n"; + $sClassRes .= self::MakeDictEntry("Class:$sClass", self::GetName_Obsolete($sClass), $sClass, $bNotInDico); + $sClassRes .= self::MakeDictEntry("Class:$sClass+", self::GetClassDescription_Obsolete($sClass), '', $bNotInDico); + foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + // Skip this attribute if not originaly defined in this class + if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue; + + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode", $oAttDef->GetLabel_Obsolete(), $sAttCode, $bNotInDico); + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode+", $oAttDef->GetDescription_Obsolete(), '', $bNotInDico); + if ($oAttDef instanceof AttributeEnum) + { + if (self::GetStateAttributeCode($sClass) == $sAttCode) + { + foreach (self::EnumStates($sClass) as $sStateCode => $aStateData) + { + if (array_key_exists('label', $aStateData)) + { + $sValue = $aStateData['label']; + } + else + { + $sValue = MetaModel::GetStateLabel($sClass, $sStateCode); + } + if (array_key_exists('description', $aStateData)) + { + $sValuePlus = $aStateData['description']; + } + else + { + $sValuePlus = MetaModel::GetStateDescription($sClass, $sStateCode); + } + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode", $sValue, '', $bNotInDico); + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode+", $sValuePlus, '', $bNotInDico); + } + } + else + { + foreach ($oAttDef->GetAllowedValues() as $sKey => $value) + { + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey", $value, '', $bNotInDico); + $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey+", $value, '', $bNotInDico); + } + } + } + } + foreach(self::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode", $oStimulus->GetLabel_Obsolete(), '', $bNotInDico); + $sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode+", $oStimulus->GetDescription_Obsolete(), '', $bNotInDico); + } + + $sClassRes .= "));\n"; + $sClassRes .= "\n"; + + if ($bNotInDico || ($sOutputFilter != 'NotInDictionary')) + { + $sRes .= $sClassRes; + } + } + } + return $sRes; + } + + public static function DBCheckFormat() + { + $aErrors = array(); + $aSugFix = array(); + foreach (self::GetClasses() as $sClass) + { + if (!self::HasTable($sClass)) continue; + + // Check that the table exists + // + $sTable = self::DBGetTable($sClass); + $sKeyField = self::DBGetKey($sClass); + $sAutoIncrement = (self::IsAutoIncrementKey($sClass) ? "AUTO_INCREMENT" : ""); + if (!CMDBSource::IsTable($sTable)) + { + $aErrors[$sClass]['*'][] = "table '$sTable' could not be found into the DB"; + $aSugFix[$sClass]['*'][] = "CREATE TABLE `$sTable` (`$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY) ENGINE = ".MYSQL_ENGINE." CHARACTER SET utf8 COLLATE utf8_unicode_ci"; + } + // Check that the key field exists + // + elseif (!CMDBSource::IsField($sTable, $sKeyField)) + { + $aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) could not be found"; + $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` ADD `$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY"; + } + else + { + // Check the key field properties + // + if (!CMDBSource::IsKey($sTable, $sKeyField)) + { + $aErrors[$sClass]['id'][] = "key '$sKeyField' is not a key for table '$sTable'"; + $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable`, DROP PRIMARY KEY, ADD PRIMARY key(`$sKeyField`)"; + } + if (self::IsAutoIncrementKey($sClass) && !CMDBSource::IsAutoIncrement($sTable, $sKeyField)) + { + $aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) is not automatically incremented"; + $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` CHANGE `$sKeyField` `$sKeyField` INT(11) NOT NULL AUTO_INCREMENT"; + } + } + + // Check that any defined field exists + // + $aTableInfo = CMDBSource::GetTableInfo($sTable); + + foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not originaly defined in this class + if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue; + + foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType) + { + $bIndexNeeded = $oAttDef->RequiresIndex(); + $sFieldSpecs = $oAttDef->IsNullAllowed() ? "$sDBFieldType NULL" : "$sDBFieldType NOT NULL"; + if (!CMDBSource::IsField($sTable, $sField)) + { + $aErrors[$sClass][$sAttCode][] = "field '$sField' could not be found in table '$sTable'"; + $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD `$sField` $sFieldSpecs"; + if ($bIndexNeeded) + { + $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)"; + } + } + else + { + // The field already exists, does it have the relevant properties? + // + $bToBeChanged = false; + if ($oAttDef->IsNullAllowed() != CMDBSource::IsNullAllowed($sTable, $sField)) + { + $bToBeChanged = true; + if ($oAttDef->IsNullAllowed()) + { + $aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' could be NULL"; + } + else + { + $aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' could NOT be NULL"; + } + } + $sActualFieldType = CMDBSource::GetFieldType($sTable, $sField); + if (strcasecmp($sDBFieldType, $sActualFieldType) != 0) + { + $bToBeChanged = true; + $aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' has a wrong type: found '$sActualFieldType' while expecting '$sDBFieldType'"; + } + if ($bToBeChanged) + { + $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs"; + } + + // Create indexes (external keys only... so far) + // + if ($bIndexNeeded && !CMDBSource::HasIndex($sTable, $sField)) + { + $aErrors[$sClass][$sAttCode][] = "Foreign key '$sField' in table '$sTable' should have an index"; + $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)"; + } + } + } + } + } + return array($aErrors, $aSugFix); + } + + public static function DBCheckViews() + { + $aErrors = array(); + $aSugFix = array(); + + // Reporting views (must be created after any other table) + // + foreach (self::GetClasses('bizmodel') as $sClass) + { + $sView = self::DBGetView($sClass); + if (CMDBSource::IsTable($sView)) + { + // Check that the view is complete + // + $bIsComplete = true; + foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + foreach($oAttDef->GetSQLExpressions() as $sSuffix => $sTrash) + { + $sCol = $sAttCode.$sSuffix; + if (!CMDBSource::IsField($sView, $sCol)) + { + $bIsComplete = false; + $aErrors[$sClass][$sAttCode][] = "field '$sCol' could not be found in view '$sView'"; + $aSugFix[$sClass][$sAttCode][] = ""; + } + } + } + if (!$bIsComplete) + { + // Rework the view + // + $oFilter = new DBObjectSearch($sClass, ''); + $oFilter->AllowAllData(); + $sSQL = self::MakeSelectQuery($oFilter); + $aErrors[$sClass]['*'][] = "View '$sView' is currently not complete"; + $aSugFix[$sClass]['*'][] = "ALTER VIEW `$sView` AS $sSQL"; + } + } + else + { + // Create the view + // + $oFilter = new DBObjectSearch($sClass, ''); + $oFilter->AllowAllData(); + $sSQL = self::MakeSelectQuery($oFilter); + $aErrors[$sClass]['*'][] = "Missing view for class: $sClass"; + $aSugFix[$sClass]['*'][] = "CREATE VIEW `$sView` AS $sSQL"; + } + } + return array($aErrors, $aSugFix); + } + + private static function DBCheckIntegrity_Check2Delete($sSelWrongRecs, $sErrorDesc, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel, $bProcessingFriends = false) + { + $sRootClass = self::GetRootClass($sClass); + $sTable = self::DBGetTable($sClass); + $sKeyField = self::DBGetKey($sClass); + + if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0) + { + $sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')"; + } + $aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id"); + if (count($aWrongRecords) == 0) return; + + if (!array_key_exists($sRootClass, $aErrorsAndFixes)) $aErrorsAndFixes[$sRootClass] = array(); + if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass])) $aErrorsAndFixes[$sRootClass][$sTable] = array(); + + foreach ($aWrongRecords as $iRecordId) + { + if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable])) + { + switch ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action']) + { + case 'Delete': + // Already planned for a deletion + // Let's concatenate the errors description together + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc; + break; + + case 'Update': + // Let's plan a deletion + break; + } + } + else + { + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc; + } + + if (!$bProcessingFriends) + { + if (!array_key_exists($sTable, $aPlannedDel) || !in_array($iRecordId, $aPlannedDel[$sTable])) + { + // Something new to be deleted... + $iNewDelCount++; + } + } + + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Delete'; + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array(); + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123; + $aPlannedDel[$sTable][] = $iRecordId; + } + + // Now make sure that we would delete the records of the other tables for that class + // + if (!$bProcessingFriends) + { + $sDeleteKeys = "'".implode("', '", $aWrongRecords)."'"; + foreach (self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL) as $sFriendClass) + { + $sFriendTable = self::DBGetTable($sFriendClass); + $sFriendKey = self::DBGetKey($sFriendClass); + + // skip the current table + if ($sFriendTable == $sTable) continue; + + $sFindRelatedRec = "SELECT DISTINCT maintable.`$sFriendKey` AS id FROM `$sFriendTable` AS maintable WHERE maintable.`$sFriendKey` IN ($sDeleteKeys)"; + self::DBCheckIntegrity_Check2Delete($sFindRelatedRec, "Cascading deletion of record in friend table `$sTable`", $sFriendClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel, true); + } + } + } + + private static function DBCheckIntegrity_Check2Update($sSelWrongRecs, $sErrorDesc, $sColumn, $sNewValue, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel) + { + $sRootClass = self::GetRootClass($sClass); + $sTable = self::DBGetTable($sClass); + $sKeyField = self::DBGetKey($sClass); + + if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0) + { + $sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')"; + } + $aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id"); + if (count($aWrongRecords) == 0) return; + + if (!array_key_exists($sRootClass, $aErrorsAndFixes)) $aErrorsAndFixes[$sRootClass] = array(); + if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass])) $aErrorsAndFixes[$sRootClass][$sTable] = array(); + + foreach ($aWrongRecords as $iRecordId) + { + if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable])) + { + switch ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action']) + { + case 'Delete': + // No need to update, the record will be deleted! + break; + + case 'Update': + // Already planned for an update + // Add this new update spec to the list + $bFoundSameSpec = false; + foreach ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] as $aUpdateSpec) + { + if (($sColumn == $aUpdateSpec['column']) && ($sNewValue == $aUpdateSpec['newvalue'])) + { + $bFoundSameSpec = true; + } + } + if (!$bFoundSameSpec) + { + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'][] = (array('column' => $sColumn, 'newvalue'=>$sNewValue)); + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc; + } + break; + } + } + else + { + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc; + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Update'; + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array(array('column' => $sColumn, 'newvalue'=>$sNewValue)); + $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123; + } + + } + } + + // returns the count of records found for deletion + public static function DBCheckIntegrity_SinglePass(&$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel) + { + foreach (self::GetClasses() as $sClass) + { + if (!self::HasTable($sClass)) continue; + $sRootClass = self::GetRootClass($sClass); + $sTable = self::DBGetTable($sClass); + $sKeyField = self::DBGetKey($sClass); + + if (!self::IsStandaloneClass($sClass)) + { + if (self::IsRootClass($sClass)) + { + // Check that the final class field contains the name of a class which inherited from the current class + // + $sFinalClassField = self::DBGetClassField($sClass); + + $aAllowedValues = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); + $sAllowedValues = implode(",", CMDBSource::Quote($aAllowedValues, true)); + + $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE `$sFinalClassField` NOT IN ($sAllowedValues)"; + self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "final class (field `$sFinalClassField`) is wrong (expected a value in {".$sAllowedValues."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + else + { + $sRootTable = self::DBGetTable($sRootClass); + $sRootKey = self::DBGetKey($sRootClass); + $sFinalClassField = self::DBGetClassField($sRootClass); + + $aExpectedClasses = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); + $sExpectedClasses = implode(",", CMDBSource::Quote($aExpectedClasses, true)); + + // Check that any record found here has its counterpart in the root table + // and which refers to a child class + // + $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` as maintable LEFT JOIN `$sRootTable` ON maintable.`$sKeyField` = `$sRootTable`.`$sRootKey` AND `$sRootTable`.`$sFinalClassField` IN ($sExpectedClasses) WHERE `$sRootTable`.`$sRootKey` IS NULL"; + self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in `$sTable`, but no counterpart in root table `$sRootTable` (inc. records pointing to a class in {".$sExpectedClasses."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + + // Check that any record found in the root table and referring to a child class + // has its counterpart here (detect orphan nodes -root or in the middle of the hierarchy) + // + $sSelWrongRecs = "SELECT DISTINCT maintable.`$sRootKey` AS id FROM `$sRootTable` AS maintable LEFT JOIN `$sTable` ON maintable.`$sRootKey` = `$sTable`.`$sKeyField` WHERE `$sTable`.`$sKeyField` IS NULL AND maintable.`$sFinalClassField` IN ($sExpectedClasses)"; + self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in root table `$sRootTable`, but no counterpart in table `$sTable` (root records pointing to a class in {".$sExpectedClasses."})", $sRootClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + } + + foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue; + + if ($oAttDef->IsExternalKey()) + { + // Check that any external field is pointing to an existing object + // + $sRemoteClass = $oAttDef->GetTargetClass(); + $sRemoteTable = self::DBGetTable($sRemoteClass); + $sRemoteKey = self::DBGetKey($sRemoteClass); + + $aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) + $sExtKeyField = current($aCols); // get the first column for an external key + + // Note: a class/table may have an external key on itself + $sSelBase = "SELECT DISTINCT maintable.`$sKeyField` AS id, maintable.`$sExtKeyField` AS extkey FROM `$sTable` AS maintable LEFT JOIN `$sRemoteTable` ON maintable.`$sExtKeyField` = `$sRemoteTable`.`$sRemoteKey`"; + + $sSelWrongRecs = $sSelBase." WHERE `$sRemoteTable`.`$sRemoteKey` IS NULL"; + if ($oAttDef->IsNullAllowed()) + { + // Exclude the records pointing to 0/null from the errors + $sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL"; + $sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0"; + self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '$sAttCode') non existing objects", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + else + { + self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '$sAttCode') non existing objects", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + + // Do almost the same, taking into account the records planned for deletion + if (array_key_exists($sRemoteTable, $aPlannedDel) && count($aPlannedDel[$sRemoteTable]) > 0) + { + // This could be done by the mean of a 'OR ... IN (aIgnoreRecords) + // but in that case you won't be able to track the root cause (cascading) + $sSelWrongRecs = $sSelBase." WHERE maintable.`$sExtKeyField` IN ('".implode("', '", $aPlannedDel[$sRemoteTable])."')"; + if ($oAttDef->IsNullAllowed()) + { + // Exclude the records pointing to 0/null from the errors + $sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL"; + $sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0"; + self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '$sAttCode') a record planned for deletion", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + else + { + self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '$sAttCode') a record planned for deletion", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + } + } + else if ($oAttDef->IsDirectField()) + { + // Check that the values fit the allowed values + // + $aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode); + if (!is_null($aAllowedValues) && count($aAllowedValues) > 0) + { + $sExpectedValues = implode(",", CMDBSource::Quote(array_keys($aAllowedValues), true)); + + $aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) + $sMyAttributeField = current($aCols); // get the first column for the moment + $sDefaultValue = $oAttDef->GetDefaultValue(); + $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE maintable.`$sMyAttributeField` NOT IN ($sExpectedValues)"; + self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record having a column ('$sAttCode') with an unexpected value", $sMyAttributeField, CMDBSource::Quote($sDefaultValue), $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + } + } + } + } + } + + public static function DBCheckIntegrity($sRepairUrl = "", $sSQLStatementArgName = "") + { + // Records in error, and action to be taken: delete or update + // by RootClass/Table/Record + $aErrorsAndFixes = array(); + + // Records to be ignored in the current/next pass + // by Table = array of RecordId + $aPlannedDel = array(); + + // Count of errors in the next pass: no error means that we can leave... + $iErrorCount = 0; + // Limit in case of a bug in the algorythm + $iLoopCount = 0; + + $iNewDelCount = 1; // startup... + while ($iNewDelCount > 0) + { + $iNewDelCount = 0; + self::DBCheckIntegrity_SinglePass($aErrorsAndFixes, $iNewDelCount, $aPlannedDel); + $iErrorCount += $iNewDelCount; + + // Safety net #1 - limit the planned deletions + // + $iMaxDel = 1000; + $iPlannedDel = 0; + foreach ($aPlannedDel as $sTable => $aPlannedDelOnTable) + { + $iPlannedDel += count($aPlannedDelOnTable); + } + if ($iPlannedDel > $iMaxDel) + { + throw new CoreWarning("DB Integrity Check safety net - Exceeding the limit of $iMaxDel planned record deletion"); + break; + } + // Safety net #2 - limit the iterations + // + $iLoopCount++; + $iMaxLoops = 10; + if ($iLoopCount > $iMaxLoops) + { + throw new CoreWarning("DB Integrity Check safety net - Reached the limit of $iMaxLoops loops"); + break; + } + } + + // Display the results + // + $iIssueCount = 0; + $aFixesDelete = array(); + $aFixesUpdate = array(); + + foreach ($aErrorsAndFixes as $sRootClass => $aTables) + { + foreach ($aTables as $sTable => $aRecords) + { + foreach ($aRecords as $iRecord => $aError) + { + $sAction = $aError['Action']; + $sReason = $aError['Reason']; + $iPass = $aError['Pass']; + + switch ($sAction) + { + case 'Delete': + $sActionDetails = ""; + $aFixesDelete[$sTable][] = $iRecord; + break; + + case 'Update': + $aUpdateDesc = array(); + foreach($aError['Action_Details'] as $aUpdateSpec) + { + $aUpdateDesc[] = $aUpdateSpec['column']." -> ".$aUpdateSpec['newvalue']; + $aFixesUpdate[$sTable][$aUpdateSpec['column']][$aUpdateSpec['newvalue']][] = $iRecord; + } + $sActionDetails = "Set ".implode(", ", $aUpdateDesc); + + break; + + default: + $sActionDetails = "bug: unknown action '$sAction'"; + } + $aIssues[] = "$sRootClass / $sTable / $iRecord / $sReason / $sAction / $sActionDetails"; + $iIssueCount++; + } + } + } + + if ($iIssueCount > 0) + { + // Build the queries to fix in the database + // + // First step, be able to get class data out of the table name + // Could be optimized, because we've made the job earlier... but few benefits, so... + $aTable2ClassProp = array(); + foreach (self::GetClasses() as $sClass) + { + if (!self::HasTable($sClass)) continue; + + $sRootClass = self::GetRootClass($sClass); + $sTable = self::DBGetTable($sClass); + $sKeyField = self::DBGetKey($sClass); + + $aErrorsAndFixes[$sRootClass][$sTable] = array(); + $aTable2ClassProp[$sTable] = array('rootclass'=>$sRootClass, 'class'=>$sClass, 'keyfield'=>$sKeyField); + } + // Second step, build a flat list of SQL queries + $aSQLFixes = array(); + $iPlannedUpdate = 0; + foreach ($aFixesUpdate as $sTable => $aColumns) + { + foreach ($aColumns as $sColumn => $aNewValues) + { + foreach ($aNewValues as $sNewValue => $aRecords) + { + $iPlannedUpdate += count($aRecords); + $sWrongRecords = "'".implode("', '", $aRecords)."'"; + $sKeyField = $aTable2ClassProp[$sTable]['keyfield']; + + $aSQLFixes[] = "UPDATE `$sTable` SET `$sColumn` = $sNewValue WHERE `$sKeyField` IN ($sWrongRecords)"; + } + } + } + $iPlannedDel = 0; + foreach ($aFixesDelete as $sTable => $aRecords) + { + $iPlannedDel += count($aRecords); + $sWrongRecords = "'".implode("', '", $aRecords)."'"; + $sKeyField = $aTable2ClassProp[$sTable]['keyfield']; + + $aSQLFixes[] = "DELETE FROM `$sTable` WHERE `$sKeyField` IN ($sWrongRecords)"; + } + + // Report the results + // + echo "
"; + echo "

Database corruption error(s): $iErrorCount issues have been encountered. $iPlannedDel records will be deleted, $iPlannedUpdate records will be updated:

\n"; + // #@# later -> this is the responsibility of the caller to format the output + echo "
    \n"; + foreach ($aIssues as $sIssueDesc) + { + echo "
  • $sIssueDesc
  • \n"; + } + echo "
\n"; + self::DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes); + echo "

Aborting...

\n"; + echo "
\n"; + exit; + } + } + + public static function Startup($sConfigFile, $bModelOnly = false) + { + self::LoadConfig($sConfigFile); + if ($bModelOnly) return; + + CMDBSource::SelectDB(self::$m_sDBName); + + // Some of the init could not be done earlier (requiring classes to be declared and DB to be accessible) + // To be deprecated + self::InitPlugins(); + + foreach(get_declared_classes() as $sPHPClass) + { + if (is_subclass_of($sPHPClass, 'ModuleHandlerAPI')) + { + $aCallSpec = array($sPHPClass, 'OnMetaModelStarted'); + call_user_func_array($aCallSpec, array()); + } + } + + if (false) + { + echo "Debug
\n"; + self::static_var_dump(); + } + } + + public static function LoadConfig($sConfigFile) + { + self::$m_oConfig = new Config($sConfigFile); + + // Set log ASAP + if (self::$m_oConfig->GetLogGlobal()) + { + if (self::$m_oConfig->GetLogIssue()) + { + self::$m_bLogIssue = true; + IssueLog::Enable(APPROOT.'/error.log'); + } + self::$m_bLogNotification = self::$m_oConfig->GetLogNotification(); + self::$m_bLogWebService = self::$m_oConfig->GetLogWebService(); + + ToolsLog::Enable(APPROOT.'/tools.log'); + } + else + { + self::$m_bLogIssue = false; + self::$m_bLogNotification = false; + self::$m_bLogWebService = false; + } + + if (self::$m_oConfig->GetLogKPIDuration()) + { + ExecutionKPI::EnableDuration(); + } + if (self::$m_oConfig->GetLogKPIMemory()) + { + ExecutionKPI::EnableMemory(); + } + + self::$m_bTraceQueries = self::$m_oConfig->GetDebugQueries(); + self::$m_bQueryCacheEnabled = self::$m_oConfig->GetQueryCacheEnabled(); + + self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write'); + self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys'); + + // Note: load the dictionary as soon as possible, because it might be + // needed when some error occur + foreach (self::$m_oConfig->GetDictionaries() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'dictionaries', $sToInclude); + } + // Set the language... after the dictionaries have been loaded! + Dict::SetDefaultLanguage(self::$m_oConfig->GetDefaultLanguage()); + + // Romain: this is the only way I've found to cope with the fact that + // classes have to be derived from cmdbabstract (to be editable in the UI) + require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); + + foreach (self::$m_oConfig->GetAppModules() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'application', $sToInclude); + } + foreach (self::$m_oConfig->GetDataModels() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'business', $sToInclude); + } + foreach (self::$m_oConfig->GetWebServiceCategories() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'webservice', $sToInclude); + } + foreach (self::$m_oConfig->GetAddons() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'addons', $sToInclude); + } + + $sServer = self::$m_oConfig->GetDBHost(); + $sUser = self::$m_oConfig->GetDBUser(); + $sPwd = self::$m_oConfig->GetDBPwd(); + $sSource = self::$m_oConfig->GetDBName(); + $sTablePrefix = self::$m_oConfig->GetDBSubname(); + $sCharacterSet = self::$m_oConfig->GetDBCharacterSet(); + $sCollation = self::$m_oConfig->GetDBCollation(); + + $oKPI = new ExecutionKPI(); + + // The include have been included, let's browse the existing classes and + // develop some data based on the proposed model + self::InitClasses($sTablePrefix); + + $oKPI->ComputeAndReport('Initialization of Data model structures'); + + self::$m_sDBName = $sSource; + self::$m_sTablePrefix = $sTablePrefix; + + CMDBSource::Init($sServer, $sUser, $sPwd); // do not select the DB (could not exist) + CMDBSource::SetCharacterSet($sCharacterSet, $sCollation); + } + + public static function GetModuleSetting($sModule, $sProperty, $defaultvalue = null) + { + return self::$m_oConfig->GetModuleSetting($sModule, $sProperty, $defaultvalue); + } + + public static function GetConfig() + { + return self::$m_oConfig; + } + + protected static $m_aPlugins = array(); + public static function RegisterPlugin($sType, $sName, $aInitCallSpec = array()) + { + self::$m_aPlugins[$sName] = array( + 'type' => $sType, + 'init' => $aInitCallSpec, + ); + } + + protected static function Plugin($sConfigFile, $sModuleType, $sToInclude) + { + $sFirstChar = substr($sToInclude, 0, 1); + $sSecondChar = substr($sToInclude, 1, 1); + if (($sFirstChar != '/') && ($sFirstChar != '\\') && ($sSecondChar != ':')) + { + // It is a relative path, prepend APPROOT + if (substr($sToInclude, 0, 3) == '../') + { + // Preserve compatibility with config files written before 1.0.1 + // Replace '../' by '/' + $sFile = APPROOT.'/'.substr($sToInclude, 3); + } + else + { + $sFile = APPROOT.'/'.$sToInclude; + } + } + else + { + // Leave as is - should be an absolute path + $sFile = $sToInclude; + } + if (!file_exists($sFile)) + { + throw new CoreException('Wrong filename in configuration file', array('file' => $sConfigFile, 'module' => $sModuleType, 'filename' => $sFile)); + } + require_once($sFile); + } + + // #@# to be deprecated! + // + protected static function InitPlugins() + { + foreach(self::$m_aPlugins as $sName => $aData) + { + $aCallSpec = @$aData['init']; + if (count($aCallSpec) == 2) + { + if (!is_callable($aCallSpec)) + { + throw new CoreException('Wrong declaration in plugin', array('plugin' => $aData['name'], 'type' => $aData['type'], 'class' => $aData['class'], 'init' => $aData['init'])); + } + call_user_func($aCallSpec); + } + } + } + + // Building an object + // + // + private static $aQueryCacheGetObject = array(); + private static $aQueryCacheGetObjectHits = array(); + public static function GetQueryCacheStatus() + { + $aRes = array(); + $iTotalHits = 0; + foreach(self::$aQueryCacheGetObjectHits as $sClass => $iHits) + { + $aRes[] = "$sClass: $iHits"; + $iTotalHits += $iHits; + } + return $iTotalHits.' ('.implode(', ', $aRes).')'; + } + + public static function MakeSingleRow($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false) + { + if (!array_key_exists($sClass, self::$aQueryCacheGetObject)) + { + // NOTE: Quick and VERY dirty caching mechanism which relies on + // the fact that the string '987654321' will never appear in the + // standard query + // This will be replaced for sure with a prepared statement + // or a view... next optimization to come! + $oFilter = new DBObjectSearch($sClass); + $oFilter->AddCondition('id', 987654321, '='); + if ($bAllowAllData) + { + $oFilter->AllowAllData(); + } + + $sSQL = self::MakeSelectQuery($oFilter); + self::$aQueryCacheGetObject[$sClass] = $sSQL; + self::$aQueryCacheGetObjectHits[$sClass] = 0; + } + else + { + $sSQL = self::$aQueryCacheGetObject[$sClass]; + self::$aQueryCacheGetObjectHits[$sClass] += 1; +// echo " -load $sClass/$iKey- ".self::$aQueryCacheGetObjectHits[$sClass]."
\n"; + } + $sSQL = str_replace('987654321', CMDBSource::Quote($iKey), $sSQL); + $res = CMDBSource::Query($sSQL); + + $aRow = CMDBSource::FetchArray($res); + CMDBSource::FreeResult($res); + if ($bMustBeFound && empty($aRow)) + { + throw new CoreException("No result for the single row query: '$sSQL'"); + } + return $aRow; + } + + public static function GetObjectByRow($sClass, $aRow, $sClassAlias = '') + { + self::_check_subclass($sClass); + + if (strlen($sClassAlias) == 0) + { + $sClassAlias = $sClass; + } + + // Compound objects: if available, get the final object class + // + if (!array_key_exists($sClassAlias."finalclass", $aRow)) + { + // Either this is a bug (forgot to specify a root class with a finalclass field + // Or this is the expected behavior, because the object is not made of several tables + } + elseif (empty($aRow[$sClassAlias."finalclass"])) + { + // The data is missing in the DB + // @#@ possible improvement: check that the class is valid ! + $sRootClass = self::GetRootClass($sClass); + $sFinalClassField = self::DBGetClassField($sRootClass); + throw new CoreException("Empty class name for object $sClass::{$aRow["id"]} (root class '$sRootClass', field '{$sFinalClassField}' is empty)"); + } + else + { + // do the job for the real target class + $sClass = $aRow[$sClassAlias."finalclass"]; + } + return new $sClass($aRow, $sClassAlias); + } + + public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false) + { + self::_check_subclass($sClass); + $aRow = self::MakeSingleRow($sClass, $iKey, $bMustBeFound, $bAllowAllData); + if (empty($aRow)) + { + return null; + } + return self::GetObjectByRow($sClass, $aRow); + } + + public static function GetObjectFromOQL($sQuery, $aParams = null, $bAllowAllData = false) + { + $oFilter = DBObjectSearch::FromOQL($sQuery, $aParams); + if ($bAllowAllData) + { + $oFilter->AllowAllData(); + } + $oSet = new DBObjectSet($oFilter); + $oObject = $oSet->Fetch(); + return $oObject; + } + + public static function GetHyperLink($sTargetClass, $iKey) + { + if ($iKey < 0) + { + return "$sTargetClass: $iKey (invalid value)"; + } + $oObj = self::GetObject($sTargetClass, $iKey, false); + if (is_null($oObj)) + { + return "$sTargetClass: $iKey (not found)"; + } + return $oObj->GetHyperLink(); + } + + public static function NewObject($sClass) + { + self::_check_subclass($sClass); + return new $sClass(); + } + + public static function GetNextKey($sClass) + { + $sRootClass = MetaModel::GetRootClass($sClass); + $sRootTable = MetaModel::DBGetTable($sRootClass); + $iNextKey = CMDBSource::GetNextInsertId($sRootTable); + return $iNextKey; + } + + public static function BulkDelete(DBObjectSearch $oFilter) + { + $sSQL = self::MakeDeleteQuery($oFilter); + if (!self::DBIsReadOnly()) + { + CMDBSource::Query($sSQL); + } + } + + public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues) + { + // $aValues is an array of $sAttCode => $value + $sSQL = self::MakeUpdateQuery($oFilter, $aValues); + if (!self::DBIsReadOnly()) + { + CMDBSource::Query($sSQL); + } + } + + // Links + // + // + public static function EnumReferencedClasses($sClass) + { + self::_check_subclass($sClass); + + // 1-N links (referenced by my class), returns an array of sAttCode=>sClass + $aResult = array(); + foreach(self::$m_aAttribDefs[$sClass] as $sAttCode=>$oAttDef) + { + if ($oAttDef->IsExternalKey()) + { + $aResult[$sAttCode] = $oAttDef->GetTargetClass(); + } + } + return $aResult; + } + public static function EnumReferencingClasses($sClass, $bSkipLinkingClasses = false, $bInnerJoinsOnly = false) + { + self::_check_subclass($sClass); + + if ($bSkipLinkingClasses) + { + $aLinksClasses = self::EnumLinksClasses(); + } + + // 1-N links (referencing my class), array of sClass => array of sAttcode + $aResult = array(); + foreach (self::$m_aAttribDefs as $sSomeClass=>$aClassAttributes) + { + if ($bSkipLinkingClasses && in_array($sSomeClass, $aLinksClasses)) continue; + + $aExtKeys = array(); + foreach ($aClassAttributes as $sAttCode=>$oAttDef) + { + if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) continue; + if ($oAttDef->IsExternalKey() && (self::IsParentClass($oAttDef->GetTargetClass(), $sClass))) + { + if ($bInnerJoinsOnly && $oAttDef->IsNullAllowed()) continue; + // Ok, I want this one + $aExtKeys[$sAttCode] = $oAttDef; + } + } + if (count($aExtKeys) != 0) + { + $aResult[$sSomeClass] = $aExtKeys; + } + } + return $aResult; + } + public static function EnumLinksClasses() + { + // Returns a flat array of classes having at least two external keys + $aResult = array(); + foreach (self::$m_aAttribDefs as $sSomeClass=>$aClassAttributes) + { + $iExtKeyCount = 0; + foreach ($aClassAttributes as $sAttCode=>$oAttDef) + { + if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) continue; + if ($oAttDef->IsExternalKey()) + { + $iExtKeyCount++; + } + } + if ($iExtKeyCount >= 2) + { + $aResult[] = $sSomeClass; + } + } + return $aResult; + } + public static function EnumLinkingClasses($sClass = "") + { + // N-N links, array of sLinkClass => (array of sAttCode=>sClass) + $aResult = array(); + foreach (self::EnumLinksClasses() as $sSomeClass) + { + $aTargets = array(); + $bFoundClass = false; + foreach (self::ListAttributeDefs($sSomeClass) as $sAttCode=>$oAttDef) + { + if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) continue; + if ($oAttDef->IsExternalKey()) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + if (empty($sClass)) + { + $aTargets[$sAttCode] = $sRemoteClass; + } + elseif ($sClass == $sRemoteClass) + { + $bFoundClass = true; + } + else + { + $aTargets[$sAttCode] = $sRemoteClass; + } + } + } + if (empty($sClass) || $bFoundClass) + { + $aResult[$sSomeClass] = $aTargets; + } + } + return $aResult; + } + + public static function GetLinkLabel($sLinkClass, $sAttCode) + { + self::_check_subclass($sLinkClass); + + // e.g. "supported by" (later: $this->GetLinkLabel(), computed on link data!) + return self::GetLabel($sLinkClass, $sAttCode); + } + + /** + * Replaces all the parameters by the values passed in the hash array + */ + static public function ApplyParams($aInput, $aParams) + { + $aSearches = array(); + $aReplacements = array(); + foreach($aParams as $sSearch => $replace) + { + // Some environment parameters are objects, we just need scalars + if (is_object($replace)) continue; + + $aSearches[] = '$'.$sSearch.'$'; + $aReplacements[] = (string) $replace; + } + return str_replace($aSearches, $aReplacements, $aInput); + } + +} // class MetaModel + + +// Standard attribute lists +MetaModel::RegisterZList("noneditable", array("description"=>"non editable fields", "type"=>"attributes")); + +MetaModel::RegisterZList("details", array("description"=>"All attributes to be displayed for the 'details' of an object", "type"=>"attributes")); +MetaModel::RegisterZList("list", array("description"=>"All attributes to be displayed for a list of objects", "type"=>"attributes")); +MetaModel::RegisterZList("preview", array("description"=>"All attributes visible in preview mode", "type"=>"attributes")); + +MetaModel::RegisterZList("standard_search", array("description"=>"List of criteria for the standard search", "type"=>"filters")); +MetaModel::RegisterZList("advanced_search", array("description"=>"List of criteria for the advanced search", "type"=>"filters")); + + +?> diff --git a/core/modulehandler.class.inc.php b/core/modulehandler.class.inc.php new file mode 100644 index 0000000000..b967326415 --- /dev/null +++ b/core/modulehandler.class.inc.php @@ -0,0 +1,37 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +abstract class ModuleHandlerAPI +{ + public static function OnMetaModelStarted() + { + } + + public static function OnMenuCreation() + { + } +} +?> diff --git a/core/oql/build.cmd b/core/oql/build.cmd new file mode 100644 index 0000000000..2187cec66f --- /dev/null +++ b/core/oql/build.cmd @@ -0,0 +1,3 @@ +c:\itop\php-5.2.3\php.exe -q "C:\itop\PHP-5.2.3\PEAR\PHP\LexerGenerator\cli.php" oql-lexer.plex +c:\itop\php-5.2.3\php.exe -q "C:\itop\PHP-5.2.3\PEAR\PHP\ParserGenerator\cli.php" oql-parser.y +pause \ No newline at end of file diff --git a/core/oql/exclude.txt b/core/oql/exclude.txt new file mode 100644 index 0000000000..323e1dd251 --- /dev/null +++ b/core/oql/exclude.txt @@ -0,0 +1,7 @@ +# +# The following source files are not re-distributed with the "build" of the application +# since they are used solely for constructing other files during the build process +# +build.cmd +oql-lexer.plex +oql-parser.y \ No newline at end of file diff --git a/core/oql/oql-lexer.php b/core/oql/oql-lexer.php new file mode 100644 index 0000000000..624a8bb42d --- /dev/null +++ b/core/oql/oql-lexer.php @@ -0,0 +1,564 @@ +data = $data; + $this->count = 0; + $this->line = 1; + } + + + private $_yy_state = 1; + private $_yy_stack = array(); + + function yylex() + { + return $this->{'yylex' . $this->_yy_state}(); + } + + function yypushstate($state) + { + array_push($this->_yy_stack, $this->_yy_state); + $this->_yy_state = $state; + } + + function yypopstate() + { + $this->_yy_state = array_pop($this->_yy_stack); + } + + function yybegin($state) + { + $this->_yy_state = $state; + } + + + + + function yylex1() + { + if ($this->count >= strlen($this->data)) { + return false; // end of input + } + do { + $rules = array( + '/^[ \t\n\r]+/', + '/^SELECT/', + '/^FROM/', + '/^AS/', + '/^WHERE/', + '/^JOIN/', + '/^ON/', + '/^\//', + '/^\\*/', + '/^\\+/', + '/^-/', + '/^AND/', + '/^OR/', + '/^,/', + '/^\\(/', + '/^\\)/', + '/^=/', + '/^!=/', + '/^>/', + '/^=/', + '/^<=/', + '/^LIKE/', + '/^NOT LIKE/', + '/^IN/', + '/^NOT IN/', + '/^INTERVAL/', + '/^IF/', + '/^ELT/', + '/^COALESCE/', + '/^CONCAT/', + '/^SUBSTR/', + '/^TRIM/', + '/^DATE/', + '/^DATE_FORMAT/', + '/^CURRENT_DATE/', + '/^NOW/', + '/^TIME/', + '/^TO_DAYS/', + '/^FROM_DAYS/', + '/^YEAR/', + '/^MONTH/', + '/^DAY/', + '/^HOUR/', + '/^MINUTE/', + '/^SECOND/', + '/^DATE_ADD/', + '/^DATE_SUB/', + '/^ROUND/', + '/^FLOOR/', + '/^INET_ATON/', + '/^INET_NTOA/', + '/^[0-9]+|0x[0-9a-fA-F]+/', + '/^\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/', + '/^([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/', + '/^:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/', + '/^\\./', + ); + $match = false; + foreach ($rules as $index => $rule) { + if (preg_match($rule, substr($this->data, $this->count), $yymatches)) { + if ($match) { + if (strlen($yymatches[0]) > strlen($match[0][0])) { + $match = array($yymatches, $index); // matches, token + } + } else { + $match = array($yymatches, $index); + } + } + } + if (!$match) { + throw new Exception('Unexpected input at line' . $this->line . + ': ' . $this->data[$this->count]); + } + $this->token = $match[1]; + $this->value = $match[0][0]; + $yysubmatches = $match[0]; + array_shift($yysubmatches); + if (!$yysubmatches) { + $yysubmatches = array(); + } + $r = $this->{'yy_r1_' . $this->token}($yysubmatches); + if ($r === null) { + $this->count += strlen($this->value); + $this->line += substr_count($this->value, "\n"); + // accept this token + return true; + } elseif ($r === true) { + // we have changed state + // process this token in the new state + return $this->yylex(); + } elseif ($r === false) { + $this->count += strlen($this->value); + $this->line += substr_count($this->value, "\n"); + if ($this->count >= strlen($this->data)) { + return false; // end of input + } + // skip this token + continue; + } else { + $yy_yymore_patterns = array_slice($rules, $this->token, true); + // yymore is needed + do { + if (!isset($yy_yymore_patterns[$this->token])) { + throw new Exception('cannot do yymore for the last token'); + } + $match = false; + foreach ($yy_yymore_patterns[$this->token] as $index => $rule) { + if (preg_match('/' . $rule . '/', + substr($this->data, $this->count), $yymatches)) { + $yymatches = array_filter($yymatches, 'strlen'); // remove empty sub-patterns + if ($match) { + if (strlen($yymatches[0]) > strlen($match[0][0])) { + $match = array($yymatches, $index); // matches, token + } + } else { + $match = array($yymatches, $index); + } + } + } + if (!$match) { + throw new Exception('Unexpected input at line' . $this->line . + ': ' . $this->data[$this->count]); + } + $this->token = $match[1]; + $this->value = $match[0][0]; + $yysubmatches = $match[0]; + array_shift($yysubmatches); + if (!$yysubmatches) { + $yysubmatches = array(); + } + $this->line = substr_count($this->value, "\n"); + $r = $this->{'yy_r1_' . $this->token}(); + } while ($r !== null || !$r); + if ($r === true) { + // we have changed state + // process this token in the new state + return $this->yylex(); + } else { + // accept + $this->count += strlen($this->value); + $this->line += substr_count($this->value, "\n"); + return true; + } + } + } while (true); + + } // end function + + function yy_r1_0($yy_subpatterns) + { + + return false; + } + function yy_r1_1($yy_subpatterns) + { + + $this->token = OQLParser::SELECT; + } + function yy_r1_2($yy_subpatterns) + { + + $this->token = OQLParser::FROM; + } + function yy_r1_3($yy_subpatterns) + { + + $this->token = OQLParser::AS_ALIAS; + } + function yy_r1_4($yy_subpatterns) + { + + $this->token = OQLParser::WHERE; + } + function yy_r1_5($yy_subpatterns) + { + + $this->token = OQLParser::JOIN; + } + function yy_r1_6($yy_subpatterns) + { + + $this->token = OQLParser::ON; + } + function yy_r1_7($yy_subpatterns) + { + + $this->token = OQLParser::MATH_DIV; + } + function yy_r1_8($yy_subpatterns) + { + + $this->token = OQLParser::MATH_MULT; + } + function yy_r1_9($yy_subpatterns) + { + + $this->token = OQLParser::MATH_PLUS; + } + function yy_r1_10($yy_subpatterns) + { + + $this->token = OQLParser::MATH_MINUS; + } + function yy_r1_11($yy_subpatterns) + { + + $this->token = OQLParser::LOG_AND; + } + function yy_r1_12($yy_subpatterns) + { + + $this->token = OQLParser::LOG_OR; + } + function yy_r1_13($yy_subpatterns) + { + + $this->token = OQLParser::COMA; + } + function yy_r1_14($yy_subpatterns) + { + + $this->token = OQLParser::PAR_OPEN; + } + function yy_r1_15($yy_subpatterns) + { + + $this->token = OQLParser::PAR_CLOSE; + } + function yy_r1_16($yy_subpatterns) + { + + $this->token = OQLParser::EQ; + } + function yy_r1_17($yy_subpatterns) + { + + $this->token = OQLParser::NOT_EQ; + } + function yy_r1_18($yy_subpatterns) + { + + $this->token = OQLParser::GT; + } + function yy_r1_19($yy_subpatterns) + { + + $this->token = OQLParser::LT; + } + function yy_r1_20($yy_subpatterns) + { + + $this->token = OQLParser::GE; + } + function yy_r1_21($yy_subpatterns) + { + + $this->token = OQLParser::LE; + } + function yy_r1_22($yy_subpatterns) + { + + $this->token = OQLParser::LIKE; + } + function yy_r1_23($yy_subpatterns) + { + + $this->token = OQLParser::NOT_LIKE; + } + function yy_r1_24($yy_subpatterns) + { + + $this->token = OQLParser::IN; + } + function yy_r1_25($yy_subpatterns) + { + + $this->token = OQLParser::NOT_IN; + } + function yy_r1_26($yy_subpatterns) + { + + $this->token = OQLParser::INTERVAL; + } + function yy_r1_27($yy_subpatterns) + { + + $this->token = OQLParser::F_IF; + } + function yy_r1_28($yy_subpatterns) + { + + $this->token = OQLParser::F_ELT; + } + function yy_r1_29($yy_subpatterns) + { + + $this->token = OQLParser::F_COALESCE; + } + function yy_r1_30($yy_subpatterns) + { + + $this->token = OQLParser::F_CONCAT; + } + function yy_r1_31($yy_subpatterns) + { + + $this->token = OQLParser::F_SUBSTR; + } + function yy_r1_32($yy_subpatterns) + { + + $this->token = OQLParser::F_TRIM; + } + function yy_r1_33($yy_subpatterns) + { + + $this->token = OQLParser::F_DATE; + } + function yy_r1_34($yy_subpatterns) + { + + $this->token = OQLParser::F_DATE_FORMAT; + } + function yy_r1_35($yy_subpatterns) + { + + $this->token = OQLParser::F_CURRENT_DATE; + } + function yy_r1_36($yy_subpatterns) + { + + $this->token = OQLParser::F_NOW; + } + function yy_r1_37($yy_subpatterns) + { + + $this->token = OQLParser::F_TIME; + } + function yy_r1_38($yy_subpatterns) + { + + $this->token = OQLParser::F_TO_DAYS; + } + function yy_r1_39($yy_subpatterns) + { + + $this->token = OQLParser::F_FROM_DAYS; + } + function yy_r1_40($yy_subpatterns) + { + + $this->token = OQLParser::F_YEAR; + } + function yy_r1_41($yy_subpatterns) + { + + $this->token = OQLParser::F_MONTH; + } + function yy_r1_42($yy_subpatterns) + { + + $this->token = OQLParser::F_DAY; + } + function yy_r1_43($yy_subpatterns) + { + + $this->token = OQLParser::F_HOUR; + } + function yy_r1_44($yy_subpatterns) + { + + $this->token = OQLParser::F_MINUTE; + } + function yy_r1_45($yy_subpatterns) + { + + $this->token = OQLParser::F_SECOND; + } + function yy_r1_46($yy_subpatterns) + { + + $this->token = OQLParser::F_DATE_ADD; + } + function yy_r1_47($yy_subpatterns) + { + + $this->token = OQLParser::F_DATE_SUB; + } + function yy_r1_48($yy_subpatterns) + { + + $this->token = OQLParser::F_ROUND; + } + function yy_r1_49($yy_subpatterns) + { + + $this->token = OQLParser::F_FLOOR; + } + function yy_r1_50($yy_subpatterns) + { + + $this->token = OQLParser::F_INET_ATON; + } + function yy_r1_51($yy_subpatterns) + { + + $this->token = OQLParser::F_INET_NTOA; + } + function yy_r1_52($yy_subpatterns) + { + + $this->token = OQLParser::NUMVAL; + } + function yy_r1_53($yy_subpatterns) + { + + $this->token = OQLParser::STRVAL; + } + function yy_r1_54($yy_subpatterns) + { + + $this->token = OQLParser::NAME; + } + function yy_r1_55($yy_subpatterns) + { + + $this->token = OQLParser::VARNAME; + } + function yy_r1_56($yy_subpatterns) + { + + $this->token = OQLParser::DOT; + } + + +} + +define('UNEXPECTED_INPUT_AT_LINE', 'Unexpected input at line'); + +class OQLLexerException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sUnexpected) + { + parent::__construct("Syntax error", $sInput, $iLine, $iCol, $sUnexpected); + } +} + +class OQLLexer extends OQLLexerRaw +{ + public function getTokenPos() + { + return max(0, $this->count - strlen($this->value)); + } + + function yylex() + { + try + { + return parent::yylex(); + } + catch (Exception $e) + { + $sMessage = $e->getMessage(); + if (substr($sMessage, 0, strlen(UNEXPECTED_INPUT_AT_LINE)) == UNEXPECTED_INPUT_AT_LINE) + { + $sLineAndChar = substr($sMessage, strlen(UNEXPECTED_INPUT_AT_LINE)); + if (preg_match('#^([0-9]+): (.+)$#', $sLineAndChar, $aMatches)) + { + $iLine = $aMatches[1]; + $sUnexpected = $aMatches[2]; + throw new OQLLexerException($this->data, $iLine, $this->count, $sUnexpected); + } + } + // Default: forward the exception + throw $e; + } + } +} +?> diff --git a/core/oql/oql-lexer.plex b/core/oql/oql-lexer.plex new file mode 100644 index 0000000000..1191b6c376 --- /dev/null +++ b/core/oql/oql-lexer.plex @@ -0,0 +1,357 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +// Notes (from the source file: oql-lexer.plex) - Romain +// +// The strval rule is a little bit cryptic. +// This is due to both a bug in the lexer generator and the complexity of our need +// The rule means: either a quoted string with ", or a quoted string with ' +// literal " (resp. ') must be escaped by a \ +// \ must be escaped by an additional \ +// +// Here are the issues and limitation found in the lexer generator: +// * Matching simple quotes is an issue, because regexp are not correctly escaped (and the ESC code is escaped itself) +// Workaround: insert '.chr(39).' which will be a real ' in the end +// * Matching an alternate regexp is an issue because you must specify "|^...." +// and the regexp parser will not accept that syntax +// Workaround: insert '.chr(94).' which will be a real ^ +// +// Let's analyze an overview of the regexp, we have +// 1) The strval rule in the lexer definition +// /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ +// 2) Becomes the php expression in the lexer +// (note the escaped double quotes, hopefully having no effect, but showing where the issue is!) +// $myRegexp = '/^\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/'; +// +// To be fixed in LexerGenerator/Parser.y, in doLongestMatch (doFirstMatch is ok) +// +// +// Now, let's explain how the regexp has been designed. +// Here is a simplified version, dealing with simple quotes, and based on the assumption that the lexer generator has been fixed! +// The strval rule in the lexer definition +// /'([^\\']*(\\')*(\\\\)*)*'/ +// This means anything containing \\ or \' or any other char but a standalone ' or \ +// This means ' or \ could not be found without a preceding \ +// +class OQLLexerRaw +{ + protected $data; // input string + public $token; // token id + public $value; // token string representation + protected $line; // current line + protected $count; // current column + + function __construct($data) + { + $this->data = $data; + $this->count = 0; + $this->line = 1; + } + +/*!lex2php +%input $this->data +%counter $this->count +%token $this->token +%value $this->value +%line $this->line +%matchlongest 1 +whitespace = /[ \t\n\r]+/ +select = "SELECT" +from = "FROM" +as_alias = "AS" +where = "WHERE" +join = "JOIN" +on = "ON" +coma = "," +par_open = "(" +par_close = ")" +math_div = "/" +math_mult = "*" +math_plus = "+" +math_minus = "-" +log_and = "AND" +log_or = "OR" +eq = "=" +not_eq = "!=" +gt = ">" +lt = "<" +ge = ">=" +le = "<=" +like = "LIKE" +not_like = "NOT LIKE" +in = "IN" +not_in = "NOT IN" +interval = "INTERVAL" +f_if = "IF" +f_elt = "ELT" +f_coalesce = "COALESCE" +f_concat = "CONCAT" +f_substr = "SUBSTR" +f_trim = "TRIM" +f_date = "DATE" +f_date_format = "DATE_FORMAT" +f_current_date = "CURRENT_DATE" +f_now = "NOW" +f_time = "TIME" +f_to_days = "TO_DAYS" +f_from_days = "FROM_DAYS" +f_year = "YEAR" +f_month = "MONTH" +f_day = "DAY" +f_hour = "HOUR" +f_minute = "MINUTE" +f_second = "SECOND" +f_date_add = "DATE_ADD" +f_date_sub = "DATE_SUB" +f_round = "ROUND" +f_floor = "FLOOR" +f_inet_aton = "INET_ATON" +f_inet_ntoa = "INET_NTOA" +numval = /[0-9]+|0x[0-9a-fA-F]+/ +strval = /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ +name = /([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/ +varname = /:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/ +dot = "." +*/ + +/*!lex2php +whitespace { + return false; +} +select { + $this->token = OQLParser::SELECT; +} +from { + $this->token = OQLParser::FROM; +} +as_alias { + $this->token = OQLParser::AS_ALIAS; +} +where { + $this->token = OQLParser::WHERE; +} +join { + $this->token = OQLParser::JOIN; +} +on { + $this->token = OQLParser::ON; +} +math_div { + $this->token = OQLParser::MATH_DIV; +} +math_mult { + $this->token = OQLParser::MATH_MULT; +} +math_plus { + $this->token = OQLParser::MATH_PLUS; +} +math_minus { + $this->token = OQLParser::MATH_MINUS; +} +log_and { + $this->token = OQLParser::LOG_AND; +} +log_or { + $this->token = OQLParser::LOG_OR; +} +coma { + $this->token = OQLParser::COMA; +} +par_open { + $this->token = OQLParser::PAR_OPEN; +} +par_close { + $this->token = OQLParser::PAR_CLOSE; +} +eq { + $this->token = OQLParser::EQ; +} +not_eq { + $this->token = OQLParser::NOT_EQ; +} +gt { + $this->token = OQLParser::GT; +} +lt { + $this->token = OQLParser::LT; +} +ge { + $this->token = OQLParser::GE; +} +le { + $this->token = OQLParser::LE; +} +like { + $this->token = OQLParser::LIKE; +} +not_like { + $this->token = OQLParser::NOT_LIKE; +} +in { + $this->token = OQLParser::IN; +} +not_in { + $this->token = OQLParser::NOT_IN; +} +interval { + $this->token = OQLParser::INTERVAL; +} +f_if { + $this->token = OQLParser::F_IF; +} +f_elt { + $this->token = OQLParser::F_ELT; +} +f_coalesce { + $this->token = OQLParser::F_COALESCE; +} +f_concat { + $this->token = OQLParser::F_CONCAT; +} +f_substr { + $this->token = OQLParser::F_SUBSTR; +} +f_trim { + $this->token = OQLParser::F_TRIM; +} +f_date { + $this->token = OQLParser::F_DATE; +} +f_date_format { + $this->token = OQLParser::F_DATE_FORMAT; +} +f_current_date { + $this->token = OQLParser::F_CURRENT_DATE; +} +f_now { + $this->token = OQLParser::F_NOW; +} +f_time { + $this->token = OQLParser::F_TIME; +} +f_to_days { + $this->token = OQLParser::F_TO_DAYS; +} +f_from_days { + $this->token = OQLParser::F_FROM_DAYS; +} +f_year { + $this->token = OQLParser::F_YEAR; +} +f_month { + $this->token = OQLParser::F_MONTH; +} +f_day { + $this->token = OQLParser::F_DAY; +} +f_hour { + $this->token = OQLParser::F_HOUR; +} +f_minute { + $this->token = OQLParser::F_MINUTE; +} +f_second { + $this->token = OQLParser::F_SECOND; +} +f_date_add { + $this->token = OQLParser::F_DATE_ADD; +} +f_date_sub { + $this->token = OQLParser::F_DATE_SUB; +} +f_round { + $this->token = OQLParser::F_ROUND; +} +f_floor { + $this->token = OQLParser::F_FLOOR; +} +f_inet_aton { + $this->token = OQLParser::F_INET_ATON; +} +f_inet_ntoa { + $this->token = OQLParser::F_INET_NTOA; +} +numval { + $this->token = OQLParser::NUMVAL; +} +strval { + $this->token = OQLParser::STRVAL; +} +name { + $this->token = OQLParser::NAME; +} +varname { + $this->token = OQLParser::VARNAME; +} +dot { + $this->token = OQLParser::DOT; +} +*/ + +} + +define('UNEXPECTED_INPUT_AT_LINE', 'Unexpected input at line'); + +class OQLLexerException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sUnexpected) + { + parent::__construct("Syntax error", $sInput, $iLine, $iCol, $sUnexpected); + } +} + +class OQLLexer extends OQLLexerRaw +{ + public function getTokenPos() + { + return max(0, $this->count - strlen($this->value)); + } + + function yylex() + { + try + { + return parent::yylex(); + } + catch (Exception $e) + { + $sMessage = $e->getMessage(); + if (substr($sMessage, 0, strlen(UNEXPECTED_INPUT_AT_LINE)) == UNEXPECTED_INPUT_AT_LINE) + { + $sLineAndChar = substr($sMessage, strlen(UNEXPECTED_INPUT_AT_LINE)); + if (preg_match('#^([0-9]+): (.+)$#', $sLineAndChar, $aMatches)) + { + $iLine = $aMatches[1]; + $sUnexpected = $aMatches[2]; + throw new OQLLexerException($this->data, $iLine, $this->count, $sUnexpected); + } + } + // Default: forward the exception + throw $e; + } + } +} +?> diff --git a/core/oql/oql-parser.php b/core/oql/oql-parser.php new file mode 100644 index 0000000000..d97146a5d7 --- /dev/null +++ b/core/oql/oql-parser.php @@ -0,0 +1,1740 @@ +string = $s->string; + $this->metadata = $s->metadata; + } else { + $this->string = (string) $s; + if ($m instanceof OQLParser_yyToken) { + $this->metadata = $m->metadata; + } elseif (is_array($m)) { + $this->metadata = $m; + } + } + } + + function __toString() + { + return $this->_string; + } + + function offsetExists($offset) + { + return isset($this->metadata[$offset]); + } + + function offsetGet($offset) + { + return $this->metadata[$offset]; + } + + function offsetSet($offset, $value) + { + if ($offset === null) { + if (isset($value[0])) { + $x = ($value instanceof OQLParser_yyToken) ? + $value->metadata : $value; + $this->metadata = array_merge($this->metadata, $x); + return; + } + $offset = count($this->metadata); + } + if ($value === null) { + return; + } + if ($value instanceof OQLParser_yyToken) { + if ($value->metadata) { + $this->metadata[$offset] = $value->metadata; + } + } elseif ($value) { + $this->metadata[$offset] = $value; + } + } + + function offsetUnset($offset) + { + unset($this->metadata[$offset]); + } +} + +/** The following structure represents a single element of the + * parser's stack. Information stored includes: + * + * + The state number for the parser at this level of the stack. + * + * + The value of the token stored at this level of the stack. + * (In other words, the "major" token.) + * + * + The semantic value stored at this level of the stack. This is + * the information used by the action routines in the grammar. + * It is sometimes called the "minor" token. + */ +class OQLParser_yyStackEntry +{ + public $stateno; /* The state-number */ + public $major; /* The major token value. This is the code + ** number for the token at this stack level */ + public $minor; /* The user-supplied minor token value. This + ** is the value of the token */ +}; + +// code external to the class is included here + +// declare_class is output here +#line 24 "oql-parser.y" +class OQLParserRaw#line 102 "oql-parser.php" +{ +/* First off, code is included which follows the "include_class" declaration +** in the input file. */ + +/* Next is all token values, as class constants +*/ +/* +** These constants (all generated automatically by the parser generator) +** specify the various kinds of tokens (terminals) that the parser +** understands. +** +** Each symbol here is a terminal symbol in the grammar. +*/ + const SELECT = 1; + const AS_ALIAS = 2; + const FROM = 3; + const COMA = 4; + const WHERE = 5; + const JOIN = 6; + const ON = 7; + const EQ = 8; + const PAR_OPEN = 9; + const PAR_CLOSE = 10; + const INTERVAL = 11; + const F_SECOND = 12; + const F_MINUTE = 13; + const F_HOUR = 14; + const F_DAY = 15; + const F_MONTH = 16; + const F_YEAR = 17; + const DOT = 18; + const VARNAME = 19; + const NAME = 20; + const NUMVAL = 21; + const STRVAL = 22; + const NOT_EQ = 23; + const LOG_AND = 24; + const LOG_OR = 25; + const MATH_DIV = 26; + const MATH_MULT = 27; + const MATH_PLUS = 28; + const MATH_MINUS = 29; + const GT = 30; + const LT = 31; + const GE = 32; + const LE = 33; + const LIKE = 34; + const NOT_LIKE = 35; + const IN = 36; + const NOT_IN = 37; + const F_IF = 38; + const F_ELT = 39; + const F_COALESCE = 40; + const F_CONCAT = 41; + const F_SUBSTR = 42; + const F_TRIM = 43; + const F_DATE = 44; + const F_DATE_FORMAT = 45; + const F_CURRENT_DATE = 46; + const F_NOW = 47; + const F_TIME = 48; + const F_TO_DAYS = 49; + const F_FROM_DAYS = 50; + const F_DATE_ADD = 51; + const F_DATE_SUB = 52; + const F_ROUND = 53; + const F_FLOOR = 54; + const F_INET_ATON = 55; + const F_INET_NTOA = 56; + const YY_NO_ACTION = 234; + const YY_ACCEPT_ACTION = 233; + const YY_ERROR_ACTION = 232; + +/* Next are that tables used to determine what action to take based on the +** current state and lookahead token. These tables are used to implement +** functions that take a state number and lookahead value and return an +** action integer. +** +** Suppose the action integer is N. Then the action is determined as +** follows +** +** 0 <= N < self::YYNSTATE Shift N. That is, +** push the lookahead +** token onto the stack +** and goto state N. +** +** self::YYNSTATE <= N < self::YYNSTATE+self::YYNRULE Reduce by rule N-YYNSTATE. +** +** N == self::YYNSTATE+self::YYNRULE A syntax error has occurred. +** +** N == self::YYNSTATE+self::YYNRULE+1 The parser accepts its +** input. (and concludes parsing) +** +** N == self::YYNSTATE+self::YYNRULE+2 No such action. Denotes unused +** slots in the yy_action[] table. +** +** The action table is constructed as a single large static array $yy_action. +** Given state S and lookahead X, the action is computed as +** +** self::$yy_action[self::$yy_shift_ofst[S] + X ] +** +** If the index value self::$yy_shift_ofst[S]+X is out of range or if the value +** self::$yy_lookahead[self::$yy_shift_ofst[S]+X] is not equal to X or if +** self::$yy_shift_ofst[S] is equal to self::YY_SHIFT_USE_DFLT, it means that +** the action is not in the table and that self::$yy_default[S] should be used instead. +** +** The formula above is for computing the action when the lookahead is +** a terminal symbol. If the lookahead is a non-terminal (as occurs after +** a reduce action) then the static $yy_reduce_ofst array is used in place of +** the static $yy_shift_ofst array and self::YY_REDUCE_USE_DFLT is used in place of +** self::YY_SHIFT_USE_DFLT. +** +** The following are the tables generated in this section: +** +** self::$yy_action A single table containing all actions. +** self::$yy_lookahead A table containing the lookahead for each entry in +** yy_action. Used to detect hash collisions. +** self::$yy_shift_ofst For each state, the offset into self::$yy_action for +** shifting terminals. +** self::$yy_reduce_ofst For each state, the offset into self::$yy_action for +** shifting non-terminals after a reduce. +** self::$yy_default Default action for each state. +*/ + const YY_SZ_ACTTAB = 384; +static public $yy_action = array( + /* 0 */ 4, 117, 5, 11, 8, 106, 121, 122, 130, 103, + /* 10 */ 89, 91, 82, 83, 26, 3, 134, 118, 116, 12, + /* 20 */ 105, 70, 54, 58, 60, 59, 63, 64, 57, 90, + /* 30 */ 107, 108, 127, 126, 125, 123, 124, 128, 129, 133, + /* 40 */ 132, 131, 113, 112, 81, 109, 110, 114, 16, 52, + /* 50 */ 69, 31, 30, 29, 95, 97, 4, 33, 96, 101, + /* 60 */ 49, 27, 121, 122, 130, 25, 89, 91, 82, 83, + /* 70 */ 94, 86, 85, 84, 94, 86, 85, 84, 50, 28, + /* 80 */ 141, 141, 74, 25, 53, 90, 107, 108, 127, 126, + /* 90 */ 125, 123, 124, 128, 129, 133, 132, 131, 113, 112, + /* 100 */ 81, 109, 110, 114, 4, 87, 42, 88, 93, 23, + /* 110 */ 121, 122, 130, 74, 89, 91, 82, 83, 46, 2, + /* 120 */ 7, 94, 86, 85, 84, 102, 82, 83, 19, 48, + /* 130 */ 62, 45, 105, 90, 107, 108, 127, 126, 125, 123, + /* 140 */ 124, 128, 129, 133, 132, 131, 113, 112, 81, 109, + /* 150 */ 110, 114, 233, 111, 100, 52, 56, 74, 74, 74, + /* 160 */ 6, 97, 37, 34, 96, 101, 49, 17, 38, 186, + /* 170 */ 22, 23, 14, 6, 41, 44, 76, 55, 23, 52, + /* 180 */ 94, 86, 85, 84, 50, 97, 40, 34, 96, 101, + /* 190 */ 49, 47, 20, 75, 22, 52, 14, 1, 41, 35, + /* 200 */ 61, 51, 67, 52, 94, 86, 85, 84, 50, 97, + /* 210 */ 40, 34, 96, 101, 49, 13, 104, 52, 22, 24, + /* 220 */ 14, 74, 41, 66, 50, 10, 80, 91, 94, 86, + /* 230 */ 85, 84, 50, 98, 52, 25, 36, 120, 119, 23, + /* 240 */ 97, 37, 34, 96, 101, 49, 50, 92, 74, 22, + /* 250 */ 52, 14, 43, 41, 71, 68, 51, 23, 52, 94, + /* 260 */ 86, 85, 84, 50, 97, 18, 34, 96, 101, 49, + /* 270 */ 193, 193, 99, 22, 193, 14, 193, 41, 193, 50, + /* 280 */ 9, 193, 52, 94, 86, 85, 84, 50, 97, 32, + /* 290 */ 34, 96, 101, 49, 115, 193, 193, 22, 193, 14, + /* 300 */ 193, 41, 193, 193, 193, 193, 52, 94, 86, 85, + /* 310 */ 84, 50, 97, 193, 34, 96, 101, 49, 193, 193, + /* 320 */ 193, 22, 193, 14, 193, 39, 193, 193, 193, 193, + /* 330 */ 52, 94, 86, 85, 84, 50, 97, 193, 34, 96, + /* 340 */ 101, 49, 193, 193, 193, 22, 193, 15, 65, 77, + /* 350 */ 79, 78, 73, 72, 52, 94, 86, 85, 84, 50, + /* 360 */ 97, 105, 34, 96, 101, 49, 193, 193, 193, 21, + /* 370 */ 193, 193, 193, 193, 193, 193, 193, 193, 193, 94, + /* 380 */ 86, 85, 84, 50, + ); + static public $yy_lookahead = array( + /* 0 */ 9, 8, 11, 4, 79, 10, 15, 16, 17, 10, + /* 10 */ 19, 20, 21, 22, 2, 5, 23, 92, 93, 7, + /* 20 */ 25, 28, 29, 30, 31, 32, 33, 34, 35, 38, + /* 30 */ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + /* 40 */ 49, 50, 51, 52, 53, 54, 55, 56, 1, 61, + /* 50 */ 63, 3, 4, 61, 70, 67, 9, 69, 70, 71, + /* 60 */ 72, 2, 15, 16, 17, 6, 19, 20, 21, 22, + /* 70 */ 86, 87, 88, 89, 86, 87, 88, 89, 90, 2, + /* 80 */ 3, 4, 90, 6, 61, 38, 39, 40, 41, 42, + /* 90 */ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + /* 100 */ 53, 54, 55, 56, 9, 70, 62, 36, 37, 65, + /* 110 */ 15, 16, 17, 90, 19, 20, 21, 22, 83, 4, + /* 120 */ 81, 86, 87, 88, 89, 10, 21, 22, 61, 61, + /* 130 */ 61, 64, 25, 38, 39, 40, 41, 42, 43, 44, + /* 140 */ 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + /* 150 */ 55, 56, 58, 59, 60, 61, 24, 90, 90, 90, + /* 160 */ 82, 67, 68, 69, 70, 71, 72, 8, 62, 18, + /* 170 */ 76, 65, 78, 82, 80, 62, 85, 63, 65, 61, + /* 180 */ 86, 87, 88, 89, 90, 67, 68, 69, 70, 71, + /* 190 */ 72, 73, 61, 63, 76, 61, 78, 9, 80, 18, + /* 200 */ 66, 67, 84, 61, 86, 87, 88, 89, 90, 67, + /* 210 */ 68, 69, 70, 71, 72, 7, 75, 61, 76, 61, + /* 220 */ 78, 90, 80, 67, 90, 9, 84, 20, 86, 87, + /* 230 */ 88, 89, 90, 60, 61, 6, 62, 26, 27, 65, + /* 240 */ 67, 68, 69, 70, 71, 72, 90, 90, 90, 76, + /* 250 */ 61, 78, 74, 80, 62, 66, 67, 65, 61, 86, + /* 260 */ 87, 88, 89, 90, 67, 68, 69, 70, 71, 72, + /* 270 */ 94, 94, 63, 76, 94, 78, 94, 80, 94, 90, + /* 280 */ 77, 94, 61, 86, 87, 88, 89, 90, 67, 68, + /* 290 */ 69, 70, 71, 72, 91, 94, 94, 76, 94, 78, + /* 300 */ 94, 80, 94, 94, 94, 94, 61, 86, 87, 88, + /* 310 */ 89, 90, 67, 94, 69, 70, 71, 72, 94, 94, + /* 320 */ 94, 76, 94, 78, 94, 80, 94, 94, 94, 94, + /* 330 */ 61, 86, 87, 88, 89, 90, 67, 94, 69, 70, + /* 340 */ 71, 72, 94, 94, 94, 76, 94, 78, 12, 13, + /* 350 */ 14, 15, 16, 17, 61, 86, 87, 88, 89, 90, + /* 360 */ 67, 25, 69, 70, 71, 72, 94, 94, 94, 76, + /* 370 */ 94, 94, 94, 94, 94, 94, 94, 94, 94, 86, + /* 380 */ 87, 88, 89, 90, +); + const YY_SHIFT_USE_DFLT = -10; + const YY_SHIFT_MAX = 53; + static public $yy_shift_ofst = array( + /* 0 */ 47, -9, -9, 95, 95, 95, 95, 95, 95, 95, + /* 10 */ 105, 105, 207, 207, -7, -7, 207, 207, 336, 77, + /* 20 */ 59, 211, 211, 229, 229, 207, 207, 207, 207, 229, + /* 30 */ 207, 207, -5, 71, 71, 207, 10, 107, 10, 132, + /* 40 */ 107, 132, 10, 216, 10, 48, -1, 115, 12, 188, + /* 50 */ 151, 159, 181, 208, +); + const YY_REDUCE_USE_DFLT = -76; + const YY_REDUCE_MAX = 44; + static public $yy_reduce_ofst = array( + /* 0 */ 94, 118, 142, 173, 221, 197, 245, 269, 293, -12, + /* 10 */ 35, -16, 134, 189, -75, -75, 67, 156, 91, 174, + /* 20 */ 113, 203, 203, 192, 44, 68, 23, -8, 158, 106, + /* 30 */ 69, 131, 78, 178, 178, 157, 209, 78, 114, 39, + /* 40 */ 78, 39, -13, 141, 130, +); + static public $yyExpectedTokens = array( + /* 0 */ array(1, 9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 1 */ array(9, 11, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 2 */ array(9, 11, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 3 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 4 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 5 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 6 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 7 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 8 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 9 */ array(9, 15, 16, 17, 19, 20, 21, 22, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, ), + /* 10 */ array(21, 22, ), + /* 11 */ array(21, 22, ), + /* 12 */ array(20, ), + /* 13 */ array(20, ), + /* 14 */ array(8, 23, 28, 29, 30, 31, 32, 33, 34, 35, ), + /* 15 */ array(8, 23, 28, 29, 30, 31, 32, 33, 34, 35, ), + /* 16 */ array(20, ), + /* 17 */ array(20, ), + /* 18 */ array(12, 13, 14, 15, 16, 17, 25, ), + /* 19 */ array(2, 3, 4, 6, ), + /* 20 */ array(2, 6, ), + /* 21 */ array(26, 27, ), + /* 22 */ array(26, 27, ), + /* 23 */ array(6, ), + /* 24 */ array(6, ), + /* 25 */ array(20, ), + /* 26 */ array(20, ), + /* 27 */ array(20, ), + /* 28 */ array(20, ), + /* 29 */ array(6, ), + /* 30 */ array(20, ), + /* 31 */ array(20, ), + /* 32 */ array(10, 25, ), + /* 33 */ array(36, 37, ), + /* 34 */ array(36, 37, ), + /* 35 */ array(20, ), + /* 36 */ array(5, ), + /* 37 */ array(25, ), + /* 38 */ array(5, ), + /* 39 */ array(24, ), + /* 40 */ array(25, ), + /* 41 */ array(24, ), + /* 42 */ array(5, ), + /* 43 */ array(9, ), + /* 44 */ array(5, ), + /* 45 */ array(3, 4, ), + /* 46 */ array(4, 10, ), + /* 47 */ array(4, 10, ), + /* 48 */ array(2, 7, ), + /* 49 */ array(9, ), + /* 50 */ array(18, ), + /* 51 */ array(8, ), + /* 52 */ array(18, ), + /* 53 */ array(7, ), + /* 54 */ array(), + /* 55 */ array(), + /* 56 */ array(), + /* 57 */ array(), + /* 58 */ array(), + /* 59 */ array(), + /* 60 */ array(), + /* 61 */ array(), + /* 62 */ array(), + /* 63 */ array(), + /* 64 */ array(), + /* 65 */ array(), + /* 66 */ array(), + /* 67 */ array(), + /* 68 */ array(), + /* 69 */ array(), + /* 70 */ array(), + /* 71 */ array(), + /* 72 */ array(), + /* 73 */ array(), + /* 74 */ array(), + /* 75 */ array(), + /* 76 */ array(), + /* 77 */ array(), + /* 78 */ array(), + /* 79 */ array(), + /* 80 */ array(), + /* 81 */ array(), + /* 82 */ array(), + /* 83 */ array(), + /* 84 */ array(), + /* 85 */ array(), + /* 86 */ array(), + /* 87 */ array(), + /* 88 */ array(), + /* 89 */ array(), + /* 90 */ array(), + /* 91 */ array(), + /* 92 */ array(), + /* 93 */ array(), + /* 94 */ array(), + /* 95 */ array(), + /* 96 */ array(), + /* 97 */ array(), + /* 98 */ array(), + /* 99 */ array(), + /* 100 */ array(), + /* 101 */ array(), + /* 102 */ array(), + /* 103 */ array(), + /* 104 */ array(), + /* 105 */ array(), + /* 106 */ array(), + /* 107 */ array(), + /* 108 */ array(), + /* 109 */ array(), + /* 110 */ array(), + /* 111 */ array(), + /* 112 */ array(), + /* 113 */ array(), + /* 114 */ array(), + /* 115 */ array(), + /* 116 */ array(), + /* 117 */ array(), + /* 118 */ array(), + /* 119 */ array(), + /* 120 */ array(), + /* 121 */ array(), + /* 122 */ array(), + /* 123 */ array(), + /* 124 */ array(), + /* 125 */ array(), + /* 126 */ array(), + /* 127 */ array(), + /* 128 */ array(), + /* 129 */ array(), + /* 130 */ array(), + /* 131 */ array(), + /* 132 */ array(), + /* 133 */ array(), + /* 134 */ array(), +); + static public $yy_default = array( + /* 0 */ 232, 169, 232, 232, 232, 232, 232, 232, 232, 232, + /* 10 */ 232, 232, 232, 232, 162, 163, 232, 232, 232, 147, + /* 20 */ 147, 161, 160, 146, 147, 232, 232, 232, 232, 147, + /* 30 */ 232, 232, 232, 159, 158, 232, 144, 151, 144, 165, + /* 40 */ 172, 164, 144, 232, 144, 232, 232, 232, 232, 232, + /* 50 */ 184, 232, 232, 232, 201, 140, 196, 207, 202, 204, + /* 60 */ 203, 149, 142, 205, 206, 174, 150, 170, 148, 138, + /* 70 */ 200, 145, 179, 178, 186, 139, 173, 175, 177, 176, + /* 80 */ 171, 228, 189, 190, 183, 182, 181, 167, 208, 187, + /* 90 */ 210, 188, 185, 209, 180, 168, 152, 153, 143, 137, + /* 100 */ 136, 154, 155, 166, 157, 197, 156, 211, 212, 229, + /* 110 */ 230, 135, 227, 226, 231, 191, 193, 194, 192, 199, + /* 120 */ 198, 225, 224, 216, 217, 215, 214, 213, 218, 219, + /* 130 */ 223, 222, 221, 220, 195, +); +/* The next thing included is series of defines which control +** various aspects of the generated parser. +** self::YYNOCODE is a number which corresponds +** to no legal terminal or nonterminal number. This +** number is used to fill in empty slots of the hash +** table. +** self::YYFALLBACK If defined, this indicates that one or more tokens +** have fall-back values which should be used if the +** original value of the token will not parse. +** self::YYSTACKDEPTH is the maximum depth of the parser's stack. +** self::YYNSTATE the combined number of states. +** self::YYNRULE the number of rules in the grammar +** self::YYERRORSYMBOL is the code number of the error symbol. If not +** defined, then do no error processing. +*/ + const YYNOCODE = 95; + const YYSTACKDEPTH = 100; + const YYNSTATE = 135; + const YYNRULE = 97; + const YYERRORSYMBOL = 57; + const YYERRSYMDT = 'yy0'; + const YYFALLBACK = 0; + /** The next table maps tokens into fallback tokens. If a construct + * like the following: + * + * %fallback ID X Y Z. + * + * appears in the grammer, then ID becomes a fallback token for X, Y, + * and Z. Whenever one of the tokens X, Y, or Z is input to the parser + * but it does not parse, the type of the token is changed to ID and + * the parse is retried before an error is thrown. + */ + static public $yyFallback = array( + ); + /** + * Turn parser tracing on by giving a stream to which to write the trace + * and a prompt to preface each trace message. Tracing is turned off + * by making either argument NULL + * + * Inputs: + * + * - A stream resource to which trace output should be written. + * If NULL, then tracing is turned off. + * - A prefix string written at the beginning of every + * line of trace output. If NULL, then tracing is + * turned off. + * + * Outputs: + * + * - None. + * @param resource + * @param string + */ + static function Trace($TraceFILE, $zTracePrompt) + { + if (!$TraceFILE) { + $zTracePrompt = 0; + } elseif (!$zTracePrompt) { + $TraceFILE = 0; + } + self::$yyTraceFILE = $TraceFILE; + self::$yyTracePrompt = $zTracePrompt; + } + + /** + * Output debug information to output (php://output stream) + */ + static function PrintTrace() + { + self::$yyTraceFILE = fopen('php://output', 'w'); + self::$yyTracePrompt = ''; + } + + /** + * @var resource|0 + */ + static public $yyTraceFILE; + /** + * String to prepend to debug output + * @var string|0 + */ + static public $yyTracePrompt; + /** + * @var int + */ + public $yyidx; /* Index of top element in stack */ + /** + * @var int + */ + public $yyerrcnt; /* Shifts left before out of the error */ + /** + * @var array + */ + public $yystack = array(); /* The parser's stack */ + + /** + * For tracing shifts, the names of all terminals and nonterminals + * are required. The following table supplies these names + * @var array + */ + static public $yyTokenName = array( + '$', 'SELECT', 'AS_ALIAS', 'FROM', + 'COMA', 'WHERE', 'JOIN', 'ON', + 'EQ', 'PAR_OPEN', 'PAR_CLOSE', 'INTERVAL', + 'F_SECOND', 'F_MINUTE', 'F_HOUR', 'F_DAY', + 'F_MONTH', 'F_YEAR', 'DOT', 'VARNAME', + 'NAME', 'NUMVAL', 'STRVAL', 'NOT_EQ', + 'LOG_AND', 'LOG_OR', 'MATH_DIV', 'MATH_MULT', + 'MATH_PLUS', 'MATH_MINUS', 'GT', 'LT', + 'GE', 'LE', 'LIKE', 'NOT_LIKE', + 'IN', 'NOT_IN', 'F_IF', 'F_ELT', + 'F_COALESCE', 'F_CONCAT', 'F_SUBSTR', 'F_TRIM', + 'F_DATE', 'F_DATE_FORMAT', 'F_CURRENT_DATE', 'F_NOW', + 'F_TIME', 'F_TO_DAYS', 'F_FROM_DAYS', 'F_DATE_ADD', + 'F_DATE_SUB', 'F_ROUND', 'F_FLOOR', 'F_INET_ATON', + 'F_INET_NTOA', 'error', 'result', 'query', + 'condition', 'class_name', 'join_statement', 'where_statement', + 'class_list', 'join_item', 'join_condition', 'field_id', + 'expression_prio4', 'expression_basic', 'scalar', 'var_name', + 'func_name', 'arg_list', 'list_operator', 'list', + 'expression_prio1', 'operator1', 'expression_prio2', 'operator2', + 'expression_prio3', 'operator3', 'operator4', 'scalar_list', + 'argument', 'interval_unit', 'num_scalar', 'str_scalar', + 'num_value', 'str_value', 'name', 'num_operator1', + 'num_operator2', 'str_operator', + ); + + /** + * For tracing reduce actions, the names of all rules are required. + * @var array + */ + static public $yyRuleName = array( + /* 0 */ "result ::= query", + /* 1 */ "result ::= condition", + /* 2 */ "query ::= SELECT class_name join_statement where_statement", + /* 3 */ "query ::= SELECT class_name AS_ALIAS class_name join_statement where_statement", + /* 4 */ "query ::= SELECT class_list FROM class_name join_statement where_statement", + /* 5 */ "query ::= SELECT class_list FROM class_name AS_ALIAS class_name join_statement where_statement", + /* 6 */ "class_list ::= class_name", + /* 7 */ "class_list ::= class_list COMA class_name", + /* 8 */ "where_statement ::= WHERE condition", + /* 9 */ "where_statement ::=", + /* 10 */ "join_statement ::= join_item join_statement", + /* 11 */ "join_statement ::= join_item", + /* 12 */ "join_statement ::=", + /* 13 */ "join_item ::= JOIN class_name AS_ALIAS class_name ON join_condition", + /* 14 */ "join_item ::= JOIN class_name ON join_condition", + /* 15 */ "join_condition ::= field_id EQ field_id", + /* 16 */ "condition ::= expression_prio4", + /* 17 */ "expression_basic ::= scalar", + /* 18 */ "expression_basic ::= field_id", + /* 19 */ "expression_basic ::= var_name", + /* 20 */ "expression_basic ::= func_name PAR_OPEN arg_list PAR_CLOSE", + /* 21 */ "expression_basic ::= PAR_OPEN expression_prio4 PAR_CLOSE", + /* 22 */ "expression_basic ::= expression_basic list_operator list", + /* 23 */ "expression_prio1 ::= expression_basic", + /* 24 */ "expression_prio1 ::= expression_prio1 operator1 expression_basic", + /* 25 */ "expression_prio2 ::= expression_prio1", + /* 26 */ "expression_prio2 ::= expression_prio2 operator2 expression_prio1", + /* 27 */ "expression_prio3 ::= expression_prio2", + /* 28 */ "expression_prio3 ::= expression_prio3 operator3 expression_prio2", + /* 29 */ "expression_prio4 ::= expression_prio3", + /* 30 */ "expression_prio4 ::= expression_prio4 operator4 expression_prio3", + /* 31 */ "list ::= PAR_OPEN scalar_list PAR_CLOSE", + /* 32 */ "scalar_list ::= scalar", + /* 33 */ "scalar_list ::= scalar_list COMA scalar", + /* 34 */ "arg_list ::=", + /* 35 */ "arg_list ::= argument", + /* 36 */ "arg_list ::= arg_list COMA argument", + /* 37 */ "argument ::= expression_prio4", + /* 38 */ "argument ::= INTERVAL expression_prio4 interval_unit", + /* 39 */ "interval_unit ::= F_SECOND", + /* 40 */ "interval_unit ::= F_MINUTE", + /* 41 */ "interval_unit ::= F_HOUR", + /* 42 */ "interval_unit ::= F_DAY", + /* 43 */ "interval_unit ::= F_MONTH", + /* 44 */ "interval_unit ::= F_YEAR", + /* 45 */ "scalar ::= num_scalar", + /* 46 */ "scalar ::= str_scalar", + /* 47 */ "num_scalar ::= num_value", + /* 48 */ "str_scalar ::= str_value", + /* 49 */ "field_id ::= name", + /* 50 */ "field_id ::= class_name DOT name", + /* 51 */ "class_name ::= name", + /* 52 */ "var_name ::= VARNAME", + /* 53 */ "name ::= NAME", + /* 54 */ "num_value ::= NUMVAL", + /* 55 */ "str_value ::= STRVAL", + /* 56 */ "operator1 ::= num_operator1", + /* 57 */ "operator2 ::= num_operator2", + /* 58 */ "operator2 ::= str_operator", + /* 59 */ "operator2 ::= EQ", + /* 60 */ "operator2 ::= NOT_EQ", + /* 61 */ "operator3 ::= LOG_AND", + /* 62 */ "operator4 ::= LOG_OR", + /* 63 */ "num_operator1 ::= MATH_DIV", + /* 64 */ "num_operator1 ::= MATH_MULT", + /* 65 */ "num_operator2 ::= MATH_PLUS", + /* 66 */ "num_operator2 ::= MATH_MINUS", + /* 67 */ "num_operator2 ::= GT", + /* 68 */ "num_operator2 ::= LT", + /* 69 */ "num_operator2 ::= GE", + /* 70 */ "num_operator2 ::= LE", + /* 71 */ "str_operator ::= LIKE", + /* 72 */ "str_operator ::= NOT_LIKE", + /* 73 */ "list_operator ::= IN", + /* 74 */ "list_operator ::= NOT_IN", + /* 75 */ "func_name ::= F_IF", + /* 76 */ "func_name ::= F_ELT", + /* 77 */ "func_name ::= F_COALESCE", + /* 78 */ "func_name ::= F_CONCAT", + /* 79 */ "func_name ::= F_SUBSTR", + /* 80 */ "func_name ::= F_TRIM", + /* 81 */ "func_name ::= F_DATE", + /* 82 */ "func_name ::= F_DATE_FORMAT", + /* 83 */ "func_name ::= F_CURRENT_DATE", + /* 84 */ "func_name ::= F_NOW", + /* 85 */ "func_name ::= F_TIME", + /* 86 */ "func_name ::= F_TO_DAYS", + /* 87 */ "func_name ::= F_FROM_DAYS", + /* 88 */ "func_name ::= F_YEAR", + /* 89 */ "func_name ::= F_MONTH", + /* 90 */ "func_name ::= F_DAY", + /* 91 */ "func_name ::= F_DATE_ADD", + /* 92 */ "func_name ::= F_DATE_SUB", + /* 93 */ "func_name ::= F_ROUND", + /* 94 */ "func_name ::= F_FLOOR", + /* 95 */ "func_name ::= F_INET_ATON", + /* 96 */ "func_name ::= F_INET_NTOA", + ); + + /** + * This function returns the symbolic name associated with a token + * value. + * @param int + * @return string + */ + function tokenName($tokenType) + { + if ($tokenType === 0) { + return 'End of Input'; + } + if ($tokenType > 0 && $tokenType < count(self::$yyTokenName)) { + return self::$yyTokenName[$tokenType]; + } else { + return "Unknown"; + } + } + + /** + * The following function deletes the value associated with a + * symbol. The symbol can be either a terminal or nonterminal. + * @param int the symbol code + * @param mixed the symbol's value + */ + static function yy_destructor($yymajor, $yypminor) + { + switch ($yymajor) { + /* Here is inserted the actions which take place when a + ** terminal or non-terminal is destroyed. This can happen + ** when the symbol is popped from the stack during a + ** reduce or during error processing or when a parser is + ** being destroyed before it is finished parsing. + ** + ** Note: during a reduce, the only symbols destroyed are those + ** which appear on the RHS of the rule, but which are not used + ** inside the C code. + */ + default: break; /* If no destructor action specified: do nothing */ + } + } + + /** + * Pop the parser's stack once. + * + * If there is a destructor routine associated with the token which + * is popped from the stack, then call it. + * + * Return the major token number for the symbol popped. + * @param OQLParser_yyParser + * @return int + */ + function yy_pop_parser_stack() + { + if (!count($this->yystack)) { + return; + } + $yytos = array_pop($this->yystack); + if (self::$yyTraceFILE && $this->yyidx >= 0) { + fwrite(self::$yyTraceFILE, + self::$yyTracePrompt . 'Popping ' . self::$yyTokenName[$yytos->major] . + "\n"); + } + $yymajor = $yytos->major; + self::yy_destructor($yymajor, $yytos->minor); + $this->yyidx--; + return $yymajor; + } + + /** + * Deallocate and destroy a parser. Destructors are all called for + * all stack elements before shutting the parser down. + */ + function __destruct() + { + while ($this->yyidx >= 0) { + $this->yy_pop_parser_stack(); + } + if (is_resource(self::$yyTraceFILE)) { + fclose(self::$yyTraceFILE); + } + } + + /** + * Based on the current state and parser stack, get a list of all + * possible lookahead tokens + * @param int + * @return array + */ + function yy_get_expected_tokens($token) + { + $state = $this->yystack[$this->yyidx]->stateno; + $expected = self::$yyExpectedTokens[$state]; + if (in_array($token, self::$yyExpectedTokens[$state], true)) { + return $expected; + } + $stack = $this->yystack; + $yyidx = $this->yyidx; + do { + $yyact = $this->yy_find_shift_action($token); + if ($yyact >= self::YYNSTATE && $yyact < self::YYNSTATE + self::YYNRULE) { + // reduce action + $done = 0; + do { + if ($done++ == 100) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + // too much recursion prevents proper detection + // so give up + return array_unique($expected); + } + $yyruleno = $yyact - self::YYNSTATE; + $this->yyidx -= self::$yyRuleInfo[$yyruleno]['rhs']; + $nextstate = $this->yy_find_reduce_action( + $this->yystack[$this->yyidx]->stateno, + self::$yyRuleInfo[$yyruleno]['lhs']); + if (isset(self::$yyExpectedTokens[$nextstate])) { + $expected += self::$yyExpectedTokens[$nextstate]; + if (in_array($token, + self::$yyExpectedTokens[$nextstate], true)) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + return array_unique($expected); + } + } + if ($nextstate < self::YYNSTATE) { + // we need to shift a non-terminal + $this->yyidx++; + $x = new OQLParser_yyStackEntry; + $x->stateno = $nextstate; + $x->major = self::$yyRuleInfo[$yyruleno]['lhs']; + $this->yystack[$this->yyidx] = $x; + continue 2; + } elseif ($nextstate == self::YYNSTATE + self::YYNRULE + 1) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + // the last token was just ignored, we can't accept + // by ignoring input, this is in essence ignoring a + // syntax error! + return array_unique($expected); + } elseif ($nextstate === self::YY_NO_ACTION) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + // input accepted, but not shifted (I guess) + return $expected; + } else { + $yyact = $nextstate; + } + } while (true); + } + break; + } while (true); + return array_unique($expected); + } + + /** + * Based on the parser state and current parser stack, determine whether + * the lookahead token is possible. + * + * The parser will convert the token value to an error token if not. This + * catches some unusual edge cases where the parser would fail. + * @param int + * @return bool + */ + function yy_is_expected_token($token) + { + if ($token === 0) { + return true; // 0 is not part of this + } + $state = $this->yystack[$this->yyidx]->stateno; + if (in_array($token, self::$yyExpectedTokens[$state], true)) { + return true; + } + $stack = $this->yystack; + $yyidx = $this->yyidx; + do { + $yyact = $this->yy_find_shift_action($token); + if ($yyact >= self::YYNSTATE && $yyact < self::YYNSTATE + self::YYNRULE) { + // reduce action + $done = 0; + do { + if ($done++ == 100) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + // too much recursion prevents proper detection + // so give up + return true; + } + $yyruleno = $yyact - self::YYNSTATE; + $this->yyidx -= self::$yyRuleInfo[$yyruleno]['rhs']; + $nextstate = $this->yy_find_reduce_action( + $this->yystack[$this->yyidx]->stateno, + self::$yyRuleInfo[$yyruleno]['lhs']); + if (isset(self::$yyExpectedTokens[$nextstate]) && + in_array($token, self::$yyExpectedTokens[$nextstate], true)) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + return true; + } + if ($nextstate < self::YYNSTATE) { + // we need to shift a non-terminal + $this->yyidx++; + $x = new OQLParser_yyStackEntry; + $x->stateno = $nextstate; + $x->major = self::$yyRuleInfo[$yyruleno]['lhs']; + $this->yystack[$this->yyidx] = $x; + continue 2; + } elseif ($nextstate == self::YYNSTATE + self::YYNRULE + 1) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + if (!$token) { + // end of input: this is valid + return true; + } + // the last token was just ignored, we can't accept + // by ignoring input, this is in essence ignoring a + // syntax error! + return false; + } elseif ($nextstate === self::YY_NO_ACTION) { + $this->yyidx = $yyidx; + $this->yystack = $stack; + // input accepted, but not shifted (I guess) + return true; + } else { + $yyact = $nextstate; + } + } while (true); + } + break; + } while (true); + $this->yyidx = $yyidx; + $this->yystack = $stack; + return true; + } + + /** + * Find the appropriate action for a parser given the terminal + * look-ahead token iLookAhead. + * + * If the look-ahead token is YYNOCODE, then check to see if the action is + * independent of the look-ahead. If it is, return the action, otherwise + * return YY_NO_ACTION. + * @param int The look-ahead token + */ + function yy_find_shift_action($iLookAhead) + { + $stateno = $this->yystack[$this->yyidx]->stateno; + + /* if ($this->yyidx < 0) return self::YY_NO_ACTION; */ + if (!isset(self::$yy_shift_ofst[$stateno])) { + // no shift actions + return self::$yy_default[$stateno]; + } + $i = self::$yy_shift_ofst[$stateno]; + if ($i === self::YY_SHIFT_USE_DFLT) { + return self::$yy_default[$stateno]; + } + if ($iLookAhead == self::YYNOCODE) { + return self::YY_NO_ACTION; + } + $i += $iLookAhead; + if ($i < 0 || $i >= self::YY_SZ_ACTTAB || + self::$yy_lookahead[$i] != $iLookAhead) { + if (count(self::$yyFallback) && $iLookAhead < count(self::$yyFallback) + && ($iFallback = self::$yyFallback[$iLookAhead]) != 0) { + if (self::$yyTraceFILE) { + fwrite(self::$yyTraceFILE, self::$yyTracePrompt . "FALLBACK " . + self::$yyTokenName[$iLookAhead] . " => " . + self::$yyTokenName[$iFallback] . "\n"); + } + return $this->yy_find_shift_action($iFallback); + } + return self::$yy_default[$stateno]; + } else { + return self::$yy_action[$i]; + } + } + + /** + * Find the appropriate action for a parser given the non-terminal + * look-ahead token $iLookAhead. + * + * If the look-ahead token is self::YYNOCODE, then check to see if the action is + * independent of the look-ahead. If it is, return the action, otherwise + * return self::YY_NO_ACTION. + * @param int Current state number + * @param int The look-ahead token + */ + function yy_find_reduce_action($stateno, $iLookAhead) + { + /* $stateno = $this->yystack[$this->yyidx]->stateno; */ + + if (!isset(self::$yy_reduce_ofst[$stateno])) { + return self::$yy_default[$stateno]; + } + $i = self::$yy_reduce_ofst[$stateno]; + if ($i == self::YY_REDUCE_USE_DFLT) { + return self::$yy_default[$stateno]; + } + if ($iLookAhead == self::YYNOCODE) { + return self::YY_NO_ACTION; + } + $i += $iLookAhead; + if ($i < 0 || $i >= self::YY_SZ_ACTTAB || + self::$yy_lookahead[$i] != $iLookAhead) { + return self::$yy_default[$stateno]; + } else { + return self::$yy_action[$i]; + } + } + + /** + * Perform a shift action. + * @param int The new state to shift in + * @param int The major token to shift in + * @param mixed the minor token to shift in + */ + function yy_shift($yyNewState, $yyMajor, $yypMinor) + { + $this->yyidx++; + if ($this->yyidx >= self::YYSTACKDEPTH) { + $this->yyidx--; + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sStack Overflow!\n", self::$yyTracePrompt); + } + while ($this->yyidx >= 0) { + $this->yy_pop_parser_stack(); + } + /* Here code is inserted which will execute if the parser + ** stack ever overflows */ + return; + } + $yytos = new OQLParser_yyStackEntry; + $yytos->stateno = $yyNewState; + $yytos->major = $yyMajor; + $yytos->minor = $yypMinor; + array_push($this->yystack, $yytos); + if (self::$yyTraceFILE && $this->yyidx > 0) { + fprintf(self::$yyTraceFILE, "%sShift %d\n", self::$yyTracePrompt, + $yyNewState); + fprintf(self::$yyTraceFILE, "%sStack:", self::$yyTracePrompt); + for($i = 1; $i <= $this->yyidx; $i++) { + fprintf(self::$yyTraceFILE, " %s", + self::$yyTokenName[$this->yystack[$i]->major]); + } + fwrite(self::$yyTraceFILE,"\n"); + } + } + + /** + * The following table contains information about every rule that + * is used during the reduce. + * + *
+     * array(
+     *  array(
+     *   int $lhs;         Symbol on the left-hand side of the rule
+     *   int $nrhs;     Number of right-hand side symbols in the rule
+     *  ),...
+     * );
+     * 
+ */ + static public $yyRuleInfo = array( + array( 'lhs' => 58, 'rhs' => 1 ), + array( 'lhs' => 58, 'rhs' => 1 ), + array( 'lhs' => 59, 'rhs' => 4 ), + array( 'lhs' => 59, 'rhs' => 6 ), + array( 'lhs' => 59, 'rhs' => 6 ), + array( 'lhs' => 59, 'rhs' => 8 ), + array( 'lhs' => 64, 'rhs' => 1 ), + array( 'lhs' => 64, 'rhs' => 3 ), + array( 'lhs' => 63, 'rhs' => 2 ), + array( 'lhs' => 63, 'rhs' => 0 ), + array( 'lhs' => 62, 'rhs' => 2 ), + array( 'lhs' => 62, 'rhs' => 1 ), + array( 'lhs' => 62, 'rhs' => 0 ), + array( 'lhs' => 65, 'rhs' => 6 ), + array( 'lhs' => 65, 'rhs' => 4 ), + array( 'lhs' => 66, 'rhs' => 3 ), + array( 'lhs' => 60, 'rhs' => 1 ), + array( 'lhs' => 69, 'rhs' => 1 ), + array( 'lhs' => 69, 'rhs' => 1 ), + array( 'lhs' => 69, 'rhs' => 1 ), + array( 'lhs' => 69, 'rhs' => 4 ), + array( 'lhs' => 69, 'rhs' => 3 ), + array( 'lhs' => 69, 'rhs' => 3 ), + array( 'lhs' => 76, 'rhs' => 1 ), + array( 'lhs' => 76, 'rhs' => 3 ), + array( 'lhs' => 78, 'rhs' => 1 ), + array( 'lhs' => 78, 'rhs' => 3 ), + array( 'lhs' => 80, 'rhs' => 1 ), + array( 'lhs' => 80, 'rhs' => 3 ), + array( 'lhs' => 68, 'rhs' => 1 ), + array( 'lhs' => 68, 'rhs' => 3 ), + array( 'lhs' => 75, 'rhs' => 3 ), + array( 'lhs' => 83, 'rhs' => 1 ), + array( 'lhs' => 83, 'rhs' => 3 ), + array( 'lhs' => 73, 'rhs' => 0 ), + array( 'lhs' => 73, 'rhs' => 1 ), + array( 'lhs' => 73, 'rhs' => 3 ), + array( 'lhs' => 84, 'rhs' => 1 ), + array( 'lhs' => 84, 'rhs' => 3 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 85, 'rhs' => 1 ), + array( 'lhs' => 70, 'rhs' => 1 ), + array( 'lhs' => 70, 'rhs' => 1 ), + array( 'lhs' => 86, 'rhs' => 1 ), + array( 'lhs' => 87, 'rhs' => 1 ), + array( 'lhs' => 67, 'rhs' => 1 ), + array( 'lhs' => 67, 'rhs' => 3 ), + array( 'lhs' => 61, 'rhs' => 1 ), + array( 'lhs' => 71, 'rhs' => 1 ), + array( 'lhs' => 90, 'rhs' => 1 ), + array( 'lhs' => 88, 'rhs' => 1 ), + array( 'lhs' => 89, 'rhs' => 1 ), + array( 'lhs' => 77, 'rhs' => 1 ), + array( 'lhs' => 79, 'rhs' => 1 ), + array( 'lhs' => 79, 'rhs' => 1 ), + array( 'lhs' => 79, 'rhs' => 1 ), + array( 'lhs' => 79, 'rhs' => 1 ), + array( 'lhs' => 81, 'rhs' => 1 ), + array( 'lhs' => 82, 'rhs' => 1 ), + array( 'lhs' => 91, 'rhs' => 1 ), + array( 'lhs' => 91, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 92, 'rhs' => 1 ), + array( 'lhs' => 93, 'rhs' => 1 ), + array( 'lhs' => 93, 'rhs' => 1 ), + array( 'lhs' => 74, 'rhs' => 1 ), + array( 'lhs' => 74, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + array( 'lhs' => 72, 'rhs' => 1 ), + ); + + /** + * The following table contains a mapping of reduce action to method name + * that handles the reduction. + * + * If a rule is not set, it has no handler. + */ + static public $yyReduceMap = array( + 0 => 0, + 1 => 0, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 32 => 6, + 35 => 6, + 7 => 7, + 33 => 7, + 36 => 7, + 8 => 8, + 9 => 9, + 12 => 9, + 10 => 10, + 11 => 11, + 13 => 13, + 14 => 14, + 15 => 15, + 16 => 16, + 17 => 16, + 18 => 16, + 19 => 16, + 23 => 16, + 25 => 16, + 27 => 16, + 29 => 16, + 37 => 16, + 39 => 16, + 40 => 16, + 41 => 16, + 42 => 16, + 43 => 16, + 44 => 16, + 45 => 16, + 46 => 16, + 20 => 20, + 21 => 21, + 22 => 22, + 24 => 22, + 26 => 22, + 28 => 22, + 30 => 22, + 31 => 31, + 34 => 34, + 38 => 38, + 47 => 47, + 48 => 47, + 49 => 49, + 50 => 50, + 51 => 51, + 75 => 51, + 76 => 51, + 77 => 51, + 78 => 51, + 79 => 51, + 80 => 51, + 81 => 51, + 82 => 51, + 83 => 51, + 84 => 51, + 85 => 51, + 86 => 51, + 87 => 51, + 88 => 51, + 89 => 51, + 90 => 51, + 91 => 51, + 92 => 51, + 93 => 51, + 94 => 51, + 95 => 51, + 96 => 51, + 52 => 52, + 53 => 53, + 54 => 54, + 56 => 54, + 57 => 54, + 58 => 54, + 59 => 54, + 60 => 54, + 61 => 54, + 62 => 54, + 63 => 54, + 64 => 54, + 65 => 54, + 66 => 54, + 67 => 54, + 68 => 54, + 69 => 54, + 70 => 54, + 71 => 54, + 72 => 54, + 73 => 54, + 74 => 54, + 55 => 55, + ); + /* Beginning here are the reduction cases. A typical example + ** follows: + ** #line + ** function yy_r0($yymsp){ ... } // User supplied code + ** #line + */ +#line 29 "oql-parser.y" + function yy_r0(){ $this->my_result = $this->yystack[$this->yyidx + 0]->minor; } +#line 1288 "oql-parser.php" +#line 32 "oql-parser.y" + function yy_r2(){ + $this->_retvalue = new OqlObjectQuery($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -1]->minor, array($this->yystack[$this->yyidx + -2]->minor)); + } +#line 1293 "oql-parser.php" +#line 35 "oql-parser.y" + function yy_r3(){ + $this->_retvalue = new OqlObjectQuery($this->yystack[$this->yyidx + -4]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -1]->minor, array($this->yystack[$this->yyidx + -2]->minor)); + } +#line 1298 "oql-parser.php" +#line 39 "oql-parser.y" + function yy_r4(){ + $this->_retvalue = new OqlObjectQuery($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + -4]->minor); + } +#line 1303 "oql-parser.php" +#line 42 "oql-parser.y" + function yy_r5(){ + $this->_retvalue = new OqlObjectQuery($this->yystack[$this->yyidx + -4]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + -6]->minor); + } +#line 1308 "oql-parser.php" +#line 47 "oql-parser.y" + function yy_r6(){ + $this->_retvalue = array($this->yystack[$this->yyidx + 0]->minor); + } +#line 1313 "oql-parser.php" +#line 50 "oql-parser.y" + function yy_r7(){ + array_push($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor); + $this->_retvalue = $this->yystack[$this->yyidx + -2]->minor; + } +#line 1319 "oql-parser.php" +#line 55 "oql-parser.y" + function yy_r8(){ $this->_retvalue = $this->yystack[$this->yyidx + 0]->minor; } +#line 1322 "oql-parser.php" +#line 56 "oql-parser.y" + function yy_r9(){ $this->_retvalue = null; } +#line 1325 "oql-parser.php" +#line 58 "oql-parser.y" + function yy_r10(){ + // insert the join statement on top of the existing list + array_unshift($this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -1]->minor); + // and return the updated array + $this->_retvalue = $this->yystack[$this->yyidx + 0]->minor; + } +#line 1333 "oql-parser.php" +#line 64 "oql-parser.y" + function yy_r11(){ + $this->_retvalue = Array($this->yystack[$this->yyidx + 0]->minor); + } +#line 1338 "oql-parser.php" +#line 70 "oql-parser.y" + function yy_r13(){ + // create an array with one single item + $this->_retvalue = new OqlJoinSpec($this->yystack[$this->yyidx + -4]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor); + } +#line 1344 "oql-parser.php" +#line 75 "oql-parser.y" + function yy_r14(){ + // create an array with one single item + $this->_retvalue = new OqlJoinSpec($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + 0]->minor); + } +#line 1350 "oql-parser.php" +#line 80 "oql-parser.y" + function yy_r15(){ $this->_retvalue = new BinaryOqlExpression($this->yystack[$this->yyidx + -2]->minor, '=', $this->yystack[$this->yyidx + 0]->minor); } +#line 1353 "oql-parser.php" +#line 82 "oql-parser.y" + function yy_r16(){ $this->_retvalue = $this->yystack[$this->yyidx + 0]->minor; } +#line 1356 "oql-parser.php" +#line 87 "oql-parser.y" + function yy_r20(){ $this->_retvalue = new FunctionOqlExpression($this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + -1]->minor); } +#line 1359 "oql-parser.php" +#line 88 "oql-parser.y" + function yy_r21(){ $this->_retvalue = $this->yystack[$this->yyidx + -1]->minor; } +#line 1362 "oql-parser.php" +#line 89 "oql-parser.y" + function yy_r22(){ $this->_retvalue = new BinaryOqlExpression($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + 0]->minor); } +#line 1365 "oql-parser.php" +#line 104 "oql-parser.y" + function yy_r31(){ + $this->_retvalue = new ListOqlExpression($this->yystack[$this->yyidx + -1]->minor); + } +#line 1370 "oql-parser.php" +#line 115 "oql-parser.y" + function yy_r34(){ + $this->_retvalue = array(); + } +#line 1375 "oql-parser.php" +#line 126 "oql-parser.y" + function yy_r38(){ $this->_retvalue = new IntervalOqlExpression($this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + 0]->minor); } +#line 1378 "oql-parser.php" +#line 138 "oql-parser.y" + function yy_r47(){ $this->_retvalue = new ScalarOqlExpression($this->yystack[$this->yyidx + 0]->minor); } +#line 1381 "oql-parser.php" +#line 141 "oql-parser.y" + function yy_r49(){ $this->_retvalue = new FieldOqlExpression($this->yystack[$this->yyidx + 0]->minor); } +#line 1384 "oql-parser.php" +#line 142 "oql-parser.y" + function yy_r50(){ $this->_retvalue = new FieldOqlExpression($this->yystack[$this->yyidx + 0]->minor, $this->yystack[$this->yyidx + -2]->minor); } +#line 1387 "oql-parser.php" +#line 143 "oql-parser.y" + function yy_r51(){ $this->_retvalue=$this->yystack[$this->yyidx + 0]->minor; } +#line 1390 "oql-parser.php" +#line 146 "oql-parser.y" + function yy_r52(){ $this->_retvalue = new VariableOqlExpression(substr($this->yystack[$this->yyidx + 0]->minor, 1)); } +#line 1393 "oql-parser.php" +#line 148 "oql-parser.y" + function yy_r53(){ + if ($this->yystack[$this->yyidx + 0]->minor[0] == '`') + { + $name = substr($this->yystack[$this->yyidx + 0]->minor, 1, strlen($this->yystack[$this->yyidx + 0]->minor) - 2); + } + else + { + $name = $this->yystack[$this->yyidx + 0]->minor; + } + $this->_retvalue = new OqlName($name, $this->m_iColPrev); + } +#line 1406 "oql-parser.php" +#line 160 "oql-parser.y" + function yy_r54(){$this->_retvalue=$this->yystack[$this->yyidx + 0]->minor; } +#line 1409 "oql-parser.php" +#line 161 "oql-parser.y" + function yy_r55(){$this->_retvalue=stripslashes(substr($this->yystack[$this->yyidx + 0]->minor, 1, strlen($this->yystack[$this->yyidx + 0]->minor) - 2)); } +#line 1412 "oql-parser.php" + + /** + * placeholder for the left hand side in a reduce operation. + * + * For a parser with a rule like this: + *
+     * rule(A) ::= B. { A = 1; }
+     * 
+ * + * The parser will translate to something like: + * + * + * function yy_r0(){$this->_retvalue = 1;} + * + */ + private $_retvalue; + + /** + * Perform a reduce action and the shift that must immediately + * follow the reduce. + * + * For a rule such as: + * + *
+     * A ::= B blah C. { dosomething(); }
+     * 
+ * + * This function will first call the action, if any, ("dosomething();" in our + * example), and then it will pop three states from the stack, + * one for each entry on the right-hand side of the expression + * (B, blah, and C in our example rule), and then push the result of the action + * back on to the stack with the resulting state reduced to (as described in the .out + * file) + * @param int Number of the rule by which to reduce + */ + function yy_reduce($yyruleno) + { + //int $yygoto; /* The next state */ + //int $yyact; /* The next action */ + //mixed $yygotominor; /* The LHS of the rule reduced */ + //OQLParser_yyStackEntry $yymsp; /* The top of the parser's stack */ + //int $yysize; /* Amount to pop the stack */ + $yymsp = $this->yystack[$this->yyidx]; + if (self::$yyTraceFILE && $yyruleno >= 0 + && $yyruleno < count(self::$yyRuleName)) { + fprintf(self::$yyTraceFILE, "%sReduce (%d) [%s].\n", + self::$yyTracePrompt, $yyruleno, + self::$yyRuleName[$yyruleno]); + } + + $this->_retvalue = $yy_lefthand_side = null; + if (array_key_exists($yyruleno, self::$yyReduceMap)) { + // call the action + $this->_retvalue = null; + $this->{'yy_r' . self::$yyReduceMap[$yyruleno]}(); + $yy_lefthand_side = $this->_retvalue; + } + $yygoto = self::$yyRuleInfo[$yyruleno]['lhs']; + $yysize = self::$yyRuleInfo[$yyruleno]['rhs']; + $this->yyidx -= $yysize; + for($i = $yysize; $i; $i--) { + // pop all of the right-hand side parameters + array_pop($this->yystack); + } + $yyact = $this->yy_find_reduce_action($this->yystack[$this->yyidx]->stateno, $yygoto); + if ($yyact < self::YYNSTATE) { + /* If we are not debugging and the reduce action popped at least + ** one element off the stack, then we can push the new element back + ** onto the stack here, and skip the stack overflow test in yy_shift(). + ** That gives a significant speed improvement. */ + if (!self::$yyTraceFILE && $yysize) { + $this->yyidx++; + $x = new OQLParser_yyStackEntry; + $x->stateno = $yyact; + $x->major = $yygoto; + $x->minor = $yy_lefthand_side; + $this->yystack[$this->yyidx] = $x; + } else { + $this->yy_shift($yyact, $yygoto, $yy_lefthand_side); + } + } elseif ($yyact == self::YYNSTATE + self::YYNRULE + 1) { + $this->yy_accept(); + } + } + + /** + * The following code executes when the parse fails + * + * Code from %parse_fail is inserted here + */ + function yy_parse_failed() + { + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sFail!\n", self::$yyTracePrompt); + } + while ($this->yyidx >= 0) { + $this->yy_pop_parser_stack(); + } + /* Here code is inserted which will be executed whenever the + ** parser fails */ + } + + /** + * The following code executes when a syntax error first occurs. + * + * %syntax_error code is inserted here + * @param int The major type of the error token + * @param mixed The minor type of the error token + */ + function yy_syntax_error($yymajor, $TOKEN) + { +#line 25 "oql-parser.y" + +throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN); +#line 1528 "oql-parser.php" + } + + /** + * The following is executed when the parser accepts + * + * %parse_accept code is inserted here + */ + function yy_accept() + { + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sAccept!\n", self::$yyTracePrompt); + } + while ($this->yyidx >= 0) { + $stack = $this->yy_pop_parser_stack(); + } + /* Here code is inserted which will be executed whenever the + ** parser accepts */ + } + + /** + * The main parser program. + * + * The first argument is the major token number. The second is + * the token value string as scanned from the input. + * + * @param int the token number + * @param mixed the token value + * @param mixed any extra arguments that should be passed to handlers + */ + function doParse($yymajor, $yytokenvalue) + { +// $yyact; /* The parser action. */ +// $yyendofinput; /* True if we are at the end of input */ + $yyerrorhit = 0; /* True if yymajor has invoked an error */ + + /* (re)initialize the parser, if necessary */ + if ($this->yyidx === null || $this->yyidx < 0) { + /* if ($yymajor == 0) return; // not sure why this was here... */ + $this->yyidx = 0; + $this->yyerrcnt = -1; + $x = new OQLParser_yyStackEntry; + $x->stateno = 0; + $x->major = 0; + $this->yystack = array(); + array_push($this->yystack, $x); + } + $yyendofinput = ($yymajor==0); + + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sInput %s\n", + self::$yyTracePrompt, self::$yyTokenName[$yymajor]); + } + + do { + $yyact = $this->yy_find_shift_action($yymajor); + if ($yymajor < self::YYERRORSYMBOL && + !$this->yy_is_expected_token($yymajor)) { + // force a syntax error + $yyact = self::YY_ERROR_ACTION; + } + if ($yyact < self::YYNSTATE) { + $this->yy_shift($yyact, $yymajor, $yytokenvalue); + $this->yyerrcnt--; + if ($yyendofinput && $this->yyidx >= 0) { + $yymajor = 0; + } else { + $yymajor = self::YYNOCODE; + } + } elseif ($yyact < self::YYNSTATE + self::YYNRULE) { + $this->yy_reduce($yyact - self::YYNSTATE); + } elseif ($yyact == self::YY_ERROR_ACTION) { + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sSyntax Error!\n", + self::$yyTracePrompt); + } + if (self::YYERRORSYMBOL) { + /* A syntax error has occurred. + ** The response to an error depends upon whether or not the + ** grammar defines an error token "ERROR". + ** + ** This is what we do if the grammar does define ERROR: + ** + ** * Call the %syntax_error function. + ** + ** * Begin popping the stack until we enter a state where + ** it is legal to shift the error symbol, then shift + ** the error symbol. + ** + ** * Set the error count to three. + ** + ** * Begin accepting and shifting new tokens. No new error + ** processing will occur until three tokens have been + ** shifted successfully. + ** + */ + if ($this->yyerrcnt < 0) { + $this->yy_syntax_error($yymajor, $yytokenvalue); + } + $yymx = $this->yystack[$this->yyidx]->major; + if ($yymx == self::YYERRORSYMBOL || $yyerrorhit ){ + if (self::$yyTraceFILE) { + fprintf(self::$yyTraceFILE, "%sDiscard input token %s\n", + self::$yyTracePrompt, self::$yyTokenName[$yymajor]); + } + $this->yy_destructor($yymajor, $yytokenvalue); + $yymajor = self::YYNOCODE; + } else { + while ($this->yyidx >= 0 && + $yymx != self::YYERRORSYMBOL && + ($yyact = $this->yy_find_shift_action(self::YYERRORSYMBOL)) >= self::YYNSTATE + ){ + $this->yy_pop_parser_stack(); + } + if ($this->yyidx < 0 || $yymajor==0) { + $this->yy_destructor($yymajor, $yytokenvalue); + $this->yy_parse_failed(); + $yymajor = self::YYNOCODE; + } elseif ($yymx != self::YYERRORSYMBOL) { + $u2 = 0; + $this->yy_shift($yyact, self::YYERRORSYMBOL, $u2); + } + } + $this->yyerrcnt = 3; + $yyerrorhit = 1; + } else { + /* YYERRORSYMBOL is not defined */ + /* This is what we do if the grammar does not define ERROR: + ** + ** * Report an error message, and throw away the input token. + ** + ** * If the input token is $, then fail the parse. + ** + ** As before, subsequent error messages are suppressed until + ** three input tokens have been successfully shifted. + */ + if ($this->yyerrcnt <= 0) { + $this->yy_syntax_error($yymajor, $yytokenvalue); + } + $this->yyerrcnt = 3; + $this->yy_destructor($yymajor, $yytokenvalue); + if ($yyendofinput) { + $this->yy_parse_failed(); + } + $yymajor = self::YYNOCODE; + } + } else { + $this->yy_accept(); + $yymajor = self::YYNOCODE; + } + } while ($yymajor != self::YYNOCODE && $this->yyidx >= 0); + } +}#line 211 "oql-parser.y" + + +class OQLParserException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sTokenName, $sTokenValue) + { + $sIssue = "Unexpected token $sTokenName"; + + parent::__construct($sIssue, $sInput, $iLine, $iCol, $sTokenValue); + } +} + +class OQLParser extends OQLParserRaw +{ + // dirty, but working for us (no other mean to get the final result :-( + protected $my_result; + + public function GetResult() + { + return $this->my_result; + } + + // More info on the source query and the current position while parsing it + // Data used when an exception is raised + protected $m_iLine; // still not used + protected $m_iCol; + protected $m_iColPrev; // this is the interesting one, because the parser will reduce on the next token + protected $m_sSourceQuery; + + public function __construct($sQuery) + { + $this->m_iLine = 0; + $this->m_iCol = 0; + $this->m_iColPrev = 0; + $this->m_sSourceQuery = $sQuery; + // no constructor - parent::__construct(); + } + + public function doParse($token, $value, $iCurrPosition = 0) + { + $this->m_iColPrev = $this->m_iCol; + $this->m_iCol = $iCurrPosition; + + return parent::DoParse($token, $value); + } + + public function doFinish() + { + $this->doParse(0, 0); + return $this->my_result; + } + + public function __destruct() + { + // Bug in the original destructor, causing an infinite loop ! + // This is a real issue when a fatal error occurs on the first token (the error could not be seen) + if (is_null($this->yyidx)) + { + $this->yyidx = -1; + } + parent::__destruct(); + } +} + +#line 1747 "oql-parser.php" diff --git a/core/oql/oql-parser.y b/core/oql/oql-parser.y new file mode 100644 index 0000000000..ee28b97282 --- /dev/null +++ b/core/oql/oql-parser.y @@ -0,0 +1,275 @@ + +/* + +This is a LALR(1) grammar +(seek for Lemon grammar to get some documentation from the Net) +That doc was helpful: http://www.hwaci.com/sw/lemon/lemon.html + +To handle operators precedence we could have used the %left directive +(we took another option, because that one was discovered right after... +which option is the best for us?) +Example: +%left LOG_AND. +%left LOG_OR. +%nonassoc EQ NE GT GE LT LE. +%left PLUS MINUS. +%left TIMES DIVIDE MOD. +%right EXP NOT. + +TODO : solve the 2 remaining shift-reduce conflicts (JOIN) + +*/ + +%name OQLParser_ +%declare_class {class OQLParserRaw} +%syntax_error { +throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN); +} + +result ::= query(X). { $this->my_result = X; } +result ::= condition(X). { $this->my_result = X; } + +query(A) ::= SELECT class_name(X) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, X, W, J, array(X)); +} +query(A) ::= SELECT class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, Y, W, J, array(Y)); +} + +query(A) ::= SELECT class_list(E) FROM class_name(X) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, X, W, J, E); +} +query(A) ::= SELECT class_list(E) FROM class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, Y, W, J, E); +} + + +class_list(A) ::= class_name(X). { + A = array(X); +} +class_list(A) ::= class_list(L) COMA class_name(X). { + array_push(L, X); + A = L; +} + +where_statement(A) ::= WHERE condition(C). { A = C;} +where_statement(A) ::= . { A = null;} + +join_statement(A) ::= join_item(J) join_statement(S). { + // insert the join statement on top of the existing list + array_unshift(S, J); + // and return the updated array + A = S; +} +join_statement(A) ::= join_item(J). { + A = Array(J); +} +join_statement(A) ::= . { A = null;} + +join_item(A) ::= JOIN class_name(X) AS_ALIAS class_name(Y) ON join_condition(C). +{ + // create an array with one single item + A = new OqlJoinSpec(X, Y, C); +} +join_item(A) ::= JOIN class_name(X) ON join_condition(C). +{ + // create an array with one single item + A = new OqlJoinSpec(X, X, C); +} + +join_condition(A) ::= field_id(X) EQ field_id(Y). { A = new BinaryOqlExpression(X, '=', Y); } + +condition(A) ::= expression_prio4(X). { A = X; } + +expression_basic(A) ::= scalar(X). { A = X; } +expression_basic(A) ::= field_id(X). { A = X; } +expression_basic(A) ::= var_name(X). { A = X; } +expression_basic(A) ::= func_name(X) PAR_OPEN arg_list(Y) PAR_CLOSE. { A = new FunctionOqlExpression(X, Y); } +expression_basic(A) ::= PAR_OPEN expression_prio4(X) PAR_CLOSE. { A = X; } +expression_basic(A) ::= expression_basic(X) list_operator(Y) list(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio1(A) ::= expression_basic(X). { A = X; } +expression_prio1(A) ::= expression_prio1(X) operator1(Y) expression_basic(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio2(A) ::= expression_prio1(X). { A = X; } +expression_prio2(A) ::= expression_prio2(X) operator2(Y) expression_prio1(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio3(A) ::= expression_prio2(X). { A = X; } +expression_prio3(A) ::= expression_prio3(X) operator3(Y) expression_prio2(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio4(A) ::= expression_prio3(X). { A = X; } +expression_prio4(A) ::= expression_prio4(X) operator4(Y) expression_prio3(Z). { A = new BinaryOqlExpression(X, Y, Z); } + + +list(A) ::= PAR_OPEN scalar_list(X) PAR_CLOSE. { + A = new ListOqlExpression(X); +} +scalar_list(A) ::= scalar(X). { + A = array(X); +} +scalar_list(A) ::= scalar_list(L) COMA scalar(X). { + array_push(L, X); + A = L; +} + +arg_list(A) ::= . { + A = array(); +} +arg_list(A) ::= argument(X). { + A = array(X); +} +arg_list(A) ::= arg_list(L) COMA argument(X). { + array_push(L, X); + A = L; +} +argument(A) ::= expression_prio4(X). { A = X; } +argument(A) ::= INTERVAL expression_prio4(X) interval_unit(Y). { A = new IntervalOqlExpression(X, Y); } + +interval_unit(A) ::= F_SECOND(X). { A = X; } +interval_unit(A) ::= F_MINUTE(X). { A = X; } +interval_unit(A) ::= F_HOUR(X). { A = X; } +interval_unit(A) ::= F_DAY(X). { A = X; } +interval_unit(A) ::= F_MONTH(X). { A = X; } +interval_unit(A) ::= F_YEAR(X). { A = X; } + +scalar(A) ::= num_scalar(X). { A = X; } +scalar(A) ::= str_scalar(X). { A = X; } + +num_scalar(A) ::= num_value(X). { A = new ScalarOqlExpression(X); } +str_scalar(A) ::= str_value(X). { A = new ScalarOqlExpression(X); } + +field_id(A) ::= name(X). { A = new FieldOqlExpression(X); } +field_id(A) ::= class_name(X) DOT name(Y). { A = new FieldOqlExpression(Y, X); } +class_name(A) ::= name(X). { A=X; } + + +var_name(A) ::= VARNAME(X). { A = new VariableOqlExpression(substr(X, 1)); } + +name(A) ::= NAME(X). { + if (X[0] == '`') + { + $name = substr(X, 1, strlen(X) - 2); + } + else + { + $name = X; + } + A = new OqlName($name, $this->m_iColPrev); +} + +num_value(A) ::= NUMVAL(X). {A=X;} +str_value(A) ::= STRVAL(X). {A=stripslashes(substr(X, 1, strlen(X) - 2));} + + +operator1(A) ::= num_operator1(X). {A=X;} +operator2(A) ::= num_operator2(X). {A=X;} +operator2(A) ::= str_operator(X). {A=X;} +operator2(A) ::= EQ(X). {A=X;} +operator2(A) ::= NOT_EQ(X). {A=X;} +operator3(A) ::= LOG_AND(X). {A=X;} +operator4(A) ::= LOG_OR(X). {A=X;} + +num_operator1(A) ::= MATH_DIV(X). {A=X;} +num_operator1(A) ::= MATH_MULT(X). {A=X;} +num_operator2(A) ::= MATH_PLUS(X). {A=X;} +num_operator2(A) ::= MATH_MINUS(X). {A=X;} +num_operator2(A) ::= GT(X). {A=X;} +num_operator2(A) ::= LT(X). {A=X;} +num_operator2(A) ::= GE(X). {A=X;} +num_operator2(A) ::= LE(X). {A=X;} + +str_operator(A) ::= LIKE(X). {A=X;} +str_operator(A) ::= NOT_LIKE(X). {A=X;} + +list_operator(A) ::= IN(X). {A=X;} +list_operator(A) ::= NOT_IN(X). {A=X;} + +func_name(A) ::= F_IF(X). { A=X; } +func_name(A) ::= F_ELT(X). { A=X; } +func_name(A) ::= F_COALESCE(X). { A=X; } +func_name(A) ::= F_CONCAT(X). { A=X; } +func_name(A) ::= F_SUBSTR(X). { A=X; } +func_name(A) ::= F_TRIM(X). { A=X; } +func_name(A) ::= F_DATE(X). { A=X; } +func_name(A) ::= F_DATE_FORMAT(X). { A=X; } +func_name(A) ::= F_CURRENT_DATE(X). { A=X; } +func_name(A) ::= F_NOW(X). { A=X; } +func_name(A) ::= F_TIME(X). { A=X; } +func_name(A) ::= F_TO_DAYS(X). { A=X; } +func_name(A) ::= F_FROM_DAYS(X). { A=X; } +func_name(A) ::= F_YEAR(X). { A=X; } +func_name(A) ::= F_MONTH(X). { A=X; } +func_name(A) ::= F_DAY(X). { A=X; } +func_name(A) ::= F_DATE_ADD(X). { A=X; } +func_name(A) ::= F_DATE_SUB(X). { A=X; } +func_name(A) ::= F_ROUND(X). { A=X; } +func_name(A) ::= F_FLOOR(X). { A=X; } +func_name(A) ::= F_INET_ATON(X). { A=X; } +func_name(A) ::= F_INET_NTOA(X). { A=X; } + + +%code { + +class OQLParserException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sTokenName, $sTokenValue) + { + $sIssue = "Unexpected token $sTokenName"; + + parent::__construct($sIssue, $sInput, $iLine, $iCol, $sTokenValue); + } +} + +class OQLParser extends OQLParserRaw +{ + // dirty, but working for us (no other mean to get the final result :-( + protected $my_result; + + public function GetResult() + { + return $this->my_result; + } + + // More info on the source query and the current position while parsing it + // Data used when an exception is raised + protected $m_iLine; // still not used + protected $m_iCol; + protected $m_iColPrev; // this is the interesting one, because the parser will reduce on the next token + protected $m_sSourceQuery; + + public function __construct($sQuery) + { + $this->m_iLine = 0; + $this->m_iCol = 0; + $this->m_iColPrev = 0; + $this->m_sSourceQuery = $sQuery; + // no constructor - parent::__construct(); + } + + public function doParse($token, $value, $iCurrPosition = 0) + { + $this->m_iColPrev = $this->m_iCol; + $this->m_iCol = $iCurrPosition; + + return parent::DoParse($token, $value); + } + + public function doFinish() + { + $this->doParse(0, 0); + return $this->my_result; + } + + public function __destruct() + { + // Bug in the original destructor, causing an infinite loop ! + // This is a real issue when a fatal error occurs on the first token (the error could not be seen) + if (is_null($this->yyidx)) + { + $this->yyidx = -1; + } + parent::__destruct(); + } +} + +} diff --git a/core/oql/oqlexception.class.inc.php b/core/oql/oqlexception.class.inc.php new file mode 100644 index 0000000000..952a15bf2d --- /dev/null +++ b/core/oql/oqlexception.class.inc.php @@ -0,0 +1,102 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +class OQLException extends CoreException +{ + public function __construct($sIssue, $sInput, $iLine, $iCol, $sUnexpected, $aExpecting = null) + { + $this->m_MyIssue = $sIssue; + $this->m_sInput = $sInput; + $this->m_iLine = $iLine; + $this->m_iCol = $iCol; + $this->m_sUnexpected = $sUnexpected; + $this->m_aExpecting = $aExpecting; + + if (is_null($this->m_aExpecting) || (count($this->m_aExpecting) == 0)) + { + $sMessage = "$sIssue - found '{$this->m_sUnexpected}' at $iCol in '$sInput'"; + } + else + { + $sExpectations = '{'.implode(', ', $this->m_aExpecting).'}'; + $sSuggest = self::FindClosestString($this->m_sUnexpected, $this->m_aExpecting); + $sMessage = "$sIssue - found '{$this->m_sUnexpected}' at $iCol in '$sInput', expecting $sExpectations, I would suggest to use '$sSuggest'"; + } + + // make sure everything is assigned properly + parent::__construct($sMessage, 0); + } + + public function getHtmlDesc($sHighlightHtmlBegin = '', $sHighlightHtmlEnd = '') + { + $sRet = htmlentities($this->m_MyIssue.", found '".$this->m_sUnexpected."' in: "); + $sRet .= htmlentities(substr($this->m_sInput, 0, $this->m_iCol)); + $sRet .= $sHighlightHtmlBegin.htmlentities(substr($this->m_sInput, $this->m_iCol, strlen($this->m_sUnexpected))).$sHighlightHtmlEnd; + $sRet .= htmlentities(substr($this->m_sInput, $this->m_iCol + strlen($this->m_sUnexpected))); + + if (!is_null($this->m_aExpecting) && (count($this->m_aExpecting) > 0)) + { + $sExpectations = '{'.implode(', ', $this->m_aExpecting).'}'; + $sRet .= ", expecting ".htmlentities($sExpectations); + $sSuggest = self::FindClosestString($this->m_sUnexpected, $this->m_aExpecting); + if (strlen($sSuggest) > 0) + { + $sRet .= ", I would suggest to use '$sHighlightHtmlBegin".htmlentities($sSuggest)."$sHighlightHtmlEnd'"; + } + } + + return $sRet; + } + + static protected function FindClosestString($sInput, $aDictionary) + { + // no shortest distance found, yet + $fShortest = -1; + $sRet = ''; + + // loop through words to find the closest + foreach ($aDictionary as $sSuggestion) + { + // calculate the distance between the input string and the suggested one + $fDist = levenshtein($sInput, $sSuggestion); + if ($fDist == 0) + { + // Exact match + return $sSuggestion; + } + + if ($fShortest < 0 || ($fDist < 4 && $fDist <= $fShortest)) + { + // set the closest match, and shortest distance + $sRet = $sSuggestion; + $fShortest = $fDist; + } + } + return $sRet; + } +} + +?> diff --git a/core/oql/oqlinterpreter.class.inc.php b/core/oql/oqlinterpreter.class.inc.php new file mode 100644 index 0000000000..bd249307d1 --- /dev/null +++ b/core/oql/oqlinterpreter.class.inc.php @@ -0,0 +1,84 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +class OqlNormalizeException extends OQLException +{ + public function __construct($sIssue, $sInput, OqlName $oName, $aExpecting = null) + { + parent::__construct($sIssue, $sInput, 0, $oName->GetPos(), $oName->GetValue(), $aExpecting); + } +} + +class OqlInterpreterException extends OQLException +{ +} + + +class OqlInterpreter +{ + public $m_sQuery; + + public function __construct($sQuery) + { + $this->m_sQuery = $sQuery; + } + + // Note: this function is left public for unit test purposes + public function Parse() + { + $oLexer = new OQLLexer($this->m_sQuery); + $oParser = new OQLParser($this->m_sQuery); + + while($oLexer->yylex()) + { + $oParser->doParse($oLexer->token, $oLexer->value, $oLexer->getTokenPos()); + } + $res = $oParser->doFinish(); + return $res; + } + + public function ParseObjectQuery() + { + $oRes = $this->Parse(); + if (!$oRes instanceof OqlObjectQuery) + { + throw new OQLException('Expecting an OQL query', $this->m_sQuery, 0, 0, get_class($oRes)); + } + return $oRes; + } + + public function ParseExpression() + { + $oRes = $this->Parse(); + if (!$oRes instanceof Expression) + { + throw new OQLException('Expecting an OQL expression', $this->m_sQuery, 0, 0, get_class($oRes), array('Expression')); + } + return $oRes; + } +} + +?> diff --git a/core/oql/oqlquery.class.inc.php b/core/oql/oqlquery.class.inc.php new file mode 100644 index 0000000000..159636f38d --- /dev/null +++ b/core/oql/oqlquery.class.inc.php @@ -0,0 +1,211 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +// Position a string within an OQL query +// This is a must if we want to be able to pinpoint an error at any stage of the query interpretation +// In particular, the normalization phase requires this +class OqlName +{ + protected $m_sValue; + protected $m_iPos; + + public function __construct($sValue, $iPos) + { + $this->m_iPos = $iPos; + $this->m_sValue = $sValue; + } + + public function GetValue() + { + return $this->m_sValue; + } + + public function GetPos() + { + return $this->m_iPos; + } + + public function __toString() + { + return $this->m_sValue; + } +} + +class OqlJoinSpec +{ + protected $m_oClass; + protected $m_oClassAlias; + protected $m_oLeftField; + protected $m_oRightField; + + protected $m_oNextJoinspec; + + public function __construct($oClass, $oClassAlias, BinaryExpression $oExpression) + { + $this->m_oClass = $oClass; + $this->m_oClassAlias = $oClassAlias; + $this->m_oLeftField = $oExpression->GetLeftExpr(); + $this->m_oRightField = $oExpression->GetRightExpr(); + } + + public function GetClass() + { + return $this->m_oClass->GetValue(); + } + public function GetClassAlias() + { + return $this->m_oClassAlias->GetValue(); + } + + public function GetClassDetails() + { + return $this->m_oClass; + } + public function GetClassAliasDetails() + { + return $this->m_oClassAlias; + } + + public function GetLeftField() + { + return $this->m_oLeftField; + } + public function GetRightField() + { + return $this->m_oRightField; + } +} + +class BinaryOqlExpression extends BinaryExpression +{ +} + +class ScalarOqlExpression extends ScalarExpression +{ +} + +class FieldOqlExpression extends FieldExpression +{ + protected $m_oParent; + protected $m_oName; + + public function __construct($oName, $oParent = null) + { + if (is_null($oParent)) + { + $oParent = new OqlName('', 0); + } + $this->m_oParent = $oParent; + $this->m_oName = $oName; + + parent::__construct($oName->GetValue(), $oParent->GetValue()); + } + + public function GetParentDetails() + { + return $this->m_oParent; + } + + public function GetNameDetails() + { + return $this->m_oName; + } +} + +class VariableOqlExpression extends VariableExpression +{ +} + +class ListOqlExpression extends ListExpression +{ +} + +class FunctionOqlExpression extends FunctionExpression +{ +} + +class IntervalOqlExpression extends IntervalExpression +{ +} + +abstract class OqlQuery +{ + protected $m_aJoins; // array of OqlJoinSpec + protected $m_oCondition; // condition tree (expressions) + + public function __construct($oCondition = null, $aJoins = null) + { + $this->m_aJoins = $aJoins; + $this->m_oCondition = $oCondition; + } + + public function GetJoins() + { + return $this->m_aJoins; + } + public function GetCondition() + { + return $this->m_oCondition; + } +} + +class OqlObjectQuery extends OqlQuery +{ + protected $m_aSelect; // array of selected classes + protected $m_oClass; + protected $m_oClassAlias; + + public function __construct($oClass, $oClassAlias, $oCondition = null, $aJoins = null, $aSelect = null) + { + $this->m_aSelect = $aSelect; + $this->m_oClass = $oClass; + $this->m_oClassAlias = $oClassAlias; + parent::__construct($oCondition, $aJoins); + } + + public function GetSelectedClasses() + { + return $this->m_aSelect; + } + public function GetClass() + { + return $this->m_oClass->GetValue(); + } + public function GetClassAlias() + { + return $this->m_oClassAlias->GetValue(); + } + + public function GetClassDetails() + { + return $this->m_oClass; + } + public function GetClassAliasDetails() + { + return $this->m_oClassAlias; + } +} + +?> diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php new file mode 100644 index 0000000000..ad23e6c058 --- /dev/null +++ b/core/ormdocument.class.inc.php @@ -0,0 +1,120 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * ormDocument + * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob + * + * @package itopORM + */ + +class ormDocument +{ + protected $m_data; + protected $m_sMimeType; + protected $m_sFileName; + + /** + * Constructor + */ + public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '') + { + $this->m_data = $data; + $this->m_sMimeType = $sMimeType; + $this->m_sFileName = $sFileName; + } + + public function __toString() + { + return MyHelpers::beautifulstr($this->m_data, 100, true); + } + + public function IsEmpty() + { + return ($this->m_data == null); + } + + public function GetMimeType() + { + return $this->m_sMimeType; + } + public function GetMainMimeType() + { + $iSeparatorPos = strpos($this->m_sMimeType, '/'); + if ($iSeparatorPos > 0) + { + return substr($this->m_sMimeType, 0, $iSeparatorPos); + } + return $this->m_sMimeType; + } + + public function GetData() + { + return $this->m_data; + } + + public function GetFileName() + { + return $this->m_sFileName; + } + + public function GetAsHTML() + { + $sResult = ''; + if ($this->IsEmpty()) + { + // If the filename is not empty, display it, this is used + // by the creation wizard while the file has not yet been uploaded + $sResult = $this->GetFileName(); + } + else + { + $data = $this->GetData(); + $sResult = $this->GetFileName().' [ '.$this->GetMimeType().', size: '.strlen($data).' byte(s) ]
'; + } + return $sResult; + } + + /** + * Returns an hyperlink to display the document *inline* + * @return string + */ + public function GetDisplayLink($sClass, $Id, $sAttCode) + { + return "".$this->GetFileName()."\n"; + } + + /** + * Returns an hyperlink to download the document (content-disposition: attachment) + * @return string + */ + public function GetDownloadLink($sClass, $Id, $sAttCode) + { + return "".$this->GetFileName()."\n"; + } +} +?> diff --git a/core/ormpassword.class.inc.php b/core/ormpassword.class.inc.php new file mode 100644 index 0000000000..5a220e1456 --- /dev/null +++ b/core/ormpassword.class.inc.php @@ -0,0 +1,119 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + * @package itopORM + */ + +class ormPassword +{ + protected $m_sHashed; + protected $m_sSalt; + + /** + * Constructor, initializes the password from the encrypted values + */ + public function __construct($sHash = '', $sSalt = '') + { + $this->m_sHashed = $sHash; + $this->m_sSalt = $sSalt; + } + + /** + * Encrypts the clear text password, with a unique salt + */ + public function SetPassword($sClearTextPassword) + { + $this->m_sSalt = SimpleCrypt::GetNewSalt(); + $this->m_sHashed = $this->ComputeHash($sClearTextPassword); + } + + /** + * Print the password: displays some stars + * @return string + */ + public function __toString() + { + return '*****'; // Password can not be read + } + + public function IsEmpty() + { + return ($this->m_hashed == null); + } + + public function GetHash() + { + return $this->m_sHashed; + } + + public function GetSalt() + { + return $this->m_sSalt; + } + + /** + * Displays the password: displays some stars + * @return string + */ + public function GetAsHTML() + { + return '*****'; // Password can not be read + } + + /** + * Check if the supplied clear text password matches the encrypted one + * @param string $sClearTextPassword + * @return boolean True if it matches, false otherwise + */ + public function CheckPassword($sClearTextPassword) + { + $bResult = false; + $sHashedPwd = $this->ComputeHash($sClearTextPassword); + if ($this->m_sHashed == $sHashedPwd) + { + $bResult = true; + } + return $bResult; + } + + /** + * Computes the hashed version of a password using a unique salt + * for this password. A unique salt is generated if needed + * @return string + */ + protected function ComputeHash($sClearTextPwd) + { + if ($this->m_sSalt == null) + { + $this->m_sSalt = SimpleCrypt::GetNewSalt(); + } + return hash('sha256', $this->m_sSalt.$sClearTextPwd); + } +} +?> \ No newline at end of file diff --git a/core/simplecrypt.class.inc.php b/core/simplecrypt.class.inc.php new file mode 100644 index 0000000000..f2bf00a8d6 --- /dev/null +++ b/core/simplecrypt.class.inc.php @@ -0,0 +1,235 @@ +encrypt('a_key','the_text'); + * $sClearText = $oSimpleCrypt->decrypt('a_key',$encrypted); + * + * The result is $plain equals to 'the_text' + * + * You can use a different engine if you don't have Mcrypt: + * $oSimpleCrypt = new SimpleCrypt('Simple'); + * + * A string encrypted with one engine can't be decrypted with + * a different one even if the key is the same. + * + * @author Miguel Ros + * @author Erwan Taloc + * @author Romain Quetiez + * @author Denis Flaven + * @version 0.3 + * @license GPL + */ + +class SimpleCrypt +{ + /** + * Constructor + * @param string $sEngineName Engine for encryption. Values: Simple, Mcrypt + */ + function __construct($sEngineName = 'Mcrypt') + { + if (($sEngineName == 'Mcrypt') && (!function_exists('mcrypt_module_open'))) + { + // Defaults to Simple encryption if the mcrypt module is not present + $sEngineName = 'Simple'; + } + $sEngineName = 'SimpleCrypt' . $sEngineName . 'Engine'; + $this->oEngine = new $sEngineName; + } + + /** + * Encrypts the string with the given key + * @param string $key + * @param string $sString Plaintext string + * @return string Ciphered string + */ + function Encrypt($key, $sString) + { + return $this->oEngine->Encrypt($key,$sString); + } + + + /** + * Decrypts the string by the given key + * @param string $key + * @param string $string Ciphered string + * @return string Plaintext string + */ + function Decrypt($key, $string) + { + return $this->oEngine->Decrypt($key,$string); + } + + /** + * Returns a random "salt" value, to be used when "hashing" a password + * using a one-way encryption algorithm, to prevent an attack using a "rainbow table" + * Tryes to use the best available random number generator + * @return string The generated random "salt" + */ + static function GetNewSalt() + { + // Copied from http://www.php.net/manual/en/function.mt-rand.php#83655 + // get 128 pseudorandom bits in a string of 16 bytes + + $sRandomBits = null; + + // Unix/Linux platform? + $fp = @fopen('/dev/urandom','rb'); + if ($fp !== FALSE) + { + //echo "Random bits pulled from /dev/urandom
\n"; + $sRandomBits .= @fread($fp,16); + @fclose($fp); + } + else + { + // MS-Windows platform? + if (@class_exists('COM')) + { + // http://msdn.microsoft.com/en-us/library/aa388176(VS.85).aspx + try + { + $CAPI_Util = new COM('CAPICOM.Utilities.1'); + $sBase64RandomBits = ''.$CAPI_Util->GetRandom(16,0); + + // if we ask for binary data PHP munges it, so we + // request base64 return value. We squeeze out the + // redundancy and useless ==CRLF by hashing... + if ($sBase64RandomBits) + { + //echo "Random bits got from CAPICOM.Utilities.1
\n"; + $sRandomBits = md5($sBase64RandomBits, TRUE); + } + } + catch (Exception $ex) + { + // echo 'Exception: ' . $ex->getMessage(); + } + } + } + if ($sRandomBits == null) + { + // No "strong" random generator available, use PHP's built-in mechanism + //echo "Random bits generated from mt_rand
\n"; + mt_srand(crc32(microtime())); + $sRandomBits = ''; + for($i = 0; $i < 4; $i++) + { + $sRandomBits .= sprintf('%04x', mt_rand(0, 65535)); + } + + + } + return $sRandomBits; + } +} + +/** + * Interface for encryption engines + */ +interface CryptEngine +{ + function Encrypt($key, $sString); + function Decrypt($key, $encrypted_data); +} + +/** + * Simple Engine doesn't need any PHP extension. + * Every encryption of the same string with the same key + * will return the same encrypted string + */ +class SimpleCryptSimpleEngine implements CryptEngine +{ + public function Encrypt($key, $sString) + { + $result = ''; + for($i=1; $i<=strlen($sString); $i++) + { + $char = substr($sString, $i-1, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)+ord($keychar)); + $result.=$char; + } + return $result; + } + + public function Decrypt($key, $encrypted_data) + { + $result = ''; + for($i=1; $i<=strlen($encrypted_data); $i++) + { + $char = substr($encrypted_data, $i-1, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)-ord($keychar)); + $result.=$char; + } + return $result; + } +} + +/** + * McryptEngine requires Mcrypt extension + * Every encryption of the same string with the same key + * will return a different encrypted string. + */ +class SimpleCryptMcryptEngine implements CryptEngine +{ + var $alg = MCRYPT_BLOWFISH; + var $td = null; + + public function __construct() + { + $this->td = mcrypt_module_open($this->alg,'','cbc',''); + } + + public function Encrypt($key, $sString) + { + $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($this->td), MCRYPT_RAND); // MCRYPT_RAND is the only choice on Windows prior to PHP 5.3 + mcrypt_generic_init($this->td, $key, $iv); + if (empty($sString)) + { + $sString = str_repeat("\0", 8); + } + $encrypted_data = mcrypt_generic($this->td, $sString); + mcrypt_generic_deinit($this->td); + return $iv.$encrypted_data; + } + + public function Decrypt($key, $encrypted_data) + { + $iv = substr($encrypted_data, 0, mcrypt_enc_get_iv_size($this->td)); + $string = substr($encrypted_data, mcrypt_enc_get_iv_size($this->td)); + mcrypt_generic_init($this->td, $key, $iv); + $decrypted_data = rtrim(mdecrypt_generic($this->td, $string), "\0"); + mcrypt_generic_deinit($this->td); + return $decrypted_data; + } + + public function __destruct() + { + mcrypt_module_close($this->td); + } +} +?> \ No newline at end of file diff --git a/core/sqlquery.class.inc.php b/core/sqlquery.class.inc.php new file mode 100644 index 0000000000..e7b9ae5a9f --- /dev/null +++ b/core/sqlquery.class.inc.php @@ -0,0 +1,455 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * SQLQuery + * build an mySQL compatible SQL query + * + * @package iTopORM + */ + +require_once('cmdbsource.class.inc.php'); + + +class SQLExpression extends BinaryExpression +{ +} +class ScalarSQLExpression extends ScalarExpression +{ +} +class TrueSQLExpression extends TrueExpression +{ +} +class FieldSQLExpression extends FieldExpression +{ +} +class VariableSQLExpression extends VariableExpression +{ +} + + + +class SQLQuery +{ + private $m_sTable = ''; + private $m_sTableAlias = ''; + private $m_aFields = array(); + private $m_oConditionExpr = null; + private $m_aFullTextNeedles = array(); + private $m_bToDelete = true; // The current table must be listed for deletion ? + private $m_aValues = array(); // Values to set in case of an update query + private $m_aJoinSelects = array(); + + public function __construct($sTable, $sTableAlias, $aFields, $oConditionExpr, $aFullTextNeedles, $bToDelete = true, $aValues = array()) + { + // This check is not needed but for developping purposes + //if (!CMDBSource::IsTable($sTable)) + //{ + // throw new CoreException("Unknown table '$sTable'"); + //} + + // $aFields must be an array of "alias"=>"expr" + // $oConditionExpr must be a condition tree + // $aValues is an array of "alias"=>value + + $this->m_sTable = $sTable; + $this->m_sTableAlias = $sTableAlias; + $this->m_aFields = $aFields; + $this->m_oConditionExpr = $oConditionExpr; + if (is_null($oConditionExpr)) + { + $this->m_oConditionExpr = new TrueExpression; + } + else if (!$oConditionExpr instanceof Expression) + { + throw new CoreException('Invalid type for condition, expecting an Expression', array('class' => get_class($oConditionExpr))); + } + $this->m_aFullTextNeedles = $aFullTextNeedles; + $this->m_bToDelete = $bToDelete; + $this->m_aValues = $aValues; + } + + public function DisplayHtml() + { + if (count($this->m_aFields) == 0) $sFields = ""; + else + { + $aFieldDesc = array(); + foreach ($this->m_aFields as $sAlias => $oExpression) + { + $aFieldDesc[] = $oExpression->Render()." as $sAlias"; + } + $sFields = " => ".implode(', ', $aFieldDesc); + } + echo "$this->m_sTable$sFields
\n"; + // #@# todo - display html of an expression tree + //$this->m_oConditionExpr->DisplayHtml() + if (count($this->m_aFullTextNeedles) > 0) + { + echo "Full text criteria...
\n"; + echo "
    \n"; + foreach ($this->m_aFullTextNeedles as $sFTNeedle) + { + echo "
  • $sFTNeedle
  • \n"; + } + echo "
"; + } + if (count($this->m_aJoinSelects) > 0) + { + echo "Joined to...
\n"; + echo "
    \n"; + foreach ($this->m_aJoinSelects as $aJoinInfo) + { + $sJoinType = $aJoinInfo["jointype"]; + $oSQLQuery = $aJoinInfo["select"]; + $sLeftField = $aJoinInfo["leftfield"]; + $sRightField = $aJoinInfo["rightfield"]; + $sRightTableAlias = $aJoinInfo["righttablealias"]; + + echo "
  • Join '$sJoinType', $sLeftField, $sRightTableAlias.$sRightField".$oSQLQuery->DisplayHtml()."
  • \n"; + } + echo "
"; + } + $aFrom = array(); + $aFields = array(); + $oCondition = null; + $aDelTables = array(); + $aSetValues = array(); + $this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues); + echo "From ...
\n"; + echo "
\n";
+		print_r($aFrom);
+		echo "
"; + } + + public function SetCondition($oConditionExpr) + { + $this->m_oConditionExpr = $oConditionExpr; + } + + public function AddCondition($oConditionExpr) + { + $this->m_oConditionExpr->LogAnd($oConditionExpr); + } + + private function AddJoin($sJoinType, $oSQLQuery, $sLeftField, $sRightField, $sRightTableAlias = '') + { + assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__)); + // No need to check this here but for development purposes + //if (!CMDBSource::IsField($this->m_sTable, $sLeftField)) + //{ + // throw new CoreException("Unknown field '$sLeftField' in table '".$this->m_sTable); + //} + + if (empty($sRightTableAlias)) + { + $sRightTableAlias = $oSQLQuery->m_sTableAlias; + } +// #@# Could not be verified here because the namespace is unknown - do we need to check it there? +// +// if (!CMDBSource::IsField($sRightTable, $sRightField)) +// { +// throw new CoreException("Unknown field '$sRightField' in table '".$sRightTable."'"); +// } + + $this->m_aJoinSelects[] = array( + "jointype" => $sJoinType, + "select" => $oSQLQuery, + "leftfield" => $sLeftField, + "rightfield" => $sRightField, + "righttablealias" => $sRightTableAlias + ); + } + public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRigthtTable = '') + { + $this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRigthtTable); + } + public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField) + { + return $this->AddJoin("left", $oSQLQuery, $sLeftField, $sRightField); + } + + // Interface, build the SQL query + public function RenderDelete($aArgs = array()) + { + // The goal will be to complete the list as we build the Joins + $aFrom = array(); + $aFields = array(); + $oCondition = null; + $aDelTables = array(); + $aSetValues = array(); + $this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues); + + // Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ... + + $sDelete = self::ClauseDelete($aDelTables); + $sFrom = self::ClauseFrom($aFrom); + // #@# safety net to redo ? + /* + if ($this->m_oConditionExpr->IsAny()) + -- if (count($aConditions) == 0) -- + { + throw new CoreException("Building a request wich will delete every object of a given table -looks suspicious- please use truncate instead..."); + } + */ + $sWhere = self::ClauseWhere($oCondition, $aArgs); + return "DELETE $sDelete FROM $sFrom WHERE $sWhere"; + } + + // Interface, build the SQL query + public function RenderUpdate($aArgs = array()) + { + // The goal will be to complete the list as we build the Joins + $aFrom = array(); + $aFields = array(); + $oCondition = null; + $aDelTables = array(); + $aSetValues = array(); + $this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues); + + $sFrom = self::ClauseFrom($aFrom); + $sValues = self::ClauseValues($aSetValues); + $sWhere = self::ClauseWhere($oCondition, $aArgs); + return "UPDATE $sFrom SET $sValues WHERE $sWhere"; + } + + // Interface, build the SQL query + public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false) + { + // The goal will be to complete the lists as we build the Joins + $aFrom = array(); + $aFields = array(); + $oCondition = null; + $aDelTables = array(); + $aSetValues = array(); + $this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues); + + $sFrom = self::ClauseFrom($aFrom); + $sWhere = self::ClauseWhere($oCondition, $aArgs); + if ($bGetCount) + { + $sSQL = "SELECT COUNT(*) AS COUNT FROM $sFrom WHERE $sWhere"; + } + else + { + $sSelect = self::ClauseSelect($aFields); + $sOrderBy = self::ClauseOrderBy($aOrderBy); + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy"; + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + $sSQL = "SELECT DISTINCT $sSelect FROM $sFrom WHERE $sWhere $sOrderBy $sLimit"; + } + return $sSQL; + } + + private static function ClauseSelect($aFields) + { + $aSelect = array(); + foreach ($aFields as $sFieldAlias => $sSQLExpr) + { + $aSelect[] = "$sSQLExpr AS $sFieldAlias"; + } + $sSelect = implode(', ', $aSelect); + return $sSelect; + } + + private static function ClauseDelete($aDelTableAliases) + { + $aDelTables = array(); + foreach ($aDelTableAliases as $sTableAlias) + { + $aDelTables[] = "$sTableAlias"; + } + $sDelTables = implode(', ', $aDelTables); + return $sDelTables; + } + + private static function ClauseFrom($aFrom) + { + $sFrom = ""; + foreach ($aFrom as $sTableAlias => $aJoinInfo) + { + switch ($aJoinInfo["jointype"]) + { + case "first": + $sFrom .= "`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"]); + break; + case "inner": + $sFrom .= " INNER JOIN (`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"]); + $sFrom .= ") ON ".$aJoinInfo["joincondition"]; + break; + case "left": + $sFrom .= " LEFT JOIN (`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"]); + $sFrom .= ") ON ".$aJoinInfo["joincondition"]; + break; + default: + throw new CoreException("Unknown jointype: '".$aJoinInfo["jointype"]."'"); + } + } + return $sFrom; + } + + private static function ClauseValues($aValues) + { + $aSetValues = array(); + foreach ($aValues as $sFieldSpec => $value) + { + $aSetValues[] = "$sFieldSpec = ".CMDBSource::Quote($value); + } + $sSetValues = implode(', ', $aSetValues); + return $sSetValues; + } + + private static function ClauseWhere($oConditionExpr, $aArgs = array()) + { + return $oConditionExpr->Render($aArgs); + } + + private static function ClauseOrderBy($aOrderBy) + { + $aOrderBySpec = array(); + foreach($aOrderBy as $sFieldAlias => $bAscending) + { + $aOrderBySpec[] = '`'.$sFieldAlias.'`'.($bAscending ? " ASC" : " DESC"); + } + $sOrderBy = implode(", ", $aOrderBySpec); + return $sOrderBy; + } + + // Purpose: prepare the query data, once for all + private function privRender(&$aFrom, &$aFields, &$oCondition, &$aDelTables, &$aSetValues) + { + $sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aDelTables, $aSetValues); + + // Add the full text search condition, based on each and every requested field + // + // To be updated with a real full text search based on the mySQL settings + // (then it might move somewhere else !) + // + $oCondition = $this->m_oConditionExpr; + if ((count($aFields) > 0) && (count($this->m_aFullTextNeedles) > 0)) + { + $aFieldExp = array(); + foreach ($aFields as $sField) + { + // This is TEMPORARY (that's why it is weird, actually) + // Full text match will be done as an expression in the filter condition + + // $sField is already a string `table`.`column` + // Let's make an expression out of it (again !) + $aFieldExp[] = Expression::FromOQL($sField); + } + $oFullTextExpr = new CharConcatExpression($aFieldExp); + // The cast is necessary because the CONCAT result in a binary string: + // if any of the field is a binary string => case sensitive comparison + // + foreach($this->m_aFullTextNeedles as $sFTNeedle) + { + $oNewCond = new BinaryExpression($oFullTextExpr, 'LIKE', new ScalarExpression("%$sFTNeedle%")); + $oCondition = $oCondition->LogAnd($oNewCond); + } + } + + return $sTableAlias; + } + + private function privRenderSingleTable(&$aFrom, &$aFields, &$aDelTables, &$aSetValues, $sJoinType = 'first', $sCallerAlias = '', $sLeftField = '', $sRightField = '', $sRightTableAlias = '') + { + $aActualTableFields = CMDBSource::GetTableFieldsList($this->m_sTable); + + $aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias; + + // Handle the various kinds of join (or first table in the list) + // + if (empty($sRightTableAlias)) + { + $sRightTableAlias = $this->m_sTableAlias; + } + $sJoinCond = "`$sCallerAlias`.`$sLeftField` = `$sRightTableAlias`.`$sRightField`"; + switch ($sJoinType) + { + case "first": + $aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>""); + break; + case "inner": + case "left": + // table or tablealias ??? + $aFrom[$this->m_sTableAlias] = array("jointype"=>$sJoinType, "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond"); + break; + } + + // Given the alias, modify the fields and conditions + // before adding them into the current lists + // + foreach($this->m_aFields as $sAlias => $oExpression) + { + $sTable = $oExpression->GetParent(); + $sColumn = $oExpression->GetName(); + $aFields["`$sAlias`"] = $oExpression->Render(); + } + if ($this->m_bToDelete) + { + $aDelTables[] = "`{$this->m_sTableAlias}`"; + } + foreach($this->m_aValues as $sFieldName=>$value) + { + $aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further! + } + + // loop on joins, to complete the list of tables/fields/conditions + // + $aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query + foreach ($this->m_aJoinSelects as $aJoinData) + { + $sJoinType = $aJoinData["jointype"]; + $oRightSelect = $aJoinData["select"]; + $sLeftField = $aJoinData["leftfield"]; + $sRightField = $aJoinData["rightfield"]; + $sRightTableAlias = $aJoinData["righttablealias"]; + + $sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aDelTables, $aSetValues, $sJoinType, $this->m_sTableAlias, $sLeftField, $sRightField, $sRightTableAlias); + } + $aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom; + + return $this->m_sTableAlias; + } + +} + +?> diff --git a/core/stimulus.class.inc.php b/core/stimulus.class.inc.php new file mode 100644 index 0000000000..9133eed1d9 --- /dev/null +++ b/core/stimulus.class.inc.php @@ -0,0 +1,137 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * A stimulus is the trigger that makes the lifecycle go ahead (state machine) + * + * @package iTopORM + */ + +// #@# Really dirty !!! +// #@# TO BE CLEANED -> ALIGN WITH OTHER METAMODEL DECLARATIONS + +class ObjectStimulus +{ + private $m_aParams = array(); + private $m_sHostClass = null; + private $m_sCode = null; + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + + public function SetHostClass($sHostClass) + { + $this->m_sHostClass = $sHostClass; + } + public function GetHostClass() + { + return $this->m_sHostClass; + } + public function GetCode() + { + return $this->m_sCode; + } + + public function GetLabel() + { + return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode, $this->m_sCode); + } + public function GetDescription() + { + return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode.'+', ''); + } + + public function GetLabel_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('label', $this->m_aParams)) + { + return $this->m_aParams['label']; + } + else + { + return $this->GetLabel(); + } + } + + public function GetDescription_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('description', $this->m_aParams)) + { + return $this->m_aParams['description']; + } + else + { + return $this->GetDescription(); + } + } + +// obsolete- public function Get($sParamName) {return $this->m_aParams[$sParamName];} + + // Note: I could factorize this code with the parameter management made for the AttributeDef class + // to be overloaded + static protected function ListExpectedParams() + { + return array(); + } + + private function ConsistencyCheck() + { + + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new CoreException("missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } +} + + + +class StimulusUserAction extends ObjectStimulus +{ + // Entry in the menus +} + +class StimulusInternal extends ObjectStimulus +{ + // Applied from page xxxx +} + +?> diff --git a/core/trigger.class.inc.php b/core/trigger.class.inc.php new file mode 100644 index 0000000000..b6ced9b520 --- /dev/null +++ b/core/trigger.class.inc.php @@ -0,0 +1,253 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +/** + * A user defined trigger, to customize the application + * A trigger will activate an action + * + * @package iTopORM + */ +abstract class Trigger extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_trigger", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("action_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"trigger_id", "ext_key_to_remote"=>"action_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('finalclass', 'description', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass')); // 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 + } + + public function DoActivate($aContextArgs) + { + // Find the related + $oLinkedActions = $this->Get('action_list'); + while ($oLink = $oLinkedActions->Fetch()) + { + $iActionId = $oLink->Get('action_id'); + $oAction = MetaModel::GetObject('Action', $iActionId); + if ($oAction->IsActive()) + { + $oAction->DoExecute($this, $aContextArgs); + } + } + } +} + +abstract class TriggerOnObject extends Trigger +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_trigger_onobject", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("class_category"=>"bizmodel", "more_values"=>null, "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // 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 + } +} + +abstract class TriggerOnStateChange extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_trigger_onstatechange", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("state", array("allowed_values"=>null, "sql"=>"state", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'state')); // 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 TriggerOnStateEnter extends TriggerOnStateChange +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_trigger_onstateenter", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // 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 TriggerOnStateLeave extends TriggerOnStateChange +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_trigger_onstateleave", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('')); // Criteria of the advanced search form + } +} + +class TriggerOnObjectCreate extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "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(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'action_list')); // 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() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(""), + "db_table" => "priv_link_action_trigger", + "db_key_field" => "link_id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> '', "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("action_name", array("allowed_values"=>null, "extkey_attcode"=> 'action_id', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> '', "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("trigger_name", array("allowed_values"=>null, "extkey_attcode"=> 'trigger_id', "target_attcode"=>"description"))); + MetaModel::Init_AddAttribute(new AttributeInteger("order", array("allowed_values"=>null, "sql"=>"order", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list + MetaModel::Init_SetZListItems('list', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('action_id', 'trigger_id', 'order')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('action_id', 'trigger_id', 'order')); // Criteria of the advanced search form + } +} +?> diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php new file mode 100644 index 0000000000..34c53f9b9e --- /dev/null +++ b/core/userrights.class.inc.php @@ -0,0 +1,823 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + + +class UserRightException extends CoreException +{ +} + + +define('UR_ALLOWED_NO', 0); +define('UR_ALLOWED_YES', 1); +define('UR_ALLOWED_DEPENDS', 2); + +define('UR_ACTION_READ', 1); // View an object +define('UR_ACTION_MODIFY', 2); // Create/modify an object/attribute +define('UR_ACTION_DELETE', 3); // Delete an object + +define('UR_ACTION_BULK_READ', 4); // Export multiple objects +define('UR_ACTION_BULK_MODIFY', 5); // Create/modify multiple objects +define('UR_ACTION_BULK_DELETE', 6); // Delete multiple objects + +define('UR_ACTION_APPLICATION_DEFINED', 10000); // Application specific actions (CSV import, View schema...) + +/** + * User management module API + * + * @package iTopORM + */ +abstract class UserRightsAddOnAPI +{ + abstract public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US'); // could be used during initial installation + + abstract public function Init(); // loads data (possible optimizations) + + // Used to build select queries showing only objects visible for the given user + abstract public function GetSelectFilter($sLogin, $sClass); // returns a filter object + + abstract public function IsActionAllowed($oUser, $sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsAdministrator($oUser); + abstract public function IsPortalUser($oUser); + abstract public function FlushPrivileges(); +} + + +abstract class User extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core", + "key_type" => "autoincrement", + "name_attcode" => "login", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_user", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("contactid", array("targetclass"=>"Person", "allowed_values"=>null, "sql"=>"contactid", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("last_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("first_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"first_name"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("email", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"email"))); + + MetaModel::Init_AddAttribute(new AttributeString("login", array("allowed_values"=>null, "sql"=>"login", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>"EN US", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("profile_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"profileid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("allowed_org_list", array("linked_class"=>"URP_UserOrg", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"allowed_org_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('contactid', 'first_name', 'email', 'login', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'login')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('login', 'contactid')); // Criteria of the advanced search form + } + + abstract public function CheckCredentials($sPassword); + abstract public function TrustWebServerContext(); + abstract public function CanChangePassword(); + abstract public function ChangePassword($sOldPassword, $sNewPassword); + + /* + * Compute a name in best effort mode + */ + public function GetFriendlyName() + { + if (!MetaModel::IsValidAttCode(get_class($this), 'contactid')) + { + return $this->Get('login'); + } + if ($this->Get('contactid') != 0) + { + $sFirstName = $this->Get('first_name'); + $sLastName = $this->Get('last_name'); + $sEmail = $this->Get('email'); + if (strlen($sFirstName) > 0) + { + return "$sFirstName $sLastName"; + } + elseif (strlen($sEmail) > 0) + { + return "$sLastName <$sEmail>"; + } + else + { + return $sLastName; + } + } + } + + /* + * Overload the standard behavior + */ + public function DoCheckToWrite() + { + parent::DoCheckToWrite(); + + // Note: This MUST be factorized later: declare unique keys (set of columns) in the data model + $aChanges = $this->ListChanges(); + if (array_key_exists('login', $aChanges)) + { + $sNewLogin = $aChanges['login']; + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT User WHERE login = :newlogin"); + $oSet = new DBObjectSet($oSearch, array(), array('newlogin' => $sNewLogin)); + if ($oSet->Count() > 0) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:LoginMustBeUnique', $sNewLogin); + } + } + // Check that this user has at least one profile assigned + $oSet = $this->Get('profile_list'); + $aProfileLinks = $oSet->ToArray(); + if (count($aProfileLinks) == 0) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:AtLeastOneProfileIsNeeded'); + } + + } + + function GetGrantAsHtml($sClass, $iAction) + { + if (UserRights::IsActionAllowed($sClass, $iAction, null, $this)) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage, $sClassCategory) + { + if (UserRights::IsAdministrator($this)) + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + $oKPI = new ExecutionKPI(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses($sClassCategory) as $sClass) + { + $aClassStimuli = MetaModel::EnumStimuli($sClass); + if (count($aClassStimuli) > 0) + { + $aStimuli = array(); + foreach ($aClassStimuli as $sStimulusCode => $oStimulus) + { + if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $this)) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel()).''; + } + } + $sStimuli = implode(', ', $aStimuli); + } + else + { + $sStimuli = ''.Dict::S('UI:UserManagement:NoLifeCycleApplicable').''; + } + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($sClass, UR_ACTION_READ), + 'bulkread' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_READ), + 'write' => $this->GetGrantAsHtml($sClass, UR_ACTION_MODIFY), + 'bulkwrite' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_MODIFY), + 'stimuli' => $sStimuli, + ); + } + + $oKPI->ComputeAndReport('Computation of user rights'); + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage, 'bizmodel'); + + // debug + if (false) + { + $oPage->SetCurrentTab('More on user rigths (dev only)'); + $oPage->add("

User rights

\n"); + $this->DoShowGrantSumary($oPage, 'addon/userrights'); + $oPage->add("

Change log

\n"); + $this->DoShowGrantSumary($oPage, 'core/cmdb'); + $oPage->add("

Application

\n"); + $this->DoShowGrantSumary($oPage, 'application'); + $oPage->add("

GUI

\n"); + $this->DoShowGrantSumary($oPage, 'gui'); + + } + } + } +} + +/** + * Abstract class for all types of "internal" authentication i.e. users + * for which the application is supplied a login and a password opposed + * to "external" users for whom the authentication is performed outside + * of the application (by the web server for example). + * Note that "internal" users do not necessary correspond to a local authentication + * they may be authenticated by a remote system, like in authent-ldap. + */ +abstract class UserInternal extends User +{ + // Nothing special, just a base class to categorize this type of authenticated users + public static function Init() + { + $aParams = array + ( + "category" => "core", + "key_type" => "autoincrement", + "name_attcode" => "login", + "state_attcode" => "", + "reconc_keys" => array('login'), + "db_table" => "priv_internalUser", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('contactid', 'first_name', 'email', 'login', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'login')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('login', 'contactid')); // Criteria of the advanced search form + } +} + +/** + * User management core API + * + * @package iTopORM + */ +class UserRights +{ + protected static $m_oAddOn; + protected static $m_oUser; + protected static $m_oRealUser; + + public static function SelectModule($sModuleName) + { + if (!class_exists($sModuleName)) + { + throw new CoreException("Could not select this module, '$sModuleName' in not a valid class name"); + return; + } + if (!is_subclass_of($sModuleName, 'UserRightsAddOnAPI')) + { + throw new CoreException("Could not select this module, the class '$sModuleName' is not derived from UserRightsAddOnAPI"); + return; + } + self::$m_oAddOn = new $sModuleName; + self::$m_oAddOn->Init(); + self::$m_oUser = null; + self::$m_oRealUser = null; + } + + public static function GetModuleInstance() + { + return self::$m_oAddOn; + } + + // Installation: create the very first user + public static function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + $bRes = self::$m_oAddOn->CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage); + self::FlushPrivileges(true /* reset admin cache */); + return $bRes; + } + + protected static function IsLoggedIn() + { + if (self::$m_oUser == null) + { + return false; + } + else + { + return true; + } + } + + public static function Login($sName, $sAuthentication = 'any') + { + $oUser = self::FindUser($sName, $sAuthentication); + if (is_null($oUser)) + { + return false; + } + self::$m_oUser = $oUser; + Dict::SetUserLanguage(self::GetUserLanguage()); + return true; + } + + public static function CheckCredentials($sName, $sPassword, $sAuthentication = 'any') + { + $oUser = self::FindUser($sName, $sAuthentication); + if (is_null($oUser)) + { + return false; + } + + if (!$oUser->CheckCredentials($sPassword)) + { + return false; + } + return true; + } + + public static function TrustWebServerContext() + { + if (!is_null(self::$m_oUser)) + { + return self::$m_oUser->TrustWebServerContext(); + } + else + { + return false; + } + } + + public static function CanChangePassword() + { + if (MetaModel::DBIsReadOnly()) + { + return false; + } + + if (!is_null(self::$m_oUser)) + { + return self::$m_oUser->CanChangePassword(); + } + else + { + return false; + } + } + + public static function CanLogOff() + { + if (!is_null(self::$m_oUser)) + { + return self::$m_oUser->CanLogOff(); + } + else + { + return false; + } + } + + public static function ChangePassword($sOldPassword, $sNewPassword, $sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + // find the id out of the login string + $oUser = self::FindUser($sName); + } + if (is_null($oUser)) + { + return false; + } + else + { + return $oUser->ChangePassword($sOldPassword, $sNewPassword); + } + } + + public static function Impersonate($sName, $sPassword) + { + if (!self::CheckLogin()) return false; + + $oUser = self::FindUser($sName); + if (is_null($oUser)) + { + return false; + } + if (!$oUser->CheckCredentials($sPassword)) + { + return false; + } + + self::$m_oRealUser = self::$m_oUser; + self::$m_oUser = $oUser; + Dict::SetUserLanguage(self::GetUserLanguage()); + return true; + } + + public static function GetUser() + { + if (is_null(self::$m_oUser)) + { + return ''; + } + else + { + return self::$m_oUser->Get('login'); + } + } + + public static function GetUserObject() + { + if (is_null(self::$m_oUser)) + { + return null; + } + else + { + return self::$m_oUser; + } + } + + public static function GetUserLanguage() + { + if (is_null(self::$m_oUser)) + { + return 'EN US'; + + } + else + { + return self::$m_oUser->Get('language'); + } + } + + public static function GetUserId($sName = '') + { + if (empty($sName)) + { + // return current user id + if (is_null(self::$m_oUser)) + { + return null; + } + return self::$m_oUser->GetKey(); + } + else + { + // find the id out of the login string + $oUser = self::$m_oAddOn->FindUser($sName); + if (is_null($oUser)) + { + return null; + } + return $oUser->GetKey(); + } + } + + public static function GetContactId($sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + $oUser = FindUser($sName); + } + if (is_null($oUser)) + { + return ''; + } + if (!MetaModel::IsValidAttCode(get_class($oUser), 'contactid')) + { + return ''; + } + return $oUser->Get('contactid'); + } + + // Render the user name in best effort mode + public static function GetUserFriendlyName($sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + $oUser = FindUser($sName); + } + if (is_null($oUser)) + { + return ''; + } + return $oUser->GetFriendlyName(); + } + + public static function IsImpersonated() + { + if (is_null(self::$m_oRealUser)) + { + return false; + } + return true; + } + + public static function GetRealUser() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->Get('login'); + } + + public static function GetRealUserId() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->GetKey(); + } + + public static function GetRealUserFriendlyName() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->GetFriendlyName(); + } + + protected static function CheckLogin() + { + if (!self::IsLoggedIn()) + { + //throw new UserRightException('No user logged in', array()); + return false; + } + return true; + } + + public static function GetSelectFilter($sClass) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return true; + + if (self::IsAdministrator()) return true; + // Portal users actions are limited by the portal page... + if (self::IsPortalUser()) return true; + + if (MetaModel::HasCategory($sClass, 'bizmodel')) + { + return self::$m_oAddOn->GetSelectFilter(self::$m_oUser, $sClass); + } + else + { + return true; + } + } + + public static function IsActionAllowed($sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return true; + + if (MetaModel::DBIsReadOnly()) + { + if ($iActionCode == UR_ACTION_MODIFY) return false; + if ($iActionCode == UR_ACTION_DELETE) return false; + if ($iActionCode == UR_ACTION_BULK_MODIFY) return false; + if ($iActionCode == UR_ACTION_BULK_DELETE) return false; + } + + if (self::IsAdministrator($oUser)) return true; + + if (MetaModel::HasCategory($sClass, 'bizmodel')) + { + // #@# Temporary????? + // The read access is controlled in MetaModel::MakeSelectQuery() + if ($iActionCode == UR_ACTION_READ) return true; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + return self::$m_oAddOn->IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet); + } + elseif(($iActionCode == UR_ACTION_READ) && MetaModel::HasCategory($sClass, 'view_in_gui')) + { + return true; + } + else + { + // Other classes could be edited/listed by the administrators + return false; + } + } + + public static function IsStimulusAllowed($sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return true; + + if (MetaModel::DBIsReadOnly()) + { + if ($iActionCode == UR_ACTION_MODIFY) return false; + if ($iActionCode == UR_ACTION_DELETE) return false; + if ($iActionCode == UR_ACTION_BULK_MODIFY) return false; + if ($iActionCode == UR_ACTION_BULK_DELETE) return false; + } + + if (self::IsAdministrator($oUser)) return true; + + if (MetaModel::HasCategory($sClass, 'bizmodel')) + { + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + return self::$m_oAddOn->IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet); + } + else + { + // Other classes could be edited/listed by the administrators + return false; + } + } + + public static function IsActionAllowedOnAttribute($sClass, $sAttCode, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return true; + + if (MetaModel::DBIsReadOnly()) + { + if ($iActionCode == UR_ACTION_MODIFY) return false; + if ($iActionCode == UR_ACTION_DELETE) return false; + if ($iActionCode == UR_ACTION_BULK_MODIFY) return false; + if ($iActionCode == UR_ACTION_BULK_DELETE) return false; + } + + if (self::IsAdministrator($oUser)) return true; + + // this module is forbidden for non admins + if (MetaModel::HasCategory($sClass, 'addon/userrights')) return false; + + // the rest is allowed (#@# to be improved) + if (!MetaModel::HasCategory($sClass, 'bizmodel')) return true; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + return self::$m_oAddOn->IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet); + } + + static $m_aAdmins = array(); + public static function IsAdministrator($oUser = null) + { + if (!self::CheckLogin()) return false; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + $iUser = $oUser->GetKey(); + if (!isset(self::$m_aAdmins[$iUser])) + { + self::$m_aAdmins[$iUser] = self::$m_oAddOn->IsAdministrator($oUser); + } + return self::$m_aAdmins[$iUser]; + } + + static $m_aPortalUsers = array(); + public static function IsPortalUser($oUser = null) + { + if (!self::CheckLogin()) return false; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + $iUser = $oUser->GetKey(); + if (!isset(self::$m_aPortalUsers[$iUser])) + { + self::$m_aPortalUsers[$iUser] = self::$m_oAddOn->IsPortalUser($oUser); + } + return self::$m_aPortalUsers[$iUser]; + } + + /** + * Reset cached data + * @param Bool Reset admin cache as well + * @return void + */ + // Reset cached data + // + public static function FlushPrivileges($bResetAdminCache = false) + { + if ($bResetAdminCache) + { + self::$m_aAdmins = array(); + } + return self::$m_oAddOn->FlushPrivileges(); + } + + static $m_aCacheUsers; + /** + * Find a user based on its login and its type of authentication + * @param string $sLogin Login/identifier of the user + * @param string $sAuthentication Type of authentication used: internal|external|any + * @return User The found user or null + */ + protected static function FindUser($sLogin, $sAuthentication = 'any') + { + if ($sAuthentication == 'any') + { + $oUser = self::FindUser($sLogin, 'internal'); + if ($oUser == null) + { + $oUser = self::FindUser($sLogin, 'external'); + } + } + else + { + if (!isset(self::$m_aCacheUsers)) + { + self::$m_aCacheUsers = array('internal' => array(), 'external' => array()); + } + + if (!isset(self::$m_aCacheUsers[$sAuthentication][$sLogin])) + { + switch($sAuthentication) + { + case 'external': + $sBaseClass = 'UserExternal'; + break; + + case 'internal': + $sBaseClass = 'UserInternal'; + break; + + default: + echo "

sAuthentication = $sAuthentication

\n"; + assert(false); // should never happen + } + $oSearch = DBObjectSearch::FromOQL("SELECT $sBaseClass WHERE login = :login"); + $oSet = new DBObjectSet($oSearch, array(), array('login' => $sLogin)); + $oUser = $oSet->fetch(); + self::$m_aCacheUsers[$sAuthentication][$sLogin] = $oUser; + } + $oUser = self::$m_aCacheUsers[$sAuthentication][$sLogin]; + } + return $oUser; + } +} + +?> diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php new file mode 100644 index 0000000000..ee9bfddecf --- /dev/null +++ b/core/valuesetdef.class.inc.php @@ -0,0 +1,284 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL + */ + +require_once('MyHelpers.class.inc.php'); + +/** + * ValueSetDefinition + * value sets API and implementations + * + * @package iTopORM + */ +abstract class ValueSetDefinition +{ + protected $m_bIsLoaded = false; + protected $m_aValues = array(); + + + // Displayable description that could be computed out of the std usage context + public function GetValuesDescription() + { + $aValues = $this->GetValues(array(), ''); + $aDisplayedValues = array(); + foreach($aValues as $key => $value) + { + $aDisplayedValues[] = "$key => $value"; + } + $sAllowedValues = implode(', ', $aDisplayedValues); + return $sAllowedValues; + } + + + public function GetValues($aArgs, $sContains = '') + { + if (!$this->m_bIsLoaded) + { + $this->LoadValues($aArgs); + $this->m_bIsLoaded = true; + } + if (strlen($sContains) == 0) + { + $aRet = $this->m_aValues; + } + else + { + $aRet = array(); + foreach ($this->m_aValues as $sKey=>$sValue) + { + if (stripos($sValue, $sContains) !== false) + { + $aRet[$sKey] = $sValue; + } + } + } + asort($aRet); + return $aRet; + } + + abstract protected function LoadValues($aArgs); +} + + +/** + * Set of existing values for an attribute, given a search filter + * + * @package iTopORM + */ +class ValueSetObjects extends ValueSetDefinition +{ + protected $m_sFilterExpr; // in OQL + protected $m_sValueAttCode; + protected $m_aOrderBy; + private $m_bAllowAllData; + + public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false) + { + $this->m_sFilterExpr = $sFilterExp; + $this->m_sValueAttCode = $sValueAttCode; + $this->m_aOrderBy = $aOrderBy; + $this->m_bAllowAllData = $bAllowAllData; + } + + protected function LoadValues($aArgs) + { + $this->m_aValues = array(); + + if ($this->m_bAllowAllData) + { + $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); + } + else + { + $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); + } + if (!$oFilter) return false; + + $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); + while ($oObject = $oObjects->Fetch()) + { + if (empty($this->m_sValueAttCode)) + { + $this->m_aValues[$oObject->GetKey()] = $oObject->GetName(); + } + else + { + $this->m_aValues[$oObject->GetKey()] = $oObject->Get($this->m_sValueAttCode); + } + } + return true; + } + + public function GetValuesDescription() + { + return 'Filter: '.$this->m_sFilterExpr; + } +} + + +/** + * Set of existing values for a link set attribute, given a relation code + * + * @package iTopORM + */ +class ValueSetRelatedObjectsFromLinkSet extends ValueSetDefinition +{ + protected $m_sLinkSetAttCode; + protected $m_sExtKeyToRemote; + protected $m_sRelationCode; + protected $m_iMaxDepth; + protected $m_sTargetClass; + protected $m_sTargetExtKey; +// protected $m_aOrderBy; + + public function __construct($sLinkSetAttCode, $sExtKeyToRemote, $sRelationCode, $iMaxDepth, $sTargetClass, $sTargetLinkClass, $sTargetExtKey) + { + $this->m_sLinkSetAttCode = $sLinkSetAttCode; + $this->m_sExtKeyToRemote = $sExtKeyToRemote; + $this->m_sRelationCode = $sRelationCode; + $this->m_iMaxDepth = $iMaxDepth; + $this->m_sTargetClass = $sTargetClass; + $this->m_sTargetLinkClass = $sTargetLinkClass; + $this->m_sTargetExtKey = $sTargetExtKey; +// $this->m_aOrderBy = $aOrderBy; + } + + protected function LoadValues($aArgs) + { + $this->m_aValues = array(); + + if (!array_key_exists('this', $aArgs)) + { + throw new CoreException("Missing 'this' in arguments", array('args' => $aArgs)); + } + + $oTarget = $aArgs['this->object()']; + + // Nodes from which we will start the search for neighbourhood + $oNodes = DBObjectSet::FromLinkSet($oTarget, $this->m_sLinkSetAttCode, $this->m_sExtKeyToRemote); + + // Neighbours, whatever their class + $aRelated = $oNodes->GetRelatedObjects($this->m_sRelationCode, $this->m_iMaxDepth); + + $sRootClass = MetaModel::GetRootClass($this->m_sTargetClass); + if (array_key_exists($sRootClass, $aRelated)) + { + $aLinksToCreate = array(); + foreach($aRelated[$sRootClass] as $iKey => $oObject) + { + if (MetaModel::IsParentClass($this->m_sTargetClass, get_class($oObject))) + { + $oNewLink = MetaModel::NewObject($this->m_sTargetLinkClass); + $oNewLink->Set($this->m_sTargetExtKey, $iKey); + //$oNewLink->Set('role', 'concerned by an impacted CI'); + + $aLinksToCreate[] = $oNewLink; + } + } + // #@# or AddObjectArray($aObjects) ? + $oSetToCreate = DBObjectSet::FromArray($this->m_sTargetLinkClass, $aLinksToCreate); + $this->m_aValues[$oObject->GetKey()] = $oObject->GetAsHTML($oObject->GetName()); + } + + return true; + } + + public function GetValuesDescription() + { + return 'Filter: '.$this->m_sFilterExpr; + } +} + + +/** + * Fixed set values (could be hardcoded in the business model) + * + * @package iTopORM + */ +class ValueSetEnum extends ValueSetDefinition +{ + protected $m_values; + + public function __construct($Values) + { + $this->m_values = $Values; + } + + protected function LoadValues($aArgs) + { + if (is_array($this->m_values)) + { + $aValues = $this->m_values; + } + elseif (is_string($this->m_values) && strlen($this->m_values) > 0) + { + $aValues = array(); + foreach (explode(",", $this->m_values) as $sVal) + { + $sVal = trim($sVal); + $sKey = $sVal; + $aValues[$sKey] = $sVal; + } + } + else + { + $aValues = array(); + } + $this->m_aValues = $aValues; + return true; + } +} + + +/** + * Data model classes + * + * @package iTopORM + */ +class ValueSetEnumClasses extends ValueSetEnum +{ + protected $m_sCategories; + + 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/css/blue_green.css b/css/blue_green.css new file mode 100644 index 0000000000..18585c3cd0 --- /dev/null +++ b/css/blue_green.css @@ -0,0 +1,222 @@ +/* CSS Document */ +body { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + background-color: #68a; + color:#000000; + margin: 0; /* Remove body margin/padding */ + padding: 0; + overflow: hidden; /* Remove scroll bars on browser window */ +} + +table { + border: 1px solid #000000; +} + +.raw_output { + font-family: Courier-New, Courier, Arial, Helevtica; + font-size: smaller; + background-color: #eeeeee; + color: #000000; + border: 1px dashed #000000; + padding: 0.25em; + margin-top: 1em; +} + +th { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + background-color:#ace27d; +} + +td { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + background-color: #b7cfe8; +} + +tr.clicked td { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + background-color: #ffcfe8; +} + +td.label { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + background-color:#ace27d; + padding: 0.2em; +} + +td a, td a:visited { + text-decoration:none; + color:#000000; +} +td a:hover { + text-decoration:underline; + color:#FFFFFF; +} + +a.small_action { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + text-decoration:none; +} + +.display_block { + noborder: 1px dashed #CCC; + background: #79b; + padding:0.25em; +} +div#TopPane .display_block { + background: #f0eee0; + padding:0.25em; + text-align:center; +} +div#TopPane label { + color:#000; + background: #f0eee0; +} + +div#TopPane td { + color:#000; + background: #f0eee0; +} + +.loading { + noborder: 1px dashed #CCC; + background: #b9c1c8; + padding:0.25em; +} + +label { + font-family:Georgia, "Times New Roman", Times, serif; + color:#FFFFFF; + text-align:right; +} + +input.textSearch { + border:1px solid #333; + noheight:1.2em; + font-size:0.8em; + font-family:Verdana, Arial, Helvetica, sans-serif; + color:#000000; +} + +/* By Rom */ +.csvimport_createobj { + color: #AA0000; + background-color:#EEEEEE; +} +.csvimport_error { + font-weight: bold; + color: #FF0000; + background-color:#EEEEEE; +} +.csvimport_warning { + color: #CC8888; + background-color:#EEEEEE; +} +.csvimport_ok { + color: #00000; + background-color:#BBFFBB; +} +.csvimport_reconkey { + font-style: italic; + color: #888888; + background-color:#FFFFF; +} +.csvimport_extreconkey { + color: #888888; + background-color:#FFFFFF; +} + +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; + font-size:0.9em; +} + +ul.dir li { + padding: 2px 0 0 16px; +} + +.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } +.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } +.treeview .expandable { background-image: url(../images/tv-expandable.gif); } +.treeview .last { background-image: url(../images/tv-item-last.gif); } +.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } +.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } + +#Header { padding: 0; background:#ccc url(../images/bandeau2.gif) repeat-x center;} +div.iTopLogo { + background:url(../images/iTop.gif) no-repeat center; + width:100px; + height:56px; +} +div.iTopLogo span { + display:none; +} + +#MySplitter { + /* Height is set to match window size in $().ready() below */ + border:0px; + margin:4px; + padding:0px; + min-width: 100px; /* Splitter can't be too thin ... */ + min-height: 100px; /* ... or too flat */ +} +#LeftPane { + background: #f0eee0; + padding: 4px; + overflow: auto; /* Scroll bars appear as needed */ + color:#666; +} +#TopPane { /* Top nested in right pane */ + background: #f0eee0; + padding: 4px; + height: 150px; /* Initial height */ + min-height: 75px; /* Minimum height */ + overflow: auto; + color:#666; +} +#RightPane { /* Bottom nested in right pane */ + background: #79b; + height:150px; /* Initial height */ + min-height:130px; + no.padding:15px; + no.margin:10px; + overflow:auto; + color:#fff; +} + +#BottomPane { /* Bottom nested in right pane */ + background: #79b; + padding: 4px; + overflow: auto; + color:#fff; +} +#MySplitter .vsplitbar { + width: 7px; + height: 50px; + background: #68a url(../images/vgrabber2.gif) no-repeat center; +} +#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { + background: #68a url(../images/vgrabber2_active.gif) no-repeat center; +} +#MySplitter .hsplitbar { + height: 8px; + background: #68a url(../images/hgrabber2.gif) no-repeat center; +} +#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { + background: #68a url(../images/hgrabber2_active.gif) no-repeat center; +} diff --git a/css/date.picker.css b/css/date.picker.css new file mode 100644 index 0000000000..a394482a08 --- /dev/null +++ b/css/date.picker.css @@ -0,0 +1,117 @@ + + +table.jCalendar { + border: 1px solid #000; + background: #aaa; + border-collapse: separate; + border-spacing: 2px; +} +table.jCalendar th { + background: #333; + color: #fff; + font-weight: bold; + padding: 3px 5px; +} +table.jCalendar td { + background: #ccc; + color: #000; + padding: 3px 5px; + text-align: center; +} +table.jCalendar td.other-month { + background: #ddd; + color: #aaa; +} +table.jCalendar td.today { + background: #666; + color: #fff; +} +table.jCalendar td.selected { + background: #f66; + color: #fff; +} +table.jCalendar td.selected:hover { + background: #f33; + color: #fff; +} +table.jCalendar td:hover, table.jCalendar td.dp-hover { + background: #fff; + color: #000; +} +table.jCalendar td.disabled, table.jCalendar td.disabled:hover { + background: #bbb; + color: #888; +} + +/* For the popup */ + +/* NOTE - you will probably want to style a.dp-choose-date - see how I did it in demo.css */ + +div.dp-popup { + position: relative; + background: #ccc; + font-size: 10px; + font-family: arial, sans-serif; + padding: 2px; + width: 171px; + line-height: 1.2em; +} +div#dp-popup { + position: absolute; + z-index: 199; +} +div.dp-popup h2 { + font-size: 12px; + text-align: center; + margin: 2px 0; + padding: 0; +} +a#dp-close { + font-size: 11px; + padding: 4px 0; + text-align: center; + display: block; +} +a#dp-close:hover { + text-decoration: underline; +} +div.dp-popup a { + color: #000; + text-decoration: none; + padding: 3px 2px 0; +} +div.dp-popup div.dp-nav-prev { + position: absolute; + top: 2px; + left: 4px; + width: 100px; +} +div.dp-popup div.dp-nav-prev a { + float: left; +} +/* Opera needs the rules to be this specific otherwise it doesn't change the cursor back to pointer after you have disabled and re-enabled a link */ +div.dp-popup div.dp-nav-prev a, div.dp-popup div.dp-nav-next a { + cursor: pointer; +} +div.dp-popup div.dp-nav-prev a.disabled, div.dp-popup div.dp-nav-next a.disabled { + cursor: default; +} +div.dp-popup div.dp-nav-next { + position: absolute; + top: 2px; + right: 4px; + width: 100px; +} +div.dp-popup div.dp-nav-next a { + float: right; +} +div.dp-popup a.disabled { + cursor: default; + color: #aaa; +} +div.dp-popup td { + cursor: pointer; +} +div.dp-popup td.disabled { + cursor: default; +} \ No newline at end of file diff --git a/css/default.css b/css/default.css new file mode 100644 index 0000000000..eaabed838f --- /dev/null +++ b/css/default.css @@ -0,0 +1,165 @@ +/* CSS Document */ +body { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + background-color: #ffffff; + color:#000000; + margin: 0; /* Remove body margin/padding */ + padding: 0; + overflow: hidden; /* Remove scroll bars on browser window */ +} + +table { + border: 1px solid #000000; +} + +.raw_output { + font-family: Courier-New, Courier, Arial, Helevtica; + font-size: smaller; + background-color: #eeeeee; + color: #000000; + border: 1px dashed #000000; + padding: 0.25em; + margin-top: 1em; +} + +th { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + color: #000000; + background-color:#E1DEB5; +} + +td { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; +} + +td.label { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + color: #000000; + background-color:#E1DEB5; + padding: 0.2em; +} + +a.small_action { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + text-decoration:none; +} + +.display_block { + border: 1px dashed #CCC; + background: #CFC; + padding:0.25em; +} + +.loading { + border: 1px dashed #CCC; + background: #FCC; + padding:0.25em; +} + +/* By Rom */ +.csvimport_createobj { + color: #AA0000; + background-color:#EEEEEE; +} +.csvimport_error { + font-weight: bold; + color: #FF0000; + background-color:#EEEEEE; +} +.csvimport_warning { + color: #CC8888; + background-color:#EEEEEE; +} +.csvimport_ok { + color: #00000; + background-color:#BBFFBB; +} +.csvimport_reconkey { + font-style: italic; + color: #888888; + background-color:#FFFFF; +} +.csvimport_extreconkey { + color: #888888; + background-color:#FFFFFF; +} + +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; +} + +ul.dir li { padding: 2px 0 0 16px; } + +.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } +.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } +.treeview .expandable { background-image: url(../images/tv-expandable.gif); } +.treeview .last { background-image: url(../images/tv-item-last.gif); } +.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } +.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } + +#MySplitter { + /* Height is set to match window size in $().ready() below */ + border:0px; + margin:4px; + padding:0px; + min-width: 100px; /* Splitter can't be too thin ... */ + min-height: 100px; /* ... or too flat */ +} +#LeftPane { + background: #f0eee0; + padding: 4px; + overflow: auto; /* Scroll bars appear as needed */ + color:#666; +} +#TopPane { /* Top nested in right pane */ + background: #f0eee0; + padding: 4px; + height: 150px; /* Initial height */ + min-height: 75px; /* Minimum height */ + overflow: auto; + color:#666; +} +#RightPane { /* Bottom nested in right pane */ + background: #79b; + height:150px; /* Initial height */ + min-height:130px; + no.padding:15px; + no.margin:10px; + overflow:auto; + color:#fff; +} + +#BottomPane { /* Bottom nested in right pane */ + background: #79b; + padding: 4px; + overflow: auto; + color:#fff; +} +#MySplitter .vsplitbar { + width: 7px; + height: 50px; + background: #68a url(../images/vgrabber2.gif) no-repeat center; +} +#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { + background: #68a url(../images/vgrabber2_active.gif) no-repeat center; +} +#MySplitter .hsplitbar { + height: 8px; + background: #68a url(../images/hgrabber2.gif) no-repeat center; +} +#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { + background: #68a url(../images/hgrabber2_active.gif) no-repeat center; +} diff --git a/css/jqModal.css b/css/jqModal.css new file mode 100644 index 0000000000..41e0593437 --- /dev/null +++ b/css/jqModal.css @@ -0,0 +1,39 @@ +/* jqModal base Styling courtesy of; + Brice Burgess */ + +/* The Window's CSS z-index value is respected (takes priority). If none is supplied, + the Window's z-index value will be set to 3000 by default (via jqModal.js). */ + +.jqmWindow { + display: none; + + position: fixed; + no.top: 17%; + no.left: 50%; + + no.margin-left: -300px; + no.width: 700px; + + background-color: #EEE; + color: #333; + border: 1px solid black; + padding: 12px; + + z-index:9999; +} + +.jqmOverlay { background-color: #000; } + +/* Background iframe styling for IE6. Prevents ActiveX bleed-through (
');return D.join('');};k.dialog.labeledElement.call(this,u,v,w,C);},textarea:function(u,v,w){if(arguments.length<3)return;m.call(this,v);var x=this,y=this._.inputId=e.getNextId()+'_textarea',z={};if(v.validate)this.validate=v.validate;z.rows=v.rows||5;z.cols=v.cols||20;var A=function(){z['aria-labelledby']=this._.labelId;this._.required&&(z['aria-required']=this._.required);var B=['');return B.join('');};k.dialog.labeledElement.call(this,u,v,w,A);},checkbox:function(u,v,w){if(arguments.length<3)return;var x=m.call(this,v,{'default':!!v['default']});if(v.validate)this.validate=v.validate;var y=function(){var z=e.extend({},v,{id:v.id?v.id+'_checkbox':e.getNextId()+'_checkbox'},true),A=[],B=e.getNextId()+'_label',C={'class':'cke_dialog_ui_checkbox_input',type:'checkbox','aria-labelledby':B};t(z);if(v['default'])C.checked='checked';if(typeof z.controlStyle!='undefined')z.style=z.controlStyle;x.checkbox=new k.dialog.uiElement(u,z,A,'input',null,C);A.push(' ');return A.join('');};k.dialog.uiElement.call(this,u,v,w,'span',null,null,y);},radio:function(u,v,w){if(arguments.length<3)return;m.call(this,v);if(!this._['default'])this._['default']=this._.initValue=v.items[0][1];if(v.validate)this.validate=v.valdiate;var x=[],y=this,z=function(){var A=[],B=[],C={'class':'cke_dialog_ui_radio_item','aria-labelledby':this._.labelId},D=v.id?v.id+'_radio':e.getNextId()+'_radio';for(var E=0;E'+e.htmlEncode(v.label)+'');},select:function(u,v,w){if(arguments.length<3)return;var x=m.call(this,v);if(v.validate)this.validate=v.validate;x.inputId=e.getNextId()+'_select';var y=function(){var z=e.extend({},v,{id:v.id?v.id+'_select':e.getNextId()+'_select'},true),A=[],B=[],C={id:x.inputId,'class':'cke_dialog_ui_input_select','aria-labelledby':this._.labelId};if(v.size!=undefined)C.size=v.size;if(v.multiple!=undefined)C.multiple=v.multiple;t(z);for(var D=0,E;D ',e.htmlEncode(E[0]));if(typeof z.controlStyle!='undefined')z.style=z.controlStyle;x.select=new k.dialog.uiElement(u,z,A,'select',null,C,B.join(''));return A.join('');};k.dialog.labeledElement.call(this,u,v,w,y);},file:function(u,v,w){if(arguments.length<3)return;if(v['default']===undefined)v['default']='';var x=e.extend(m.call(this,v),{definition:v,buttons:[]});if(v.validate)this.validate=v.validate;var y=function(){x.frameId=e.getNextId()+'_fileInput';var z=b.isCustomDomain(),A=['');return A.join('');};u.on('load',function(){var z=a.document.getById(x.frameId),A=z.getParent(); +A.addClass('cke_dialog_ui_input_file');});k.dialog.labeledElement.call(this,u,v,w,y);},fileButton:function(u,v,w){if(arguments.length<3)return;var x=m.call(this,v),y=this;if(v.validate)this.validate=v.validate;var z=e.extend({},v),A=z.onClick;z.className=(z.className?z.className+' ':'')+'cke_dialog_ui_button';z.onClick=function(B){var C=v['for'];if(!A||A.call(this,B)!==false){u.getContentElement(C[0],C[1]).submit();this.disable();}};u.on('load',function(){u.getContentElement(v['for'][0],v['for'][1])._.buttons.push(y);});k.dialog.button.call(this,u,z,w);},html:(function(){var u=/^\s*<[\w:]+\s+([^>]*)?>/,v=/^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/,w=/\/$/;return function(x,y,z){if(arguments.length<3)return;var A=[],B,C=y.html,D,E;if(C.charAt(0)!='<')C=''+C+'';var F=y.focus;if(F){var G=this.focus;this.focus=function(){G.call(this);typeof F=='function'&&F.call(this);this.fire('focus');};if(y.isFocusable){var H=this.isFocusable;this.isFocusable=H;}this.keyboardFocusable=true;}k.dialog.uiElement.call(this,x,y,A,'span',null,null,'');B=A.join('');D=B.match(u);E=C.match(v)||['','',''];if(w.test(E[1])){E[1]=E[1].slice(0,-1);E[2]='/'+E[2];}z.push([E[1],' ',D[1]||'',E[2]].join(''));};})(),fieldset:function(u,v,w,x,y){var z=y.label,A=function(){var B=[];z&&B.push(''+z+'');for(var C=0;C0)u.remove(0);return this;},keyboardFocusable:true},q,true);k.dialog.checkbox.prototype=e.extend(new k.dialog.uiElement(),{getInputElement:function(){return this._.checkbox.getElement();},setValue:function(u,v){this.getInputElement().$.checked=u;!v&&this.fire('change',{value:u});},getValue:function(){return this.getInputElement().$.checked;},accessKeyUp:function(){this.setValue(!this.getValue());},eventProcessors:{onChange:function(u,v){if(!c)return r.onChange.apply(this,arguments);else{u.on('load',function(){var w=this._.checkbox.getElement();w.on('propertychange',function(x){x=x.data.$;if(x.propertyName=='checked')this.fire('change',{value:w.$.checked});},this);},this);this.on('change',v);}return null;}},keyboardFocusable:true},q,true);k.dialog.radio.prototype=e.extend(new k.dialog.uiElement(),{setValue:function(u,v){var w=this._.children,x;for(var y=0;y0?new h(u.$.forms[0].elements[0]):this.getElement();},submit:function(){this.getInputElement().getParent().$.submit();return this;},getAction:function(){return this.getInputElement().getParent().$.action;},registerEvents:function(u){var v=/^on([A-Z]\w+)/,w,x=function(z,A,B,C){z.on('formLoaded',function(){z.getInputElement().on(B,C,z);});};for(var y in u){if(!(w=y.match(v)))continue;if(this.eventProcessors[y])this.eventProcessors[y].call(this,this._.dialog,u[y]);else x(this,this._.dialog,w[1].toLowerCase(),u[y]);}return this;},reset:function(){var u=this._,v=a.document.getById(u.frameId),w=v.getFrameDocument(),x=u.definition,y=u.buttons,z=this.formLoadedNumber,A=this.formUnloadNumber,B=u.dialog._.editor.lang.dir,C=u.dialog._.editor.langCode;if(!z){z=this.formLoadedNumber=e.addFunction(function(){this.fire('formLoaded');},this);A=this.formUnloadNumber=e.addFunction(function(){this.getInputElement().clearCustomData();},this);this.getDialog()._.editor.on('destroy',function(){e.removeFunction(z);e.removeFunction(A);});}function D(){w.$.open();if(b.isCustomDomain())w.$.domain=document.domain;var E='';if(x.size)E=x.size-(c?7:0);w.$.write(['','
','','
','',''].join(''));w.$.close();for(var F=0;F
');return o;},getHolderElement:function(){var m=this._.holder;if(!m){if(this.forceIFrame||this.css.length){var n=this.document.getById(this.id+'_frame'),o=n.getParent(),p=o.getAttribute('dir'),q=o.getParent().getAttribute('class'),r=o.getParent().getAttribute('lang'),s=n.getFrameDocument();s.$.open();if(b.isCustomDomain())s.$.domain=document.domain;var t=e.addFunction(e.bind(function(v){this.isLoaded=true;if(this.onLoad)this.onLoad();},this));s.$.write(''+''+''+''+''+e.buildStyleHtml(this.css)+''); +s.$.close();var u=s.getWindow();u.$.CKEDITOR=a;s.on('key'+(b.opera?'press':'down'),function(v){var y=this;var w=v.data.getKeystroke(),x=y.document.getById(y.id).getAttribute('dir');if(y._.onKeyDown&&y._.onKeyDown(w)===false){v.data.preventDefault();return;}if(w==27||w==(x=='rtl'?39:37))if(y.onEscape&&y.onEscape(w)===false)v.data.preventDefault();},this);m=s.getBody();m.unselectable();}else m=this.document.getById(this.id);this._.holder=m;}return m;},addBlock:function(m,n){var o=this;n=o._.blocks[m]=n instanceof k.panel.block?n:new k.panel.block(o.getHolderElement(),n);if(!o._.currentBlock)o.showBlock(m);return n;},getBlock:function(m){return this._.blocks[m];},showBlock:function(m){var n=this._.blocks,o=n[m],p=this._.currentBlock,q=this.forceIFrame?this.document.getById(this.id+'_frame'):this._.holder;q.getParent().getParent().disableContextMenu();if(p){q.removeAttributes(p.attributes);p.hide();}this._.currentBlock=o;q.setAttributes(o.attributes);a.fire('ariaWidget',q);o._.focusIndex=-1;this._.onKeyDown=o.onKeyDown&&e.bind(o.onKeyDown,o);o.onMark=function(r){q.setAttribute('aria-activedescendant',r.getId()+'_option');};o.onUnmark=function(){q.removeAttribute('aria-activedescendant');};o.show();return o;},destroy:function(){this.element&&this.element.remove();}};k.panel.block=e.createClass({$:function(m,n){var o=this;o.element=m.append(m.getDocument().createElement('div',{attributes:{tabIndex:-1,'class':'cke_panel_block',role:'presentation'},styles:{display:'none'}}));if(n)e.extend(o,n);if(!o.attributes.title)o.attributes.title=o.attributes['aria-label'];o.keys={};o._.focusIndex=-1;o.element.disableContextMenu();},_:{markItem:function(m){var p=this;if(m==-1)return;var n=p.element.getElementsByTag('a'),o=n.getItem(p._.focusIndex=m);if(b.webkit||b.opera)o.getDocument().getWindow().focus();o.focus();p.onMark&&p.onMark(o);}},proto:{show:function(){this.element.setStyle('display','');},hide:function(){var m=this;if(!m.onHide||m.onHide.call(m)!==true)m.element.setStyle('display','none');},onKeyDown:function(m){var r=this;var n=r.keys[m];switch(n){case 'next':var o=r._.focusIndex,p=r.element.getElementsByTag('a'),q;while(q=p.getItem(++o)){if(q.getAttribute('_cke_focus')&&q.$.offsetWidth){r._.focusIndex=o;q.focus();break;}}return false;case 'prev':o=r._.focusIndex;p=r.element.getElementsByTag('a');while(o>0&&(q=p.getItem(--o))){if(q.getAttribute('_cke_focus')&&q.$.offsetWidth){r._.focusIndex=o;q.focus();break;}}return false;case 'click':o=r._.focusIndex;q=o>=0&&r.element.getElementsByTag('a').getItem(o); +if(q)q.$.click?q.$.click():q.$.onclick();return false;}return true;}}});j.add('listblock',{requires:['panel'],onLoad:function(){k.panel.prototype.addListBlock=function(m,n){return this.addBlock(m,new k.listBlock(this.getHolderElement(),n));};k.listBlock=e.createClass({base:k.panel.block,$:function(m,n){var q=this;n=n||{};var o=n.attributes||(n.attributes={});(q.multiSelect=!!n.multiSelect)&&(o['aria-multiselectable']=true);!o.role&&(o.role='listbox');q.base.apply(q,arguments);var p=q.keys;p[40]='next';p[9]='next';p[38]='prev';p[2000+9]='prev';p[32]='click';q._.pendingHtml=[];q._.items={};q._.groups={};},_:{close:function(){if(this._.started){this._.pendingHtml.push('');delete this._.started;}},getClick:function(){if(!this._.click)this._.click=e.addFunction(function(m){var o=this;var n=true;if(o.multiSelect)n=o.toggle(m);else o.mark(m);if(o.onClick)o.onClick(m,n);},this);return this._.click;}},proto:{add:function(m,n,o){var r=this;var p=r._.pendingHtml,q=e.getNextId();if(!r._.started){p.push('