From 06a67ec883bc254b4e51015089ede61e4f25724c Mon Sep 17 00:00:00 2001
From: solotandem <jim@boombatower.com>
Date: Thu, 22 Jan 2015 19:30:50 +0000
Subject: [PATCH] Issue #2412497: Make webform and its views integration more
 portable.

---
 includes/webform.components.inc                    |   84 ++++++++-
 views/webform.views.inc                            |  190 ++++++++++++++++++--
 views/webform_handler_field_submission_data.inc    |   27 ++-
 views/webform_handler_filter_submission_data.inc   |    6 +
 ...ebform_handler_relationship_submission_data.inc |   29 ++-
 webform.install                                    |   27 +++
 webform.module                                     |   35 +++-
 7 files changed, 381 insertions(+), 17 deletions(-)

diff --git a/includes/webform.components.inc b/includes/webform.components.inc
index 0900f65..f68fc49 100644
--- a/includes/webform.components.inc
+++ b/includes/webform.components.inc
@@ -734,6 +734,78 @@ function webform_component_delete_form_submit($form, &$form_state) {
 }
 
 /**
+ * Returns the component rows from the database that match the search criteria.
+ *
+ * @param string $column1
+ *   Column name to use with $value1.
+ * @param string $column2
+ *   Column name to use with $value2.
+ * @param array $value1
+ *   List of $column1 values of the components to load.
+ * @param array $value2
+ *   (optional) The value to match against $column2.
+ *
+ * @return array
+ *   An associative array of components keyed by webform name and component key.
+ */
+function webform_component_load($column1, $column2, $value1, $value2 = '') {
+  $query = db_select('webform_component', 'wc');
+  $query->join('webform', 'w', 'w.nid = wc.nid');
+  $query->addField('w', 'name', 'webform_name');
+  $query
+    ->fields('wc')
+    ->condition("wc.$column1", $value1, 'IN')
+    ->orderBy('webform_name')
+    ->orderBy('form_key');
+
+  if ($value2) {
+    $query->condition("w.$column2", $value2);
+  }
+
+  $rows = $query
+    ->execute()
+    ->fetchAll(PDO::FETCH_ASSOC);
+
+  $components = array();
+  foreach ($rows as $row) {
+    $key = $row['webform_name'] . ':' . $row['form_key'];
+    $components[$key] = $row;
+  }
+
+  return $components;
+}
+
+/**
+ * Returns component rows from the database using the machine names.
+ *
+ * @param array $machine_names
+ *   List of machine names (or 'form_key' in schema) of the components to load.
+ * @param array $webform_name
+ *   (optional) The webform name, a unique machine_name in the webform table.
+ *
+ * @return array
+ *   An associative array of components keyed by webform name and component key.
+ */
+function webform_component_load_by_names($machine_names, $webform_name = '') {
+  return webform_component_load('form_key', 'name', $machine_names, $webform_name);
+}
+
+/**
+ * Returns component rows from the database using the nid and cid.
+ *
+ * @param array $cids
+ *   List of component IDs of the components to load.
+ * @param array $nid
+ *   (optional) The webform node ID.
+ *
+ * @return array
+ *   An associative array of components keyed by webform name and component key.
+ */
+function webform_component_load_by_ids($cids, $nid = '') {
+  return webform_component_load('cid', 'nid', $cids, $nid);
+}
+
+/**
  * Insert a new component into the database.
  *
  * @param $component
@@ -973,8 +1045,13 @@ function webform_component_property($type, $property) {
  * @param $pagebreak_groups
  *   Determine if pagebreaks should be converted to option groups in the
  *   returned list of options.
+ * @param $cid_key
+ *   (optional) TRUE to use cid as key in returned array; FALSE to use form_key.
+ *
+ * @return array
+ *   An associative list of components keyed by page number and component key.
  */
-function webform_component_list($node, $component_filter = NULL, $prepend_group = TRUE, $pagebreak_groups = FALSE) {
+function webform_component_list($node, $component_filter = NULL, $prepend_group = TRUE, $pagebreak_groups = FALSE, $cid_key = TRUE) {
   $options = array();
   $page_names = array();
   $parent_names = array();
@@ -1000,12 +1077,13 @@ function webform_component_list($node, $component_filter = NULL, $prepend_group
     }
     // If this component should be included in the options, add it with any prefix, in a page group, as needed.
     if (!isset($feature) || webform_component_feature($component['type'], $feature) || $prepend_group === TRUE) {
+      $key = $cid_key ? $cid : $component['form_key'];
       $prefix = ($prepend_group && $component['pid']) ? $parent_names[$component['pid']] : '';
       if ($pagebreak_groups && $page_num > 1) {
-        $options[$page_names[$page_num]][$cid] = $prefix . $component['name'];
+        $options[$page_names[$page_num]][$key] = $prefix . $component['name'];
       }
       else {
-        $options[$cid] = $prefix . $component['name'];
+        $options[$key] = $prefix . $component['name'];
       }
     }
   }
diff --git a/views/webform.views.inc b/views/webform.views.inc
index 3b84c8d..51eecbc 100644
--- a/views/webform.views.inc
+++ b/views/webform.views.inc
@@ -584,13 +584,51 @@ function webform_views_autocomplete($string = '') {
 
 /**
  * Shared form for the Webform submission data field and relationship handler.
+ *
+ * @param integer $form_state
+ *   Includes handler options in 'options' key.
+ * @param integer $nid
+ *   (deprecated) The webform node ID.
+ * @param string $cid
+ *   (deprecated) The webform component ID.
  */
 function _webform_views_options_form(&$form, &$form_state, $nid, $cid) {
   form_load_include($form_state, 'inc', 'webform', 'includes/webform.components');
+
+  $options = $form_state['options'];
+  $nid = $options['webform_nid'];
+  $cid = $options['webform_cid'];
+  $key = $options['webform_key'];
+
   $node = $nid ? node_load($nid) : NULL;
 
-  $form['webform_nid'] = array(
+  $form['webform_name'] = array(
     '#type' => 'textfield',
+    '#title' => t('Webform name'),
+    '#default_value' => isset($node) ? $node->title . ' [nid:' . $node->nid . ']' : '',
+    '#ajax' => array(
+      'path' => views_ui_build_form_url($form_state),
+      'event' => 'blur',
+    ),
+    '#autocomplete_path' => 'webform/autocomplete',
+    '#description' => t('Enter the title or NID [or machine name] of the webform whose values should be made available.'),
+    '#submit' => array('views_ui_config_item_form_submit_temporary'),
+    '#executes_submit_callback' => TRUE,
+  );
+
+  $options = webform_component_list($node, NULL, 'path', TRUE, FALSE);
+  $form['webform_key'] = array(
+    '#title' => t('Component data'),
+    '#type' => 'select',
+    '#options' => $options,
+    '#default_value' => $key,
+    '#access' => count($options), // @todo Is this property necessary?
+    '#description' => t('Select the component whose values should be made available.'),
+  );
+
+  // Keep these for backward compatability but change to fixed values.
+  $form['webform_nid'] = array(
+    '#type' => 'value', // 'textfield',
     '#title' => t('Webform node'),
     '#default_value' => isset($node) ? $node->title . ' [nid:' . $node->nid . ']' : '',
     '#ajax' => array(
@@ -608,15 +646,18 @@ function _webform_views_options_form(&$form, &$form_state, $nid, $cid) {
     $components = $node->webform['components'];
   }
 
+/*
+  // @todo This is unused; remove?
   $type_options = array();
   foreach (webform_components() as $key => $component) {
     $type_options[$key] = check_plain($component['label']);
   }
+*/
 
   $options = webform_component_list($node, NULL, 'path', TRUE);
   $form['webform_cid'] = array(
     '#title' => t('Component data'),
-    '#type' => 'select',
+    '#type' => 'value', // 'select',
     '#options' => $options,
     '#default_value' => $cid,
     '#access' => count($components),
@@ -628,23 +669,150 @@ function _webform_views_options_form(&$form, &$form_state, $nid, $cid) {
  * Validate handler for webform_views_options_form().
  */
 function _webform_views_options_validate(&$form, &$form_state) {
-  // Just store the checked components of the selected type.
-  if (empty($form_state['values']['options']['webform_nid'])) {
-    form_error($form['webform_nid'], t('Webform NID is required.'));
+  if (empty($form_state['values']['options']['webform_name'])) {
+    form_error($form['webform_name'], t('Webform name is required.'));
   }
   else {
-    $nid = preg_replace('/^.*?nid:([0-9]+).*?$/', '$1', $form_state['values']['options']['webform_nid']);
-    if (!($nid && ($node = node_load($nid)) && !empty($node->webform['components']))) {
-      form_error($form['webform_nid'], t('The specified node is not valid.'));
+    $nid = preg_replace('/^.*?nid:([0-9]+).*?$/', '$1', $form_state['values']['options']['webform_name']);
+    if (!$nid) {
+      form_error($form['webform_name'], t('The specified node ID is not valid.'));
+    }
+    elseif (!($node = node_load($nid))) {
+      form_error($form['webform_name'], t('The specified node is not a valid webform.'));
+    }
+    elseif (empty($node->webform['components'])) {
+      form_error($form['webform_name'], t('The webform contains no components. Add components to webform, save it, then retry this.'));
+    }
+    else {
+      // Set this for reuse in submit handler.
+      $form_state['node'] = $node;
     }
   }
 }
 
 /**
  * Submit handler for webform_views_options_form().
+ *
+ * For portability, the properties saved with the view are the webform name and
+ * the component key (called 'form_key' in schema). These handlers 'prefer' the
+ * 'form_key' being unique like other machine name columns. In the interim, the
+ * cid value may help filter the duplicate component keys.
+ *
+ * For backward compatability, include the nid and cid; drop them in a future
+ * release.
  */
 function _webform_views_options_submit(&$form, &$form_state) {
-  // Save the NID as just the number instead of the title.
-  $nid = preg_replace('/^.*?nid:([0-9]+).*?$/', '$1', $form_state['values']['options']['webform_nid']);
-  $form_state['values']['options']['webform_nid'] = $nid;
+  $node = $form_state['node'];
+  $form_state['values']['options']['webform_name'] = $node->webform['name'];
+  // Store nid and cid for backward compatability.
+  $form_state['values']['options']['webform_nid'] = $node->nid;
+  foreach ($node->webform['components'] as $component) {
+    if ($component['form_key'] == $form_state['values']['options']['webform_key']) {
+      $form_state['values']['options']['webform_cid'] = $component['cid'];
+    }
+  }
+}
+
+/**
+ * Sets the component keys that represent the best fit to the inputs.
+ *
+ * The inputs in $options may include:
+ * - webform_name
+ * - webform_key (the component name or 'form_key')
+ * - webform_nid
+ * - webform_cid (a component cid on the webform)
+ *
+ * The first two are preferred for portability reasons and are inspected first.
+ * The latter two are used as a fallback or during the interim when a view has
+ * not been updated to include the first two keys and/or the component keys are
+ * not unique.
+ *
+ * @param array $options
+ *   The options for the field or filter handler.
+ */
+function _webform_views_component_keys(&$options) {
+  if (!_webform_views_component_from_machine_names($options)) {
+    _webform_views_component_from_ids($options);
+  }
+}
+
+/**
+ * Sets the component keys based on the machine name inputs.
+ *
+ * @param array $options
+ *   The options for the field or filter handler.
+ */
+function _webform_views_component_from_machine_names(&$options) {
+  $name = $options['webform_name'];
+  $key = $options['webform_key'];
+  if (empty($name) || empty($key)) {
+    // Seems reasonable that either both are or neither is set.
+    return FALSE;
+  }
+
+  $nid = $options['webform_nid'] ? $options['webform_nid']: 0;
+  $cid = $options['webform_cid'] ? $options['webform_cid']: 0;
+
+  // Load component by machine name values.
+  // The component key should be unique although may not be in current webform.
+  module_load_include('inc', 'webform', 'includes/webform.components');
+  $components = webform_component_load_by_names(array($key), $name);
+  if (count($components) > 1) {
+    // The input 'form_key' is NOT unique on this webform.
+    // For backward compatability, try to match on nid and cid.
+    foreach ($components as $combo_key => $component) {
+      if ($component['nid'] == $nid && $component['cid'] == $cid) {
+        // @todo If we add an hook_update_N to rename the component keys, then
+        // this will not be necessary; it will not occur.
+
+        // This handler was set using old code.
+        // Require both nid and cid; maybe only the cid
+        // The $component ID values will always be positive; the $nid and $cid
+        // values will equal zero if not set.
+        // @todo Will the zero values exist in the future if we have backward compatability?
+        return TRUE;
+      }
+    }
+    // None of the existing components retrieved using the machine name values
+    // has nid and cid values matching the corresponding inputs. 'Unset' the nid
+    // and cid values as they can not be set or relied upon with confidence.
+    $options['webform_nid'] = 0;
+    $options['webform_cid'] = 0;
+    return FALSE;
+  }
+
+  $component = array_shift($components);
+  if (!empty($component)) {
+    // The input 'form_key' is unique on this webform.
+    $options['webform_nid'] = $component['nid'];
+    $options['webform_cid'] = $component['cid'];
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Sets the component keys based on the integer ID inputs.
+ *
+ * @param array $options
+ *   The options for the field or filter handler.
+ */
+function _webform_views_component_from_ids(&$options) {
+  $nid = $options['webform_nid'] ? $options['webform_nid']: 0;
+  $cid = $options['webform_cid'] ? $options['webform_cid']: 0;
+  if (empty($nid) || empty($cid)) {
+    // Seems reasonable that either both are or neither is set.
+    return FALSE;
+  }
+
+  // Load component (using cid and nid).
+  module_load_include('inc', 'webform', 'includes/webform.components');
+  $components = webform_component_load_by_ids(array($cid), $nid);
+  $component = array_shift($components);
+  if (!empty($component)) {
+    $options['webform_name'] = $component['webform_name'];
+    $options['webform_key'] = $component['form_key'];
+    return TRUE;
+  }
+  return FALSE;
 }
diff --git a/views/webform_handler_field_submission_data.inc b/views/webform_handler_field_submission_data.inc
index 8bc1dc0..8cd0f70 100644
--- a/views/webform_handler_field_submission_data.inc
+++ b/views/webform_handler_field_submission_data.inc
@@ -23,6 +23,9 @@ class webform_handler_field_submission_data extends views_handler_field {
     $options = parent::option_definition();
     $options['format'] = array('default' => 'html');
     $options['custom_label'] = array('default' => 'default');
+    $options['webform_name'] = array('default' => NULL);
+    $options['webform_key'] = array('default' => NULL);
+    // Retain the next two for backward compatability.
     $options['webform_nid'] = array('default' => NULL);
     $options['webform_cid'] = array('default' => NULL);
     $options['webform_datatype'] = array('default' => 'string');
@@ -43,10 +46,16 @@ class webform_handler_field_submission_data extends views_handler_field {
     $form['label']['#dependency'] = array('radio:options[custom_label]' => array('custom'));
 
     if (!$this->webform_expand) {
+      // Convert machine names to integer IDs.
+      $this->component_keys();
+
       $nid = (int) $this->options['webform_nid'];
       $cid = (int) $this->options['webform_cid'];
 
-      // Helper function provides webform_nid and webform_cid options.
+      // @todo Rename this key to 'handler_options'???
+      $form_state['options'] = $this->options;
+
+      // Helper function provides webform_name and webform_key options.
       _webform_views_options_form($form, $form_state, $nid, $cid);
     }
 
@@ -126,6 +135,8 @@ class webform_handler_field_submission_data extends views_handler_field {
    * Load the node and submissions needed for this components values.
    */
   function pre_render(&$values) {
+    // Convert machine names to integer IDs.
+    $this->component_keys();
     $nid = $this->options['webform_nid'];
     $this->webform_node = node_load($nid);
     // Load all the submissions needed for this page. This is stored at the
@@ -148,6 +159,8 @@ class webform_handler_field_submission_data extends views_handler_field {
    * Get this field's label based on the selected component.
    */
   function label() {
+    // Convert machine names to integer IDs.
+    $this->component_keys();
     if ($this->options['custom_label'] === 'default' && isset($this->options['webform_cid'])) {
       if (isset($this->webform_node)) {
         $node = $this->webform_node;
@@ -213,4 +226,16 @@ class webform_handler_field_submission_data extends views_handler_field {
       return render($render);
     }
   }
+
+  /**
+   * Returns the node and component IDs corresponding to the inputs.
+   */
+  function component_keys() {
+    // If machine names exist, convert them to integer IDs.
+    module_load_include('inc', 'webform', 'views/webform.views');
+    _webform_views_component_keys($this->options);
+    $nid = $this->options['webform_nid'];
+    $cid = $this->options['webform_cid'];
+    return array($nid, $cid);
+  }
 }
diff --git a/views/webform_handler_filter_submission_data.inc b/views/webform_handler_filter_submission_data.inc
index b591d79..73232a1 100644
--- a/views/webform_handler_filter_submission_data.inc
+++ b/views/webform_handler_filter_submission_data.inc
@@ -44,6 +44,12 @@ class webform_handler_filter_submission_data extends views_handler_filter_string
 
     // Adjust the exposed filter options based on the component selected.
     if ($which === 'title') {
+      // @todo $this->view->relationship does not appear to be defined when this
+      // routine is called. Only if operator is exposed.
+      if (isset($relationship) && is_object($relationship)) {
+        // Ensure the nid and cid options are set when this runs.
+        $relationship->component_keys();
+      }
       $nid = $this->view->relationship[$this->options['relationship']]->options['webform_nid'];
       $cid = $this->view->relationship[$this->options['relationship']]->options['webform_cid'];
 
diff --git a/views/webform_handler_relationship_submission_data.inc b/views/webform_handler_relationship_submission_data.inc
index eb85cc5..7e15bd2 100644
--- a/views/webform_handler_relationship_submission_data.inc
+++ b/views/webform_handler_relationship_submission_data.inc
@@ -2,12 +2,20 @@
 /**
  * @file
  * Views' relationship handlers.
+ *
+ * @todo
+ * What is the use case for this handler? Why does it care about a specific
+ * component? Should it not simply be a relationship to the table. The specific
+ * component and its value seems more relevant to the filter handler.
  */
 
 class webform_handler_relationship_submission_data extends views_handler_relationship  {
 
   function option_definition() {
     $options = parent::option_definition();
+    $options['webform_name'] = array('default' => NULL);
+    $options['webform_key'] = array('default' => NULL);
+    // Retain the next two for backward compatability.
     $options['webform_nid'] = array('default' => NULL);
     $options['webform_cid'] = array('default' => NULL);
     return $options;
@@ -17,10 +25,16 @@ class webform_handler_relationship_submission_data extends views_handler_relatio
     parent::options_form($form, $form_state);
     form_load_include($form_state, 'inc', 'webform', 'views/webform.views');
 
+    // Convert machine names to integer IDs.
+    list($nid, $cid) = $this->component_keys();
+
     $nid = (int) $this->options['webform_nid'];
     $cid = (int) $this->options['webform_cid'];
 
-    // Helper function provides webform_nid and webform_cid options.
+    // @todo Rename this key to 'handler_options'???
+    $form_state['options'] = $this->options;
+
+    // Helper function provides webform_name and webform_key options.
     _webform_views_options_form($form, $form_state, $nid, $cid);
   }
 
@@ -40,6 +54,7 @@ class webform_handler_relationship_submission_data extends views_handler_relatio
    * It respects the given component ids, provided via options form.
    */
   function query() {
+    $this->component_keys();
     $this->definition['extra'][] = array(
       'table' => NULL,
       'field' => "%alias.nid",
@@ -54,4 +69,16 @@ class webform_handler_relationship_submission_data extends views_handler_relatio
     // The rest of building the join is performed by the parent.
     parent::query();
   }
+
+  /**
+   * Returns the node and component IDs corresponding to the inputs.
+   */
+  function component_keys() {
+    // If machine names exist, convert them to integer IDs.
+    module_load_include('inc', 'webform', 'views/webform.views');
+    _webform_views_component_keys($this->options);
+    $nid = $this->options['webform_nid'];
+    $cid = $this->options['webform_cid'];
+    return array($nid, $cid);
+  }
 }
diff --git a/webform.install b/webform.install
index 9838fa4..f108045 100644
--- a/webform.install
+++ b/webform.install
@@ -20,6 +20,12 @@ function webform_schema() {
         'unsigned' => TRUE,
         'not null' => TRUE,
       ),
+      'name' => array(
+        'description' => 'The unique name of the webform.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
       'next_serial' => array(
         'description' => 'The serial number to give to the next submission to this webform.',
         'type' => 'int',
@@ -1908,3 +1914,24 @@ function webform_update_7420() {
   user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('edit webform components'));
 }
 
+/**
+ * Add machine name column to webform table and initialize it as 'webform_[nid]'.
+ */
+function webform_update_7421() {
+  // Add field to database.
+  $tables = webform_schema();
+  $table = 'webform';
+  $field = 'name';
+  $spec = $tables[$table]['fields'][$field];
+  // Set initial value when creating NOT NULL column with no default value in
+  // existing table.
+  $spec['initial'] = '';
+  if (!db_field_exists($table, $field)) {
+    db_add_field($table, $field, $spec);
+  }
+  // Initialize column with unique value based on nid.
+  db_update('webform')
+    ->expression($field, "CONCAT('webform_', nid)")
+    ->condition($field, '')
+    ->execute();
+}
diff --git a/webform.module b/webform.module
index 07c0ff3..65ca088 100644
--- a/webform.module
+++ b/webform.module
@@ -1385,6 +1385,7 @@ function webform_node_insert($node) {
   // Ensure values for all defaults are provided. Useful for importing from
   // older versions into newer ones.
   $node->webform += webform_node_defaults();
+  $node->webform['name'] = $node->webform_name;
 
   // Do not make an entry if this node does not have any Webform settings.
   if ($node->webform == webform_node_defaults() && !in_array($node->type, webform_variable_get('webform_node_types_primary'))) {
@@ -1582,6 +1583,7 @@ function webform_node_delete($node) {
 function webform_node_defaults() {
   $progress_bar_defaults = webform_variable_get('webform_progressbar_style');
   $defaults = array(
+    'name' => '',
     'confirmation' => '',
     'confirmation_format' => NULL,
     'redirect_url' => '<confirmation>',
@@ -1753,20 +1755,51 @@ function webform_form_alter(&$form, $form_state, $form_id) {
   $matches = array();
   if (isset($form['#node']->type) && $form_id == $form['#node']->type . '_node_form' && variable_get('webform_node_' . $form['#node']->type, FALSE)) {
     $node = $form['#node'];
+    // Change text of title element.
+    $form['title']['#title'] = t('Webform name');
     // Preserve all Webform options currently set on the node.
     $form['webform'] = array(
       '#type' => 'value',
       '#value' => $node->webform,
     );
 
-    // If a new node, redirect the user to the components form after save.
     if (empty($node->nid) && in_array($node->type, webform_variable_get('webform_node_types_primary'))) {
+      // This is a new node; redirect to the components form after save.
       $form['actions']['submit']['#submit'][] = 'webform_form_submit';
+
+      // Add machine name element.
+      $form['webform_name'] = array(
+        '#type' => 'machine_name',
+        '#title' => t('Machine name'),
+        '#description' => t('A unique machine-readable name containing letters, numbers, and underscores.'),
+        '#maxlength' => 32,
+        '#machine_name' => array(
+          'source' => array('title'),
+          'exists' => '_webform_name_exists',
+        ),
+      );
     }
   }
 }
 
 /**
+ * AJAX callback: Checks if a webform machine name is taken.
+ *
+ * @param $value
+ *   The webform machine name.
+ *
+ * @return
+ *   Whether or not the webform machine name is taken.
+ */
+function _webform_name_exists($value) {
+  $names = db_select('webform', 'w')
+    ->fields('w', array('name'))
+    ->execute()
+    ->fetchAllKeyed(0,0);
+  return in_array($value, $names);
+}
+
+/**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
 function webform_form_node_type_form_alter(&$form, $form_state) {
-- 
1.7.10.4

