From 070f75778b29d2cbcbf9d5145b2301761409c178 Mon Sep 17 00:00:00 2001
From: Kyle Browning <kylebrowning@me.com>
Date: Wed, 23 Mar 2011 11:47:36 -0700
Subject: [PATCH] Various CTOOLS Fixes

---
 .../export_ui/services_ctools_export_ui.class.php  |  378 ++++++++++++++++++++
 plugins/export_ui/services_ctools_export_ui.inc    |  204 +++++++++++
 services.admin.inc                                 |  349 +------------------
 services.install                                   |   24 ++
 services.module                                    |  133 ++-----
 services.resource_build.inc                        |    3 +
 tests/functional/ServicesEndpointTests.test        |   23 +-
 tests/functional/ServicesResourceFileTests.test    |    2 +
 tests/unit/TestServicesModule.test                 |   61 +---
 9 files changed, 669 insertions(+), 508 deletions(-)
 create mode 100644 plugins/export_ui/services_ctools_export_ui.class.php
 create mode 100644 plugins/export_ui/services_ctools_export_ui.inc

diff --git a/plugins/export_ui/services_ctools_export_ui.class.php b/plugins/export_ui/services_ctools_export_ui.class.php
new file mode 100644
index 0000000..330eb82
--- /dev/null
+++ b/plugins/export_ui/services_ctools_export_ui.class.php
@@ -0,0 +1,378 @@
+<?php
+
+/**
+ * @file
+ * Export-ui handler for the Services module.
+ */
+
+class services_ctools_export_ui extends ctools_export_ui {
+
+  /**
+   * Page callback for the resources page.
+   */
+  function resources_page($js, $input, $item) {
+    drupal_set_title($this->get_page_title('resources', $item));
+    return services_edit_endpoint_resources($item);
+  }
+
+  /**
+   * Page callback for the authentication page.
+   */
+  function authentication_page($js, $input, $item) {
+    drupal_set_title($this->get_page_title('authentication', $item));
+    return drupal_get_form('services_edit_form_endpoint_authentication', $item);
+  }
+}
+
+/**
+ * Endpoint authentication configuration form.
+ */
+function services_edit_form_endpoint_authentication($form, &$form_state) {
+  list($endpoint) = $form_state['build_info']['args'];
+  // Loading runtime include as needed by services_authentication_info().
+  module_load_include('runtime.inc', 'services');
+
+  $auth_modules = module_implements('services_authentication_info');
+
+  $form['endpoint_object'] = array(
+    '#type'  => 'value',
+    '#value' => $endpoint,
+  );
+
+  if (empty($auth_modules)) {
+    $form['message'] = array(
+      '#type'          => 'item',
+      '#title'         => t('No installed authentication modules'),
+      '#description'   => t('No authentication modules are installed, standard ' .
+        'Drupal session based security will be used.'),
+    );
+  }
+  elseif (empty($endpoint->authentication)) {
+    $form['message'] = array(
+      '#type'          => 'item',
+      '#title'         => t('No enabled authentication modules'),
+      '#value'   => t('No authentication modules are enabled, standard ' .
+        'Drupal session based security will be used.'),
+    );
+  }
+  else {
+    // Add configuration fieldsets for the authentication modules
+    foreach ($endpoint->authentication as $module => $settings) {
+      $info = services_authentication_info($module);
+      if ($info) {
+        $form[$module] = array(
+          '#type' => 'fieldset',
+          '#title' => $info['title'],
+          '#tree' => TRUE,
+        ) + services_auth_invoke($module, 'security_settings', $settings);
+      }
+    }
+  }
+
+  $form['submit'] = array(
+    '#type'  => 'submit',
+    '#value' => 'Save',
+  );
+
+  return $form;
+}
+
+function services_edit_form_endpoint_authentication_submit($form, $form_state) {
+  $endpoint = $form_state['values']['endpoint_object'];
+
+  foreach (array_keys($endpoint->authentication) as $module) {
+    $endpoint->authentication[$module] = $form_state['values'][$module];
+  }
+
+  drupal_set_message(t('Your authentication options have been saved.'));
+  services_endpoint_save($endpoint);
+}
+
+
+/**
+ * services_edit_endpoint_resources function.
+ *
+ * Edit Resources endpoint form
+ * @param object $endpoint
+ * @return string  The form to be displayed
+ */
+function services_edit_endpoint_resources($endpoint) {
+  if (!is_object($endpoint)) {
+    $endpoint = services_endpoint_load($endpoint);
+  }
+  if ($endpoint && !empty($endpoint->title)) {
+    drupal_set_title(check_plain($endpoint->title));
+  }
+  return drupal_get_form('services_edit_form_endpoint_resources', $endpoint);
+}
+
+/**
+ * services_edit_form_endpoint_resources function.
+ *
+ * @param array &$form_state
+ * @param object $endpoint
+ * @return Form
+ */
+function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
+  module_load_include('resource_build.inc', 'services');
+
+  $form = array();
+
+  $form['endpoint_object'] = array(
+    '#type'  => 'value',
+    '#value' => $endpoint,
+  );
+
+  $ops = array(
+    'create'   => t('Create'),
+    'retrieve' => t('Retrieve'),
+    'update'   => t('Update'),
+    'delete'   => t('Delete'),
+    'index'    => t('Index'),
+  );
+
+  // Call _services_build_resources() directly instead of
+  // services_get_resources to bypass caching.
+  $resources = _services_build_resources();
+  // Apply the endpoint in a non-strict mode, so that the non-active resources
+  // are preserved.
+  _services_apply_endpoint($resources, $endpoint, FALSE);
+
+  $res = array(
+    '#tree' => TRUE,
+  );
+
+  foreach ($resources as $name => $resource) {
+    $rc = $resource['endpoint'];
+    $res_set = array(
+      '#type'        => 'fieldset',
+      '#title'       => t('!name resource', array(
+        '!name' => preg_replace('/[_-]+/', ' ', $name),
+      )),
+      '#collapsible' => TRUE,
+      '#collapsed'   => TRUE,
+      '#tree'        => TRUE,
+      '#attributes'  => array(
+        'class' => array('resource'),
+      ),
+    );
+
+    $res_set['alias'] = array(
+      '#type'          => 'textfield',
+      '#title'         => t('Alias'),
+      '#description'   => t('The alias you enter here will be used instead of the resource name.'),
+      '#size'          => 40,
+      '#maxlength'     => 255,
+      '#default_value' => isset($rc['alias']) ? $rc['alias'] : '',
+    );
+
+    $res_set['operations'] = array(
+      '#tree' => TRUE,
+    );
+    foreach ($ops as $op => $title) {
+      if (isset($resource[$op])) {
+        $res_set['operations'][$op] = array(
+          '#type'        => 'fieldset',
+          '#title'       => $title,
+          '#collapsible' => TRUE,
+          '#collapsed'   => FALSE,
+        );
+        _services_resource_operation_settings($res_set['operations'][$op], $endpoint, $resource, $op);
+      }
+    }
+
+    $classes = array(
+      'actions'          => 'actions',
+      'targeted_actions' => 'targeted actions',
+      'relationships'    => 'relationships',
+    );
+    foreach ($classes as $element => $class) {
+      if (!empty($resource[$class])) {
+        $res_set[$element] = array(
+          '#type'  => 'fieldset',
+          '#title' => t($class),
+          '#tree'  => TRUE,
+        );
+        foreach ($resource[$class] as $action => $definition) {
+          $res_set[$element][$action] = array(
+            '#type'        => 'fieldset',
+            '#title'       => $action,
+            '#collapsible' => TRUE,
+            '#collapsed'   => FALSE,
+          );
+          _services_resource_operation_settings($res_set[$element][$action], $endpoint, $resource, $class, $action);
+        }
+      }
+    }
+
+    drupal_alter('services_resource_settings', $res_set, $resource);
+
+    $res[$name] = $res_set;
+  }
+
+  $form['resources'] = $res;
+
+  $form['save'] = array(
+    '#type'  => 'submit',
+    '#value' => t('Save'),
+  );
+  return $form;
+}
+
+/**
+ * services_edit_form_endpoint_resources_validate function.
+ *
+ * @param array $form
+ * @param array $form_state
+ * @return void
+ */
+function services_edit_form_endpoint_resources_validate($form, $form_state) {
+  $res = $form_state['values']['resources'];
+  // Validate aliases
+  foreach ($res as $name => $resource) {
+    if (!empty($resource['alias'])) {
+      if (!preg_match('/^[a-z-]+$/', $resource['alias'])) {
+        form_set_error("resources][{$name}][alias", t("The alias for the !name may only contain lower case a-z and dashes.", array(
+          '!name' => $form['resources'][$name]['#title'],
+        )));
+      }
+    }
+  }
+}
+
+/**
+ * services_edit_form_endpoint_resources_submit function.
+ *
+ * @param array $form
+ * @param array $form_state
+ * @return void
+ */
+function services_edit_form_endpoint_resources_submit($form, $form_state) {
+  $resources = $form_state['input']['resources'];
+  $endpoint  = $form_state['build_info']['args'][0];
+  foreach ($resources as $name => $resource) {
+    $used = FALSE;
+    $c = isset($endpoint->resources[$name]) ? $endpoint->resources[$name] : array();
+
+    $c['alias'] = $resource['alias'];
+    if (isset($resource['operations'])) {
+      foreach ($resource['operations'] as $op => $def) {
+        $cop = isset($c['operations'][$op]) ? $c['operations'][$op] : array();
+        $cop = array_merge($cop, $def);
+        if ($cop['enabled']) {
+          $c['operations'][$op] = $cop;
+          $used = $used || TRUE;
+        }
+        else {
+          unset($c['operations'][$op]);
+        }
+      }
+    }
+
+    $classes = array(
+      'actions' => 'actions',
+      'targeted_actions' => 'targeted actions',
+      'relationships' => 'relationships',
+    );
+    foreach ($classes as $element => $class) {
+      $class_used = FALSE;
+      if (!empty($resource[$element])) {
+        foreach ($resource[$element] as $act => $def) {
+          $cop = isset($c[$class][$act]) ? $c[$class][$act] : array();
+          $cop = array_merge($cop, $def);
+          if ($cop['enabled']) {
+            $c[$class][$act] = $cop;
+            $class_used = $class_used || TRUE;
+          }
+          else {
+            unset($c[$class][$act]);
+          }
+        }
+        if (!$class_used) {
+          unset($c[$class]);
+        }
+        $used = $class_used || $used;
+      }
+    }
+
+    if ($used) {
+      $endpoint->resources[$name] = $c;
+    }
+    else {
+      unset($endpoint->resources[$name]);
+    }
+  }
+  drupal_set_message(t('Your resources have been saved.'));
+  services_endpoint_save($endpoint);
+}
+
+/**
+ * Returns information about a resource operation given it's class and name.
+ *
+ * @return array
+ *  Information about the operation, or NULL if no matching
+ *  operation was found.
+ */
+function services_get_resource_operation_info($resource, $class, $name = NULL) {
+  $op = NULL;
+
+  if (isset($resource[$class])) {
+    $op = $resource[$class];
+    if (!empty($name)) {
+      $op = isset($op[$name]) ? $op[$name] : NULL;
+    }
+  }
+
+  return $op;
+}
+/**
+ * Constructs the settings form for resource operation.
+ *
+ * @param array $settings
+ *  The root element for the settings form.
+ * @param string $resource
+ *  The resource information array.
+ * @param string $class
+ *  The class of the operation. Can be 'create', 'retrieve', 'update',
+ *  'delete', 'index', 'actions' or 'targeted actions' or 'relationships'.
+ * @param string $name
+ *  Optional. The name parameter is only used for actions, targeted actions
+ *  and relationship.
+ */
+function _services_resource_operation_settings(&$settings, $endpoint, $resource, $class, $name = NULL) {
+  module_load_include('runtime.inc', 'services');
+  if ($rop = services_get_resource_operation_info($resource, $class, $name)) {
+    $settings['enabled'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Enabled'),
+      '#default_value' => !empty($rop['endpoint']) && $rop['endpoint']['enabled'],
+    );
+
+    if (!empty($rop['endpoint']['preprocess'])) {
+      $settings['preprocess'] = array(
+        '#type' => 'item',
+        '#title' => t('Preprocess function'),
+        '#value' => $rop['endpoint']['preprocess'],
+      );
+    }
+
+    if (!empty($rop['endpoint']['postprocess'])) {
+      $settings['preprocess'] = array(
+        '#type' => 'item',
+        '#title' => t('Postprocess function'),
+        '#value' => $rop['endpoint']['Postprocess'],
+      );
+    }
+
+    // Let authentication modules add their configuration options
+    if(isset($endpoint->authentication))
+    foreach ($endpoint->authentication as $auth_module => $auth_settings) {
+      $settings_form = services_auth_invoke($auth_module, 'controller_settings', $auth_settings, $rop, $endpoint, $class, $name);
+      if (!empty($settings_form)) {
+        $settings[$auth_module] = $settings_form;
+      }
+    }
+
+    drupal_alter('services_resource_operation_settings', $settings, $endpoint, $resource, $class, $name);
+  }
+}
diff --git a/plugins/export_ui/services_ctools_export_ui.inc b/plugins/export_ui/services_ctools_export_ui.inc
new file mode 100644
index 0000000..df93ad5
--- /dev/null
+++ b/plugins/export_ui/services_ctools_export_ui.inc
@@ -0,0 +1,204 @@
+<?php
+/*
+ * Plugin definition for Ctools Export UI
+ */
+$plugin = array(
+  'schema' => 'services_endpoint',
+  'menu' => array(
+    'menu item' => 'services',
+    'menu description' => 'Manage Services',
+    // Add services specific own menu callbacks.
+    'items' => array(
+      'resources' => array(
+        'path' => 'list/%ctools_export_ui/resources',
+        'title' => 'Resources',
+        'page callback' => 'ctools_export_ui_switcher_page',
+        'page arguments' => array('services_ctools_export_ui', 'resources', 4),
+        'load arguments' => array('services_ctools_export_ui'),
+        'access arguments' => array('administer services'),
+        'type' => MENU_LOCAL_TASK,
+      ),
+      'authentication' => array(
+        'path' => 'list/%ctools_export_ui/authentication',
+        'title' => 'Authentication',
+        'page callback' => 'ctools_export_ui_switcher_page',
+        'page arguments' => array('services_ctools_export_ui', 'authentication', 4),
+        'load arguments' => array('services_ctools_export_ui'),
+        'access arguments' => array('administer services'),
+        'type' => MENU_LOCAL_TASK,
+      ),
+    ),
+  ),
+  // Add our custom operations.
+  'allowed operations' => array(
+    'resources'  => array('title' => t('Resources')),
+    'authentication' => array('title' => t('Authentication')),
+  ),
+
+  'handler' => array(
+    'class' => 'services_ctools_export_ui',
+    'parent' => 'ctools_export_ui',
+  ),
+
+  'title' => t('Services'),
+
+  'title singular' => t('endpoint'),
+  'title plural' => t('endpoints'),
+  'title singular proper' => t('Endppint'),
+  'title plural proper' => t('Endpoints'),
+);
+
+/**
+ * Form to edit the settings of an endpoint.
+ */
+function services_ctools_export_ui_form(&$form, &$form_state) {
+  // Loading runtime include as needed by services_auth_info().
+  module_load_include('runtime.inc', 'services');
+  $endpoint = $form_state['item'];
+  $form['eid'] = array(
+    '#type'  => 'value',
+    '#value' => isset($endpoint->eid) ? $endpoint->eid : '',
+  );
+
+  $form['endpoint_object'] = array(
+    '#type'  => 'value',
+    '#value' => $endpoint,
+  );
+
+  $form['title'] = array(
+    '#type'          => 'textfield',
+    '#size'          => 24,
+    '#maxlength'     => 255,
+    '#default_value' => $endpoint->title,
+    '#title'         => t('Endpoint title'),
+    '#required'      => TRUE,
+  );
+
+  $servers = services_get_servers();
+  $server_opts = array(
+    '' => t('-- Select a server'),
+  );
+  foreach ($servers as $server => $info) {
+    $server_opts[$server] = $info['name'];
+  }
+  $form['server'] = array(
+    '#type'          => 'select',
+    '#options'       => $server_opts,
+    '#default_value' => $endpoint->server,
+    '#title'         => t('Server'),
+    '#description'   => t('Select a the server that should be used to handle requests to this endpoint.'),
+    '#required'      => TRUE,
+  );
+
+  $form['path'] = array(
+    '#type'          => 'textfield',
+    '#size'          => 24,
+    '#maxlength'     => 255,
+    '#default_value' => $endpoint->path,
+    '#title'         => t('Path to endpoint'),
+    '#required'      => TRUE,
+  );
+  
+  $form['debug'] = array(
+    '#type'          => 'checkbox',
+    '#default_value' => $endpoint->debug,
+    '#title'         => t('Debug mode enabled'),
+    '#description'   => t('Useful for developers. Do not enable on production environments'),
+    '#required'      => FALSE,
+  );
+  $auth_modules = module_implements('services_authentication_info');
+
+  if (!empty($auth_modules)) {
+    $auth_options = array();
+    foreach ($auth_modules as $module) {
+      $info = services_authentication_info($module);
+      $auth_options[$module] = $info['title'];
+    }
+
+    $form['authentication'] = array(
+      '#type'          => 'checkboxes',
+      '#options'       => $auth_options,
+      '#default_value' => array_keys($endpoint->authentication),
+      '#title'         => t('Authentication'),
+      '#description'   => t('Choose which authentication schemes that should ' .
+        'be used with your endpoint. If no authentication method is selected ' .
+        'the standard Drupal session security is used.'),
+    );
+  }
+  else {
+    $form['authentication'] = array(
+      '#type'          => 'item',
+      '#title'         => t('Authentication'),
+      '#description'   => t('No authentication modules are installed, standard ' .
+        'Drupal session based security will be used.'),
+    );
+  }
+
+  return $form;
+
+}
+
+/**
+ * Validate submission of the preset edit form.
+ */
+function services_ctools_export_ui_form_validate(&$form, &$form_state) {
+  // Test uniqueness of name:
+  if (preg_match("/[^A-Za-z0-9_]/", $form_state['values']['name'])) {
+    form_error($form['name'], t('Endpoint name must be alphanumeric or underscores only.'));
+  }
+  else {
+    $query = db_select('services_endpoint', 'e');
+    $query->addField('e', 'eid');
+    $query->condition('name', $form_state['values']['name']);
+
+    if (!empty($form_state['values']['eid']) && is_numeric($form_state['values']['eid'])) {
+      $query->condition('eid', $form_state['values']['eid'], '!=');
+    }
+
+    $res = $query->execute()->fetchField();
+    if (!empty($res)) {
+      form_error($form['name'], t('Endpoint name must be unique.'));
+    }
+  }
+  //TODO: More validation? Eg. validate path? Transliteration etc?
+}
+
+/**
+ * Submit handler for endpoint.
+ */
+function services_ctools_export_ui_form_submit(&$form, &$form_state) {
+  $endpoint = $form_state['values']['endpoint_object'];
+
+  $endpoint->name           = $form_state['values']['name'];
+  $endpoint->title          = $form_state['values']['title'];
+  $endpoint->server         = $form_state['values']['server'];
+  $endpoint->path           = $form_state['values']['path'];
+  $endpoint->debug          = $form_state['values']['debug'];
+
+  // Set the authentication modules, and preserve the settings for modules
+  // that already exist.
+  $auth = array();
+  if (isset($form_state['values']['authentication'])) {
+    foreach (array_keys($form_state['values']['authentication']) as $module) {
+      if (isset($endpoint->authentication[$module])) {
+        $auth[$module] = $endpoint->authentication[$module];
+      }
+      else {
+        $auth[$module] = array();
+      }
+    }
+  }
+  $endpoint->authentication = $auth;
+
+/*
+  if (empty($endpoint->eid)) {
+    drupal_set_message(t('Your new endpoint %title has been saved.', array('%title' => $endpoint->title)));
+    services_endpoint_save($endpoint);
+    $form_state['values']['eid'] = $endpoint->eid;
+  }
+  else {
+    drupal_set_message(t('Your changes have been saved.'));
+    services_endpoint_save($endpoint);
+  }
+*/
+}
\ No newline at end of file
diff --git a/services.admin.inc b/services.admin.inc
index 6c1fca6..073cc9a 100644
--- a/services.admin.inc
+++ b/services.admin.inc
@@ -454,351 +454,4 @@ function services_disable_endpoint($endpoint) {
   cache_clear_all(NULL, 'cache_menu');
   menu_rebuild();
   drupal_goto();
-}
-
-
-/**
- * Configure authentication for a endpoint.
- */
-function services_edit_endpoint_authentication($endpoint) {
-  if (!is_object($endpoint)) {
-    $endpoint = services_endpoint_load($endpoint);
-  }
-  if ($endpoint && !empty($endpoint->title)) {
-    drupal_set_title(check_plain($endpoint->title));
-  }
-  return drupal_get_form('services_edit_form_endpoint_authentication', $endpoint);
-}
-
-/**
- * Endpoint authentication configuration form.
- */
-function services_edit_form_endpoint_authentication($form, &$form_state) {
-  list($endpoint) = $form_state['build_info']['args'];
-  // Loading runtime include as needed by services_authentication_info().
-  module_load_include('runtime.inc', 'services');
-
-  $auth_modules = module_implements('services_authentication_info');
-
-  $form['endpoint_object'] = array(
-    '#type'  => 'value',
-    '#value' => $endpoint,
-  );
-
-  if (empty($auth_modules)) {
-    $form['message'] = array(
-      '#type'          => 'item',
-      '#title'         => t('No installed authentication modules'),
-      '#description'   => t('No authentication modules are installed, standard ' .
-        'Drupal session based security will be used.'),
-    );
-  }
-  elseif (empty($endpoint->authentication)) {
-    $form['message'] = array(
-      '#type'          => 'item',
-      '#title'         => t('No enabled authentication modules'),
-      '#value'   => t('No authentication modules are enabled, standard ' .
-        'Drupal session based security will be used.'),
-    );
-  }
-  else {
-    // Add configuration fieldsets for the authentication modules
-    foreach ($endpoint->authentication as $module => $settings) {
-      $info = services_authentication_info($module);
-      if ($info) {
-        $form[$module] = array(
-          '#type' => 'fieldset',
-          '#title' => $info['title'],
-          '#tree' => TRUE,
-        ) + services_auth_invoke($module, 'security_settings', $settings);
-      }
-    }
-  }
-
-  $form['submit'] = array(
-    '#type'  => 'submit',
-    '#value' => 'Save',
-  );
-
-  return $form;
-}
-
-function services_edit_form_endpoint_authentication_submit($form, $form_state) {
-  $endpoint = $form_state['values']['endpoint_object'];
-
-  foreach (array_keys($endpoint->authentication) as $module) {
-    $endpoint->authentication[$module] = $form_state['values'][$module];
-  }
-
-  drupal_set_message(t('Your authentication options have been saved.'));
-  services_endpoint_save($endpoint);
-}
-
-/**
- * Add resources to an endpoint.
- */
-function services_edit_endpoint_resources($endpoint) {
-  if (!is_object($endpoint)) {
-    $endpoint = services_endpoint_load($endpoint);
-  }
-  if ($endpoint && !empty($endpoint->title)) {
-    drupal_set_title(check_plain($endpoint->title));
-  }
-  return drupal_get_form('services_edit_form_endpoint_resources', $endpoint);
-}
-
-/**
- * Form to add resources to an endpoint.
- */
-function services_edit_form_endpoint_resources($form, &$form_state) {
-  list($endpoint) = $form_state['build_info']['args'];
-  module_load_include('resource_build.inc', 'services');
-
-  $form['endpoint_object'] = array(
-    '#type'  => 'value',
-    '#value' => $endpoint,
-  );
-
-  $ops = array(
-    'create'   => t('Create'),
-    'retrieve' => t('Retrieve'),
-    'update'   => t('Update'),
-    'delete'   => t('Delete'),
-    'index'    => t('Index'),
-  );
-
-  // Call _services_build_resources() directly instead of
-  // services_get_resources to bypass caching.
-  $resources = _services_build_resources();
-  // Apply the endpoint in a non-strict mode, so that the non-active resources
-  // are preserved.
-  _services_apply_endpoint($resources, $endpoint, FALSE);
-
-  $res = array(
-    '#tree' => TRUE,
-  );
-
-  foreach ($resources as $name => $resource) {
-    $rc = $resource['endpoint'];
-    $res_set = array(
-      '#type'        => 'fieldset',
-      '#title'       => t('!name resource', array(
-        '!name' => preg_replace('/[_-]+/', ' ', $name),
-      )),
-      '#collapsible' => TRUE,
-      '#collapsed'   => TRUE,
-      '#tree'        => TRUE,
-      '#attributes'  => array(
-        'class' => array('resource'),
-      ),
-    );
-
-    $res_set['alias'] = array(
-      '#type'          => 'textfield',
-      '#title'         => t('Alias'),
-      '#description'   => t('The alias you enter here will be used instead of the resource name.'),
-      '#size'          => 40,
-      '#maxlength'     => 255,
-      '#default_value' => isset($rc['alias']) ? $rc['alias'] : '',
-    );
-
-    $res_set['operations'] = array(
-      '#tree' => TRUE,
-    );
-    foreach ($ops as $op => $title) {
-      if (isset($resource[$op])) {
-        $res_set['operations'][$op] = array(
-          '#type'        => 'fieldset',
-          '#title'       => $title,
-          '#collapsible' => TRUE,
-          '#collapsed'   => FALSE,
-        );
-        _services_resource_operation_settings($res_set['operations'][$op], $endpoint, $resource, $op);
-      }
-    }
-
-    $classes = array(
-      'actions'          => 'actions',
-      'targeted_actions' => 'targeted actions',
-      'relationships'    => 'relationships',
-    );
-    foreach ($classes as $element => $class) {
-      if (!empty($resource[$class])) {
-        $res_set[$element] = array(
-          '#type'  => 'fieldset',
-          '#title' => t($class),
-          '#tree'  => TRUE,
-        );
-        foreach ($resource[$class] as $action => $definition) {
-          $res_set[$element][$action] = array(
-            '#type'        => 'fieldset',
-            '#title'       => $action,
-            '#collapsible' => TRUE,
-            '#collapsed'   => FALSE,
-          );
-          _services_resource_operation_settings($res_set[$element][$action], $endpoint, $resource, $class, $action);
-        }
-      }
-    }
-
-    drupal_alter('services_resource_settings', $res_set, $resource);
-
-    $res[$name] = $res_set;
-  }
-
-  $form['resources'] = $res;
-
-  $form['save'] = array(
-    '#type'  => 'submit',
-    '#value' => t('Save'),
-  );
-
-  return $form;
-}
-
-function services_edit_form_endpoint_resources_validate($form, $form_state) {
-  $res = $form_state['values']['resources'];
-
-  // Validate aliases
-  foreach ($res as $name => $resource) {
-    if (!empty($resource['alias'])) {
-      if (!preg_match('/^[a-z-]+$/', $resource['alias'])) {
-        form_set_error("resources][{$name}][alias", t("The alias for the !name may only contain lower case a-z and dashes.", array(
-          '!name' => $form['resources'][$name]['#title'],
-        )));
-      }
-    }
-  }
-}
-
-function services_edit_form_endpoint_resources_submit($form, $form_state) {
-  $resources = $form_state['values']['resources'];
-  $endpoint  = $form_state['values']['endpoint_object'];
-
-  foreach ($resources as $name => $resource) {
-    $used = FALSE;
-    $c = isset($endpoint->resources[$name]) ? $endpoint->resources[$name] : array();
-
-    $c['alias'] = $resource['alias'];
-    if (isset($resource['operations'])) {
-      foreach ($resource['operations'] as $op => $def) {
-        $cop = isset($c['operations'][$op]) ? $c['operations'][$op] : array();
-        $cop = array_merge($cop, $def);
-        if ($cop['enabled']) {
-          $c['operations'][$op] = $cop;
-          $used = $used || TRUE;
-        }
-        else {
-          unset($c['operations'][$op]);
-        }
-      }
-    }
-
-    $classes = array(
-      'actions' => 'actions',
-      'targeted_actions' => 'targeted actions',
-      'relationships' => 'relationships',
-    );
-    foreach ($classes as $element => $class) {
-      $class_used = FALSE;
-      if (!empty($resource[$element])) {
-        foreach ($resource[$element] as $act => $def) {
-          $cop = isset($c[$class][$act]) ? $c[$class][$act] : array();
-          $cop = array_merge($cop, $def);
-          if ($cop['enabled']) {
-            $c[$class][$act] = $cop;
-            $class_used = $class_used || TRUE;
-          }
-          else {
-            unset($c[$class][$act]);
-          }
-        }
-        if (!$class_used) {
-          unset($c[$class]);
-        }
-        $used = $class_used || $used;
-      }
-    }
-
-    if ($used) {
-      $endpoint->resources[$name] = $c;
-    }
-    else {
-      unset($endpoint->resources[$name]);
-    }
-  }
-
-  drupal_set_message(t('Your resources have been saved.'));
-  services_endpoint_save($endpoint);
-}
-
-/**
- * Returns information about a resource operation given it's class and name.
- *
- * @return array
- *  Information about the operation, or NULL if no matching
- *  operation was found.
- */
-function services_get_resource_operation_info($resource, $class, $name = NULL) {
-  $op = NULL;
-
-  if (isset($resource[$class])) {
-    $op = $resource[$class];
-    if (!empty($name)) {
-      $op = isset($op[$name]) ? $op[$name] : NULL;
-    }
-  }
-
-  return $op;
-}
-
-/**
- * Constructs the settings form for resource operation.
- *
- * @param array $settings
- *  The root element for the settings form.
- * @param string $resource
- *  The resource information array.
- * @param string $class
- *  The class of the operation. Can be 'create', 'retrieve', 'update',
- *  'delete', 'index', 'actions' or 'targeted actions' or 'relationships'.
- * @param string $name
- *  Optional. The name parameter is only used for actions, targeted actions
- *  and relationship.
- */
-function _services_resource_operation_settings(&$settings, $endpoint, $resource, $class, $name = NULL) {
-  module_load_include('runtime.inc', 'services');
-  if ($rop = services_get_resource_operation_info($resource, $class, $name)) {
-    $settings['enabled'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Enabled'),
-      '#default_value' => !empty($rop['endpoint']) && $rop['endpoint']['enabled'],
-    );
-
-    if (!empty($rop['endpoint']['preprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Preprocess function'),
-        '#value' => $rop['endpoint']['preprocess'],
-      );
-    }
-
-    if (!empty($rop['endpoint']['postprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Postprocess function'),
-        '#value' => $rop['endpoint']['Postprocess'],
-      );
-    }
-
-    // Let authentication modules add their configuration options
-    foreach ($endpoint->authentication as $auth_module => $auth_settings) {
-      $settings_form = services_auth_invoke($auth_module, 'controller_settings', $auth_settings, $rop, $endpoint, $class, $name);
-      if (!empty($settings_form)) {
-        $settings[$auth_module] = $settings_form;
-      }
-    }
-
-    drupal_alter('services_resource_operation_settings', $settings, $endpoint, $resource, $class, $name);
-  }
-}
+}
\ No newline at end of file
diff --git a/services.install b/services.install
index 9798802..c311fe1 100644
--- a/services.install
+++ b/services.install
@@ -75,6 +75,7 @@ function services_schema() {
     'export' => array(
       'key' => 'name',
       'identifier' => 'endpoint',
+      'primary key' => 'name',
       'api' => array(
         'owner' => 'services',
         'api' => 'services',
@@ -121,3 +122,26 @@ function services_update_7301() {
 function services_update_7302() {
   db_drop_field('services_endpoint', 'status');
 }
+
+/**
+ * Implements hook_requirements().
+ *
+ * Check version of ctools.
+ */
+function services_requirements($phase) {
+  $t = get_t();
+  $requirements = array();
+  $ctools_info = db_query('SELECT info FROM {system} WHERE name = :ctools', array(':ctools' => 'ctools'))->fetchField();
+  $ctools_info = unserialize($ctools_info);
+  // Check whether version of ctools is minimum alpha3
+  // as we need patch applied from http://drupal.org/node/933946
+  if ($ctools_info['datestamp'] < 1300395967) {
+    $requirements['ctools'] = array(
+      'title' => 'CTools',
+      'value' => $ctools_info['version'],
+      'severity' => REQUIREMENT_ERROR,
+      'description' => $t('Please update CTools module to latest stable version (minimum 7.x-1.0-alpha3).'),
+    );
+  }
+  return $requirements;
+}
diff --git a/services.module b/services.module
index f0355e1..858661a 100644
--- a/services.module
+++ b/services.module
@@ -6,6 +6,11 @@
  */
 
 /**
+ * Minimum CTools version needed.
+ */
+define('SERVICES_REQUIRED_CTOOLS_API', '1.7');
+
+/**
  * Implements hook_help().
  */
 function services_help($path, $arg) {
@@ -15,7 +20,7 @@ function services_help($path, $arg) {
     case 'admin/help#services':
       $output = '<p>' . t('Visit the <a href="@handbook_url">Services Handbook</a> for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) . '</p>';
       break;
-    case 'admin/config/services/services':
+    case 'admin/structure/services':
       $output = '<p>' . t('Services are collections of methods available to remote applications. They are defined in modules, and may be accessed in a number of ways through server modules. Visit the <a href="@handbook_url">Services Handbook</a> for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) . '</p>';
       $output .= '<p>' . t('All enabled services and methods are shown. Click on any method to view information or test.') . '</p>';
       $output .= '<div id="services-help-legend"><div class="services-endpoint-enabled">Enabled</div><div class="services-endpoint-debug">Enabled With Debug</div><div class="services-endpoint-disabled">Disabled</div></div>';
@@ -71,88 +76,11 @@ function services_hook_info() {
 
 /**
  * Implements hook_menu().
+ *
+ * Services UI is defined in the export-ui plugin.
  */
 function services_menu() {
-  $base = array(
-    'access arguments' => array('administer services'),
-    'file'             => 'services.admin.inc',
-  );
-
-  $items['admin/config/services/services'] = array(
-    'title'          => 'Services',
-    'description'    => 'Manage how external applications communicates with Drupal.',
-    'page callback'  => 'services_list_endpoint',
-  ) + $base;
-
-  $items['admin/config/services/services/list'] = array(
-    'title'          => 'List',
-    'page callback'  => 'services_list_endpoint',
-    'type'           => MENU_DEFAULT_LOCAL_TASK,
-    'weight'         => -10,
-  ) + $base;
-  $items['admin/config/services/services/add'] = array(
-    'title'          => 'Add endpoint',
-    'page callback'  => 'services_add_endpoint',
-    'type'           => MENU_LOCAL_TASK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/edit'] = array(
-    'title'          => 'Edit endpoint',
-    'page callback'  => 'services_edit_endpoint',
-    'page arguments' => array(4),
-    'type'           => MENU_LOCAL_TASK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/authentication'] = array(
-    'title'          => 'Authentication',
-    'page callback'  => 'services_edit_endpoint_authentication',
-    'page arguments' => array(4),
-    'type'           => MENU_LOCAL_TASK,
-    'weight'         => 5,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/resources'] = array(
-    'title'          => 'Resources',
-    'page callback'  => 'services_edit_endpoint_resources',
-    'page arguments' => array(4),
-    'type'           => MENU_LOCAL_TASK,
-    'weight'         => 10,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/export'] = array(
-    'title'          => 'Export endpoint',
-    'page callback'  => 'drupal_get_form',
-    'page arguments' => array('services_export_endpoint', 4),
-    'type'           => MENU_LOCAL_TASK,
-    'weight'         => 20,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/delete'] = array(
-    'title'          => 'Delete endpoint',
-    'page callback'  => 'drupal_get_form',
-    'page arguments' => array('services_delete_confirm_endpoint', 4),
-    'type'           => MENU_CALLBACK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/disabledebug'] = array(
-     'page callback'  => 'services_disable_debug_mode',
-     'page arguments' => array(4),
-     'type'           => MENU_CALLBACK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/enabledebug'] = array(
-     'page callback'  => 'services_enable_debug_mode',
-     'page arguments' => array(4),
-     'type'           => MENU_CALLBACK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/disable'] = array(
-    'page callback'  => 'services_disable_endpoint',
-    'page arguments' => array(4),
-    'type'           => MENU_CALLBACK,
-  ) + $base;
-  $items['admin/config/services/services/%services_endpoint/enable'] = array(
-    'page callback'  => 'services_enable_endpoint',
-    'page arguments' => array(4),
-    'type'           => MENU_CALLBACK,
-  ) + $base;
-  $items['admin/config/services/services/ahah/security-options'] = array(
-    'page callback' => '_services_ahah_security_options',
-    'type'          => MENU_CALLBACK,
-  ) + $base;
-
+  $items = array();
   $endpoints = services_endpoint_load_all();
   foreach ($endpoints as $endpoint) {
     if (empty($endpoint->disabled)) {
@@ -167,6 +95,27 @@ function services_menu() {
   }
   return $items;
 }
+/**
+ * Implements of hook_ctools_plugin_api().
+ */
+function services_ctools_plugin_api($module, $api) {
+  if ($module == 'services' && $api == 'plugins') {
+    return array('version' => 3);
+  }
+}
+
+/**
+ * Implement of hook_ctools_plugin_directory().
+ */
+function services_ctools_plugin_directory($module, $type) {
+  // Safety: go away if CTools is not at an appropriate version.
+  if (!module_invoke('ctools', 'api_version', SERVICES_REQUIRED_CTOOLS_API)) {
+    return;
+  }
+  if ($type =='export_ui') {
+    return 'plugins/export_ui';
+  }
+}
 
 /**
  * Access callback that always returns TRUE.
@@ -313,8 +262,10 @@ function services_endpoint_load_all() {
  * @return void
  */
 function services_endpoint_save($endpoint) {
-  $update = (isset($endpoint->eid)) ? array('eid') : array();
-  drupal_write_record('services_endpoint', $endpoint, $update);
+  if(is_array($endpoint) && array_key_exists('build_info', $endpoint)) {
+    $endpoint = $endpoint['build_info']['args'][0];
+  }
+  ctools_export_crud_save('services_endpoint', $endpoint);
   menu_rebuild();
   cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE);
 }
@@ -325,8 +276,7 @@ function services_endpoint_save($endpoint) {
  * @return void
  */
 function services_endpoint_delete($endpoint) {
-  $eid = $endpoint->eid;
-  db_delete('services_endpoint')->condition('eid', $eid)->execute();
+  ctools_export_crud_delete('services_endpoint', $endpoint);
   menu_rebuild();
   cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE);
 }
@@ -342,19 +292,6 @@ function services_endpoint_export($endpoint, $indent = '') {
   return $output;
 }
 
-/**
- * Lists all available endpoints.
- *
- * @return array
- */
-function services_endpoint_list() {
-  $return = array();
-  $endpoints = services_endpoint_load_all();
-  foreach ($endpoints as $endpoint) {
-    $return[$endpoint->name] = $endpoint->title;
-  }
-  return $return;
-}
 
 /**
  * Gets all resource definitions.
diff --git a/services.resource_build.inc b/services.resource_build.inc
index 7ea6f5c..5032202 100644
--- a/services.resource_build.inc
+++ b/services.resource_build.inc
@@ -91,6 +91,9 @@ function _services_build_resources($endpoint_name = '') {
  * @return void
  */
 function _services_apply_endpoint(&$resources, $endpoint, $strict = TRUE) {
+  if(is_array($endpoint) && array_key_exists('build_info', $endpoint)) {
+    $endpoint = $endpoint['build_info']['args'][0];
+  }
   foreach ($resources as $name => &$resource) {
     $cres = ($endpoint && isset($endpoint->resources[$name])) ? $endpoint->resources[$name] : array();
     $resource['endpoint'] = $cres;
diff --git a/tests/functional/ServicesEndpointTests.test b/tests/functional/ServicesEndpointTests.test
index 613b067..24b8b88 100644
--- a/tests/functional/ServicesEndpointTests.test
+++ b/tests/functional/ServicesEndpointTests.test
@@ -20,9 +20,10 @@ class ServicesEndpointTests extends ServicesWebTestCase {
 // Create and log in our privileged user.
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
-    $this->drupalPost('admin/config/services/services/add', $edit, t('Save and proceed'));
+    $this->drupalPost('admin/structure/services/add', $edit, t('Save'));
     $this->drupalGet($edit['path']);
   }
 
@@ -34,16 +35,16 @@ class ServicesEndpointTests extends ServicesWebTestCase {
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
       'administer content types',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
-    $this->drupalPost('admin/config/services/services/add', $edit, t('Save and proceed'));
+    $this->drupalPost('admin/structure/services/add', $edit, t('Save'));
     $this->assertResponse('200', t('expected 200'));
-    $this->drupalGet('admin/config/services/services/list');
+    $this->drupalGet('admin/structure/services');
     $this->assertResponse('200', t('expected 200'));
 
-    $this->assertText($edit['title'], t('Endpoint path appears'));
-    $this->assertText('rest_server', t('Server is rest server'));
-    $this->assertText('In database', t('Storage is in database'));
+    $this->assertText($edit['name'], t('Endpoint path appears'));
+    $this->assertText('Normal', t('Storage is in database'));
   }
 
   /**
@@ -55,9 +56,10 @@ class ServicesEndpointTests extends ServicesWebTestCase {
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
       'administer content types',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
-    $this->drupalPost('admin/config/services/services/add', $edit, t('Save and proceed'));
+    $this->drupalPost('admin/structure/services/add', $edit, t('Save'));
     $this->assertResponse('200', 'expected 200');
 
     $this->assertText('Path to endpoint field is required.',
@@ -77,9 +79,10 @@ class ServicesEndpointTests extends ServicesWebTestCase {
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
       'administer content types',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
-    $this->drupalPost('admin/config/services/services/add', $edit, t('Save and proceed'));
+    $this->drupalPost('admin/structure/services/add', $edit, t('Save'));
     $this->assertResponse('200', 'expected 200');
 
     $this->assertText('Endpoint title field is required.',
@@ -99,10 +102,10 @@ class ServicesEndpointTests extends ServicesWebTestCase {
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
       'administer content types',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
-    $this->drupalPost('admin/config/services/services/add', $edit,
-      t('Save and proceed'));
+    $this->drupalPost('admin/structure/services/add', $edit, t('Save'));
     $this->assertResponse('200', 'expected 200');
 
     $this->assertText('Server field is required.',
diff --git a/tests/functional/ServicesResourceFileTests.test b/tests/functional/ServicesResourceFileTests.test
index 7a899d7..4fed21e 100644
--- a/tests/functional/ServicesResourceFileTests.test
+++ b/tests/functional/ServicesResourceFileTests.test
@@ -32,6 +32,8 @@ class ServicesResourceFileTests extends ServicesWebTestCase {
     $this->privileged_user = $this->drupalCreateUser(array(
       'get own binary files',
       'save file information',
+      'administer services',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privileged_user);
     // Get a test file.
diff --git a/tests/unit/TestServicesModule.test b/tests/unit/TestServicesModule.test
index dfae5dd..ccf1a9c 100644
--- a/tests/unit/TestServicesModule.test
+++ b/tests/unit/TestServicesModule.test
@@ -37,6 +37,7 @@ class ServicesModuleTests extends DrupalWebTestCase {
     // Create and logi in our privileged user.
     $this->privilegedUser = $this->drupalCreateUser(array(
       'administer services',
+      'administer site configuration',
     ));
     $this->drupalLogin($this->privilegedUser);
   }
@@ -50,7 +51,7 @@ class ServicesModuleTests extends DrupalWebTestCase {
     $message = t('There should be a help message returned from services help.') ;
     $this->assertNotNull($result, $message) ;
 
-    $result = services_help('admin/config/services/services', array()) ;
+    $result = services_help('admin/structure/services', array()) ;
     $message = t('There should be help text when going to the services endpoint administration page.') ;
     $this->assertNotNull($result, $message) ;
   }
@@ -130,50 +131,6 @@ class ServicesModuleTests extends DrupalWebTestCase {
   } // function
 
   /**
-   * Test hook_menu implementation.
-   */
-  public function testHookMenu() {
-    // see how many with no endpoints
-    $items = services_menu() ;
-
-    $message = t('There should be 13 menu items') ;
-    $this->assertEqual(count($items), 13, $message) ;
-
-    $menuItem = 'admin/config/services/services' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/list' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/add' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/edit' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/authentication' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menutItem = 'admin/config/services/services/%services_endpoint/resources' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/export' ;
-    $this->helpMenuItem($menutItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/delete' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/disable' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/%services_endpoint/enable' ;
-    $this->helpMenuItem($menuItem, $items) ;
-
-    $menuItem = 'admin/config/services/services/ahah/security-options' ;
-    $this->helpMenuItem($menuItem, $items) ;
-  } // function
-
-  /**
    * Test thta adding a menu endpoint creates an menu path for that item.
    */
   public function testEndpointMenu() {
@@ -185,26 +142,26 @@ class ServicesModuleTests extends DrupalWebTestCase {
        'server' => 'rest_server',
     );
 
-    $this->drupalPost('admin/config/services/services/add', $endpointSettings,
-      t('Save and proceed'));
-    $this->assertResponse('200', t('Failed to create endpoint.'));
-
+    $this->drupalPost('admin/structure/services/add', $endpointSettings, t('Save'));
+    cache_clear_all(NULL, 'cache_menu');
+    menu_rebuild();
+    $this->assertResponse('200', t('Attempting to create Endpoint.'));
     // Check path.
     $this->drupalGet($endpointSettings['path']);
     $this->assertResponse('200', t('Failed to access endpoint menu path.'));
 
     // Check edit.
-    $this->drupalGet('admin/config/services/services/' . $endpointSettings['name']
+    $this->drupalGet('admin/structure/services/list/' . $endpointSettings['name']
      . '/edit');
     $this->assertResponse('200', t('Failed to access endpoint edit path.')) ;
 
     // Check export.
-    $this->drupalGet('admin/config/services/services/' . $endpointSettings['name']
+    $this->drupalGet('admin/structure/services/list/' . $endpointSettings['name']
      . '/export');
     $this->assertResponse('200', t('Failed to access endpoint export path.')) ;
 
     // Check delete.
-    $this->drupalGet('admin/config/services/services/' . $endpointSettings['name']
+    $this->drupalGet('admin/structure/services/list/' . $endpointSettings['name']
      . '/delete');
     $this->assertResponse('200', t('Failed to access endpoint delete path.')) ;
   }
-- 
1.7.3.4

