You are here

hosting_task.module in Hosting 6.2

Web server node type is defined here.

File

task/hosting_task.module
View source
<?php

/**
 * @file Web server node type is defined here.
 */

/**
 * Implementation of hook_menu().
 */
function hosting_task_menu() {
  $items = array();
  $tasks = hosting_available_tasks();
  foreach ($tasks as $type => $type_tasks) {
    if (empty($type_tasks)) {

      // this is to workaround problems in the upgrade path where the
      // hook returns nothing (e.g. for server)
      continue;
    }
    foreach ($type_tasks as $task => $info) {
      if (empty($info['hidden'])) {
        $path = sprintf("node/%%hosting_%s_node/%s_%s", $type, $type, $task);
        $items[$path] = array(
          'title' => $info['title'],
          'description' => $info['description'],
          'page callback' => 'drupal_get_form',
          'page arguments' => array(
            'hosting_task_confirm_form',
            1,
            $task,
          ),
          'access callback' => 'hosting_task_menu_access_csrf',
          'access arguments' => array(
            1,
            $task,
          ),
          'type' => MENU_CALLBACK,
        );

        // Complement the $items array with attributes from the task definition
        // e.g. a custom access callback.
        $items[$path] = array_merge($items[$path], $info);
      }
    }
  }
  $items['hosting/tasks/%node/list'] = array(
    'title' => t('Task list'),
    'description' => t('AJAX callback for refreshing task list'),
    'page callback' => 'hosting_task_ajax_list',
    'page arguments' => array(
      2,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      2,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['hosting/tasks/%node/cancel'] = array(
    'title' => t('Task list'),
    'description' => t('Callback for stopping tasks'),
    'page callback' => 'hosting_task_cancel',
    'page arguments' => array(
      2,
    ),
    'access callback' => 'hosting_task_cancel_access',
    'access arguments' => array(
      2,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['hosting/tasks/queue'] = array(
    'title' => t('Task list'),
    'description' => t('AJAX callback for refreshing task queue'),
    'page callback' => 'hosting_task_ajax_queue',
    'access arguments' => array(
      'access task logs',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}
function hosting_task_ajax_list($node) {
  $return['markup'] = hosting_task_table($node);
  $return['changed'] = $node->changed;
  $return['navigate_url'] = url('node/' . $node->nid);
  drupal_json($return);
  exit;
}
function hosting_task_ajax_queue() {
  $view = views_get_view('hosting_task_list');
  $view
    ->set_display('block');
  $view
    ->pre_execute();
  $return['markup'] = $view
    ->render('block');
  drupal_json($return);
  exit;
}
function hosting_task_cancel($node) {
  if ($node->type == 'task') {
    if ($node->task_status == HOSTING_TASK_QUEUED) {
      $ref = node_load($node->rid);
      hosting_add_task($node->rid, $node->task_type, array(), HOSTING_TASK_ERROR);

      // Log the cancellation
      hosting_task_log($node->vid, 'error', t("Task was cancelled."));
      drupal_goto('node/' . $node->rid);
    }
  }
  drupal_access_denied();
}

/*
 * Implementation of hook_access()
 */
function hosting_task_cancel_access($node) {

  // bring $user into scope, so we can test task ownership
  global $user;

  // To prevent CSRF attacks, a unique token based upon user is used. Deny
  // access if the token is missing or invalid.
  if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $user->uid)) {
    return FALSE;
  }

  // 'administer tasks' allows cancelling any and all tasks on the system
  if (user_access('administer tasks')) {
    return TRUE;
  }

  // 'cancel own tasks' allows cancelling any task the user *could have* created, on nodes she can view
  if (user_access('cancel own tasks') && user_access('create ' . $node->task_type . ' task') && node_access('view', $node)) {
    return TRUE;
  }
}

/**
 * Task access controls
 *
 * This function defines which tasks should be showed to the user but
 * especially which will be accessible to him, in the permissions system.
 *
 * @arg $node object
 *   the node object we're trying to access
 *
 * @arg $task string
 *   the task type we're trying to do on the $node
 *
 * @see hosting_task_menu()
 */
function hosting_task_menu_access($node, $task) {
  if (user_access("create " . $task . " task")) {
    if ($node->type == 'site') {
      if (hosting_task_outstanding($node->nid, 'delete') || $node->site_status == HOSTING_SITE_DELETED) {
        return FALSE;
      }
      if ($task == 'login-reset' && $node->site_status != HOSTING_SITE_ENABLED) {
        return FALSE;
      }
      $safe_tasks = array(
        'backup',
        'backup-delete',
        'verify',
        'enable',
      );
      if (!in_array($task, $safe_tasks)) {

        // Don't show certain tasks if the site is the 'special' main aegir site
        $profile = node_load($node->profile);
        if ($profile->short_name == 'hostmaster') {
          return FALSE;
        }
      }
      $site_enabled = hosting_task_outstanding($node->nid, 'enable') || $node->site_status == HOSTING_SITE_ENABLED;
      $deletable = $task == "delete";
      $enabable = $task == "enable";
      $delete_or_enable = $deletable || $enabable;

      // If the site is not enabled, we can either delete it, or enable it again
      if (!$site_enabled) {
        return $delete_or_enable;
      }
      else {

        // Site is enabled
        return !variable_get('hosting_require_disable_before_delete', TRUE) && $deletable || !$delete_or_enable;
      }
    }
    if ($node->type == 'platform') {

      // if the user can't edit this node, he can't create tasks on it
      if (!node_access('update', $node, $GLOBALS['user'])) {
        return FALSE;
      }

      // If the platform is in a deleted state, nothing else can be done with it
      if (hosting_task_outstanding($node->nid, 'delete') || $node->platform_status == HOSTING_PLATFORM_DELETED) {
        return FALSE;
      }

      // If the platform's been locked, we can unlock it, delete, batch migrate existing sites or verify
      if ($node->platform_status == HOSTING_PLATFORM_LOCKED) {
        $platform_tasks = array(
          'verify',
          'unlock',
          'delete',
          'migrate',
        );
        return in_array($task, $platform_tasks);
      }
      else {

        // Platform is not locked, so no need for an unlock task
        if ($task == 'unlock') {
          return FALSE;
        }
        return TRUE;
      }
    }
    if ($node->type === 'server') {

      // If the user can't edit this node, he can't create tasks on it.
      if (!node_access('update', $node, $GLOBALS['user'])) {
        return FALSE;
      }

      // todo probably need more checks
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Access callback helper for hosting task menu items.
 *
 * Implemented as a helper function since we only want to validate the CSRF
 * token when the user accesses a certain path, not when (for example) building
 * the list of tasks a user has access to.
 *
 * @arg $node object
 *   the node object we're trying to access
 *
 * @arg $task string
 *   the task type we're trying to do on the $node
 *
 *
 * @see hosting_task_menu_access()
 */
function hosting_task_menu_access_csrf($node, $task) {
  global $user;
  $interactive_tasks = array(
    'migrate',
    'clone',
  );

  // To prevent CSRF attacks, a unique token based upon user is used. Deny
  // access if the token is missing or invalid. We only do this on
  // non-interactive tasks.
  if (!in_array($task, $interactive_tasks) && (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $user->uid))) {
    return FALSE;
  }

  // Call the main menu access handler.
  return hosting_task_menu_access($node, $task);
}

/**
 * Implementation of hook_node_info()
 */
function hosting_task_node_info() {

  #management
  $types["task"] = array(
    "type" => 'task',
    "name" => 'Task',
    "module" => 'hosting_task',
    "has_title" => FALSE,
    "title_label" => '',
    "description" => hosting_node_help("task"),
    "has_body" => 0,
    "body_label" => '',
    "min_word_count" => 0,
  );
  return $types;
}

/**
 * Implementation of hook_access()
 */
function hosting_task_access($op, $node, $account) {
  if (hosting_feature('client')) {

    // we rely on hosting_client_node_grants() instead of global configuration
    return NULL;
  }
  if (user_access('administer tasks', $account)) {
    return TRUE;
  }
}

/**
 * Implementation of hook_perm()
 */
function hosting_task_perm() {
  return array(
    'administer tasks',
    'create backup task',
    'create restore task',
    'create disable task',
    'create enable task',
    'create delete task',
    'create verify task',
    'create lock task',
    'create unlock task',
    'create login-reset task',
    'create backup-delete task',
    'view own tasks',
    'view task',
    'access task logs',
    'retry failed tasks',
    'update status of tasks',
    'cancel own tasks',
  );
}

/**
 * Implementation of hook_hosting_queues
 *
 * Return a list of queues that this module needs to manage.
 */
function hosting_task_hosting_queues() {
  $queue['tasks'] = array(
    'name' => t('Task queue'),
    'description' => t('Process the queue of outstanding hosting tasks.'),
    'type' => 'serial',
    # run queue sequentially. always with the same parameters.
    'frequency' => strtotime("1 minute", 0),
    # run queue every minute.
    'items' => 5,
    # process 20 queue items per execution.
    'total_items' => hosting_task_count(),
    'singular' => t('task'),
    'plural' => t('tasks'),
    'running_items' => hosting_task_count_running(),
  );
  return $queue;
}

/**
 * Insert an entry in the task log.
 *
 * @param $vid
 *   The vid of the task to add an entry for.
 * @param $type
 *   The type of log entry being added.
 * @param $message
 *   The message for this log entry.
 * @param $error
 *   (optional) The error code associated to this log entry.
 * @param $timestamp
 *   (optional) The UNIX timestamp of this log message, this defaults to the
 *   current time.
 */
function hosting_task_log($vid, $type, $message, $error = '', $timestamp = NULL) {

  // We keep track of nids we've looked up in this request, for faster lookups.
  static $nids = array();
  $timestamp = $timestamp ? $timestamp : time();

  // We need to insert the nid in addition to the vid, so look it up.
  if (!isset($nids[$vid])) {
    $nids[$vid] = (int) db_result(db_query('SELECT nid FROM {hosting_task} WHERE vid = %d', $vid));
  }
  db_query("INSERT INTO {hosting_task_log} (vid, nid, type, message, error, timestamp) VALUES (%d, %d, '%s', '%s', '%s', %d)", $vid, $nids[$vid], $type, $message, $error, $timestamp);
}

/**
 * Retry the given task
 */
function hosting_task_retry($task_id) {
  $node = node_load($task_id);
  if ($node->task_status != HOSTING_TASK_QUEUED) {
    drupal_set_message(t("The task is being retried and has been added to the hosting queue again"));
    hosting_task_log($node->vid, 'queue', t("The task is being retried and has been added to the hosting queue again"));
    $node->revision = TRUE;
    $node->changed = time();
    $node->task_status = HOSTING_TASK_QUEUED;
    node_save($node);
  }
}

/**
 * Helper function to generate new task node
 */
function hosting_add_task($nid, $type, $args = null, $status = HOSTING_TASK_QUEUED) {
  global $user;
  $task = hosting_get_most_recent_task($nid, $type);

  // Guard against destructive tasks run on the hostmaster site
  $hostmaster_nid = hosting_get_hostmaster_nid();
  if ($nid == $hostmaster_nid && in_array($type, array(
    'delete',
    'disable',
  ))) {
    drupal_set_message(t('You cannot !task the Hostmaster site.', array(
      '!task' => $type,
    )), 'warning');
    watchdog('hostmaster', t('Attempt to !task the Hostmaster site blocked.', array(
      '!task' => $type,
    )), array(), WATCHDOG_WARNING);
    return FALSE;
  }
  $node = db_fetch_object(db_query("SELECT nid, uid, title FROM {node} WHERE nid=%d", $nid));
  if (!$task) {
    $task = new stdClass();
  }
  $task->type = 'task';

  # TODO: make this pretty
  $task->title = t("!type !title", array(
    '!type' => $type,
    '!title' => $node->title,
  ));
  $task->task_type = $type;
  $task->rid = $node->nid;

  /*
   * fallback the owner of the task to the owner of the node we operate
   * upon
   *
   * this is mostly for the signup form, which runs as the anonymous
   * user, but for which the node is set to the right user
   */
  $task->uid = $user->uid ? $user->uid : $node->uid;
  $task->status = 1;
  $task->task_status = $status;
  if ($status == HOSTING_TASK_QUEUED) {
    $task->revision = TRUE;
  }

  #arguments, such as which backup to restore.
  if (is_array($args)) {
    $task->task_args = $args;
  }
  node_save($task);
  drupal_set_message(t("Task %type was added to the queue. Next queue run is %date, server time is %time.", array(
    '%type' => $type,
    '%date' => format_date(_hosting_queue_next_run('tasks'), 'custom', 'H:i:sO'),
    '%time' => format_date(time(), 'custom', 'H:i:sO'),
  )));
  return $task;
}

/**
 * Implementation of hook_form().
 */
function hosting_task_confirm_form($form_state, $node, $task) {
  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
  $tasks = hosting_available_tasks($node->type);
  if (!isset($tasks[$task]['dialog']) || !$tasks[$task]['dialog']) {
    hosting_add_task($node->nid, $task);
    drupal_goto('node/' . $node->nid);
  }
  $form['help'] = array(
    '#value' => $tasks[$task]['description'],
  );
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  $form['task'] = array(
    '#type' => 'value',
    '#value' => $task,
  );
  $form['parameters'] = array(
    '#tree' => TRUE,
  );
  $func = 'hosting_task_' . str_replace('-', '_', $task) . '_form';
  if (function_exists($func)) {
    $form['parameters'] += $func($node);
  }
  $func = $func . '_validate';
  if (function_exists($func)) {
    $form['#validate'][] = $func;
    $form['#func_param_1'] = $node;
    $form['#func_param_2'] = $task;
  }
  $question = t("Are you sure you want to @task @object?", array(
    '@task' => $task,
    '@object' => $node->title,
  ));
  $path = !empty($_REQUEST['destination']) ? $_REQUEST['destination'] : 'node/' . $node->nid;
  $form = confirm_form($form, $question, $path, '', $tasks[$task]['title']);

  // add an extra class to the actions to allow us to disable the cancel link via javascript for the modal dialog
  $form['actions']['#prefix'] = '<div id="hosting-task-confirm-form-actions" class="container-inline">';
  return $form;
}

/**
 * Customize the task confirmation form for restore.
 *
 * This adds the backup listing to the confirmation dialog.
 */
function hosting_task_restore_form($node) {
  $list = hosting_site_backup_list($node->nid);
  if (sizeof($list)) {
    $form['bid'] = array(
      '#type' => 'radios',
      '#title' => t('Backups'),
      '#options' => $list,
      '#required' => TRUE,
    );
  }
  else {
    $form['no_backups'] = array(
      '#type' => 'item',
      '#title' => t('Backups'),
      '#value' => t('There are no valid backups available.'),
    );
  }
  return $form;
}

/**
 * Implementation of hook_form_alter()
 */
function hosting_task_restore_form_validate($form, &$form_state) {
  if ($form['parameters']['no_backups']) {
    form_set_error('no_backups', t('There are no valid backups available.'));
  }
}

/**
 * Generic form submit handler for tasks confirmation
 *
 * This handler gets called after any task has been confirmed by the user. It
 * will inject a new task in the queue and redirect the user to the
 * originating node.
 *
 * @see hosting_add_task()
 */
function hosting_task_confirm_form_submit($form, &$form_state) {
  $values = $form_state['values'];
  hosting_add_task($values['nid'], $values['task'], $values['parameters']);
  $form_state['redirect'] = 'node/' . $values['nid'];
  modalframe_close_dialog();
}

/**
 * Set the title of tasks automatically and in a consistent way
 *
 * Tasks should always be named 'task_type node_title'.
 */
function hosting_task_set_title(&$node) {
  $ref = db_fetch_object(db_query("SELECT vid, nid, title, type FROM {node} WHERE nid=%d", $node->rid));
  $tasks = hosting_available_tasks($ref->type);
  $node->title = drupal_ucfirst($tasks[$node->task_type]['title']) . ': ' . $ref->title;
  db_query("UPDATE {node} SET title='%s' WHERE nid=%d", $node->title, $node->nid);
  db_query("UPDATE {node_revisions} SET title='%s' WHERE vid=%d", $node->title, $node->vid);
}

/**
 * Process the hosting task queue.
 *
 * Iterates through the list of outstanding tasks, and execute the commands on the back end.
 */
function hosting_tasks_queue($count = 20) {
  global $provision_errors;
  drush_log(dt("Running tasks queue"));
  $tasks = _hosting_get_new_tasks($count);
  foreach ($tasks as $task) {
    drush_invoke_process('@self', "hosting-task", array(
      $task->nid,
    ), array(
      'strict' => FALSE,
    ), array(
      'fork' => TRUE,
    ));
  }
}

/**
 * Determine whether there is an outstanding task of a specific type.
 *
 * This is used to ensure that there are not multiple tasks of the same type queued.
 */
function hosting_task_outstanding($nid, $type) {
  $return = db_result(db_query("\n      SELECT t.nid FROM {hosting_task} t\n        INNER JOIN {node} n ON t.vid = n.vid \n      WHERE \n        t.rid = %d \n        AND t.task_status = %d \n        AND t.task_type = '%s' \n        ORDER BY t.vid DESC\n        LIMIT 1", $nid, HOSTING_TASK_QUEUED, $type));
  return $return;
}

/**
 * Return the amount of items still in the queue
 */
function hosting_task_count() {
  return db_result(db_query("SELECT COUNT(t.vid) FROM {hosting_task} t INNER JOIN {node} n ON t.vid = n.vid WHERE t.task_status = %d", HOSTING_TASK_QUEUED));
}

/**
 * Return the amount of items running in the queue
 */
function hosting_task_count_running() {
  return db_result(db_query("SELECT COUNT(t.nid) FROM {node} n INNER JOIN {hosting_task} t ON n.vid=t.vid WHERE type='task' AND t.task_status = '%d'", HOSTING_TASK_PROCESSING));
}

/**
 * Get a list of tasks that can be invoked by the user.
 *
 * Note this does not check permissions or relevance of the tasks.
 *
 * Modules can extend this list using hook_hosting_tasks().
 *
 * @param $type
 *   Tasks are grouped by the type of thing that the task is performed on. This
 *   parameter should either be NULL, or a string indicating the type of thing
 *   that you want possible tasks to operate on.
 * @param $reset
 *   This function has an internal static cache, to reset the cache, pass in
 *   TRUE.
 *
 * @return
 *   Depending on the value of the $type parameter, you will either be returned:
 *   - If $type is NULL: an associative array whose keys are things that can be
 *     acted upon by the tasks defined in the corresponding values.
 *   - If $type is a string, then an associative array whose keys are task types
 *     and whose values are definitions for those tasks.
 *
 * @see hook_hosting_tasks()
 * @see hook_hosting_tasks_alter()
 * @see hosting_task_menu_access()
 */
function hosting_available_tasks($type = NULL, $reset = FALSE) {
  static $cache = array();
  if (!sizeof($cache) || $reset) {
    $cache = module_invoke_all('hosting_tasks');
    drupal_alter('hosting_tasks', $cache);
  }
  if (isset($type)) {
    return $cache[$type];
  }
  else {
    return $cache;
  }
}

/**
 * Implementation of hook_insert().
 */
function hosting_task_insert($node) {
  $node->executed = isset($node->executed) ? $node->executed : NULL;
  $node->delta = isset($node->delta) ? $node->delta : NULL;
  db_query("INSERT INTO {hosting_task} (vid, nid, task_type, task_status, rid, executed, delta) VALUES (%d, %d, '%s', %d, %d, %d, %d)", $node->vid, $node->nid, $node->task_type, $node->task_status, $node->rid, $node->executed, $node->delta);
  if (isset($node->task_args) && is_array($node->task_args)) {
    foreach ($node->task_args as $key => $value) {
      db_query("INSERT INTO {hosting_task_arguments} (vid, nid, name, value) VALUES (%d, %d, '%s', '%s')", $node->vid, $node->nid, $key, $value);
    }
  }
  hosting_task_set_title($node);
}

/**
 * Implementation of hook_update().
 *
 * As an existing node is being updated in the database, we need to do our own
 * database updates.
 */
function hosting_task_update($node) {

  // if this is a new node or we're adding a new revision,
  if (!empty($node->revision)) {
    hosting_task_insert($node);
  }
  else {
    hosting_task_set_title($node);
    db_query("UPDATE {hosting_task} SET nid=%d, task_type = '%s', task_status = %d, rid = %d, executed=%d, delta=%d WHERE vid=%d", $node->nid, $node->task_type, $node->task_status, $node->rid, $node->executed, $node->delta, $node->vid);
    if (isset($node->task_args) && is_array($node->task_args)) {

      # Wipe out old arguments first, since arguments could theoretically be removed.
      db_query("DELETE FROM {hosting_task_arguments} WHERE vid=%d", $node->vid);
      foreach ($node->task_args as $key => $value) {
        db_query("INSERT INTO {hosting_task_arguments} (vid, nid, name, value) VALUES (%d, %d, '%s', '%s')", $node->vid, $node->nid, $key, $value);
      }
    }
  }
}

/**
 * Implementation of hook_delete_revision()
 */
function hosting_nodeapi_task_delete_revision(&$node) {
  db_query('DELETE FROM {hosting_task} WHERE vid = %d', $node->vid);
  db_query('DELETE FROM {hosting_task_arguments} WHERE vid = %d', $node->vid);
  db_query('DELETE FROM {hosting_task_log} WHERE vid = %d', $node->vid);
}

/**
 * Implementation of hook_delete().
 */
function hosting_task_delete($node) {
  db_query('DELETE FROM {hosting_task} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {hosting_task_arguments} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {hosting_task_log} WHERE nid = %d', $node->nid);
}

/**
 * Implementation of hook_load().
 */
function hosting_task_load($node) {
  $additions = db_fetch_object(db_query('SELECT task_type, executed, delta, rid, task_status FROM {hosting_task} WHERE vid = %d', $node->vid));
  $result = db_query("SELECT name, value FROM {hosting_task_arguments} WHERE vid=%d", $node->vid);
  if ($result) {
    while ($arg = db_fetch_object($result)) {
      $additions->task_args[$arg->name] = $arg->value;
    }
  }
  return $additions;
}
function hosting_task_retry_form($form_state, $nid) {
  $form['#prefix'] = '<div class="hosting-task-retry">';
  $form['task'] = array(
    '#type' => 'hidden',
    '#default_value' => $nid,
  );
  $form['retry'] = array(
    '#type' => 'submit',
    '#value' => t('Retry'),
  );
  $form['#suffix'] = '</div>';
  return $form;
}
function hosting_task_retry_form_submit($form, &$form_state) {
  hosting_task_retry($form_state['values']['task']);
  modalframe_close_dialog();
}
function hosting_task_update_status_form($form_state, $vid) {
  $form['#prefix'] = '<div class="hosting-task-retry">';
  $form['task'] = array(
    '#type' => 'hidden',
    '#default_value' => $vid,
  );
  $form['update-status'] = array(
    '#type' => 'submit',
    '#value' => t('Update status'),
  );
  $form['#suffix'] = '</div>';
  return $form;
}
function hosting_task_update_status_form_submit($form, &$form_state) {
  hosting_task_update_status($form_state['values']['task']);
  modalframe_close_dialog();
}

/**
 * Implementation of hook_view().
 */
function hosting_task_view($node, $teaser = FALSE, $page = FALSE) {
  drupal_add_js(drupal_get_path('module', 'hosting') . '/hosting.js');
  $node = node_prepare($node, $teaser);
  $ref = node_load($node->rid);
  hosting_set_breadcrumb($node);
  $node->content['info']['#prefix'] = '<div id="hosting-task-info" class="clear-block">';
  $node->content['info']['reference'] = array(
    '#type' => 'item',
    '#title' => drupal_ucfirst($ref->type),
    '#value' => _hosting_node_link($node->rid),
  );
  if ($node->task_status != HOSTING_TASK_QUEUED) {
    if ($node->task_status == HOSTING_TASK_PROCESSING) {
      $node->content['info']['started'] = array(
        '#type' => 'item',
        '#title' => t('Started'),
        '#value' => format_date($node->executed),
        '#weight' => 1,
      );
      $node->content['info']['delta'] = array(
        '#type' => 'item',
        '#title' => t('Processing time'),
        '#value' => format_interval(time() - $node->executed),
        '#weight' => 2,
      );
    }
    else {
      $node->content['info']['executed'] = array(
        '#type' => 'item',
        '#title' => t('Executed'),
        '#value' => format_date($node->executed),
        '#weight' => 1,
      );
      $node->content['info']['delta'] = array(
        '#type' => 'item',
        '#title' => t('Execution time'),
        '#value' => format_interval($node->delta),
        '#weight' => 2,
      );
    }
  }
  else {
    $queues = hosting_get_queues();
    $queue = $queues['tasks'];
    $next = _hosting_queue_next_run($queue);
    $node->content['info']['notexecuted'] = array(
      '#type' => 'item',
      '#title' => t('This task has not been processed yet'),
      '#value' => t('It will be processed around %date, if the queue is not too crowded. The queue is currently run every %freq, was last run %last and processes %items items at a time. Server time is %time.', array(
        '%freq' => format_interval($queue['frequency']),
        '%date' => format_date($next, 'custom', 'H:i:sO'),
        '%last' => hosting_format_interval($queue['last_run']),
        '%items' => $queue['items'],
        '%time' => format_date(time(), 'custom', 'H:i:sO'),
      )),
    );
  }
  if ($node->task_status) {
    $node->content['info']['status'] = array(
      '#type' => 'item',
      '#title' => t('Status'),
      '#value' => _hosting_parse_error_code($node->task_status),
    );
  }
  $node->content['info']['#suffix'] = '</div>';
  if (user_access('retry failed tasks') && $node->task_status == HOSTING_TASK_ERROR) {
    $node->content['retry'] = array(
      '#type' => 'markup',
      '#value' => drupal_get_form('hosting_task_retry_form', $node->nid),
      '#weight' => 5,
    );
  }
  if (user_access('update status of tasks') && $node->task_status == HOSTING_TASK_PROCESSING) {
    $node->content['update-status'] = array(
      '#type' => 'markup',
      '#value' => drupal_get_form('hosting_task_update_status_form', $node->vid),
      '#weight' => 5,
    );
  }
  if (user_access('access task logs')) {
    if ($table = _hosting_task_log_table($node->vid)) {
      $node->content['hosting_log'] = array(
        '#weight' => 10,
        '#type' => 'item',
        '#value' => $table,
      );
    }
  }
  return $node;
}

/**
 * Display table containing the logged information for this task
 */
function _hosting_task_log_table($vid) {
  $result = db_query("SELECT * FROM {hosting_task_log} WHERE vid = %d ORDER BY lid", $vid);
  if ($result) {
    $header = array(
      'data' => 'Log message',
    );
    while ($entry = db_fetch_object($result)) {
      if (strlen($entry->message) > 300) {
        $summary = "<span class='hosting-task-summary'>" . filter_xss(substr($entry->message, 0, 75), array()) . "... <a href='javascript:void(0)' class='hosting-summary-expand modalframe-exclude'>(" . t('Expand') . ')</a></span>';
        $message = $summary . "<span class='hosting-task-full'>" . filter_xss($entry->message) . '</span>';
      }
      else {
        $message = filter_xss($entry->message);
      }
      $row = array(
        array(
          'data' => $message,
          'class' => 'hosting-status',
        ),
      );
      $rows[] = array(
        'data' => $row,
        'class' => _hosting_task_log_class($entry->type),
      );
    }
    return theme("table", $header, (array) $rows, array(
      'id' => 'hosting-task-log',
      'class' => 'hosting-table',
    ));
  }
  return false;
}

/**
 * Map entry statuses to coincide with our CSS classes.
 *
 * @todo make this irrelevant.
 * @see _drush_print_log().
 */
function _hosting_task_log_class($type) {
  switch (strtolower($type)) {
    case 'warning':
    case 'cancel':
    case 'rollback':

      // aegir-specific
      $type = 'warning';
      break;
    case 'failed':
    case 'error':
      $type = 'error';
      break;
    case 'queue':

      // aegir-specific
      $type = 'queue';
      break;
    case 'ok':
    case 'completed':
    case 'success':
      $type = 'success';
      break;
    case 'notice':
    case 'info':
    case 'message':
    default:
      $type = 'info';
  }
  return 'hosting-' . $type;
}

/**
 * Retrieve the latest task related to the specified platform, of a specific type
 *
 * This is used for documenting issues with verification.
 */
function hosting_get_most_recent_task($rid, $type) {
  $nid = db_result(db_query("SELECT t.nid FROM {hosting_task} t INNER JOIN {node} n ON n.vid = t.vid WHERE task_type='%s' and t.rid=%d ORDER BY t.vid DESC limit 1", $type, $rid));
  if ($nid) {
    return node_load($nid);
  }
  return false;
}

/**
 * Retrieve tasks with specified criterias
 *
 * @arg $filter_by string a field to filter the list with, unchecked
 * @arg $filter_value string what to restrict the field to, checked
 * @arg $count integer the number of tasks to return
 * @arg $element integer which element to start from
 */
function hosting_get_tasks($filter_by = null, $filter_value = null, $count = 5, $element = 0) {
  $nodes = array();
  $args[] = 'task';
  $cond = '';
  if ($filter_by && $filter_value) {
    $cond = ' AND t.' . $filter_by . ' = %d';
    $args[] = $filter_value;
  }
  $result = pager_query(db_rewrite_sql("SELECT n.*, t.task_status, t.task_type, t.rid FROM {node} n INNER JOIN {hosting_task} t on n.vid=t.vid WHERE type='%s'" . $cond . " ORDER BY n.vid DESC"), $count, $element, NULL, $args);
  while ($row = db_fetch_object($result)) {
    $nodes[] = $row;
  }
  return $nodes;
}

/**
 * Retrieve a list of outstanding tasks.
 *
 * @param limit
 *   The amount of items to return.
 * @return
 *   An associative array containing task nodes, indexed by node id.
 */
function _hosting_get_new_tasks($limit = 20) {
  $return = array();
  $result = db_query("SELECT t.nid\n    FROM {hosting_task} t\n    INNER JOIN {node} n ON t.vid = n.vid\n    WHERE t.task_status = %d GROUP BY t.rid ORDER BY n.changed, n.nid ASC LIMIT %d", HOSTING_TASK_QUEUED, $limit);
  while ($node = db_fetch_object($result)) {
    $return[$node->nid] = node_load($node->nid);
  }
  return $return;
}

/**
 * @name Error status definitions
 * @{
 * Bitmask values used to generate the error code to return.
 * @see drush_set_error(), drush_get_error(), drush_cmp_error()
 */

/**
 * The task is being processed
 */
define('HOSTING_TASK_PROCESSING', -1);

/**
 * The task is queued
 */
define('HOSTING_TASK_QUEUED', 0);

/**
 * The command completed succesfully.
 */
define('HOSTING_TASK_SUCCESS', 1);

/**
 * The command was not successfully completed. This is the default error
 * status.
 */
define('HOSTING_TASK_ERROR', 2);

/**
 * The command was completed successfully, but with warnings. This is requires
 * attention.
 */
define('HOSTING_TASK_WARNING', 3);

/**
 * @} End of "name Error status definitions".
 */

/**
 * Turn bitmask integer error code into associative array
 */
function _hosting_task_error_codes() {
  $codes = array(
    HOSTING_TASK_SUCCESS => t('Successful'),
    HOSTING_TASK_QUEUED => t('Queued'),
    HOSTING_TASK_ERROR => t('Failed'),
    HOSTING_TASK_PROCESSING => t('Processing'),
    HOSTING_TASK_WARNING => t('Warning'),
  );
  return $codes;
}

/**
 * Turn bitmask integer error code into translatable label.
 */
function _hosting_parse_error_code($code) {
  $messages = _hosting_task_error_codes();
  return $messages[$code];
}

/**
 * Return the status of the task matching the specification
 */
function hosting_task_status($filter_by, $filter_value, $type = 'install') {
  $args[] = 'task';
  $args[] = $type;
  $cond = '';
  if ($filter_by && $filter_value) {
    $cond = ' AND t.' . $filter_by . ' = %d';
    $args[] = $filter_value;
  }
  $result = db_fetch_array(db_query("SELECT t.task_status AS status FROM {node} n INNER JOIN {hosting_task} t on n.vid=t.vid WHERE n.type='%s' AND t.task_type='%s' " . $cond . " ORDER BY t.vid DESC", $args));
  return $result['status'];
}

/**
 * Return the status of a task in human-readable form
 *
 * @see hosting_task_status()
 */
function hosting_task_status_output($filter_by, $filter_value, $type = 'install') {
  $status = hosting_task_status($filter_by, $filter_value, $type);
  if (is_int($status)) {
    return _hosting_parse_error_code($status);
  }
  else {
    return $status;

    # should be NULL
  }
}

/**
 * Display list of tasks
 */
function hosting_task_list($filter_by = null, $filter_value = null) {
  return _hosting_task_list($filter_by, $filter_value, 25, 12, 'title');
}

/**
 * Implementation of hosting_hook_summary()
 */
function hosting_task_summary($filter_by = null, $filter_value = null) {
  modalframe_parent_js();
  $more_link = l(t('More tasks'), 'hosting/queues/tasks');
  return hosting_task_queue_block() . $more_link;
}

/**
 * Hosting task list queue block
 */
function hosting_task_queue_block() {
  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
  $settings['hostingTaskRefresh'] = array(
    'queueBlock' => 1,
  );
  drupal_add_js($settings, 'setting');
  $nodes = hosting_get_tasks('t.task-status', HOSTING_TASK_QUEUED, 5);
  $headers = array(
    t('Task'),
    t('Actions'),
  );
  $rows[] = array();
  foreach ($nodes as $node) {
    $row = array();
    $row['type'] = array(
      'data' => drupal_ucfirst(str_replace(array(
        '_',
        '-',
      ), ' ', $node->task_type)) . ' ' . _hosting_node_link($node->rid),
      'class' => 'hosting-status',
    );
    $log_button = _hosting_task_button(t('View'), 'node/' . $node->nid, t("Display the task log"), 'hosting-button-log', isset($node->nid) && user_access('access task logs'), true, false);
    $row['actions'] = array(
      'data' => $log_button,
      'class' => 'hosting-actions',
    );
    $class = hosting_task_status_class($node->task_status);
    $rows[] = array(
      'data' => $row,
      'class' => $class,
    );
  }
  return "<div id='hosting-task-queue-block'>" . theme('table', $headers, $rows, array(
    'class' => 'hosting-table',
  )) . "</div>";
}

/**
 * A concise table listing of the tasks affecting this node
 *
 * This shows a table view of the tasks relevant to this node. It will show
 * tasks that can be executed as well as tasks that have been in a single
 * simple interface.
 */
function hosting_task_table($node) {
  $output = '';
  $headers[] = t('Task');
  $headers[] = array(
    'data' => t('Actions'),
    'class' => 'hosting-actions',
  );
  $tasklist = hosting_task_fetch_tasks($node->nid);
  foreach ($tasklist as $task => $info) {
    $row = array();
    if (!isset($info['nid']) && !$info['task_permitted']) {

      // just don't show those tasks, since we'll not be able to run them
      continue;
    }
    $row['type'] = array(
      'data' => $info['title'],
      'class' => 'hosting-status',
    );
    $actions = array();
    if (isset($info['task_status']) && $info['task_status'] == 0) {
      $actions['cancel'] = _hosting_task_button(t('Cancel'), sprintf("hosting/tasks/%d/cancel", $info['nid']), t("Cancel the task and remove it from the queue"), 'hosting-button-stop', !$info['task_permitted']);
    }
    else {
      $actions['run'] = _hosting_task_button(t('Run'), sprintf("node/%d/%s_%s", $node->nid, $node->type, $task), $info['description'], 'hosting-button-run', $info['task_permitted'], $info['dialog']);
    }
    $actions['log'] = _hosting_task_button(t('View'), isset($info['nid']) ? 'node/' . $info['nid'] : '<front>', t("Display the task log"), 'hosting-button-log', isset($info['nid']) && user_access('access task logs'), TRUE, FALSE);
    $row['actions'] = array(
      'data' => implode('', $actions),
      'class' => 'hosting-actions',
    );
    $rows[] = array(
      'data' => $row,
      'class' => $info['class'],
    );
  }
  $output .= theme('table', $headers, $rows, array(
    'class' => 'hosting-table',
  ));
  return $output;
}
function _hosting_task_button($title, $link, $description, $class = '', $status = TRUE, $dialog = FALSE, $add_token = TRUE) {
  global $user;
  if ($status) {
    $classes[] = 'hosting-button-enabled';
    if (!empty($class)) {
      $classes[] = $class;
    }
    if ($dialog) {
      $classes[] = 'hosting-button-dialog';
    }
    $options['attributes'] = array(
      'title' => $description,
      'class' => implode(" ", $classes),
    );
    if ($add_token) {
      $options['query'] = array(
        'token' => drupal_get_token($user->uid),
      );
    }
    return l($title, $link, $options);
  }
  else {
    return "<span class='hosting-button-disabled'>" . $title . "</span>";
  }
}

/**
 * Theme a task list
 */
function _hosting_task_list($filter_by, $filter_value, $count = 5, $element = 0, $field = 'title', $skip = array(), $pager = TRUE) {
  $nodes = hosting_get_tasks($filter_by, $filter_value, $count, $element);
  if (!$nodes) {
    return t('No tasks available');
  }
  else {
    $headers[t('Task')] = '';
    foreach ($nodes as $node) {
      $row = array();
      if ($field == 'title') {
        $data = drupal_ucfirst($node->task_type) . ' ' . _hosting_node_link($node->rid);
      }
      else {
        $data = $node->{$field};
      }
      $row['type'] = array(
        'data' => $data,
        'class' => 'hosting-status',
      );
      if (!in_array('created', $skip)) {
        $row['created'] = t("@interval ago", array(
          '@interval' => format_interval(time() - $node->created, 1),
        ));
        $headers[t('Created')] = '';
      }
      $row['executed'] = t("@interval ago", array(
        '@interval' => format_interval(time() - $node->changed, 1),
      ));
      $headers[t('Executed')] = '';
      $headers[t('Actions')] = '';
      $actions['log'] = l(t('View'), 'node/' . $node->nid, array(
        'attributes' => array(
          'class' => 'hosting-button-dialog hosting-button-enabled hosting-button-log',
        ),
      ));
      $row['actions'] = array(
        'data' => $actions['log'],
        'class' => 'hosting-actions',
      );
      $class = hosting_task_status_class($node->task_status);
      $rows[] = array(
        'data' => $row,
        'class' => $class,
      );
    }
    $output = theme('table', array_keys($headers), $rows, array(
      'class' => 'hosting-table',
    ));
    if ($pager === TRUE) {
      $output .= theme('pager', NULL, $count, $element);
    }
    elseif (is_string($pager)) {
      $output .= $pager;
    }
    return $output;
  }
}
function hosting_task_fetch_tasks($rid) {
  $node = node_load($rid);
  $result = db_query("SELECT n.nid, t.task_type, t.task_status FROM {node} n INNER JOIN {hosting_task} t ON n.vid = t.vid \n    WHERE n.type = 'task' AND t.rid = %d\n    ORDER BY t.task_status ASC, n.changed DESC", $rid);
  while ($obj = db_fetch_object($result)) {
    $return[$obj->task_type] = array(
      'nid' => $obj->nid,
      'task_status' => $obj->task_status,
      'exists' => TRUE,
    );
  }
  $tasks = hosting_available_tasks($node->type);
  ksort($tasks);
  foreach ($tasks as $type => $hook_task) {
    if (!isset($return[$type])) {
      $return[$type] = array();
    }
    $access_callback = !empty($hook_task['access callback']) ? $hook_task['access callback'] : 'hosting_task_menu_access';
    $task = array();
    $task = array_merge($return[$type], $hook_task);
    $allowed = isset($task['exists']) && !in_array($task['task_status'], array(
      HOSTING_TASK_QUEUED,
      HOSTING_TASK_PROCESSING,
    )) || !isset($task['exists']);
    if ($allowed && empty($task['hidden']) && $access_callback($node, $type)) {
      $task['task_permitted'] = TRUE;
    }
    else {
      $task['task_permitted'] = FALSE;
    }

    // @TODO: Use task defaults array to prevent notices.
    if (!isset($task['dialog'])) {
      $task['dialog'] = FALSE;
    }
    if (!isset($task['task_status'])) {
      $task['task_status'] = NULL;
    }
    $task['class'] = hosting_task_status_class($task['task_status']);
    $return[$type] = $task;
  }
  return $return;
}
function hosting_task_status_class($status = null) {
  $class = null;
  if (!is_null($status)) {
    switch ($status) {
      case HOSTING_TASK_SUCCESS:
        $class = 'hosting-success';
        break;
      case HOSTING_TASK_ERROR:
        $class = 'hosting-error';
        break;
      case HOSTING_TASK_QUEUED:
        $class = 'hosting-queue';
        break;
      case HOSTING_TASK_PROCESSING:
        $class = 'hosting-processing';
        break;
      case HOSTING_TASK_WARNING:
        $class = 'hosting-warning';
        break;
    }
  }
  return $class;
}

/**
 * Views integration
 */
function hosting_task_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'hosting_task'),
  );
}

/**
 * Implements hook_preprocess_views_view_table().
 */
function hosting_task_preprocess_views_view_table(&$vars) {
  $id = "{$vars['view']->name}-{$vars['view']->current_display}";
  switch ($id) {
    case 'hosting_task_list-block':
      modalframe_parent_js();
      drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
      $settings['hostingTaskRefresh'] = array(
        'queueBlock' => 1,
      );
      drupal_add_js($settings, 'setting');
      break;
  }
}

/**
 * Set a task's status according to its log.
 *
 * @param A task object or the revision ID of a task node.
 *
 * @return The task's status error code.
 */
function hosting_task_update_status($task) {
  if (is_numeric($task)) {
    $vid = $task;
  }
  else {
    $vid = $task->vid;
  }

  // Get the status of the task by parsing the log.
  $status = hosting_task_parse_log($vid);
  db_query('UPDATE {hosting_task} SET task_status=%d WHERE vid = %d', $status, $vid);

  // Invoke hook_hosting_task_update_status()
  module_invoke_all('hosting_task_update_status', $task, $status);
  return $status;
}

/**
 * Parse a task's log, and return its status accordingly.
 *
 * @param The revision ID of a task node.
 *
 * @return The task's status error code.
 */
function hosting_task_parse_log($vid) {

  // Assume the best
  $update = HOSTING_TASK_SUCCESS;
  $result = db_query('SELECT type FROM {hosting_task_log} WHERE vid = %d', $vid);
  while ($status = db_result($result)) {
    if ($status == 'error' || $status == 'failed') {
      $update = HOSTING_TASK_ERROR;
    }
    elseif ($update != HOSTING_TASK_ERROR && $status == 'warning') {
      $update = HOSTING_TASK_WARNING;
    }
  }
  return $update;
}

Functions

Namesort descending Description
hosting_add_task Helper function to generate new task node
hosting_available_tasks Get a list of tasks that can be invoked by the user.
hosting_get_most_recent_task Retrieve the latest task related to the specified platform, of a specific type
hosting_get_tasks Retrieve tasks with specified criterias
hosting_nodeapi_task_delete_revision Implementation of hook_delete_revision()
hosting_tasks_queue Process the hosting task queue.
hosting_task_access Implementation of hook_access()
hosting_task_ajax_list
hosting_task_ajax_queue
hosting_task_cancel
hosting_task_cancel_access
hosting_task_confirm_form Implementation of hook_form().
hosting_task_confirm_form_submit Generic form submit handler for tasks confirmation
hosting_task_count Return the amount of items still in the queue
hosting_task_count_running Return the amount of items running in the queue
hosting_task_delete Implementation of hook_delete().
hosting_task_fetch_tasks
hosting_task_hosting_queues Implementation of hook_hosting_queues
hosting_task_insert Implementation of hook_insert().
hosting_task_list Display list of tasks
hosting_task_load Implementation of hook_load().
hosting_task_log Insert an entry in the task log.
hosting_task_menu Implementation of hook_menu().
hosting_task_menu_access Task access controls
hosting_task_menu_access_csrf Access callback helper for hosting task menu items.
hosting_task_node_info Implementation of hook_node_info()
hosting_task_outstanding Determine whether there is an outstanding task of a specific type.
hosting_task_parse_log Parse a task's log, and return its status accordingly.
hosting_task_perm Implementation of hook_perm()
hosting_task_preprocess_views_view_table Implements hook_preprocess_views_view_table().
hosting_task_queue_block Hosting task list queue block
hosting_task_restore_form Customize the task confirmation form for restore.
hosting_task_restore_form_validate Implementation of hook_form_alter()
hosting_task_retry Retry the given task
hosting_task_retry_form
hosting_task_retry_form_submit
hosting_task_set_title Set the title of tasks automatically and in a consistent way
hosting_task_status Return the status of the task matching the specification
hosting_task_status_class
hosting_task_status_output Return the status of a task in human-readable form
hosting_task_summary Implementation of hosting_hook_summary()
hosting_task_table A concise table listing of the tasks affecting this node
hosting_task_update Implementation of hook_update().
hosting_task_update_status Set a task's status according to its log.
hosting_task_update_status_form
hosting_task_update_status_form_submit
hosting_task_view Implementation of hook_view().
hosting_task_views_api Views integration
_hosting_get_new_tasks Retrieve a list of outstanding tasks.
_hosting_parse_error_code Turn bitmask integer error code into translatable label.
_hosting_task_button
_hosting_task_error_codes Turn bitmask integer error code into associative array
_hosting_task_list Theme a task list
_hosting_task_log_class Map entry statuses to coincide with our CSS classes.
_hosting_task_log_table Display table containing the logged information for this task

Constants

Namesort descending Description
HOSTING_TASK_ERROR The command was not successfully completed. This is the default error status.
HOSTING_TASK_PROCESSING The task is being processed
HOSTING_TASK_QUEUED The task is queued
HOSTING_TASK_SUCCESS The command completed succesfully.
HOSTING_TASK_WARNING The command was completed successfully, but with warnings. This is requires attention.