You are here

workflow.module in Workflow 6.2

Support workflows made up of arbitrary states.

File

workflow.module
View source
<?php

/**
 * @file
 * Support workflows made up of arbitrary states.
 */
define('WORKFLOW_CREATION', 1);
define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
define('WORKFLOW_DELETION', 0);

/**
 * Implementation of hook_perm().
 */
function workflow_perm() {
  return array(
    'schedule workflow transitions',
  );
}

/**
 * Implementation of hook_menu().
 */
function workflow_menu() {
  $items['node/%node/workflow'] = array(
    'title' => 'Workflow',
    'type' => MENU_LOCAL_TASK,
    'access callback' => 'workflow_node_tab_access',
    'access arguments' => array(
      1,
    ),
    'page callback' => 'workflow_tab_page',
    'page arguments' => array(
      1,
    ),
    'file' => 'workflow.pages.inc',
    'weight' => 2,
  );
  return $items;
}

/**
 * Menu access control callback. Determine access to Workflow tab.
 */
function workflow_node_tab_access($node = NULL) {
  global $user;
  $wid = workflow_get_workflow_for_type($node->type);
  if ($wid === FALSE) {

    // No workflow associated with this node type.
    return FALSE;
  }
  $roles = array_keys($user->roles);
  if ($node->uid == $user->uid) {
    $roles = array_merge(array(
      'author',
    ), $roles);
  }
  $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid));
  $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array();
  if (user_access('administer nodes') || array_intersect($roles, $allowed_roles)) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Implementation of hook_theme().
 */
function workflow_theme() {
  return array(
    'workflow_history_table_row' => array(
      'arguments' => array(
        'history' => NULL,
        'old_state_name' => NULL,
        'state_name' => null,
      ),
    ),
    'workflow_history_table' => array(
      'arguments' => array(
        'rows' => array(),
        'footer' => NULL,
      ),
    ),
    'workflow_current_state' => array(
      'arguments' => array(
        'state_name' => NULL,
      ),
    ),
    'workflow_deleted_state' => array(
      'arguments' => array(
        'state_name' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_nodeapi().
 */
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'load':
      $node->_workflow = workflow_node_current_state($node);

      // Add scheduling information.
      $res = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
      if ($row = db_fetch_object($res)) {
        $node->_workflow_scheduled_sid = $row->sid;
        $node->_workflow_scheduled_timestamp = $row->scheduled;
        $node->_workflow_scheduled_comment = $row->comment;
      }
      break;
    case 'insert':

      // If the state is not specified, use first valid state.
      // For example, a new node must move from (creation) to some
      // initial state.
      if (empty($node->workflow)) {
        $choices = workflow_field_choices($node);
        $keys = array_keys($choices);
        $sid = array_shift($keys);
      }

    // Note no break; fall through to 'update' case.
    case 'update':

      // Do nothing if there is no workflow for this node type.
      $wid = workflow_get_workflow_for_type($node->type);
      if (!$wid) {
        break;
      }

      // Get new state from value of workflow form field, stored in $node->workflow.
      if (!isset($sid)) {
        $sid = $node->workflow;
      }
      workflow_transition($node, $sid);
      break;
    case 'delete':
      $node->workflow_stamp = time();
      db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid);
      _workflow_write_history($node, WORKFLOW_DELETION, t('Node deleted'));

      // Delete any scheduled transitions for this node.
      db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = %d", $node->nid);
      break;
  }
}

/**
 * Implementation of hook_comment().
 */
function workflow_comment($a1, $op) {
  if (($op == 'insert' || $op == 'update') && isset($a1['workflow'])) {
    $node = node_load($a1['nid']);
    $sid = $a1['workflow'];
    $node->workflow_comment = $a1['workflow_comment'];
    if (isset($a1['workflow_scheduled'])) {
      $node->workflow_scheduled = $a1['workflow_scheduled'];
      $node->workflow_scheduled_date = $a1['workflow_scheduled_date'];
      $node->workflow_scheduled_hour = $a1['workflow_scheduled_hour'];
    }
    workflow_transition($node, $sid);
  }
}

/**
 * Validate target state and either execute a transition immediately or schedule
 * a transition to be executed later by cron.
 *
 * @param $node
 * @param $sid
 *   An integer; the target state ID.
 */
function workflow_transition($node, $sid) {

  // Make sure new state is a valid choice.
  if (array_key_exists($sid, workflow_field_choices($node))) {
    $node->workflow_scheduled = isset($node->workflow_scheduled) ? $node->workflow_scheduled : FALSE;
    if (!$node->workflow_scheduled) {

      // It's an immediate change. Do the transition.
      workflow_execute_transition($node, $sid, isset($node->workflow_comment) ? $node->workflow_comment : NULL);
    }
    else {

      // Schedule the the time to change the state.
      $comment = $node->workflow_comment;
      $old_sid = workflow_node_current_state($node);
      if ($node->workflow_scheduled_date['day'] < 10) {
        $node->workflow_scheduled_date['day'] = '0' . $node->workflow_scheduled_date['day'];
      }
      if ($node->workflow_scheduled_date['month'] < 10) {
        $node->workflow_scheduled_date['month'] = '0' . $node->workflow_scheduled_date['month'];
      }
      if (!$node->workflow_scheduled_hour) {
        $node->workflow_scheduled_hour = '00:00';
      }
      $scheduled = $node->workflow_scheduled_date['year'] . $node->workflow_scheduled_date['month'] . $node->workflow_scheduled_date['day'] . ' ' . $node->workflow_scheduled_hour . 'Z';
      if ($scheduled = strtotime($scheduled)) {

        // Adjust for user and site timezone settings.
        global $user;
        if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
          $timezone = $user->timezone;
        }
        else {
          $timezone = variable_get('date_default_timezone', 0);
        }
        $scheduled = $scheduled - $timezone;

        // Clear previous entries and insert.
        db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = %d", $node->nid);
        db_query("INSERT INTO {workflow_scheduled_transition} VALUES (%d, %d, %d, %d, '%s')", $node->nid, $old_sid, $sid, $scheduled, $comment);

        // Get name of state.
        $state_name = workflow_get_state_name($sid);
        watchdog('workflow', '@node_title scheduled for state change to %state_name on !scheduled_date', array(
          '@node_title' => $node->title,
          '%state_name' => $state_name,
          '!scheduled_date' => format_date($scheduled),
        ), WATCHDOG_NOTICE, l('view', "node/{$node->nid}/workflow"));
        drupal_set_message(t('@node_title is scheduled for state change to %state_name on !scheduled_date', array(
          '@node_title' => $node->title,
          '%state_name' => $state_name,
          '!scheduled_date' => format_date($scheduled),
        )));
      }
    }
  }
}

/**
 * Form builder. Add form widgets for workflow change to $form.
 *
 * This builder is factored out of workflow_form_alter() because
 * it is also used on the Workflow tab.
 *
 * @param $form
 *   An existing form definition array.
 * @param $name
 *   The name of the workflow.
 * @param $current
 *   The state ID of the current state, used as the default value.
 * @param $choices
 *   An array of possible target states.
 */
function workflow_node_form(&$form, $form_state, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) {

  // No sense displaying choices if there is only one choice.
  if (sizeof($choices) == 1) {
    $form['workflow'][$name] = array(
      '#type' => 'hidden',
      '#value' => $current,
    );
  }
  else {
    $form['workflow'][$name] = array(
      '#type' => 'radios',
      '#title' => $form['#wf']->options['name_as_title'] ? $title : '',
      '#options' => $choices,
      '#name' => $name,
      '#parents' => array(
        'workflow',
      ),
      '#default_value' => $current,
    );

    // Display scheduling form only if a node is being edited and user has
    // permission. State change cannot be scheduled at node creation because
    // that leaves the node in the (creation) state.
    if (!(arg(0) == 'node' && arg(1) == 'add') && user_access('schedule workflow transitions')) {
      $scheduled = $timestamp ? 1 : 0;
      $timestamp = $scheduled ? $timestamp : time();
      $form['workflow']['workflow_scheduled'] = array(
        '#type' => 'radios',
        '#title' => t('Schedule'),
        '#options' => array(
          t('Immediately'),
          t('Schedule for state change at:'),
        ),
        '#default_value' => isset($form_state['values']['workflow_scheduled']) ? $form_state['values']['workflow_scheduled'] : $scheduled,
      );
      $form['workflow']['workflow_scheduled_date'] = array(
        '#type' => 'date',
        '#default_value' => array(
          'day' => isset($form_state['values']['workflow_scheduled_date']['day']) ? $form_state['values']['workflow_scheduled_date']['day'] : format_date($timestamp, 'custom', 'j'),
          'month' => isset($form_state['values']['workflow_scheduled_date']['month']) ? $form_state['values']['workflow_scheduled_date']['month'] : format_date($timestamp, 'custom', 'n'),
          'year' => isset($form_state['values']['workflow_scheduled_date']['year']) ? $form_state['values']['workflow_scheduled_date']['year'] : format_date($timestamp, 'custom', 'Y'),
        ),
      );
      $hours = format_date($timestamp, 'custom', 'H:i');
      $form['workflow']['workflow_scheduled_hour'] = array(
        '#type' => 'textfield',
        '#description' => t('Please enter a time in 24 hour (eg. HH:MM) format. If no time is included, the default will be midnight on the specified date. The current time is: ') . format_date(time()),
        '#default_value' => $scheduled ? isset($form_state['values']['workflow_scheduled_hour']) ? $form_state['values']['workflow_scheduled_hour'] : $hours : NULL,
      );
    }
    if (isset($form['#tab'])) {
      $determiner = 'comment_log_tab';
    }
    else {
      $determiner = 'comment_log_node';
    }
    $form['workflow']['workflow_comment'] = array(
      '#type' => $form['#wf']->options[$determiner] ? 'textarea' : 'hidden',
      '#title' => t('Comment'),
      '#description' => t('A comment to put in the workflow log.'),
      '#default_value' => $comment,
      '#rows' => 2,
    );
  }
}

/**
 * Implementation of hook_form_alter().
 *
 * @param object &$node
 * @return array
 */
function workflow_form_alter(&$form, $form_state, $form_id) {

  // Ignore all forms except comment forms and node editing forms.
  if ($form_id == 'comment_form' || isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
    if (isset($form['#node'])) {
      $node = $form['#node'];

      // Abort if no workflow is assigned to this node type.
      if (!in_array('node', variable_get('workflow_' . $node->type, array(
        'node',
      )))) {
        return;
      }
    }
    else {
      $type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $form['nid']['#value']));

      // Abort if user does not want to display workflow form on node editing form.
      if (!in_array('comment', variable_get('workflow_' . $type, array(
        'node',
      )))) {
        return;
      }
      $node = node_load($form['nid']['#value']);
    }
    $choices = workflow_field_choices($node);
    $wid = workflow_get_workflow_for_type($node->type);
    $states = workflow_get_states($wid);

    // If this is a preview, the current state should come from
    // the form values, not the node, as the user may have changed
    // the state.
    $current = isset($form_state['values']['workflow']) ? $form_state['values']['workflow'] : workflow_node_current_state($node);
    $min = $states[$current] == t('(creation)') ? 1 : 2;

    // Stop if user has no new target state(s) to choose.
    if (count($choices) < $min) {
      return;
    }
    $workflow = workflow_load($wid);
    $form['#wf'] = $workflow;
    $name = check_plain($workflow->name);

    // If the current node state is not one of the choices, autoselect first choice.
    // We know all states in $choices are states that user has permission to
    // go to because workflow_field_choices() has already checked that.
    if (!isset($choices[$current])) {
      $array = array_keys($choices);
      $current = $array[0];
    }
    if (sizeof($choices) > 1) {
      $form['workflow'] = array(
        '#type' => 'fieldset',
        '#title' => $name,
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
        '#weight' => 10,
      );
    }
    $timestamp = NULL;
    $comment = '';

    // See if scheduling information is present.
    if (isset($node->_workflow_scheduled_timestamp) && isset($node->_workflow_scheduled_sid)) {

      // The default value should be the upcoming sid.
      $current = $node->_workflow_scheduled_sid;
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }
    if (isset($form_state['values']['workflow_comment'])) {
      $comment = $form_state['values']['workflow_comment'];
    }
    workflow_node_form($form, $form_state, $name, $name, $current, $choices, $timestamp, $comment);
  }
}

/**
 * Execute a transition (change state of a node).
 *
 * @param $node
 * @param $sid
 *   Target state ID.
 * @param $comment
 *   A comment for the node's workflow history.
 * @param $force
 *   If set to TRUE, workflow permissions will be ignored.
 *
 * @return int
 *   ID of new state.
 */
function workflow_execute_transition($node, $sid, $comment = NULL, $force = FALSE) {
  global $user;
  $old_sid = workflow_node_current_state($node);
  if ($old_sid == $sid) {

    // Stop if not going to a different state.
    // Write comment into history though.
    if ($comment && !$node->_workflow_scheduled_comment) {
      $node->workflow_stamp = time();
      db_query("UPDATE {workflow_node} SET stamp = %d WHERE nid = %d", $node->workflow_stamp, $node->nid);
      $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
      _workflow_write_history($node, $sid, $comment);
      unset($node->workflow_comment);
      $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);

      // Rules integration
      if (module_exists('rules')) {
        rules_invoke_event('workflow_comment_added', $node, $old_sid, $sid);
      }
    }
    return;
  }
  $tid = workflow_get_transition_id($old_sid, $sid);
  if (!$tid && !$force) {
    watchdog('workflow', 'Attempt to go to nonexistent transition (from %old to %new)', array(
      '%old' => $old_sid,
      '%new' => $sid,
      WATCHDOG_ERROR,
    ));
    return;
  }

  // Make sure this transition is valid and allowed for the current user.
  // Check allowability of state change if user is not superuser (might be cron).
  if ($user->uid != 1 && !$force) {
    if (!workflow_transition_allowed($tid, array_merge(array_keys($user->roles), array(
      'author',
    )))) {
      watchdog('workflow', 'User %user not allowed to go from state %old to %new', array(
        '%user' => $user->name,
        '%old' => $old_sid,
        '%new' => $sid,
        WATCHDOG_NOTICE,
      ));
      return;
    }
  }

  // Invoke a callback indicating a transition is about to occur. Modules
  // may veto the transition by returning FALSE.
  $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);

  // Stop if a module says so.
  if (in_array(FALSE, $result)) {
    watchdog('workflow', 'Transition vetoed by module.');
    return;
  }

  // If the node does not have an existing $node->_workflow property, save
  // the $old_sid there so _workflow_write_history() can log it.
  if (!isset($node->_workflow)) {
    $node->_workflow = $old_sid;
  }

  // Change the state.
  _workflow_node_to_state($node, $sid, $comment);
  $node->_workflow = $sid;

  // Register state change with watchdog.
  $state_name = workflow_get_state_name($sid);
  $type = node_get_types('name', $node->type);
  watchdog('workflow', 'State of @type %node_title set to %state_name', array(
    '@type' => $type,
    '%node_title' => $node->title,
    '%state_name' => $state_name,
  ), WATCHDOG_NOTICE, l('view', 'node/' . $node->nid));

  // Notify modules that transition has occurred. Actions should take place
  // in response to this callback, not the previous one.
  module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);

  // Clear any references in the scheduled listing.
  db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);

  // Rules integration
  if (module_exists('rules')) {
    rules_invoke_event('workflow_state_changed', $node, $old_sid, $sid);
  }
  return $sid;
}

/**
 * Get the states current user can move to for a given node.
 *
 * @param object $node
 *   The node to check.
 * @return
 *   Array of transitions.
 */
function workflow_field_choices($node) {
  global $user;
  $wid = workflow_get_workflow_for_type($node->type);
  if (!$wid) {

    // No workflow for this type.
    return array();
  }
  $states = workflow_get_states($wid);
  $roles = array_keys($user->roles);
  $current_sid = workflow_node_current_state($node);

  // If user is node author or this is a new page, give the authorship role.
  if ($user->uid == $node->uid && $node->uid > 0 || arg(0) == 'node' && arg(1) == 'add') {
    $roles += array(
      'author' => 'author',
    );
  }
  if ($user->uid == 1) {

    // Superuser is special.
    $roles = 'ALL';
  }
  $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);

  // Include current state if it is not the (creation) state.
  if ($current_sid == _workflow_creation_state($wid)) {
    unset($transitions[$current_sid]);
  }
  return $transitions;
}

/**
 * Get the current state of a given node.
 *
 * @param $node
 *   The node to check.
 * @return
 *   The ID of the current state.
 */
function workflow_node_current_state($node) {
  $sid = FALSE;

  // There is no nid when creating a node.
  if (!empty($node->nid)) {
    $sid = db_result(db_query('SELECT sid FROM {workflow_node} WHERE nid = %d', $node->nid));
  }
  if (!$sid && !empty($node->type)) {

    // No current state. Use creation state.
    $wid = workflow_get_workflow_for_type($node->type);
    $sid = _workflow_creation_state($wid);
  }
  return $sid;
}

/**
 * Return the ID of the creation state for this workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 */
function _workflow_creation_state($wid) {
  static $cache;
  if (!isset($cache[$wid])) {
    $result = db_result(db_query("SELECT sid FROM {workflow_states} WHERE wid = %d AND sysid = %d", $wid, WORKFLOW_CREATION));
    $cache[$wid] = $result;
  }
  return $cache[$wid];
}

/**
 * Load function.
 *
 * @param $wid
 *   The ID of the workflow to load.
 * @return $workflow
 *   Object representing the workflow.
 */
function workflow_load($wid) {
  $workflow = db_fetch_object(db_query('SELECT * FROM {workflows} WHERE wid = %d', $wid));
  $workflow->options = unserialize($workflow->options);
  return $workflow;
}

/**
 * Update the transitions for a workflow.
 *
 * @param array $transitions
 *   Transitions, for example:
 *     18 => array(
 *       20 => array(
 *         'author' => 1,
 *         1        => 0,
 *         2        => 1,
 *       )
 *     )
 *   means the transition from state 18 to state 20 can be executed by
 *   the node author or a user in role 2. The $transitions array should
 *   contain ALL transitions for the workflow.
 */
function workflow_update_transitions($transitions = array()) {

  // Empty string is sometimes passed in instead of an array.
  if (!$transitions) {
    return;
  }
  foreach ($transitions as $from => $to_data) {
    foreach ($to_data as $to => $role_data) {
      foreach ($role_data as $role => $can_do) {
        if ($can_do) {
          workflow_transition_add_role($from, $to, $role);
        }
        else {
          workflow_transition_delete_role($from, $to, $role);
        }
      }
    }
  }
  db_query("DELETE FROM {workflow_transitions} WHERE roles = ''");
}

/**
 * Add a role to the list of those allowed for a given transition.
 *
 * Add the transition if necessary.
 *
 * @param int $from
 * @param int $to
 * @param mixed $role
 *   Int (role ID) or string ('author').
 */
function workflow_transition_add_role($from, $to, $role) {
  $transition = array(
    'sid' => $from,
    'target_sid' => $to,
    'roles' => $role,
  );
  $tid = workflow_get_transition_id($from, $to);
  if ($tid) {
    $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
    $roles = explode(',', $roles);
    if (array_search($role, $roles) === FALSE) {
      $roles[] = $role;
      $transition['roles'] = implode(',', $roles);
      $transition['tid'] = $tid;
      drupal_write_record('workflow_transitions', $transition, 'tid');
    }
  }
  else {
    drupal_write_record('workflow_transitions', $transition);
  }
}

/**
 * Remove a role from the list of those allowed for a given transition.
 *
 * @param int $tid
 * @param mixed $role
 *   Int (role ID) or string ('author').
 */
function workflow_transition_delete_role($from, $to, $role) {
  $tid = workflow_get_transition_id($from, $to);
  if ($tid) {
    $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
    $roles = explode(',', $roles);
    if (($i = array_search($role, $roles)) !== FALSE) {
      unset($roles[$i]);
      db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid);
    }
  }
}

/**
 * See if a transition is allowed for a given role.
 *
 * @param int $tid
 * @param mixed $role
 *   A single role (int or string 'author') or array of roles.
 * @return
 *   TRUE if the role is allowed to do the transition.
 */
function workflow_transition_allowed($tid, $role = NULL) {
  $allowed = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid = %d", $tid));
  $allowed = explode(',', $allowed);
  if ($role) {
    if (!is_array($role)) {
      $role = array(
        $role,
      );
    }
    return array_intersect($role, $allowed) == TRUE;
  }
}

/**
 * Tell caller whether a state is a protected system state, such as the creation state.
 *
 * @param $state
 *   The name of the state to test
 * @return
 *   TRUE if the state is a system state.
 */
function workflow_is_system_state($state) {
  static $states;
  if (!isset($states)) {
    $states = array(
      t('(creation)') => TRUE,
    );
  }
  return isset($states[$state]);
}

/**
 * Given the ID of a workflow, return its name.
 *
 * @param integer $wid
 *   The ID of the workflow.
 * @return string
 *   The name of the workflow.
 */
function workflow_get_name($wid) {
  return db_result(db_query("SELECT name FROM {workflows} WHERE wid = %d", $wid));
}

/**
 * Get ID of a workflow for a node type.
 *
 * @param $type
 *   Machine readable node type name, e.g. 'story'.
 * @return int
 *   The ID of the workflow or FALSE if no workflow is mapped to this type.
 */
function workflow_get_workflow_for_type($type) {
  static $cache;
  if (!isset($cache[$type])) {
    $wid = db_result(db_query("SELECT wid FROM {workflow_type_map} WHERE type = '%s'", $type));
    $cache[$type] = $wid;
  }
  else {
    $wid = $cache[$type];
  }
  return $wid > 0 ? $wid : FALSE;
}

/**
 * Get names and IDs of all workflows from the database.
 *
 * @return
 *   An array of workflows keyed by workflow ID.
 */
function workflow_get_all() {
  $workflows = array();
  $result = db_query("SELECT wid, name FROM {workflows} ORDER BY name ASC");
  while ($data = db_fetch_object($result)) {
    $workflows[$data->wid] = check_plain(t($data->name));
  }
  return $workflows;
}

/**
 * Create a workflow and its (creation) state.
 *
 * @param $name
 *   The name of the workflow.
 */
function workflow_create($name) {
  $workflow = array(
    'name' => $name,
    'options' => serialize(array(
      'comment_log_node' => 1,
      'comment_log_tab' => 1,
    )),
  );
  drupal_write_record('workflows', $workflow);
  workflow_state_save(array(
    'wid' => $workflow['wid'],
    'state' => t('(creation)'),
    'sysid' => WORKFLOW_CREATION,
    'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT,
  ));

  // Workflow creation affects tabs (local tasks), so force menu rebuild.
  menu_rebuild();
  return $workflow['wid'];
}

/**
 * Save a workflow's name in the database.
 *
 * @param $wid
 *   The ID of the workflow.
 * @param $name
 *   The name of the workflow.
 * @param $tab_roles
 *   Array of role IDs allowed to see the workflow tab.
 * @param $options
 *   Array of key-value pairs that constitute various settings for
 *   this workflow. An example is whether to show the comment form
 *   on the workflow tab page or not.
 */
function workflow_update($wid, $name, $tab_roles, $options) {
  db_query("UPDATE {workflows} SET name = '%s', tab_roles = '%s', options = '%s' WHERE wid = %d", $name, implode(',', $tab_roles), serialize($options), $wid);

  // Workflow name change affects tabs (local tasks), so force menu rebuild.
  menu_rebuild();
}

/**
 * Delete a workflow from the database. Deletes all states,
 * transitions and node type mappings, too. Removes workflow state
 * information from nodes participating in this workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 */
function workflow_deletewf($wid) {
  $wf = workflow_get_name($wid);
  $result = db_query('SELECT sid FROM {workflow_states} WHERE wid = %d', $wid);
  while ($data = db_fetch_object($result)) {

    // Delete the state and any associated transitions and actions.
    workflow_state_delete($data->sid);
    db_query('DELETE FROM {workflow_node} WHERE sid = %d', $data->sid);
  }
  db_query("DELETE FROM {workflow_type_map} WHERE wid = %d", $wid);
  db_query('DELETE FROM {workflows} WHERE wid = %d', $wid);

  // Notify any interested modules.
  module_invoke_all('workflow', 'workflow delete', $wid, NULL, NULL);

  // Workflow deletion affects tabs (local tasks), so force menu rebuild.
  cache_clear_all('*', 'cache_menu', TRUE);
  menu_rebuild();
}

/**
 * Load workflow states for a workflow from the database.
 * If $wid is not passed, all states for all workflows are given.
 * States that have been deleted are not included.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   An array of workflow states keyed by state ID.
 */
function workflow_get_states($wid = NULL) {
  $states = array();
  if (isset($wid)) {
    $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d AND status = 1 ORDER BY weight, sid", $wid);
    while ($data = db_fetch_object($result)) {
      $states[$data->sid] = check_plain(t($data->state));
    }
  }
  else {
    $result = db_query("SELECT ws.sid, ws.state, w.name FROM {workflow_states} ws INNER JOIN {workflows} w ON ws.wid = w.wid WHERE status = 1 ORDER BY sid");
    while ($data = db_fetch_object($result)) {
      $states[$data->sid] = check_plain(t($data->name)) . ': ' . check_plain(t($data->state));
    }
  }
  return $states;
}

/**
 * Given the ID of a workflow state, return a keyed array representing the state.
 *
 * Note: this will retrieve states that have been deleted (their status key
 *   will be set to 0).
 *
 * @param $sid
 *   The ID of the workflow state.
 * @return
 *   A keyed array with all attributes of the state.
 */
function workflow_get_state($sid) {
  $state = array();
  $result = db_query('SELECT wid, state, weight, sysid, status FROM {workflow_states} WHERE sid = %d', $sid);

  // State IDs are unique, so there should be only one row.
  $data = db_fetch_object($result);
  $state['wid'] = $data->wid;
  $state['state'] = $data->state;
  $state['weight'] = $data->weight;
  $state['sysid'] = $data->sysid;
  $state['status'] = $data->status;
  return $state;
}

/**
 * Given the ID of a state, return its name.
 *
 * @param integer $sid
 *   The ID of the workflow state.
 * @return string
 *   The name of the workflow state.
 */
function workflow_get_state_name($sid) {
  return db_result(db_query('SELECT state FROM {workflow_states} WHERE sid = %d', $sid));
}

/**
 * Add or update a workflow state to the database.
 *
 * @param $edit
 *   An array containing values for the new or updated workflow state.
 * @return
 *   The ID of the new or updated workflow state.
 */
function workflow_state_save($state) {
  if (!isset($state['sid'])) {
    drupal_write_record('workflow_states', $state);
  }
  else {
    drupal_write_record('workflow_states', $state, 'sid');
  }
  return $state['sid'];
}

/**
 * Delete a workflow state from the database, including any
 * transitions the state was involved in and any associations
 * with actions that were made to that transition.
 *
 * @param $sid
 *   The ID of the state to delete.
 * @param $new_sid
 *   Deleting a state will leave any nodes to which that state is assigned
 *   without a state. If $new_sid is given, it will be assigned to those
 *   orphaned nodes
 */
function workflow_state_delete($sid, $new_sid = NULL) {
  if ($new_sid) {

    // Assign nodes to new state so they are not orphaned.
    // A candidate for the batch API.
    $node = new stdClass();
    $node->workflow_stamp = time();
    $result = db_query("SELECT nid FROM {workflow_node} WHERE sid = %d", $sid);
    while ($data = db_fetch_object($result)) {
      $node->nid = $data->nid;
      $node->_workflow = $sid;
      _workflow_write_history($node, $new_sid, t('Previous state deleted'));
      db_query("UPDATE {workflow_node} SET sid = %d WHERE nid = %d AND sid = %d", $new_sid, $data->nid, $sid);
    }
  }
  else {

    // Go ahead and orphan nodes.
    db_query('DELETE from {workflow_node} WHERE sid = %d', $sid);
  }

  // Find out which transitions this state is involved in.
  $preexisting = array();
  $result = db_query("SELECT sid, target_sid FROM {workflow_transitions} WHERE sid = %d OR target_sid = %d", $sid, $sid);
  while ($data = db_fetch_object($result)) {
    $preexisting[$data->sid][$data->target_sid] = TRUE;
  }

  // Delete the transitions and associated actions, if any.
  foreach ($preexisting as $from => $array) {
    foreach (array_keys($array) as $target_id) {
      $tid = workflow_get_transition_id($from, $target_id);
      workflow_transition_delete($tid);
    }
  }

  // Delete the state.
  db_query("UPDATE {workflow_states} SET status = 0 WHERE sid = %d", $sid);

  // Notify interested modules.
  module_invoke_all('workflow', 'state delete', $sid, NULL, NULL);
}

/**
 * Delete a transition (and any associated actions).
 *
 * @param $tid
 *   The ID of the transition.
 */
function workflow_transition_delete($tid) {

  // TODO - turn this into a hook callback.
  if (function_exists('workflow_actions_get_actions')) {
    $actions = workflow_actions_get_actions($tid);
    foreach ($actions as $aid => $type) {
      workflow_actions_actions_remove($tid, $aid);
    }
  }
  db_query("DELETE FROM {workflow_transitions} WHERE tid = %d", $tid);

  // Notify interested modules.
  module_invoke_all('workflow', 'transition delete', $tid, NULL, NULL);
}

/**
 * Get allowable transitions for a given workflow state. Typical use:
 *
 * global $user;
 * $possible = workflow_allowable_transitions($sid, 'to', $user->roles);
 *
 * If the state ID corresponded to the state named "Draft", $possible now
 * contains the states that the current user may move to from the Draft state.
 *
 * @param $sid
 *   The ID of the state in question.
 * @param $dir
 *   The direction of the transition: 'to' or 'from' the state denoted by $sid.
 *   When set to 'to' all the allowable states that may be moved to are
 *   returned; when set to 'from' all the allowable states that may move to the
 *   current state are returned.
 * @param mixed $roles
 *   Array of ints (and possibly the string 'author') representing the user's
 *   roles. If the string 'ALL' is passed (instead of an array) the role
 *   constraint is ignored (this is the default for backwards compatibility).
 *
 * @return
 *   Associative array of states ($sid => $state_name pairs), excluding current state.
 */
function workflow_allowable_transitions($sid, $dir = 'to', $roles = 'ALL') {
  $transitions = array();
  if ($dir == 'to') {
    $field = 'target_sid';
    $field_where = 'sid';
  }
  else {
    $field = 'sid';
    $field_where = 'target_sid';
  }
  $result = db_query("(SELECT t.tid, t.%s as state_id, s.state AS state_name, s.weight AS state_weight " . "FROM {workflow_transitions} t " . "INNER JOIN {workflow_states} s " . "ON s.sid = t.%s " . "WHERE t.%s = %d AND s.status = 1 " . "ORDER BY state_weight) " . "UNION " . "(SELECT s.sid as tid, s.sid as state_id, s.state as state_name, s.weight as state_weight " . "FROM {workflow_states} s " . "WHERE s.sid = %d AND s.status = 1) " . "ORDER BY state_weight, state_id", $field, $field, $field_where, $sid, $sid);
  while ($t = db_fetch_object($result)) {
    if ($roles == 'ALL' || $sid == $t->state_id || workflow_transition_allowed($t->tid, $roles)) {
      $transitions[$t->state_id] = check_plain(t($t->state_name));
    }
  }
  return $transitions;
}

/**
 * Save mapping of workflow to node type. E.g., "the story node type
 * is using the Foo workflow."
 *
 * @param $form_state['values']
 */
function workflow_types_save($form_values) {
  db_query("DELETE FROM {workflow_type_map}");
  $node_types = node_get_types();
  foreach ($node_types as $type => $name) {
    db_query("INSERT INTO {workflow_type_map} (type, wid) VALUES ('%s', %d)", $type, $form_values[$type]['workflow']);
    variable_set('workflow_' . $type, array_keys(array_filter($form_values[$type]['placement'])));
  }
}

/**
 * Get the tid of a transition, if it exists.
 *
 * @param int $from
 *   ID (sid) of originating state.
 * @param int $to
 *   ID (sid) of target state.
 * @return int
 *   Tid or FALSE if no such transition exists.
 */
function workflow_get_transition_id($from, $to) {
  return db_result(db_query("SELECT tid FROM {workflow_transitions} WHERE sid= %d AND target_sid= %d", $from, $to));
}

/**
 * Put a node into a state.
 * No permission checking here; only call this from other functions that know
 * what they're doing.
 *
 * @see workflow_execute_transition()
 *
 * @param object $node
 * @param int $sid
 */
function _workflow_node_to_state($node, $sid, $comment = NULL) {
  global $user;
  $node->workflow_stamp = time();
  if (db_result(db_query("SELECT nid FROM {workflow_node} WHERE nid = %d", $node->nid))) {
    db_query("UPDATE {workflow_node} SET sid = %d, uid = %d, stamp = %d WHERE nid = %d", $sid, $user->uid, $node->workflow_stamp, $node->nid);
  }
  else {
    db_query("INSERT INTO {workflow_node} (nid, sid, uid, stamp) VALUES (%d, %d, %d, %d)", $node->nid, $sid, $user->uid, $node->workflow_stamp);
  }
  _workflow_write_history($node, $sid, $comment);
}
function _workflow_write_history($node, $sid, $comment) {
  global $user;
  db_query("INSERT INTO {workflow_node_history} (nid, old_sid, sid, uid, comment, stamp) VALUES (%d, %d, %d, %d, '%s', %d)", $node->nid, $node->_workflow, $sid, $user->uid, $comment, $node->workflow_stamp);
}

/**
 * Get a list of roles.
 *
 * @return
 *   Array of role names keyed by role ID, including the 'author' role.
 */
function workflow_get_roles() {
  static $roles = NULL;
  if (!$roles) {
    $result = db_query('SELECT * FROM {role} ORDER BY name');
    $roles = array(
      'author' => 'author',
    );
    while ($data = db_fetch_object($result)) {
      $roles[$data->rid] = check_plain($data->name);
    }
  }
  return $roles;
}

/**
 * Implementation of hook_cron().
 */
function workflow_cron() {
  $clear_cache = FALSE;

  // If the time now is greater than the time to execute a
  // transition, do it.
  $nodes = db_query('SELECT * FROM {workflow_scheduled_transition} s WHERE s.scheduled > 0 AND s.scheduled < %d', time());
  while ($row = db_fetch_object($nodes)) {
    $node = node_load($row->nid);

    // Make sure transition is still valid; i.e., the node is
    // still in the state it was when the transition was scheduled.
    if ($node->_workflow == $row->old_sid) {

      // Do transition.
      workflow_execute_transition($node, $row->sid, $row->comment, TRUE);
      watchdog('content', '%type: scheduled transition of %title.', array(
        '%type' => t($node->type),
        '%title' => $node->title,
      ), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
      $clear_cache = TRUE;
    }
    else {

      // Node is not in the same state it was when the transition
      // was scheduled. Defer to the node's current state and
      // abandon the scheduled transition.
      db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
    }
  }
  if ($clear_cache) {

    // Clear the cache so that if the transition resulted in a node
    // being published, the anonymous user can see it.
    cache_clear_all();
  }
}

/**
 * Implementation of hook_token_values().
 */
function workflow_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case 'node':
    case 'workflow':
      $node = (object) $object;
      if ($wid = workflow_get_workflow_for_type($node->type)) {
        $values['workflow-name'] = workflow_get_name($wid);
        $states = workflow_get_states($wid);
      }
      else {
        break;
      }
      $result = db_query_range("SELECT h.* FROM {workflow_node_history} h WHERE nid = %d ORDER BY stamp DESC", $node->nid, 0, 1);
      if ($row = db_fetch_object($result)) {
        $account = user_load(array(
          'uid' => $row->uid,
        ));
        $comment = $row->comment;
      }
      if (isset($node->workflow) && !isset($node->workflow_stamp)) {

        // The node is being submitted but the form data has not been saved to the database yet,
        // so we set the token values from the workflow form fields.
        $sid = $node->workflow;
        $old_sid = isset($row->sid) ? $row->sid : _workflow_creation_state($wid);
        $date = time();
        $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
        $uid = $node->uid;
        $mail = $node->uid ? $node->user_mail : '';
        $comment = isset($node->workflow_comment) ? $node->workflow_comment : '';
      }
      else {
        if (!isset($node->workflow) && empty($row->sid)) {

          // If the state is not specified and the node has no workflow history,
          // the node is being inserted and will soon be transitioned to the first valid state.
          // We find this state using the same logic as workflow_nodeapi().
          $choices = workflow_field_choices($node);
          $keys = array_keys($choices);
          $sid = array_shift($keys);
          $old_sid = _workflow_creation_state($wid);
          $date = time();
          $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
          $uid = $node->uid;
          $mail = $node->uid ? $node->user_mail : '';
          $comment = isset($node->workflow_comment) ? $node->workflow_comment : '';
        }
        else {

          // Default to the most recent transition data in the workflow history table.
          $sid = $row->sid;
          $old_sid = $row->old_sid;
          $date = $row->stamp;
          $user_name = $account->uid ? $account->name : variable_get('anonymous', 'Anonymous');
          $uid = $account->uid;
          $mail = $account->uid ? $account->mail : '';
        }
      }
      $values['workflow-current-state-name'] = $states[$sid];
      $values['workflow-old-state-name'] = $states[$old_sid];
      $values['workflow-current-state-date-iso'] = date('Ymdhis', $date);
      $values['workflow-current-state-date-tstamp'] = $date;
      $values['workflow-current-state-date-formatted'] = date('M d, Y h:i:s', $date);
      $values['workflow-current-state-updating-user-name'] = check_plain($user_name);
      $values['workflow-current-state-updating-user-uid'] = $uid;
      $values['workflow-current-state-updating-user-mail'] = check_plain($mail);
      $values['workflow-current-state-log-entry'] = filter_xss($comment, array(
        'a',
        'em',
        'strong',
      ));
      break;
  }
  return $values;
}

/**
 * Implementation of hook_token_list().
 */
function workflow_token_list($type = 'all') {
  $tokens = array();
  if ($type == 'workflow' || $type == 'node' || $type == 'all') {
    $tokens['workflow']['workflow-name'] = 'Name of workflow appied to this node';
    $tokens['workflow']['workflow-current-state-name'] = 'Current state of content';
    $tokens['workflow']['workflow-old-state-name'] = 'Old state of content';
    $tokens['workflow']['workflow-current-state-date-iso'] = 'Date of last state change (ISO)';
    $tokens['workflow']['workflow-current-state-date-tstamp'] = 'Date of last state change (timestamp)';
    $tokens['workflow']['workflow-current-state-date-formatted'] = 'Date of last state change (formated - M d, Y h:i:s)';
    $tokens['workflow']['workflow-current-state-updating-user-name'] = 'Username of last state changer';
    $tokens['workflow']['workflow-current-state-updating-user-uid'] = 'uid of last state changer';
    $tokens['workflow']['workflow-current-state-updating-user-mail'] = 'email of last state changer';
    $tokens['workflow']['workflow-current-state-log-entry'] = 'Last workflow comment log';
    $tokens['node'] = $tokens['workflow'];
  }
  return $tokens;
}

/**
 * Implementation of hook_content_extra_fields().
 */
function workflow_content_extra_fields($type_name) {
  $extra = array();
  if (!in_array('node', variable_get('workflow_' . $type_name, array(
    'node',
  )))) {
    return;
  }
  $extra['workflow'] = array(
    'label' => t('Workflow'),
    'description' => t('Workflow module form'),
    'weight' => 10,
  );
  return $extra;
}

/**
 * Implementation of hook_user().
 */
function workflow_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'delete':
      db_query("UPDATE {workflow_node} SET uid = 0 WHERE uid = %d", $account->uid);
      db_query("UPDATE {workflow_node_history} SET uid = 0 WHERE uid = %d", $account->uid);
      break;
  }
}

Functions

Namesort descending Description
workflow_allowable_transitions Get allowable transitions for a given workflow state. Typical use:
workflow_comment Implementation of hook_comment().
workflow_content_extra_fields Implementation of hook_content_extra_fields().
workflow_create Create a workflow and its (creation) state.
workflow_cron Implementation of hook_cron().
workflow_deletewf Delete a workflow from the database. Deletes all states, transitions and node type mappings, too. Removes workflow state information from nodes participating in this workflow.
workflow_execute_transition Execute a transition (change state of a node).
workflow_field_choices Get the states current user can move to for a given node.
workflow_form_alter Implementation of hook_form_alter().
workflow_get_all Get names and IDs of all workflows from the database.
workflow_get_name Given the ID of a workflow, return its name.
workflow_get_roles Get a list of roles.
workflow_get_state Given the ID of a workflow state, return a keyed array representing the state.
workflow_get_states Load workflow states for a workflow from the database. If $wid is not passed, all states for all workflows are given. States that have been deleted are not included.
workflow_get_state_name Given the ID of a state, return its name.
workflow_get_transition_id Get the tid of a transition, if it exists.
workflow_get_workflow_for_type Get ID of a workflow for a node type.
workflow_is_system_state Tell caller whether a state is a protected system state, such as the creation state.
workflow_load Load function.
workflow_menu Implementation of hook_menu().
workflow_nodeapi Implementation of hook_nodeapi().
workflow_node_current_state Get the current state of a given node.
workflow_node_form Form builder. Add form widgets for workflow change to $form.
workflow_node_tab_access Menu access control callback. Determine access to Workflow tab.
workflow_perm Implementation of hook_perm().
workflow_state_delete Delete a workflow state from the database, including any transitions the state was involved in and any associations with actions that were made to that transition.
workflow_state_save Add or update a workflow state to the database.
workflow_theme Implementation of hook_theme().
workflow_token_list Implementation of hook_token_list().
workflow_token_values Implementation of hook_token_values().
workflow_transition Validate target state and either execute a transition immediately or schedule a transition to be executed later by cron.
workflow_transition_add_role Add a role to the list of those allowed for a given transition.
workflow_transition_allowed See if a transition is allowed for a given role.
workflow_transition_delete Delete a transition (and any associated actions).
workflow_transition_delete_role Remove a role from the list of those allowed for a given transition.
workflow_types_save Save mapping of workflow to node type. E.g., "the story node type is using the Foo workflow."
workflow_update Save a workflow's name in the database.
workflow_update_transitions Update the transitions for a workflow.
workflow_user Implementation of hook_user().
_workflow_creation_state Return the ID of the creation state for this workflow.
_workflow_node_to_state Put a node into a state. No permission checking here; only call this from other functions that know what they're doing.
_workflow_write_history

Constants

Namesort descending Description
WORKFLOW_CREATION @file Support workflows made up of arbitrary states.
WORKFLOW_CREATION_DEFAULT_WEIGHT
WORKFLOW_DELETION