<?php
// $Id: workflow.module,v 1.54.2.34 2008/07/19 01:45:08 jvandyk Exp $

define('WORKFLOW_CREATION', 1);
define('WORKFLOW_DELETION', -1);
define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
define('WORKFLOW_ARROW', '&#8594;');

/**
 * Implementation of hook_help().
 */
function workflow_help($section) {
  switch ($section) {
    case strstr($section, 'admin/build/workflow/edit'):
      return t('You are currently viewing the possible transitions to and from workflow states. The state is shown in the left column; the state to be moved to is to the right. For each transition, check the box next to the role(s) that may initiate the transition. For example, if only the editor role may move a node from Review state to the Published state, check the box next to editor. The author role is built in and refers to the user who authored the node. For a summary of which role may do which transition, look at the bottom of this page.');
    case 'admin/build/workflow/add':
      return t('To get started, provide a name for your workflow. This name will be used as a label when the workflow status is shown during node editing.');
    case strstr($section, 'admin/build/workflow/state') && !strstr($section, 'delete'):
      return t('Enter the name for a state in your workflow. For example, if you were doing a meal workflow it may include states like <em>shop</em>, <em>prepare food</em>, <em>eat</em>, and <em>clean up</em>.');
    case strstr($section, 'admin/build/workflow/actions') && (count($section) == 22):
      return t('Use this page to set actions to happen when transitions occur. To <a href="@link">configure actions</a>, use the actions module.', array('@link' => url('admin/actions')));
  }
}

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

/**
 * Implementation of hook_menu().
 */
function workflow_menu($may_cache) {
  $items = array();
  $access = user_access('administer workflow');

  if ($may_cache) {
    $items[] = array('path' => 'admin/build/workflow',
      'title'    => t('Workflow'),
      'access'   => $access,
      'callback' => 'workflow_overview',
      'description' => t('Allows the creation and assignment of arbitrary workflows to content types.'));

    $items[] = array('path' => 'admin/build/workflow/edit',
      'title'    => t('Edit workflow'),
      'type'     => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_edit_form'));

    $items[] = array('path' => 'admin/build/workflow/list',
      'title'    => t('List'),
      'weight'   => -10,
      'callback' => 'workflow_page',
      'type'     => MENU_DEFAULT_LOCAL_TASK);

    $items[] = array('path' => 'admin/build/workflow/add',
      'title'    => t('Add workflow'),
      'weight'   => -8,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_add_form'),
      'type'     => MENU_LOCAL_TASK);

    $items[] = array('path' => 'admin/build/workflow/state',
      'title'    => t('Add state'),
      'type'     => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_state_add_form'));

    $items[] = array('path' => 'admin/build/workflow/state/delete',
      'title'    => t('Delete State'),
      'type'     => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_state_delete_form'));

    $items[] = array('path' => 'admin/build/workflow/delete',
      'title'    => t('Delete workflow'),
      'type'     => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_delete_form')
    );

    // Add triggers page with multilevel local tasks.
    $items[] = array(
      'path' => 'admin/build/trigger/workflow',
      'title' => t('Workflow'),
      'callback' => 'actions_assign',
      'callback arguments' => array('workflow'),
      'access' => $access,
      'type' => MENU_LOCAL_TASK,
    );
    if (workflow_get_states()) {
      // Add a subtab for each workflow.
      $result = db_query("SELECT wid, name FROM {workflows}");
      $default_assigned = FALSE;
      while ($data = db_fetch_object($result)) {
        $items[] = array(
          'path' => 'admin/build/trigger/workflow/'. $data->wid,
          'title' => check_plain($data->name),
          'callback' => 'actions_assign',
          'callback arguments' => array('workflow'),
          'access' => $access,
          'type' => MENU_LOCAL_TASK,
//          'type' => $default_assigned ? MENU_LOCAL_TASK : MENU_DEFAULT_LOCAL_TASK,
        );
        $default_assigned = TRUE;
      }
    }
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      $wid = workflow_get_workflow_for_type($node->type);
      // If a workflow has been assigned to this node type.
      if ($wid) {
        global $user;
        $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();
        $items[] = array('path' => "node/$node->nid/workflow",
          'title'    => t('Workflow'),
          'access'   => array_intersect($roles, $allowed_roles) || user_access('administer nodes'),
          'type'     => MENU_LOCAL_TASK,
          'weight'   => 2,
          'callback' => 'workflow_tab_page',
          'callback arguments' => arg(1),
        );
      }
    }
  }
  return $items;
}

/**
 * Menu callback. Displays the workflow information for a node.
 *
 * @param $nid
 *   Node ID of node for which workflow information will be displayed.
 * @return unknown
 */
function workflow_tab_page($nid) {
  $node = node_load($nid);
  drupal_set_title(check_plain($node->title));
  $wid = workflow_get_workflow_for_type($node->type);
  $states_per_page = variable_get('workflow_states_per_page', 20);
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 1 ORDER BY sid");
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }
  $deleted_states = array();
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 0 ORDER BY sid");
  while ($data = db_fetch_object($result)) {
    $deleted_states[$data->sid] = $data->state;
  }
  $current = workflow_node_current_state($node);
 
  // theme_workflow_current_state() must run state through check_plain().
  $output = '<p>'. t('Current state: !state', array('!state' => theme('workflow_current_state', $states[$current]))) . "</p>\n";
  $output .= drupal_get_form('workflow_tab_form', $node, $wid, $states, $current);

  $result = pager_query("SELECT h.*, u.name FROM {workflow_node_history} h LEFT JOIN {users} u ON h.uid = u.uid WHERE nid = %d ORDER BY hid DESC", $states_per_page, 0, NULL, $nid);
  $rows = array();
  while ($history = db_fetch_object($result)) {
    if ($history->sid == $current && !isset($deleted_states[$history->sid]) && !isset($current_themed)) {
      // Theme the current state differently so it stands out.
      $state_name = theme('workflow_current_state', $states[$history->sid]);
      // Make a note that we have themed the current state; other times in the history
      // of this node where the node was in this state do not need to be specially themed.
      $current_themed = TRUE;
    }
    elseif (isset($deleted_states[$history->sid])) {
      // The state has been deleted, but we include it in the history.
      $state_name = theme('workflow_deleted_state', $deleted_states[$history->sid]);
      $footer_needed = TRUE;
    }
    else {
      // Regular state.
      $state_name = check_plain($states[$history->sid]);
    }

    if (isset($deleted_states[$history->old_sid])) {
      $old_state_name = theme('workflow_deleted_state', $deleted_states[$history->old_sid]);
      $footer_needed = TRUE;
    }
    else {
      $old_state_name = check_plain($states[$history->old_sid]);
    }
    $rows[] = theme('workflow_history_table_row', $history, $old_state_name, $state_name);
  }
  $output .= theme('workflow_history_table', $rows, !empty($footer_needed));  
  $output .= theme('pager', $states_per_page);
  return $output;
}

/*
 * Theme one workflow history table row.
 */
function theme_workflow_history_table_row($history, $old_state_name, $state_name) {
  return array(
    format_date($history->stamp),
    $old_state_name,
    $state_name,
    theme('username', $history),
    filter_xss($history->comment, array('a', 'em', 'strong')),
  );
}

/*
 * Theme entire workflow history table.
 */
function theme_workflow_history_table($rows, $footer) {
  $output = theme('table', array(t('Date'), t('Old State'), t('New State'), t('By'), t('Comment')), $rows, array('class' => 'workflow_history'), t('Workflow History'));
  if ($footer) {
    $output .= t('*State is no longer available.');
  }
  return $output;
}

/**
 * Theme the current state in the workflow history table.
 */
function theme_workflow_current_state($state_name) {
  return '<strong>'. check_plain($state_name) .'</strong>';
}

/**
 * Theme a deleted state in the workflow history table.
 */
function theme_workflow_deleted_state($state_name) {
  return check_plain($state_name) .'*';
}

/**
 * Creates form definition of the workflow editing form.
 *
 * @param $node
 *   Node for which workflow information will be displayed.
 * @param $wid
 *   The workflow ID.
 * @param unknown_type $states
 *   Array of states for the workflow.
 * @param $current
 *   The current state that this node is in.
 * @return
 *   Form definition.
 */
function workflow_tab_form(&$node, $wid, $states, $current) {
  $form = array();
  $choices = workflow_field_choices($node);

  $min = $states[$current] == t('(creation)') ? 1 : 2;
  // Only build form if user has possible target state(s).
  if (count($choices) >= $min) {
    $wid = workflow_get_workflow_for_type($node->type);
    $name = check_plain(workflow_get_name($wid));
    // See if scheduling information is present.
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
      // The default value should be the upcoming sid.
      $current = $node->_workflow_scheduled_sid;
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }

    workflow_node_form($form, t('Change %s state', array('%s' => $name)), $name, $current, $choices, $timestamp, $comment);

    $form['node'] = array(
      '#type' => 'value',
      '#value' => $node,
    );

    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit')
    );
  }

  return $form;
}

/**
 * Submit handler for workflow page.
 */
function workflow_tab_form_submit($form_id, $form_values) {
  $node = $form_values['node'];

  // Mockup a node so we don't need to repeat the code for processing this.
  $node->workflow = $form_values['workflow'];
  $node->workflow_comment = $form_values['workflow_comment'];
  $node->workflow_scheduled = $form_values['workflow_scheduled'];
  $node->workflow_scheduled_date = $form_values['workflow_scheduled_date'];
  $node->workflow_scheduled_hour = $form_values['workflow_scheduled_hour'];

  // Call node_save() to make sure all saving properties run on this node.
  node_save($node);

  // Redirect user from tab page to node itself.
  return 'node/' . $node->nid;
}

/**
 * Implementation of hook_nodeapi().
 */
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {

    case 'load':
      // Add workflow information to the node.
      $node->_workflow = workflow_node_current_state($node);

      // Add scheduling information to the node.
      $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':
      db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid);
      _workflow_write_history($node, WORKFLOW_DELETION, t('Node deleted'));
      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'];
    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))) {
    if (!$node->workflow_scheduled) {
      // It's an immediate change. Do the transition.
      workflow_execute_transition($node, $sid, $node->workflow_comment);
    }
    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);

        drupal_set_message(t("@node_title is scheduled for state change on !scheduled_date", array( "@node_title" => $node->title, "!scheduled_date" => format_date($scheduled))));
      }
    }
  }
}

/**
 * Add the actual form widgets for workflow change to a form definition.
 *
 * @param $form
 *   A form definition array.
 * @param $name
 *   The name of the workflow.
 * @param $current
 *   The state ID of the current state.
 * @param $choices
 *   An array of possible target states.
 */
function workflow_node_form(&$form, $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' => $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' => $scheduled,
      );

      $form['workflow']['workflow_scheduled_date'] = array(
        '#type' => 'date',
        '#default_value' => array(
          'day'   => format_date($timestamp, 'custom', 'j'),
          'month' => format_date($timestamp, 'custom', 'n'),
          '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 ? $hours : NULL,
      );
    }

    $form['workflow']['workflow_comment'] = array(
      '#type' => 'textarea',
      '#title' => t('Comment'),
      '#description' => t('A comment to put in the workflow log.'),
      '#default_value' => $comment,
      '#rows' => 2,
    );
  }
}

/**
 * Modify the node form to add the workflow field.
 */
function workflow_form_alter($form_id, &$form) {
  // Ignore all forms except comment forms and node editing forms.
  if ($form_id == 'comment_form' || isset($form['type']) && $form['#base'] == 'node_form') {
    if (isset($form['#node'])) {
      $node = $form['#node'];
      // Abort if user does not want to display workflow form on node editing form.
      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);
    $current = workflow_node_current_state($node);
    $min = $states[$current] == t('(creation)') ? 1 : 2;
    // Bail out if user has no new target state(s).
    if (count($choices) < $min) {
      return;
    }

    $name = check_plain(workflow_get_name($wid));

    // 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,
      );
    }

    // See if scheduling information is present.
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
      // The default value should be the upcoming sid.
      $current = $node->_workflow_scheduled_sid;
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }

    workflow_node_form($form, $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
 *   ID of new state.
 */
function workflow_execute_transition(&$node, $sid, $comment = NULL, $force = FALSE) {
  $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);
    }
      $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
    return;
  }

  $tid = workflow_get_transition_id($old_sid, $sid);
  if (!$tid) {
    watchdog('workflow', t('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.
  global $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', t('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', t('Transition vetoed by module.'));
    return;
  }

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

  // Register state change with watchdog.
  $state_name = db_result(db_query('SELECT state FROM {workflow_states} WHERE sid = %d', $sid));
  $type = node_get_types('name', $node->type);
  watchdog('workflow', t('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);
}

/**
 * Implementation of hook-action_info().
 */
function workflow_action_info() {
  return array(
    'workflow_select_next_state_action' => array(
      'type' => 'node',
      'description' => t('Change workflow state of post to next state'),
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('presave'),
        'comment' => array('insert', 'update'),
        'workflow' => array('any'),
      ),
    ),
    'workflow_select_given_state_action' => array(
      'type' => 'node',
      'description' => t('Change workflow state of post to new state'),
      'configurable' => TRUE,
      'hooks' => array(
        'nodeapi' => array('presave'),
        'comment' => array('insert', 'update'),
        'workflow' => array('any'),
      ),
    ),
  );
}

/**
 * Implementation of a Drupal action. Move a node to the next state in the workfow.
 */
function workflow_select_next_state_action($node, $context) {
  // If this action is being fired because it's attached to a workflow transition
  // then the node's new state (now its current state) should be in $node->workflow
  // because that is where the value from the workflow form field is stored;
  // otherwise the current state is placed in $node->_workflow by our nodeapi load.
  if (!isset($node->workflow) && !isset($node->_workflow)) {
    watchdog('workflow', t('Unable to get current workflow state of node %nid.', array('%nid' => $node->nid)));
    return;
  }
  $current_state = isset($node->workflow) ? $node->workflow : $node->_workflow;

  // Get the node's new state.
  $choices = workflow_field_choices($node);
  foreach ($choices as $sid => $name) {
    if (isset($flag)) {
      $new_state = $sid;
      $new_state_name = $name;
      break;
    }
    if ($sid == $current_state) {
      $flag = TRUE;
    }
  }

  // Fire the transition
  workflow_execute_transition($node, $new_state);
}

/**
 * Implementation of a Drupal action. Move a node to a specified state.
 */
function workflow_select_given_state_action($node, $context) {
  $comment = t($context['workflow_comment'], array('%title' => check_plain($node->title), '%state' => check_plain($context['state_name'])));
  workflow_execute_transition($node, $context['target_sid'], $comment, $context['force']);
}

function workflow_select_given_state_action_form($context) {
  $result = db_query("SELECT * FROM {workflow_states} ws LEFT JOIN {workflows} w ON ws.wid = w.wid WHERE ws.sysid = 0 AND ws.status = 1 ORDER BY ws.wid, ws.weight");
  $previous_workflow = '';
  $options = array();
  while ($data = db_fetch_object($result)) {
    $options[$data->name][$data->sid] = $data->state;
  }
  $form['target_sid'] = array(
    '#type' => 'select',
    '#title' => t('Target state'),
    '#description' => t('Please select that state that should be assigned when this action runs.'),
    '#default_value' => isset($context['target_sid']) ? $context['target_sid'] : '',
    '#options' => $options,
  );
  $form['force'] = array(
    '#type' => 'checkbox',
    '#title' => t('Force transition'),
    '#description' => t('If this box is checked, the new state will be assigned even if workflow permissions disallow it.'),
    '#default_value' => isset($context['force']) ? $context['force'] : '',
  );
  $form['workflow_comment'] = array(
    '#type' => 'textfield',
    '#title' => t('Message'),
    '#description' => t('This message will be written into the workflow history log when the action runs. You may include the following variables: %state, %title'),
    '#default_value' => isset($context['workflow_history']) ? $context['workflow_history'] : t('Action set %title to %state.'),
  );
  return $form;
}

function workflow_select_given_state_action_submit($form_id, $form_values) {
  $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $form_values['target_sid']));
  return array(
    'target_sid' => $form_values['target_sid'],
    'state_name' => $state_name,
    'force' => $form_values['force'],
    'workflow_comment' => $form_values['workflow_comment'],
  );
}


/**
 * Get the states one can move to for a given node.
 *
 * @param object $node
 * @return array
 */
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 object $node
 * @return string state ID
 */
function workflow_node_current_state($node) {
  $sid = db_result(db_query('SELECT sid FROM {workflow_node} WHERE nid = %d', $node->nid));

  if (!$sid) {
    $wid = workflow_get_workflow_for_type($node->type);
    $sid = _workflow_creation_state($wid);
  }
  return $sid;
}

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];
}

/**
 * Implementation of hook_workflow().
 */
function workflow_workflow($op, $old_state, $new_state, $node) {
  switch ($op) {
    case 'transition pre':
      break;

    case 'transition post':
      // A transition has occurred; fire off actions associated with this transition.
      // Can't fire actions if actions module is not enabled.
      if (!function_exists('actions_do')) {
        break;
      }
      $tid = workflow_get_transition_id($old_state, $new_state);
      $op = 'workflow-'. $node->type .'-'. $tid;
      $aids = _actions_get_hook_aids('workflow', $op);
      if ($aids) {
        $context = array(
          'hook' => 'workflow',
          'op' => $op,
        );

        // We need to get the expected object if the action's type is not 'node'.
        // We keep the object in $objects so we can reuse it if we have multiple actions
        // that make changes to an object.
        foreach ($aids as $aid => $action_info) {
          if ($action_info['type'] != 'node') {
            if (!isset($objects[$action_info['type']])) {
              $objects[$action_info['type']] = _actions_normalize_node_context($action_info['type'], $node);
           }
           // Since we know about the node, we pass that info along to the action.
           $context['node'] = $node;
           $result = actions_do($aid, $objects[$action_info['type']], $context);
         }
         else {
           actions_do($aid, $node, $context);
          }
        }
      }
      break;
  }
}

/**
 * Create the form for adding/editing a workflow.
 *
 * @param $name
 *   Name of the workflow if editing.
 * @param $add
 *   Boolean, if true edit workflow name.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_add_form($name = NULL) {
  $form = array();
  $form['wf_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Workflow Name'),
    '#maxlength' => '254',
    '#default_value' => $name
    );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Add Workflow'));

  return $form;
}

function workflow_add_form_validate($form_id, $form_values) {
  $workflow_name = $form_values['wf_name'];
  $workflows = array_flip(workflow_get_all());
  // Make sure a nonblank workflow name is provided.
  if ($workflow_name == '') {
    form_set_error('wf_name', t('Please provide a nonblank name for the new workflow.'));
  }
  // Make sure workflow name is not a duplicate.
  if (array_key_exists($workflow_name, $workflows)) {
    form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for your new workflow.',
      array('%name' => $workflow_name)));
  }
}

function workflow_add_form_submit($form_id, $form_values) {
  $workflow_name = $form_values['wf_name'];
  if (array_key_exists('wf_name', $form_values) && $workflow_name != '')  {
    workflow_create($workflow_name);
    watchdog('workflow', t('Created workflow %name', array('%name' => $workflow_name)));
    drupal_set_message(t('The workflow %name was created. You should now add states to your workflow.',
      array('%name' => $workflow_name)), 'warning');
    return ('admin/build/workflow');
  }
}

/**
 * Create the form for confirmation of deleting a workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_delete_form($wid, $sid = NULL) {
  if (isset($sid)) {
    return workflow_state_delete_form($wid, $sid);
  }

  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  return confirm_form(
    $form,
    t('Are you sure you want to delete %title? All nodes that have a workflow state associated with this workflow will have those workflow states removed.', array('%title' => workflow_get_name($wid))),
    $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow',
    t('This action cannot be undone.'),
    t('Delete'),
    t('Cancel')
  );
}

function workflow_delete_form_submit($form_id, $form_values) {
  if ($form_values['confirm'] == 1) {
    $workflow_name = workflow_get_name($form_values['wid']);
    workflow_deletewf($form_values['wid']);
    watchdog('workflow', t('Deleted workflow %name with all its states', array('%name' => $workflow_name)));
    drupal_set_message(t('The workflow %name with all its states was deleted.', array('%name' => $workflow_name)));
    return ('admin/build/workflow');
  }
}

/**
 * View workflow permissions by role
 *
 * @param $wid
 *   The ID of the workflow
 */
function workflow_permissions($wid) {
  $name = workflow_get_name($wid);
  $all = array();
  $roles = array('author' => t('author')) + user_roles();
  foreach ($roles as $role => $value) {
    $all[$role]['name'] = $value;
  }
  $result = db_query(
    "SELECT t.roles, s1.state AS state_name, s2.state AS target_state_name "
  . "FROM {workflow_transitions} t "
  . "INNER JOIN {workflow_states} s1 ON s1.sid = t.sid "
  . "INNER JOIN {workflow_states} s2 ON s2.sid = t.target_sid "
  . "WHERE s1.wid = %d AND s1.status = 1 "
  . "ORDER BY s1.weight ASC , s1.state ASC , s2.weight ASC , s2.state ASC",
    $wid);
  while ($data = db_fetch_object($result)) {
    foreach (explode(',', $data->roles) as $role) {
      $all[$role]['transitions'][] = array($data->state_name, WORKFLOW_ARROW, $data->target_state_name);
    }
  }

  $output = '';
  $header = array(t('From'), '', t('To'));
  foreach ($all as $role => $value) {
    if ($role == 'author') {
      $output .= '<h3>'. t("The author of the post may do these transitions:") .'</h3>';
    }
    else {
      $output .= '<h3>'. t("The role %role may do these transitions:", array('%role' => $value['name'])) .'</h3>';
    }
    if ($value['transitions']) {
      $output .= theme('table', $header, $value['transitions']) . '<p></p>';
    }
    else {
      $output .= '<table><tbody><tr class="even"><td>' . t('None') . '</td><td></tr></tbody></table><p></p>';
    }
  }
  return $output;
}

/**
 * Menu callback to edit a workflow's properties.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 */
function workflow_edit_form($wid) {
  $workflow = db_fetch_object(db_query('SELECT * FROM {workflows} WHERE wid = %d', $wid));
  $form['wid'] = array(
    '#type' => 'value',
    '#value' => $workflow->wid,
  );
  $form['basic'] = array(
    '#type' => 'fieldset',
    '#title' => t('Workflow information')
  );
  $form['basic']['wf_name'] = array(
    '#type' => 'textfield',
    '#default_value' => $workflow->name,
    '#title' => t('Workflow Name'),
    '#size' => '16',
    '#maxlength' => '254',
  );
  $form['tab'] = array(
    '#type' => 'fieldset',
    '#title' => t('Workflow tab permissions'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['tab']['tab_roles'] = array(
    '#type' => 'checkboxes',
    '#options' => workflow_get_roles(),
    '#default_value' => explode(',', $workflow->tab_roles),
    '#description' => t('Select any roles that should have access to the workflow tab on nodes that have a workflow.'),
  );

  $form['transitions'] = workflow_transition_grid_form($workflow->wid);
  $form['transitions']['#tree'] = TRUE;

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save')
  );

  $form['permissions'] = array(
    '#type' => 'fieldset',
    '#title' => t('Permissions Summary'),
  );
  $form['permissions']['summary'] = array(
    '#value' => workflow_permissions($workflow->wid),
  );

  return $form;
}

function theme_workflow_edit_form($form) {
  $output = drupal_render($form['wf_name']);
  $wid = $form['wid']['#value'];
  $states = workflow_get_states($wid);
  drupal_set_title(t('Edit workflow %name', array('%name' => workflow_get_name($wid))));
  if ($states) {
    $roles = workflow_get_roles();
    $header = array(array('data' => t('From / To ') . '&nbsp;' . WORKFLOW_ARROW));
    $rows = array();
    foreach ($states as $state_id => $name) {
      // Don't allow transition TO (creation).
      if ($name != t('(creation)')) {
        $header[] = array('data' => t($name));
      }
      $row = array(array('data' => $name));
      foreach ($states as $nested_state_id => $nested_name) {
        if ($nested_name == t('(creation)')) {
          // Don't allow transition TO (creation).
          continue;
        }
        if ($nested_state_id != $state_id) {
          // Need to render checkboxes for transition from $state to $nested_state.
          $from = $state_id;
          $to = $nested_state_id;
          $cell = '';
          foreach ($roles as $rid => $role_name) {
            $cell .= drupal_render($form['transitions'][$from][$to][$rid]);
          }
          $row[] = array('data' => $cell);
        }
        else {
          $row[] = array('data' => '');
        }
      }
      $rows[] = $row;
    }
    $output .= theme('table', $header, $rows);

  }
  else {
    $output = t('There are no states defined for this workflow.');
  }

  $output .= drupal_render($form);
  return $output;
}

function workflow_edit_form_validate($form_id, $form_values) {
  $wid = $form_values['wid'];
  if (array_key_exists('wf_name', $form_values) && $form_values['wf_name'] != '') {
    // Validate: Make sure workflow name is not a duplicate.
    $workflow_name = $form_values['wf_name'];
    $workflows = array_flip(workflow_get_all());
    if (array_key_exists($workflow_name, $workflows) && $wid != $workflows[$workflow_name]) {
      form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for this workflow.',
        array('%name' => $workflow_name)));
    }
  }
  // Validate: Make sure a nonblank workflow name is provided
  else {
    form_set_error('wf_name', t('Please provide a nonblank name for this workflow.'));
  }

  // Make sure 'author' is checked for (creation) -> [something].
  $creation_id = _workflow_creation_state($wid);
  if (is_array($form_values['transitions'][$creation_id])) {
    foreach ($form_values['transitions'][$creation_id] as $to => $roles) {
      if ($roles['author']) {
        $author_has_permission = true;
        break;
      }
    }
  }
  $state_count = db_result(db_query("SELECT COUNT(sid) FROM {workflow_states} WHERE wid = %d", $wid));
  if (!$author_has_permission && $state_count > 1) {
    form_set_error('transitions', t('Please give the author permission to go from %creation to at least one state!',
      array('%creation' => '(creation)')));
  }
}

function workflow_edit_form_submit($form_id, $form_values) {
  workflow_update($form_values['wid'], $form_values['wf_name'], array_filter($form_values['tab_roles']));
  workflow_update_transitions($form_values['transitions']);
  drupal_set_message(t('The workflow was updated.'));
  return ('admin/build/workflow');
}

/**
 * Menu callback to create form to add a workflow state.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_state_add_form($wid, $sid = NULL) {
  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);

  if (isset($sid)) {
    $state = workflow_get_state($sid);
    if (isset($state) && $state['status']) {
      drupal_set_title(t('Edit workflow state %state', array('%state' => $state['state'])));
      $form['sid'] = array('#type' => 'value', '#value' => $sid);
    }
  }

  // If we don't have a state or db_fetch_array returned FALSE, load defaults.
  if (!isset($state) || $state === FALSE) {
    $state = array('state' => '', 'weight' => 0);
    drupal_set_title(t('Add a new state to workflow %workflow', array('%workflow' => workflow_get_name($wid))));
  }

  $form['state'] = array(
    '#type'  => 'textfield',
    '#title' => t('State name'),
    '#default_value' => $state['state'],
    '#size'  => '16',
    '#maxlength' => '254',
    '#required' => TRUE,
    '#description' => t('Enter the name for a state in your workflow. For example, if you were doing a meal workflow '
    . 'it may include states like <em>shop</em>, <em>prepare food</em>, <em>eat</em>, and <em>clean up</em>.'),
  );
  $form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => $state['weight'],
    '#description' => t('In listings, the heavier states will sink and the lighter states will be positioned nearer the top.'),
  );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
  return $form;
}

function workflow_state_add_form_validate($form_id, $form_values) {
  $state_name = $form_values['state'];
  $wf_states = array_flip(workflow_get_states($form_values['wid']));
  if (array_key_exists('sid', $form_values)) {
    // Validate changes to existing state:
    // Make sure a nonblank state name is provided
    if ($state_name == '') {
      form_set_error('state', t('Please provide a nonblank name for this state.'));
    }
    // Make sure changed state name is not a duplicate
    if (array_key_exists($state_name, $wf_states) && $form_values['sid'] != $wf_states[$state_name]) {
      form_set_error('state', t('A state with the name %state already exists in this workflow. '
      . 'Please enter another name for this state.', array('%state' => $state_name)));
    }
  }
  else {
    // Validate new state:
    // Make sure a nonblank state name is provided
    if ($state_name == '') {
      form_set_error('state', t('Please provide a nonblank name for the new state.'));
    }
    // Make sure state name is not a duplicate
    if (array_key_exists($state_name, $wf_states)) {
      form_set_error('state', t('A state with the name %state already exists in this workflow. '
      . 'Please enter another name for your new state.', array('%state' => $state_name)));
    }
  }
}

function workflow_state_add_form_submit($form_id, $form_values) {
  workflow_state_save($form_values);
  if (array_key_exists('sid', $form_values)) {
    drupal_set_message(t('The workflow state was updated.'));
  }
  else {
    watchdog('workflow', t('Created workflow state %name', array('%name' => $form_values['state'])));
    drupal_set_message(t('The workflow state %name was created.', array('%name' => $form_values['state'])));
  }
  return ('admin/build/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) {
  $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;
      db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid);
    }
  }
  else {
    db_query("INSERT INTO {workflow_transitions} (tid, sid, target_sid, roles) VALUES (%d, %d, %d, '%s')", db_next_id('{workflow_transitions}_tid'), $from, $to, $role);
  }
}

/**
 * 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);
    }
  }
}

/**
 * Build the grid of transitions for defining a workflow.
 *
 * @param int $wid
 */
function workflow_transition_grid_form($wid) {
  $form = array();

  $roles = workflow_get_roles();
  $states = workflow_get_states($wid);
  if (!$states) {
    $form = array(
      '#type' => 'markup',
      '#value' => t('There are no states defined for this workflow.')
    );
    return $form;
  }
  foreach ($states as $state_id => $name) {
    foreach ($states as $nested_state_id => $nested_name) {
      if ($nested_name == t('(creation)')) {
        // Don't allow transition TO (creation).
        continue;
      }
      if ($nested_state_id != $state_id) {
        // Need to generate checkboxes for transition from $state to $nested_state.
        $from = $state_id;
        $to = $nested_state_id;
        foreach ($roles as $rid => $role_name) {
          $tid = workflow_get_transition_id($from, $to);
          $form[$from][$to][$rid] = array(
            '#type' => 'checkbox',
            '#title' => $role_name,
            '#default_value' => $tid ? workflow_transition_allowed($tid, $rid) : FALSE);
        }
      }
    }
  }
  return $form;
}

/**
 * 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 bool
 */
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;
  }
  else {
    return $allowed == TRUE; // allowed for anybody?
  }
}

/**
 * Create the main workflow page, which gives an overview
 * of workflows and workflow states.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_overview() {
  $workflows = workflow_get_all();
  $row = array();

  foreach ($workflows as $wid => $name) {
    $links = array(
      'workflow_overview_add_state' => array(
        'title' => t('Add state'),
        'href' => "admin/build/workflow/state/$wid"),
      'workflow_overview_actions' => array(
        'title' => t('Actions'),
        'href' => "admin/build/trigger/workflow/$wid"),
      'workflow_overview_edit' => array(
        'title' => t('Edit'),
        'href' => "admin/build/workflow/edit/$wid"),
      'workflow_overview_delete' => array(
        'title' => t('Delete'),
        'href' => "admin/build/workflow/delete/$wid"),
    );
    
    // Allow modules to insert their own workflow operations.
    $links = array_merge($links, module_invoke_all('workflow_operations', 'workflow', $wid));
    
    $states = workflow_get_states($wid);
    if (!module_exists('actions')) {
      unset($links['workflow_overview_actions']);
    }

    $row[] = array($name, theme('links', $links));
    $subrows = array();

    foreach ($states as $sid => $state_name) {
      $state_links = array();
      if (!workflow_is_system_state(t($state_name))) {
        $state_links = array(
          'workflow_overview_edit_state' => array(
            'title' => t('Edit'),
            'href' => "admin/build/workflow/state/$wid/$sid",
          ),
          'workflow_overview_delete_state' => array(
            'title' => t('Delete'),
            'href' => "admin/build/workflow/state/delete/$wid/$sid"
          ),
        );
      }
      // Allow modules to insert state operations.
      $state_links = array_merge($state_links, module_invoke_all('workflow_operations', 'state', $wid, $sid));
      $subrows[] = array(t($state_name), theme('links', $state_links));
      unset($state_links);
    }

    $subheader_state      = array('data' => t('State'), 'style' => 'width: 30%');
    $subheader_operations = array('data' => t('Operations'), 'style' => 'width: 70%');
    $subheader_style      = array('style' => 'width: 100%; margin: 3px 20px 20px;');
    $subtable = theme('table', array($subheader_state, $subheader_operations), $subrows, $subheader_style);

    $row[] = array(array('data' => $subtable, 'colspan' => '2'));
  }

  if ($row) {
    $output = theme('table', array(t('Workflow'), t('Operations')), $row);
    $output .= drupal_get_form('workflow_types_form');
  }
  else {
    $output = '<p>' . t('No workflows have been added. Would you like to <a href="@link">add a workflow</a>?', array('@link' => url('admin/build/workflow/add'))) . '</p>';
  }

  return $output;
}

/**
 * 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
 *   boolean
 *
 */
function workflow_is_system_state($state) {
  static $states;
  if (!isset($states)) {
    $states = array(t('(creation)') => TRUE);
  }
  return isset($states[$state]);
}

/**
 * Create the form for confirmation of deleting a workflow state.
 *
 * @param $wid
 *   integer The ID of the workflow.
 * @param $sid
 *   The ID of the workflow state.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_state_delete_form($wid, $sid) {
  $states = workflow_get_states($wid);
  $state_name = $states[$sid];

  // Will any nodes have no state if this state is deleted?
  if ($count = db_result(db_query("SELECT COUNT(nid) FROM {workflow_node} WHERE sid = %d", $sid))) {
    // Cannot assign a node to (creation) because that implies
    // that the node does not exist.
    $key = array_search(t('(creation)'), $states);
    unset($states[$key]);

    // Don't include the state to be deleted in our list of possible
    // states that can be assigned.
    unset($states[$sid]);
    $form['new_sid'] = array(
      '#type' => 'select',
      '#title' => t('State to be assigned to orphaned nodes'),
      '#description' => format_plural($count, 'Since you are deleting a workflow state, @count node which is in that state will be orphaned, and must be reassigned to a new state. Please choose the new state.', 'Since you are deleting a workflow state, @count nodes which are in that state will be orphaned, and must be reassigned to a new state. Please choose the new state.'),
      '#options' => $states,
    );
  }

  $form['wid'] = array(
    '#type' => 'value',
    '#value' => $wid
  );
  $form['sid'] = array(
    '#type' => 'value',
    '#value' => $sid
  );

  return confirm_form(
    $form,
    t('Are you sure you want to delete %title (and all its transitions)?', array('%title' => $state_name)),
    $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow',
    t('This action cannot be undone.'),
    t('Delete'),
    t('Cancel')
  );
}

function workflow_state_delete_form_submit($form_id, $form_values) {
  $states = workflow_get_states($form_values['wid']);
  $state_name = $states[$form_values['sid']];
  $new_sid = isset($form_values['new_sid']) ? $form_values['new_sid'] : NULL;
  if ($form_values['confirm'] == 1) {
    workflow_state_delete($form_values['sid'], $new_sid);
    watchdog('workflow', t('Deleted workflow state %name', array('%name' => $state_name)));
    drupal_set_message(t('The workflow state %name was deleted.', array('%name' => $state_name)));
  }
  return ('admin/build/workflow');
}

function workflow_types_form() {
  $form = array();

  $workflows = array('<' . t('None') . '>') + workflow_get_all();
  if (count($workflows) == 0) {
    return $form;
  }

  $type_map = array();
  $result = db_query("SELECT wid, type FROM {workflow_type_map}");
  while ($data = db_fetch_object($result)) {
    $type_map[$data->type] = $data->wid;
  }

  $form['#theme'] = 'workflow_types_form';
  $form['#tree'] = TRUE;
  $form['help'] = array(
    '#type' => 'item',
    '#value' => t('Each content type may have a separate workflow. The form for changing workflow state can be displayed when editing a node, editing a comment for a node, or both.'),
  );

  foreach (node_get_types('names') as $type => $name) {
    $form[$type]['workflow'] = array(
      '#type' => 'select',
      '#title' => $name,
      '#options' => $workflows,
      '#default_value' => isset($type_map[$type]) ? $type_map[$type] : 0
    );
    $form[$type]['placement'] = array(
      '#type' => 'checkboxes',
      '#options' => array(
        'node' => t('Post'),
        'comment' => t('Comment'),
      ),
      '#default_value' => variable_get('workflow_' . $type, array('node')),
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save Workflow Mapping')
  );
  return $form;
}

function theme_workflow_types_form($form) {
  $header = array(t('Content Type'), t('Workflow'), t('Display Workflow Form for:'));
  $rows = array();
  foreach (node_get_types('names') as $type => $name) {
  //foreach (element_children($form) as $key) {
  //  if (in_array($form[$key]['#type'], array('select', 'checkboxes'))) {
      $name = $form[$type]['workflow']['#title'];
      unset($form[$type]['workflow']['#title']);
      $rows[] = array($name, drupal_render($form[$type]['workflow']), drupal_render($form[$type]['placement']));
  //  }
  }
  $output = drupal_render($form['help']);
  $output .= theme('table', $header, $rows);
  return $output . drupal_render($form);
}

function workflow_types_form_submit($form_id, $form_values) {
  workflow_types_save($form_values);
  drupal_set_message(t('The workflow mapping was saved.'));
  return ('admin/build/workflow');
}

/**
 * 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) {
  $name = db_result(db_query("SELECT name FROM {workflows} WHERE wid = %d", $wid));
  return $name;
}

/**
 * Get ID of a workflow for a node type.
 *
 * @return int
 *   The ID of the workflow or FALSE if none.
 *
 */
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 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] = $data->name;
  }

  return $workflows;
}

/**
 * Create a workflow and its (creation) state.
 *
 * @param $name
 *   The name of the workflow.
 *
 */
function workflow_create($name) {
  $wid = db_next_id('{workflows}_wid');
  db_query("INSERT INTO {workflows} (wid, name) VALUES (%d, '%s')", $wid, $name);
  workflow_state_save(array('wid' => $wid, 'state' => t('(creation)'), 'sysid' => WORKFLOW_CREATION, 'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT));
  // Workflow creation affects tabs (local tasks), so force menu rebuild.
  cache_clear_all('*', 'cache_menu', TRUE);
  menu_rebuild();

  return $wid;
}

/**
 * Save a workflow's name in the database.
 *
 * @param $name
 *   The name of the workflow.
 *
 */
function workflow_update($wid, $name, $tab_roles) {
  db_query("UPDATE {workflows} SET name = '%s', tab_roles = '%s' WHERE wid = %d", $name, implode(',', $tab_roles), $wid);
  // Workflow name change affects tabs (local tasks), so force menu rebuild.
  cache_clear_all('*', 'cache_menu', TRUE);
  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);
  }
  workflow_types_delete($wid);
  db_query('DELETE FROM {workflows} WHERE wid = %d', $wid);
  // 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);
  }
  else {
    $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 1 ORDER BY sid");
  }
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $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);
  while ($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;
}

/**
 * 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($edit) {
  $defaults = array('weight' => 0, 'sysid' => 0);
  $edit = array_merge($defaults, $edit);

  if (!isset($edit['sid'])) {
    $edit['sid'] = db_next_id('{workflow_states}_sid');
    db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid, weight) VALUES (%d, %d, '%s', %d, %d)", $edit['sid'], $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight']);
  }
  else {
    db_query("UPDATE {workflow_states} SET wid = %d, state = '%s', sysid = %d, weight = %d WHERE sid = %d", $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight'], $edit['sid']);
  }

  return $edit['sid'];
}

/**
 * Legacy code for workflow_state_create().
 */
function workflow_state_create($wid, $name) {
  return workflow_state_save(array('wid' => $wid, 'state' => $name));
}

/**
 * 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);
}

/**
 * Delete a transition (and any associated actions).
 *
 * @param $tid
 *   The ID of the transition.
 */
function workflow_transition_delete($tid) {
  $aids = workflow_get_actions($tid);
  foreach ($aids as $aid => $description) {
    workflow_actions_remove($tid, $aid, $description);
  }
  db_query("DELETE FROM {workflow_transitions} WHERE tid = %d", $tid);
}

/**
 * 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 => 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 // Include current state for same-state transitions.
      || workflow_transition_allowed($t->tid, $roles)) {
      $transitions[$t->state_id] = $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_values
 */
function workflow_types_save($form_values) {
  db_query("DELETE FROM {workflow_type_map}");
  foreach (node_get_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']))));
  }
}

function workflow_types_delete($wid) {
  db_query("DELETE FROM {workflow_type_map} WHERE wid = %d", $wid);
}

/**
 * Get the actions associated with a given transition.
 *
 * @param int $tid
 * @return array
 *   Actions as aid => description pairs.
 */
function workflow_get_actions($tid) {
  $actions = array();
  if (!function_exists('actions_do')) {
    watchdog('workflow', t('Unable to get actions associated with a transition because the actions module is not enabled.'), WATCHDOG_WARNING);
    return $actions;
  }
  $op = 'workflow-'. $tid;
  $actions = _actions_get_hook_actions('workflow', $op);
  return $actions;
}

/**
 * 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));
}

function workflow_actions_remove($tid, $aid, $description) {
  db_query("DELETE FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = '%s'", 'workflow', 'workflow'. $tid, $aid);
  watchdog('workflow', t('Action %action has been unassigned.',  array('%action' => $description)));
}

/**
 * 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);
}

/**
 * Function to get a list of roles. Used kind of often.
 */
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] = $data->name;
    }
  }
  return $roles;
}

/**
 * Implementation of hook_views_tables()
 */
function workflow_views_tables() {
  $table = array(
    'name' => 'workflow_node',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'node',
        'field' => 'nid',
       ),
       'right' => array(
         'field' => 'nid',
       ),
     ),
    'filters' => array(
      'sid' => array(
        'name' => t('Workflow: state'),
        'operator' => 'views_handler_operator_andor',
        'list' => 'workflow_handler_filter_sid',
        'list-type' => 'list',
        'value-type' => 'array',
        'help' => t('Include only nodes in the selected workflow states.'),
      ),
    )
  );
  $tables[$table['name']] = $table;

  $table = array(
    'name' => 'workflow_states',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'workflow_node',
        'field' => 'sid',
       ),
       'right' => array(
         'field' => 'sid',
       ),
     ),
    "sorts" => array(
      'weight' => array(
        'name' => t('Workflow: state'),
        'field' => array('weight', 'state'),
        'help' => t('Order nodes by workflow state.'),
      ),
    ),
    "fields" => array(
      'state' => array(
        'name' => t('Workflow: state'),
        'sortable' => TRUE,
        'help' => t('Display the workflow state of the node.'),
      ),
    ),
  );
  $tables[$table['name']] = $table;

  $table = array(
    'name' => 'workflow_node_history',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'workflow_node',
        'field' => 'nid',
       ),
       'right' => array(
         'field' => 'nid',
       ),
       'extra' => array(
          'stamp = workflow_node.stamp' => NULL,
        ),
     ),
    "fields" => array(
      'comment' => array(
        'name' => t('Workflow: comment'),
        'sortable' => false,
        'help' => t('Display the most recent workflow comment of the node.'),
      ),
    ),
  );
  $tables[$table['name']] = $table;

  return $tables;
}

/**
 * Implementation of hook_views_arguments()
 */
function workflow_views_arguments() {
  $arguments = array(
    'workflow_state' => array(
      'name' => t('Workflow: state'),
      'handler' => 'workflow_handler_arg_sid',
      'help' => t('The work flow argument allows users to filter a view by workflow state.'),
    ),
  );
  return $arguments;
}

/**
 * Handler to provide a list of workflow states for the filter list.
 */
function workflow_handler_filter_sid() {
  $result = db_query("SELECT sid, state FROM {workflow_states} ORDER BY weight, state");
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }

  return $states;
}

/**
 * Handler to deal with sid as an argument.
 */
function workflow_handler_arg_sid($op, &$query, $argtype, $arg = '') {
  switch ($op) {
    case 'summary':
      $query->add_table('workflow_states', TRUE);
      $fieldinfo['field'] = "workflow_states.sid";
      $query->add_field('sid', 'workflow_states');
      $query->add_field('state', 'workflow_states');
      $query->add_where('workflow_node.sid IS NOT NULL');
      return $fieldinfo;
      break;
    case 'filter':
      $query->add_table('workflow_states', TRUE);
      if (is_numeric($arg)) {
        $query->add_where("workflow_states.sid = %d", $arg);
      }
      else {
        $query->add_where("workflow_states.state = '%s'", $arg);
      }
      break;
    case 'link':
      return l($query->state, "$arg/$query->sid");
    case 'title':
      $state = db_fetch_object(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $query));
      return $state->state;
  }
}

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

  // If the time now is greater than the time to publish a node, publish 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.
    if ($node->_workflow == $row->old_sid) {
      // Do transition.
      workflow_execute_transition($node, $row->sid, $row->comment);

      watchdog('content', t('%type: scheduled transition of %title.', array('%type' => t($node->type), '%title' => $node->title)), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid));
      $clear_cache = TRUE;

    }
  }

  if ($clear_cache) {
    // Clear the cache so an anonymous poster can see the node being published or unpublished.
    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;
      }

      $values['workflow-current-state-name']                      =  $states[$row->sid];
      $values['workflow-old-state-name']                          =  $states[$row->old_sid];

      $values['workflow-current-state-date-iso']                  =  date('Ymdhis', $row->stamp);
      $values['workflow-current-state-date-tstamp']               =  $row->stamp;
      $values['workflow-current-state-date-formatted']            =  date('M d, Y h:i:s', $row->stamp);

      $values['workflow-current-state-updating-user-name']        = $account->uid ? $account->name : variable_get('anonymous', 'Anonymous');
      $values['workflow-current-state-updating-user-uid']         = $account->uid;
      $values['workflow-current-state-updating-user-mail']        = $account->uid ? $account->mail : '';

      $values['workflow-current-state-log-entry']                 = $row->comment;
      break;
  }

  return $values;
}

/**
 * Implementation of hook_token_list().
 */
function workflow_token_list($type = 'all') {

  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_hook_info().
 * Expose each transition as a hook.
 */
function workflow_hook_info() {
  $states = workflow_get_states();
  if (!$states) {
    return;
  }
  $trigger_page = substr($_GET['q'], 0, 28) == 'admin/build/trigger/workflow';
  if ($trigger_page && $wid = arg(4)) {
    $result = db_query("SELECT tm.type, w.wid, w.name, ws.state, wt.tid, wt.sid, wt.target_sid FROM {workflow_type_map} tm LEFT JOIN {workflows} w ON tm.wid = w.wid LEFT JOIN {workflow_states} ws ON w.wid = ws.wid LEFT JOIN {workflow_transitions} wt ON ws.sid = wt.sid WHERE w.wid = %d AND wt.target_sid IS NOT NULL ORDER BY tm.type, ws.weight", $wid);
  }
  else {
    $result = db_query("SELECT tm.type, w.wid, w.name, ws.state, wt.tid, wt.sid, wt.target_sid FROM {workflow_type_map} tm LEFT JOIN {workflows} w ON tm.wid = w.wid LEFT JOIN {workflow_states} ws ON w.wid = ws.wid LEFT JOIN {workflow_transitions} wt ON ws.sid = wt.sid WHERE wt.target_sid IS NOT NULL ORDER BY tm.type, ws.weight");
  }
  while ($data = db_fetch_object($result)) {
    $pseudohooks['workflow-'. $data->type .'-'. $data->tid] = array('runs when' => t('When %type moves from %state to %target_state', array('%type' => $data->type, '%state' => $states[$data->sid], '%target_state' => $states[$data->target_sid])));
  }
  // $pseudohooks will not be set if no workflows have been assigned
  // to node types.
  if (isset($pseudohooks)) {
    return array(
      'workflow' => array(
        'workflow' => $pseudohooks,
      ),
    );
  }
  if ($trigger_page) {
    drupal_set_message(t('No transitions have been set up. To enable the assignment of actions, edit the workflow to assign permissions for roles to do transitions. After that is completed, transitions will appear here and you will be able to assign actions to them. The workflow must also be assigned to at least one content type.'));
  }
}