sid])) { self::$states[$this->sid] = $this; } } } /* // Implementing clone needs a list of tid-less transitions, and a conversion // of sids for both States and ConfigTransitions. // public function __clone() {} */ /** * Alternative constructor, loading objects from table {workflow_states}. * * @param int $sid * The requested State ID * @param int $wid * An optional Workflow ID, to check if the requested State is valid for the Workflow. * * @return WorkflowState|NULL|FALSE $state * WorkflowState if state is successfully loaded, * NULL if not loaded, * FALSE if state does not belong to requested Workflow. */ public static function load($sid, $wid = 0) { $states = self::getStates(); $state = isset($states[$sid]) ? $states[$sid] : NULL; if ($wid && $state && ($wid != $state->wid)) { return FALSE; } return $state; } /** * Get all states in the system, with options to filter, only where a workflow exists. * * @param $wid * The requested Workflow ID. * @param bool $reset * An option to refresh all caches. * * @return array $states * An array of cached states. * * D7.x-2.x: deprecated workflow_get_workflow_states --> workflow_state_load_multiple * D7.x-2.x: deprecated workflow_get_workflow_states_all --> workflow_state_load_multiple * D7.x-2.x: deprecated workflow_get_other_states_by_sid --> workflow_state_load_multiple */ public static function getStates($wid = 0, $reset = FALSE) { if ($reset) { self::$states = array(); } if (empty(self::$states)) { // Build the query, and get ALL states. // Note: self::states[] is populated in respective constructors. $query = db_select('workflow_states', 'ws'); $query->fields('ws'); $query->orderBy('ws.weight'); $query->orderBy('ws.wid'); // Just for grins, add a tag that might result in modifications. $query->addTag('workflow_states'); // @see #2285983 for using SQLite. // $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowState'); /* @var $tmp DatabaseStatementBase */ $statement = $query->execute(); $statement->setFetchMode(PDO::FETCH_CLASS,'WorkflowState'); foreach ($statement->fetchAll() as $state) { self::$states[$state->sid] = $state; } } if (!$wid) { // All states are requested and cached: return them. return self::$states; } else { // All states of only 1 Workflow is requested: return this one. $result = array(); foreach (self::$states as $state) { if ($state->wid == $wid) { $result[$state->sid] = $state; } } return $result; } } /** * Get all states in the system, with options to filter, only where a workflow exists. * * May return more then one State, since a name is not (yet) an UUID. * * @param $name * @param int $wid * * @return WorkflowState */ public static function loadByName($name, $wid = 0) { /* @var $state WorkflowState */ foreach ($states = self::getStates($wid) as $state) { if ($name == $state->name) { return $state; } } return NULL; } /** * Deactivate a Workflow State, moving existing nodes to a given State. * * @param int $new_sid * The state ID, to which all affected entities must be moved. * * D7.x-2.x: deprecated workflow_delete_workflow_states_by_sid() --> WorkflowState->deactivate() + delete() */ public function deactivate($new_sid) { $current_sid = $this->sid; $force = TRUE; // Notify interested modules. We notify first to allow access to data before we zap it. // E.g., Node API implements this. // - re-parents any nodes that we don't want to orphan, whilst deactivating a State. // - delete any lingering node to state values. module_invoke_all('workflow', 'state delete', $current_sid, $new_sid, NULL, $force); // Re-parent any nodes that we don't want to orphan, whilst deactivating a State. if ($new_sid) { // A candidate for the batch API. // @TODO: Future updates should seriously consider setting this with batch. global $user; // We can use global, since deactivate() is a UI-only function. $comment = t('Previous state deleted'); // Re-assign workflow_node nodes. foreach (workflow_get_workflow_node_by_sid($current_sid) as $workflow_node) { // @todo: add Field support in 'state delete', by using workflow_node_history or reading current field. $entity_type = 'node'; $entity = entity_load_single('node', $workflow_node->nid); $field_name = ''; $transition = new WorkflowTransition(); $transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment); $transition->force($force); // Execute Transition, invoke 'pre' and 'post' events, save new state in workflow_node, save also in workflow_node_history. // For Workflow Node, only {workflow_node} and {workflow_node_history} are updated. For Field, also the Entity itself. $new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force); } // Re-assign workflow_field_entities. foreach(_workflow_info_fields() as $field_name => $field_info) { $query = new EntityFieldQuery(); $query->fieldCondition($field_name, 'value', $current_sid, '='); $result = $query->execute(); foreach ($result as $entity_type => $entities) { if ($entity_type == 'comment') { // Do not reset comments. continue; } foreach ($entities as $entity_id => $entity) { $entity = entity_load_single($entity_type, $entity_id); /* @var $transition WorkflowTransition */ $transition = new WorkflowTransition(); $transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment, TRUE); $transition->force($force); // Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history. // For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself. $new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force); } } } } // Delete any lingering node to state values. workflow_delete_workflow_node_by_sid($current_sid); // Delete the config transitions this state is involved in. $workflow = workflow_load_single($this->wid); /* @var $transition WorkflowTransition */ foreach ($workflow->getTransitionsBySid($current_sid, 'ALL') as $transition) { $transition->delete(); } foreach ($workflow->getTransitionsByTargetSid($current_sid, 'ALL') as $transition) { $transition->delete(); } // Delete the state. -- We don't actually delete, just deactivate. // This is a matter up for some debate, to delete or not to delete, since this // causes name conflicts for states. In the meantime, we just stick with what we know. // If you really want to delete the states, use workflow_cleanup module, or delete(). $this->status = FALSE; $this->save(); // Clear the cache. self::getStates(0, TRUE); } /** * Property functions. */ /** * Returns the Workflow object of this State. * * @return Workflow * Workflow object. */ public function getWorkflow() { if (isset($this->workflow)) { return $this->workflow; } return workflow_load_single($this->wid); } public function setWorkflow($workflow) { $this->wid = $workflow->wid; $this->workflow = $workflow; } /** * Returns the Workflow object of this State. * * @return bool * TRUE if state is active, else FALSE. */ public function isActive() { return (bool) $this->status; } public function isCreationState() { return $this->sysid == WORKFLOW_CREATION; } /** * Determines if the Workflow Form must be shown. * * If not, a formatter must be shown, since there are no valid options. * * @param $entity_type * @param $entity * @param $field_name * @param $user * @param $force * * @return bool $show_widget * TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead. */ public function showWidget($entity_type, $entity, $field_name, $user, $force) { $options = $this->getOptions($entity_type, $entity, $field_name, $user, $force); $count = count($options); // The easiest case first: more then one option: always show form. if ($count > 1) { return TRUE; } // #2226451: Even in Creation state, we must have 2 visible states to show the widget. // // Only when in creation phase, one option is sufficient, // // since the '(creation)' option is not included in $options. // // When in creation state, // if ($this->isCreationState()) { // return TRUE; // } return FALSE; } /** * Returns the allowed transitions for the current state. * * @param string $entity_type * The type of the entity at hand. * @param object $entity * The entity at hand. May be NULL (E.g., on a Field settings page). * @param string $field_name * @param null $user * @param bool $force * * @return array * An array of tid=>transition pairs with allowed transitions for State. */ public function getTransitions($entity_type = '', $entity = NULL, $field_name = '', $user = NULL, $force = FALSE) { $transitions = array(); $current_sid = $this->sid; $current_state = $this; if (!$workflow = $this->getWorkflow()) { // No workflow, no options ;-) return $transitions; } // Get the role IDs of the user, to get the proper permissions. $roles = $user ? array_keys($user->roles) : array(); // Some entities (e.g., taxonomy_term) do not have a uid. $entity_uid = isset($entity->uid) ? $entity->uid : 0; // Fetch entity_id from entity for _newness_ check $entity_id = ($entity) ? entity_id($entity_type, $entity) : ''; if ($force || ($user && $user->uid == 1)) { // Superuser is special. And $force allows Rules to cause transition. $roles = 'ALL'; } elseif ($entity && (!empty($entity->is_new) || empty($entity_id))) { // Add 'author' role to user, if this is a new entity. // - $entity can be NULL (E.g., on a Field settings page). // - on display of new entity, $entity_id and $is_new are not set. // - on submit of new entity, $entity_id and $is_new are both set. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles); } elseif (($entity_uid > 0) && ($user->uid > 0) && ($entity_uid == $user->uid)) { // Add 'author' role to user, if user is author of this entity. // - Some entities (e.g, taxonomy_term) do not have a uid. // - If 'anonymous' is the author, don't allow access to History Tab, // since anyone can access it, and it will be published in Search engines. $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles); } // Set up an array with states - they are already properly sorted. // Unfortunately, the config_transitions are not sorted. // Also, $transitions does not contain the 'stay on current state' transition. // The allowed objects will be replaced with names. $transitions = $workflow->getTransitionsBySid($current_sid, $roles); // Let custom code add/remove/alter the available transitions. // Using the new drupal_alter. // Modules may veto a choice by removing a transition from the list. $context = array( 'entity_type' => $entity_type, 'entity' => $entity, 'field_name' => $field_name, 'force' => $force, 'workflow' => $workflow, 'state' => $current_state, 'user' => $user, 'user_roles' => $roles, // @todo: can be removed in D8, since $user is in. ); // @todo D8: rename to 'workflow_permitted_transitions'. drupal_alter('workflow_permitted_state_transitions', $transitions, $context); // Let custom code change the options, using old_style hook. // @todo D8: delete below foreach/hook for better performance and flexibility. // Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once. foreach ($transitions as $transition) { $new_sid = $transition->target_sid; $permitted = array(); // We now have a list of config_transitions. Check each against the Entity. // Invoke a callback indicating that we are collecting state choices. // Modules may veto a choice by returning FALSE. // In this case, the choice is never presented to the user. if ($roles != 'ALL') { $permitted = module_invoke_all('workflow', 'transition permitted', $current_sid, $new_sid, $entity, $force, $entity_type, $field_name, $transition, $user); } // If vetoed by a module, remove from list. if (in_array(FALSE, $permitted, TRUE)) { unset($transitions[$transition->tid]); } } return $transitions; } /** * Returns the allowed values for the current state. * * @param string $entity_type * The type of the entity at hand. * @param object $entity * The entity at hand. May be NULL (E.g., on a Field settings page). * @param $field_name * @param $user * @param bool $force * * @return array * An array of sid=>label pairs. * If $this->sid is set, returns the allowed transitions from this state. * If $this->sid is 0 or FALSE, then labels of ALL states of the State's * Workflow are returned. * * D7.x-2.x: deprecated workflow_field_choices() --> WorkflowState->getOptions() */ public function getOptions($entity_type, $entity, $field_name, $user, $force = FALSE) { // Define an Entity-specific cache per page load. static $cache = array(); $options = array(); $entity_id = ($entity) ? entity_id($entity_type, $entity) : ''; $current_sid = $this->sid; // Get options from page cache, using a non-empty index (just to be sure). $entity_index = (!$entity) ? 'x' : $entity_id; if (isset($cache[$entity_type][$entity_index][$force][$current_sid])) { $options = $cache[$entity_type][$entity_index][$force][$current_sid]; return $options; } $workflow = $this->getWorkflow(); if (!$workflow) { // No workflow, no options ;-) } elseif (!$current_sid) { // If no State ID is given, we return all states. // We cannot use getTransitions, since there are no ConfigTransitions // from State with ID 0, and we do not want to repeat States. foreach ($workflow->getStates() as $state) { $options[$state->value()] = $state->label(); // Translation is done as part of defaultLabel(). } } else { /* @var $transition WorkflowTransition */ $transitions = $this->getTransitions($entity_type, $entity, $field_name, $user, $force); foreach ($transitions as $transition) { // Get the label of the transition, and if empty of the target state. // Beware: the target state may not exist, since it can be invented // by custom code in the above drupal_alter() hook. if (!$label = $transition->label()) { $target_state = $transition->getNewState(); $label = $target_state ? $target_state->label() : ''; } $new_sid = $transition->target_sid; $options[$new_sid] = $label; // Translation is done as part of defaultLabel(). } // Include current state for same-state transitions, except when $sid = 0. // Caveat: this unnecessary since 7.x-2.3 (where stay-on-state transitions are saved, too.) // but only if the transitions have been saved at least one time. if ($current_sid && ($current_sid != $workflow->getCreationSid())) { if (!isset($options[$current_sid])) { $options[$current_sid] = $this->label(); // Translation is done as part of defaultLabel(). } } // Save to entity-specific cache. $cache[$entity_type][$entity_index][$force][$current_sid] = $options; } return $options; } /** * Returns the number of entities with this state. * * @return int * Counted number. * * @todo: add $options to select on entity type, etc. */ public function count() { $sid = $this->sid; // Get the numbers for Workflow Node. $result = db_select('workflow_node', 'wn') ->fields('wn') ->condition('sid', $sid, '=') ->execute(); $count = count($result->fetchAll()); // @see #2285983 for using SQLite. // Get the numbers for Workflow Field. $fields = _workflow_info_fields(); foreach ($fields as $field_name => $field_map) { if ($field_map['type'] == 'workflow') { $query = new EntityFieldQuery(); $query ->fieldCondition($field_name, 'value', $sid, '=') // ->entityCondition('bundle', 'article') // ->addMetaData('account', user_load(1)) // Run the query as user 1. ->count(); // We only need the count. $result = $query->execute(); $count += $result; } } return $count; } /** * Mimics Entity API functions. */ protected function defaultLabel() { return isset($this->state) ? t('@state', array('@state' => $this->state)) : ''; } public function getName() { return isset($this->name) ? $this->name : ''; } public function setName($name) { return $this->name = $name; } public function value() { return $this->sid; } public function save() { parent::save(); // Ensure Workflow is marked overridden. $workflow = $this->getWorkflow(); if ($workflow->status == ENTITY_IN_CODE) { $workflow->status = ENTITY_OVERRIDDEN; $workflow->save(); } } } class WorkflowStateController extends EntityAPIController { public function save($entity, DatabaseTransaction $transaction = NULL) { // Create the machine_name. if (empty($entity->name)) { if ($label = $entity->state) { $entity->name = str_replace(' ', '_', strtolower($label)); } else { $entity->name = 'state_' . $entity->sid; } } $return = parent::save($entity, $transaction); if ($return) { $workflow = $entity->getWorkflow(); // Maintain the new object in the workflow. $workflow->states[$entity->sid] = $entity; } // Reset the cache for the affected workflow. workflow_reset_cache($entity->wid); return $return; } public function delete($ids, DatabaseTransaction $transaction = NULL) { // @todo: replace with parent. foreach ($ids as $id) { if ($state = workflow_state_load($id)) { $wid = $state->wid; db_delete('workflow_states') ->condition('sid', $state->sid) ->execute(); // Reset the cache for the affected workflow. workflow_reset_cache($wid); } } } }