You are here

hosting_task.module in Hosting 7.4

Web server node type is defined here.

File

task/hosting_task.module
View source
<?php

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

/**
 * Implements hook_init().
 *
 * Adds refreshTimeout javascript variable.
 */
function hosting_task_init() {
  $settings['hostingTaskRefresh'] = array(
    'refreshTimeout' => variable_get('hosting_task_refresh_timeout', 30000),
  );
  drupal_add_js($settings, 'setting');
}

/**
 * Implements 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("hosting_confirm/%%hosting_%s_wildcard/%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' => 'Task list',
    'description' => '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' => 'Task list',
    'description' => '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' => 'Task list',
    'description' => 'AJAX callback for refreshing task queue',
    'page callback' => 'hosting_task_ajax_queue',
    'access arguments' => array(
      'access task logs',
    ),
    'type' => MENU_CALLBACK,
  );

  // Custom path to task node views for overlay.
  // See hosting_task_overlay_paths().
  $items['hosting/task/%node'] = array(
    'page callback' => 'node_page_view',
    'page arguments' => array(
      2,
    ),
    'access arguments' => array(
      'access task logs',
    ),
  );
  $items['hosting/task/log/ajax/%node/%/%'] = array(
    'page callback' => 'hosting_task_log_ajax',
    'page arguments' => array(
      4,
      5,
      6,
    ),
    'access arguments' => array(
      'access task logs',
    ),
    'delivery callback' => 'ajax_deliver',
  );
  return $items;
}

/**
 * Read additional task log data, served via AJAX.
 */
function hosting_task_log_ajax($node, $last_position, $id) {
  $commands = array();

  // Long polling for ten seconds.
  $expire = time() + 10;
  $table = FALSE;
  while ((empty($table) || !count($table['#rows'])) && time() < $expire) {
    usleep(200000);
    $table = _hosting_task_log_table($node, $last_position);
  }
  if (isset($table['#refresh_url'])) {
    $url = $table['#refresh_url'];
  }
  else {
    $url = url('hosting/task/log/ajax/' . $node->nid . '/' . $last_position);
  }
  if (!empty($table)) {
    unset($table['#header']);
    $commands[] = hosting_task_ajax_command_hosting_table_append('#' . $id, drupal_render($table));
  }
  if (!hosting_task_task_has_finished($node)) {
    $commands[] = hosting_task_ajax_command_hosting_table_check('#' . $id, $url);
  }
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}

/**
 * Determine if the given task has finished and completed.
 *
 * @param $task
 *   The task node to check.
 *
 * @return bool
 *   True if task has finished, false otherwise.
 */
function hosting_task_task_has_finished($task) {

  // Check to see if the execution time of task has been set, and if it has
  // allow an additional 30 seconds for the logs to be entered into the DB.
  return !empty($task->delta) && $task->executed + $task->delta + 30 < REQUEST_TIME;
}

/**
 * Prepare a JS command array for AJAX to append to a hosting task log table.
 *
 * @param string $selector
 *   HTML css selector of the table to append entries to.
 * @param string $html
 *   HTML table of rows to append.
 * @param array $settings
 *   Extra settings to add to Drupal.settings.
 *
 * @return array
 *   A JS command array.
 */
function hosting_task_ajax_command_hosting_table_append($selector, $html, $settings = NULL) {
  return array(
    'command' => 'hosting_table_append',
    'selector' => $selector,
    'data' => $html,
    'settings' => $settings,
  );
}

/**
 * Prepare a JS command array for AJAX to poll for additional log entries.
 *
 * @param string $selector
 *   HTML css selector of the log entries table.
 * @param string $url
 *   URL to poll for changes.
 * @param array $settings
 *   Extra settings to add to Drupal.settings.
 *
 * @return array
 *   A JS command array.
 */
function hosting_task_ajax_command_hosting_table_check($selector, $url, $settings = NULL) {
  return array(
    'command' => 'hosting_table_check',
    'selector' => $selector,
    'url' => $url,
    'settings' => $settings,
  );
}

/**
 * Page callback to provide JSON output for a task.
 */
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_output($return);
  exit;
}

/**
 * AJAX callback for refreshing task list.
 */
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_output($return);
  exit;
}

/**
 * Cancel a task before it's started.
 */
function hosting_task_cancel($node) {
  if ($node->type == 'task') {
    if ($node->task_status == HOSTING_TASK_QUEUED || $node->task_status == HOSTING_TASK_PROCESSING) {
      $node->task_status = HOSTING_TASK_WARNING;
      node_save($node);
      if (module_exists('hosting_queued')) {

        // Release the hosting_queued lock.
        // Cannot use lock release because it uses semaphore value, which only works in a single request.
        global $locks;
        unset($locks['hosting_queue_tasks_running']);
        db_delete('semaphore')
          ->condition('name', 'hosting_queue_tasks_running')
          ->execute();
      }

      // Log the cancellation.
      hosting_task_log($node->vid, 'warning', t("Task was cancelled."));
      drupal_goto('node/' . $node->rid);
    }
    else {
      drupal_set_message(t('This task is not running or queued. It cannot be cancelled.'), 'warning');
      drupal_goto('node/' . $node->nid);
    }
  }
}

/**
 * Implements hook_access().
 *
 * @param $node object
 *   the node object we're trying to 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;
      }

      // @TODO: Un-hardcode this?
      $safe_tasks = array(
        'backup',
        'backup-delete',
        'verify',
        'enable',
        'deploy',
      );
      if (!in_array($task, $safe_tasks)) {

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

      // @TODO: Do not allow on protected sites.
      // @TODO: Improve UI to inform users this is a reinstall and data will be lost.
      // @TODO: Reduce tech debt, do not add to it.
      $installable = $task == "install";
      $delete_or_enable = $deletable || $enabable || $installable;

      // 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 $installable && variable_get('hosting_allow_reinstall', TRUE) || !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 {

        // If the platform's unlocked, we can lock it, delete it or batch migrate sites
        $platform_tasks = array(
          'verify',
          'lock',
          'delete',
          'migrate',
        );
      }
      return in_array($task, $platform_tasks);
    }
    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);
}

/**
 * Implements hook_node_info().
 */
function hosting_task_node_info() {
  $types["task"] = array(
    "type" => 'task',
    "name" => t('Task'),
    'base' => 'hosting_task',
    "has_title" => FALSE,
    "title_label" => '',
    "description" => hosting_node_help("task"),
    "has_body" => 0,
    "body_label" => '',
    "min_word_count" => 0,
  );
  return $types;
}

/**
 * Implements hook_node_access().
 */
function hosting_task_node_access($node, $op, $account) {
  if (hosting_feature('client')) {

    // We rely on hosting_client_node_grants() instead of global configuration.
    return NODE_ACCESS_IGNORE;
  }
  $type = is_string($node) ? $node : $node->type;
  if ($type != 'task') {
    return NODE_ACCESS_IGNORE;
  }
  if (user_access('administer tasks', $account)) {
    return NODE_ACCESS_ALLOW;
  }
}

/**
 * Implements hook_permission().
 */
function hosting_task_permission() {
  return array(
    'administer tasks' => array(
      'title' => t('administer tasks'),
    ),
    'create backup task' => array(
      'title' => t('create backup task'),
    ),
    'create restore task' => array(
      'title' => t('create restore task'),
    ),
    'create deploy task' => array(
      'title' => t('create deploy task'),
    ),
    'create disable task' => array(
      'title' => t('create disable task'),
    ),
    'create enable task' => array(
      'title' => t('create enable task'),
    ),
    'create delete task' => array(
      'title' => t('create delete task'),
    ),
    'create verify task' => array(
      'title' => t('create verify task'),
    ),
    'create lock task' => array(
      'title' => t('create lock task'),
    ),
    'create unlock task' => array(
      'title' => t('create unlock task'),
    ),
    'create login-reset task' => array(
      'title' => t('create login-reset task'),
    ),
    'create backup-delete task' => array(
      'title' => t('create backup-delete task'),
    ),
    'view own tasks' => array(
      'title' => t('view own tasks'),
    ),
    'view task' => array(
      'title' => t('view task'),
    ),
    'access task logs' => array(
      'title' => t('access task logs'),
    ),
    'retry failed tasks' => array(
      'title' => t('retry failed tasks'),
    ),
    'cancel own tasks' => array(
      'title' => t('cancel own tasks'),
    ),
    'update status of tasks' => array(
      'title' => t('update status of tasks'),
    ),
  );
}

/**
 * Implements 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 : REQUEST_TIME;

  // We need to insert the nid in addition to the vid, so look it up.
  if (!isset($nids[$vid])) {
    $nids[$vid] = (int) db_query('SELECT nid FROM {hosting_task} WHERE vid = :vid', array(
      ':vid' => $vid,
    ))
      ->fetchField();
  }
  $id = db_insert('hosting_task_log')
    ->fields(array(
    'vid' => $vid,
    'nid' => $nids[$vid],
    'type' => $type,
    'message' => $message,
    'error' => isset($error) ? $error : '',
    'timestamp' => $timestamp,
  ))
    ->execute();
}

/**
 * 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->nid = NULL;
    $node->vid = NULL;
    $node->original = NULL;
    $node->revision = TRUE;
    $node->created = REQUEST_TIME;
    $node->changed = REQUEST_TIME;
    $node->task_status = HOSTING_TASK_QUEUED;
    node_save($node);
    drupal_goto("node/{$node->nid}");
  }
}

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

  // Guard against destructive tasks run on guarded nodes.
  if (!hosting_task_dangerous_task_is_allowed($type, $nid)) {
    return FALSE;
  }
  $node = db_query("SELECT nid, uid, title FROM {node} WHERE nid = :nid", array(
    ':nid' => $nid,
  ))
    ->fetchObject();
  $task = new stdClass();
  $task->language = LANGUAGE_NONE;
  $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);
  return $task;
}

/**
 * Implements hook_form().
 */
function hosting_task_confirm_form($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);
    if ($task == 'delete') {

      // We're deleting a Hosting object, and thus its corresponding node. If
      // we stay on the current page, we'll end up with a 404, so redirect to
      // the home page.
      drupal_set_message(t(':title has been queued for deletion.', array(
        ':title' => $node->title,
      )));
      drupal_goto();
    }
    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,
  );

  // Invoke hosting_task_TASK_TYPE_form.
  $func = 'hosting_task_' . str_replace('-', '_', $task) . '_form';
  if (function_exists($func)) {
    $form['parameters'] += $func($node);
  }

  // Invoke hosting_task_TASK_TYPE_form_validate.
  $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">';
  $form['actions']['#suffix'] = '</div>';
  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 (count($list)) {
    $form['bid'] = array(
      '#type' => 'radios',
      '#title' => t('Backups'),
      '#options' => $list,
      '#required' => TRUE,
    );
  }
  else {
    $form['no_backups'] = array(
      '#type' => 'item',
      '#title' => t('Backups'),
      '#markup' => t('There are no valid backups available.'),
    );
  }
  return $form;
}

/**
 * Implements hook_form_alter().
 *
 * TODO: Move restore functions to hosting_site, where backup functions are.
 */
function hosting_task_restore_form_validate($form, &$form_state) {
  if (isset($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'];
  $parameters = isset($values['parameters']) ? $values['parameters'] : array();
  hosting_add_task($values['nid'], $values['task'], $parameters);
  $form_state['redirect'] = 'node/' . $values['nid'];
  if (module_exists('overlay')) {
    overlay_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 = node_load($node->rid);
  $tasks = hosting_available_tasks($ref->type);
  $ref_title = $ref->type == 'site' ? hosting_site_canonical_url($ref) : $ref->title;
  $node->title = drupal_ucfirst($tasks[$node->task_type]['title']) . ': ' . $ref_title;
  db_update('node')
    ->fields(array(
    'title' => $node->title,
  ))
    ->condition('nid', $node->nid)
    ->execute();
  db_update('node_revision')
    ->fields(array(
    'title' => $node->title,
  ))
    ->condition('vid', $node->vid)
    ->execute();
}

/**
 * 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;
  $tasks = hosting_get_new_tasks($count);
  if (count($tasks)) {
    drush_log(dt("Found @count tasks (max @max) in queue. Running...", array(
      '@count' => count($tasks),
      '@max' => $count,
    )), "ok");
  }
  else {
    drush_log(dt("Found no tasks in queue. Not running."), "ok");
  }
  foreach ($tasks as $task) {
    hosting_task_execute($task, array(
      'fork' => drush_get_option('fork', TRUE),
    ));
  }
}

/**
 * Executes a task while logging to watchdog and drush.
 * Implemented by drush hosting-tasks and hosting-queued commands.
 *
 * @param $task
 *   A fully loaded task node.
 *
 * @param $backend_options
 *   @see drush_invoke_process().
 */
function hosting_task_execute($task, $backend_options = array()) {

  // Log in watchdog and drush.
  $t = array(
    '@title' => $task->title,
    '@nid' => $task->nid,
    '@url' => url("node/{$task->nid}", array(
      'absolute' => true,
    )),
  );
  watchdog('hosting_task', 'Starting task "@title": @url', $t, WATCHDOG_NOTICE, url("node/{$task->nid}"));
  drush_log(dt('Starting task "@title": @url', $t), 'ok');

  // Execute in it's own process.
  drush_invoke_process('@self', "hosting-task", array(
    $task->nid,
  ), array(
    'strict' => FALSE,
  ), $backend_options);

  // Log a message, depending on forked process or not.
  // If forked, the process may not have completed yet, so we should change the message.
  if (isset($backend_options['fork']) && $backend_options['fork']) {
    drush_log(dt('Launched task "@title" in a forked process: @url', $t), 'ok');
  }
  else {
    $task = node_load($task->nid, NULL, TRUE);
    $t['@status'] = _hosting_parse_error_code($task->task_status);
    $t['@duration'] = format_interval($task->delta, 1);
    drush_log(dt('Finished task "@title" with status "@status" in @duration: @url', $t), 'ok');
  }
}

/**
 * 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_query("\n      SELECT t.nid FROM {hosting_task} t\n        INNER JOIN {node} n ON t.vid = n.vid\n      WHERE\n        t.rid = :rid\n        AND (t.task_status = :status_queued OR t.task_status = :status_processing)\n        AND t.task_type = :type\n        ORDER BY t.vid DESC\n        LIMIT 1", array(
    ':rid' => $nid,
    ':status_queued' => HOSTING_TASK_QUEUED,
    ':status_processing' => HOSTING_TASK_PROCESSING,
    ':type' => $type,
  ))
    ->fetchField();
  return $return;
}

/**
 * Return the amount of items still in the queue.
 */
function hosting_task_count() {
  $result = db_query("SELECT COUNT(t.vid) FROM {hosting_task} t INNER JOIN {node} n ON t.vid = n.vid WHERE t.task_status = :task_status", array(
    ':task_status' => HOSTING_TASK_QUEUED,
  ))
    ->fetchField();
  return $result;
}

/**
 * Return the amount of items running in the queue.
 */
function hosting_task_count_running() {
  return db_query("SELECT COUNT(t.nid) FROM {node} n INNER JOIN {hosting_task} t ON n.vid = t.vid WHERE type = :type AND t.task_status = :task_status AND t.executed > (UNIX_TIMESTAMP() - :timeout)", array(
    ':type' => 'task',
    ':task_status' => HOSTING_TASK_PROCESSING,
    ':timeout' => 28800,
  ))
    ->fetchField();
}

/**
 * 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 string|null $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 bool $reset
 *   This function has an internal static cache, to reset the cache, pass in
 *   TRUE.
 *
 * @return array
 *   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 (!count($cache) || $reset) {

    // Invoke hook_hosting_tasks().
    $cache = module_invoke_all('hosting_tasks');

    // Invoke hook_hosting_tasks_alter().
    drupal_alter('hosting_tasks', $cache);
  }
  if (isset($type)) {
    return $cache[$type];
  }
  else {
    return $cache;
  }
}

/**
 * Implements hook_hosting_tasks_alter()
 *
 * Set 'command' property for tasks that don't implement it.
 */
function hosting_task_hosting_tasks_alter(&$cache) {

  // For any task type that doesn't use "command", add it as 'provision-$TYPE'.
  foreach ($cache as &$tasks) {
    foreach ($tasks as $type => &$task_type) {
      if (!isset($task_type['command']) || empty($task_type['command'])) {
        $task_type['command'] = 'provision-' . $type;
      }
    }
  }
}

/**
 * Implements hook_action_info().
 */
function hosting_task_action_info() {
  $available_tasks = hosting_available_tasks();
  $actions = array();
  foreach ($available_tasks as $module => $tasks) {
    foreach ($tasks as $task => $task_info) {

      // Search for dashes an replace accordingly.
      $task = str_replace('-', '_', $task);
      $function = 'hosting_' . $module . '_' . $task . '_action';
      if (function_exists($function)) {
        $label = ucwords($module . ': ' . $task);
        $actions[$function] = array(
          'type' => 'node',
          'label' => t($label),
          'configurable' => FALSE,
          'behavior' => array(
            'none',
          ),
        );
      }
    }
  }
  return $actions;
}

/**
 * Implements hook_insert().
 */
function hosting_task_insert($node) {
  $node->executed = isset($node->executed) ? $node->executed : NULL;
  $node->delta = isset($node->delta) ? $node->delta : NULL;
  $id = db_insert('hosting_task')
    ->fields(array(
    'vid' => $node->vid,
    'nid' => $node->nid,
    'task_type' => $node->task_type,
    'task_status' => $node->task_status,
    'rid' => $node->rid,
    'executed' => $node->executed,
    'delta' => $node->delta,
  ))
    ->execute();

  // For new delete tasks, save the hosting_name into a task argument.
  if ($node->task_type == 'delete' && !empty($node->ref_context)) {
    $node->task_args['hosting_context'] = $node->ref_context;
  }
  if (isset($node->task_args) && is_array($node->task_args)) {
    foreach ($node->task_args as $key => $value) {
      $id = db_insert('hosting_task_arguments')
        ->fields(array(
        'vid' => $node->vid,
        'nid' => $node->nid,
        'name' => $key,
        'value' => $value,
      ))
        ->execute();
    }
  }

  // If this is a delete task that was just inserted, delete the context so
  // another entity can claim the namespace immediately.
  if ($node->task_type == 'delete' && !empty($node->ref_context)) {
    hosting_context_delete($node->rid);

    // Add a new permanent path alias for the node.
    $path = array(
      'source' => "node/{$node->rid}",
      'alias' => "hosting/c/{$node->ref_context}/{$node->rid}",
    );
    path_save($path);
  }
  module_invoke_all('hosting_task_update_status', $node, $node->task_status);
  hosting_task_set_title($node);
}

/**
 * Implements 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_update('hosting_task')
      ->fields(array(
      'nid' => $node->nid,
      'task_type' => $node->task_type,
      'task_status' => $node->task_status,
      'rid' => $node->rid,
      'executed' => $node->executed,
      'delta' => $node->delta,
    ))
      ->condition('vid', $node->vid)
      ->execute();
    if (isset($node->task_args) && is_array($node->task_args)) {

      // Wipe out old arguments first, since arguments could theoretically be removed.
      db_delete('hosting_task_arguments')
        ->condition('vid', $node->vid)
        ->execute();
      foreach ($node->task_args as $key => $value) {
        $id = db_insert('hosting_task_arguments')
          ->fields(array(
          'vid' => $node->vid,
          'nid' => $node->nid,
          'name' => $key,
          'value' => $value,
        ))
          ->execute();
      }
    }
    module_invoke_all('hosting_task_update_status', $node, $node->task_status);
  }
}

/**
 * Implements hook_delete_revision().
 */
function hosting_nodeapi_task_delete_revision(&$node) {
  db_delete('hosting_task')
    ->condition('vid', $node->vid)
    ->execute();
  db_delete('hosting_task_arguments')
    ->condition('vid', $node->vid)
    ->execute();
  db_delete('hosting_task_log')
    ->condition('vid', $node->vid)
    ->execute();
}

/**
 * Implements hook_delete().
 */
function hosting_task_delete($node) {
  db_delete('hosting_task')
    ->condition('nid', $node->nid)
    ->execute();
  db_delete('hosting_task_arguments')
    ->condition('nid', $node->nid)
    ->execute();
  db_delete('hosting_task_log')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Delete tasks related to a given site, platform, server, etc.
 */
function hosting_task_delete_related_tasks($nid) {
  $result = db_query("SELECT distinct nid FROM {hosting_task} WHERE rid = :rid", array(
    ':rid' => $nid,
  ));
  foreach ($result as $node) {
    node_delete($node->nid);
  }
}

/**
 * Implements hook_load().
 */
function hosting_task_load($nodes) {

  // Invoke hook_hosting_tasks().
  $types = module_invoke_all('hosting_tasks');

  // Invoke hook_hosting_tasks_alter().
  drupal_alter('hosting_tasks', $types);
  foreach ($nodes as $nid => &$node) {
    $additions = db_query('SELECT task_type, executed, delta, rid, task_status, r.type as ref_type FROM {hosting_task} t LEFT JOIN {node} r ON t.rid = r.nid WHERE t.vid = :vid', array(
      ':vid' => $node->vid,
    ))
      ->fetch();
    $result = db_query("SELECT name, value FROM {hosting_task_arguments} WHERE vid = :vid", array(
      ':vid' => $node->vid,
    ));
    if ($result) {
      $additions->task_args = array();
      while ($arg = $result
        ->fetch()) {
        $additions->task_args[$arg->name] = $arg->value;
      }
    }
    foreach ($additions as $property => &$value) {
      $node->{$property} = is_numeric($value) ? (int) $value : $value;
    }

    // Load task_command property from hook_hosting_tasks data.
    $node->task_command = $types[$node->ref_type][$node->task_type]['command'];
  }
}

/**
 * Adds a retry button to failed task nodes.
 */
function hosting_task_retry_form($form, $form_state, $nid) {
  $form['#prefix'] = '<div class="hosting-task-retry">';
  $form['task'] = array(
    '#type' => 'hidden',
    '#default_value' => $nid,
  );

  // Decide on button label.
  $task = node_load($nid);
  $button = t('Retry');
  if ($task->task_type == 'install' && $task->task_status != HOSTING_TASK_ERROR) {
    $button = t('Reinstall');
  }
  elseif ($task->task_status == HOSTING_TASK_SUCCESS || $task->task_status == HOSTING_TASK_WARNING) {
    $button = t('Run Again');
  }
  $form['retry'] = array(
    '#type' => 'submit',
    '#value' => $button,
  );
  $form['#suffix'] = '</div>';
  return $form;
}

/**
 * Submit handler for the task retry button.
 */
function hosting_task_retry_form_submit($form, &$form_state) {
  hosting_task_retry($form_state['values']['task']);
  if (module_exists('overlay')) {
    overlay_close_dialog();
  }
}

/**
 * Adds button to update the status of task nodes stuck in the 'processing' state.
 */
function hosting_task_update_status_form($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;
}

/**
 * Submit handler for the 'update status' button.
 */
function hosting_task_update_status_form_submit($form, &$form_state) {
  hosting_task_update_status($form_state['values']['task']);
  if (module_exists('overlay')) {
    overlay_close_dialog();
  }
  if (module_exists('hosting_queued')) {

    // Release the hosting_queued lock.
    // Cannot use lock release because it uses semaphore value, which only works in a single request.
    global $locks;
    unset($locks['hosting_queue_tasks_running']);
    db_delete('semaphore')
      ->condition('name', 'hosting_queue_tasks_running')
      ->execute();
  }
}

/**
 * Implements hook_view().
 */
function hosting_task_view($node, $view_mode, $langcode = NULL) {
  drupal_add_js(drupal_get_path('module', 'hosting') . '/hosting.js');
  $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),
    '#markup' => _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'),
        '#markup' => format_date($node->executed),
        '#weight' => 1,
      );
      $node->content['info']['delta'] = array(
        '#type' => 'item',
        '#title' => t('Processing time'),
        '#markup' => format_interval(REQUEST_TIME - $node->executed),
        '#weight' => 2,
      );
    }
    elseif ($node->executed != NULL) {
      $node->content['info']['executed'] = array(
        '#type' => 'item',
        '#title' => t('Executed'),
        '#markup' => format_date($node->executed),
        '#weight' => 1,
      );
      $node->content['info']['delta'] = array(
        '#type' => 'item',
        '#title' => t('Execution time'),
        '#markup' => 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'),
      '#markup' => 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(REQUEST_TIME, 'custom', 'H:i:sO'),
      )),
    );
  }
  if ($node->task_status) {
    $node->content['info']['status'] = array(
      '#type' => 'item',
      '#title' => t('Status'),
      '#markup' => _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->task_status == HOSTING_TASK_WARNING || $node->task_status == HOSTING_TASK_SUCCESS)) {
    $node->content['retry'] = array(
      'form' => drupal_get_form('hosting_task_retry_form', $node->nid),
      '#weight' => 5,
    );

    // Change the name of the form button to "Run Again" if the task ended ok.
    if ($node->task_status == HOSTING_TASK_WARNING || $node->task_status == HOSTING_TASK_SUCCESS) {
      $node->content['retry']['form']['retry']['#value'] = t('Run Again');
    }
  }
  if (user_access('update status of tasks') && $node->task_status == HOSTING_TASK_PROCESSING) {
    $node->content['update-status'] = array(
      'form' => drupal_get_form('hosting_task_update_status_form', $node->vid),
      '#weight' => 5,
    );
  }
  if (user_access('access task logs')) {
    if (in_array($node->task_status, array(
      HOSTING_TASK_ERROR,
      HOSTING_TASK_WARNING,
    ))) {
      $url_options = array(
        'attributes' => array(
          'class' => array(
            'hosting-button-enabled',
          ),
          'target' => '_self',
        ),
        'fragment' => 'warning',
      );
      if (module_exists('overlay') && overlay_get_mode() == 'child') {
        $url_options['query'] = array(
          'render' => 'overlay',
        );
      }
      $node->content['jump-link-warning'] = array(
        '#markup' => '<div>' . l(t('Jump to first warning'), request_path(), $url_options) . '</div>',
        '#weight' => 8,
      );
    }
    if ($node->task_status == HOSTING_TASK_ERROR) {
      $url_options['fragment'] = 'error';
      $node->content['jump-link-error'] = array(
        '#markup' => '<div>' . l(t('Jump to first error'), request_path(), $url_options) . '</div>',
        '#weight' => 9,
      );
    }
    if ($table = _hosting_task_log_table($node)) {
      $node->content['hosting_log'] = array(
        '#weight' => 10,
        'table' => $table,
      );
    }
  }
  return $node;
}

/**
 * Display table containing the logged information for this task.
 */
function _hosting_task_log_table($node, $last_position = 0) {
  $types = variable_get('hosting_task_logs_types_display', hosting_log_types());
  $query_args = array(
    ':vid' => $node->vid,
    ':lid' => $last_position,
    ':types' => $types,
  );
  $result = db_query("SELECT * FROM {hosting_task_log} WHERE vid = :vid AND lid > :lid AND type IN (:types) ORDER BY lid", $query_args);
  if ($result) {
    $headers = array(
      'data' => 'Log message',
      'execution_time' => 'Execution time',
      '',
    );
    $rows = array();
    $last_lid = $last_position;
    $last_timestamp = 0;
    $exec_time = '';
    $row_count = -1;
    foreach ($result as $entry) {
      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);
      }

      // Add error and warning anchors, so we can provide a quick link to them.
      if ($entry->type == 'error') {
        $message = '<a name="error"></a>' . $message;
      }
      elseif ($entry->type == 'warning') {
        $message = '<a name="warning"></a>' . $message;
      }

      // Add the exec_time to the previous row
      $exec_time = $entry->timestamp - $last_timestamp;

      // "1" is unreliable because timestamps don't allow sub-second granularity.
      if ($exec_time < 1) {
        $exec_time = '<div>-</div>';
      }
      elseif ($exec_time == 1) {
        $exec_time = '<div title="Many tasks take less than 1 second to perform. This duration represents an aggregate of the preceding tasks\' duration."><strong>' . $exec_time . ' s.</strong></div>';
      }
      else {
        $exec_time = '<div><strong>' . $exec_time . ' s.</strong></div>';
      }
      if ($row_count > -1) {
        $rows[$row_count]['data'][1] = array(
          'data' => $exec_time,
        );
      }
      $row_count++;
      $last_timestamp = $entry->timestamp;
      $row = array(
        array(
          'data' => $message,
          'class' => array(
            'hosting-status',
          ),
        ),
        '',
        $entry->type,
      );
      $rows[] = array(
        'data' => $row,
        'class' => array(
          _hosting_task_log_class($entry->type),
        ),
      );

      // Record that we've seen this log row.
      $last_lid = $entry->lid;
    }
    $table = array(
      '#theme' => "table",
      '#header' => $headers,
      '#rows' => $rows,
      '#attributes' => array(
        'id' => 'hosting-task-log',
        'class' => array(
          'hosting-table',
        ),
      ),
    );

    // If the task has not finished executing, update via AJAX.
    if (!hosting_task_task_has_finished($node)) {
      $table += array(
        '#refresh_url' => url('hosting/task/log/ajax/' . $node->nid . '/' . $last_lid),
        '#attached' => array(
          'js' => array(
            array(
              'data' => array(
                'hosting_task' => array(
                  'refresh_url' => url('hosting/task/log/ajax/' . $node->nid . '/' . $last_lid),
                ),
              ),
              'type' => 'setting',
            ),
            drupal_get_path('module', 'hosting_task') . '/js/hosting_task.table.js',
          ),
          'library' => array(
            array(
              'system',
              'drupal.ajax',
            ),
          ),
        ),
      );
    }
    return $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 = null) {
  $query = db_select('hosting_task', 'ht')
    ->fields('ht', [
    'nid',
  ])
    ->condition('rid', $rid)
    ->orderBy('vid', 'DESC');
  if ($type) {
    $query
      ->condition('task_type', $type);
  }
  $nid = $query
    ->execute()
    ->fetchField();
  if ($nid) {
    return node_load($nid, NULL, TRUE);
  }
}

/**
 * Retrieve tasks with specified criteria.
 *
 * @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();
  $query = db_select('node', 'n')
    ->extend('PagerDefault');
  $query
    ->element($element);
  $query
    ->join('hosting_task', 't', 'n.vid=t.vid');
  $query = $query
    ->fields('n')
    ->fields('t', array(
    'task_status',
    'task_type',
    'rid',
  ))
    ->condition('type', 'task')
    ->orderBy('n.vid', 'DESC')
    ->limit($count)
    ->addTag('node_access');
  if (isset($filter_by) && isset($filter_value)) {
    $query = $query
      ->condition($filter_by, $filter_value);
  }
  $result = $query
    ->execute();
  foreach ($result as $row) {
    $nodes[] = $row;
  }
  return $nodes;
}

/**
 * Retrieve a list of outstanding, queued, tasks.
 *
 * @param int $limit
 *   The maximum number of tasks to return.
 * @param null $rid
 *   The NID of the referenced Node to filter by.
 * @param null $task_type
 *   The task type to filter by.
 *
 * @return array
 *   An associative array containing task nodes, indexed by node id.
 */
function hosting_get_new_tasks($limit = 20, $rid = NULL, $task_type = NULL) {
  $return = array();
  $query = db_select('hosting_task', 't');
  $query
    ->innerJoin('node', 'n', 't.vid = n.vid');
  $query
    ->fields('t', array(
    'nid',
  ))
    ->condition('t.task_status', HOSTING_TASK_QUEUED)
    ->orderBy('n.changed')
    ->orderBy('n.nid')
    ->groupBy('t.rid')
    ->range(0, $limit)
    ->addTag('hosting_get_new_tasks');
  if ($rid) {
    $query
      ->condition('t.rid', $rid);
  }
  if ($task_type) {
    $query
      ->condition('t.task_type', $task_type);
  }
  foreach ($query
    ->execute() as $node) {
    $return[$node->nid] = node_load($node->nid);
  }
  return $return;
}

/**
 * Retrieve a list of outstanding, queued, tasks.
 *
 * @deprecated First deprecated in Hosting 3.9 because this function was made
 * part of the public API, use hosting_get_new_tasks() instead.
 *
 * @see hosting_get_new_tasks
 */
function _hosting_get_new_tasks($limit = 20) {
  return hosting_get_new_tasks($limit);
}

/**
 * @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') {
  $query = db_select('node', 'n');
  $query
    ->join('hosting_task', 't', 'n.vid = t.vid');
  $query
    ->fields('t', array(
    'task_status',
  ))
    ->condition('n.type', 'task')
    ->condition('t.task_type', $type);
  if ($filter_by && $filter_value) {
    $query
      ->condition('t.' . $filter_by, $filter_value);
  }
  $query
    ->orderBy('t.vid', 'DESC');
  return $query
    ->execute()
    ->fetchField();
}

/**
 * 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 {

    // should be NULL.
    return $status;
  }
}

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

/**
 * 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' => array(
      'hosting-actions',
    ),
  );
  $tasklist = hosting_task_fetch_tasks($node->nid);
  $rows = array();
  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;
    }
    if (empty($info['title'])) {

      // Skip tasks from types that have since been removed.
      continue;
    }
    $row['type'] = array(
      'data' => $info['title'],
      'class' => array(
        '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("hosting_confirm/%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']) ? 'hosting/task/' . $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' => array(
        'hosting-actions',
      ),
    );
    $rows[] = array(
      'data' => $row,
      'class' => array(
        $info['class'],
      ),
    );
  }
  $output .= theme('table', array(
    'header' => $headers,
    'rows' => $rows,
    'attributes' => array(
      'class' => array(
        'hosting-table',
      ),
    ),
  ));
  return $output;
}

/**
 * Utility function to create HTML for a task button.
 */
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' => $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' => array(
          'hosting-status',
        ),
      );
      if (!in_array('created', $skip)) {
        $row['created'] = t("@interval ago", array(
          '@interval' => format_interval(REQUEST_TIME - $node->created, 1),
        ));
        $headers[t('Created')] = '';
      }
      if ($node->task_status == 0) {
        $row['executed'] = '';
      }
      else {
        $row['executed'] = t("@interval ago", array(
          '@interval' => format_interval(REQUEST_TIME - $node->changed, 1),
        ));
      }
      $headers[t('Executed')] = '';
      $headers[t('Actions')] = '';
      $actions['log'] = l(t('View'), 'node/' . $node->nid, array(
        'attributes' => array(
          'class' => array(
            'hosting-button-dialog',
            'hosting-button-enabled',
            'hosting-button-log',
          ),
        ),
      ));
      $row['actions'] = array(
        'data' => $actions['log'],
        'class' => array(
          'hosting-actions',
        ),
      );
      $class = hosting_task_status_class($node->task_status);
      $rows[] = array(
        'data' => $row,
        'class' => array(
          $class,
        ),
      );
    }
    $output = theme('table', array(
      'header' => array_keys($headers),
      'rows' => $rows,
      'attributes' => array(
        'class' => array(
          'hosting-table',
        ),
      ),
    ));
    if ($pager === TRUE) {
      $output .= theme('pager', array(
        'tags' => NULL,
        'element' => $element,
      ));
    }
    elseif (is_string($pager)) {
      $output .= $pager;
    }
    return $output;
  }
}

/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
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 = :ntype AND t.rid = :trid\n    ORDER BY n.changed ASC", array(
    ':ntype' => 'task',
    ':trid' => $rid,
  ));
  foreach ($result as $obj) {
    $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;
}

/**
 * Return a machine name for each state.
 */
function hosting_task_status_name($hosting_task_status = NULL) {
  $codes = array(
    HOSTING_TASK_SUCCESS => 'success',
    HOSTING_TASK_QUEUED => 'queued',
    HOSTING_TASK_ERROR => 'error',
    HOSTING_TASK_PROCESSING => 'processing',
    HOSTING_TASK_WARNING => 'warning',
  );
  if (!is_null($hosting_task_status) && isset($codes[$hosting_task_status])) {
    return $codes[$hosting_task_status];
  }
  elseif (!is_null($hosting_task_status) && !isset($codes[$hosting_task_status])) {
    return '';
  }
  else {
    return $codes;
  }
}

/**
 * Return a css class from a hosting task status integer value.
 */
function hosting_task_status_class($status = NULL) {
  if (!is_null($status) && ($name = hosting_task_status_name($status))) {
    return 'hosting-' . $name;
  }
  else {
    return '';
  }
}

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

/**
 * Implements hook_overlay_paths().
 */
function hosting_task_overlay_paths() {
  $paths = array(
    'hosting/task/*' => array(
      'width' => 600,
    ),
    'hosting_confirm/*' => array(
      'width' => 600,
    ),
  );
  return $paths;
}

/**
 * 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':
      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 object|int $task
 *   A task object or the revision ID of a task node.
 *
 * @return int
 *   The task's status error code.
 */
function hosting_task_update_status($task) {
  if (is_numeric($task)) {
    $vid = $task;
    $task = node_load(NULL, $task);
  }
  else {
    $vid = $task->vid;
  }

  // Get the status of the task by parsing the log.
  $status = hosting_task_parse_log($vid);
  db_update('hosting_task')
    ->fields(array(
    'task_status' => $status,
  ))
    ->condition('vid', $vid)
    ->execute();

  // 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 int $vid
 *   The revision ID of a task node.
 *
 * @return int
 *   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 = :vid', array(
    ':vid' => $vid,
  ));
  foreach ($result as $status) {
    if ($status->type == 'error' || $status->type == 'failed') {
      $update = HOSTING_TASK_ERROR;
    }
    elseif ($update != HOSTING_TASK_ERROR && $status->type == 'warning') {
      $update = HOSTING_TASK_WARNING;
    }
  }
  return $update;
}

/**
 * Invoke hooks to build a list of NIDs to guard.
 */
function hosting_task_get_guarded_nodes() {
  $guarded_nids =& drupal_static(__FUNCTION__);
  if (!isset($guarded_nids)) {

    // Invoke hook_hosting_task_guarded_nodes().
    $guarded_nids = module_invoke_all('hosting_task_guarded_nodes');
    drupal_alter('hosting_task_guarded_nodes', $guarded_nids);
  }
  return $guarded_nids;
}

/**
 * Invoke hooks to build a list of dangerous tasks.
 */
function hosting_task_get_dangerous_tasks() {
  $dangerous_tasks =& drupal_static(__FUNCTION__);
  if (!isset($dangerous_tasks)) {

    // Invoke hook_hosting_task_dangerous_tasks().
    $dangerous_tasks = module_invoke_all('hosting_task_dangerous_tasks');
    drupal_alter('hosting_task_dangerous_tasks', $dangerous_tasks);
  }
  return $dangerous_tasks;
}

/**
 * Implements hook_hosting_task_dangerous_tasks().
 */
function hosting_task_hosting_task_dangerous_tasks() {
  $dangerous_tasks = array(
    'disable',
    'delete',
  );
  return $dangerous_tasks;
}

/**
 * Guard against destructive tasks running on guarded Aegir entities.
 */
function hosting_task_dangerous_task_is_allowed($task_type, $nid) {
  $allowed = TRUE;
  $guarded_nids = hosting_task_get_guarded_nodes();
  if (in_array($nid, $guarded_nids)) {
    $dangerous_tasks = hosting_task_get_dangerous_tasks();
    if (in_array($task_type, $dangerous_tasks)) {
      $node = node_load($nid);
      $map = array(
        ':task' => $task_type,
        ':title' => $node->title,
        ':type' => $node->type,
      );
      drupal_set_message(t('You cannot run dangerous :task task on guarded :type :title.', $map), 'warning');
      watchdog('hostmaster', 'Detected attempt to run dangerous :task task on guarded :type :title.', $map, WATCHDOG_WARNING);
      $allowed = FALSE;
    }
  }
  return $allowed;
}

/**
 * Implements hook_entity_property_info_alter().
 */
function hosting_task_entity_property_info_alter(&$info) {
  $task_properties =& $info['node']['bundles']['task']['properties'];
  $task_properties['task_type'] = array(
    'label' => t("Task type"),
    'description' => t("The type the task node uses."),
    'type' => 'text',
    'getter callback' => 'entity_property_verbatim_get',
    'setter callback' => 'entity_property_verbatim_set',
  );
  $task_properties['task_status'] = array(
    'label' => t('Task status'),
    'description' => t('The status of the task. E.g. @TODO, etc.'),
    'type' => 'integer',
    'options list' => '_hosting_task_error_codes',
    'setter callback' => 'entity_property_verbatim_set',
  );
  $task_properties['rid'] = array(
    'label' => t('Relation ID'),
    'description' => t('The relation this task belong to: site, platform, etc.'),
    'type' => 'integer',
    'getter callback' => 'entity_property_verbatim_get',
    'setter callback' => 'entity_property_verbatim_set',
  );

  // TODO: how should we expand these arguments?

  /*
  $task_properties['task_arguments'] = array(
    'label' => t('Task arguments'),
    'description' => t('The argument for the given task.'),
    // TODO list<text> is insufficient, list<struct> maybe the solution
    // see ie commerce/modules/price/commerce_price.module commerce_price_field_data_property_info
    'type' => 'list<struct>',
    'setter callback' => 'entity_property_verbatim_set',
  );
  */
}

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_new_tasks Retrieve a list of outstanding, queued, tasks.
hosting_get_tasks Retrieve tasks with specified criteria.
hosting_nodeapi_task_delete_revision Implements hook_delete_revision().
hosting_tasks_queue Process the hosting task queue.
hosting_task_action_info Implements hook_action_info().
hosting_task_ajax_command_hosting_table_append Prepare a JS command array for AJAX to append to a hosting task log table.
hosting_task_ajax_command_hosting_table_check Prepare a JS command array for AJAX to poll for additional log entries.
hosting_task_ajax_list Page callback to provide JSON output for a task.
hosting_task_ajax_queue AJAX callback for refreshing task list.
hosting_task_cancel Cancel a task before it's started.
hosting_task_cancel_access Implements hook_access().
hosting_task_confirm_form Implements 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_dangerous_task_is_allowed Guard against destructive tasks running on guarded Aegir entities.
hosting_task_delete Implements hook_delete().
hosting_task_delete_related_tasks Delete tasks related to a given site, platform, server, etc.
hosting_task_entity_property_info_alter Implements hook_entity_property_info_alter().
hosting_task_execute Executes a task while logging to watchdog and drush. Implemented by drush hosting-tasks and hosting-queued commands.
hosting_task_fetch_tasks @todo Please document this function.
hosting_task_get_dangerous_tasks Invoke hooks to build a list of dangerous tasks.
hosting_task_get_guarded_nodes Invoke hooks to build a list of NIDs to guard.
hosting_task_hosting_queues Implements hook_hosting_queues().
hosting_task_hosting_tasks_alter Implements hook_hosting_tasks_alter()
hosting_task_hosting_task_dangerous_tasks Implements hook_hosting_task_dangerous_tasks().
hosting_task_init Implements hook_init().
hosting_task_insert Implements hook_insert().
hosting_task_list Display list of tasks.
hosting_task_load Implements hook_load().
hosting_task_log Insert an entry in the task log.
hosting_task_log_ajax Read additional task log data, served via AJAX.
hosting_task_menu Implements 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_access Implements hook_node_access().
hosting_task_node_info Implements hook_node_info().
hosting_task_outstanding Determine whether there is an outstanding task of a specific type.
hosting_task_overlay_paths Implements hook_overlay_paths().
hosting_task_parse_log Parse a task's log, and return its status accordingly.
hosting_task_permission Implements hook_permission().
hosting_task_preprocess_views_view_table Implements hook_preprocess_views_view_table().
hosting_task_restore_form Customize the task confirmation form for restore.
hosting_task_restore_form_validate Implements hook_form_alter().
hosting_task_retry Retry the given task
hosting_task_retry_form Adds a retry button to failed task nodes.
hosting_task_retry_form_submit Submit handler for the task retry button.
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 Return a css class from a hosting task status integer value.
hosting_task_status_name Return a machine name for each state.
hosting_task_status_output Return the status of a task in human-readable form.
hosting_task_table A concise table listing of the tasks affecting this node.
hosting_task_task_has_finished Determine if the given task has finished and completed.
hosting_task_update Implements hook_update().
hosting_task_update_status Set a task's status according to its log.
hosting_task_update_status_form Adds button to update the status of task nodes stuck in the 'processing' state.
hosting_task_update_status_form_submit Submit handler for the 'update status' button.
hosting_task_view Implements hook_view().
hosting_task_views_api Views integration.
_hosting_get_new_tasks Deprecated Retrieve a list of outstanding, queued, tasks.
_hosting_parse_error_code Turn bitmask integer error code into translatable label.
_hosting_task_button Utility function to create HTML for a 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.