<?php
// $Id: workflow.module,v 1.83.2.14.2.6 2010/09/02 20:51:44 q0rban Exp $

/**
 * @file
 * Support workflows made up of arbitrary states.
 */

workflow_include_files();

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

/**
 * Implementation of hook_help().
 */
function workflow_help($path, $arg) {
  switch ($path) {
    case 'admin/build/workflow/edit/%/transitions':
      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 "production editor" role may move a node from Review state to the Published state, check the box next to "production editor". The author role is built in and refers to the user who authored the node.');
    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 'admin/build/workflow/state':
      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</em>, <em>eat</em>, and <em>clean up</em>.');
    case 'admin/build/trigger/workflow':
      return t('Use this page to set actions to happen when transitions occur. To configure actions, use the <a href="@link">actions settings page</a>.', array('@link' => url('admin/settings/actions')));
  }
}

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

/**
 * Implementation of hook_menu().
 */
function workflow_menu() {
  $items['admin/build/workflow'] = array(
    'title' => 'Workflow',
    'access arguments' => array('administer workflow'),
    'page callback' => 'workflow_overview',
    'description' => 'Allows the creation and assignment of arbitrary workflows to node types.',
    'file' => 'workflow.admin.inc',
  );
  $items['admin/build/workflow/edit/%workflow'] = array(
    'title' => 'Edit workflow',
    'type' => MENU_CALLBACK,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_edit_form', 4),
    'file' => 'workflow.admin.inc',
  );
  $items['admin/build/workflow/edit/%workflow/info'] = array(
    'title' => 'Edit',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/build/workflow/edit/%workflow/states'] = array(
    'title' => 'States',
    'weight' => 0,
    'access arguments' => array('administer workflow'),
    'page callback' => 'workflow_states',
    'page arguments' => array(4),
    'file' => 'workflow.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/build/workflow/edit/%workflow/transitions'] = array(
    'title' => 'Transitions',
    'weight' => 1,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_transition_grid_form', 4),
    'file' => 'workflow.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/build/workflow/list'] = array(
    'title' => 'List',
    'weight' => -10,
    'access arguments' => array('administer workflow'),
    'page callback' => 'workflow_overview',
    'file' => 'workflow.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/build/workflow/add'] = array(
    'title' => 'Add workflow',
    'weight' => -8,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_add_form'),
    'file' => 'workflow.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/build/workflow/state/disable/%workflow'] = array(
    'title' => 'Disable State',
    'type' => MENU_CALLBACK,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_state_disable_form', 5, 6),
    'file' => 'workflow.admin.inc',
  );
  $items['admin/build/workflow/delete'] = array(
    'title' => 'Delete workflow',
    'type' => MENU_CALLBACK,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_delete_form'),
    'file' => 'workflow.admin.inc',
  );
  $items['admin/build/workflow/revert'] = array(
    'title' => 'Revert workflow',
    'type' => MENU_CALLBACK,
    'access arguments' => array('administer workflow'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workflow_revert_form'),
    'file' => 'workflow.admin.inc',
  );  
  $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;

  if (!($workflow = workflow_get_workflow_for_type($node->type))) {
    // 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);
  }

  $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_transition_grid_form' => array(
      'arguments' => array(
        'form' => array(),
      ),
    ),
    'workflow_states_form' => array(
      'arguments' => array(
        'form' => array(),
      ),
    ),
    'workflow_types_form' => array(
      'arguments' => array(
        'form' => array(),
      ),
    ),
    'workflow_actions_form' => array(
      'arguments' => array(
        'form' => 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(
        'label' => NULL,
      ),
    ),
    'workflow_deleted_state' => array(
      'arguments' => array(
        'label' => NULL,
      ),
    ),
    'workflow_permissions' => array(
      'arguments' => array(
        'map' => array(),
      ),
    ),
  );
}

/**
 * Implementation of hook_forms().
 */
function workflow_forms() {
  $forms = array();
  foreach (workflow_load_all() as $workflow_name => $workflow) {
    $forms['workflow_states_form_'. $workflow_name]['callback'] = 'workflow_states_form';
  }
  return $forms;
}

/**
 * Implementation of hook_views_api().
 */
function workflow_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'workflow') .'/includes',
  );
}

/**
 * Implementation of hook_nodeapi().
 */
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'load':
      $curr_state = workflow_node_current_state($node);
      //-- is current node type managed by a workflow?
      if (!empty($curr_state)) {
        $node->_workflow = $curr_state;
      }

      // 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_state_name = $row->state_name;
        $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)) {
        $workflow = workflow_get_workflow_for_type($node->type);
        $choices = workflow_field_choices($node, $workflow);
        $keys = array_keys($choices);
        $node->workflow = array_shift($keys);
      }

      workflow_transition($node, $node->workflow, $workflow);
      break;

    case 'update':
      // Load up the workflow from the state stored in ->workflow, not by
      // workflow_get_workflow_for_type(). The reason we do this is because the
      // workflow may have changed on this particular node, due to lack of
      // restrictions on the workflow_types_form().
      
      //-- $node->workflow is misnamed and is actually the name of the workflow state 
      //-- that user is trying to switch to.
      $state = workflow_get_state($node->workflow);
      $workflow = workflow_load($state->workflow_name);
      workflow_transition($node, $state->name, $workflow);
      break;

    case 'delete':
      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']);
    $state_name = $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, $state_name);
  }
}

/**
 * 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 $title
 *   The translated and sanitized title of the state field.
 * @param $name
 *   The machine name of the workflow.
 * @param $default_value
 *   The machine name for the default state.
 * @param $choices
 *   An array of possible target states.
 * @param $timestamp
 *   Optional timestamp of the scheduled state transition.
 * @param $comment
 *   Optional comment of the scheduled state transition.
 */
function workflow_node_form(&$form, $form_state, $title, $name, $default_value, $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' => $default_value,
    );
  }
  else {
    $form['workflow'][$name] = array(
      '#type' => 'radios',
      '#title' => $title,
      '#options' => $choices,
      '#name' => $name,
      '#parents' => array('workflow'),
      '#default_value' => $default_value,
    );

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

      $sched_date = $form_state['values']['workflow_scheduled_date'];

      $form['workflow']['workflow_scheduled_date'] = array(
        '#type' => 'date',
        '#default_value' => array(
          'day'   => isset($sched_date['day']) ? $sched_date['day'] : format_date($timestamp, 'custom', 'j'),
          'month' => isset($sched_date['month']) ? $sched_date['month'] : format_date($timestamp, 'custom', 'n'),
          'year'  => isset($sched_date['year']) ? $sched_date['year'] : format_date($timestamp, 'custom', 'Y')
        ),
      );

      // Set up the hour field.
      $sched_hour = isset($form_state['values']['workflow_scheduled_hour']) ? $form_state['values']['workflow_scheduled_hour'] : format_date($timestamp, 'custom', 'H:i');
      $description = '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: %time';
      $t_args = array('%time' => format_date(time()));

      $form['workflow']['workflow_scheduled_hour'] = array(
        '#type' => 'textfield',
        '#title' => t('Time'),
        '#description' => t($description, $t_args),
        '#size' => 10,
        '#default_value' => $scheduled ? $sched_hour : '00:00',
      );
    }
    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'];
    }
    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']);
    }
  
    // Abort if no workflow is assigned to this node type.
    if (!($workflow = workflow_get_workflow_for_type($node->type))) {
      return;
    }

    $choices = workflow_field_choices($node, $workflow);
    $name = $workflow->name;

    // 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.
    $default_value = isset($form_state['values']['workflow']) ? $form_state['values']['workflow'] : workflow_node_current_state($node);
    $min = ($default_value == _workflow_creation_state($name)) ? 1 : 2;
    // Stop if user has no new target state(s) to choose.
    if (count($choices) < $min) {
      return;
    }

    $form['#wf'] = $workflow;

    // 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[$default_value])) {
      $array = array_keys($choices);
      $default_value = $array[0];
    }

    if (sizeof($choices) > 1) {
      $form['workflow'] = array(
        '#type' => 'fieldset',
        '#title' => $workflow->label,
        '#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_state_name)) {
      // The default value should be the upcoming state_name.
      $default_value = $node->_workflow_scheduled_state_name;
      $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, $workflow->label, $name, $default_value, $choices, $timestamp, $comment);
  }
}

/**
 * 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_state_name) {
      // Do transition.
      workflow_execute_transition($node, $row->state_name, $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_action_info_alter().
 */
function workflow_action_info_alter(&$info) {
  foreach (array_keys($info) as $key) {
    // Modify each action's hooks declaration, changing it to say
    // that the action supports any hook.
    $info[$key]['hooks']['any'] = TRUE;
  }
}

/**
 * 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 && $workflow_name = arg(4)) {
    $result = db_query("SELECT tm.type, w.name, w.label, wt.tid, wt.state_name, wt.target_state_name "
      . "FROM {workflow_type_map} tm "
      . "LEFT JOIN {workflows} w ON tm.workflow_name = w.name "
      . "LEFT JOIN {workflow_states} ws ON w.name = ws.workflow_name "
      . "LEFT JOIN {workflow_transitions} wt ON ws.name = wt.state_name "
      . "WHERE w.name = '%s' AND wt.target_state_name IS NOT NULL "
      . "ORDER BY tm.type, ws.weight", $workflow_name);
  }
  else {
    $result = db_query("SELECT tm.type, w.name, w.label, wt.tid, wt.state_name, wt.target_state_name "
      . "FROM {workflow_type_map} tm "
      . "LEFT JOIN {workflows} w ON tm.workflow_name = w.name "
      . "LEFT JOIN {workflow_states} ws ON w.name = ws.workflow_name "
      . "LEFT JOIN {workflow_transitions} wt ON ws.name = wt.state_name "
      . "WHERE wt.target_state_name IS NOT NULL "
      . "ORDER BY tm.type, ws.weight");
  }
  while ($data = db_fetch_object($result)) {
    $state = $states[$data->state_name];
    $target_state = $states[$data->target_state_name];
    $t_args = array('%type' => $data->type, '%state' => $state->label, '%target_state' => $target_state->label);
    $hook_key = 'workflow-'. $data->type .'-'. $data->tid;
    $pseudohooks[$hook_key] = array('runs when' => t('When %type moves from %state to %target_state', $t_args));
  }
  // $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('Either no transitions have been set up or this workflow has not yet been assigned to a content type. 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.'));
  }
}

/**
 * Implementation of hook_menu_alter().
 *
 * Work around loss of menu local task inheritance in Drupal 6.2.
 */
function workflow_menu_alter(&$callbacks) {
  if (module_exists('trigger') & isset($callbacks['admin/build/trigger/workflow'])) {
    $callbacks['admin/build/trigger/workflow']['access callback'] = 'trigger_access_check';
  }
}

/**
 * Implementation of hook_token_values().
 */
function workflow_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case 'node':
    case 'workflow':
      $node = (object)$object;

      if ($workflow = workflow_get_workflow_for_type($node->type)) {
        $values['workflow-name'] = $workflow->label;
        $values['workflow-machine-name'] = $workflow->name;
        $states = $workflow->states;
      }
      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.
        $state_name = $node->workflow;
        $old_state_name = isset($row->state_name) ? $row->state_name : _workflow_creation_state($machine_name);
        $date = time();
        $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
        $uid = $node->uid;
        $mail = $node->uid ? $node->user_mail : '';
        $comment = $node->workflow_comment;
      }
      elseif (!isset($node->workflow) && empty($row->state_name)) {
        // 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().

        // Abort if no workflow is assigned to this node type.
        if (!($workflow = workflow_get_workflow_for_type($node->type))) {
          return array();
        }
    
        $choices = workflow_field_choices($node, $workflow);
        
        $keys = array_keys($choices);
        $state_name = array_shift($keys);
        $old_state_name = _workflow_creation_state($machine_name);
        $date = time();
        $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
        $uid = $node->uid;
        $mail = $node->uid ? $node->user_mail : '';
        $comment = $node->workflow_comment;
      }
      else {
        // Default to the most recent transition data in the workflow history table.
        $state_name = $row->state_name;
        $old_state_name = $row->old_state_name;
        $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[$state_name]->label;
      $values['workflow-old-state-name']                    =  $states[$old_state_name]->label;

      $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') {
  if ($type == 'workflow' || $type == 'node' || $type == 'all') {
    $tokens['workflow']['workflow-name']                          =  'Name of workflow applied to this node';
    $tokens['workflow']['workflow-machine-name']                  =  'Machine name of workflow applied 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 (formatted - 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_workflow().
 *
 * @param $op
 *   The current workflow operation: 'transition pre' or 'transition post'.
 * @param $old_state
 *   The state ID of the current state.
 * @param  $new_state
 *   The state ID of the new state.
 * @param $node
 *   The node whose workflow state is changing.
 */
function workflow_workflow($op, $old_state, $new_state, $node) {
  switch ($op) {
    case 'transition pre':
      // The workflow module does nothing during this operation.
      // But your module's implementation of the workflow hook could
      // return FALSE here and veto the transition.
      break;

    case 'transition post':
      // A transition has occurred; fire off actions associated with this transition.
      // Can't fire actions if trigger module is not enabled.
      if (!module_exists('trigger')) {
        break;
      }
      $tid = workflow_get_transition_id($old_state, $new_state);
      $op = 'workflow-'. $node->type .'-'. $tid;
      $aids = _trigger_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']] = _trigger_normalize_node_context($action_info['type'], $node);
            }
            // Pass the node as the object for actions of type 'system'.
            if (!isset($objects[$action_info['type']]) && $action_info['type'] == 'system') {
              $objects[$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;
  }
}

/**
 * Loads all necessary includes.
 */
function workflow_include_files() {
  static $finished;
  
  if (!$finished) {
    $files = array('workflow', 'workflow.actions', 'workflow.node', 'workflow.classes');
    foreach ($files as $file) {
      module_load_include('inc', 'workflow', 'includes/'. $file);
    }

    $finished = TRUE;
  }
}
