Skip to content
page_manager.admin.inc 60.4 KiB
Newer Older
<?php

/**
 * @file
 * Administrative functions for the page manager.
 *
 * This provides the UI to list, create, edit and delete pages, though much
 * of this is delegated through to individual tasks.
 */

/**
 * Output a list of pages that are managed.
 */
function page_manager_list_page($js = NULL) {
  // Prevent this page from showing up when random other links fail.
  if ($js && $js != 'ajax' && $js != 'nojs') {
    return drupal_not_found();
  }

  // TRUE if 'ajax', FALSE if otherwise.
  $js = $js == 'ajax';

  // If we do any form rendering, it's to completely replace a form on the
  // page, so don't let it force our ids to change.
  if ($js && isset($_POST['ajax_html_ids'])) {
    unset($_POST['ajax_html_ids']);
  }

  if (module_exists('advanced_help') && !$js) {
    drupal_set_message(theme('advanced_help_topic', 'page_manager', 'getting-started', t('See the getting started guide for more information.')));
  }

  $tasks = page_manager_get_tasks_by_type('page');
  $pages = array('operations' => array(), 'tasks' => array());
  // Add lock icon to all locked tasks.
  global $user;
  ctools_include('object-cache');
  $locks = ctools_object_cache_test_objects('page_manager_page', $pages['tasks']);
  foreach ($locks as $task_name => $lock) {
    if ($lock->uid == $user->uid) {
      $pages['rows'][$task_name]['class'][] = ' page-manager-locked';
      $pages['rows'][$task_name]['title'] = t('This page is currently locked for editing by you. Nobody else may edit this page until these changes are saved or canceled.');
    }
    else {
      $pages['rows'][$task_name]['class'][] = ' page-manager-locked-other';
      $pages['rows'][$task_name]['title'] = t('This page is currently locked for editing by another user. You may not edit this page without breaking the lock.');
    }
  }

  $input = $_POST;

  // Respond to a reset command by clearing session and doing a drupal goto
  // back to the base URL.
  if (isset($input['op']) && $input['op'] == t('Reset')) {
    unset($_SESSION['page_manager']['#admin']);
    if (!$js) {
      return drupal_goto($_GET['q']);
    }
    // clear everything but form id, form build id and form token:
    $keys = array_keys($input);
    foreach ($keys as $id) {
      if ($id != 'form_id' && $id != 'form_build_id' && $id != 'form_token') {
        unset($input[$id]);
      }
    }
    $replace_form = TRUE;
  if (count($input) <= 1) {
    if (isset($_SESSION['page_manager']['#admin']) && is_array($_SESSION['page_manager']['#admin'])) {
      $input  = $_SESSION['page_manager']['#admin'];
    $_SESSION['page_manager']['#admin'] = $input;
    unset($_SESSION['page_manager']['#admin']['q']);
  }

  $form_state = array(
    'pages' => &$pages,
    'input' => $input,
    'rerender' => TRUE,
    'no_redirect' => TRUE,
  );

  // This form will sort and filter the pages.
  $form = drupal_build_form('page_manager_list_pages_form', $form_state);
    array('data' => t('Type'), 'class' => array('page-manager-page-type')),
    array('data' => t('Name'), 'class' => array('page-manager-page-name')),
    array('data' => t('Title'), 'class' => array('page-manager-page-title')),
    array('data' => t('Path'), 'class' => array('page-manager-page-path')),
    array('data' => t('Storage'), 'class' => array('page-manager-page-storage')),
  $header[] = array('data' => t('Operations'), 'class' => array('page-manager-page-operations'));
  $table = theme('table', array('header' => $header, 'rows' => $pages['rows'], 'attributes' => array('id' => 'page-manager-list-pages')));
  $operations = '<div id="page-manager-links" class="links">' . theme('links', array('links' => $pages['operations'])) . '</div>';

  drupal_add_css(drupal_get_path('module', 'page_manager') . '/css/page-manager.css');
    return array('#markup' => drupal_render($form) . $table . $operations);
  ctools_include('ajax');
  $commands = array();
  $commands[] = ajax_command_replace('#page-manager-list-pages', $table);
  if (!empty($replace_form)) {
    $commands[] = ajax_command_replace('#page-manager-list-pages-form', drupal_render($form));
  print ajax_render($commands);
  ajax_footer();
}

/**
 * Sort tasks into buckets based upon whether or not they have subtasks.
 */
function page_manager_get_pages($tasks, &$pages, $task_id = NULL) {
  foreach ($tasks as $id => $task) {
Earl Miles's avatar
Earl Miles committed
    if (empty($task_id) && !empty($task['page operations'])) {
      $pages['operations'] = array_merge($pages['operations'], $task['page operations']);
    }

    // If a type has subtasks, add its subtasks in its own table.
    if (!empty($task['subtasks'])) {
      page_manager_get_pages(page_manager_get_task_subtasks($task), $pages, $task['name']);
      continue;
    }

    if (isset($task_id)) {
      $task_name = page_manager_make_task_name($task_id, $task['name']);
    }
    else {
      $task_name = $task['name'];
    }

    $class = array('page-task-' . $id);
      $class[] = $task['row class'];
      $class[] = 'page-manager-disabled';
    $visible_path = '';
    if (!empty($task['admin path'])) {
      foreach (explode('/', $task['admin path']) as $bit) {
        if ($bit[0] != '!') {
          $path[] = $bit;
        }
      $path = implode('/', $path);
      if (empty($task['disabled']) && strpos($path, '%') === FALSE) {
        $visible_path = l('/' . $task['admin path'], $path);
      }
      else {
        $visible_path = '/' . $task['admin path'];
      }
    $row = array('data' => array(), 'class' => $class, 'title' => strip_tags($task['admin description']));

    $type = isset($task['admin type']) ? $task['admin type'] : t('System');
    $pages['types'][$type] = $type;
    $row['data']['type'] = array('data' => $type, 'class' => array('page-manager-page-type'));
    $row['data']['name'] = array('data' => $task_name, 'class' => array('page-manager-page-name'));
    $row['data']['title'] = array('data' => $task['admin title'], 'class' => array('page-manager-page-title'));
    $row['data']['path'] = array('data' => $visible_path, 'class' => array('page-manager-page-path'));

    $storage = isset($task['storage']) ? $task['storage'] : t('In code');
    $pages['storages'][$storage] = $storage;
    $row['data']['storage'] = array('data' => $storage, 'class' => array('page-manager-page-storage'));
    if (empty($task['disabled'])) {
      $item = menu_get_item($path);
      if (empty($item)) {
        dsm($path);
      }
      else {
        dsm($item);
      }
    }
    $operations = array(
      array(
        'title' => t('Edit'),
        'href' => page_manager_edit_url($task_name),
    if (!empty($task['enable callback'])) {
      if (!empty($task['disabled'])) {
        $operations[] = array(
          'title' => t('Enable'),
          'href' => 'admin/structure/pages/nojs/enable/' . $task_name,
          'query' => array('token' => drupal_get_token($task_name)),
        );
      }
      else {
        $operations[] = array(
          'title' => t('Disable'),
          'href' => 'admin/structure/pages/nojs/disable/' . $task_name,
          'query' => array('token' => drupal_get_token($task_name)),
    $row['data']['operations'] = array('data' => theme('links', array('links' => $operations)), 'class' => array('page-manager-page-operations'));
    $pages['disabled'][$task_name] = !empty($task['disabled']);
    $pages['rows'][$task_name] = $row;
  }
}

/**
 * Provide a form for sorting and filtering the list of pages.
 */
function page_manager_list_pages_form($form, &$form_state) {
  // This forces the form to *always* treat as submitted which is
  // necessary to make it work.
  if (empty($_POST)) {
    $form["#programmed"] = TRUE;
  }
  $form['#action'] = url('admin/structure/pages/nojs/', array('absolute' => TRUE));
  if (!variable_get('clean_url', FALSE)) {
    $form['q'] = array(
      '#type' => 'hidden',
      '#value' => $_GET['q'],
    );
  }

  $all = array('all' => t('<All>'));

  $form['type'] = array(
    '#type' => 'select',
    '#title' => t('Type'),
    '#options' => $all + $form_state['pages']['types'],
    '#default_value' => 'all',
  );

  $form['storage'] = array(
    '#type' => 'select',
    '#title' => t('Storage'),
    '#options' => $all + $form_state['pages']['storages'],
    '#default_value' => 'all',
  );

  $form['disabled'] = array(
    '#type' => 'select',
    '#title' => t('Enabled'),
    '#options' => $all + array('0' => t('Enabled'), '1' => t('Disabled')),
    '#default_value' => 'all',
  );
  $form['search'] = array(
    '#type' => 'textfield',
    '#title' => t('Search'),
  );
  $form['order'] = array(
    '#type' => 'select',
    '#title' => t('Sort by'),
    '#options' => array(
      'title' => t('Title'),
      'name' => t('Name'),
      'path' => t('Path'),
      'type' => t('Type'),
      'storage' => t('Storage'),
    ),
  );

  $form['sort'] = array(
    '#type' => 'select',
    '#title' => t('Order'),
    '#options' => array(
      'asc' => t('Up'),
      'desc' => t('Down'),
    ),
    '#default_value' => 'asc',
  );

  $form['submit'] = array(
    '#name' => '', // so it won't in the $_GET args
    '#type' => 'submit',
    '#id' => 'edit-pages-apply',
    '#value' => t('Apply'),
    '#attributes' => array('class' => array('use-ajax-submit ctools-auto-submit-click')),
  $form['reset'] = array(
    '#type' => 'submit',
    '#id' => 'edit-pages-reset',
    '#value' => t('Reset'),
    '#attributes' => array('class' => array('use-ajax-submit')),
  $form['#attached']['js'] = array('misc/ajax.js', 'misc/progress.js', 'misc/jquery.form.js', ctools_attach_js('auto-submit'), ctools_attach_js('page-list', 'page_manager'));
  $form['#prefix'] = '<div class="clearfix">';
  $form['#suffix'] = '</div>';
  $form['#attributes'] = array('class' => array('ctools-auto-submit-full-form'));

  return $form;
}

/**
 * Accept submission from the page manager sort/filter form and apply it
 * to the list of pages.
 */
function page_manager_list_pages_form_submit(&$form, &$form_state) {
  // Filter and re-sort the pages.

  // This is a copy.
  $rows = $form_state['pages']['rows'];

  $sorts = array();
  foreach ($rows as $name => $data) {
    // Filter
    if ($form_state['values']['type'] != 'all' && $form_state['values']['type'] != $data['data']['type']['data']) {
      continue;
    }

    if ($form_state['values']['storage'] != 'all' && $form_state['values']['storage'] != $data['data']['storage']['data']) {
      continue;
    }

    if ($form_state['values']['disabled'] != 'all' && $form_state['values']['disabled'] != $form_state['pages']['disabled'][$name]) {
      continue;
    }

    if ($form_state['values']['search'] &&
        strpos($data['data']['name']['data'], $form_state['values']['search']) === FALSE &&
        strpos($data['data']['path']['data'], $form_state['values']['search']) === FALSE &&
        strpos($data['data']['title']['data'], $form_state['values']['search']) === FALSE) {
          continue;
    }
    // Set up sorting
    switch ($form_state['values']['order']) {
      case 'disabled':
        $sorts[$name] = !$form_state['pages']['disabled'][$name] . $data['data']['title']['data'];
        break;
      case 'title':
        $sorts[$name] = $data['data']['title']['data'];
        break;
      case 'name':
        $sorts[$name] = $data['data']['name']['data'];
        break;
      case 'path':
        $sorts[$name] = $data['data']['path']['data'];
        break;
      case 'type':
        $sorts[$name] = $data['data']['type']['data'];
        break;
      case 'storage':
        $sorts[$name] = $data['data']['storage']['data'];
        break;
    }
  }

  // Now actually sort
  if ($form_state['values']['sort'] == 'desc') {
    arsort($sorts);
  }
  else {
    asort($sorts);
  }

  // Nuke the original.
  $form_state['pages']['rows'] = array();
  // And restore.
  foreach ($sorts as $name => $title) {
    $form_state['pages']['rows'][$name] = $rows[$name];
  }

}

/**
 * Render the edit page for a a page, custom or system.
 */
function page_manager_edit_page($page) {
  drupal_set_title($page->subtask['admin title']);
  // Provide and process the save page form before anything else.
  $form_state = array('page' => &$page);
  $built_form = drupal_build_form('page_manager_save_page_form', $form_state);
  $form = drupal_render($built_form);

  $operations = page_manager_get_operations($page);
  $args = array('summary');
  $rendered_operations = page_manager_render_operations($page, $operations, $args, array('class' => array('operations-main')), 'nav');
  $content = page_manager_get_operation_content(FALSE, $page, $args, $operations);
  $output = theme('page_manager_edit_page', array('page' => $page, 'save' => $form, 'operations' => $rendered_operations, 'content' => $content));
  return array('#markup' => $output);
}

/**
 * Entry point to edit a single operation for a page.
 *
 * @param $js
 *   Whether or not the page was called via javascript.
 * @param $page
 *   The cached page that is being edited.
 * @param ...
 *   A number of items used to drill down into the actual operation called.
 */
function page_manager_edit_page_operation() {
  $args = func_get_args();
  $js = array_shift($args);
  $page = array_shift($args);

  $operations = page_manager_get_operations($page);
  $content = page_manager_get_operation_content($js, $page, $args, $operations);

  // If the operation requested we go somewhere else afterward, oblige it.
  if (isset($content['new trail'])) {
    $args = $content['new trail'];
    // Get operations again, for the operation may have changed their availability.
    $operations = page_manager_get_operations($page);
    $content = page_manager_get_operation_content($js, $page, $args, $operations);
  }

  // Rendering the content may have been a form submission that changed the
  // operations, such as renaming or adding a handler. Thus we get a new set
  // of operations.
  $operations = page_manager_get_operations($page);
  $rendered_operations = page_manager_render_operations($page, $operations, $args, array('class' => array('operations-main')), 'nav');

  // Since this form should never be submitted to this page, process it late so
  // that we can be sure it notices changes.
  $form_state = array('page' => &$page);
  $built_form = drupal_build_form('page_manager_save_page_form', $form_state);
  $form = drupal_render($built_form);
  $output = theme('page_manager_edit_page', array('page' => $page, 'save' => $form, 'operations' => $rendered_operations, 'content' => $content));
    $commands[] = ajax_command_replace('#page-manager-edit', $output);
    print ajax_render($commands);
    ajax_footer();
    return;
  }

  drupal_set_title($page->subtask['admin title']);
  return $output;
}

/**
 * Take the operations array from a task and expand it.
 *
 * This allows some of the operations to be dynamic, based upon settings
 * on the task or the task's handlers. Each operation should have a type. In
 * addition to all the types allowed in page_manager_render_operations, these
 * types will be dynamically replaced with something else:
 * - 'handlers': An automatically created group that contains all the task's
 *   handlers and appropriate links.
 * - 'function': A callback (which will be placed in the 'function' parameter
 *   that should return an array of operations. This can be used to provide
 *   additional, dynamic links if needed.
 */
function page_manager_get_operations($page, $operations = NULL) {
  if (!isset($operations)) {
    // All tasks have at least these 2 ops:
    $operations = array(
      'summary' => array(
        'title' => t('Summary'),
        'description' => t('Get a summary of the information about this page.'),
        'path' => 'admin/structure/pages/edit',
        'ajax' => FALSE,
        'no operations' => TRUE,
        'form info' => array(
          'no buttons' => TRUE,
        ),
        'form' => 'page_manager_page_summary',
      ),
      'actions' => array(
        'type' => 'group',
        'title' => '',
        'class' => array('operations-actions'),
        'location' => 'primary',
        'children' => array(),
      ),
    );
    if (isset($page->subtask['operations'])) {
      $operations += $page->subtask['operations'];
      // add actions separately.
      if (!empty($page->subtask['operations']['actions'])) {
        $operations['actions']['children'] += $page->subtask['operations']['actions']['children'];
      }
    }
    $operations['handlers'] = array('type' => 'handlers');
  }

  $result = array();
  foreach ($operations as $id => $operation) {
    if (empty($operation['type'])) {
      $operation['type'] = 'operation';
    }
    switch ($operation['type']) {
      case 'handlers':
        $result[$id] = page_manager_get_handler_operations($page);
        break;
      case 'function':
        if (function_exists($operation['function'])) {
          $retval = $function($page, $operation);
          if (is_array($retval)) {
            $result[$id] = $retval;
          }
        }
        break;
      default:
        $result[$id] = $operation;
    }
  }

  if (!empty($page->subtask['enable callback']) && !empty($page->subtask['disabled']) && empty($result['actions']['children']['enable'])) {
    $result['actions']['children']['enable'] = array(
      'title' => t('Enable'),
      'description' => t('Activate this page so that it will be in use in your system.'),
      'form' => 'page_manager_enable_form',
      'ajax' => FALSE,
      'silent' => TRUE,
      'no update and save' => TRUE,
      'form info' => array(
        'finish text' => t('Enable'),
      ),
    );
  }

  if (!empty($page->subtask['enable callback']) && empty($page->subtask['disabled']) && empty($result['actions']['children']['disable'])) {
    $result['actions']['children']['disable'] = array(
      'title' => t('Disable'),
      'description' => t('De-activate this page. The data will remain but the page will not be in use on your system.'),
      'form' => 'page_manager_disable_form',
      'ajax' => FALSE,
      'silent' => TRUE,
      'no update and save' => TRUE,
      'form info' => array(
        'finish text' => t('Disable'),
      ),
    );
  }

  $result['actions']['children']['add'] = array(
    'title' => t('Add variant'),
    'description' => t('Add a new variant to this page.'),
    'form' => 'page_manager_handler_add',
    'ajax' => FALSE,
    'silent' => TRUE, // prevents a message about updating and prevents this item from showing as changed.
    'no update and save' => TRUE, // get rid of update and save button which is bad here.
      'finish text' => t('Create variant'),
  // Restrict variant import to users who can already execute arbitrary PHP
  if (user_access('use PHP for settings')) {
    $result['actions']['children']['import'] = array(
      'title' => t('Import variant'),
      'description' => t('Add a new variant to this page from code exported from another page.'),
      'form' => 'page_manager_handler_import',
    );
  }

  if (count($page->handlers) > 1) {
    $result['actions']['children']['rearrange'] = array(
      'title' => t('Reorder variants'),
      'ajax' => FALSE,
      'description' => t('Change the priority of the variants to ensure that the right one gets selected.'),
      'form' => 'page_manager_handler_rearrange',
    );
  }

  // This is a special operation used to configure a new task handler before
  // it is added.
  if (isset($page->new_handler)) {
  $plugin = page_manager_get_task_handler($page->new_handler->handler);
    $result['actions']['children']['configure'] = array(
      'title' => t('Configure'),
      'description' => t('Configure a newly created variant prior to actually adding it to the page.'),
      'ajax' => FALSE,
      'no update and save' => TRUE, // get rid of update and save button which is bad here.
      'form info' => array(
        // We use our own cancel and finish callback to handle the fun stuff.
        'finish callback' => 'page_manager_handler_add_finish',
        'cancel callback' => 'page_manager_handler_add_cancel',
        'show trail' => TRUE,
        'show back' => TRUE,
        'finish text' => t('Create variant'),
      ),
      'form' => array(
        'forms' => $plugin['forms'],
      ),
    );

    foreach ($page->forms as $id) {
      if (isset($plugin['add features'][$id])) {
        $result['actions']['children']['configure']['form']['order'][$id] = $plugin['add features'][$id];
      }
      else if (isset($plugin['required forms'][$id])) {
        $result['actions']['children']['configure']['form']['order'][$id] = $plugin['required forms'][$id];
      }
    }
  }

  if ($page->locked) {
    $result['actions']['children']['break-lock'] = array(
      'title' => t('Break lock'),
      'description' => t('Break the lock on this page so that you can edit it.'),
      'form' => 'page_manager_break_lock',
      'ajax' => FALSE,
      'no update and save' => TRUE, // get rid of update and save button which is bad here.
      'form info' => array(
        'finish text' => t('Break lock'),
       ),
      'even locked' => TRUE, // show button even if locked
      'silent' => TRUE,
    );
  }

  drupal_alter('page_manager_operations', $result, $page);
  return $result;
}

/**
 * Collect all the operations related to task handlers (variants) and
 * build a menu.
 */
function page_manager_get_handler_operations(&$page) {
  ctools_include('export');
  $group = array(
    'type' => 'group',
    'class' => array('operations-handlers'),
  // If there is only one variant, let's not have it collapsible.
  $collapsible = count($page->handler_info) != 1;
  foreach ($page->handler_info as $id => $info) {
    if ($info['changed'] & PAGE_MANAGER_CHANGED_DELETED) {
      continue;
    }
    $handler = $page->handlers[$id];
    $plugin = page_manager_get_task_handler($handler->handler);

    $operations[$id] = array(
      'type' => 'group',
      'class' => array('operations-handlers-' . $id),
      'title' => page_manager_get_handler_title($plugin, $handler, $page->task, $page->subtask_id),
      'collapsible' => $collapsible,
      'children' => array(),
    );

    $operations[$id]['children']['actions'] = array(
      'type' => 'group',
      'class' => array('operations-handlers-actions-' . $id),
      'title' => t('Variant operations'),
      'children' => array(),
      'location' => $id,
    );

    // There needs to be a 'summary' item here for variants.
    $operations[$id]['children']['summary'] = array(
      'title' => t('Summary'),
      'description' => t('Get a summary of the information about this variant.'),
      'form info' => array(
        'no buttons' => TRUE,
      ),
      'form' => 'page_manager_handler_summary',
    );

    if ($plugin && isset($plugin['operations'])) {
      $operations[$id]['children'] += $plugin['operations'];
    }
    $actions = &$operations[$id]['children']['actions']['children'];

    $actions['clone'] = array(
      'title' => t('Clone'),
      'description' => t('Make an exact copy of this variant.'),
      'form' => 'page_manager_handler_clone',
    );
    $actions['export'] = array(
      'title' => t('Export'),
      'description' => t('Export this variant into code to import into another page.'),
      'form' => 'page_manager_handler_export',
    );
    if ($handler->export_type == (EXPORT_IN_CODE | EXPORT_IN_DATABASE)) {
      $actions['delete'] = array(
        'title' => t('Revert'),
        'description' => t('Remove all changes to this variant and revert to the version in code.'),
        'form' => 'page_manager_handler_delete',
        'no update and save' => TRUE,
        'form info' => array(
          'finish text' => t('Revert'),
        ),
      );
    }
    else if ($handler->export_type != EXPORT_IN_CODE) {
      $actions['delete'] = array(
        'title' => t('Delete'),
        'description' => t('Remove this variant from the page completely.'),
        'form' => 'page_manager_handler_delete',
        'form info' => array(
          'finish text' => t('Delete'),
          'save text' => t('Delete and save'),
        ),
      );
    }
    if (!empty($handler->disabled)) {
      $actions['enable'] = array(
        'title' => t('Enable'),
        'description' => t('Activate this variant so that it will be in use in your system.'),
        'form' => 'page_manager_handler_enable',
        'silent' => TRUE,
        'form info' => array(
          'finish text' => t('Enable'),
          'save text' => t('Enable and save'),
        ),
      );
    }
    else {
      $actions['disable'] = array(
        'title' => t('Disable'),
        'description' => t('De-activate this variant. The data will remain but the variant will not be in use on your system.'),
        'form' => 'page_manager_handler_disable',
        'silent' => TRUE,
        'form info' => array(
          'finish text' => t('Disable'),
          'save text' => t('Disable and save'),

    drupal_alter('page_manager_variant_operations', $operations[$id], $handler);
  }
  if (empty($operations)) {
    $operations['empty'] = array(
      'type' => 'text',
      'title' => t('No variants'),
    );
  }

  $group['children'] = $operations;
  return $group;
}

/**
 * Get an operation from a trail.
 *
 * @return array($operation, $active, $args)
 */
function page_manager_get_operation($operations, $trail) {
  $args = $trail;
  $stop = FALSE;
  $active = array();
  $titles = array();
  // Drill down into operations array:
  while (!$stop) {
    $check = reset($args);
    $stop = TRUE;
    if (is_array($operations)) {
      if (isset($operations[$check])) {
        $active[] = $check;
        $operation = array_shift($args);
        // check to see if this operation has children. If so, we continue.
        if (!isset($operations[$check]['children'])) {
          $operations = $operations[$check];
        }
        else {
          $titles[] = $operations[$check]['title'];
          $operations = $operations[$check]['children'];
          // continue only if the operation hs children.
          $stop = FALSE;
        }
      }
    }
  }

  return array($operations, $active, $args, $titles);
}

/**
 * Fetch the content for an operation.
 *
 * First, this drills down through the arguments to find the operation, and
 * turns whatever it finds into the active trail which is then used to
 * hilite where we are when rendering the operation list.
 *
 * The arguments are discovered from the URL, and are an exact match for where
 * the operation is in the hierarchy. For example, handlers/foo/settings will
 * be the operation to edit the settings for the handler named foo. This comes
 * in as an array ('handlers', 'foo', 'settings') and is used to find where the
 * data for that operation is in the array.
 */
function page_manager_get_operation_content($js, &$page, $trail, $operations) {
  list($operation, $active, $args, $titles) = page_manager_get_operation($operations, $trail);
  // Once we've found the operation, send it off to render.
  if ($operation) {
    $content = _page_manager_get_operation_content($js, $page, $active, $operation, $titles, $args);
  }

  if (empty($content)) {
    $content = _page_manager_get_operation_content($js, $page, array('summary'), $operations['summary']);
  }

  return $content;
}

/**
 * Fetch the content for an operation, after it's been discovered from arguments.
 *
 * This system runs through the CTools form wizard. Each operation specifies a form
 * or set of forms that it may use. Operations may also specify wrappers and can
 * set their own next/finish handlers so that they can make additional things happen
 * at the end.
 */
function _page_manager_get_operation_content($js, &$page, $active, $operation, $titles = array(), $args = array()) {
  if (isset($operation['form'])) {
    $form_info = array(
      'id' => 'page_manager_page',
      'finish text' => t('Update'),
      'show trail' => FALSE,
      'show back' => FALSE,
      'show return' => FALSE,
      'show cancel' => FALSE,
      'next callback' => 'page_manager_edit_page_next',
      'finish callback' => 'page_manager_edit_page_finish',
      // Items specific to the 'edit' routines that will get moved over:
      'path' => page_manager_edit_url($page->task_name, $active) . "/%step",
      // wrapper function to add an extra finish button.
      'wrapper' => 'page_manager_operation_wrapper',
    );

    // If $operation['form'] is simply a string, then it is the function
    // name of the form.
    if (!is_array($operation['form'])) {
      $form_info['order'] = array(
        'form' => $operation['title'],
      );
      $form_info['forms'] = array(
        'form' => array('form id' => $operation['form']),
      );
      if (isset($operation['wrapper'])) {
        $form_info['forms']['form']['wrapper'] = $operation['wrapper'];
      }
    }
    // Otherwise it's the order and forms arrays directly.
    else {
      $form_info['order'] = $operation['form']['order'];
      $form_info['forms'] = $operation['form']['forms'];
    }

    // Allow the operation to override any form info settings:
    if (isset($operation['form info'])) {
      foreach ($operation['form info'] as $key => $setting) {
        $form_info[$key] = $setting;
      }
    }

    if (!empty($page->subtask['operations include'])) {
      // Quickly load any files necessary to display the forms.
      $page->subtask['operations include']['function'] = 'nop';
      ctools_plugin_get_function($page->subtask, 'operations include');
    }

    $step = array_shift($args);
    // If step is unset, go with the basic step.
    if (!isset($step)) {
      $step = current(array_keys($form_info['order']));
    }

    // If it is locked, hide the buttonzzz!
    if ($page->locked && empty($operation['even locked'])) {
      $form_info['no buttons'] = TRUE;
    }

    ctools_include('wizard');
    $form_state = array(
      'page' => $page,
      'type' => 'edit',
      'ajax' => $js && (!isset($operation['ajax']) || !empty($operation['ajax'])),
      'rerender' => TRUE,
      'trail' => $active,
      'task_name' => $page->task_name,
      'task_id' => $page->task_id,
      'task' => $page->task,
      'subtask_id' => $page->subtask_id,
      'subtask' => $page->subtask,
      'operation' => $operation,
    );

    if ($active && $active[0] == 'handlers' && isset($form_state['page']->handlers[$form_state['trail'][1]])) {
      $form_state['handler_id'] = $form_state['trail'][1];
      $form_state['handler'] = &$form_state['page']->handlers[$form_state['handler_id']];
    }

    if ($active && $active[0] == 'actions' && $active[1] == 'configure' && isset($form_state['page']->new_handler)) {
      $form_state['handler_id'] = $form_state['page']->new_handler->name;
      $form_state['handler'] = &$form_state['page']->new_handler;
    }

    $built_form = ctools_wizard_multistep_form($form_info, $step, $form_state);
    $output = drupal_render($built_form);
    $title = empty($form_state['title']) ? $operation['title'] : $form_state['title'];
    $titles[] = $title;
    $title = implode(' &raquo ', array_filter($titles));

    // If there are messages for the form, render them.
    if ($messages = theme('status_messages')) {
      $output = $messages . $output;
    }

    $description = isset($operation['admin description']) ? $operation['admin description'] : (isset($operation['description']) ? $operation['description'] : '');
    // Append any extra content, used for the preview which is submitted then
    // rendered.
    if (isset($form_state['content'])) {
      $return['content'] .= $form_state['content'];
    }
    // If the form wanted us to go somewhere else next, pass that along.
    if (isset($form_state['new trail'])) {
      $return['new trail'] = $form_state['new trail'];
    }
  }
  else {
    $return = array(
      'title' => t('Error'),
      'content' => t('This operation trail does not exist.'),
function page_manager_operation_wrapper($form, &$form_state) {
  if (empty($form_state['operation']['no update and save']) && !empty($form['buttons']['return']['#wizard type']) && $form['buttons']['return']['#wizard type']) {
    $form['buttons']['save'] = array(
      '#type' => 'submit',
      '#value' => !empty($form_state['form_info']['save text']) ? $form_state['form_info']['save text'] : t('Update and save'),
      '#wizard type' => 'finish',
      '#attributes' => $form['buttons']['return']['#attributes'],
      '#save' => TRUE,
    );
  }
 * Callback generated when the an operation edit finished.
 */
function page_manager_edit_page_finish(&$form_state) {
  if (empty($form_state['operation']['silent'])) {
    if (empty($form_state['clicked_button']['#save'])) {
      drupal_set_message(t('The page has been updated. Changes will not be permanent until you save.'));
    }
    else {
      drupal_set_message(t('The page has been updated and saved.'));
    }
    $path = array();
    foreach ($form_state['trail'] as $operation) {
      $path[] = $operation;
      $form_state['page']->changes[implode('/', $path)] = TRUE;
    }
  }

  // If a handler was modified, set it to changed so we know to overwrite it.
  if (isset($form_state['handler_id'])) {
    $form_state['page']->handler_info[$form_state['handler_id']]['changed'] |= PAGE_MANAGER_CHANGED_CACHED;
  }

  // While we make buttons go away on locked pages, it is still possible to
  // have a lock a appear while you were editing, and have your changes
  // disappear. This at least warns the user that this has happened.