getEntity() to fetch this. public $nid; // @todo D8: remove $nid, use $entity_id. (requires conversion of Views displays.) // Transition data. // public $hid = 0; public $wid = 0; public $old_sid = 0; public $new_sid = 0; public $sid = 0; // @todo D8: remove $sid, use $new_sid. (requires conversion of Views displays.) public $uid = 0; // Use WorkflowTransition->getUser() to fetch this. public $stamp; public $comment = ''; // Cached data, from $this->entity_id and $this->uid. protected $entity = NULL; // Use WorkflowTransition->getEntity() to fetch this. protected $user = NULL; // Use WorkflowTransition->getUser() to fetch this. // Extra data. protected $is_scheduled = NULL; protected $is_executed = NULL; protected $force = NULL; /** * Entity class functions. */ /** * Creates a new entity. * * @param array $values * The initial values. * @param string $entityType * The entity type of this Entity subclass. * * @see entity_create() * * No arguments passed, when loading from DB. * All arguments must be passed, when creating an object programmatically. * One argument $entity may be passed, only to directly call delete() afterwards. */ public function __construct(array $values = array(), $entityType = 'WorkflowTransition') { // Please be aware that $entity_type and $entityType are different things! parent::__construct($values, $entityType); $this->hid = isset($this->hid) ? $this->hid : 0; // This transition is not scheduled $this->is_scheduled = FALSE; // This transition is not executed, if it has no hid, yet, upon load. $this->is_executed = ($this->hid > 0); // Fill the 'new' fields correctly. @todo D8: rename these fields in db table. $this->entity_id = $this->nid; $this->new_sid = $this->sid; // Initialize wid, if not set. if ($this->old_sid && !$this->wid) { $this->getWorkflow(); } } /** * Helper function for __construct. Used for all children of WorkflowTransition (aka WorkflowScheduledTransition) * * @param $entity_type * @param $entity * @param $field_name * @param $old_sid * @param $new_sid * @param null $uid * @param int $stamp * @param string $comment */ public function setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $uid = NULL, $stamp = REQUEST_TIME, $comment = '') { // Normally, the values are passed in an array, and set in parent::__construct, but we do it ourselves. // (But there is no objection to do it there.) global $user; $this->entity_type = (!$entity_type) ? $this->entity_type : $entity_type; $this->field_name = (!$field_name) ? $this->field_name : $field_name; $uid = ($uid === NULL) ? $user->uid : $uid; // If constructor is called with new() and arguments. // Load the supplied entity. if ($entity && !$entity_type) { // Not all parameters are passed programmatically. drupal_set_message(t('Wrong call to new Workflow*Transition()'), 'error'); } elseif ($entity) { $this->setEntity($entity_type, $entity); } if (!$entity && !$old_sid && !$new_sid) { // If constructor is called without arguments, e.g., loading from db. } elseif ($entity && $old_sid) { // Caveat: upon entity_delete, $new_sid is '0'. // If constructor is called with new() and arguments. $this->old_sid = $old_sid; $this->sid = $new_sid; $this->uid = $uid; $this->stamp = $stamp; $this->comment = $comment; // Set language. Multi-language is not supported for Workflow Node. $this->language = _workflow_metadata_workflow_get_properties($entity, array(), 'langcode', $entity_type, $field_name); } elseif (!$old_sid) { // Not all parameters are passed programmatically. drupal_set_message( t('Wrong call to constructor Workflow*Transition(@old_sid to @new_sid)', array('@old_sid' => $old_sid, '@new_sid' => $new_sid)), 'error'); } // Fill the 'new' fields correctly. @todo D8: rename these fields in db table. $this->entity_id = $this->nid; $this->new_sid = $this->sid; // Initialize wid, if not set. if ($this->old_sid && !$this->wid) { $this->getWorkflow(); } } /** * {@inheritdoc} */ protected function defaultLabel() { // @todo; Should return title of WorkflowConfigTransition. Make it a superclass?? return t('Workflow transition !hid', array('!hid' =>3)); } // protected function defaultUri() { // return array('path' => 'workflow_transition/' . $this->hid); // } /** * CRUD functions. */ /** * Given a node, get all transitions for it. * * Since this may return a lot of data, a limit is included to allow for only one result. * * @param string $entity_type * @param array $entity_ids * @param string $field_name * Optional. Can be NULL, if you want to load any field. * @param null $limit * @param string $langcode * * @return array * An array of WorkflowTransitions. */ public static function loadMultiple($entity_type, array $entity_ids, $field_name = '', $limit = NULL, $langcode = '') { $query = db_select('workflow_node_history', 'h'); $query->condition('h.entity_type', $entity_type); if ($entity_ids) { $query->condition('h.nid', $entity_ids); } if ($field_name !== NULL) { // If we do not know/care for the field_name, fetch all history. // E.g., in workflow.tokens. $query->condition('h.field_name', $field_name); } // Add selection on language. // Workflow Node: only has 'und'. // Workflow Field: untranslated field have 'und'. // Workflow Field: translated fields may be specified. if ($langcode) { $query->condition('h.language', $langcode); } $query->fields('h'); // The timestamp is only granular to the second; on a busy site, we need the id. // $query->orderBy('h.stamp', 'DESC'); $query->orderBy('h.hid', 'DESC'); if ($limit) { $query->range(0, $limit); } $result = $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowTransition'); return $result; } /** * Property functions. */ /** * Verifies if the given transition is allowed. * * - In settings; * - In permissions; * - By permission hooks, implemented by other modules. * * @param $roles * @param $user * @param $force * * @return bool TRUE if OK, else FALSE. * TRUE if OK, else FALSE. * * Having both $roles AND $user seems redundant, but $roles have been * tampered with, even though they belong to the $user. * @see WorkflowConfigTransition::isAllowed() */ protected function isAllowed($roles, $user, $force) { if ($force || ($user->uid == 1)) { return TRUE; } // Check allow-ability of state change if user is not superuser (might be cron). // Get the WorkflowConfigTransition. // @todo: some day, WorkflowConfigTransition can be a parent of WorkflowTransition. $workflow = $this->getWorkflow(); $config_transitions = $workflow->getTransitionsBySidTargetSid($this->old_sid, $this->new_sid); $config_transition = reset($config_transitions); if (!$config_transition || !$config_transition->isAllowed($roles)) { $t_args = array( '%old_sid' => $this->old_sid, '%new_sid' => $this->new_sid, ); watchdog('workflow', 'Attempt to go to nonexistent transition (from %old_sid to %new_sid)', $t_args, WATCHDOG_ERROR); return FALSE; } return TRUE; } /** * Execute a transition (change state of a node). * * @param bool $force * If set to TRUE, workflow permissions will be ignored. * * @return int * New state ID. If execution failed, old state ID is returned, * * deprecated workflow_execute_transition() --> WorkflowTransition::execute(). */ public function execute($force = FALSE) { $user = $this->getUser(); $old_sid = $this->old_sid; $new_sid = $this->new_sid; // Load the entity, if not already loaded. // This also sets the (empty) $revision_id in Scheduled Transitions. $entity = $this->getEntity(); // Only after getEntity(), the following are surely set. $entity_type = $this->entity_type; $entity_id = $this->entity_id; $field_name = $this->field_name; // Make sure $force is set in the transition, too. if ($force) { $this->force($force); } $force = $this->isForced(); // Prepare an array of arguments for error messages. $args = array( '%user' => isset($user->name) ? $user->name : '', '%old' => $old_sid, '%new' => $new_sid, ); if (!$this->getOldState()) { drupal_set_message($message = t('You tried to set a Workflow State, but the entity is not relevant. Please contact your system administrator.'), 'error'); $message = 'Setting a non-relevant Entity from state %old to %new'; $uri = entity_uri($entity_type, $entity); watchdog('workflow', $message, $args, WATCHDOG_ERROR, l('view', $uri['path'])); return $old_sid; } // Check if the state has changed. $state_changed = ($old_sid != $new_sid); // If so, check the permissions. if ($state_changed) { // State has changed. Do some checks upfront. if (!$force) { // Make sure this transition is allowed by workflow module Admin UI. $roles = array_keys($user->roles); $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles); if (!$this->isAllowed($roles, $user, $force)) { watchdog('workflow', 'User %user not allowed to go from state %old to %new', $args, WATCHDOG_NOTICE); // If incorrect, quit. return $old_sid; } } if (!$force) { // Make sure this transition is allowed by custom module. // @todo D8: remove, or replace by 'transition pre'. See WorkflowState::getOptions(). // @todo D8: replace all parameters that are included in $transition. $permitted = module_invoke_all('workflow', 'transition permitted', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this, $user); // Stop if a module says so. if (in_array(FALSE, $permitted, TRUE)) { watchdog('workflow', 'Transition vetoed by module.'); return $old_sid; } } // Make sure this transition is valid and allowed for the current user. // Invoke a callback indicating a transition is about to occur. // Modules may veto the transition by returning FALSE. // (Even if $force is TRUE, but they shouldn't do that.) $permitted = module_invoke_all('workflow', 'transition pre', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); // Stop if a module says so. if (in_array(FALSE, $permitted, TRUE)) { watchdog('workflow', 'Transition vetoed by module.'); return $old_sid; } } elseif ($this->comment) { // No need to ask permission for adding comments. // Since you should not add actions to a 'transition pre' event, there is // no need to invoke the event. } else { // There is no state change, and no comment. // We may need to clean up something. } if ($state_changed || $this->comment) { // Store the transition, so it can be easily fetched later on. // Store in an array, to prepare for multiple workflow_fields per entity. // This is a.o. used in hook_entity_update to trigger 'transition post'. // Only add the Transition once! or you will encounter endless loops in // hook_entity_update() in workflow_actions_entity_update et all. if (!isset($entity->workflow_transitions[$field_name])) { $entity->workflow_transitions[$field_name] = &$this; } // The transition is allowed. Let other modules modify the comment. // @todo D8: remove all but last items from $context. $context = array( 'node' => $entity, 'sid' => $new_sid, 'old_sid' => $old_sid, 'uid' => $user->uid, 'transition' => $this, ); drupal_alter('workflow_comment', $this->comment, $context); } // Now, change the database. // Log the new state in {workflow_node}. if (!$field_name) { if ($state_changed || $this->comment) { // If the node does not have an existing 'workflow' property, // save the $old_sid there, so it can be logged. if (!isset($entity->workflow)) { // This is a workflow_node sid. $entity->workflow = $old_sid; // This is a workflow_node sid. } // Change the state for {workflow_node}. // The equivalent for Field API is in WorkflowDefaultWidget::submit. $data = array( 'nid' => $entity_id, 'sid' => $new_sid, 'uid' => (isset($entity->workflow_uid) ? $entity->workflow_uid : $user->uid), 'stamp' => REQUEST_TIME, ); workflow_update_workflow_node($data); $entity->workflow = $new_sid; // This is a workflow_node sid. } } else { // This is a Workflow Field. // Until now, adding code here (instead of in workflow_execute_transition() ) // doesn't work, creating an endless loop. // Update 10-dec-2016: the following line, added above, may have resolved that. // if (!isset($entity->workflow_transitions[$field_name])) /* if ($state_changed || $this->comment) { // Do a separate update to update the field (Workflow Field API) // This will call hook_field_update() and WorkflowFieldDefaultWidget::submit(). // $entity->{$field_name}[$this->language] = array(); // $entity->{$field_name}[$this->language][0]['workflow']['workflow_sid'] = $new_sid; // $entity->{$field_name}[$this->language][0]['workflow']['workflow_comment'] = $this->comment; $entity->{$field_name}[$this->language][0]['transition'] = $this; // Save the entity, but not through entity_save(), // since this will check permissions again and trigger rules. // @TODO: replace below by a workflow_field setter callback. // The transition was successfully executed, or else a message was raised. // entity_save($entity_type, $entity); // or // field_attach_update($entity_type, $entity); // Reset the entity cache after update. entity_get_controller($entity_type)->resetCache(array($entity_id)); $new_sid = workflow_node_current_state($entity, $entity_type, $field_name); } */ } $this->is_executed = TRUE; if ($state_changed || $this->comment) { // Log the transition in {workflow_node_history}. $this->save(); // Register state change with watchdog. if ($state_changed) { $workflow = $this->getWorkflow(); // Get the workflow_settings, unified for workflow_node and workflow_field. // @todo D8: move settings back to Workflow (like workflownode currently is). // @todo D8: to move settings back, grep for "workflow->options" and "field['settings']". $field = _workflow_info_field($field_name, $workflow); if (($new_state = $this->getNewState()) && !empty($field['settings']['watchdog_log'])) { $entity_type_info = entity_get_info($entity_type); $message = ($this->isScheduled()) ? 'Scheduled state change of @type %label to %state_name executed' : 'State of @type %label set to %state_name'; $args = array( '@type' => $entity_type_info['label'], '%label' => entity_label($entity_type, $entity), '%state_name' => $new_state->label(), ); $uri = entity_uri($entity_type, $entity); watchdog('workflow', $message, $args, WATCHDOG_NOTICE, l('view', $uri['path'])); } } // Remove any scheduled state transitions. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { /* @var $scheduled_transition WorkflowScheduledTransition */ $scheduled_transition->delete(); } // Notify modules that transition has occurred. // Action triggers should take place in response to this callback, not the 'transaction pre'. if (!$field_name) { // Now that workflow data is saved, reset stuff to avoid problems // when Rules etc want to resave the data. // Remember, this is only for nodes, and node_save() is not necessarily performed. unset($entity->workflow_comment); module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); entity_get_controller('node')->resetCache(array($entity->nid)); // from entity_load(), node_save(); } else { // module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); // We have a problem here with Rules, Trigger, etc. when invoking // 'transition post': the entity has not been saved, yet. we are still // IN the transition, not AFTER. Alternatives: // 1. Save the field here explicitly, using field_attach_save; // 2. Move the invoke to another place: hook_entity_insert(), hook_entity_update(); // 3. Rely on the entity hooks. This works for Rules, not for Trigger. // --> We choose option 2: // - First, $entity->workflow_transitions[] is set for easy re-fetching. // - Then, post_execute() is invoked via workflowfield_entity_insert(), _update(). } } return $new_sid; } /** * Invokes 'transition post'. * * Add the possibility to invoke the hook from elsewhere. */ public function post_execute($force = FALSE) { $old_sid = $this->old_sid; $new_sid = $this->new_sid; $entity = $this->getEntity(); // Entity may not be loaded, yet. $entity_type = $this->entity_type; // $entity_id = $this->entity_id; $field_name = $this->field_name; $state_changed = ($old_sid != $new_sid); if ($state_changed || $this->comment) { module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); } } /** * Get the Transitions $workflow. * * @return Workflow|NULL * The workflow for this Transition. */ public function getWorkflow() { $workflow = NULL; if (!$this->wid) { $state = workflow_state_load_single($this->new_sid ? $this->new_sid : $this->old_sid); $this->wid = (int) $state->wid; } if ($this->wid) { $workflow = workflow_load($this->wid); } return $workflow; } /** * Get the Transitions $entity. * * @return object * The entity, that is added to the Transition. */ public function getEntity() { if (empty($this->entity) && $this->entity_type) { $entity_type = $this->entity_type; $entity_id = $this->entity_id; $entity = entity_load_single($entity_type, $entity_id); // Set the entity cache. $this->entity = $entity; // Make sure the vid of Entity and Transition are equal. // Especially for Scheduled Transition, that do not have this set, yet, // or may have an outdated revision ID. $info = entity_get_info($entity_type); $revision_key = $info['entity keys']['revision']; $this->revision_id = (isset($entity->{$revision_key})) ? $entity->{$revision_key} : NULL; } return $this->entity; } /** * Set the Transitions $entity. * * @param string $entity_type * The entity type of the entity. * @param mixed $entity * The Entity ID or the Entity object, to add to the Transition. * * @return object $entity * The Entity, that is added to the Transition. */ public function setEntity($entity_type, $entity) { if (!is_object($entity)) { $entity_id = $entity; // Use node API or Entity API to load the object first. $entity = entity_load_single($entity_type, $entity_id); } $this->entity = $entity; $this->entity_type = $entity_type; list($this->entity_id, $this->revision_id,) = entity_extract_ids($entity_type, $entity); // For backwards compatibility, set nid. $this->nid = $this->entity_id; return $this->entity; } public function getUser() { if (!isset($this->user) || ($this->user->uid != $this->uid)) { $this->user = user_load($this->uid); } return $this->user; } /** * {@inheritdoc} */ public function getFieldName() { return $this->field_name; } /** * Functions, common to the WorkflowTransitions. */ public function getOldState() { return workflow_state_load_single($this->old_sid); } public function getNewState() { return workflow_state_load_single($this->new_sid); } /** * {@inheritdoc} */ public function getComment() { return $this->comment; } /** * Returns the time on which the transitions was or will be executed. * * @return mixed */ public function getTimestamp() { return $this->stamp; } /** * {@inheritdoc} */ public function getTimestampFormatted() { $timestamp = $this->stamp; return format_date($timestamp);; } /** * {@inheritdoc} */ public function setTimestamp($value) { $this->stamp = $value; return $this; } /** * Returns if this is a Scheduled Transition. */ public function isScheduled() { return $this->is_scheduled; } public function schedule($schedule = TRUE) { return $this->is_scheduled = $schedule; } public function isExecuted() { return $this->is_executed; } /** * A transition may be forced skipping checks. */ public function isForced() { return (bool) $this->force; } public function force($force = TRUE) { return $this->force = $force; } /** * Helper debugging function to easily show the contents fo a transition. */ public function dpm($function = '') { $transition = $this; $entity = $transition->getEntity(); $entity_type = $transition->entity_type; list($entity_id, , $entity_bundle) = ($entity) ? entity_extract_ids($entity_type, $entity) : array('', '', ''); $time = $transition->getTimestampFormatted(); // Do this extensive $user_name lines, for some troubles with Action. $user = $transition->getUser(); $user_name = ($user) ? $user->name : 'unknown username'; $t_string = get_class($this) . ' ' . (isset($this->hid) ? $this->hid : '') . ' ' . ($function ? ("in function '$function'") : ''); $output[] = 'Entity = ' . ((!$entity) ? 'NULL' : ($entity_type . '/' . $entity_bundle . '/' . $entity_id)); $output[] = 'Field = ' . $transition->getFieldName(); $output[] = 'From/To = ' . $transition->old_sid . ' > ' . $transition->new_sid . ' @ ' . $time; $output[] = 'Comment = ' . $user_name . ' says: ' . $transition->getComment(); $output[] = 'Forced = ' . ($transition->isForced() ? 'yes' : 'no'); if (function_exists('dpm')) { dpm($output, $t_string); } } } /** * Implements a controller class for WorkflowTransition. * * The 'true' controller class is 'Workflow'. */ class WorkflowTransitionController extends EntityAPIController { /** * Overrides DrupalDefaultEntityController::cacheGet(). * * Override default function, due to core issue #1572466. */ protected function cacheGet($ids, $conditions = array()) { // Load any available entities from the internal cache. if ($ids === FALSE && !$conditions) { return $this->entityCache; } return parent::cacheGet($ids, $conditions); } /** * Insert (no update) a transition. * * deprecated workflow_insert_workflow_node_history() --> WorkflowTransition::save() */ public function save($entity, DatabaseTransaction $transaction = NULL) { // Check for no transition. if ($entity->old_sid == $entity->new_sid) { if (!$entity->comment) { // Write comment into history though. return; } } if (empty($entity->hid)) { // Insert the transition. Make sure it hasn't already been inserted. $last_history = workflow_transition_load_single($entity->entity_type, $entity->entity_id, $entity->field_name, $entity->language); if ($last_history && $last_history->stamp == REQUEST_TIME && $last_history->new_sid == $entity->new_sid) { return; } else { unset($entity->hid); $entity->stamp = isset($entity->stamp) ? $entity->stamp : REQUEST_TIME; return parent::save($entity, $transaction); } } else { // Update the transition. return parent::save($entity, $transaction); } } }