From bb0ea4bb0f0e28ab296e3cb38df7ce914aa82eba Mon Sep 17 00:00:00 2001
From: Earl Miles <merlin@logrus.com>
Date: Fri, 19 Dec 2008 17:35:58 +0000
Subject: [PATCH] Checkpoint checkin: Can now create arbitrary pages! Yay. Task
 handler manipulation on arbitrary pages works, though there are some clear
 disconnects in the UI that will be addressed next. Not all page admin
 functions exist yet (no clone, enable, disable, revert, delete). Not all
 arguments have criteria yet.

---
 ctools.module                               |  73 +++++
 delegator/css/page-task.css                 |  10 +
 delegator/delegator.module                  |   9 +
 delegator/help/api-task-type.html           |   3 +
 delegator/help/page-task-type.html          |   4 +
 delegator/plugins/task_types/page.admin.inc |  88 ++++++
 delegator/plugins/task_types/page.inc       |  56 ++++
 delegator/plugins/tasks/node_view.inc       |   4 +
 delegator/plugins/tasks/page.admin.inc      | 172 +++++++++-
 delegator/plugins/tasks/page.inc            | 329 +++++++++++++-------
 help/context-arguments.html                 |   5 +
 includes/context.inc                        |   2 +
 includes/dropdown.theme.inc                 |  39 ++-
 includes/plugins.inc                        |   1 -
 js/collapsible-div.js                       |  15 +-
 js/dependent.js                             |   2 +-
 js/dropdown.js                              |  23 +-
 plugins/arguments/nid.inc                   |  60 +++-
 plugins/arguments/uid.inc                   |  50 ++-
 19 files changed, 804 insertions(+), 141 deletions(-)
 create mode 100644 delegator/css/page-task.css
 create mode 100644 delegator/help/api-task-type.html
 create mode 100644 delegator/help/page-task-type.html
 create mode 100644 delegator/plugins/task_types/page.admin.inc
 create mode 100644 delegator/plugins/task_types/page.inc

diff --git a/ctools.module b/ctools.module
index dc461734..92315a64 100644
--- a/ctools.module
+++ b/ctools.module
@@ -106,3 +106,76 @@ function ctools_get_roles() {
 
   return $roles;
 }
+
+/**
+ * Determine if the current user has access via a plugin.
+ *
+ * This function is meant to be embedded in the Drupal menu system, and
+ * therefore is in the .module file since sub files can't be loaded, and
+ * takes arguments a little bit more haphazardly than ctools_access().
+ *
+ * @param $plugin_name
+ *   The access plugin to use. If empty or 'none' then no access control
+ *   is being used and this function returns TRUE. If the plugin can't be
+ *   found otherwise, this function automatically returns FALSE.
+ * @param $settings
+ *   An array of settings theoretically set by the user.
+ * @param ...
+ *   zero or more context arguments generated from argument plugins. These
+ *   contexts must have an 'id' attached to them so that they can be
+ *   properly associated. The argument plugin system should set this, but
+ *   if the context is coming from elsewhere it will need to be set manually.
+ *
+ * @return
+ *   TRUE if access is granted, false if otherwise.
+ */
+function ctools_access_menu($plugin_name, $settings) {
+  $contexts = array();
+  foreach (func_get_args() as $arg) {
+    if (is_object($arg) && get_class($arg) == 'ctools_context') {
+      $contexts[$arg->id] = $arg;
+    }
+  }
+
+  global $user;
+  return ctools_access($plugin_name, $settings, $contexts, $user);
+}
+
+/**
+ * Determine if the current user has access via  plugin.
+ *
+ * @param $plugin_name
+ *   The access plugin to use. If empty or 'none' then no access control
+ *   is being used and this function returns TRUE. If the plugin can't be
+ *   found otherwise, this function automatically returns FALSE.
+ * @param $settings
+ *   An array of settings theoretically set by the user.
+ * @param $contexts
+ *   An array of zero or more contexts that may be used to determine if
+ *   the user has access.
+ * @param $account
+ *   The account to test against. If NULL the logged in user will be tested.
+ *
+ * @return
+ *   TRUE if access is granted, false if otherwise.
+ */
+function ctools_access($plugin_name, $settings, $contexts = array(), $account = NULL) {
+  if (!$account) {
+    global $user;
+    $account = $user;
+  }
+
+  if (empty($plugin_name) || $plugin_name == 'none') {
+    return TRUE;
+  }
+
+  ctools_include('context');
+  $plugin = ctools_get_access_plugin($plugin_name);
+  if (!$plugin) {
+    return FALSE;
+  }
+
+  if ($function = ctools_plugin_get_function($plugin, 'callback')) {
+    return $function($settings, $contexts, $account);
+  }
+}
diff --git a/delegator/css/page-task.css b/delegator/css/page-task.css
new file mode 100644
index 00000000..f4579235
--- /dev/null
+++ b/delegator/css/page-task.css
@@ -0,0 +1,10 @@
+/* $Id$ */
+
+.delegator-page-operations {
+  text-align: right;
+}
+
+td.delegator-page-operations {
+  width: 175px;
+}
+
diff --git a/delegator/delegator.module b/delegator/delegator.module
index 4eda9a64..213ac7f7 100644
--- a/delegator/delegator.module
+++ b/delegator/delegator.module
@@ -539,3 +539,12 @@ function delegator_make_task_name($task_id, $subtask_id) {
   }
 }
 
+/**
+ * Delegator for arg load function because menu system will not load extra
+ * files for these; they must be in a .module.
+ */
+function dp_arg_load($value, $subtask, $argument) {
+  require_once './' . drupal_get_path('module', 'delegator') . '/plugins/tasks/page.inc';
+  return _dp_arg_load($value, $subtask, $argument);
+}
+
diff --git a/delegator/help/api-task-type.html b/delegator/help/api-task-type.html
new file mode 100644
index 00000000..144846e0
--- /dev/null
+++ b/delegator/help/api-task-type.html
@@ -0,0 +1,3 @@
+<!-- $Id$ -->
+
+defines a task type, grouping tasks together and providing a common UI for them.
\ No newline at end of file
diff --git a/delegator/help/page-task-type.html b/delegator/help/page-task-type.html
new file mode 100644
index 00000000..c382c76f
--- /dev/null
+++ b/delegator/help/page-task-type.html
@@ -0,0 +1,4 @@
+
+Additional 'task' keys support:
+
+operations -- a list of operations suitable for theme('links')
\ No newline at end of file
diff --git a/delegator/plugins/task_types/page.admin.inc b/delegator/plugins/task_types/page.admin.inc
new file mode 100644
index 00000000..79ca9f38
--- /dev/null
+++ b/delegator/plugins/task_types/page.admin.inc
@@ -0,0 +1,88 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Administrative functions for the page system
+ */
+function delegator_page_type_list() {
+  $tasks = delegator_get_tasks_by_type('page');
+
+  $tables = array(
+    'singles' => array(
+      'title' => t('System pages'),
+      'description' => t('Pages provided by the system that can be overridden to provide alternate content or layout.'),
+      'rows' => array(),
+    ),
+    'page' => array(), // give this one special weighting,
+  );
+
+  delegator_page_type_sort_tasks($tasks, $tables, 'singles');
+
+  $output = '';
+  $header = array(
+    array('data' => t('Title'), 'class' => 'delegator-page-title'),
+    array('data' => t('Path'), 'class' => 'delegator-page-path'),
+    array('data' => t('Operations'), 'class' => 'delegator-page-operations'),
+  );
+
+  foreach ($tables as $bucket => $info) {
+    if (!empty($info['rows'])) {
+      $output .= '<h3>' . check_plain($info['title']) . '</h3>';
+      $output .= '<div class="description">' . check_plain($info['description']) . '</div>';
+      if (isset($info['operations'])) {
+        $info['rows'][] = array('data' => array(array('data' => theme('links', $info['operations']), 'colspan' => 3)), 'class' => 'delegator-page-operations');
+      }
+    }
+
+    $output .= theme('table', $header, $info['rows']);
+  }
+
+  drupal_add_css(drupal_get_path('module', 'delegator') . '/css/page-task.css');
+  return $output;
+}
+
+/**
+ * Sort tasks into buckets based upon whether or not they have subtasks.
+ */
+function delegator_page_type_sort_tasks($tasks, &$tables, $bucket, $task_id = NULL) {
+  foreach ($tasks as $id => $task) {
+    // If a type has subtasks, add its subtasks in its own table.
+    if (!empty($task['subtasks'])) {
+      $tables[$id]['title'] = $task['title'];
+      $tables[$id]['description'] = $task['description'];
+      if (isset($task['operations'])) {
+        $tables[$id]['operations'] = $task['operations'];
+      }
+
+      delegator_page_type_sort_tasks(delegator_get_task_subtasks($task), $tables, $id, $task['name']);
+      continue;
+    }
+
+    if (isset($task_id)) {
+      $task_name = delegator_make_task_name($task_id, $task['name']);
+    }
+    else {
+      $task_name = $task['name'];
+    }
+
+    $row = array('data' => array(), 'class' => 'page-task-' . $id);
+    $row['data'][] = array('data' => $task['admin title'], 'class' => 'delegator-page-title');
+    $row['data'][] = array('data' => $task['admin path'], 'class' => 'delegator-page-path');
+    if (isset($task['operations'])) {
+      $operations = $task['operations'];
+    }
+    else {
+      $operations = array(
+        array(
+          'title' => t('Task handlers'),
+          'href' => "admin/build/delegator/$task_name"
+        ),
+      );
+    }
+    $row['data'][] = array('data' => theme('ctools_dropdown', t('Operations'), $operations), 'class' => 'delegator-page-operations');
+
+    $tables[$bucket]['rows'][] = $row;
+  }
+}
+
diff --git a/delegator/plugins/task_types/page.inc b/delegator/plugins/task_types/page.inc
new file mode 100644
index 00000000..54259333
--- /dev/null
+++ b/delegator/plugins/task_types/page.inc
@@ -0,0 +1,56 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Groups tasks that provide pages and provides a common administrative UI for them.
+ *
+ * The page task supports any task that defines itself as type 'page' and it also
+ * includes special handling for the 'page' task to provide a nice location to
+ * place administer the list of pages, including adding removing and editing
+ * the handlers of pages alongside the less generic tasks such as node and
+ * user viewing.
+ */
+
+/**
+ * Specialized implementation of hook_delegator_tasks(). See api-task.html for
+ * more information.
+ */
+function delegator_page_delegator_task_types() {
+  return array(
+    'page' => array(
+      'title' => t('Pages'),
+      'admin path' => 'admin/build/pages',
+      'hook menu' => 'delegator_page_type_menu',
+    ),
+  );
+}
+
+/**
+ * Delegated implementation of hook_menu().
+ */
+function delegator_page_type_menu(&$items, $task) {
+  // Set up access permissions.
+  $access_callback = isset($task['admin access callback']) ? $task['admin access callback'] : 'user_access';
+
+  // @todo use 'administer pages' perm instead?
+  $access_arguments = isset($task['admin access arguments']) ? $task['admin access arguments'] : array('administer delegator');
+
+  $base = array(
+    'access callback' => $access_callback,
+    'access arguments' => $access_arguments,
+    'file' => 'plugins/task_types/page.admin.inc',
+  );
+
+  $items['admin/build/pages'] = array(
+    'title' => 'Pages',
+    'description' => 'Add, edit and remove overridden system pages and user defined pages from the system.',
+    'page callback' => 'delegator_page_type_list',
+  ) + $base;
+
+  $items['admin/build/pages/list'] = array(
+    'title' => 'List',
+    'page callback' => 'delegator_page_type_list',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+}
diff --git a/delegator/plugins/tasks/node_view.inc b/delegator/plugins/tasks/node_view.inc
index 059bbd0b..a5d03e1f 100644
--- a/delegator/plugins/tasks/node_view.inc
+++ b/delegator/plugins/tasks/node_view.inc
@@ -53,6 +53,10 @@ function delegator_node_view($node) {
     if ($function = ctools_plugin_load_function('delegator', 'task_handlers', $handler->handler, 'render')) {
       $output = $function($handler, $node);
       if ($output) {
+        // Since we're not using node_show() we need to emulate what it used to do.
+        // Update the history table, stating that this user viewed this node.
+        node_tag_new($node->nid);
+
         // TRUE is a special return used to let us know that it handled the
         // task but does not wish us to render anything, as it already did.
         // This is needed for the 'no blocks' functionality.
diff --git a/delegator/plugins/tasks/page.admin.inc b/delegator/plugins/tasks/page.admin.inc
index e4c1fb6b..07881067 100644
--- a/delegator/plugins/tasks/page.admin.inc
+++ b/delegator/plugins/tasks/page.admin.inc
@@ -9,6 +9,161 @@
  * only when needed.
  */
 
+/**
+ * Delegated implementation of hook_menu().
+ */
+function delegator_page_menu(&$items, $task) {
+  // Set up access permissions.
+  $access_callback = isset($task['admin access callback']) ? $task['admin access callback'] : 'user_access';
+  $access_arguments = isset($task['admin access arguments']) ? $task['admin access arguments'] : array('administer delegator');
+
+  $base = array(
+    'access callback' => $access_callback,
+    'access arguments' => $access_arguments,
+    'file' => 'plugins/tasks/page.admin.inc',
+  );
+
+  $items['admin/build/pages/add'] = array(
+    'title' => 'Add page',
+    'description' => 'Add a delegator page subtask.',
+    'page callback' => 'delegator_page_add_subtask',
+    'type' => MENU_LOCAL_TASK,
+  ) + $base;
+
+  $form_info = delegator_page_edit_form_info();
+  $default_task = FALSE;
+  $weight = 0;
+  foreach ($form_info['order'] as $form_id => $form_title) {
+    // The first edit form is the default for tabs, so it gets a bit
+    // of special treatment here.
+    if (!$default_task) {
+      $default_task = TRUE;
+      // Add the callback for the default tab.
+      $items["admin/build/pages/edit/%"] = array(
+        'title' => t('Edit'),
+        'page callback' => 'delegator_page_edit_subtask',
+        'page arguments' => array(4, $form_id),
+      ) + $base;
+
+      // And make sure it's the default local task.
+      $type = MENU_DEFAULT_LOCAL_TASK;
+    }
+    else {
+      // This allows an empty form title to provide a hidden form
+      // which is useful for doing more branch-like multi-step
+      // functionality.
+      $type = $form_title ? MENU_LOCAL_TASK : MENU_CALLBACK;
+    }
+
+    // Handler to edit delegator task handlers. May exist in its own UI.
+    $items["admin/build/pages/edit/%/$form_id"] = array(
+      'title' => $form_title,
+      'page callback' => 'delegator_page_edit_subtask',
+      'page arguments' => array(4, 5),
+      'type' => $type,
+      'weight' => $weight++,
+    ) + $base;
+  }
+
+  // AJAX callbacks for argument modal.
+  $items['admin/build/delegator/argument'] = array(
+    'page callback' => 'delegator_page_subtask_argument_ajax',
+  ) + $base;
+
+  // Add menu entries for each subtask
+  foreach (delegator_page_load_all() as $subtask_id => $subtask) {
+    if (!isset($subtask->access['settings'])) {
+      $subtask->access['settings'] = NULL;
+    }
+
+    $path = array();
+    $page_arguments = array($subtask_id);
+    $access_arguments = array($subtask->access['type'], $subtask->access['settings']);
+    $load_arguments = array($subtask_id, '%index');
+
+    // Replace named placeholders with our own placeholder to load contexts.
+    foreach (explode('/', $subtask->path) as $position => $bit) {
+      if ($bit[0] == '%' && $bit != '%') {
+        // If an argument, swap it out with our argument loader and make sure
+        // the argument gets passed through to the page callback.
+        $path[] = '%dp_arg';
+        $page_arguments[] = $position;
+        $access_arguments[] = $position;
+      }
+      else {
+        $path[] = $bit;
+      }
+    }
+
+    $menu_path = implode('/', $path);
+
+    $items[$menu_path] = delegator_page_menu_item($subtask->menu, $access_arguments, $page_arguments, $load_arguments);
+
+    // Add a parent menu item if one is configured.
+    if ($subtask->menu['type'] == 'default tab' && $subtask->menu['parent']['type'] != 'none') {
+      array_pop($path);
+      $parent_path = implode('/', $path);
+      $items[$parent_path] = delegator_page_menu_item($subtask->menu['parent'], $access_arguments, $page_arguments, $load_arguments);
+    }
+  }
+}
+
+/**
+ * Create a menu item for delegator pages.
+ *
+ * @param $menu
+ *   The configuration to use. It will contain a type, and depending on the
+ *   type may also contain weight, title and name. These are presumed to have
+ *   been configured from the UI.
+ * @param $access_arguments
+ *   Arguments that go with ctools_access_menu; it should be loaded with
+ *   the access plugin type, settings, and positions of any arguments that
+ *   may produce contexts.
+ * @param $page_arguments
+ *   This should be seeded with the subtask name for easy loading and like
+ *   the access arguments above should contain positions of arguments so
+ *   that the menu system passes contexts through.
+ * @param $load_arguments
+ *   Arguments to send to the arg loader; should be the subtask id and '%index'.
+ */
+function delegator_page_menu_item($menu, $access_arguments, $page_arguments, $load_arguments) {
+  $item = array(
+    'access callback' => 'ctools_access_menu',
+    'access arguments' => $access_arguments,
+    'page callback' => 'delegator_page_execute',
+    'page arguments' => $page_arguments,
+    'load arguments' => $load_arguments,
+    'file' => 'plugins/tasks/page.admin.inc',
+  );
+
+  if (isset($menu['title'])) {
+    $item['title'] = $menu['title'];
+  }
+  if (isset($menu['weight'])) {
+    $item['weight'] = $menu['weight'];
+  }
+
+  switch ($menu['type']) {
+    case 'none':
+    default:
+      $item['type'] = MENU_CALLBACK;
+      break;
+    case 'normal':
+      $item['type'] = MENU_NORMAL_ITEM;
+      // Insert item into the proper menu
+      $item['menu_name'] = $menu['name'];
+      break;
+    case 'tab':
+      $item['type'] = MENU_LOCAL_TASK;
+      break;
+    case 'default tab':
+      $item['type'] = MENU_DEFAULT_LOCAL_TASK;
+      break;
+  }
+
+  return $item;
+}
+
 /**
  * Get the cached changes to a given task handler.
  */
@@ -147,7 +302,7 @@ function delegator_page_edit_subtask($page_name, $step = NULL) {
 function delegator_page_add_subtask_finish(&$form_state) {
   $page = &$form_state['page'];
   // Ensure $page->arguments contains only real arguments:
-  $arguments = delegator_page_get_arguments($page->path);
+  $arguments = delegator_page_get_named_arguments($page->path);
   $args = array();
   foreach ($arguments as $keyword => $position) {
     if (isset($page->arguments[$keyword])) {
@@ -240,7 +395,7 @@ function delegator_page_form_basic_validate_filter($value) {
  */
 function delegator_page_form_basic_validate(&$form, &$form_state) {
   // Ensure name is properly formed.
-  $args = delegator_page_get_arguments($form_state['values']['path']);
+  $args = delegator_page_get_named_arguments($form_state['values']['path']);
   if ($invalid_args = array_filter($args, 'delegator_page_form_basic_validate_filter')) {
     foreach ($invalid_args as $arg => $position) {
       form_error($form['path'], t('Duplicated argument %arg', array('%arg' => $arg)));
@@ -428,14 +583,7 @@ function delegator_page_form_access(&$form, &$form_state) {
 
   $contexts = array();
   // Load contexts based on argument data:
-  if ($page->arguments) {
-    $arguments = array();
-    foreach ($page->arguments as $keyword => $argument) {
-      if (isset($argument['name'])) {
-        $argument['keyword'] = $keyword;
-        $arguments[$argument['id']] = $argument;
-      }
-    }
+  if ($arguments = delegator_page_get_arguments($page)) {
     $contexts = ctools_context_get_placeholders_from_argument($arguments);
   }
 
@@ -549,7 +697,7 @@ function delegator_page_form_argument(&$form, &$form_state) {
   $path = $form_state['page']->path;
   $page = &$form_state['page'];
 
-  $arguments = delegator_page_get_arguments($path);
+  $arguments = delegator_page_get_named_arguments($path);
 
   $form['table'] = array(
     '#theme' => 'delegator_page_form_argument_table',
@@ -671,7 +819,7 @@ function delegator_page_subtask_argument_ajax($step = NULL, $cache_name = NULL,
   }
 
   $path = $page->path;
-  $arguments = delegator_page_get_arguments($path);
+  $arguments = delegator_page_get_named_arguments($path);
 
   // Load stored object from cache.
   if (!isset($arguments[$keyword])) {
diff --git a/delegator/plugins/tasks/page.inc b/delegator/plugins/tasks/page.inc
index 84630943..f86658b2 100644
--- a/delegator/plugins/tasks/page.inc
+++ b/delegator/plugins/tasks/page.inc
@@ -7,6 +7,9 @@
  *
  * This creates subtasks and stores them in the delegator_pages table. These
  * are exportable objects, too.
+ *
+ * The render callback for this task type has $handler, $page, $contexts as
+ * parameters.
  */
 
 /**
@@ -19,8 +22,13 @@ function delegator_page_delegator_tasks() {
       'title' => t('User pages'),
       'description' => t('Administrator created pages that have a URL path, access control and entries in the Drupal menu system.'),
       'subtasks' => TRUE,
+      'subtask callback' => 'delegator_page_subtask',
       'subtasks callback' => 'delegator_page_subtasks',
-      'hook menu' => 'delegator_page_menu',
+      'hook menu' => array(
+        'file' => 'page.admin.inc',
+        'path' => drupal_get_path('module', 'delegator') . '/plugins/tasks',
+        'function' => 'delegator_page_menu',
+      ),
       'hook theme' => 'delegator_page_theme',
 
       // page only items
@@ -35,6 +43,11 @@ function delegator_page_delegator_tasks() {
           'href' => 'admin/build/pages/add',
         ),
       ),
+
+      // context only items
+      'type' => 'context',
+      'get arguments' => 'delegator_page_get_argument_callback',
+      'get context placeholders' => 'delegator_page_get_contexts',
     ),
   );
 }
@@ -43,131 +56,92 @@ function delegator_page_delegator_tasks() {
  * Return a list of all subtasks.
  */
 function delegator_page_subtasks($task) {
-  $subtasks = delegator_page_load_all();
+  $pages = delegator_page_load_all();
   $return = array();
-  foreach ($subtasks as $name => $subtask) {
-    $form_info = delegator_page_edit_form_info();
-    $edit_links = array();
-    foreach ($form_info['order'] as $form_id => $form_title) {
-      $edit_links[] = array(
-        'title' => $form_title,
-        'href' => "admin/build/pages/edit/$name/$form_id",
-      );
-    }
-
-    $operations = array();
-
-    if (empty($subtask->disabled)) {
-      $operations[] =  array(
-        'title' => '<span class="text">' . t('Edit page') . '</span>' . theme('links', $edit_links),
-        'html' => TRUE,
-      );
-      $operations[] = array(
-        'title' => t('Clone'),
-        'href' => "admin/build/pages/clone/$name",
-      );
-      $operations[] = array(
-        'title' => t('Export'),
-        'href' => "admin/build/pages/export/$name",
-      );
-      if ($subtask->export_type == (EXPORT_IN_CODE | EXPORT_IN_DATABASE)) {
-        $operations[] = array(
-          'title' => t('Revert'),
-          'href' => "admin/build/pages/delete/$name",
-        );
-      }
-      else if ($subtask->export_type == EXPORT_IN_CODE) {
-        $operations[] = array(
-          'title' => t('Disable'),
-          'href' => "admin/build/pages/disable/$name",
-        );
-      }
-      else {
-        $operations[] = array(
-          'title' => t('Delete'),
-          'href' => "admin/build/pages/delete/$name",
-        );
-      }
-    }
-    else {
-      $operations[] = array(
-        'title' => t('Enable'),
-        'href' => "admin/build/pages/enable/$name",
-      );
-    }
-    $return[$name] = array(
-      'name' => $name,
-      'admin title' => $subtask->admin_title,
-      'admin description' => t('TODO'),
-      'admin path' => $subtask->path,
-      'subtask' => $subtask,
-      'operations' => $operations,
-    );
+  foreach ($pages as $name => $page) {
+    $return[$name] = delegator_page_build_subtask($task, $page);
   }
 
   return $return;
 }
 
 /**
- * Delegated implementation of hook_menu().
+ * Callback to return a single subtask.
  */
-function delegator_page_menu(&$items, $task) {
-  // Set up access permissions.
-  $access_callback = isset($task['admin access callback']) ? $task['admin access callback'] : 'user_access';
-  $access_arguments = isset($task['admin access arguments']) ? $task['admin access arguments'] : array('administer delegator');
-
-  $base = array(
-    'access callback' => $access_callback,
-    'access arguments' => $access_arguments,
-    'file' => 'plugins/tasks/page.admin.inc',
-  );
-
-  $items['admin/build/pages/add'] = array(
-    'title' => 'Add page',
-    'description' => 'Add a delegator page subtask.',
-    'page callback' => 'delegator_page_add_subtask',
-    'type' => MENU_LOCAL_TASK,
-  ) + $base;
+function delegator_page_subtask($task, $subtask_id) {
+  $page = delegator_page_load($subtask_id);
+  if ($page) {
+    return delegator_page_build_subtask($task, $page);
+  }
+}
 
+/**
+ * Build a subtask array for a given page.
+ */
+function delegator_page_build_subtask($task, $page) {
   $form_info = delegator_page_edit_form_info();
-  $default_task = FALSE;
-  $weight = 0;
+  $edit_links = array();
+  $name = $page->name;
+
   foreach ($form_info['order'] as $form_id => $form_title) {
-    // The first edit form is the default for tabs, so it gets a bit
-    // of special treatment here.
-    if (!$default_task) {
-      $default_task = TRUE;
-      // Add the callback for the default tab.
-      $items["admin/build/pages/edit/%"] = array(
-        'title' => t('Edit'),
-        'page callback' => 'delegator_page_edit_subtask',
-        'page arguments' => array(4, $form_id),
-      ) + $base;
-
-      // And make sure it's the default local task.
-      $type = MENU_DEFAULT_LOCAL_TASK;
+    $edit_links[] = array(
+      'title' => $form_title,
+      'href' => "admin/build/pages/edit/$name/$form_id",
+    );
+  }
+
+  $operations = array();
+
+  if (empty($page->disabled)) {
+    $operations[] = array(
+      'title' => t('Task handlers'),
+      'href' => "admin/build/delegator/" . delegator_make_task_name($task['name'], $name),
+    );
+    $operations[] =  array(
+      'title' => '<span class="text">' . t('Edit page') . '</span>' . theme('links', $edit_links),
+      'html' => TRUE,
+    );
+    $operations[] = array(
+      'title' => t('Clone'),
+      'href' => "admin/build/pages/clone/$name",
+    );
+    $operations[] = array(
+      'title' => t('Export'),
+      'href' => "admin/build/pages/export/$name",
+    );
+    if ($page->export_type == (EXPORT_IN_CODE | EXPORT_IN_DATABASE)) {
+      $operations[] = array(
+        'title' => t('Revert'),
+        'href' => "admin/build/pages/delete/$name",
+      );
+    }
+    else if ($page->export_type == EXPORT_IN_CODE) {
+      $operations[] = array(
+        'title' => t('Disable'),
+        'href' => "admin/build/pages/disable/$name",
+      );
     }
     else {
-      // This allows an empty form title to provide a hidden form
-      // which is useful for doing more branch-like multi-step
-      // functionality.
-      $type = $form_title ? MENU_LOCAL_TASK : MENU_CALLBACK;
+      $operations[] = array(
+        'title' => t('Delete'),
+        'href' => "admin/build/pages/delete/$name",
+      );
     }
-
-    // Handler to edit delegator task handlers. May exist in its own UI.
-    $items["admin/build/pages/edit/%/$form_id"] = array(
-      'title' => $form_title,
-      'page callback' => 'delegator_page_edit_subtask',
-      'page arguments' => array(4, 5),
-      'type' => $type,
-      'weight' => $weight++,
-    ) + $base;
   }
-
-  // AJAX callbacks for argument modal.
-  $items['admin/build/delegator/argument'] = array(
-    'page callback' => 'delegator_page_subtask_argument_ajax',
-  ) + $base;
+  else {
+    $operations[] = array(
+      'title' => t('Enable'),
+      'href' => "admin/build/pages/enable/$name",
+    );
+  }
+  return array(
+    'name' => $name,
+    'admin title' => $page->admin_title,
+    'admin description' => t('TODO'),
+    'admin path' => $page->path,
+    'subtask' => $page,
+    'operations' => $operations,
+  );
 }
 
 /**
@@ -221,6 +195,139 @@ function delegator_page_edit_form_info() {
   );
 }
 
+// --------------------------------------------------------------------------
+// Page execution functions
+
+/**
+ * Load a context from an argument for a given page task.
+ *
+ * @param $value
+ *   The incoming argument value.
+ * @param $subtask
+ *   The subtask id.
+ * @param $argument
+ *   The numeric position of the argument in the path, counting from 0.
+ *
+ * @return
+ *   A context item if one is configured, the argument if one is not, or
+ *   FALSE if restricted or invalid.
+ */
+function _dp_arg_load($value, $subtask, $argument) {
+  $page = delegator_page_load($subtask);
+  if (!$page) {
+    return FALSE;
+  }
+
+  $path = explode('/', $page->path);
+  if (empty($path[$argument])) {
+    return FALSE;
+  }
+
+  $keyword = substr($path[$argument], 1);
+  if (empty($page->arguments[$keyword])) {
+    return $value;
+  }
+
+  $page->arguments[$keyword]['keyword'] = $keyword;
+
+  ctools_include('context');
+  $context = ctools_context_get_context_from_argument($page->arguments[$keyword], $value);
+
+  // convert false equivalents to false.
+  return $context ? $context : FALSE;
+}
+
+/**
+ * Execute a page task.
+ *
+ * This is the callback to entries in the Drupal menu system created by the
+ * page task.
+ *
+ * @param $subtask_id
+ *   The name of the page task used.
+ * @param ...
+ *   A number of context objects as specified by the user when
+ *   creating named arguments in the path.
+ */
+function delegator_page_execute($subtask_id) {
+  // Turn the contexts into a properly keyed array.
+  $contexts = array();
+  foreach (func_get_args() as $arg) {
+    if (is_object($arg) && get_class($arg) == 'ctools_context') {
+      $contexts[$arg->id] = $arg;
+    }
+  }
+
+  $task = delegator_get_task('page');
+  $handlers = delegator_load_sorted_handlers($task, $subtask_id);
+
+  $page = delegator_page_load($subtask_id);
+
+  // Try each handler.
+  foreach ($handlers as $handler) {
+    if ($function = ctools_plugin_load_function('delegator', 'task_handlers', $handler->handler, 'render')) {
+      $output = $function($handler, $page, $contexts);
+      if ($output) {
+        // TRUE is a special return used to let us know that it handled the
+        // task but does not wish us to render anything, as it already did.
+        // This is needed for the 'no blocks' functionality.
+        if ($output === TRUE) {
+          return;
+        }
+        return $output;
+      }
+    }
+  }
+
+  return drupal_access_denied();
+}
+
+// --------------------------------------------------------------------------
+// Context type callbacks
+
+/**
+ * Return a list of arguments used by this task.
+ */
+function delegator_page_get_argument_callback($task, $subtask_id) {
+  $page = delegator_page_load($subtask_id);
+
+  if ($page) {
+    return delegator_page_get_arguments($page);
+  }
+}
+
+/**
+ * Get a group of context placeholders for the arguments.
+ */
+function delegator_page_get_contexts($task, $subtask_id) {
+  $page = delegator_page_load($subtask_id);
+
+  if ($page && $arguments = delegator_page_get_arguments($page)) {
+    ctools_include('context');
+    return ctools_context_get_placeholders_from_argument($arguments);
+  }
+}
+
+/**
+ * Return a list of arguments used by this page.
+ *
+ * This provides a list of arguments suitable for using in the context
+ * system, which is slightly more data than we store in the database.
+ * This also filters out arguments that have no contexts.
+ */
+function delegator_page_get_arguments($page) {
+  if ($page->arguments) {
+    $arguments = array();
+    foreach ($page->arguments as $keyword => $argument) {
+      if (isset($argument['name'])) {
+        $argument['keyword'] = $keyword;
+        $arguments[$keyword] = $argument;
+      }
+    }
+    return $arguments;
+  }
+}
+
 // --------------------------------------------------------------------------
 // Page task database info.
 
diff --git a/help/context-arguments.html b/help/context-arguments.html
index fca3a017..f59b4a64 100644
--- a/help/context-arguments.html
+++ b/help/context-arguments.html
@@ -13,3 +13,8 @@ string as though it came from a URL element.
     'settings form' => params: $form, $form_state, $conf -- gets the whole form. Should put anything it wants to keep automatically in $form['settings']
     'settings form validate' => params: $form, $form_state
     'settings form submit' => params: $form, $form_state
+
+    'criteria form' => params: $form, &$form_state, $conf, $argument, $id -- gets the whole argument. It should only put form widgets in $form[$id]. $conf may not be properly initialized so always guard against this due to arguments being changed and handlers not being updated to match.
+    + submit + validate
+
+    'criteria select' => returns true if the selected criteria matches the context. params: $context, $conf
diff --git a/includes/context.inc b/includes/context.inc
index 23d6a1e4..0a3bfcda 100644
--- a/includes/context.inc
+++ b/includes/context.inc
@@ -480,6 +480,8 @@ function ctools_context_get_context_from_argument($argument, $arg, $empty = FALS
       $context->identifier = $argument['identifier'];
       $context->page_title = isset($argument['title']) ? $argument['title'] : '';
       $context->keyword    = $argument['keyword'];
+      $context->id         = ctools_context_id($argument, 'argument');
+      $context->original_argument = $arg;
     }
     return $context;
   }
diff --git a/includes/dropdown.theme.inc b/includes/dropdown.theme.inc
index c52f6b7c..54b42b58 100644
--- a/includes/dropdown.theme.inc
+++ b/includes/dropdown.theme.inc
@@ -3,7 +3,34 @@
 
 /**
  * @file
- * Theme registry for dropdown-div tool.
+ * Provide a javascript based dropdown menu.
+ *
+ * The dropdown menu will show up as a clickable link; when clicked,
+ * a small menu will slide down beneath it, showing the list of links.
+ *
+ * The dropdown will stay open until either the user has moved the mouse
+ * away from the box for > .5 seconds, or can be immediately closed by
+ * clicking the link again. The code is smart enough that if the mouse
+ * moves away and then back within the .5 second window, it will not
+ * re-close.
+ *
+ * Multiple dropdowns can be placed per page.
+ *
+ * If the user does not have javascript enabled, the link will not appear,
+ * and instead by default the list of links will appear as a normal inline
+ * list.
+ *
+ * The menu is heavily styled by default, and to make it look different
+ * will require a little bit of CSS. You can apply your own class to the
+ * dropdown to help ensure that your CSS can override the dropdown's CSS.
+ *
+ * In particular, the text, link, background and border colors may need to
+ * be changed. Please see dropdown.css for information about the existing
+ * styling.
+ */
+
+/**
+ * Delegated implementation of hook_theme()
  */
 function ctools_dropdown_theme(&$items) {
   $items['ctools_dropdown'] = array(
@@ -14,6 +41,16 @@ function ctools_dropdown_theme(&$items) {
 
 /**
  * Create a dropdown menu.
+ *
+ * @param $title
+ *   The text to place in the clickable area to activate the dropdown.
+ * @param $links
+ *   A list of links to provide within the dropdown, suitable for use
+ *   in via Drupal's theme('links').
+ * @param $class
+ *   An optional class to add to the dropdown's container div to allow you
+ *   to style a single dropdown however you like without interfering with
+ *   other dropdowns.
  */
 function theme_ctools_dropdown($title, $links, $class = '') {
   // Provide a unique identifier for every dropdown on the page.
diff --git a/includes/plugins.inc b/includes/plugins.inc
index 4c910603..61235643 100644
--- a/includes/plugins.inc
+++ b/includes/plugins.inc
@@ -258,7 +258,6 @@ function ctools_plugin_get_function($plugin, $function_name) {
   // If cached the .inc file may not have been loaded. require_once is quite safe
   // and fast so it's okay to keep calling it.
   if (isset($plugin['file'])) {
-    if (!is_array($plugin)) { vpr_trace(); }
     require_once './' . $plugin['path'] . '/' . $plugin['file'];
   }
 
diff --git a/js/collapsible-div.js b/js/collapsible-div.js
index a51035c1..36b54f2b 100644
--- a/js/collapsible-div.js
+++ b/js/collapsible-div.js
@@ -1,11 +1,8 @@
 // $Id$
-
-// All CTools tools begin with this:
-if (!Drupal.CTools) {
-  Drupal.CTools = {};
-}
-
 /**
+ * @file
+ * Javascript required for a simple collapsible div.
+ *
  * Creating a collapsible div with this doesn't take too much. There are 
  * three classes necessary:
  *
@@ -21,6 +18,12 @@ if (!Drupal.CTools) {
  * a class, which will cause the container to draw collapsed.
  */
 
+// All CTools tools begin with this if they need to use the CTools namespace.
+if (!Drupal.CTools) {
+  Drupal.CTools = {};
+}
+
+
 // Set up an array for callbacks.
 Drupal.CTools.CollapsibleCallbacks = [];
 Drupal.CTools.CollapsibleCallbacksAfterToggle = [];
diff --git a/js/dependent.js b/js/dependent.js
index 5f2be002..8c800c4f 100644
--- a/js/dependent.js
+++ b/js/dependent.js
@@ -1,6 +1,6 @@
 // $Id$
 /**
- * @file dependent.js
+ * @file
  *
  * Written by dmitrig01 (Dmitri Gaskin) for CTools; this provides dependent
  * visibility for form items in CTools' ajax forms.
diff --git a/js/dropdown.js b/js/dropdown.js
index 67fcd6ae..17a51499 100644
--- a/js/dropdown.js
+++ b/js/dropdown.js
@@ -1,5 +1,26 @@
 // $Id$
-
+/**
+ * @file
+ * Implement a simple, clickable dropdown menu.
+ *
+ * See dropdown.theme.inc for primary documentation.
+ *
+ * The javascript relies on four classes:
+ * - The dropdown must be fully contained in a div with the class 
+ *   ctools-dropdown. It must also contain the class ctools-dropdown-no-js
+ *   which will be immediately removed by the javascript; this allows for
+ *   graceful degradation.
+ * - The trigger that opens the dropdown must be an a tag wit hthe class
+ *   ctools-dropdown-link. The href should just be '#' as this will never
+ *   be allowed to complete.
+ * - The part of the dropdown that will appear when the link is clicked must
+ *   be a div with class ctools-dropdown-container.
+ * - Finally, ctools-dropdown-hover will be placed on any link that is being
+ *   hovered over, so that the browser can restyle the links.
+ *
+ * This tool isn't meant to replace click-tips or anything, it is specifically
+ * meant to work well presenting menus.
+ */
 Drupal.behaviors.CToolsDropdown = function() {
   $('div.ctools-dropdown:not(.ctools-dropdown-processed)')
     .removeClass('ctools-dropdown-no-js')
diff --git a/plugins/arguments/nid.inc b/plugins/arguments/nid.inc
index 2dc9009b..7fc11503 100644
--- a/plugins/arguments/nid.inc
+++ b/plugins/arguments/nid.inc
@@ -15,7 +15,9 @@ function ctools_nid_ctools_arguments() {
     'title' => t("Node ID"),
     'keyword' => 'node',
     'description' => t('Creates a node context from a node ID argument.'),
-    'context' => 'ctools_nid_context',
+    'context' => 'ctools_argument_nid_context',
+    'criteria form' => 'ctools_argument_nid_criteria_form',
+    'criteria select' => 'ctools_argument_nid_criteria_select',
   );
   return $args;
 }
@@ -23,19 +25,65 @@ function ctools_nid_ctools_arguments() {
 /**
  * Discover if this argument gives us the node we crave.
  */
-function ctools_nid_context($node = NULL, $conf = NULL, $empty = FALSE) {
+function ctools_argument_nid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
   // If unset it wants a generic, unfilled context.
   if ($empty) {
     return ctools_context_create_empty('node');
   }
 
-  if (!is_object($node)) {
-    return NULL;
+  if (!is_numeric($arg)) {
+    return FALSE;
   }
 
-  if (array_filter($conf['types']) && empty($conf['types'][$node->type])) {
-    return NULL;
+  $node = node_load($arg);
+  if (!$node) {
+    return FALSE;
   }
 
   return ctools_context_create('node', $node);
 }
+
+/**
+ * Provide a criteria form for selecting a node.
+ */
+function ctools_argument_nid_criteria_form(&$form, &$form_state, $conf, $argument, $id) {
+  // Ensure $conf has valid defaults:
+  if (!is_array($conf)) {
+    $conf = array();
+  }
+
+  $conf += array(
+    'type' => array(),
+  );
+
+  $types = node_get_types();
+  foreach ($types as $type => $info) {
+    $options[$type] = check_plain($info->name);
+  }
+
+  $form[$id]['type'] = array(
+    '#title' => t('Select types for %identifier', array('%identifier' => $argument['identifier'])),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#description' => t('This item will only be selected for nodes having the selected node types. If no node types are selected, it will be selected for all node types.'),
+    '#default_value' => $conf['type'],
+  );
+}
+
+
+/**
+ * Provide a criteria form for selecting a node.
+ */
+function ctools_argument_nid_criteria_select($conf, $context) {
+  // As far as I know there should always be a context at this point, but this
+  // is safe.
+  if (empty($context) || empty($context->data) || empty($context->data->type)) {
+    return FALSE;
+  }
+
+  if (array_filter($conf['type']) && empty($conf['type'][$context->data->type])) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
diff --git a/plugins/arguments/uid.inc b/plugins/arguments/uid.inc
index 8e76d75e..c310b04a 100644
--- a/plugins/arguments/uid.inc
+++ b/plugins/arguments/uid.inc
@@ -16,7 +16,9 @@ function ctools_uid_ctools_arguments() {
     // keyword to use for %substitution
     'keyword' => 'user',
     'description' => t('Creates a user context from a user ID argument.'),
-    'context' => 'ctools_uid_context',
+    'context' => 'ctools_argument_uid_context',
+    'criteria form' => 'ctools_argument_uid_criteria_form',
+    'criteria select' => 'ctools_argument_uid_criteria_select',
   );
   return $args;
 }
@@ -24,7 +26,7 @@ function ctools_uid_ctools_arguments() {
 /**
  * Discover if this argument gives us the user we crave.
  */
-function ctools_uid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+function ctools_argument_uid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
   // If unset it wants a generic, unfilled context.
   if ($empty) {
     return ctools_context_create_empty('user');
@@ -41,3 +43,47 @@ function ctools_uid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
 
   return ctools_context_create('user', $account);
 }
+
+/**
+ * Provide a criteria form for selecting a node.
+ */
+function ctools_argument_uid_criteria_form(&$form, &$form_state, $conf, $argument, $id) {
+  // Ensure $conf has valid defaults:
+  if (!is_array($conf)) {
+    $conf = array();
+  }
+
+  $conf += array(
+    'rids' => array(),
+  );
+
+
+  $form[$id]['rids'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Select roles for %identifier', array('%identifier' => $argument['identifier'])),
+    '#default_value' => $conf['rids'],
+    '#options' => ctools_get_roles(),
+    '#description' => t('This item will only be selected for users having the selected roles. If no roles are selected, it will be selected for all users.'),
+  );
+}
+
+/**
+ * Provide a criteria form for selecting a node.
+ */
+function ctools_argument_uid_criteria_select($conf, $context) {
+  // As far as I know there should always be a context at this point, but this
+  // is safe.
+  if (empty($context) || empty($context->data) || !isset($context->data->roles)) {
+    return FALSE;
+  }
+
+  $rids = array_filter($conf['rids']);
+  if (!$rids) {
+    return TRUE;
+  }
+
+  $roles = array_keys($context->data->roles);
+  $roles[] = $context->data->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
+
+  return array_intersect($rids, $roles);
+}
-- 
GitLab