diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 140ebe042b..7159a49c9e 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -502,6 +502,11 @@ abstract class DBObject implements iDisplay public function GetStrict($sAttCode) { + if ($sAttCode == 'id') + { + return $this->m_iKey; + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); if (!$oAttDef->LoadInObject()) @@ -3063,5 +3068,355 @@ abstract class DBObject implements iDisplay } return $sFingerprint; } + + /** + * Execute a set of scripted actions onto the current object + * See ExecAction for the syntax and features of the scripted actions + * + * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)") + * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element + * @throws Exception + */ + public function ExecActions($aActions, $aSourceObjects) + { + foreach($aActions as $sAction) + { + try + { + if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot + { + $sVerb = trim($aMatches[1]); + $sParams = $aMatches[2]; + + // the coma is the separator for the parameters + // comas can be escaped: \, + $sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams); + $sParams = trim($sParams); + + if (strlen($sParams) == 0) + { + $aParams = array(); + } + else + { + $aParams = explode(',', $sParams); + foreach ($aParams as &$sParam) + { + $sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam); + $sParam = trim($sParam); + } + } + $this->ExecAction($sVerb, $aParams, $aSourceObjects); + } + else + { + throw new Exception("Invalid syntax"); + } + } + catch(Exception $e) + { + throw new Exception('Action: '.$sAction.' - '.$e->getMessage()); + } + } + } + + /** + * Helper to copy an attribute between two objects (in memory) + * Originally designed for ExecAction() + */ + public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode) + { + if ($sSourceAttCode == 'id') + { + $oSourceAttDef = null; + } + else + { + if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode); + } + if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode); + } + + $oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode); + } + if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet()) + { + // The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets) + $oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass()); + $oSourceSet = $oSourceObject->Get($sSourceAttCode); + $oSourceSet->Rewind(); + while ($oSourceLink = $oSourceSet->Fetch()) + { + // Clone the link + $sLinkClass = get_class($oSourceLink); + $oLinkClone = MetaModel::NewObject($sLinkClass); + foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef) + { + // As of now, ignore other attribute (do not attempt to recurse!) + if ($oAttDef->IsScalar()) + { + $oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode)); + } + } + + // Not necessary - this will be handled by DBObject + // $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0); + $oDestSet->AddObject($oLinkClone); + } + $this->Set($sDestAttCode, $oDestSet); + } + else + { + $this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode)); + } + } + + /** + * Execute a scripted action onto the current object + * - clone (att1, att2, att3, ...) + * - clone_scalars () + * - copy (source_att, dest_att) + * - reset (att) + * - nullify (att) + * - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) + * - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) + * - add_to_list (source_key_att, dest_att) + * - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value) + * - apply_stimulus (stimulus) + * - call_method (method_name) + * + * @param $sVerb string Any of the verb listed above (e.g. "set") + * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$') + * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element + * @throws CoreException + * @throws CoreUnexpectedValue + * @throws Exception + */ + public function ExecAction($sVerb, $aParams, $aSourceObjects) + { + switch($sVerb) + { + case 'clone': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + foreach($aParams as $sAttCode) + { + $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); + } + break; + + case 'clone_scalars': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar()) + { + $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); + } + } + break; + + case 'copy': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: source attribute'); + } + $sSourceAttCode = $aParams[0]; + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: target attribute'); + } + $sDestAttCode = $aParams[1]; + $this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode); + break; + + case 'reset': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $this->Set($sAttCode, $oAttDef->GetDefaultValue()); + break; + + case 'nullify': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $this->Set($sAttCode, $oAttDef->GetNullValue()); + break; + + case 'set': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: value to set'); + } + $sRawValue = $aParams[1]; + $aContext = array(); + foreach ($aSourceObjects as $sAlias => $oObject) + { + $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); + } + $aContext['current_contact_id'] = UserRights::GetContactId(); + $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); + $aContext['current_date'] = date('Y-m-d'); + $aContext['current_time'] = date('H:i:s'); + $sValue = MetaModel::ApplyParams($sRawValue, $aContext); + $this->Set($sAttCode, $sValue); + break; + + case 'append': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: value to append'); + } + $sRawAddendum = $aParams[1]; + $aContext = array(); + foreach ($aSourceObjects as $sAlias => $oObject) + { + $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); + } + $aContext['current_contact_id'] = UserRights::GetContactId(); + $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); + $aContext['current_date'] = date('Y-m-d'); + $aContext['current_time'] = date('H:i:s'); + $sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext); + $this->Set($sAttCode, $this->Get($sAttCode).$sAddendum); + break; + + case 'add_to_list': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: source attribute'); + } + $sSourceKeyAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: target attribute (link set)'); + } + $sTargetListAttCode = $aParams[1]; // indirect !!! + if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode); + } + if (isset($aParams[2]) && isset($aParams[3])) + { + $sRoleAttCode = $aParams[2]; + $sRoleValue = $aParams[3]; + } + + $iObjKey = $oObjectToRead->Get($sSourceKeyAttCode); + if ($iObjKey > 0) + { + $oLinkSet = $this->Get($sTargetListAttCode); + + $oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode); + $oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass()); + $oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey); + if (isset($sRoleAttCode)) + { + if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode); + } + $oLnk->Set($sRoleAttCode, $sRoleValue); + } + $oLinkSet->AddObject($oLnk); + $this->Set($sTargetListAttCode, $oLinkSet); + } + break; + + case 'apply_stimulus': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: stimulus'); + } + $sStimulus = $aParams[0]; + if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this)))) + { + throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus); + } + $this->ApplyStimulus($sStimulus); + break; + + case 'call_method': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: method name'); + } + $sMethod = $aParams[0]; + $aCallSpec = array($this, $sMethod); + if (!is_callable($aCallSpec)) + { + throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()'); + } + // Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToReadą + call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects); + break; + + default: + throw new Exception("Invalid verb"); + } + } } diff --git a/test/testlist.inc.php b/test/testlist.inc.php index cc17f1de62..edd068dd14 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -4894,4 +4894,158 @@ class TestLinkSetRecording_1NAdd_Remove extends TestBizModel sort($aRet); return $aRet; } +} + +class TestExecActions extends TestBizModel +{ + static public function GetName() + { + return 'Scripted actions API DBObject::ExecAction - syntax errors'; + } + + static public function GetDescription() + { + return 'Check that wrong arguments are correclty reported'; + } + + protected function DoExecute() + { + $oSource = new UserRequest(); + $oSource->Set('title', 'Houston!'); + $oSource->Set('description', 'Looks like we have a problem'); + + $oTarget = new Server(); + + //////////////////////////////////////////////////////////////////////////////// + // Scenarii + // + $aScenarii = array( + array( + 'action' => 'set', + 'error' => 'Action: set - Invalid syntax' + ), + array( + 'action' => 'smurf()', + 'error' => 'Action: smurf() - Invalid verb' + ), + array( + 'action' => ' smurf () ', + 'error' => 'Action: smurf () - Invalid syntax' + ), + array( + 'action' => 'clone(some_att_code, another_one)', + 'error' => 'Action: clone(some_att_code, another_one) - Unknown attribute Server::some_att_code' + ), + array( + 'action' => 'copy(toto, titi)', + 'error' => 'Action: copy(toto, titi) - Unknown attribute Server::titi' + ), + array( + 'action' => 'copy(toto, name)', + 'error' => 'Action: copy(toto, name) - Unknown attribute UserRequest::toto' + ), + array( + 'action' => 'copy()', + 'error' => 'Action: copy() - Missing argument #1: source attribute' + ), + array( + 'action' => 'copy(title)', + 'error' => 'Action: copy(title) - Missing argument #2: target attribute' + ), + array( + 'action' => 'set(toto)', + 'error' => 'Action: set(toto) - Unknown attribute Server::toto' + ), + array( + 'action' => 'set(toto, something)', + 'error' => 'Action: set(toto, something) - Unknown attribute Server::toto' + ), + array( + 'action' => 'set()', + 'error' => 'Action: set() - Missing argument #1: target attribute' + ), + array( + 'action' => 'reset(toto)', + 'error' => 'Action: reset(toto) - Unknown attribute Server::toto' + ), + array( + 'action' => 'reset()', + 'error' => 'Action: reset() - Missing argument #1: target attribute' + ), + array( + 'action' => 'nullify(toto)', + 'error' => 'Action: nullify(toto) - Unknown attribute Server::toto' + ), + array( + 'action' => 'nullify()', + 'error' => 'Action: nullify() - Missing argument #1: target attribute' + ), + array( + 'action' => 'append(toto, something)', + 'error' => 'Action: append(toto, something) - Unknown attribute Server::toto' + ), + array( + 'action' => 'append(name)', + 'error' => 'Action: append(name) - Missing argument #2: value to append' + ), + array( + 'action' => 'append()', + 'error' => 'Action: append() - Missing argument #1: target attribute' + ), + array( + 'action' => 'add_to_list(toto, titi)', + 'error' => 'Action: add_to_list(toto, titi) - Unknown attribute UserRequest::toto' + ), + array( + 'action' => 'add_to_list(caller_id, titi)', + 'error' => 'Action: add_to_list(caller_id, titi) - Unknown attribute Server::titi' + ), + array( + 'action' => 'add_to_list(caller_id)', + 'error' => 'Action: add_to_list(caller_id) - Missing argument #2: target attribute (link set)' + ), + array( + 'action' => 'add_to_list()', + 'error' => 'Action: add_to_list() - Missing argument #1: source attribute' + ), + array( + 'action' => 'apply_stimulus(toto)', + 'error' => 'Action: apply_stimulus(toto) - Unknown stimulus Server::toto' + ), + array( + 'action' => 'apply_stimulus()', + 'error' => 'Action: apply_stimulus() - Missing argument #1: stimulus' + ), + array( + 'action' => 'call_method(toto)', + 'error' => 'Action: call_method(toto) - Unknown method Server::toto()' + ), + array( + 'action' => 'call_method()', + 'error' => 'Action: call_method() - Missing argument #1: method name' + ), + ); + + foreach ($aScenarii as $aScenario) + { + echo "