Array(), 'nextActivity' => Array(), 'nextUser' => Array()); /** * @var array $properties Instance properties * @access protected */ var $properties = Array(); /** * @var array $cleared Used to detect conflicts on sync with the database * @access protected */ var $cleared = Array(); /** * @var string $owner Instance owner * @access protected */ var $owner = ''; /** * @var string $status Instance status * @access protected */ var $status = ''; /** * @var bool $started * @access protected */ var $started; /** * @var array $nextActivity * @access protected */ var $nextActivity=Array(); /** * @var string $nextUser * @access protected */ var $nextUser; /** * @var bool $ended * @access protected */ var $ended; /** * @var string $name Instance name * @access protected */ var $name=''; /** * @var string $category Instance category * @access protected */ var $category; /** * @var int $prioryty Instance priority * @access protected */ var $priority = 1; /** * @var bool $isChildInstance Indicates wether the current instance is a child instance or not * @access public */ var $isChildInstance = false; /** * @var bool $parentLock Indicates wether the parent instance depends on the child instance or not * @access public */ var $parentLock = false; /** * @var int $parentActivityId The activity ID of the parent instance * @access public */ var $parentActivityId; /** * @var int $parentInstanceId The instance ID of the parent instance * @access public */ var $parentInstanceId; /** * @var array $activities Array of assocs(activityId, status, started, ended, user, name, interactivity, autorouting) * @access protected */ var $activities = Array(); /** * @var int $pIdProcess id * @access protected */ var $pId; /** * @var int $instanceId Instance id * @access protected */ var $instanceId = 0; /** * @var array $workitems An array of workitem ids, date, duration, activity name, user, activity type and interactivity * @access protected */ var $workitems = Array(); /** * @var object $security Performs some tests and locks * @access protected */ var $security; /** * @var bool $__activity_completed Internal reminder * @access protected */ var $__activity_completed=false; /** * @var bool $unsynch indicator, if true we are not synchronised in the memory object with the database * @see sync() * @access protected */ var $unsynch=false; /** * Constructor * * @param object $db ADOdb object * @access public */ var $activityID = null; function Instance() { $this->child_name = 'Instance'; parent::Base(); } /** * Method used to load an instance data from the database. * This function will load/initialize members of the instance object from the database * it will populatae all members and will by default populate the related activities array * and the workitems (history) array. * * @param int $instanceId * @param bool $load_activities true by default, do we need to reload activities from the database? * @param bool $load_workitems true by default, do we need to reload workitems from the database? * @return bool * @access protected */ function getInstance($instanceId, $load_activities=true, $load_workitems=true) { if (!($instanceId)) return true; //start activities for example - pseudo instances // Get the instance data $query = "select * from `".GALAXIA_TABLE_PREFIX."instances` where `wf_instance_id`=?"; $result = $this->query($query,array((int)$instanceId)); if( empty($result) || (!$result->numRows())) return false; $res = $result->fetchRow(); //Populate $this->properties = unserialize(base64_decode($res['wf_properties'])); $this->status = $res['wf_status']; $this->pId = $res['wf_p_id']; $this->instanceId = $res['wf_instance_id']; $this->priority = $res['wf_priority']; $this->owner = $res['wf_owner']; $this->started = $res['wf_started']; $this->ended = $res['wf_ended']; $this->nextActivity = unserialize(base64_decode($res['wf_next_activity'])); $this->nextUser = unserialize(base64_decode($res['wf_next_user'])); $this->name = $res['wf_name']; $this->category = $res['wf_category']; // Get the activities where the instance is (nothing for start activities) if ($load_activities) { $this->_populate_activities($instanceId); } // Get the workitems where the instance is if ($load_workitems) { $query = "select wf_item_id, wf_order_id, gw.wf_instance_id, gw.wf_activity_id, wf_started, wf_ended, gw.wf_user, ga.wf_name, ga.wf_type, ga.wf_is_interactive from ".GALAXIA_TABLE_PREFIX."workitems gw INNER JOIN ".GALAXIA_TABLE_PREFIX."activities ga ON ga.wf_activity_id = gw.wf_activity_id where wf_instance_id=? order by wf_order_id ASC"; $result = $this->query($query,array((int)$instanceId)); if (!(empty($result))) { while($res = $result->fetchRow()) { $this->workitems[]=$res; } } return true; } } /** * Loads all activities related to the insance given in parameter in the activities array * * @access private * @param int $instanceId */ function _populate_activities($instanceId) { $this->activities=Array(); $query = "select gia.wf_activity_id, gia.wf_instance_id, wf_started, wf_ended, wf_started, wf_user, wf_status, ga.wf_is_autorouted, ga.wf_is_interactive, ga.wf_name, ga.wf_type from ".GALAXIA_TABLE_PREFIX."instance_activities gia INNER JOIN ".GALAXIA_TABLE_PREFIX."activities ga ON ga.wf_activity_id = gia.wf_activity_id where wf_instance_id=?"; $result = $this->query($query,array((int)$instanceId)); if (!(empty($result))) { while($res = $result->fetchRow()) { $this->activities[] = $res; } } } /** * Performs synchronization on an instance member * * @param bool $changed * @param array $init * @param array $actual * @param string $name * @param string $fieldname * @param array $namearray * @param array $vararray * @return void * @access private */ function _synchronize_member(&$changed,&$init,&$actual,$name,$fieldname,&$namearray,&$vararray) { //if we work with arrays then it's more complex //echo "
$name is_array?".(is_array($changed)); _debug_array($changed); if (!(is_array($changed))) { if (isset($changed)) { //detect unsynchro if (!($actual==$init)) { $this->error[] = tra('Instance: unable to modify %1, someone has changed it before us', $name); } else { $namearray[] = $fieldname; $vararray[] = $changed; $actual = $changed; } unset ($changed); } } else //we are working with arrays (properties for example) { $modif_done = false; foreach ($changed as $key => $value) { //detect unsynchro if (!($actual[$key]==$init[$key])) { $this->error[] = tra('Instance: unable to modify %1 [%2], someone has changed it before us', $name, $key); } else { $actual[$key] = $value; $modif_done = true; } } if ($modif_done) //at least one modif { $namearray[] = $fieldname; //no more serialize, done by the core security_cleanup $vararray[] = $actual; //serialize($actual); } $changed=Array(); } } /** * Synchronize thes instance object with the database. All change smade will be recorded except * conflicting ones (changes made on members or properties that has been changed by another source * --could be another 'instance' of this instance or an admin form-- since the last call of sync() ) * the unsynch private member is used to test if more heavy tests should be done or not * pseudo instances (start, standalone) are not synchronised since there is no record on database * * @access public * @return bool */ function sync() { if ( (!($this->instanceId)) || (!($this->unsynch)) ) { //echo "
nothing to do ".$this->unsynch; return true; } //echo "
synch!";_debug_array($this->changed); //do it in a transaction, can have several activities running $this->db->StartTrans(); //we need to make a row lock now, $where = 'wf_instance_id='.(int)$this->instanceId; if (!($this->db->RowLock(GALAXIA_TABLE_PREFIX.'instances', $where))) { $this->error[] = 'sync: '.tra('failed to obtain lock on %1 table', 'instances'); $this->db->FailTrans(); } else { //wf_p_id and wf_instance_id are set in creation only. //we remember initial values $init_properties = $this->properties; $init_status = $this->status; $init_priority = $this->priority; $init_owner = $this->owner; $init_started = $this->started; $init_ended = $this->ended; $init_nextUser = $this->nextUser; $init_nextActivity = $this->nextActivity; $init_name = $this->name; $init_category = $this->category; // we re-read instance members to detect conflicts, changes made while we were unsynchronised $this->getInstance($this->instance_id, false, false); // Now for each modified field we'll change the database vale if nobody has changed // the database value before us // (Drovetto) Inclusion of the nextUser field in the synchronization $bindvars = Array(); $querysets = Array(); $queryset = ''; $this->_synchronize_member($this->changed['status'],$init_status,$this->status,tra('status'),'wf_status',$querysets,$bindvars); $this->_synchronize_member($this->changed['priority'],$init_priority,$this->priority,tra('priority'),'wf_priority',$querysets,$bindvars); $this->_synchronize_member($this->changed['owner'],$init_owner,$this->owner,tra('owner'),'wf_owner',$querysets,$bindvars); $this->_synchronize_member($this->changed['started'],$init_started,$this->started,tra('started'),'wf_started',$querysets,$bindvars); $this->_synchronize_member($this->changed['ended'],$init_ended,$this->ended,tra('ended'),'wf_ended',$querysets,$bindvars); $this->_synchronize_member($this->changed['name'],$init_name,$this->name,tra('name'),'wf_name',$querysets,$bindvars); $this->_synchronize_member($this->changed['category'],$init_category,$this->category,tra('category'),'wf_category',$querysets,$bindvars); $this->_synchronize_member($this->changed['properties'],$init_properties,$this->properties,tra('property'),'wf_properties',$querysets,$bindvars); $this->_synchronize_member($this->changed['nextUser'],$init_nextUser,$this->nextUser,tra('next user'),'wf_next_user',$querysets,$bindvars); $this->_synchronize_member($this->changed['nextActivity'],$init_nextActivity,$this->nextActivity,tra('next activity'),'wf_next_activity',$querysets,$bindvars); /* remove unsetted properties */ if (($propertiesIndex = array_search("wf_properties", $querysets)) !== false) foreach ($this->cleared as $clearedName) unset($bindvars[$propertiesIndex][$clearedName]); /* remove unsetted nextUser */ if (($nextUserIndex = array_search("wf_next_user", $querysets)) !== false) foreach ($bindvars[$nextUserIndex] as $nextUserKey => $nextUserValue) if ($bindvars[$nextUserIndex][$nextUserKey] == '__UNDEF__') { unset($bindvars[$nextUserIndex][$nextUserKey]); unset($this->nextUser[$nextUserKey]); } if (!(empty($querysets))) { $queryset = implode(' = ?,', $querysets). ' = ?'; $query = 'update '.GALAXIA_TABLE_PREFIX.'instances set '.$queryset .' where wf_instance_id=?'; $bindvars[] = $this->instanceId; //echo "
query $query"; _debug_array($bindvars); $this->query($query,$bindvars); } } if (!($this->db->CompleteTrans())) { $this->error[] = tra('failed to synchronize instance data with the database'); return false; } //we are not unsynchronized anymore. $this->unsynch = false; return true; } /** * Sets the next activity to be executed, if the current activity is * a switch activity the complete() method will use the activity setted * in this method as the next activity for the instance. * The object records an array of transitions, as the instance can be splitted in several * running activities, transition from the current activity to the given activity will * be recorded and all previous recorded transitions starting from the current activity * will be deleted * * @param int $activityId Running activity Id * @param string $actname Activity name as argument (not an id) * @return bool * @access public */ function setNextActivity($activityId, $actname) { $pId = $this->pId; $actname=trim($actname); $aid = $this->getOne('select wf_activity_id from '.GALAXIA_TABLE_PREFIX.'activities where wf_p_id=? and wf_name=?',array($pId,$actname)); if (!($aid)) { $this->error[] = tra('setting next activity to an unexisting activity'); return false; } $this->changed['nextActivity'][$activityId]=$aid; $this->unsynch = true; return true; } /** * This method can be used to set the user that must perform the next * activity of the process. this effectively "assigns" the instance to * some user * * @param mixed $user The user which will execute the next activity * @param mixed $activityID The ID of the activity that the user will be able to execute. '*' means any next activity * @return bool true in case of success or false otherwise * @access public */ function setNextUser($user, $activityID = '*') { if ($activityID == '*') { $candidates = $this->getActivityCandidates($this->activityID); foreach ($candidates as $candidate) $this->changed['nextUser'][$candidate] = $user; } else $this->changed['nextUser'][$activityID] = $user; $this->unsynch = true; return true; } /** * Sets next instance user role * * @param string $roleName The name of the role * @param mixed $activityID The ID of the activity that the role will be able to execute. '*' means any next activity * @return bool true in case of success or false otherwise * @access public */ function setNextRole($roleID, $activityID = '*') { return $this->setNextUser('p' . $roleID, $activityID); } /** * Removes the user/role of the provided activity ID * * @param mixed $activityID The ID of the activity from which the users/roles will be removed. * @return void * @access public */ function unsetNextUser($activityID = '*') { $this->setNextUser('__UNDEF__', $activityID); } /** * This method can be used to get the user that must perform the next * activity of the process. This can be empty if no setNextUser was done before. * It wont return the default user but inly the user which was assigned by a setNextUser * * @param mixed $activityID The ID of the activity from which we want the user/role that will execute it. * @return string * @access public */ function getNextUser($activityID = '*') { if ($activityID == '*') $order = array('*' . $this->activityID); else $order = array($activityID, '*' . $this->activityID); foreach ($order as $currentOrder) { if (isset($this->changed['nextUser'][$currentOrder]) && ($this->changed['nextUser'][$currentOrder] == '__UNDEF__')) continue; if (isset($this->changed['nextUser'][$currentOrder])) return $this->changed['nextUser'][$currentOrder]; if (isset($this->nextUser[$currentOrder])) return $this->nextUser[$currentOrder]; } return ''; } /** * Creates a new instance. * This method is called in start activities when the activity is completed * to create a new instance representing the started process. * * @param int $activityId start activity id * @param int $user current user id * @return bool * @access private */ function _createNewInstance($activityId,$user) { // Creates a new instance setting up started, ended, user, status and owner $pid = $this->getOne('select wf_p_id from '.GALAXIA_TABLE_PREFIX.'activities where wf_activity_id=?',array((int)$activityId)); $this->pId = $pid; $this->setStatus('active'); //$this->setNextUser(''); $now = date("U"); $this->setStarted($now); $this->setOwner($user); $query = 'insert into '.GALAXIA_TABLE_PREFIX.'instances (wf_started,wf_ended,wf_status,wf_p_id,wf_owner,wf_properties) values(?,?,?,?,?,?)'; $this->query($query,array($now,0,'active',$pid,$user,$this->security_cleanup(Array(),false))); $this->instanceId = $this->getOne('select max(wf_instance_id) from '.GALAXIA_TABLE_PREFIX.'instances where wf_started=? and wf_owner=?',array((int)$now,$user)); $iid=$this->instanceId; // Then add in ".GALAXIA_TABLE_PREFIX."instance_activities an entry for the // activity the user and status running and started now $query = 'insert into '.GALAXIA_TABLE_PREFIX.'instance_activities (wf_instance_id,wf_activity_id,wf_user, wf_started,wf_status) values(?,?,?,?,?)'; $this->query($query,array((int)$iid,(int)$activityId,$user,(int)$now,'running')); if (($this->isChildInstance) && (!is_null($this->parentInstanceId))) { $query = 'INSERT INTO '.GALAXIA_TABLE_PREFIX.'interinstance_relations(wf_parent_instance_id, wf_parent_activity_id, wf_child_instance_id, wf_parent_lock) VALUES(?,?,?,?)'; $this->query($query,array((int) $this->parentInstanceId, (int) $this->parentActivityId, (int) $iid, (int) (($this->parentLock) ? 1 : 0))); } //update database with other datas stored in the object return $this->sync(); } /** * Sets the name of this instance * * @param string $value * @return bool * @access public */ function setName($value) { $this->changed['name'] = substr($value,0,120); $this->unsynch = true; return true; } /** * Get the name of this instance * * @return string * @access public */ function getName() { if (!(isset($this->changed['name']))) { return $this->name; } else { return $this->changed['name']; } } /** * Sets the category of this instance * * @param string $value * @return bool * @access public */ function setCategory($value) { $this->changed['category'] = $value; $this->unsynch = true; return true; } /** * Get the category of this instance * * @return string * @access public */ function getCategory() { if (!(isset($this->changed['category']))) { return $this->category; } else { return $this->changed['category']; } } /** * Normalizes a property name * * @param string $name name you want to normalize * @return string property name * @access private */ function _normalize_name($name) { $name = trim($name); $name = str_replace(" ","_",$name); $name = preg_replace("/[^0-9A-Za-z\_]/",'',$name); return $name; } /** * Sets a property in this instance. This method is used in activities to * set instance properties. * all property names are normalized for security reasons and to avoid localisation * problems (A->z, digits and _ for spaces). If you have several set to call look * at the setProperties function. Each call to this function has an impact on database * * @param string $name property name (it will be normalized) * @param mixed $value value you want for this property * @return bool * @access public */ function set($name,$value) { $name = $this->_normalize_name($name); unset($this->cleared[$name]); $this->changed['properties'][$name] = $this->security_cleanup($value); if (is_array($value)) $this->changed['properties'][$name] = "__ARRAY__" . $this->changed['properties'][$name]; $this->unsynch = true; return true; } /** * Unsets a property in this instance. This method is used in activities to * unset instance properties. * All property names are normalized for security reasons and to avoid localisation * problems (A->z, digits and _ for spaces). Each call to this function has an impact on database * * @param string $name the property name (it will be normalized) * @return bool * @access public */ function clear($name) { $this->set($name, ''); $this->cleared[$name] = $name; $this->unsynch = true; return true; } /** * Checks if a property in this instance exists. This method is used in activities to * check the existance of instance properties. * All property names are normalized for security reasons and to avoid localisation * problems (A->z, digits and _ for spaces). * * @param string $name property name (it will be normalized) * @return bool * @access public */ function exists($name) { $name = $this->_normalize_name($name); return ((isset($this->changed['properties'][$name]) || isset($this->properties[$name])) && !isset($this->cleared[$name])); } /** * Sets several properties in this instance. This method is used in activities to * set instance properties. Use this method if you have several properties to set * as it will avoid to re-call the SQL engine for each property. * all property names are normalized for security reasons and to avoid localisation * problems (A->z, digits and _ for spaces). * * @param array $properties_array associative array containing for each record the * property name as the key and the property value as the value * @return bool * @access public */ function setProperties($properties_array) { $backup_values = $this->properties; foreach ($properties_array as $key => $value) { $name = $this->_normalize_name($key); $this->changed['properties'][$name] = $this->security_cleanup($value); } $this->unsynch = true; return true; } /** * Gets the value of an instance property * * @param string $name name of the property * @param mixed $defaultValue * @return bool * @access public */ function get($name, $defaultValue = "__UNDEF__") { $name = $this->_normalize_name($name); $output = ""; /* select the value of the current property */ if (isset($this->changed['properties'][$name])) { $output = $this->changed['properties'][$name]; } else { if (isset($this->properties[$name])) { $output = $this->properties[$name]; } } /* if the property doesn't exist return the default value or throw an error */ if (isset($this->cleared[$name]) || ((!isset($this->properties[$name])) && (!isset($this->changed['properties'][$name])))) { if ($defaultValue != "__UNDEF__") { $output = $defaultValue; } else { $this->error[] = tra('property %1 not found', $name); $output = false; } } /* if the requested value is an enconded/serialized array, change it back to its original type */ if (@strcmp(@substr($output, 0, 9), "__ARRAY__") == 0) if ( ($tmp = base64_decode(substr($output, 9))) !== false ) if ( ($tmp = unserialize($tmp)) !== false ) $output = $tmp; return $output; } /** * Returns an array of assocs describing the activities where the instance * is present, can be more than one activity if the instance was "splitted" * * @return array * @access public */ function getActivities() { return $this->activities; } /** * Gets the instance status can be * 'completed', 'active', 'aborted' or 'exception' * * @return string * @access public */ function getStatus() { if (!(isset($this->changed['status']))) { return $this->status; } else { return $this->changed['status']; } } /** * Sets the instance status * * @param string_type $status it can be: 'completed', 'active', 'aborted' or 'exception' * @return bool * @access public */ function setStatus($status) { if (!(($status=='completed') || ($status=='active') || ($status=='aborted') || ($status=='exception'))) { $this->error[] = tra('unknown status'); return false; } $this->changed['status'] = $status; $this->unsynch = true; return true; } /** * Gets the instance priority * * @return int * @access public */ function getPriority() { if (!(isset($this->changed['priority']))) { return $this->priority; } else { return $this->changed['priority']; } } /** * Sets the instance priority * * @param int $priority * @return bool * @access public */ function setPriority($priority) { $mypriority = (int)$priority; $this->changed['priority'] = $mypriority; $this->unsynch = true; return true; } /** * Returns the instanceId * * @return int * @access public */ function getInstanceId() { return $this->instanceId; } /** * Returns the processId for this instance * * @return int * @access public */ function getProcessId() { return $this->pId; } /** * Returns the user that created the instance * * @return int * @access public */ function getOwner() { if (!(isset($this->changed['owner']))) { return $this->owner; } else { return $this->changed['owner']; } } /** * Sets the instance creator user * * @param int $user new owner id, musn't be false, 0 or empty * @return bool * @access public */ function setOwner($user) { if (empty($user)) { return false; } $this->changed['owner'] = $user; $this->unsynch = true; return true; } /** * Sets the user that must execute the activity indicated by the activityId. * Note that the instance MUST be present in the activity to set the user, * you can't program who will execute an activity. * If the user is empty then the activity user is setted to *, allowing any * authorised user to take the token later concurrent access to this function * is normally handled by WfRuntime and WfSecurity theses objects are the only ones * which should call this function. WfRuntime is handling the current transaction and * WfSecurity is Locking the instance and instance_activities table on a 'run' action * which is the action leading to this setActivityUser call (could be a release * as well on auto-release) * * @param int $activityId * @param int $theuser user id or '*' (or 0, '' or null which will be set to '*') * @return bool * @access public */ function setActivityUser($activityId,$theuser) { if(empty($theuser)) $theuser='*'; $found = false; for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { // here we are in the good activity $found = true; // prepare queries $where = ' where wf_activity_id=? and wf_instance_id=?'; $bindvars = array((int)$activityId,(int)$this->instanceId); if(!($theuser=='*')) { $where .= ' and (wf_user=? or wf_user=? or wf_user LIKE ?)'; $bindvars[]= $theuser; $bindvars[]= '*'; $bindvars[]= 'p%'; } // update the user $query = 'update '.GALAXIA_TABLE_PREFIX.'instance_activities set wf_user=?'; $query .= $where; $bindvars_update = array_merge(array($theuser),$bindvars); $this->query($query,$bindvars_update); $this->activities[$i]['wf_user']=$theuser; return true; } } // if we didn't find the activity it will be false return $found; } /** * Returns the user that must execute or is already executing an activity * wherethis instance is present * * @param int $activityId * @return bool * @access public */ function getActivityUser($activityId) { for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { return $this->activities[$i]['wf_user']; } } return false; } /** * Sets the status of the instance in some activity * * @param int $activityId * @param string $status new status, it can be 'running' or 'completed' * @return bool * @access public */ function setActivityStatus($activityId,$status) { if (!(($status=='running') || ($status=='completed'))) { $this->error[] = tra('unknown status'); return false; } for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { $query = 'update '.GALAXIA_TABLE_PREFIX.'instance_activities set wf_status=? where wf_activity_id=? and wf_instance_id=?'; $this->query($query,array($status,(int)$activityId,(int)$this->instanceId)); return true; } } $this->error[] = tra('new status not set, no corresponding activity was found.'); return false; } /** * Gets the status of the instance in some activity, can be * 'running' or 'completed' * * @param int $activityId * @return mixed * @access public */ function getActivityStatus($activityId) { for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { return $this->activities[$i]['wf_status']; } } $this->error[] = tra('activity status not avaible, no corresponding activity was found.'); return false; } /** * Resets the start time of the activity indicated to the current time * * @param int $activityId * @return bool * @access public */ function setActivityStarted($activityId) { $now = date("U"); for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { $this->activities[$i]['wf_started']=$now; $query = "update `".GALAXIA_TABLE_PREFIX."instance_activities` set `wf_started`=? where `wf_activity_id`=? and `wf_instance_id`=?"; $this->query($query,array($now,(int)$activityId,(int)$this->instanceId)); return true; } } $this->error[] = tra('activity start not set, no corresponding activity was found.'); return false; } /** * Gets the Unix timstamp of the starting time for the given activity * * @param int $activityId * @return mixed * @access public */ function getActivityStarted($activityId) { for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { return $this->activities[$i]['wf_started']; } } $this->error[] = tra('activity start not avaible, no corresponding activity was found.'); return false; } /** * Gets an activity from the list of activities of the instance * the result is an array describing the instance * * @param int $activityId * @return mixed * @access private */ function _get_instance_activity($activityId) { for($i=0;$iactivities);$i++) { if($this->activities[$i]['wf_activity_id']==$activityId) { return $this->activities[$i]; } } $this->error[] = tra('no corresponding activity was found.'); return false; } /** * Sets the time where the instance was started * * @param int $time * @return bool * @access public */ function setStarted($time) { $this->changed['started'] = $time; $this->unsynch = true; return true; } /** * Gets the time where the instance was started (Unix timestamp) * * @return int * @access public */ function getStarted() { if (!(isset($this->changed['started']))) { return $this->started; } else { return $this->changed['started']; } } /** * Sets the end time of the instance (when the process was completed) * * @param int $time * @return bool * @access public */ function setEnded($time) { $this->changed['ended']=$time; $this->unsynch = true; return true; } /** * Gets the end time of the instance (when the process was completed) * * @return int * @access public */ function getEnded() { if (!(isset($this->changed['ended']))) { return $this->ended; } else { return $this->changed['ended']; } } /** * This set to true or false the 'Activity Completed' status which will * be important to know if the user code has completed the current activity * * @param bool $bool true by default, it will be the next status of the 'Activity Completed' indicator * @access private * @return void */ function setActivityCompleted($bool) { $this->__activity_completed = $bool; } /** * Gets the 'Activity Completed' status * * @return mixed * @access public */ function getActivityCompleted() { return $this->__activity_completed; } /** * This function can be called by the instance object himself (for automatic activities) * or by the WfRuntime object. In interactive activities code users use complete() --without args-- * which refer to the WfRuntime->complete() function which call this one. * In non-interactive activities a call to a complete() will generate errors because the engine * does it his own way as I said first. * Particularity of this Complete is that it is Transactional, i.e. it it done completely * or not and row locks are ensured * * @param int $activityId activity that is being completed * @param bool $addworkitem indicates if a workitem should be added for the completed * activity (true by default) * @return bool false it means the complete was not done for some internal reason * consult $instance->get_error() for more informations * @access public */ function complete($activityId,$addworkitem=true) { //$this->db-> $result = $this->query("SELECT 1 FROM " . GALAXIA_TABLE_PREFIX . "instances i, " . GALAXIA_TABLE_PREFIX . "interinstance_relations ir WHERE (ir.wf_child_instance_id = i.wf_instance_id) AND (i.wf_status IN ('active', 'exception')) AND (ir.wf_parent_lock = 1) AND (ir.wf_parent_instance_id = ?) AND (ir.wf_parent_activity_id = ?)", array($this->instanceId, $activityId)); if ($result->numRows() > 0) die("Esta instância está aguardando que outras instâncias, das quais depende, sejam finalizadas."); //ensure it's false at first $this->setActivityCompleted(false); //The complete() is in a transaction, it will be completly done or not at all $this->db->StartTrans(); //lock rows and ensure access is granted if (!(isset($this->security))) $this->security = &Factory::getInstance('WfSecurity'); if (!($this->security->checkUserAction($activityId,$this->instanceId,'complete'))) { $this->error[] = tra('you were not allowed to complete the activity'); $this->db->FailTrans(); } else { if (!($this->_internalComplete($activityId,$addworkitem))) { $this->error[] = tra('The activity was not completed'); $this->db->FailTrans(); } } //we mark completion with result of the transaction wich will be false if any error occurs //this is the end of the transaction $this->setActivityCompleted($this->db->CompleteTrans()); //we return the completion state. return $this->getActivityCompleted(); } /** * YOU MUST NOT CALL _internalComplete() directly, use Complete() instead * * @param int $activityId activity that is being completed * @param bool $addworkitem indicates if a workitem should be added for the completed * activity (true by default) * @return bool false it means the complete was not done for some internal reason * consult $instance->get_error() for more informations * @access private */ function _internalComplete($activityId,$addworkitem=true) { global $user; if(empty($user)) { $theuser='*'; } else { $theuser=$user; } if(!($activityId)) { $this->error[] = tra('it was impossible to complete, no activity was given.'); return false; } $now = date("U"); // If we are completing a start activity then the instance must // be created first! $type = $this->getOne('select wf_type from '.GALAXIA_TABLE_PREFIX.'activities where wf_activity_id=?',array((int)$activityId)); if($type=='start') { if (!($this->_createNewInstance((int)$activityId,$theuser))) { return false; } } else { // Now set ended $query = 'update '.GALAXIA_TABLE_PREFIX.'instance_activities set wf_ended=? where wf_activity_id=? and wf_instance_id=?'; $this->query($query,array((int)$now,(int)$activityId,(int)$this->instanceId)); } //Set the status for the instance-activity to completed //except for start activities if (!($type=='start')) { if (!($this->setActivityStatus($activityId,'completed'))) { return false; } } //If this and end actt then terminate the instance if($type=='end') { if (!($this->terminate($now))) { return false; } } //now we synchronise instance with the database if (!($this->sync())) return false; //Add a workitem to the instance if ($addworkitem) { return $this->addworkitem($type,$now, $activityId); } else { return true; } } /** * This function will add a workitem in the workitems table, the instance MUST be synchronised before * calling this function. * * @param string $activity_type activity type, needed because internals are different for start activities * @param int $ended ending time * @param int $activityId finishing activity id * @return mixed * @access private */ function addworkitem($activity_type, $ended, $activityId) { $iid = $this->instanceId; $max = $this->getOne('select max(wf_order_id) from '.GALAXIA_TABLE_PREFIX.'workitems where wf_instance_id=?',array((int)$iid)); if(!$max) { $max=1; } else { $max++; } if($activity_type=='start') { //Then this is a start activity ending $started = $this->getStarted(); //at this time owner is the creator $putuser = $this->getOwner(); } else { $act = $this->_get_instance_activity($activityId); if(!$act) { //this will abort the function $this->error[] = tra('failed to create workitem'); return false; } else { $started = $act['wf_started']; $putuser = $act['wf_user']; } } //no more serialize, done by the core security_cleanup $properties = $this->security_cleanup($this->properties, false); //serialize($this->properties); $query='insert into '.GALAXIA_TABLE_PREFIX.'workitems (wf_instance_id,wf_order_id,wf_activity_id,wf_started,wf_ended,wf_properties,wf_user) values(?,?,?,?,?,?,?)'; $this->query($query,array((int)$iid,(int)$max,(int)$activityId,(int)$started,(int)$ended,$properties,$putuser)); return true; } /** * Send autorouted activities to the next one(s) * YOU MUST NOT CALL sendAutorouted() for non-interactive activities since * the engine does automatically complete and send automatic activities after * executing them * This function is in fact a Private function runned by the engine * You should never use it without knowing very well what you're doing * * @param int $activityId activity that is being completed, when this is not * passed the engine takes it from the $_REQUEST array,all activities * are executed passing the activityId in the URI * @param bool $force indicates that the instance must be routed no matter if the * activity is auto-routing or not. This is used when "sending" an * instance from a non-auto-routed activity to the next activity * @return mixed * @access private */ function sendAutorouted($activityId,$force=false) { $returned_value = Array(); $type = $this->getOne("select `wf_type` from `".GALAXIA_TABLE_PREFIX."activities` where `wf_activity_id`=?",array((int)$activityId)); //on a end activity we have nothing to do if ($type == 'end') { return true; } //If the activity ending is not autorouted then we have nothing to do if (!(($force) || ($this->getOne("select `wf_is_autorouted` from `".GALAXIA_TABLE_PREFIX."activities` where `wf_activity_id`=?",array($activityId)) == 'y'))) { $returned_value['transition']['status'] = 'not autorouted'; return $returned_value; } //If the activity ending is autorouted then send to the activity // Now determine where to send the instance $candidates = $this->getActivityCandidates($activityId); if($type == 'split') { $erase_from = false; $num_candidates = count($candidates); $returned_data = Array(); $i = 1; foreach ($candidates as $cand) { // only erase split activity in instance when all the activities comming from the split have been set up if ($i == $num_candidates) { $erase_from = true; } $returned_data[$i] = $this->sendTo($activityId,$cand,$erase_from); $this->unsetNextUser($cand); $this->sync(); $i++; } $this->unsetNextUser('*' . $activityId); $this->sync(); return $returned_data; } elseif($type == 'switch') { if (in_array($this->nextActivity[$activityId],$candidates)) { $selectedActivity = $this->nextActivity[$activityId]; foreach ($candidates as $candidate) if ($candidate != $selectedActivity) $this->unsetNextUser($candidate); $output = $this->sendTo((int)$activityId,(int)$selectedActivity); $this->unsetNextUser($selectedActivity); $this->unsetNextUser('*' . $activityId); $this->sync(); return $output; } else { $returned_value['transition']['failure'] = tra('Error: nextActivity does not match any candidate in autorouting switch activity'); return $returned_value; } } else { if (count($candidates)>1) { $returned_value['transition']['failure'] = tra('Error: non-deterministic decision for autorouting activity'); return $returned_value; } else { $output = $this->sendTo((int)$activityId,(int)$candidates[0]); $this->unsetNextUser($candidates[0]); $this->unsetNextUser('*' . $activityId); $this->sync(); return $output; } } } /** * This is a semi-private function, use GUI's abort function * Aborts an activity and terminates the whole instance. We still create a workitem to keep track * of where in the process the instance was aborted * * @param bool $addworkitem * @return bool * @access public * @todo review, reuse of completed code */ function abort($addworkitem=true) { // If we are aborting a start activity then the instance must // be created first! // ==> No, there's no reason to have an uncompleted start activity to abort // load all the activities of the current instance if ($addworkitem) { $activities = array(); $query = 'SELECT a.wf_type AS wf_type, ia.wf_activity_id AS wf_activity_id FROM '.GALAXIA_TABLE_PREFIX.'activities a, '.GALAXIA_TABLE_PREFIX.'instance_activities ia WHERE (ia.wf_activity_id = a.wf_activity_id) AND (ia.wf_instance_id = ?)'; $result = $this->query($query,array((int)$this->instanceId)); while ($row = $result->fetchRow()) $activities[] = $row; } // Now set ended on instance_activities $now = date("U"); // terminate the instance with status 'aborted' if (!($this->terminate($now,'aborted'))) return false; //now we synchronise instance with the database if (!($this->sync())) return false; //Add a workitem to the instance if ($addworkitem) { foreach ($activities as $activity) if (!($this->addworkitem($activity['wf_type'], $now, $activity['wf_activity_id']))) return false; return true; } else { return true; } } /** * Terminates the instance marking the instance and the process * as completed. This is the end of a process * Normally you should not call this method since it is automatically * called when an end activity is completed * object is synched at the end of this function * * @param int $time terminating time * @param string $status * @return bool * @access private */ function terminate($time, $status = 'completed') { //Set the status of the instance to completed if (!($this->setEnded((int)$time))) return false; if (!($this->setStatus($status))) return false; $query = "delete from `".GALAXIA_TABLE_PREFIX."instance_activities` where `wf_instance_id`=?"; $this->query($query,array((int)$this->instanceId)); return $this->sync(); } /** * Sends the instance from some activity to another activity. (walk on a transition) * You should not call this method unless you know very very well what * you are doing * * @param int $from activity id at the start of the transition * @param int $activityId activity id at the end of the transition * @param bool $erase_from true by default, if true the coming activity row will be erased from * instance_activities table. You should set it to false for example with split activities while * you still want to re-call this function * @return mixed false if anything goes wrong, true if we are at the end of the execution tree and an array * if a part of the process was automatically runned at the end of the transition. this array contains * 2 keys 'transition' is the transition we walked on, 'activity' is the result of the run part if it was an automatic activity. * 'activity' value is an associated array containing several usefull keys: * 'completed' is a boolean indicating that the activity was completed or not * 'debug contains debug messages * 'info' contains some usefull infos about the activity-instance running (like names) * 'next' is the result of a SendAutorouted part which could in fact be the result of a call to this function, etc * @access public */ function sendTo($from,$activityId,$erase_from=true) { //we will use an array for return value $returned_data = Array(); //1: if we are in a join check //if this instance is also in //other activity if so do //nothing $query = 'select wf_type, wf_name from '.GALAXIA_TABLE_PREFIX.'activities where wf_activity_id=?'; $result = $this->query($query,array($activityId)); if (empty($result)) { $returned_data['transition']['failure'] = tra('Error: trying to send an instance to an activity but it was impossible to get this activity'); return $returned_data; } while ($res = $result->fetchRow()) { $type = $res['wf_type']; $targetname = $res['wf_name']; } $returned_data['transition']['target_id'] = $activityId; $returned_data['transition']['target_name'] = $targetname; // Verify the existence of a transition if(!$this->getOne("select count(*) from `".GALAXIA_TABLE_PREFIX."transitions` where `wf_act_from_id`=? and `wf_act_to_id`=?",array($from,(int)$activityId))) { $returned_data['transition']['failure'] = tra('Error: trying to send an instance to an activity but no transition found'); return $returned_data; } //init $putuser=0; //try to determine the user or * //Use the nextUser $the_next_user = $this->getNextUser($activityId); if($the_next_user) { //we check rights for this user on the next activity if (!(isset($this->security))) $this->security = &Factory::getInstance('WfSecurity'); if ($this->security->checkUserAccess($the_next_user,$activityId)) { $putuser = $the_next_user; } } if ($putuser===0) { // then check to see if there is a default user $activity_manager = &Factory::newInstance('ActivityManager'); //get_default_user will give us '*' if there is no default_user or if the default user has no role //mapped anymore $default_user = $activity_manager->get_default_user($activityId,true); unset($activity_manager); // if they were no nextUser, no unique user avaible, no default_user then we'll have '*' // which will let user having the good role mapping grab this activity later $putuser = $default_user; } if ($the_next_user) $this->setNextUser($the_next_user, '*' . $activityId); //update the instance_activities table //if not splitting delete first //please update started,status,user if (($erase_from) && (!empty($this->instanceId))) { $query = "delete from `".GALAXIA_TABLE_PREFIX."instance_activities` where `wf_instance_id`=? and `wf_activity_id`=?"; $this->query($query,array((int)$this->instanceId,$from)); } if ($type == 'join') { if (count($this->activities)>1) { // This instance will have to wait! $returned_data['transition']['status'] = 'waiting'; return $returned_data; } } //create the new instance-activity $returned_data['transition']['target_id'] = $activityId; $returned_data['transition']['target_name'] = $targetname; $now = date("U"); $iid = $this->instanceId; $query="delete from `".GALAXIA_TABLE_PREFIX."instance_activities` where `wf_instance_id`=? and `wf_activity_id`=?"; $this->query($query,array((int)$iid,(int)$activityId)); $query="insert into `".GALAXIA_TABLE_PREFIX."instance_activities`(`wf_instance_id`,`wf_activity_id`,`wf_user`,`wf_status`,`wf_started`) values(?,?,?,?,?)"; $this->query($query,array((int)$iid,(int)$activityId,$putuser,'running',(int)$now)); //record the transition walk $returned_data['transition']['status'] = 'done'; //we are now in a new activity $this->_populate_activities($iid); //if the activity is not interactive then //execute the code for the activity and //complete the activity $isInteractive = $this->getOne("select `wf_is_interactive` from `".GALAXIA_TABLE_PREFIX."activities` where `wf_activity_id`=?",array((int)$activityId)); if ($isInteractive=='n') { //first we sync actual instance because the next activity could need it if (!($this->sync())) { $returned_data['activity']['failure'] = true; return $returned_data; } // Now execute the code for the activity $this->activityID = $activityId; $returned_data['activity'] = $this->executeAutomaticActivity($activityId, $iid); $this->activityID = $from; } else { // we sync actual instance if (!($this->sync())) { $returned_data['failure'] = true; return $returned_data; } } return $returned_data; } /** * This is a public method only because the GUI can ask this action for the admin * on restart failed automated activities, but in fact it's quite an internal function, * This function handle the execution of automatic activities (and the launch of transitions * which can be related to this activity) * * @param int $activityId activity id at the end of the transition * @param int $iid instance id * @return array * @access public */ function executeAutomaticActivity($activityId, $iid) { $returned_data = Array(); // Now execute the code for the activity (function defined in galaxia's config.php) $returned_data =& galaxia_execute_activity($activityId, $iid , 1); //we should have some info in $returned_data now. if it is false there's a problem if ((!(is_array($returned_data))) && (!($returned_data)) ) { $this->error[] = tra('failed to execute automatic activity'); //record the failure $returned_data['failure'] = true; return $returned_data; } else { //ok, we have an array, but it can still be a bad result //this one is just for debug info if (isset($returned_data['debug'])) { //we retrieve this info here, in this object $this->error[] = $returned_data['debug']; } //and this really test if it worked, if not we have a nice failure message (better than just failure=true) if (isset($returned_data['failure'])) { $this->error[] = tra('failed to execute automatic activity'); $this->error[] = $returned_data['failure']; //record the failure return $returned_data; } } // Reload in case the activity did some change, last sync was done just before calling this function //TODO: check if this sync is really needed $this->getInstance($this->instanceId, false, false); //complete the automatic activity---------------------------- if ($this->Complete($activityId)) { $returned_data['completed'] = true; //and send the next autorouted activity if any $returned_data['next'] = $this->sendAutorouted($activityId); } else { $returned_data['failure'] = $this->get_error(); } return $returned_data; } /** * Gets a comment for this instance * * @param int $cId * @return mixed * @access public */ function get_instance_comment($cId) { $iid = $this->instanceId; $query = "select * from `".GALAXIA_TABLE_PREFIX."instance_comments` where `wf_instance_id`=? and `wf_c_id`=?"; $result = $this->query($query,array((int)$iid,(int)$cId)); $res = $result->fetchRow(); return $res; } /** * Inserts or updates an instance comment * * @param int $cId * @param int $activityId * @param object $activity * @param int $user * @param string $title * @param mixed $comment * @return bool * @access public */ function replace_instance_comment($cId, $activityId, $activity, $user, $title, $comment) { if (!$user) { $user = 'Anonymous'; } $iid = $this->instanceId; //no need on pseudo-instance if (!!($this->instanceId)) { if ($cId) { $query = "update `".GALAXIA_TABLE_PREFIX."instance_comments` set `wf_title`=?,`wf_comment`=? where `wf_instance_id`=? and `wf_c_id`=?"; $this->query($query,array($title,$comment,(int)$iid,(int)$cId)); } else { $hash = md5($title.$comment); if ($this->getOne("select count(*) from `".GALAXIA_TABLE_PREFIX."instance_comments` where `wf_instance_id`=? and `wf_hash`=?",array($iid,$hash))) { return false; } $now = date("U"); $query ="insert into `".GALAXIA_TABLE_PREFIX."instance_comments`(`wf_instance_id`,`wf_user`,`wf_activity_id`,`wf_activity`,`wf_title`,`wf_comment`,`wf_timestamp`,`wf_hash`) values(?,?,?,?,?,?,?,?)"; $this->query($query,array((int)$iid,$user,(int)$activityId,$activity,$title,$comment,(int)$now,$hash)); } } return true; } /** * Removes an instance comment * * @param int $cId * @access public * @return void */ function remove_instance_comment($cId) { $iid = $this->instanceId; $query = "delete from `".GALAXIA_TABLE_PREFIX."instance_comments` where `wf_c_id`=? and `wf_instance_id`=?"; $this->query($query,array((int)$cId,(int)$iid)); } /** * Lists instance comments * * @return array * @access public */ function get_instance_comments() { $iid = $this->instanceId; $query = "select * from `".GALAXIA_TABLE_PREFIX."instance_comments` where `wf_instance_id`=? order by ".$this->convert_sortmode("timestamp_desc"); $result = $this->query($query,array((int)$iid)); $ret = Array(); while($res = $result->fetchRow()) { $ret[] = $res; } return $ret; } /** * Get the activity candidates (more than one in split and switch) * * @param int $activityID The activity from which we want to obtain the candidate activities * @return array List of activities that can be reached from one given activity * @access public */ function getActivityCandidates($activityID) { $query = 'SELECT wf_act_to_id FROM ' . GALAXIA_TABLE_PREFIX . 'transitions WHERE (wf_act_from_id = ?)'; $result = $this->query($query, array((int) $activityID)); $candidates = Array(); while ($res = $result->fetchRow()) $candidates[] = $res['wf_act_to_id']; return $candidates; } } ?>