Browse Source

updated webform localization and phone, uuid, term_merge, spambot, performance

Bachir Soussi Chiadmi 7 years ago
parent
commit
0521608bb7
57 changed files with 3543 additions and 1604 deletions
  1. 74 0
      sites/all/modules/contrib/admin/uuid/.travis.yml
  2. 1 0
      sites/all/modules/contrib/admin/uuid/README.txt
  3. 13 4
      sites/all/modules/contrib/admin/uuid/plugins/arguments/entity_uuid.inc
  4. 2 0
      sites/all/modules/contrib/admin/uuid/uuid.admin.inc
  5. 11 31
      sites/all/modules/contrib/admin/uuid/uuid.api.php
  6. 91 97
      sites/all/modules/contrib/admin/uuid/uuid.core.inc
  7. 2 2
      sites/all/modules/contrib/admin/uuid/uuid.drush.inc
  8. 70 31
      sites/all/modules/contrib/admin/uuid/uuid.entity.inc
  9. 15 4
      sites/all/modules/contrib/admin/uuid/uuid.features.inc
  10. 106 35
      sites/all/modules/contrib/admin/uuid/uuid.inc
  11. 3 3
      sites/all/modules/contrib/admin/uuid/uuid.info
  12. 44 25
      sites/all/modules/contrib/admin/uuid/uuid.install
  13. 10 7
      sites/all/modules/contrib/admin/uuid/uuid.module
  14. 171 75
      sites/all/modules/contrib/admin/uuid/uuid.test
  15. 2 1
      sites/all/modules/contrib/admin/uuid/uuid.views.inc
  16. 3 3
      sites/all/modules/contrib/admin/uuid/uuid_path/uuid_path.info
  17. 7 8
      sites/all/modules/contrib/admin/uuid/uuid_path/uuid_path.module
  18. 0 20
      sites/all/modules/contrib/admin/uuid/uuid_services/resources/field_collection.resource.inc
  19. 8 0
      sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.admin.inc
  20. 3 3
      sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.info
  21. 22 25
      sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.module
  22. 1 0
      sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.features.inc
  23. 3 3
      sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.info
  24. 2 1
      sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.module
  25. 1 0
      sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.services.inc
  26. 3 3
      sites/all/modules/contrib/dev/performance/performance.info
  27. 2 2
      sites/all/modules/contrib/dev/performance/performance.module
  28. 59 0
      sites/all/modules/contrib/form/webform_localization/components/grid.inc
  29. 102 0
      sites/all/modules/contrib/form/webform_localization/components/select.inc
  30. 48 42
      sites/all/modules/contrib/form/webform_localization/includes/webform_localization.component.sync.inc
  31. 90 126
      sites/all/modules/contrib/form/webform_localization/includes/webform_localization.i18n.inc
  32. 22 19
      sites/all/modules/contrib/form/webform_localization/includes/webform_localization.sync.inc
  33. 88 17
      sites/all/modules/contrib/form/webform_localization/tests/webform_localization.test
  34. 72 0
      sites/all/modules/contrib/form/webform_localization/webform_localization.api.php
  35. 13 6
      sites/all/modules/contrib/form/webform_localization/webform_localization.info
  36. 5 6
      sites/all/modules/contrib/form/webform_localization/webform_localization.install
  37. 619 121
      sites/all/modules/contrib/form/webform_localization/webform_localization.module
  38. 3 0
      sites/all/modules/contrib/form/webform_phone/webform_phone.components.inc
  39. 3 3
      sites/all/modules/contrib/form/webform_phone/webform_phone.info
  40. 1 1
      sites/all/modules/contrib/form/webform_phone/webform_phone.module
  41. 40 24
      sites/all/modules/contrib/taxonomy/term_merge/README.txt
  42. 9 0
      sites/all/modules/contrib/taxonomy/term_merge/help/term_merge.help.ini
  43. 20 0
      sites/all/modules/contrib/taxonomy/term_merge/help/term_merge.html
  44. 16 0
      sites/all/modules/contrib/taxonomy/term_merge/help/term_merge_duplicate_suggestion.html
  45. 19 0
      sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/description.inc
  46. 28 0
      sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/name.inc
  47. 19 0
      sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/parent.inc
  48. 4 3
      sites/all/modules/contrib/taxonomy/term_merge/term_merge.info
  49. 126 41
      sites/all/modules/contrib/taxonomy/term_merge/term_merge.module
  50. 209 188
      sites/all/modules/contrib/taxonomy/term_merge/term_merge.pages.inc
  51. 279 172
      sites/all/modules/contrib/taxonomy/term_merge/term_merge.test
  52. 60 0
      sites/all/modules/contrib/users/spambot/README.txt
  53. 159 56
      sites/all/modules/contrib/users/spambot/spambot.admin.inc
  54. 6 4
      sites/all/modules/contrib/users/spambot/spambot.info
  55. 52 50
      sites/all/modules/contrib/users/spambot/spambot.install
  56. 390 200
      sites/all/modules/contrib/users/spambot/spambot.module
  57. 312 142
      sites/all/modules/contrib/users/spambot/spambot.pages.inc

+ 74 - 0
sites/all/modules/contrib/admin/uuid/.travis.yml

@@ -0,0 +1,74 @@
+language: php
+sudo: false
+
+php:
+  - 5.5
+  - 5.6
+  - 7.0
+  - hhvm
+
+matrix:
+  fast_finish: true
+  allow_failures:
+    - php: hhvm
+
+mysql:
+  database: drupal
+  username: root
+  encoding: utf8
+
+install:
+  # add composer's global bin directory to the path
+  # see: https://github.com/drush-ops/drush#install---composer
+  - export PATH="$HOME/.composer/vendor/bin:$PATH"
+
+  # install drush globally
+  - composer global require drush/drush:7.*
+
+  # Install PHP_CodeSniffer and Drupal config
+  - composer global require drupal/coder
+  - phpcs --config-set installed_paths ~/.composer/vendor/drupal/coder/coder_sniffer
+
+  # Create the database
+  - mysql -e 'create database drupal;'
+
+before_script:
+
+  # Disable sendmail
+  - echo sendmail_path=`which true` >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
+
+  # Increase the MySQL connection timeout on the PHP end.
+  - echo "mysql.connect_timeout=3000" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
+  - echo "default_socket_timeout=3000" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
+
+  # Increase the MySQL server timetout and packet size.
+  - mysql -e "SET GLOBAL wait_timeout = 36000;"
+  - mysql -e "SET GLOBAL max_allowed_packet = 33554432;"
+
+  # Download Drupal 7.x
+  - drush pm-download drupal-7 --destination=/tmp --drupal-project-rename=drupal
+
+  # Add our module
+  - ln -s $(pwd) /tmp/drupal/sites/all/modules/
+
+  # Switch to the drupal site directory
+  - cd /tmp/drupal
+
+  # Install site and dependencies
+  - drush -vv --yes site-install --db-url=mysql://root:@127.0.0.1/drupal
+  - drush pm-download entity
+  - drush pm-enable simpletest entity uuid --yes
+
+  # Use the PHP builtin webserver to serve the site.
+  #- drush runserver 8080 > /dev/null 2>&1 &
+  - drush runserver 127.0.0.1:8888 > ~/server.log 2>&1 &
+  - export counter=0; until nc -zv localhost 8888; do if [ $counter -ge 12 ]; then echo "Failed to start server."; exit 1; fi; echo "Waiting for web server to start on port 8888..."; sleep 5; counter=$[$counter +1]; done
+
+script:
+  - cd /tmp/drupal
+  - php ./scripts/run-tests.sh --verbose --color --php "$(which php)" --url http://127.0.0.1:8888 UUID | tee ~/tests.log
+  - phpcs --standard=Drupal --extensions=php,module,inc,install,test,profile,theme sites/all/modules/uuid
+
+after_failure:
+  # See what happened with the server.
+  - cat ~/server.log

+ 1 - 0
sites/all/modules/contrib/admin/uuid/README.txt

@@ -11,6 +11,7 @@ FEATURES
  * Automatic UUID generation:
    UUIDs will be generated for all core entities. An API is provided for other
    modules to enable support for custom entities.
+   See https://www.drupal.org/node/2387671
  * UUID API for entities, properties and fields:
    With this unified API you can load entities with entity_uuid_load() so that
    all supported properties and fields are made with UUID references. You can

+ 13 - 4
sites/all/modules/contrib/admin/uuid/plugins/arguments/entity_uuid.inc

@@ -6,8 +6,7 @@
  */
 
 /**
- * Plugins are described by creating a $plugin array which will be used
- * by the system that includes this file.
+ * CTools UUID entity context plugin definition.
  */
 $plugin = array(
   'title' => t("Entity: UUID"),
@@ -18,7 +17,12 @@ $plugin = array(
 );
 
 /**
- * @todo document me properly
+ * Fetches the "child" information for a given parent entity.
+ *
+ * @todo document me properly.
+ *
+ * @return array
+ *   The children.
  */
 function uuid_entity_uuid_get_child($plugin, $parent, $child) {
   $plugins = uuid_entity_uuid_get_children($plugin, $parent);
@@ -26,7 +30,12 @@ function uuid_entity_uuid_get_child($plugin, $parent, $child) {
 }
 
 /**
- * @todo document me properly
+ * Fetches all children types for a given parent entity.
+ *
+ * @todo document me properly.
+ *
+ * @return array
+ *   All the children.
  */
 function uuid_entity_uuid_get_children($original_plugin, $parent) {
   $entities = entity_get_info();

+ 2 - 0
sites/all/modules/contrib/admin/uuid/uuid.admin.inc

@@ -42,7 +42,9 @@ function uuid_devel_load_by_uuid($entity_type, $entity) {
     // Get the keys for local ID and UUID.
     $uuid_key = $info['entity keys']['uuid'];
     $uuid_entities = entity_uuid_load($entity_type, array($entity->{$uuid_key}));
+    // @codingStandardsIgnoreStart
     return kdevel_print_object(reset($uuid_entities), '$' . $entity_type . '->');
+    // @codingStandardsIgnoreEnd
   }
   else {
     return t("This entity doesn't support UUID.");

+ 11 - 31
sites/all/modules/contrib/admin/uuid/uuid.api.php

@@ -5,26 +5,6 @@
  * Hooks provided by the UUID module.
  */
 
-/**
- * Defines one or more UUID generators exposed by a module.
- *
- * @return
- *   An associative array with the key being the machine name for the
- *   implementation and the values being an array with the following keys:
- *     - title: The human readable name for the generator.
- *     - callback: The function to be called for generating the UUID.
- *
- * @see uuid_get_info()
- */
-function hook_uuid_info() {
-  $generators = array();
-  $generators['my_module'] = array(
-    'title' => t('My module UUID generator'),
-    'callback' => 'my_module_generate_uuid',
-  );
-  return $generators;
-}
-
 /**
  * Ensures all records have a UUID assigned to them.
  *
@@ -38,39 +18,35 @@ function hook_uuid_sync() {
 }
 
 /**
- * Let modules transform their properties with local IDs to UUIDs when an
- * entity is loaded.
+ * Transform entity properties from local IDs to UUIDs when they are loaded.
  */
 function hook_entity_uuid_load(&$entities, $entity_type) {
 
 }
 
 /**
- * Let modules transform their fields with local IDs to UUIDs when an entity
- * is loaded.
+ * Transform field values from local IDs to UUIDs when an entity is loaded.
  */
 function hook_field_uuid_load($entity_type, $entity, $field, $instance, $langcode, &$items) {
 
 }
 
 /**
- * Let modules transform their properties with UUIDs to local IDs when an
- * entity is saved.
+ * Transform entity properties from UUIDs to local IDs before entity is saved.
  */
 function hook_entity_uuid_presave(&$entity, $entity_type) {
 
 }
 
 /**
- * Let modules transform their fields with UUIDs to local IDs when an entity
- * is saved.
+ * Transform field values from UUIDs to local IDs before an entity is saved.
  */
 function hook_field_uuid_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
 
 }
 
 /**
- * Let modules transform their properties when an entity is saved.
+ * Transform entity properties after an entity is saved.
  */
 function hook_entity_uuid_save($entity, $entity_type) {
 
@@ -78,20 +54,24 @@ function hook_entity_uuid_save($entity, $entity_type) {
 
 /**
  * Let modules act when an entity is deleted.
+ *
+ * Generally hook_entity_delete() should be used instead of this hook.
+ *
+ * @see hook_entity_delete()
  */
 function hook_entity_uuid_delete($entity, $entity_type) {
 
 }
 
 /**
- * Let modules modify paths when they are being converted to UUID ones.
+ * Modifies paths when they are being converted to UUID ones.
  */
 function hook_uuid_menu_path_to_uri_alter($path, &$uri) {
 
 }
 
 /**
- * Let modules modify paths when they are being converted from UUID ones.
+ * Modifies paths when they are being converted from UUID ones.
  */
 function hook_uuid_menu_uri_to_path(&$path, $uri) {
 

+ 91 - 97
sites/all/modules/contrib/admin/uuid/uuid.core.inc

@@ -32,6 +32,31 @@ function node_entity_uuid_presave(&$entity, $entity_type) {
   if ($entity_type == 'node') {
     entity_property_uuid_to_id($entity, 'user', array('uid', 'revision_uid'));
     entity_property_uuid_to_id($entity, 'node', 'tnid');
+
+    // A node always must have an author.
+    if (empty($entity->uid)) {
+      global $user;
+      $entity->uid = $user->uid;
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_uuid_save().
+ */
+function node_entity_uuid_save(&$entity, $entity_type) {
+  /*
+   * When a node is translated, the source node's tnid is set to it's own nid.
+   * When deploying the node for the first time the tnid can't be translated
+   * to an nid until after the node has been saved. So if the entity's tnid
+   * is still a uuid at this point it needs to be translated to an nid.
+   */
+  if ($entity_type == 'node' && uuid_is_valid($entity->tnid)) {
+    entity_property_uuid_to_id($entity, 'node', 'tnid');
+    db_update('node')
+      ->fields(array('tnid' => $entity->tnid))
+      ->condition('nid', $entity->nid)
+      ->execute();
   }
 }
 
@@ -64,17 +89,33 @@ function book_entity_uuid_presave(&$entity, $entity_type) {
  * Implements hook_entity_uuid_presave().
  */
 function user_entity_uuid_presave(&$entity, $entity_type) {
-  if ($entity_type == 'user') {
-    if (!empty($entity->picture)) {
-      $uuids = entity_get_id_by_uuid('file', array($entity->picture['uuid']));
-      $fid = current($uuids);
-      if (!$entity->is_new) {
-        $entity->picture = file_load($fid);
-      }
-      else {
-        $entity->picture = $fid;
-      }
-    }
+  if ($entity_type != 'user') {
+    return;
+  }
+
+  /*
+   * We need to ensure new user's passwords are encrypted. The Services module
+   * transparently encrypts the password for new users. md5() is used by
+   * users who's accounts were migrated from Drupal 6 and who haven't updated
+   * their password.
+   */
+  if (isset($entity->pass)
+    && (!('$S$D' == substr($entity->pass, 0, 4)) || preg_match('/^[a-f0-9]{32}$/', $entity->pass))) {
+    // Ensure user's password is hashed.
+    $entity->pass = user_hash_password($entity->pass);
+  }
+
+  if (empty($entity->picture)) {
+    return;
+  }
+
+  $uuids = entity_get_id_by_uuid('file', array($entity->picture['uuid']));
+  $fid = current($uuids);
+  if (!$entity->is_new) {
+    $entity->picture = file_load($fid);
+  }
+  else {
+    $entity->picture = $fid;
   }
 }
 
@@ -104,6 +145,10 @@ function comment_entity_uuid_presave(&$entity, $entity_type) {
       break;
 
     case 'comment':
+      // entity_make_entity_local() may have unset cid, add back if necessary.
+      if (!isset($entity->cid)) {
+        $entity->cid = NULL;
+      }
       entity_property_uuid_to_id($entity, 'user', array('uid', 'u_uid'));
       entity_property_uuid_to_id($entity, 'node', 'nid');
       break;
@@ -124,10 +169,14 @@ function file_entity_uuid_load(&$entities, $entity_type) {
  */
 function file_entity_uuid_presave(&$entity, $entity_type) {
   if ($entity_type == 'file') {
+    // entity_make_entity_local() may have unset fid, add back if necessary.
+    if (!isset($entity->fid)) {
+      $entity->fid = NULL;
+    }
     entity_property_uuid_to_id($entity, 'user', 'uid');
 
     // Write the new file to the local filesystem.
-    if (isset($entity->file_contents)) {
+    if (isset($entity->file_contents) && !empty($entity->filesize)) {
       // Don't try to write it if it uses a stream wrapper that isn't writeable
       // (for example, if it is a remotely-hosted video).
       $scheme = file_uri_scheme($entity->uri);
@@ -169,8 +218,9 @@ function file_entity_uuid_presave(&$entity, $entity_type) {
       }
 
       $directory = drupal_dirname($uri);
-      file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
-      $entity->uri = file_unmanaged_save_data(base64_decode($entity->file_contents), $uri, $replace);
+      if (!empty($directory) && file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
+        $entity->uri = file_unmanaged_save_data(base64_decode($entity->file_contents), $uri, $replace);
+      }
     }
   }
 }
@@ -214,7 +264,7 @@ function taxonomy_entity_uuid_presave(&$entity, $entity_type) {
  * Implements hook_entity_uuid_load().
  */
 function field_entity_uuid_load(&$entities, $entity_type) {
-  foreach ($entities as $i => $entity) {
+  foreach ($entities as $entity) {
     list(, , $bundle_name) = entity_extract_ids($entity_type, $entity);
     $instances = field_info_instances($entity_type, $bundle_name);
 
@@ -311,34 +361,8 @@ function image_field_uuid_presave($entity_type, $entity, $field, $instance, $lan
 
 /**
  * Implements hook_field_uuid_load().
- */
-function node_reference_field_uuid_load($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_id_to_uuid($items, 'node', 'nid');
-}
-
-/**
- * Implements hook_field_uuid_presave().
- */
-function node_reference_field_uuid_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_uuid_to_id($items, 'node', 'nid');
-}
-
-/**
- * Implements hook_field_uuid_load().
- */
-function user_reference_field_uuid_load($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_id_to_uuid($items, 'user', 'uid');
-}
-
-/**
- * Implements hook_field_uuid_presave().
- */
-function user_reference_field_uuid_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_uuid_to_id($items, 'user', 'uid');
-}
-
-/**
- * Implements hook_field_uuid_load().
+ *
+ * Kept here because it is in D8 core.
  */
 function entityreference_field_uuid_load($entity_type, $entity, $field, $instance, $langcode, &$items) {
   // TODO: This is not really good, but as of now 'entity_property_id_to_uuid()'
@@ -348,6 +372,8 @@ function entityreference_field_uuid_load($entity_type, $entity, $field, $instanc
 
 /**
  * Implements hook_field_uuid_presave().
+ *
+ * Kept here because it is in D8 core.
  */
 function entityreference_field_uuid_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
   // TODO: This is not really good, but as of now 'entity_property_id_to_uuid()'
@@ -355,38 +381,6 @@ function entityreference_field_uuid_presave($entity_type, $entity, $field, $inst
   entity_property_uuid_to_id($items, $field['settings']['target_type'], 'target_id');
 }
 
-/**
- * Implements hook_entity_uuid_load().
- */
-function field_collection_entity_uuid_load(&$entities, $entity_type) {
-  if ($entity_type == 'field_collection_item') {
-    entity_property_id_to_uuid($entities, 'field_collection_item', 'value');
-  }
-}
-
-/**
- * Implements hook_entity_uuid_presave().
- */
-function field_collection_entity_uuid_presave(&$entity, $entity_type) {
-  if ($entity_type == 'field_collection_item') {
-    entity_property_uuid_to_id($entity, 'field_collection_item', 'value');
-  }
-}
-
-/**
- * Implements hook_field_uuid_load().
- */
-function field_collection_field_uuid_load($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_id_to_uuid($items, 'field_collection_item', 'value');
-}
-
-/**
- * Implements hook_field_uuid_presave().
- */
-function field_collection_field_uuid_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
-  entity_property_uuid_to_id($items, 'field_collection_item', 'value');
-}
-
 /**
  * @} End of "Field implementations"
  */
@@ -400,24 +394,35 @@ function field_collection_field_uuid_presave($entity_type, $entity, $field, $ins
  * Implements hook_uuid_entities_features_export_entity_alter().
  */
 function node_uuid_entities_features_export_entity_alter(&$entity, $entity_type) {
-  if ($entity_type == 'node') {
-    foreach (array('data', 'name', 'picture', 'revision_uid', 'last_comment_timestamp') as $property) {
-      if (property_exists($entity, $property)) {
-        unset($entity->{$property});
-      }
+  if ('node' != $entity_type) {
+    return;
+  }
+
+  $properties = array(
+    'data',
+    'name',
+    'picture',
+    'revision_uid',
+    'last_comment_timestamp',
+  );
+  foreach ($properties as $property) {
+    if (property_exists($entity, $property)) {
+      unset($entity->{$property});
     }
   }
 }
 
 /**
- * Implementations hook_uuid_entities_features_export_entity_alter().
+ * Implements hook_uuid_entities_features_export_entity_alter().
  */
 function user_uuid_entities_features_export_entity_alter(&$entity, $entity_type) {
-  if ($entity_type == 'user') {
-    foreach (array('data', 'access', 'login') as $property) {
-      if (property_exists($entity, $property)) {
-        unset($entity->{$property});
-      }
+  if ('user' != $entity_type) {
+    return;
+  }
+
+  foreach (array('data', 'access', 'login') as $property) {
+    if (property_exists($entity, $property)) {
+      unset($entity->{$property});
     }
   }
 }
@@ -433,17 +438,6 @@ function file_uuid_entities_features_export_field_alter($entity_type, $entity, $
   }
 }
 
-/**
- * Implements hook_uuid_entities_features_export_entity_alter().
- */
-function workbench_uuid_entities_features_export_entity_alter(&$entity, $entity_type) {
-  foreach (array('workbench_moderation', 'my_revision', 'workbench_access', 'workbench_access_scheme', 'workbench_access_by_role') as $property) {
-    if (isset($entity->{$property})) {
-      unset($entity->{$property});
-    }
-  }
-}
-
 /**
  * @} End of "Export alterations"
  */

+ 2 - 2
sites/all/modules/contrib/admin/uuid/uuid.drush.inc

@@ -6,7 +6,7 @@
  */
 
 /**
- * Implementats hook_drush_command().
+ * Implements hook_drush_command().
  */
 function uuid_drush_command() {
   $items = array();
@@ -18,7 +18,7 @@ function uuid_drush_command() {
 }
 
 /**
- * Implementats of hook_drush_help().
+ * Implements hook_drush_help().
  */
 function uuid_drush_help($section) {
   switch ($section) {

+ 70 - 31
sites/all/modules/contrib/admin/uuid/uuid.entity.inc

@@ -11,8 +11,7 @@
 class UuidEntityException extends Exception {}
 
 /**
- * Helper function that returns entity info for all supported core modules,
- * relevant for UUID functionality.
+ * Returns entity info for all supported core entities.
  *
  * @see uuid_entity_info()
  * @see uuid_schema_alter()
@@ -98,7 +97,7 @@ function uuid_entity_info_alter(&$info) {
  */
 function uuid_entity_property_info_alter(&$info) {
   foreach (entity_get_info() as $entity_type => $entity_info) {
-    if (isset($entity_info['uuid']) && $entity_info['uuid'] == TRUE 
+    if (isset($entity_info['uuid']) && $entity_info['uuid'] == TRUE
       && !empty($entity_info['entity keys']['uuid'])
       && empty($info[$entity_type]['properties'][$entity_info['entity keys']['uuid']])) {
       $info[$entity_type]['properties'][$entity_info['entity keys']['uuid']] = array(
@@ -107,7 +106,7 @@ function uuid_entity_property_info_alter(&$info) {
         'description' => t('The universally unique ID.'),
         'schema field' => $entity_info['entity keys']['uuid'],
       );
-      if (!empty($entity_info['entity keys']['revision uuid']) 
+      if (!empty($entity_info['entity keys']['revision uuid'])
         && empty($info[$entity_type]['properties'][$entity_info['entity keys']['revision uuid']])) {
         $info[$entity_type]['properties'][$entity_info['entity keys']['revision uuid']] = array(
           'label' => t('Revision UUID'),
@@ -258,7 +257,7 @@ function entity_uuid_save($entity_type, $entity) {
   entity_make_entity_local($entity_type, $entity);
 
   // Save the entity.
-  entity_save($entity_type, $entity);
+  $result = entity_save($entity_type, $entity);
 
   $hook = 'entity_uuid_save';
   foreach (module_implements($hook) as $module) {
@@ -267,6 +266,7 @@ function entity_uuid_save($entity_type, $entity) {
       $function($entity, $entity_type);
     }
   }
+  return $result;
 }
 
 /**
@@ -340,7 +340,7 @@ function entity_make_entity_local($entity_type, $entity) {
     }
   }
   else {
-    throw new UuidEntityException(t('Trying to operate on a @type entity, which doesn\'t support UUIDs.', array('@type' => $info['label'])));
+    throw new UuidEntityException(t("Trying to operate on a @type entity, which doesn\'t support UUIDs.", array('@type' => $info['label'])));
   }
 }
 
@@ -381,7 +381,7 @@ function entity_uuid_delete($entity_type, $uuid) {
     return entity_delete($entity_type, $id);
   }
   else {
-    throw new UuidEntityException(t('Trying to delete a @type entity, which doesn\'t support UUIDs.', array('@type' => $info['label'])));
+    throw new UuidEntityException(t("Trying to delete a @type entity, which doesn\'t support UUIDs.", array('@type' => $info['label'])));
   }
 }
 
@@ -389,24 +389,34 @@ function entity_uuid_delete($entity_type, $uuid) {
  * Helper function that retrieves entity IDs by their UUIDs.
  *
  * @todo
- *   Statically cache as many IDs as possible and limit the query.
+ *   Limit the query.
  *
- * @param $entity_type
+ * @param string $entity_type
  *   The entity type we should be dealing with.
- * @param $uuids
- *   An array of UUIDs for which we should find their entity IDs. If $revision
+ * @param array $uuids
+ *   List of UUIDs for which we should find their entity IDs. If $revision
  *   is TRUE this should be revision UUIDs instead.
- * @param $revision
+ * @param bool $revision
  *   If TRUE the revision IDs is returned instead.
- * @return
- *   Array of entity IDs keyed by their UUIDs. If $revision is TRUE revision
+ *
+ * @return array
+ *   List of entity IDs keyed by their UUIDs. If $revision is TRUE revision
  *   IDs and UUIDs are returned instead.
  */
 function entity_get_id_by_uuid($entity_type, $uuids, $revision = FALSE) {
   if (empty($uuids)) {
     return array();
   }
+  $cached_ids = entity_uuid_id_cache($entity_type, $uuids, $revision);
+  if (count($cached_ids) == count($uuids)) {
+    return $cached_ids;
+  }
+  $uuids = array_diff($uuids, $cached_ids);
   $info = entity_get_info($entity_type);
+  // Some contrib entities has no support for UUID, let's skip them.
+  if (empty($info['uuid'])) {
+    return array();
+  }
   // Find out what entity keys to use.
   if (!$revision) {
     $table = $info['base table'];
@@ -424,35 +434,61 @@ function entity_get_id_by_uuid($entity_type, $uuids, $revision = FALSE) {
   }
 
   // Get all UUIDs in one query.
-  return db_select($table, 't')
+  $result = db_select($table, 't')
     ->fields('t', array($uuid_key, $id_key))
     ->condition($uuid_key, array_values($uuids), 'IN')
     ->execute()
     ->fetchAllKeyed();
+  $cache = &drupal_static('entity_uuid_id_cache', array());
+  $cache[$entity_type][(int) $revision] += $result;
+  return $result + $cached_ids;
+}
+
+/**
+ * Helper caching function.
+ */
+function entity_uuid_id_cache($entity_type, $ids, $revision) {
+  $cache = &drupal_static(__FUNCTION__, array());
+  if (empty($cache[$entity_type][(int) $revision])) {
+    $cache[$entity_type][(int) $revision] = array();
+  }
+  $cached_ids = $cache[$entity_type][(int) $revision];
+  return array_intersect_key($cached_ids, array_flip($ids));
 }
 
 /**
  * Helper function that retrieves UUIDs by their entity IDs.
  *
  * @todo
- *   Statically cache as many IDs as possible and limit the query.
+ *   Limit the query.
  *
- * @param $entity_type
+ * @param string $entity_type
  *   The entity type we should be dealing with.
- * @param $ids
- *   An array of entity IDs for which we should find their UUIDs. If $revision
+ * @param array $ids
+ *   List of entity IDs for which we should find their UUIDs. If $revision
  *   is TRUE this should be revision IDs instead.
- * @param $revision
+ * @param bool $revision
  *   If TRUE the revision UUIDs is returned instead.
- * @return
- *   Array of entity UUIDs keyed by their IDs. If $revision is TRUE revision
+ *
+ * @return array
+ *   List of entity UUIDs keyed by their IDs. If $revision is TRUE revision
  *   IDs and UUIDs are returned instead.
  */
 function entity_get_uuid_by_id($entity_type, $ids, $revision = FALSE) {
   if (empty($ids)) {
     return array();
   }
+  $cached_ids = array_flip(entity_uuid_id_cache($entity_type, $ids, $revision));
+  if (count($cached_ids) == count($ids)) {
+    return $cached_ids;
+  }
+  $ids = array_diff($ids, $cached_ids);
+
   $info = entity_get_info($entity_type);
+  // Some contrib entities has no support for UUID, let's skip them.
+  if (empty($info['uuid'])) {
+    return array();
+  }
   // Find out what entity keys to use.
   if (!$revision) {
     $table = $info['base table'];
@@ -470,11 +506,14 @@ function entity_get_uuid_by_id($entity_type, $ids, $revision = FALSE) {
   }
 
   // Get all UUIDs in one query.
-  return db_select($table, 't')
+  $result = db_select($table, 't')
     ->fields('t', array($id_key, $uuid_key))
     ->condition($id_key, array_values($ids), 'IN')
     ->execute()
     ->fetchAllKeyed();
+  $cache = &drupal_static('entity_uuid_id_cache', array());
+  $cache[$entity_type][(int) $revision] += array_flip($result);
+  return $result + $cached_ids;
 }
 
 /**
@@ -487,12 +526,12 @@ function entity_get_uuid_by_id($entity_type, $ids, $revision = FALSE) {
  * @todo
  *   Add tests for this function.
  *
- * @param $objects
- *   An array of objects that should get $properties changed. Can be either an
+ * @param array $objects
+ *   List of objects that should get $properties changed. Can be either an
  *   entity object or a field items array.
- * @param $entity_type
+ * @param string $entity_type
  *   The type of entity that all $properties refers to.
- * @param $properties
+ * @param array $properties
  *   An array of properties that should be changed. All properties must refer to
  *   the same type of entity (the one referenced in $entity_type).
  */
@@ -543,12 +582,12 @@ function entity_property_id_to_uuid(&$objects, $entity_type, $properties) {
  * @todo
  *   Add tests for this function.
  *
- * @param $objects
- *   An array of objects that should get $properties changed. Can be either an
+ * @param array $objects
+ *   List of objects that should get $properties changed. Can be either an
  *   entity object or a field items array.
- * @param $entity_type
+ * @param string $entity_type
  *   The type of entity that all $properties refers to.
- * @param $properties
+ * @param array $properties
  *   An array of properties that should be changed. All properties must refer to
  *   the same type of entity (the one referenced in $entity_type).
  */

+ 15 - 4
sites/all/modules/contrib/admin/uuid/uuid.features.inc

@@ -84,7 +84,15 @@ function uuid_entities_features_export_render($module_name, $components, $export
       }
       // We unset some common timestamp properties, since those will change and
       // constantly leave the feature overidden.
-      $keys = array('created', 'updated', 'changed', 'revision_timestamp', 'timestamp', 'stamp', 'current');
+      $keys = array(
+        'created',
+        'updated',
+        'changed',
+        'revision_timestamp',
+        'timestamp',
+        'stamp',
+        'current',
+      );
       foreach ($keys as $key) {
         if (isset($entity->{$key})) {
           unset($entity->{$key});
@@ -121,7 +129,10 @@ function uuid_entities_features_export_render($module_name, $components, $export
       }
       uuid_entities_features_clean($entity);
 
-      // Convert entities to array to avoid having them in JSON, returned from standard implementation of $entity->export().
+      /*
+       * Convert entities to array to avoid having them in JSON, returned
+       * from standard implementation of $entity->export().
+       */
       if (is_object($entity) && method_exists($entity, 'export')) {
         $entity = get_object_vars($entity);
       }
@@ -134,14 +145,14 @@ function uuid_entities_features_export_render($module_name, $components, $export
 }
 
 /**
- * Implements [component]_features_export_rebuild().
+ * Implements [component]_features_rebuild().
  */
 function uuid_entities_features_rebuild($module_name) {
   uuid_entities_rebuild($module_name, 'rebuild');
 }
 
 /**
- * Implements [component]_features_export_revert().
+ * Implements [component]_features_revert().
  */
 function uuid_entities_features_revert($module_name) {
   uuid_entities_rebuild($module_name, 'revert');

+ 106 - 35
sites/all/modules/contrib/admin/uuid/uuid.inc

@@ -8,7 +8,7 @@
 /**
  * Pattern for detecting a valid UUID.
  */
-define('UUID_PATTERN', '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}');
+define('UUID_PATTERN', '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}');
 
 /**
  * Generates an universally unique identifier.
@@ -17,7 +17,7 @@ define('UUID_PATTERN', '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}');
  * universally unique identifiers. If that doesn't exist, then it falls back on
  * PHP for generating that.
  *
- * @return
+ * @return string
  *   An UUID, made up of 32 hex digits and 4 hyphens.
  */
 function uuid_generate() {
@@ -37,6 +37,61 @@ function uuid_generate() {
   return $callback();
 }
 
+/**
+ * Generates a version 5 compliant UUID.
+ *
+ * @param string $namespace
+ *   Namespace UUID as a hex encoded string.
+ * @param string $name
+ *   The name for the generating the UUID.
+ *
+ * @return string
+ *   UUID as a hex encoded string.
+ *
+ * @link http://www.php.net/manual/en/function.uniqid.php#94959 Code lifted from
+ * PHP manual comment by Andrew Moore. @endlink
+ */
+function uuid_generate_v5($namespace, $name) {
+  if (!uuid_is_valid($namespace)) {
+    return FALSE;
+  }
+
+  // Get hexadecimal components of namespace.
+  $nhex = str_replace(array('-', '{', '}'), '', $namespace);
+
+  // Binary Value.
+  $nstr = '';
+
+  // Convert Namespace UUID to bits.
+  for ($i = 0; $i < strlen($nhex); $i += 2) {
+    $nstr .= chr(hexdec($nhex[$i] . $nhex[$i + 1]));
+  }
+
+  // Calculate hash value.
+  $hash = sha1($nstr . $name);
+
+  return sprintf('%08s-%04s-%04x-%04x-%12s',
+
+    // 32 bits for "time_low".
+    substr($hash, 0, 8),
+
+    // 16 bits for "time_mid".
+    substr($hash, 8, 4),
+
+    // 16 bits for "time_hi_and_version",
+    // four most significant bits holds version number 5.
+    (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x5000,
+
+    // 16 bits, 8 bits for "clk_seq_hi_res",
+    // 8 bits for "clk_seq_low",
+    // two most significant bits holds zero and one for variant DCE1.1.
+    (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,
+
+    // 48 bits for "node".
+    substr($hash, 20, 12)
+  );
+}
+
 /**
  * Generate all missing UUIDs.
  */
@@ -53,7 +108,7 @@ function uuid_sync_all() {
  *   The type of entity being used.
  *
  * @return string
- *  The generated UUID URI or normal URI if entity doesn't support UUIDs.
+ *   The generated UUID URI or normal URI if entity doesn't support UUIDs.
  */
 function uuid_entity_uuid_uri($entity, $entity_type) {
   $entity_info = entity_get_info($entity_type);
@@ -73,13 +128,13 @@ function uuid_entity_uuid_uri($entity, $entity_type) {
 /**
  * Converts an ID URI string to an entity data array.
  *
- * @see uuid_id_uri_array_to_data()
- *
  * @param string $uri
- *  The URI to convert.
+ *   The URI to convert.
  *
  * @return array
- *  The entity data.
+ *   The entity data.
+ *
+ * @see uuid_id_uri_array_to_data()
  */
 function uuid_id_uri_to_data($uri) {
   $parts = explode('/', $uri);
@@ -90,10 +145,10 @@ function uuid_id_uri_to_data($uri) {
  * Converts a URI array to entity data array.
  *
  * @param array $uri
- *  The URI parts, often taken from arg().
+ *   The URI parts, often taken from arg().
  *
  * @return array
- *  The entity data.
+ *   The entity data.
  */
 function uuid_id_uri_array_to_data($uri) {
   $data = array(
@@ -110,13 +165,13 @@ function uuid_id_uri_array_to_data($uri) {
 /**
  * Converts a UUID URI string to an entity data array.
  *
- * @see uuid_uri_array_to_data()
- *
  * @param string $uri
- *  The URI to convert.
+ *   The URI to convert.
  *
  * @return array
- *  The entity data.
+ *   The entity data.
+ *
+ * @see uuid_uri_array_to_data()
  */
 function uuid_uri_to_data($uri, $strip_uuid = TRUE) {
   return uuid_uri_array_to_data(explode('/', $uri));
@@ -126,10 +181,10 @@ function uuid_uri_to_data($uri, $strip_uuid = TRUE) {
  * Converts a URI array to entity data array.
  *
  * @param array $uri
- *  The URI parts, often taken from arg().
+ *   The URI parts, often taken from arg().
  *
  * @return array
- *  The entity data.
+ *   The entity data.
  */
 function uuid_uri_array_to_data($uri, $strip_uuid = TRUE) {
   if ($strip_uuid) {
@@ -138,8 +193,8 @@ function uuid_uri_array_to_data($uri, $strip_uuid = TRUE) {
 
   $data = array(
     'request' => $uri,
-    'entity_type' => $uri[0],
-    'uuid' => $uri[1],
+    'entity_type' => isset($uri[0]) ? $uri[0] : NULL,
+    'uuid' => isset($uri[1]) ? $uri[1] : NULL,
   );
 
   drupal_alter('uuid_uri_data', $data);
@@ -165,40 +220,56 @@ function _uuid_generate_pecl() {
 }
 
 /**
- * Generates a UUID v4 using PHP code.
+ * Generates a UUID v4 (RFC 4122 section 4.4) using PHP code.
+ *
+ * @see http://www.rfc-editor.org/rfc/rfc4122.txt
+ *
+ * The UUID layout and fields are defined in section 4.1.2.
  *
- * Based on code from http://php.net/uniqid#65879, but corrected.
+ * Note that there are inconsistencies in the RFC with respect to
+ * bit numbering. Network Order is correct, so the most significant bit
+ * always appears first (in left-to-right sequence). See errata 3546:
+ * http://www.rfc-editor.org/errata_search.php?rfc=4122&eid=3546
+ *
+ * Based on code from http://php.net/uniqid
  */
 function _uuid_generate_php() {
-  // The field names refer to RFC 4122 section 4.1.2.
+  // We limit each generated number to 16 bits (maximum value 0xffff)
+  // because mt_rand() returns a *signed* integer, and hence a 32-bit
+  // value can only have a 31-bit magnitude. Constructing a 32-bit
+  // number from two 16-bit random numbers guarantees that all 32 bits
+  // are random.
   return sprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x',
     // 32 bits for "time_low".
-    mt_rand(0, 65535), mt_rand(0, 65535),
+    mt_rand(0, 0xffff), mt_rand(0, 0xffff),
     // 16 bits for "time_mid".
-    mt_rand(0, 65535),
-    // 12 bits after the 0100 of (version) 4 for "time_hi_and_version".
-    mt_rand(0, 4095),
-    bindec(substr_replace(sprintf('%016b', mt_rand(0, 65535)), '10', 0, 2)),
-    // 8 bits, the last two of which (positions 6 and 7) are 01, for "clk_seq_hi_res"
-    // (hence, the 2nd hex digit after the 3rd hyphen can only be 1, 5, 9 or d)
-    // 8 bits for "clk_seq_low" 48 bits for "node".
-    mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)
+    mt_rand(0, 0xffff),
+    // 12 bits after the initial 0100 (version 4) for "time_hi_and_version".
+    mt_rand(0, 0x0fff),
+    // 16 bits in total for "clk_seq_hi_res" and "clk_seq_low", with the
+    // most significant 2 bits of clk_seq_hi_res set to '10'. We do a
+    // bitwise OR of a random 14-bit value (maximum 0x3fff) with 0x8000
+    // (a 16-bit integer with only the most significant bit set).
+    mt_rand(0, 0x3fff) | 0x8000,
+    // 48 bits for "node".
+    mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
   );
 }
 
-
-// This is wrapped in an if block to avoid conflicts with PECL's uuid_is_valid().
+// The if block avoids conflicts with PECL's uuid_is_valid().
 if (!function_exists('uuid_is_valid')) {
+
   /**
    * Check that a string appears to be in the format of a UUID.
    *
-   * @param $uuid
-   *  The string to test.
+   * @param string $uuid
+   *   The string to test.
    *
-   * @return
-   *   TRUE if the string is well formed.
+   * @return bool
+   *    TRUE if the string is well formed.
    */
   function uuid_is_valid($uuid) {
     return preg_match('/^' . UUID_PATTERN . '$/', $uuid);
   }
+
 }

+ 3 - 3
sites/all/modules/contrib/admin/uuid/uuid.info

@@ -7,9 +7,9 @@ files[] = uuid.test
 dependencies[] = node
 dependencies[] = user
 
-; Information added by Drupal.org packaging script on 2014-09-23
-version = "7.x-1.0-alpha6"
+; Information added by Drupal.org packaging script on 2016-08-02
+version = "7.x-1.0-beta2"
 core = "7.x"
 project = "uuid"
-datestamp = "1411455150"
+datestamp = "1470153540"
 

+ 44 - 25
sites/all/modules/contrib/admin/uuid/uuid.install

@@ -31,12 +31,26 @@ function uuid_schema_field_definition() {
 /**
  * Implements hook_schema_alter().
  */
-function uuid_schema_alter(&$schema = array()) {
-  $field = uuid_schema_field_definition();
-  foreach (uuid_get_core_entity_info() as $entity_type => $info) {
-    $schema[$info['base table']]['fields'][$info['entity keys']['uuid']] = $field;
-    if (!empty($info['revision table']) && !empty($info['entity keys']['revision uuid'])) {
-      $schema[$info['revision table']]['fields'][$info['entity keys']['revision uuid']] = $field;
+function uuid_schema_alter(array &$schema) {
+  $field_info = uuid_schema_field_definition();
+  $key_names = array(
+    'base table' => 'uuid',
+    'revision table' => 'revision uuid',
+  );
+
+  foreach (uuid_get_core_entity_info() as $entity_info) {
+    foreach ($key_names as $table_type => $key_name) {
+      if (isset($entity_info[$table_type], $entity_info['entity keys'][$key_name])) {
+        $field_name = $entity_info['entity keys'][$key_name];
+        $properties = array(
+          'fields' => $field_info,
+          'indexes' => array($field_name),
+        );
+
+        foreach ($properties as $property => $value) {
+          $schema[$entity_info[$table_type]][$property][$field_name] = $value;
+        }
+      }
     }
   }
 }
@@ -46,19 +60,20 @@ function uuid_schema_alter(&$schema = array()) {
  */
 function uuid_install() {
   _uuid_install_uuid_fields();
+  module_load_include('inc', 'uuid');
   uuid_sync_all();
 }
 
 /**
- * Install the 'uuid' and 'vuuid' fields into Drupal core entity tables where needed.
+ * Install the uuid and vuuid fields for Drupal core entity tables where needed.
  *
- * IMPORTANT:  This function is called both at install and update time.  If this method
- * is modified to add additional fields in the future, the update strategy must be
- * considered.  See the comment in uuid_update_7102.
+ * IMPORTANT:  This function is called both at install and update time.  If this
+ * method is modified to add additional fields in the future, the update
+ * strategy must be considered.  See the comment in uuid_update_7102.
  */
 function _uuid_install_uuid_fields() {
   $field = uuid_schema_field_definition();
-  foreach (uuid_get_core_entity_info() as $entity_type => $info) {
+  foreach (uuid_get_core_entity_info() as $info) {
     if (!db_field_exists($info['base table'], $info['entity keys']['uuid'])) {
       db_add_field($info['base table'], $info['entity keys']['uuid'], $field);
       db_add_index($info['base table'], $info['entity keys']['uuid'], array($info['entity keys']['uuid']));
@@ -76,7 +91,7 @@ function _uuid_install_uuid_fields() {
  * Implements hook_uninstall().
  */
 function uuid_uninstall() {
-  foreach (uuid_get_core_entity_info() as $entity_type => $info) {
+  foreach (uuid_get_core_entity_info() as $info) {
     if (db_field_exists($info['base table'], $info['entity keys']['uuid'])) {
       db_drop_field($info['base table'], $info['entity keys']['uuid']);
       db_drop_index($info['base table'], $info['entity keys']['uuid']);
@@ -112,8 +127,7 @@ function uuid_update_6001() {
 }
 
 /**
- * For each of out tables, drop the indexe on the UUID column and add a unique
- * key on that column.
+ * Make all uuid columns unique keys instead of indexes.
  */
 function uuid_update_6002() {
   $ret = array();
@@ -138,8 +152,7 @@ function uuid_update_6003() {
 }
 
 /**
- * Fix the column definitions for uuid columns in all tables
- * to use the more efficient char spec.
+ * Change column definitions for uuid columns to more efficient char spec.
  */
 function uuid_update_6004() {
   $ret = array();
@@ -158,6 +171,8 @@ function uuid_update_6004() {
 }
 
 /**
+ * Support deleting node revision.
+ *
  * Modify existing uuid_node_revisions table to support revision deletion, and
  * add in as much legacy data as possible.
  */
@@ -204,15 +219,17 @@ function uuid_update_7100() {
 }
 
 /**
- * Clear cache for installations that used alpha1. Modules that previously was
- * enabled in uuid_update_7100() doesn't exist anymore.
+ * Clear cache for installations that used alpha1.
+ *
+ * Modules previously enabled in uuid_update_7100() don't exist any more. We
+ * need to clear the cache so Drupal detects this change.
  */
 function uuid_update_7101() {
   drupal_flush_all_caches();
 }
 
 /**
- * Insure that the uuid and vuuid fields are added where needed.
+ * Ensure that the uuid and vuuid fields are added where needed.
  *
  * Note that update 7102 calls _uuid_install_uuid_fields(), which is an
  * idempotent function.  If _uuid_install_uuid_fields() is changed at some
@@ -220,8 +237,8 @@ function uuid_update_7101() {
  * will have run update 7102, and some will not.  A new uuid_update_7103()
  * function would would therefore be necessary to update all users to
  * the latest schema.  At the same time, uuid_update_7102() could become
- * an empty function, as it would not be necessary to call _uuid_install_uuid_fields()
- * twice.
+ * an empty function, as it would not be necessary to call
+ * _uuid_install_uuid_fields() twice.
  */
 function uuid_update_7102() {
   // If the user have disabled the UUID module during upgrade (as UPGRADE.txt
@@ -232,9 +249,11 @@ function uuid_update_7102() {
 }
 
 /**
+ * Clean up entities created by uuid_default_entities_example module.
+ *
  * Modify the labels of all example entities created by the now removed
- * uuid_default_entities_example.module to make it clear they're examples.
- * Also remove the administrator role of any example user.
+ * uuid_default_entities_example.module to make it clear they're examples. Also
+ * remove the administrator role of any example user.
  */
 function uuid_update_7103() {
   // These are UUIDs of all the example entities that might exist after having
@@ -249,7 +268,7 @@ function uuid_update_7103() {
       '7cf875e6-dc15-4404-f190-5a7c3e91d14c',
     ),
   );
-  // we can't assume taxonomy is enabled
+  // We can't assume taxonomy is enabled.
   if (isset($info['taxonomy_term'])) {
     $uuids['taxonomy_term'] = array(
       'bcb92ce8-2236-e264-65c8-0c163ae716d1',
@@ -268,7 +287,7 @@ function uuid_update_7103() {
       if ($entity_type == 'user' && $rid = array_search('administrator', $entity->roles)) {
         unset($entity->roles[$rid]);
       }
-      entity_save($entity);
+      entity_save($entity_type, $entity);
     }
   }
 }

+ 10 - 7
sites/all/modules/contrib/admin/uuid/uuid.module

@@ -34,6 +34,7 @@ function uuid_menu() {
     'title' => 'UUID redirector',
     'description' => 'Redirects requests for UUID URIs to the referenced entity.',
     'page callback' => 'uuid_redirector',
+    // The access check is handled in the page callback.
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
@@ -44,7 +45,6 @@ function uuid_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('uuid_admin_form'),
     'access arguments' => array('administer uuid'),
-    'type' => MENU_NORMAL_ITEM,
     'file' => 'uuid.admin.inc',
   );
 
@@ -123,7 +123,6 @@ function uuid_hook_info() {
   return array_fill_keys($hook_names, array('group' => 'uuid'));
 }
 
-
 /**
  * Implements hook_views_api().
  */
@@ -155,7 +154,7 @@ function uuid_module_implements_alter(&$implementss, $hook) {
  * Implements hook_uuid_sync().
  */
 function uuid_uuid_sync() {
-  foreach (entity_get_info() as $entity_type => $info) {
+  foreach (entity_get_info() as $info) {
     if (isset($info['uuid']) && $info['uuid'] == TRUE && !empty($info['entity keys']['uuid'])) {
       _uuid_sync_table($info['base table'], $info['entity keys']['id'], $info['entity keys']['uuid']);
       if (!empty($info['entity keys']['revision uuid'])) {
@@ -215,15 +214,19 @@ function uuid_redirector() {
 
   $entity_info = entity_get_info($entity_data['entity_type']);
   if (empty($entity_info['uuid'])) {
-    return drupal_not_found();
+    return MENU_NOT_FOUND;
   }
 
   $entities = entity_uuid_load($entity_data['entity_type'], array($entity_data['uuid']));
   if (!count($entities)) {
-    return drupal_not_found();
+    return MENU_NOT_FOUND;
   }
 
   $uri = entity_uri($entity_data['entity_type'], current($entities));
-  drupal_goto($uri['path'], array(), 301);
-}
 
+  if (!drupal_valid_path($uri['path'])) {
+    return MENU_ACCESS_DENIED;
+  }
+
+  drupal_goto($uri['path'], $uri['options'], 301);
+}

+ 171 - 75
sites/all/modules/contrib/admin/uuid/uuid.test

@@ -6,23 +6,28 @@
  */
 
 /**
- * Base class with some helper methods.
+ * UUID test helper trait.
+ *
+ * Contains methods that assist with running UUID tests.
  */
-class UUIDTestCase extends DrupalWebTestCase {
-
-  /**
-   * {@inheritdoc}
-   */
-  function setUp() {
-    parent::setUp(func_get_args());
-  }
+trait UUIDTestHelper {
 
   /**
    * Helper function that asserts a UUID.
    */
-  function assertUUID($uuid, $message = NULL) {
+  protected function assertUuid($uuid, $message = NULL) {
     $this->assertTrue(uuid_is_valid($uuid), $message);
   }
+
+}
+
+/**
+ * Base class with some helper methods.
+ */
+abstract class UUIDTestCase extends DrupalWebTestCase {
+
+  use UUIDTestHelper;
+
 }
 
 /**
@@ -44,29 +49,125 @@ class UUIDAPITestCase extends UUIDTestCase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
-    parent::setUp('uuid');
+  protected function setUp() {
+    parent::setUp(array('uuid'));
   }
 
   /**
    * Tests uuid function calls.
    */
-  function testAPIFunctions() {
+  public function testApiFunctions() {
     // This is a valid UUID, we know that.
     $valid_uuid = '0ab26e6b-f074-4e44-9da6-1205fa0e9761';
     // Test the uuid_is_valid() function.
-    $this->assertUUID($valid_uuid, 'UUID validation works.');
+    $this->assertUuid($valid_uuid, 'UUID validation works.');
 
     // The default generator is 'php'.
     $uuid = uuid_generate();
-    $this->assertUUID($uuid, 'PHP generator works.');
+    $this->assertUuid($uuid, 'PHP generator works.');
 
     // Test the 'mysql' generator.
     variable_set('uuid_generator', 'mysql');
     drupal_static_reset('uuid_generate');
     $uuid = uuid_generate();
-    $this->assertUUID($uuid, 'MySQL generator works.');
+    $this->assertUuid($uuid, 'MySQL generator works.');
+  }
+
+  /**
+   * Checks that schema for tables of core entities is correctly defined.
+   */
+  public function testSchemas() {
+    module_load_include('install', 'uuid');
+
+    $schemas = drupal_get_schema();
+    $field_info = uuid_schema_field_definition();
+    $key_names = array(
+      'base table' => 'uuid',
+      'revision table' => 'revision uuid',
+    );
+
+    foreach (uuid_get_core_entity_info() as $entity_info) {
+      // Test the fields in "base" and "revision" tables.
+      foreach ($key_names as $table_type => $key_name) {
+        // Table or field is not defined in entity.
+        if (!isset($entity_info[$table_type], $entity_info['entity keys'][$key_name])) {
+          // Not all entities have a revisions table.
+          continue;
+        }
+
+        $field_name = $entity_info['entity keys'][$key_name];
+        $table_name = $entity_info[$table_type];
+
+        if (!isset($schemas[$table_name])) {
+          $this->fail(sprintf('Database schema does not have a "%s" table.', $table_name));
+          continue;
+        }
+
+        $properties = array(
+          'field' => array('fields', $field_info),
+          'index' => array('indexes', array($field_name)),
+        );
+
+        // Check integrity of the field and index definition.
+        foreach ($properties as $type => $data) {
+          list($property, $value) = $data;
+
+          $message = sprintf('Definition of the "%s" %s in the "%s" schema', $field_name, $type, $table_name);
+
+          if (isset($schemas[$table_name][$property][$field_name])) {
+            $this->assertIdentical($schemas[$table_name][$property][$field_name], $value, "$message is correct.");
+          }
+          else {
+            $this->fail("$message does not exist.");
+          }
+        }
+      }
+    }
+  }
+
+}
+
+/**
+ * Tests the UUID API functions.
+ */
+class UUIDV5TestCase extends UUIDTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'UUID v5',
+      'description' => 'Tests the UUID v5 function.',
+      'group' => 'UUID',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp(array('uuid'));
+  }
+
+  /**
+   * Tests uuid function calls.
+   */
+  public function testV5Function() {
+    // DNS namespace UUID.
+    $dns_namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
+
+    // Valid DNS generation test.
+    $uuid = uuid_generate_v5($dns_namespace, 'drupal.org');
+    $this->assertUuid($uuid, 'UUID for drupal.org is valid.');
+    $this->assertEqual($uuid, 'c809fd30-48df-52e3-a9f2-2cd78129b8b1', 'UUID for drupal.org is correct.');
+
+    // Invalid namespace test.
+    $invalid_namespace = '01234567-c7a9-feda-27e5-75d00dabc123';
+    $uuid = uuid_generate_v5($invalid_namespace, 'drupal.org');
+    $this->assertFalse($uuid, 'Invalid namespace UUID rejected.');
   }
+
 }
 
 /**
@@ -88,14 +189,14 @@ class UUIDEntityTestCase extends UUIDTestCase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
-    parent::setUp('uuid');
+  protected function setUp() {
+    parent::setUp(array('uuid'));
   }
 
   /**
    * Tests Entity API's UUID functions.
    */
-  function testEntityAPIFunctions() {
+  public function testEntityApiFunctions() {
     // Create some entities that we will work with.
     $user = $this->drupalCreateUser();
     $node = $this->drupalCreateNode(array('title' => 'original title', 'uid' => $user->uid));
@@ -112,6 +213,7 @@ class UUIDEntityTestCase extends UUIDTestCase {
     $vuuids = entity_get_uuid_by_id('node', array($node->vid), TRUE);
     $this->assertTrue(in_array($node->vuuid, $vuuids), 'Lookup of entity revision UUID works.');
   }
+
 }
 
 /**
@@ -133,22 +235,23 @@ class UUIDUserTestCase extends UUIDTestCase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
+    $modules = array('uuid');
+
     // Some tests depends on the optional Entity API module.
     if (module_exists('entity')) {
-      parent::setUp('uuid', 'entity');
-    }
-    else {
-      parent::setUp('uuid');
+      $modules[] = 'entity';
     }
+
+    parent::setUp($modules);
   }
 
   /**
    * Test CRUD on users with UUID functions.
    */
-  function testUserCRUD() {
+  public function testUserCrud() {
     $user = $this->drupalCreateUser();
-    $this->assertUUID($user->uuid, 'User UUID was generated.');
+    $this->assertUuid($user->uuid, 'User UUID was generated.');
 
     // Test updating user.
     $user_test = clone $user;
@@ -178,6 +281,7 @@ class UUIDUserTestCase extends UUIDTestCase {
       $this->assertFalse($user_test, 'Deleting user with UUID worked.');
     }
   }
+
 }
 
 /**
@@ -199,14 +303,15 @@ class UUIDNodeTestCase extends UUIDTestCase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
+    $modules = array('uuid');
+
     // Some tests depends on the optional Entity API module.
     if (module_exists('entity')) {
-      parent::setUp('uuid', 'entity');
-    }
-    else {
-      parent::setUp('uuid');
+      $modules[] = 'entity';
     }
+
+    parent::setUp($modules);
   }
 
   /**
@@ -215,13 +320,13 @@ class UUIDNodeTestCase extends UUIDTestCase {
    * @todo
    *   Break out into multiple test methods to loosen coupling between tests.
    */
-  function testNodeCRUD() {
+  public function testNodeCrud() {
     // Create some entities that we will work with.
     $user = $this->drupalCreateUser();
     $node = $this->drupalCreateNode(array('title' => 'original title', 'uid' => $user->uid));
 
-    $this->assertUUID($node->uuid, 'Node UUID was generated.');
-    $this->assertUUID($node->vuuid, 'Node revision UUID was generated.');
+    $this->assertUuid($node->uuid, 'Node UUID was generated.');
+    $this->assertUuid($node->vuuid, 'Node revision UUID was generated.');
 
     // Test node update, without creating new revision.
     $node_test = clone $node;
@@ -244,7 +349,7 @@ class UUIDNodeTestCase extends UUIDTestCase {
     $node_test = node_load($node->nid, FALSE, TRUE);
     $this->assertEqual($node_test->uuid, $node->uuid, 'Node UUID was intact after updating, when creating new revision.');
     $this->assertNotEqual($node_test->vuuid, $node->vuuid, 'A new node revision UUID was generated, when creating new revision.');
-    $this->assertUUID($node_test->vuuid, 'The new node revision UUID was valid.');
+    $this->assertUuid($node_test->vuuid, 'The new node revision UUID was valid.');
 
     // Test entity_uuid_load().
     // Save some variables that we will test against.
@@ -300,7 +405,7 @@ class UUIDNodeTestCase extends UUIDTestCase {
       $this->assertEqual($node_test->title, 'newer title', 'Saving node with UUID mapped to correct node, when creating new revision.');
       $this->assertEqual($node_test->uuid, $node->uuid, 'Node UUID was intact after saving with UUID, when creating new revision.');
       $this->assertNotEqual($node_test->vuuid, $node->vuuid, 'A new node revison UUID was generated after saving with UUID, when creating new revision.');
-      $this->assertUUID($node_test->vuuid, 'New node revision UUID was valid.');
+      $this->assertUuid($node_test->vuuid, 'New node revision UUID was valid.');
       $this->assertEqual($node_test->uid, $node->uid, "Node property 'uid' was intact after saving with UUID, when creating new revision.");
 
       // Test the same thing again, but now triggering a new revision from a
@@ -310,12 +415,9 @@ class UUIDNodeTestCase extends UUIDTestCase {
       $node_test = reset($nodes);
       // Store the current local revision ID to test with later.
       $vid_old1 = $node_test->vid;
-      $vuuid_old1 = $node_test->vuuid;
       // Simulate this node coming from a remote environment by generating
       // IDs that won't match. Only the UUID match at this point.
       $node_test->uuid_services = TRUE;
-      $nid_remote = rand();
-      $vid_remote = rand();
       $vuuid_test = uuid_generate();
       $node_test->nid = $nid_test;
       $node_test->vid = $vid_test;
@@ -375,6 +477,7 @@ class UUIDNodeTestCase extends UUIDTestCase {
       $this->assertFalse($node_test, 'Deleting node with UUID worked.');
     }
   }
+
 }
 
 /**
@@ -385,6 +488,8 @@ class UUIDNodeTestCase extends UUIDTestCase {
  */
 class UUIDCommentTestCase extends CommentHelperCase {
 
+  use UUIDTestHelper;
+
   /**
    * {@inheritdoc}
    */
@@ -396,31 +501,21 @@ class UUIDCommentTestCase extends CommentHelperCase {
     );
   }
 
-  /**
-   * Helper function that asserts a UUID.
-   *
-   * We have duplicated this function from UUIDTestCase since we have to extend
-   * CommentHelperCase instead.
-   */
-  function assertUUID($uuid, $message = NULL) {
-    $this->assertTrue(uuid_is_valid($uuid), $message);
-  }
-
   /**
    * Test CRUD on comments with UUID functions.
    */
-  function testCommentCRUD() {
+  public function testCommentCrud() {
     // This is sub optimal, but due to how CommentHelperCase::setUp() is
     // constructed we are enforced to do this. So unfortunately this test
     // depends on 'entity' module for now.
-    module_enable(array('uuid', 'entity'), TRUE);
+    module_enable(array('uuid', 'entity'));
     $user = $this->drupalCreateUser();
     $this->drupalLogin($user);
     $node = $this->drupalCreateNode();
     $return = $this->postComment($node, 'Lorem ipsum');
 
     $comment = comment_load($return->id);
-    $this->assertUUID($comment->uuid, 'Comment UUID was generated.');
+    $this->assertUuid($comment->uuid, 'Comment UUID was generated.');
 
     // Test updating comment.
     $comment_test = clone $comment;
@@ -456,6 +551,7 @@ class UUIDCommentTestCase extends CommentHelperCase {
       $this->assertFalse($comment_test, 'Deleting comment with UUID worked.');
     }
   }
+
 }
 
 /**
@@ -463,6 +559,8 @@ class UUIDCommentTestCase extends CommentHelperCase {
  */
 class UUIDTaxonomyTestCase extends TaxonomyWebTestCase {
 
+  use UUIDTestHelper;
+
   /**
    * {@inheritdoc}
    */
@@ -479,38 +577,34 @@ class UUIDTaxonomyTestCase extends TaxonomyWebTestCase {
    *
    * A lot of code here is taken from TaxonomyTermTestCase::setUp().
    */
-  function setUp() {
+  protected function setUp() {
+    $modules = array('taxonomy', 'uuid');
+
     // Some tests depends on the optional Entity API module.
     if (module_exists('entity')) {
-      parent::setUp('taxonomy', 'uuid', 'entity');
-    }
-    else {
-      parent::setUp('taxonomy', 'uuid');
+      $modules[] = 'entity';
     }
-  }
 
-  /**
-   * Helper function that asserts a UUID.
-   *
-   * We have duplicated this function from UUIDTestCase since we have to extend
-   * TaxonomyWebTestCase instead.
-   */
-  function assertUUID($uuid, $message = NULL) {
-    $this->assertTrue(uuid_is_valid($uuid), $message);
+    parent::setUp($modules);
   }
 
   /**
    * Test CRUD on comments with UUID functions.
    */
-  function testTaxonomyCRUD() {
-    $user = $this->drupalCreateUser(array('administer taxonomy', 'administer nodes', 'bypass node access'));
+  public function testTaxonomyCrud() {
+    $perms = array(
+      'administer taxonomy',
+      'administer nodes',
+      'bypass node access',
+    );
+    $user = $this->drupalCreateUser($perms);
     $this->drupalLogin($user);
 
     // Create a term by tagging a node. We'll use this node later too.
-    $vocabulary = new stdClass;
+    $vocabulary = new stdClass();
     $vocabulary->vid = 1;
     $term = $this->createTerm($vocabulary);
-    $this->assertUUID($term->uuid, 'Term UUID was generated.');
+    $this->assertUuid($term->uuid, 'Term UUID was generated.');
 
     // Test updating term.
     $term_test = clone $term;
@@ -542,6 +636,7 @@ class UUIDTaxonomyTestCase extends TaxonomyWebTestCase {
       $this->assertFalse($term_test, 'Deleting term with UUID worked.');
     }
   }
+
 }
 
 /**
@@ -566,14 +661,14 @@ class UUIDSyncTestCase extends UUIDTestCase {
    * @todo
    *   There are something weird around this assertion.
    */
-  function assertTableColumn($table, $column, $message) {
+  protected function assertTableColumn($table, $column, $message) {
     $this->assertTrue(db_field_exists($table, $column), $message);
   }
 
   /**
    * Tests creating UUIDs for entities that don't have them.
    */
-  function testSync() {
+  public function testSync() {
     // These entities will not have UUID from the start, since the UUID module
     // isn't installed yet.
     $user = $this->drupalCreateUser();
@@ -600,11 +695,12 @@ class UUIDSyncTestCase extends UUIDTestCase {
 
     // Test if UUID was generated for nodes.
     $node_test = node_load($node->nid, FALSE, TRUE);
-    $this->assertUUID($node_test->uuid, 'Node UUID was generated when clicking the sync button.');
-    $this->assertUUID($node_test->vuuid, 'Node revision UUID was generated when clicking the sync button.');
+    $this->assertUuid($node_test->uuid, 'Node UUID was generated when clicking the sync button.');
+    $this->assertUuid($node_test->vuuid, 'Node revision UUID was generated when clicking the sync button.');
 
     // Test if UUID was generated for users.
     $user_test = user_load($user->uid, TRUE);
-    $this->assertUUID($user_test->uuid, 'User UUID was generated when clicking the sync button.');
+    $this->assertUuid($user_test->uuid, 'User UUID was generated when clicking the sync button.');
   }
+
 }

+ 2 - 1
sites/all/modules/contrib/admin/uuid/uuid.views.inc

@@ -1,7 +1,8 @@
 <?php
+
 /**
  * @file
- * Views Implementation for UUID
+ * Views Implementation for UUID.
  */
 
 /**

+ 3 - 3
sites/all/modules/contrib/admin/uuid/uuid_path/uuid_path.info

@@ -5,9 +5,9 @@ package = UUID
 dependencies[] = uuid
 
 
-; Information added by Drupal.org packaging script on 2014-09-23
-version = "7.x-1.0-alpha6"
+; Information added by Drupal.org packaging script on 2016-08-02
+version = "7.x-1.0-beta2"
 core = "7.x"
 project = "uuid"
-datestamp = "1411455150"
+datestamp = "1470153540"
 

+ 7 - 8
sites/all/modules/contrib/admin/uuid/uuid_path/uuid_path.module

@@ -2,7 +2,7 @@
 
 /**
  * @file
- *   UUID path module functions.
+ * UUID path module functions.
  */
 
 /**
@@ -24,7 +24,7 @@ function uuid_path_entity_uuid_save(&$entity, $entity_type) {
  */
 function _uuid_path_load_url_aliases(&$entities, $entity_type) {
   $info = entity_get_info($entity_type);
-  // we only care about entities with URLs.
+  // We only care about entities with URLs.
   if (!isset($info['uri callback'])) {
     return;
   }
@@ -35,7 +35,7 @@ function _uuid_path_load_url_aliases(&$entities, $entity_type) {
     $aliases = _uuid_path_url_alias_load($path['path']);
 
     // Ignore local IDs.
-    foreach($aliases as &$alias) {
+    foreach ($aliases as &$alias) {
       unset($alias->pid);
       unset($alias->source);
     }
@@ -50,7 +50,7 @@ function _uuid_path_load_url_aliases(&$entities, $entity_type) {
 function _uuid_path_save_url_aliases(&$entity, $entity_type) {
   $info = entity_get_info($entity_type);
 
-  // We only care when there is a url callback
+  // We only care when there is a url callback.
   if (!isset($info['uri callback'])) {
     return FALSE;
   }
@@ -63,7 +63,7 @@ function _uuid_path_save_url_aliases(&$entity, $entity_type) {
   path_delete(array('source' => $path));
 
   // Continue if aliases are present.
-  if(empty($entity->url_alias)) {
+  if (empty($entity->url_alias)) {
     return FALSE;
   }
 
@@ -77,10 +77,10 @@ function _uuid_path_save_url_aliases(&$entity, $entity_type) {
 /**
  * Loads all aliases associated with a path.
  *
- * @param $path
+ * @param string $path
  *   The source path to look up.
  *
- * @return
+ * @return array
  *   Array of paths or NULL if none found.
  */
 function _uuid_path_url_alias_load($path) {
@@ -90,4 +90,3 @@ function _uuid_path_url_alias_load($path) {
     ->execute()
     ->fetchAll(PDO::FETCH_OBJ);
 }
-

+ 0 - 20
sites/all/modules/contrib/admin/uuid/uuid_services/resources/field_collection.resource.inc

@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Field Collection services definition functions.
- */
-
-/**
- * Define a services resource for field_collections.
- */
-function _field_collection_resource_definition() {
-  if (module_exists('field_collection')) {
-    // We will allow uuid_services_services_resources_alter() to add the
-    // default UUID-related operations to this resource.
-    return array('field_collection_item' => array());
-  }
-  else {
-    return array();
-  }
-}

+ 8 - 0
sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.admin.inc

@@ -1,7 +1,15 @@
 <?php
 
+/**
+ * @file
+ * Administration functions for UUID Service module.
+ */
+
 /**
  * Settings form for UUID Services.
+ *
+ * @return array
+ *   Configuration form structure.
  */
 function uuid_services_settings() {
   $form['uuid_services_support_all_entity_types'] = array(

+ 3 - 3
sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.info

@@ -7,9 +7,9 @@ dependencies[] = services
 dependencies[] = uuid
 dependencies[] = entity
 
-; Information added by Drupal.org packaging script on 2014-09-23
-version = "7.x-1.0-alpha6"
+; Information added by Drupal.org packaging script on 2016-08-02
+version = "7.x-1.0-beta2"
 core = "7.x"
 project = "uuid"
-datestamp = "1411455150"
+datestamp = "1470153540"
 

+ 22 - 25
sites/all/modules/contrib/admin/uuid/uuid_services/uuid_services.module

@@ -1,16 +1,21 @@
 <?php
 
 /**
- * Implementation of hook_menu().
+ * @file
+ * UUID Services module functions.
+ */
+
+/**
+ * Implements hook_menu().
  */
 function uuid_services_menu() {
   $items['admin/config/services/uuid-services'] = array(
     'title' => 'UUID Services',
-    'description' => 'Configure settings for Module Filter.',
+    'description' => 'Configure settings for UUID Services.',
     'access arguments' => array('administer services'),
     'page callback' => 'drupal_get_form',
     'page arguments' => array('uuid_services_settings'),
-    'file' => 'uuid_services.admin.inc'
+    'file' => 'uuid_services.admin.inc',
   );
   return $items;
 }
@@ -158,7 +163,15 @@ function _uuid_services_entity_update($entity_type, $uuid, $entity) {
  */
 function _uuid_services_entity_delete($entity_type, $uuid) {
   try {
-    $return = entity_uuid_delete($entity_type, array($uuid));
+    $uuid_exist = (bool) entity_get_id_by_uuid($entity_type, array($uuid));
+    if (!$uuid_exist) {
+      /* UUID not found. Don't try to delete something that doesn't exist. */
+      $args = array('@uuid' => $uuid, '@type' => $entity_type);
+      watchdog('uuid_services', 'UUID @uuid not found for entity type @type', $args, WATCHDOG_WARNING);
+      return TRUE;
+    }
+
+    $return = entity_uuid_delete($entity_type, array($uuid)) !== FALSE;
     return $return;
   }
   catch (Exception $exception) {
@@ -170,14 +183,14 @@ function _uuid_services_entity_delete($entity_type, $uuid) {
 /**
  * Access callback.
  *
- * @param $op
+ * @param string $op
  *   The operation we are trying to do on the entity. Can only be:
  *   - "view"
  *   - "update"
  *   - "delete"
  *   See 'uuid_services_services_resources_alter()' for an explanation why
  *   'create' is missing.
- * @param $args
+ * @param array $args
  *   The arguments passed to the method. The keys are holding the following:
  *   0. <entity_type>
  *   1. <uuid>
@@ -208,10 +221,9 @@ function _uuid_services_entity_access($op, $args) {
     if ($op == 'update' && empty($entity_ids)) {
       $op = 'create';
     }
-    // Taxonomy and Comment module uses 'edit' instead of 'update'.
-    // Oh, how I love Drupal consistency.
-    if (($entity_type == 'taxonomy_term' || $entity_type == 'comment') && $op == 'update') {
-      $op = 'edit';
+    // If the user doesn't exist return 406 like services does.
+    if (($entity_type == 'user' && empty($entity) && $op == 'view')) {
+      return services_error(t('There is no user with UUID @uuid.', array('@uuid' => $args[1])), 406);;
     }
     // The following code is taken from entity_access() with some extra logic
     // to handle the case where an entity type is not defining an access
@@ -227,18 +239,3 @@ function _uuid_services_entity_access($op, $args) {
     return services_error($exception, 406, $entity_type);
   }
 }
-
-/**
- * Implements hook_services_resources().
- */
-function uuid_services_services_resources() {
-  module_load_include('inc', 'uuid_services', 'resources/field_collection.resource');
-
-  $resources = array(
-    '#api_version' => 3002,
-  );
-
-  $resources += _field_collection_resource_definition();
-
-  return $resources;
-}

+ 1 - 0
sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.features.inc

@@ -1,4 +1,5 @@
 <?php
+
 /**
  * @file
  * uuid_services_example.features.inc

+ 3 - 3
sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.info

@@ -11,9 +11,9 @@ features[ctools][] = services:services:3
 features[features_api][] = api:2
 features[services_endpoint][] = uuid_services_example
 
-; Information added by Drupal.org packaging script on 2014-09-23
-version = "7.x-1.0-alpha6"
+; Information added by Drupal.org packaging script on 2016-08-02
+version = "7.x-1.0-beta2"
 core = "7.x"
 project = "uuid"
-datestamp = "1411455150"
+datestamp = "1470153540"
 

+ 2 - 1
sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.module

@@ -1,7 +1,8 @@
 <?php
+
 /**
  * @file
  * Code for the UUID Services Example feature.
  */
 
-include_once('uuid_services_example.features.inc');
+include_once 'uuid_services_example.features.inc';

+ 1 - 0
sites/all/modules/contrib/admin/uuid/uuid_services_example/uuid_services_example.services.inc

@@ -1,4 +1,5 @@
 <?php
+
 /**
  * @file
  * uuid_services_example.services.inc

+ 3 - 3
sites/all/modules/contrib/dev/performance/performance.info

@@ -6,9 +6,9 @@ configure = admin/config/development/performance-logging
 tags[] = developer
 tags[] = monitoring
 
-; Information added by  packaging script on 2013-11-09
-version = "7.x-2.0"
+; Information added by Drupal.org packaging script on 2016-09-26
+version = "7.x-2.1"
 core = "7.x"
 project = "performance"
-datestamp = "1384025307"
+datestamp = "1474895941"
 

+ 2 - 2
sites/all/modules/contrib/dev/performance/performance.module

@@ -267,8 +267,8 @@ function performance_shutdown() {
     $path = variable_get('site_frontpage', 'node');
   }
 
-  // Skip certain paths defined by the user.
-  if (drupal_match_path($path, variable_get('performance_skip_paths', ''))) {
+  // Skip if page from cache or on certain paths defined by the user.
+  if (!function_exists('drupal_match_path') || drupal_match_path($path, variable_get('performance_skip_paths', ''))) {
     return;
   }
 

+ 59 - 0
sites/all/modules/contrib/form/webform_localization/components/grid.inc

@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Webform localizations for grid component.
+ * Translates the analysis component properties that are translatable.
+ *
+ * These are found in under 'translated_strings' in the 'extra' array of the
+ * component, which is build when the component is inserted / updated, or
+ * when all webform strings are updated from
+ * admin/config/regional/translate/i18n_string.
+ */
+
+/**
+ * Implements _webform_localization_analysis_data_component().
+ *
+ * @param array $data
+ *   The data array of component results.
+ * @param array $node
+ *   The node
+ * @param array $component
+ *   The component.
+ *
+ * @return array
+ *   Translated data array of component results.
+ */
+function _webform_localization_analysis_data_grid($data, $node, $component) {
+  if (!isset($component['extra']['translated_strings']) || !is_array($component['extra']['translated_strings'])) {
+    return $data;
+  }
+  $options_key_lookup = _webform_localization_string_to_key($component['extra']['options']);
+  $questions_key_lookup = _webform_localization_string_to_key($component['extra']['questions']);
+
+  foreach ($component['extra']['translated_strings'] as $name) {
+    $name_list = explode(':', $name);
+    // Translate options / questions.
+    list (, $key) = explode('-', $name_list[3]);
+
+    if (strpos($name_list[3], 'grid_options') && $name_list[3] !== '#title') {
+      if (isset($options_key_lookup[$key])) {
+        foreach ($data['table_header'] as $index => $row) {
+          if ($row == $options_key_lookup[$key]) {
+            $data['table_header'][$index] = i18n_string($name, $row);
+          }
+        }
+      }
+    }
+    if (strpos($name_list[3], 'grid_questions') && $name_list[3] !== '#title') {
+      if (isset($questions_key_lookup[$key])) {
+        foreach ($data['table_rows'] as $index => $row) {
+          if (trim($row[0]) == trim($questions_key_lookup[$key])) {
+            $data['table_rows'][$index][0] = i18n_string($name, $row[0]);
+          }
+        }
+      }
+    }
+  }
+  return $data;
+}

+ 102 - 0
sites/all/modules/contrib/form/webform_localization/components/select.inc

@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Webform localizations for select component.
+ */
+
+/**
+ * Translate a single option from component.
+ *
+ * @param array $component
+ *   The select component
+ * @param string $option
+ *   Untranslated option string.
+ *
+ * @return string
+ *   The translated option string, if found.
+ */
+function webform_localization_translate_select_option($component, $option) {
+  // Find the source for data value and translate it.
+  $item_key_lookup = _webform_localization_string_to_key($component['extra']['items']);
+  foreach ($component['extra']['translated_strings'] as $name) {
+    $name_list = explode(':', $name);
+    // Translate options.
+    if (strpos($name_list[3], '-') !== FALSE) {
+      list (, $key) = explode('-', $name_list[3]);
+      if (isset($item_key_lookup[$key]) && $option == $item_key_lookup[$key]) {
+        return i18n_string($name, $option);
+      }
+    }
+  }
+  return $option;
+}
+
+/**
+ * Implements _webform_localization_csv_header_component().
+ */
+function _webform_localization_csv_header_select($header, $component) {
+  if (!isset($component['extra']['translated_strings']) || !is_array($component['extra']['translated_strings'])) {
+    return $header;
+  }
+  // Each component has own methods and tricks to add different items to header
+  // rows. Attempt to translate whatever we can.
+  foreach ($component['extra']['translated_strings'] as $name) {
+    $name_list = explode(':', $name);
+    // Translate header from #title property, this is rather common scenario.
+    if ($name_list[3] == '#title' && $component['name'] == $header[2][0]) {
+      $header[2] = i18n_string($name, $component['name']);
+      break;
+    }
+    // Title could be found from position [1][0] and in this case the select
+    // options are on row 2.
+    if ($name_list[3] == '#title' && $component['name'] == $header[1][0]) {
+      $header[1] = i18n_string($name, $component['name']);
+      foreach ($header[2] as $i => $option) {
+        $header[2][$i] = webform_localization_translate_select_option($component, $option);
+      }
+      break;
+    }
+  }
+  return $header;
+}
+
+/**
+ * Implements _webform_localization_csv_data_component().
+ */
+function _webform_localization_csv_data_select($data, $component, $submission) {
+  // If data is an array then answers are being marked as X:es and there is no
+  // need to translate these.
+  if (is_array($data)) {
+    return $data;
+  }
+  if (!isset($component['extra']['translated_strings']) || !is_array($component['extra']['translated_strings'])) {
+    return $data;
+  }
+  return webform_localization_translate_select_option($component, $data);
+}
+
+/**
+ * Implements _webform_localization_analysis_data_component().
+ */
+function _webform_localization_analysis_data_select($data, $node, $component) {
+  if (!isset($component['extra']['translated_strings']) || !is_array($component['extra']['translated_strings'])) {
+    return $data;
+  }
+  $item_key_lookup = _webform_localization_string_to_key($component['extra']['items']);
+  foreach ($component['extra']['translated_strings'] as $name) {
+    $name_list = explode(':', $name);
+    // Translate options.
+    if (strpos($name_list[3], '-') !== FALSE) {
+      list (, $key) = explode('-', $name_list[3]);
+      if (isset($item_key_lookup[$key])) {
+        foreach ($data['table_rows'] as $index => $row) {
+          if ($row[0] == $item_key_lookup[$key]) {
+            $data['table_rows'][$index][0] = i18n_string($name, $row[0]);
+          }
+        }
+      }
+    }
+  }
+  return $data;
+}

+ 48 - 42
sites/all/modules/contrib/form/webform_localization/includes/webform_localization.component.sync.inc

@@ -18,38 +18,42 @@
  *
  * @staticvar array $component_translations
  *   An array of webform components for each tnid.
+ *
  * @param array $component
  *   A webform component array.
- * @return
- *   An array of webform components that match a tnid.
  *
+ * @return array
+ *   An array of webform components that match a tnid.
  */
 function webform_localization_component_get_translations($component) {
   static $component_translations = array();
 
   $node = node_load($component['nid']);
   $translations = translation_node_get_translations($node->tnid);
+  $component_translations[$node->tnid] = array();
 
-  if (!isset($component_translations[$node->tnid])) {
+  if (!empty($translations) && !isset($component_translations[$node->tnid])) {
     $nid_list = array();
     foreach ($translations as $trans_node) {
       $nid_list[] = $trans_node->nid;
     }
     // Load components for each translated node.
-    $components = db_select('webform_component')
-            ->fields('webform_component')
-            ->condition('nid', $nid_list, 'IN')
-            ->condition('cid', $component['cid'], '=')
-            ->orderBy('nid')
-            ->execute()
-            ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
-    // Cleanup on each component.
-    foreach ($components as $cid => $c) {
-      $components[$cid]['nid'] = $c['nid'];
-      $components[$cid]['extra'] = unserialize($c['extra']);
-      webform_component_defaults($components[$cid]);
+    if (!empty($nid_list)) {
+      $components = db_select('webform_component')
+          ->fields('webform_component')
+          ->condition('nid', $nid_list, 'IN')
+          ->condition('cid', $component['cid'], '=')
+          ->orderBy('nid')
+          ->execute()
+          ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
+      // Cleanup on each component.
+      foreach ($components as $cid => $c) {
+        $components[$cid]['nid'] = $c['nid'];
+        $components[$cid]['extra'] = unserialize($c['extra']);
+        webform_component_defaults($components[$cid]);
+      }
+      $component_translations[$node->tnid] = $components;
     }
-    $component_translations[$node->tnid] = $components;
   }
 
   return $component_translations[$node->tnid];
@@ -58,17 +62,17 @@ function webform_localization_component_get_translations($component) {
 /**
  * Synchronize the changed component with it's translations versions.
  *
- * @param $component
+ * @param array $component
  *   A webform component that have been modified.
- * @param $translations
+ * @param array $translations
  *   An Array of the translated webform components to sync with.
  */
 function webform_localization_component_sync($component, &$translations) {
-  /**
-   * Get properties to sync
-   * $sync_properties['standar_values'] = array('mandatory', 'weight', 'pid');
-   * $sync_properties['extra_values'] = array('options', 'private');
-   */
+
+  // Get properties to sync.
+  // $sync_properties['standar_values'] = array('mandatory', 'weight', 'pid');
+  // $sync_properties['extra_values'] = array('options', 'private');
+
   $sync_properties = webform_localization_synchronizable_properties($component);
   foreach ($translations as $component_key => $translation) {
     foreach ($sync_properties['standar_values'] as $sync_key) {
@@ -85,13 +89,14 @@ function webform_localization_component_sync($component, &$translations) {
 }
 
 /**
- * Get synchronizable properties for a webform component
+ * Get synchronizable properties for a webform component.
  *
  * @param array $component
  *   A webform component.
- * @param boolean $clear_cache
+ * @param bool $clear_cache
  *   A flag to force a database reading in case that properties are cached.
- * @return
+ *
+ * @return array
  *   An array with synchronizable properties.
  */
 function webform_localization_synchronizable_properties($component, $clear_cache = FALSE) {
@@ -102,11 +107,11 @@ function webform_localization_synchronizable_properties($component, $clear_cache
   if ($clear_cache || !isset($webform_component_localization_options[$nid][$cid])) {
     // Select webform localization options that match this node ID.
     $options = db_select('webform_component_localization')
-            ->fields('webform_component_localization')
-            ->condition('nid', $nid, '=')
-            ->condition('cid', $cid, '=')
-            ->execute()
-            ->fetchObject();
+        ->fields('webform_component_localization')
+        ->condition('nid', $nid, '=')
+        ->condition('cid', $cid, '=')
+        ->execute()
+        ->fetchObject();
     if (!$options) {
       $synchronizable = _webform_localization_default_properties($component);
       $webform_component_localization_options[$nid][$cid] = $synchronizable;
@@ -136,7 +141,8 @@ function webform_localization_synchronizable_properties($component, $clear_cache
  *
  * @param array $component
  *   A webform component.
- * @return
+ * 
+ * @return array
  *   An array with webform synchronizable default properties.
  */
 function _webform_localization_default_properties($component) {
@@ -171,23 +177,23 @@ function webform_localization_synchronizable_properties_delete($component) {
 }
 
 /**
- * Load a Webform Component
+ * Load a Webform Component.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
- * @param $cid
+ * @param int $cid
  *   A webform component Id.
- * @return
+ * 
+ * @return array
  *   A webform component array.
- *
  */
 function webform_localization_component_load($nid, $cid) {
   $component = db_select('webform_component')
-          ->fields('webform_component')
-          ->condition('nid', $nid, '=')
-          ->condition('cid', $cid, '=')
-          ->execute()
-          ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
+      ->fields('webform_component')
+      ->condition('nid', $nid, '=')
+      ->condition('cid', $cid, '=')
+      ->execute()
+      ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
   $component[$nid]['nid'] = $nid;
   $component[$nid]['extra'] = unserialize($component[$nid]['extra']);
   webform_component_defaults($component[$nid]);

+ 90 - 126
sites/all/modules/contrib/form/webform_localization/includes/webform_localization.i18n.inc

@@ -4,6 +4,7 @@
  * @file
  * Webform Localization i18n_string integration.
  */
+
 /**
  * Provides interface with the i18n_string module.
  * Based in patch http://drupal.org/node/245424#comment-5244256
@@ -27,9 +28,16 @@
  */
 function _webform_localization_translate_component(&$element, $component) {
   if (isset($component['extra']['translated_strings']) && is_array($component['extra']['translated_strings'])) {
+    $node = !empty($component['nid']) ? node_load($component['nid']) : NULL;
     foreach ($component['extra']['translated_strings'] as $name) {
       $name_list = explode(':', $name);
       $current_element = &$element;
+      $current_element_format;
+      if (isset($current_element['#format'])) {
+        $current_element_format = $current_element['#format'];
+      } else {
+        $current_element_format = I18N_STRING_FILTER_XSS_ADMIN;
+      }
       if (strpos($name_list[3], '[') !== FALSE) {
         // The property is deeper in the renderable array, we must extract the
         // the place where it is.
@@ -52,9 +60,15 @@ function _webform_localization_translate_component(&$element, $component) {
       }
       if (strpos($property, '-') !== FALSE) {
         // If property is array, we extract the key from the property.
-        list ($property, $key) = explode('-', $property);
+        list ($property, $key) = explode('-', $property, 2);
         if (isset($current_element['#' . $property][$key])) {
-          $current_element['#' . $property][$key] = i18n_string($name, $current_element['#' . $property][$key], array('sanitize' => FALSE));
+          $text = i18n_string($name, $current_element['#' . $property][$key], array('format' => I18N_STRING_FILTER_XSS));
+          if (module_exists('token')) {
+            $current_element['#' . $property][$key] = webform_replace_tokens($text, $node);
+          }
+          else {
+            $current_element['#' . $property][$key] = $text;
+          }
         }
       }
       else {
@@ -63,11 +77,23 @@ function _webform_localization_translate_component(&$element, $component) {
           $option_group = str_replace('/-', '', $name_list[4]);
           // If it's a element.
           if (isset($name_list[5])) {
-            $current_element['#' . $property][$option_group][$name_list[5]] = i18n_string($name, $current_element['#' . $property][$option_group][$name_list[5]]);
+            $text = i18n_string($name, $current_element['#' . $property][$option_group][$name_list[5]], array('format' => $current_element_format));
+            if (module_exists('token')) {
+              $current_element['#' . $property][$option_group][$name_list[5]] = webform_replace_tokens($text, $node);
+            }
+            else {
+              $current_element['#' . $property][$option_group][$name_list[5]] = $text;
+            }
           }
           else {
             // If it's a option group we translate the key.
-            $translated_option_group = i18n_string($name, $option_group);
+            $text = i18n_string($name, $option_group, array('format' => $current_element_format));
+            if (module_exists('token')) {
+              $translated_option_group = webform_replace_tokens($text, $node);
+            }
+            else {
+              $translated_option_group = $text;
+            }
             if ($translated_option_group != $option_group) {
               _webform_localization_array_key_replace($current_element['#' . $property], $option_group, $translated_option_group);
             }
@@ -77,84 +103,15 @@ function _webform_localization_translate_component(&$element, $component) {
           // Else we can treat the property as string.
           if (isset($current_element['#' . $property])) {
             if ($property == 'markup' && $current_element['#type'] == 'markup') {
-              $current_element['#' . $property] = i18n_string($name, $current_element['#' . $property], array('format' => $current_element['#format']));
+              $text = i18n_string($name, $current_element['#' . $property], array('format' => $current_element['#format']));
             }
             elseif ($property == 'description') {
-              $current_element['#' . $property] = i18n_string($name, $current_element['#' . $property], array('format' => I18N_STRING_FILTER_XSS));
+              $text = i18n_string($name, $current_element['#' . $property], array('sanitize' => FALSE));
             }
             else {
-              $current_element['#' . $property] = i18n_string($name, $current_element['#' . $property]);
-            }
-          }
-        }
-      }
-    }
-  }
-}
-
-/**
- * Translates the analysis component properties that are translatable.
- *
- * These are found in under 'translated_strings' in the 'extra' array of the
- * component, which is build when the component is inserted / updated, or
- * when all webform strings are updated from
- * admin/config/regional/translate/i18n_string.
- *
- * @param array $data
- *   The data array of component results.
- * @param array $component
- *   The component.
- */
-function _webform_localization_translate_analysis_component(&$data, &$component) {
-  if (!isset($component['extra']['translated_strings']) || !is_array($component['extra']['translated_strings'])) {
-    return;
-  }
-  // Attempt to translate select options.
-  if ($component['type'] == 'select') {
-    $item_key_lookup = _webform_localization_string_to_key($component['extra']['items']);
-  }
-  // Attempt to translate grid options / questions.
-  if ($component['type'] == 'grid') {
-    $options_key_lookup = _webform_localization_string_to_key($component['extra']['options']);
-    $questions_key_lookup = _webform_localization_string_to_key($component['extra']['questions']);
-  }
-
-  foreach ($component['extra']['translated_strings'] as $name) {
-    $name_list = explode(':', $name);
-    // Translate component name from title property.
-    if ($name_list[3] == '#title') {
-      $component['name'] = i18n_string($name, $component['name']);
-      continue;
-    }
-    // Translate options for select elements.
-    if ($component['type'] == 'select' && strpos($name_list[3], '-') !== FALSE) {
-      list (, $key) = explode('-', $name_list[3]);
-      if (isset($item_key_lookup[$key])) {
-        foreach ($data['table_rows'] as $index => $row) {
-          if ($row[0] == $item_key_lookup[$key]) {
-            $data['table_rows'][$index][0] = i18n_string($name, $row[0]);
-          }
-        }
-      }
-    }
-    // Translate options / questions for grid elements.
-    if ($component['type'] == 'grid' && $name_list[3] !== '#title') {
-      list (, $key) = explode('-', $name_list[3]);
-      if (strpos($name_list[3], 'grid_options')) {
-        if (isset($options_key_lookup[$key])) {
-          foreach ($data['table_header'] as $index => $row) {
-            if ($row == $options_key_lookup[$key]) {
-              $data['table_header'][$index] = i18n_string($name, $row);
-            }
-          }
-        }
-      }
-      if (strpos($name_list[3], 'grid_questions')) {
-        if (isset($questions_key_lookup[$key])) {
-          foreach ($data['table_rows'] as $index => $row) {
-            if (trim($row[0]) == trim($questions_key_lookup[$key])) {
-              $data['table_rows'][$index][0] = i18n_string($name, $row[0]);
+              $text = i18n_string($name, $current_element['#' . $property], array('sanitize' => FALSE));
             }
+            $current_element['#' . $property] = $text;
           }
         }
       }
@@ -194,9 +151,9 @@ function webform_localization_component_update_translation_strings(&$component)
  *   The renderable array to be parsed.
  * @param array $component
  *   The component which was rendered.
- * @return
- *   An array of translatabled webform properties.
  *
+ * @return array
+ *   An array of translatabled webform properties.
  */
 function _webform_localization_component_translation_parse($element, $component) {
   $translated_properies = array();
@@ -204,19 +161,26 @@ function _webform_localization_component_translation_parse($element, $component)
     $element['#parents'] = array();
   }
 
+  $element['#translatable'][] = 'placeholder';
   if (isset($element['#translatable']) && is_array($element['#translatable'])) {
     foreach ($element['#translatable'] as $key) {
-      if (isset($element['#' . $key]) && $element['#' . $key] != '') {
+      if (!empty($element['#' . $key]) || !empty($element['#attributes'][$key])) {
         if (isset($element['#parents']) && count($element['#parents'])) {
           $property = '[' . implode('][', $element['#parents']) . ']#' . $key;
         }
         else {
           $property = '#' . $key;
         }
-        if (is_array($element['#' . $key])) {
+        $element_key = '';
+        if ($key == 'placeholder' && !empty($element['#attributes']['placeholder'])) {
+          $element_key = $element['#attributes']['placeholder'];
+        } elseif ($key != 'placeholder' && !empty($element['#' . $key])) {
+          $element_key = $element['#' . $key];
+        }
+        if (is_array($element_key)) {
           // If the translatable property is an array, we translate the
           // children.
-          foreach ($element['#' . $key] as $elem_key => $elem_value) {
+          foreach ($element_key as $elem_key => $elem_value) {
             // If the child if an array, we translate the elements.
             if (is_array($elem_value)) {
               foreach ($elem_value as $k => $v) {
@@ -237,13 +201,11 @@ function _webform_localization_component_translation_parse($element, $component)
           }
         }
         else {
-          /**
-           * If the translatable property is not an array,
-           * it can be treated as a string.
-           */
+          // If the translatable property is not an array,
+          // it can be treated as a string.
           $name = webform_localization_i18n_string_name($component['nid'], $component['cid'], $property);
           $translated_properies[] = $name;
-          i18n_string($name, $element['#' . $key], array('update' => TRUE));
+          i18n_string($name, $element_key, array('update' => TRUE));
         }
       }
     }
@@ -256,8 +218,7 @@ function _webform_localization_component_translation_parse($element, $component)
     $element[$child]['#parents'][] = $child;
     // Add the translated propertied to the list.
     $translated_properies = array_merge(
-            $translated_properies,
-            _webform_localization_component_translation_parse($element[$child], $component)
+        $translated_properies, _webform_localization_component_translation_parse($element[$child], $component)
     );
   }
 
@@ -270,18 +231,18 @@ function _webform_localization_component_translation_parse($element, $component)
  * Additional arguments can be passed to add more depth to context
  *
  * @param int $node_identifier
- *   webform nid
+ *   webform nid.
  *
  * @return string
- *   i18n string name grouped by nid or uuid if module is available
+ *   i18n string name grouped by nid or uuid if module is available.
  */
 function webform_localization_i18n_string_name($node_identifier) {
-  if (module_exists('uuid')) {
+  if (module_exists('uuid') and !uuid_is_valid($node_identifier)) {
     $node_identifier = current(entity_get_uuid_by_id('node', array($node_identifier)));
   }
   $name = array('webform', $node_identifier);
   $args = func_get_args();
-  // Remove $node_identifier from args
+  // Remove $node_identifier from args.
   array_shift($args);
   foreach ($args as $arg) {
     $name[] = $arg;
@@ -290,7 +251,7 @@ function webform_localization_i18n_string_name($node_identifier) {
 }
 
 /**
- * Delete translation source for all the translatable poperties
+ * Delete translation source for all the translatable poperties.
  *
  * Process components matching webforms configuration.
  */
@@ -352,21 +313,20 @@ function webform_localization_update_translation_strings($properties) {
 /**
  * Translate general webform properties.
  *
- * @param $node
+ * @param object $node
  *   A node object.
  */
 function webform_localization_translate_strings(&$node, $update = FALSE) {
-  $option = array('update' => $update, 'sanitize' => FALSE);
+  $option = array('update' => $update, array('sanitize' => FALSE, array('format' => I18N_STRING_FILTER_XSS_ADMIN)));
+  if (!array_key_exists('nid', $node->webform)) {
+    $node->webform['nid'] = $node->nid;
+  }
   $name = webform_localization_i18n_string_name($node->webform['nid'], 'confirmation');
   $node->webform['confirmation'] = i18n_string(
-          $name,
-          $node->webform['confirmation'],
-          $option);
+      $name, $node->webform['confirmation'], $option);
   $name = webform_localization_i18n_string_name($node->webform['nid'], 'submit_text');
   $node->webform['submit_text'] = i18n_string(
-          $name,
-          $node->webform['submit_text'],
-          $option);
+      $name, $node->webform['submit_text'], $option);
 
   // Allow to translate the redirect url if it's not set to none or the
   // default confirmation page.
@@ -407,9 +367,9 @@ function webform_localization_emails_update_translation_string($properties) {
 /**
  * Update / create translation source for webform email poperties.
  *
- * @param $emails
+ * @param array $emails
  *   An array of webform emails.
- * @param $nid
+ * @param int $nid
  *   The node Id of the webform.
  */
 function webform_localization_emails_translation_string_refresh($emails, $nid) {
@@ -438,7 +398,7 @@ function webform_localization_emails_translation_string_refresh($emails, $nid) {
 /**
  * Translate webform email poperties.
  *
- * @param $node
+ * @param object $node
  *   A node object.
  */
 function webform_localization_email_translate_strings(&$node) {
@@ -459,7 +419,7 @@ function webform_localization_email_translate_strings(&$node) {
     }
     if (!empty($email['template']) && $email['template'] != 'default') {
       $name = webform_localization_i18n_string_name($nid, 'email', $eid, 'template');
-      $email['template'] = i18n_string($name, $email['template']);
+      $email['template'] = i18n_string($name, $email['template'], array('sanitize' => FALSE));
     }
   }
 }
@@ -467,9 +427,9 @@ function webform_localization_email_translate_strings(&$node) {
 /**
  * Remove translation source for webform email poperties.
  *
- * @param $eid
+ * @param int $eid
  *   A webform email Id.
- * @param $nid
+ * @param int $nid
  *   A node Id.
  */
 function webform_localization_emails_delete_translation_string($eid, $nid) {
@@ -484,7 +444,7 @@ function webform_localization_emails_delete_translation_string($eid, $nid) {
 /**
  * Translate general webform poperties.
  *
- * @param $node
+ * @param object $node
  *   A node object.
  */
 function webform_localization_delete_translate_strings($node) {
@@ -499,12 +459,11 @@ function webform_localization_delete_translate_strings($node) {
 
 /**
  * Update i18n string contexts if uuid module is enabled/disabled.
- *
  */
 function webform_localization_uuid_update_strings($disabling_uuid = FALSE) {
   module_load_install('i18n_string');
   $old_ids = db_query('SELECT distinct type FROM {i18n_string} WHERE textgroup = :webform', array(
-        ':webform' => 'webform'
+    ':webform' => 'webform',
       ))->fetchCol();
   variable_set('webform_localization_using_uuid', !$disabling_uuid);
   if (empty($old_ids)) {
@@ -528,13 +487,13 @@ function webform_localization_uuid_update_strings($disabling_uuid = FALSE) {
 /**
  * Helper function that retrieves entity IDs by their UUIDs.
  *
- *
- * @param $entity_type
+ * @param string $entity_type
  *   The entity type we should be dealing with.
- * @param $uuids
+ * @param array $uuids
  *   An array of UUIDs for which we should find their entity IDs. If $revision
  *   is TRUE this should be revision UUIDs instead.
- * @return
+ *
+ * @return array
  *   Array of entity IDs keyed by their UUIDs. If $revision is TRUE revision
  *   IDs and UUIDs are returned instead.
  */
@@ -552,22 +511,21 @@ function webform_localization_get_id_by_uuid($entity_type, $uuids) {
 
   // Get all UUIDs in one query.
   return db_select($table, 't')
-      ->fields('t', array($uuid_key, $id_key))
-      ->condition($uuid_key, array_values($uuids), 'IN')
-      ->execute()
-      ->fetchAllKeyed();
+          ->fields('t', array($uuid_key, $id_key))
+          ->condition($uuid_key, array_values($uuids), 'IN')
+          ->execute()
+          ->fetchAllKeyed();
 }
 
 /**
  * Helper function to replace an array key and its content.
  *
- * @param $array
+ * @param array $array
  *   Array To process.
- * @param $old_key
+ * @param string $old_key
  *   Array key to be replaced.
- * @param $new_key
+ * @param string $new_key
  *   The new array key.
- *
  */
 function _webform_localization_array_key_replace(&$array, $old_key, $new_key) {
   $keys = array_keys($array);
@@ -583,17 +541,23 @@ function _webform_localization_array_key_replace(&$array, $old_key, $new_key) {
 /**
  * Helper function to convert select / grid strings to array.
  *
- * @param $string_array
+ * @param string $string_array
  *   Array To process.
  *
+ * @return array
+ *   Processed array.
  */
 function _webform_localization_string_to_key($string_array) {
   $key_array = array();
   $items = explode("\n", trim($string_array));
   foreach ($items as $item) {
     $item_data = explode('|', $item);
-    $key_array[$item_data[0]] = $item_data[1];
+    if (isset($item_data[1]) && isset($item_data[0])) {
+      $key_array[$item_data[0]] = $item_data[1];
+    }
+    elseif (isset($item_data[1]) && !isset($item_data[0])) {
+      $key_array[$item_data[0]] = '';
+    }
   }
   return $key_array;
 }
-

+ 22 - 19
sites/all/modules/contrib/form/webform_localization/includes/webform_localization.sync.inc

@@ -14,10 +14,11 @@
 /**
  * Sync webform configured properties with its translated versions.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
  */
 function webform_localization_webform_properties_sync($nid) {
+
   // Gets webform localization options that match this node ID.
   $webform_localization_options = webform_localization_get_config($nid, TRUE);
   if (count($webform_localization_options['webform_properties']) > 0) {
@@ -25,10 +26,10 @@ function webform_localization_webform_properties_sync($nid) {
     if (count($node_list) > 1) {
       // Select all webforms that match these node IDs.
       $result = db_select('webform')
-              ->fields('webform')
-              ->condition('nid', $node_list, 'IN')
-              ->execute()
-              ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
+          ->fields('webform')
+          ->condition('nid', $node_list, 'IN')
+          ->execute()
+          ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
       if ($result) {
         $origin = $result[$nid];
         unset($result[$nid]);
@@ -47,16 +48,16 @@ function webform_localization_webform_properties_sync($nid) {
 /**
  * Sync webform roles with its translated versions.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
  */
 function webform_localization_roles_sync($nid) {
   $node_list = _webform_localization_translation_set_node_list($nid);
   $roles = db_select('webform_roles')
-          ->fields('webform_roles', array('rid'))
-          ->condition('nid', $nid)
-          ->execute()
-          ->fetchCol();
+      ->fields('webform_roles', array('rid'))
+      ->condition('nid', $nid)
+      ->execute()
+      ->fetchCol();
   foreach ($node_list as $n) {
     if ($n != $nid) {
       db_delete('webform_roles')->condition('nid', $n)->execute();
@@ -70,7 +71,7 @@ function webform_localization_roles_sync($nid) {
 /**
  * Sync webform emails recipients with its translated versions.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
  */
 function webform_localization_emails_sync($nid) {
@@ -111,17 +112,18 @@ function webform_localization_emails_sync($nid) {
 /**
  * Get an Array of webform emails recipients for a Node Id.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
- * @return
+ *
+ * @return array
  *   An array of webform emails.
  */
 function _webform_localization_emails_load($nid) {
   $emails = db_select('webform_emails')
-          ->fields('webform_emails')
-          ->condition('nid', $nid)
-          ->execute()
-          ->fetchAllAssoc('eid', PDO::FETCH_ASSOC);
+      ->fields('webform_emails')
+      ->condition('nid', $nid)
+      ->execute()
+      ->fetchAllAssoc('eid', PDO::FETCH_ASSOC);
   // Unserialize the exclude component list for e-mails.
   foreach ($emails as $eid => $email) {
     $emails[$eid]['excluded_components'] = array_filter(explode(',', $email['excluded_components']));
@@ -135,9 +137,10 @@ function _webform_localization_emails_load($nid) {
 /**
  * Get a node Id list of a translation set.
  *
- * @param $nid
+ * @param int $nid
  *   A node Id.
- * @return
+ *
+ * @return array
  *   An array of node ids that share a tnid.
  */
 function _webform_localization_translation_set_node_list($nid) {

+ 88 - 17
sites/all/modules/contrib/form/webform_localization/tests/webform_localization.test

@@ -9,44 +9,61 @@ class WebformLocalizationWebTestCase extends DrupalWebTestCase {
   // Webform test class instance.
   public $wtc;
   // Users.
-  public $admin_user;
+  public $adminuser;
   public $translator;
-  public $normal_user;
+  public $normaluser;
+  public $perms;
+  protected $profile = 'testing';
 
   /**
    * Implements setUp().
    */
-  function setUp($modules = array()) {
-    $modules = array_merge($modules, array('locale', 'webform', 'webform_localization'));
-    parent::setUp($modules);
+  function setUp($added_modules = array()) {
+    $modules = array('webform_localization', 'webform', 'views', 'ctools', 'i18n_translation', 'i18n_string', 'i18n', 'variable', 'translation', 'locale', 'block');
+    $added_modules = array_merge($modules, $added_modules);
+    parent::setUp($added_modules);
 
     // We load webform test class to reuse webform and components creation functions.
     module_load_include('test', 'webform', 'tests/webform');
     $this->wtc = new WebformTestCase;
 
+    /* Reset the permissions cache prior to calling drupalCreateUser
+     * see notes here: https://api.drupal.org/comment/28739#comment-28739
+     */
+    $this->checkPermissions(array(), TRUE);
     // Setup users.
-    $this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'translate content', 'create webform content',
+    $this->adminuser = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'translate content', 'create webform content',
           'edit any webform content',
           'access all webform results',
           'edit all webform submissions',
           'delete all webform submissions',
+          'edit webform components',
           'translate interface',
           'translate user-defined strings'));
 
+    /* Reset the permissions cache prior to calling drupalCreateUser
+     * see notes here: https://api.drupal.org/comment/28739#comment-28739
+     */
+    $this->checkPermissions(array('access content'), TRUE);
     $this->translator = $this->drupalCreateUser(array('translate content',
           'create webform content',
           'edit any webform content',
           'access all webform results',
+          'edit webform components',
           'translate interface',
           'translate user-defined strings'));
 
-    $this->normal_user = $this->drupalCreateUser(array('access content', 'edit own webform submissions'));
+    /* Reset the permissions cache prior to calling drupalCreateUser
+     * see notes here: https://api.drupal.org/comment/28739#comment-28739
+     */
+    $this->checkPermissions(array('access content'), TRUE);
+    $this->normaluser = $this->drupalCreateUser(array('access content', 'edit own webform submissions'));
 
     // Fix for reuse of webform test class.
-    $this->wtc->webform_users['admin'] = $this->admin_user;
-    $this->wtc->webform_users['admin']->profile_gender = array('Female', 'Male');
+    $this->wtc->webform_users['admin'] = $this->adminuser;
+    $this->wtc->webform_users['admin']->gender = array(LANGUAGE_NONE => array(array('value' => 'Female')));
 
-    $this->drupalLogin($this->admin_user);
+    $this->drupalLogin($this->adminuser);
 
     // Add languages.
     $this->addLanguage('en');
@@ -107,8 +124,9 @@ class WebformLocalizationWebTestCase extends DrupalWebTestCase {
      */
     unset($components['select_no_default_zero']);
     unset($components['radios_zero']);
+    unset($components['checkboxes_zero']);
+    unset($components['grid_keyed']);
     unset($components['select_zero']);
-    unset($components['select_optgroup']);
 
     $cid = 0;
     foreach ($components as $key => $component_info) {
@@ -286,14 +304,15 @@ class WebformLocalizationStringTranslationTestCase extends WebformLocalizationWe
       'name' => 'Webform Localization',
       'description' => 'Webform localization String Translations Tests.',
       'group' => 'Webform Localization',
+      'dependencies' => array('webform_localization', 'webform', 'views', 'ctools', 'i18n_translation', 'i18n_string', 'i18n', 'variable'),
     );
   }
 
   /**
    * Set up test.
    */
-  public function setUp($modules = array()) {
-    parent::setUp(array('translation', 'i18n', 'i18n_string'));
+  function setUp($added_modules = array()) {
+    parent::setUp($added_modules);
 
     // Set "Webform" content type to use multilingual support with translation.
     $this->drupalGet('admin/structure/types/manage/webform');
@@ -307,7 +326,7 @@ class WebformLocalizationStringTranslationTestCase extends WebformLocalizationWe
    * Test creating a webform and enabling localization by string translation
    */
   function testWebformLocalizationStringTranslation() {
-    $this->drupalLogin($this->admin_user);
+    $this->drupalLogin($this->adminuser);
     /**
      * Create the Webform test node, and enable
      * localization by string translation feature
@@ -400,8 +419,59 @@ class WebformLocalizationStringTranslationTestCase extends WebformLocalizationWe
       }
       $this->assertRaw($translation, format_string('%c translation is present.', array('%c' => $name)), 'Deutsch Webform translation');
     }
+
+    // Log in as normal user and add a answer to webform so string
+    // translatability can be tested at various webform report pages.
+    $this->drupalLogin($this->normaluser);
+    $this->drupalPost('node/' . $node->nid, array(), t('Submit'));
+    // Log back to admin account and download results, using Deutsch Webform
+    // translations
+    $this->drupalLogin($this->adminuser);
+    $edit = array(
+      'format' => 'delimited',
+    );
+    $this->drupalPost('de/node/' . $node->nid . '/webform-results/download', $edit, t('Download'));
+    // Previous form submission batch created the export file. Click the link
+    // to access export file itself.
+    $this->clickLink(t('download the file here'));
+    // Export file should contain all element titles translated
+    foreach ($options as $key => $value) {
+      $context = $value['context'];
+      if (substr($context, -5) != ':#title') {
+        continue;
+      }
+      $translation = 'de:' . $value['source'];
+      $this->assertRaw($translation, t('%c translation is present.', array('%c' => $value['source'])), 'Deutsch Webform translation');
+    }
+  }
+
+}
+
+/**
+ * The same test as WebformLocalizationStringTranslationTestCase, but with the
+ * Token module enabled.
+ */
+class WebformLocalizationStringTranslationTokenTestCase extends WebformLocalizationStringTranslationTestCase {
+  /**
+   * Test info.
+   */
+  public static function getInfo() {
+    $info = parent::getInfo();
+    $info = array_merge($info, array(
+      'name' => 'Webform Localization Token',
+      'description' => 'Webform localization String Translations with Token Tests.',
+    ));
+    $info['dependencies'][] = 'token';
+    return $info;
   }
 
+  /**
+   * Set up test.
+   */
+  function setUp($added_modules = array()) {
+    $added_modules = array_merge(array('token'), $added_modules);
+    parent::setUp($added_modules);
+  }
 }
 
 class WebformLocalizationApiTestCase extends WebformLocalizationWebTestCase {
@@ -414,14 +484,15 @@ class WebformLocalizationApiTestCase extends WebformLocalizationWebTestCase {
       'name' => 'Test Webform Localization API.',
       'description' => 'Test webform and webform localization interaction at API level.',
       'group' => 'Webform Localization',
+      'dependencies' => array('webform_localization', 'webform', 'views', 'ctools', 'i18n_translation', 'i18n_string', 'i18n', 'variable'),
     );
   }
 
   /**
    * Set up test.
    */
-  public function setUp($modules = array()) {
-    parent::setUp(array('translation', 'i18n', 'i18n_string'));
+  function setUp($added_modules = array()) {
+    parent::setUp($added_modules);
 
     // Set "Webform" content type to use multilingual support with translation.
     $this->drupalGet('admin/structure/types/manage/webform');
@@ -435,7 +506,7 @@ class WebformLocalizationApiTestCase extends WebformLocalizationWebTestCase {
    * Test submissions API function with webform localization presence.
    */
   function testWebformLocalizationApi() {
-    $this->drupalLogin($this->admin_user);
+    $this->drupalLogin($this->adminuser);
     $node = $this->createWebformForm();
     /**
      * Enables localization by string translation and reuse the single webform

+ 72 - 0
sites/all/modules/contrib/form/webform_localization/webform_localization.api.php

@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Webform localization hooks.
+ */
+
+/**
+ * Module specific instance of _webform_localization_csv_header().
+ *
+ * This allows each component to translate the analysis report CSV header.
+ * These are found in under 'translated_strings' in the 'extra' array of the
+ * component, which is build when the component is inserted / updated, or
+ * when all webform strings are updated from
+ * admin/config/regional/translate/i18n_string.
+ *
+ * @param array $header
+ *   The untranslated CSV header to be altered.
+ * @param array $component
+ *   The webform component.
+ *
+ * @return array
+ *   The translated header array.
+ */
+function _webform_localization_csv_header_component($header, $component) {
+  $header[1] = 'Custom string';
+  return $header;
+}
+
+/**
+ * Module specific instance of _webform_localization_csv_data().
+ *
+ * This allows each component to translate the analysis report CSV data.
+ *
+ * @param array $data
+ *   The untranslated CSV data to be altered.
+ * @param array $component
+ *   The webform component.
+ * @param array $submission
+ *   The webform submission.
+ *
+ * @return array
+ *   The translated data array.
+ */
+function _webform_localization_csv_data_component($data, $component, $submission) {
+  $data[1] = 'Custom string';
+  return $data;
+}
+
+/**
+ * Module specific instance of _webform_localization_analysis_data().
+ *
+ * This allows each component to translate the analysis report data.
+ * These are found in under 'translated_strings' in the 'extra' array of the
+ * component, which is build when the component is inserted / updated, or
+ * when all webform strings are updated from
+ * admin/config/regional/translate/i18n_string.
+ *
+ * @param array $data
+ *   The untranslated data to be altered
+ * @param object $node
+ *   The webform node.
+ * @param array $component
+ *   The webform component.
+ *
+ * @return array
+ *   The translated data array.
+ */
+function _webform_localization_analysis_data_component($data, $node, $component) {
+  $data[1] = 'Custom string';
+  return $data;
+}

+ 13 - 6
sites/all/modules/contrib/form/webform_localization/webform_localization.info

@@ -1,16 +1,23 @@
-
+; $Id: $
 name = Webform Localization
 description = Enables localization features to forms and questionnaires.
-dependencies[] = webform
-dependencies[] = i18n_string
 core = 7.x
 package = Webform
+php = 5.3
+dependencies[] = locale
+dependencies[] = translation
+dependencies[] = variable
+dependencies[] = i18n_translation
+dependencies[] = i18n_string
+dependencies[] = webform (4.x)
+test_dependencies[] = token
+test_dependencies[] = i18n_block
 
 files[] = tests/webform_localization.test
 
-; Information added by Drupal.org packaging script on 2014-03-28
-version = "7.x-4.x-dev"
+; Information added by Drupal.org packaging script on 2016-10-22
+version = "7.x-4.9"
 core = "7.x"
 project = "webform_localization"
-datestamp = "1396049366"
+datestamp = "1477171440"
 

+ 5 - 6
sites/all/modules/contrib/form/webform_localization/webform_localization.install

@@ -9,12 +9,11 @@
  * Implements hook_install().
  */
 function webform_localization_install() {
-  /**
-   * NOTE:
-   * We add a field to record the language of the submission since when using
-   * "Localization by String Translation" you can get single webform been
-   * submitted by several nodes in different languages.
-   */
+
+  // NOTE:
+  // We add a field to record the language of the submission since when using
+  // "Localization by String Translation" you can get single webform been
+  // submitted by several nodes in different languages.
   db_add_field('webform_submissions', 'language', array(
     'description' => 'The {languages}.language source of this submission.',
     'type' => 'varchar',

+ 619 - 121
sites/all/modules/contrib/form/webform_localization/webform_localization.module

@@ -53,53 +53,100 @@ function webform_localization_i18n_string_info() {
  * Refresh callback that regenerates all the translatable poperties of the
  * components of the matching webforms configuration.
  */
-function webform_localization_i18n_string_refresh() {
+function webform_localization_i18n_string_refresh($group) {
+  if ($group == 'webform') {
+    global $language;
+    $languages = language_list();
+    $source_language = $languages[i18n_string_source_language()];
+    // Refresh the strings using source language.
+    $language = $source_language;
+    module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
 
-  module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
+    // In case updating before UUID support.
+    if (module_exists('uuid') && !variable_get('webform_localization_using_uuid', FALSE)) {
+      webform_localization_uuid_update_strings(FALSE);
+    }
 
-  // In case updating before UUID support.
-  if (module_exists('uuid') && !variable_get('webform_localization_using_uuid', FALSE)) {
-    webform_localization_uuid_update_strings(FALSE);
-  }
+    // Get components configured as translatable.
+    $query = db_select('webform_component', 'wc');
+    $query->fields('wc');
+    $query->condition('wl.expose_strings', 0, '>');
+    $query->innerJoin('webform_localization', 'wl', 'wc.nid = wl.nid');
+    $components = $query->execute()->fetchAll();
 
-  // Get components configured as translatable.
-  $query = db_select('webform_component', 'wc');
-  $query->fields('wc');
-  $query->condition('wl.expose_strings', 0, '>');
-  $query->innerJoin('webform_localization', 'wl', 'wc.nid = wl.nid');
-  $components = $query->execute()->fetchAll();
+    foreach ($components as $component) {
+      $component = (array) $component;
+      $component['extra'] = unserialize($component['extra']);
 
-  foreach ($components as $component) {
-    $component = (array) $component;
-    $component['extra'] = unserialize($component['extra']);
+      webform_localization_component_update_translation_strings($component);
 
-    webform_localization_component_update_translation_strings($component);
+      $component['extra'] = serialize($component['extra']);
+      drupal_write_record('webform_component', $component, array('nid', 'cid'));
+    }
+    // Get emails configured as translatable.
+    $query = db_select('webform_localization', 'wl');
+    $query->fields('wl', array('nid'));
+    $query->condition('wl.expose_strings', 0, '>');
+    $nid_list = $query->execute()->fetchAllAssoc('nid');
+    // @todo: Find a more eficient way to manage webform translatable
+    // properties.
+    $nodes = node_load_multiple(array_keys($nid_list));
+    module_load_include('inc', 'webform_localization', 'includes/webform_localization.sync');
+    foreach ($nid_list as $nid => $value) {
+      $emails = _webform_localization_emails_load($nid);
+      webform_localization_emails_translation_string_refresh($emails, $nid);
+      $node = $nodes[$nid];
+      webform_localization_translate_strings($node, TRUE);
+    }
 
-    $component['extra'] = serialize($component['extra']);
-    drupal_write_record('webform_component', $component, array('nid', 'cid'));
+    // NOTE: Delete string for webforms that has disabled i18n translation.
+    // This is the only moment when we deleted translation for disabled
+    // webforms. This way we provide the feature to temporally disable the
+    // webform i18n string without losing custom translated texts.
+    webform_localization_delete_all_strings();
+    return TRUE;
   }
-  // Get emails configured as translatable.
-  $query = db_select('webform_localization', 'wl');
-  $query->fields('wl', array('nid'));
-  $query->condition('wl.expose_strings', 0, '>');
-  $nid_list = $query->execute()->fetchAllAssoc('nid');
-  // @todo: Find a more eficient way to manage webform translatable properties.
-  $nodes = node_load_multiple(array_keys($nid_list));
-  module_load_include('inc', 'webform_localization', 'includes/webform_localization.sync');
-  foreach ($nid_list as $nid => $value) {
-    $emails = _webform_localization_emails_load($nid);
-    webform_localization_emails_translation_string_refresh($emails, $nid);
-    $node = $nodes[$nid];
-    webform_localization_translate_strings($node, TRUE);
+}
+
+/**
+ * Load a component file into memory.
+ *
+ * @see webform_component_include()
+ *
+ * @param string $component_type
+ *   The string machine name of a component.
+ */
+function webform_localization_component_include($component_type) {
+  static $included = array();
+
+  // No need to load components that have already been added once.
+  if (!isset($included[$component_type])) {
+    $included[$component_type] = TRUE;
+    module_load_include('inc', 'webform_localization', 'components/' . $component_type);
+  }
+}
+
+/**
+ * Invoke a component callback.
+ *
+ * @see webform_component_invoke()
+ *
+ * @param string $type
+ *   The component type as a string.
+ * @param string $callback
+ *   The callback to execute.
+ * @param ...
+ *   Any additional parameters required by the $callback.
+ */
+function webform_localization_component_invoke($type, $callback) {
+  $args = func_get_args();
+  $type = array_shift($args);
+  $callback = array_shift($args);
+  $function = '_webform_localization_' . $callback . '_' . $type;
+  webform_localization_component_include($type);
+  if (function_exists($function)) {
+    return call_user_func_array($function, $args);
   }
-  /**
-   *  NOTE: Delete string for webforms that has disabled i18n translation.
-   *  This is the only moment when we deleted translation for disabled webforms.
-   *  This way we provide the feature to temporally disable the webform i18n
-   *  string without losing custom translated texts.
-   */
-  webform_localization_delete_all_strings();
-  return TRUE;
 }
 
 /**
@@ -110,10 +157,12 @@ function webform_localization_webform_component_insert($component) {
   $wl_options = webform_localization_get_config($component['nid']);
 
   // Create translation source for i18n_string for all the translatable
-  // poperties.
+  // properties.
   if ($wl_options['expose_strings']) {
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
     webform_localization_component_update_translation_strings($component);
+    $component['extra'] = serialize($component['extra']);
+    drupal_write_record('webform_component', $component, array('nid', 'cid'));
   }
 
   if ($wl_options['sync_components'] && _webform_localization_sync()) {
@@ -156,19 +205,25 @@ function webform_localization_webform_component_presave(&$component) {
   }
 
   if ($wl_options['sync_components'] && _webform_localization_sync()) {
-    // Turn Off Sync.
-    _webform_localization_sync(FALSE);
-    module_load_include('inc', 'webform_localization', 'includes/webform_localization.component.sync');
-    // Get all versions of the component across all translations.
-    $translations = webform_localization_component_get_translations($component);
-    unset($translations[$component['nid']]);
-    // Sync the changed component with it's translations versions.
-    webform_localization_component_sync($component, $translations);
-    foreach ($translations as $trans_c) {
-      webform_component_update($trans_c);
+    $test_node = node_load($component['nid']);
+    if ($test_node->tnid > 0) {
+      // Turn Off Sync.
+      _webform_localization_sync(FALSE);
+      module_load_include('inc', 'webform_localization', 'includes/webform_localization.component.sync');
+      // Get all versions of the component across all translations.
+      $translations = webform_localization_component_get_translations($component);
+      unset($translations[$component['nid']]);
+      // Sync the changed component with it's translations versions.
+      webform_localization_component_sync($component, $translations);
+      foreach ($translations as $trans_c) {
+        webform_component_update($trans_c);
+      }
+      // Turn On Sync.
+      _webform_localization_sync(TRUE);
+    }
+    else {
+      debug('configuration error: webform_localization_sync enabled AND Keep a single webform across a translation set. but no tnid, to resolve this either disable <Keep a single webform across a translation set> by unchecking this option, or enable node translation on the webform', t('debug'));
     }
-    // Turn On Sync.
-    _webform_localization_sync(TRUE);
   }
 }
 
@@ -191,16 +246,52 @@ function webform_localization_webform_component_delete($component) {
     // Get all versions of the node.
     $node = node_load($component['nid']);
     $translations = translation_node_get_translations($node->tnid);
-    unset($translations[$node->language]);
-    foreach ($translations as $trans_c) {
-      $component_version = webform_localization_component_load($trans_c->nid, $component['cid']);
-      webform_component_delete($trans_c, $component_version);
+    if ($translations) {
+      unset($translations[$node->language]);
+      foreach ($translations as $trans_c) {
+        $component_version = webform_localization_component_load($trans_c->nid, $component['cid']);
+        webform_component_delete($trans_c, $component_version);
+      }
     }
+
     // Turn On Sync.
     _webform_localization_sync(TRUE);
   }
 }
 
+/**
+ * A menu to_arg handler explicitly invoked by webform_menu_to_arg().
+ *
+ * If a single webform is used across all translations, use the appropriate
+ * node ID for webform paths.
+ */
+function webform_localization_webform_menu_to_arg($arg, $map, $index) {
+  if ($node = node_load($arg)) {
+    if ($nid = webform_localization_single_webform_nid($node)) {
+      $node = node_load($nid);
+    }
+  }
+  return $node ? $node->nid : $arg;
+}
+
+/**
+ * Find nid of node containing the 'single webform' for this translation set.
+ */
+function webform_localization_single_webform_nid($node) {
+  $cache = &drupal_static(__FUNCTION__, array());
+  if (!array_key_exists($node->nid, $cache)) {
+    // Select all webforms that match the localization configuration.
+    $query = db_select('webform', 'w');
+    $query->innerJoin('webform_localization', 'wl', 'w.nid = wl.nid');
+    $query->fields('w', array('nid'));
+    $query->condition('wl.single_webform', 0, '<>');
+    $query->condition('wl.single_webform', $node->tnid, '=');
+    $query->condition('w.nid', $node->nid, '<>');
+    $cache[$node->nid] = $query->execute()->fetchField();
+  }
+  return $cache[$node->nid];
+}
+
 /**
  * Implements hook_node_view().
  */
@@ -208,23 +299,13 @@ function webform_localization_node_view($node, $view_mode) {
   if (!in_array($node->type, webform_variable_get('webform_node_types'))) {
     return;
   }
-  // Select all webforms that match the localization configuration.
-  $query = db_select('webform', 'w');
-  $query->innerJoin('webform_localization', 'wl', 'w.nid = wl.nid');
-  $query->fields('w', array('nid'));
-  $query->condition('wl.single_webform', 0, '<>');
-  $query->condition('wl.single_webform', $node->tnid, '=');
-  $query->condition('w.nid', $node->nid, '<>');
-  $result = $query->execute()->fetchField();
+  if ($nid = webform_localization_single_webform_nid($node)) {
 
-  if ($result) {
-    /**
-     * NOTE:
-     * Perhaps not most eficient way.. a possible alternative
-     * @todo rewrite the webform load and view process as a
-     * independent function to reuse.
-     */
-    $source_node = node_load($result);
+    // NOTE:
+    // Perhaps not most eficient way.. a possible alternative
+    // @todo rewrite the webform load and view process as a
+    // independent function to reuse.
+    $source_node = node_load($nid);
     // We replace the webform with the node translation source.
     $node->webform = $source_node->webform;
 
@@ -265,11 +346,10 @@ function webform_localization_node_load($nodes, $types) {
  * Implements hook_webform_submission_update().
  */
 function webform_localization_webform_submission_insert($node, $submission) {
-  /**
-   * NOTE:
-   * We have 2 options here: to use the node language or the language of
-   * the page... for now make more sense use the node as language source
-   */
+
+  // NOTE:
+  // We have 2 options here: to use the node language or the language of
+  // the page... for now make more sense use the node as language source.
   $language = $node->language;
   // Update language field when a submission is updated.
   db_update('webform_submissions')
@@ -284,8 +364,8 @@ function webform_localization_webform_submission_insert($node, $submission) {
  * Implements hook_webform_submission_load().
  */
 function webform_localization_webform_submission_load(&$submissions) {
-  if(empty($submissions)) {
-      return;
+  if (empty($submissions)) {
+    return;
   }
   $query = db_select('webform_submissions', 's');
   $query->fields('s', array('language', 'sid'));
@@ -296,12 +376,20 @@ function webform_localization_webform_submission_load(&$submissions) {
   }
 }
 
+/**
+ * Implements hook_webform_submission_presave().
+ */
+function webform_localization_webform_submission_presave(&$node, &$submission) {
+  if (isset($node->tnid) && $node->tnid != 0) {
+    $submission->serial = _webform_submission_serial_next_value($node->tnid);
+  }
+}
+
 /**
  * Implements hook_node_delete().
  */
 function webform_localization_node_delete($node) {
-  if (!in_array($node->type, webform_variable_get('webform_node_types'))
-      || empty($node->webform['components'])) {
+  if (!in_array($node->type, webform_variable_get('webform_node_types')) || empty($node->webform['components'])) {
     return;
   }
   module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
@@ -321,6 +409,10 @@ function webform_localization_webform_component_render_alter(&$element, $compone
   if ($wl_options['expose_strings']) {
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
     _webform_localization_translate_component($element, $component);
+    if (!empty($element['#attributes']['placeholder'])) {
+      $name = webform_localization_i18n_string_name($component['nid'], $component['cid'], '#placeholder');
+      $element['#attributes']['placeholder'] = i18n_string($name, $element['#attributes']['placeholder']);
+    }
   }
 }
 
@@ -345,22 +437,67 @@ function webform_localization_webform_analysis_component_data_alter(&$data, $nod
   $wl_options = webform_localization_get_config($node->nid);
   // Translate the translatable properties.
   if ($wl_options['expose_strings']) {
-    module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
-    _webform_localization_translate_analysis_component($data, $component);
+    // Translate component name, this is not a component spesific property.
+    foreach ($component['extra']['translated_strings'] as $name) {
+      $name_list = explode(':', $name);
+      // Translate component name from title property.
+      if ($name_list[3] == '#title') {
+        $component['name'] = i18n_string($name, $component['name']);
+        break;
+      }
+    }
+    // Translate data array.
+    $result = webform_localization_component_invoke($component['type'], 'analysis_data', $data, $node, $component);
+    if (!empty($result)) {
+      $data = $result;
+    }
+  }
+}
+
+/**
+ * Implements hook_webform_csv_header_alter().
+ */
+function webform_localization_webform_csv_header_alter(&$header, $component) {
+  // Gets webform localization options that match this node ID.
+  $wl_options = webform_localization_get_config($component['nid']);
+  // Translate the translatable properties.
+  if ($wl_options['expose_strings']) {
+    $result = webform_localization_component_invoke($component['type'], 'csv_header', $header, $component);
+    if (!empty($result)) {
+      $header = $result;
+    }
+  }
+}
+
+/**
+ * Implements hook_webform_csv_data_alter().
+ */
+function webform_localization_webform_csv_data_alter(&$data, $component, $submission) {
+  // Gets webform localization options that match this node ID.
+  $wl_options = webform_localization_get_config($component['nid']);
+  // Translate the translatable properties.
+  if ($wl_options['expose_strings']) {
+    $result = webform_localization_component_invoke($component['type'], 'csv_data', $data, $component, $submission);
+    if (!empty($result)) {
+      $data = $result;
+    }
   }
 }
 
 /**
  * Implements hook_form_FORM_ID_alter().
  *
- * Add specific localization options to Webform Configure Form
+ * Add specific localization options to Webform Configure Form.
  */
 function webform_localization_form_webform_configure_form_alter(&$form, &$form_state, $form_id) {
 
+  // @see https://api.drupal.org/comment/52443#comment-52443
+  $enabled_languages = locale_language_list('name');
+
   // Gets webform localization options that match this node ID.
   $webform_localization_options = webform_localization_get_config($form['nid']['#value']);
 
-  if ($webform_localization_options['expose_strings']) {
+  if ($webform_localization_options['expose_strings'] && count($enabled_languages) > 1) {
     // Avoid caching for translatable element values.
     entity_get_controller('node')->resetCache(array($form['nid']['#value']));
     $form['#node'] = node_load($form['nid']['#value']);
@@ -369,6 +506,17 @@ function webform_localization_form_webform_configure_form_alter(&$form, &$form_s
       $form['submission']['redirection']['redirect_url']['#default_value'] = $form['#node']->webform['redirect_url'];
     }
     $form['advanced']['submit_text']['#default_value'] = $form['#node']->webform['submit_text'];
+    // Add friendly translation link.
+    $name = webform_localization_i18n_string_name($form['#node']->nid, 'confirmation');
+    $string_source = i18n_string_get_string($name);
+    if (isset($string_source->lid) && $string_source->lid != FALSE) {
+      $link = l(t('here'), 'admin/config/regional/translate/edit/' . $string_source->lid, array('query' => drupal_get_destination()));
+      $description = t('Click !here to translate this string.', array('!here' => $link));
+    }
+    else {
+      $description = t('<i>The message must be filled in before it can be translated.</i>');
+    }
+    $form['submission']['confirmation']['#description'] .= ' ' . $description;
   }
 
   $single_webform = 0;
@@ -495,17 +643,42 @@ function _webform_localization_webform_configure_form_submit($form, &$form_state
  */
 function _webform_localization_webform_configure_form_submit_i18n_tweaks($form, &$form_state) {
   global $language;
-  $default_language = language_default();
-  if ($default_language->language != $language->language) {
-    // Webform Configure Form not in default language.
+  $source_language = i18n_string_source_language();
+  if ($source_language != $language->language) {
+    // Webform Configure Form not in source language.
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
-    $name = webform_localization_i18n_string_name($form['#node']->webform['nid'], 'confirmation');
-    $string_source = i18n_string_get_string($name);
-    $string_translation = $form_state['values']['confirmation']['value'];
-    // We reset the source string value before saving the form.
-    $form_state['values']['confirmation']['value'] = $string_source->get_string();
-    // We save the translated string using i18n string.
-    i18n_string_translation_update($name, $string_translation, $language->language);
+    // Collect the form elements that are translatable.
+    $form_elements = array('confirmation', 'submit_text');
+    // Add redirect url if it's not set to none or the default
+    // confirmation page.
+    if (!in_array($form_state['values']['redirect_url'], array('<confirmation>', '<none>'))) {
+      $form_elements[] = 'redirect_url';
+    }
+    foreach ($form_elements as $element) {
+      // Get i18n string for $element.
+      $name = webform_localization_i18n_string_name($form['#node']->webform['nid'], $element);
+      $i18n_string = i18n_string_get_string($name);
+      // Get submitted value for $element (the translation).
+      $string_translation = ($element == 'confirmation' ? $form_state['values'][$element]['value'] : $form_state['values'][$element]);
+      if ($i18n_string->lid) {
+        // There is an i18n string source.
+        $string_source = $i18n_string->get_string();
+        // We reset the source string value before saving the form.
+        if ($element == 'confirmation') {
+          $form_state['values'][$element]['value'] = $string_source;
+        }
+        else {
+          $form_state['values'][$element] = $string_source;
+        }
+      }
+      else {
+        // No i18n string source found: use the current translation as the
+        // string source.
+        $string_source = $string_translation;
+      }
+      // We save the translated string using i18n string.
+      i18n_string_translation_update($name, $string_translation, $language->language, $string_source);
+    }
   }
 }
 
@@ -529,9 +702,13 @@ function _webform_localization_webform_email_edit_form_submit($form, &$form_stat
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.sync');
     webform_localization_emails_sync($node->nid);
   }
-  if ($webform_localization_options['expose_strings']) {
-    module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
-    webform_localization_emails_update_translation_string($form_state['values'] + array('node' => $node));
+  if (isset($form_state['values']['node'])) {
+    // Above isset avoids problem when using entity_translation.
+    // @see https://www.drupal.org/node/2482521
+    if ($webform_localization_options['expose_strings']) {
+      module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
+      webform_localization_emails_update_translation_string($form_state['values'] + array('node' => $node));
+    }
   }
 }
 
@@ -549,15 +726,19 @@ function webform_localization_form_webform_email_delete_form_alter(&$form, &$for
  */
 function _webform_localization_webform_email_delete_form_submit($form, &$form_state) {
 
-  $node = $form['#node'];
+  $node = $form['node']['#value'];
   $webform_localization_options = webform_localization_get_config($node->nid);
   if ($webform_localization_options['sync_emails']) {
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.sync');
     webform_localization_emails_sync($node->nid);
   }
-  if ($webform_localization_options['expose_strings']) {
-    module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
-    webform_localization_emails_delete_translation_string($form_state['values']['email']['eid'], $node->nid);
+  if (isset($form_state['values']['node'])) {
+    // Above isset avoids problem when using entity_translation.
+    // @see https://www.drupal.org/node/2482521
+    if ($webform_localization_options['expose_strings']) {
+      module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
+      webform_localization_emails_delete_translation_string($form_state['values']['email']['eid'], $node->nid);
+    }
   }
 }
 
@@ -600,10 +781,8 @@ function webform_localization_form_webform_component_edit_form_alter(&$form, &$f
       '#default_value' => $select_options['extra_values'],
       '#description' => t('Special properties that applies only for this type of component.'),
     );
-    /**
-     * NOTE:
-     * First we save the sync options to know what to do with the changes.
-     */
+    // NOTE:
+    // First we save the sync options to know what to do with the changes.
     array_unshift($form['#submit'], '_webform_localization_webform_component_edit_form_submit');
   }
 }
@@ -638,28 +817,131 @@ function webform_localization_field_attach_prepare_translation_alter(&$entity, $
   if ($context['entity_type'] == 'node') {
     if (isset($context['source_entity']->webform)) {
       $webform_localization_options = webform_localization_get_config($context['source_entity']->nid);
-      /**
-       * Copy all Webform settings over to translated versions of this node
-       * if the configuration match.
-       */
+      // Copy all Webform settings over to translated versions of this node
+      // if the configuration match.
       if ($webform_localization_options['sync_components']) {
-        /**
-         * NOTE:
-         * Perhaps could be interesting to copy only specific properties
-         * but for now the entire webform make more sense.
-         */
+        // NOTE:
+        // Perhaps could be interesting to copy only specific properties
+        // but for now the entire webform make more sense.
         $entity->webform = $context['source_entity']->webform;
       }
     }
   }
 }
 
+/**
+ * Implements hook_form_alter().
+ */
+function webform_localization_form_alter(&$form, &$form_state, $form_id) {
+  // n.b. We are not using hook_form_BASE_FORM_ID_alter(), as we need
+  // to interact closely with other hook_form_alter() implementations.
+  // @see webform_localization_module_implements_alter()
+  if (strpos($form_id, 'webform_client_form_') !== 0) {
+    return;
+  }
+
+  // Enhance webform's mollom support, to handle our 'single_webform' option.
+  if (module_exists('mollom')) {
+    _webform_localization_mollom_form_alter($form, $form_state, $form_id);
+  }
+}
+
+/**
+ * Interaction with mollom_form_alter() for 'single_webform' localization.
+ *
+ * If the translation source node's webform is protected by mollom, and
+ * uses our 'single_webform' setting, then we must also protect the other
+ * nodes in the translation set.
+ *
+ * This is because each node in the translation set will still have a
+ * unique client form_id (based on the nid, not the tnid), but mollom will
+ * only know about one of those form_ids.
+ * 
+ * @see webform_localization_module_implements_alter()
+ */
+function _webform_localization_mollom_form_alter(&$form, &$form_state, $form_id) {
+  // Establish that the current node has a (different) translation source.
+  // (If this is the translation source, mollom will already know about it).
+  $node = $form['#node'];
+  if (empty($node->tnid) || $node->tnid == $node->nid) {
+    return;
+  }
+
+  // Prime the static mollom form cache (for manipulation).
+  mollom_form_cache();
+  $mollom_cache = &drupal_static('mollom_form_cache');
+  $protected = &$mollom_cache['protected'];
+
+  // If the source node's webform is not protected by mollom, we can bail
+  // out immediately.
+  $source_form_id = 'webform_client_form_' . $node->tnid;
+  if (!array_key_exists($source_form_id, $protected)) {
+    return;
+  }
+
+  // Check that the 'single_webform' option is configured for the source.
+  $source_wl_options = webform_localization_get_config($node->tnid);
+  if (empty($source_wl_options['single_webform'])) {
+    return;
+  }
+
+  // Protect this form using the settings for the single webform.
+  // Firstly we take care of the mollom_form_cache() static cache.
+  $protected[$form_id] = $protected[$source_form_id];
+
+  // Then, if necessary, we update the mollom_form_load() cache.
+  $mollom_form = mollom_form_load($form_id);
+  if (!$mollom_form) {
+    $source_mollom_form = mollom_form_load($source_form_id);
+    $mollom_form = $source_mollom_form;
+    $mollom_form['form_id'] = $form_id;
+    $cid = 'mollom:form:' . $form_id;
+    cache_set($cid, $mollom_form);
+  }
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ *
+ * We need our hook_form_alter() to run before mollom's, so that we can
+ * prime and modify its cache before mollom uses it.
+ *
+ * @see webform_localization_form_alter()
+ * @see mollom_form_alter()
+ */
+function webform_localization_module_implements_alter(&$implementations, $hook) {
+  if ($hook != 'form_alter') {
+    return;
+  }
+
+  // If mollom isn't enabled, do nothing.
+  if (!module_exists('mollom')) {
+    return;
+  }
+
+  // If our code will run before mollom's, do nothing.
+  $pos = array_flip(array_keys($implementations));
+  if ($pos['webform_localization'] < $pos['mollom']) {
+    return;
+  }
+
+  // Make it so our hook implementation runs before mollom's.
+  $webform_localization = array(
+    'webform_localization' => $implementations['webform_localization'],
+  );
+  $implementations = array_diff_key($implementations, $webform_localization);
+  $implementations = array_merge(
+      array_slice($implementations, 0, $pos['mollom'], TRUE), $webform_localization, array_slice($implementations, $pos['mollom'], NULL, TRUE)
+  );
+}
+
 /**
  * Gets webform localization options that match a node ID.
  *
  * @staticvar array $webform_localization_options
  *   An array of webform localization options group by nid.
- * @param $nid
+ *
+ * @param int $nid
  *   A node Id.
  * @param bool $clear_cache
  *   A flag to force a database reading in case that properties are cached.
@@ -679,12 +961,20 @@ function webform_localization_get_config($nid, $clear_cache = FALSE) {
     unset($webform_properties['roles']);
     unset($webform_properties['emails']);
     unset($webform_properties['record_exists']);
+
+    // If webform_uuid is being used since $component['nid'] is really a UUID
+    // get the nid.
+    if (module_exists('webform_uuid') && variable_get('webform_localization_using_uuid', FALSE)) {
+      $record = entity_get_id_by_uuid('node', array($nid));
+      $nid = (!empty($record)) ? array_pop($record) : $nid;
+    }
+
     // Select webform localization options that match this node ID.
     $options = db_select('webform_localization')
-            ->fields('webform_localization')
-            ->condition('nid', $nid, '=')
-            ->execute()
-            ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
+        ->fields('webform_localization')
+        ->condition('nid', $nid, '=')
+        ->execute()
+        ->fetchAllAssoc('nid', PDO::FETCH_ASSOC);
     if (count($options) == 0) {
       $webform_localization_options[$nid] = array(
         'nid' => $nid,
@@ -795,4 +1085,212 @@ function webform_localization_modules_enabled($modules) {
     module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
     webform_localization_uuid_update_strings(FALSE);
   }
-}
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Add related translations to translation form.
+ */
+function webform_localization_form_i18n_string_locale_translate_edit_form_alter(&$form, &$form_state, $form_id) {
+  if ($form['textgroup']['#value'] == 'webform') {
+    if (isset($form['context']['#markup']) && preg_match('/#title/', $form['context']['#markup'])) {
+      list($id, $cid, $name) = explode(':', $form['context']['#markup']);
+      $form['#submit'][] = 'webform_localization_i18n_string_locale_translate_edit_form_submit';
+      $rel_key = 'webform:' . $id . ':' . $cid . '%';
+      $related = db_query('SELECT lid, source, context, textgroup, location FROM {locales_source} WHERE location LIKE :key', array(':key' => $rel_key));
+
+      $langs_installed = language_list();
+
+      $langs_enabled = language_list('enabled');
+      $langs_enabled = $langs_enabled[1];
+
+      if (isset($form['translations'])) {
+        foreach ($form['translations'] as $key => $value) {
+          if (array_key_exists($key, $langs_installed) && !array_key_exists($key, $langs_enabled)) {
+            unset($form['translations'][$key]);
+          }
+        }
+      }
+
+      $form['items'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Related strings'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+        '#weight' => 2,
+      );
+      foreach ($related as $source) {
+        if ($source->context != $form['context']['#markup']) {
+          $lid = $source->lid;
+          // Add original text to the top and some values for form altering.
+          $form['items'][$lid]['original'] = array(
+            '#type' => 'item',
+            '#title' => t('Original text'),
+            '#markup' => check_plain(wordwrap($source->source, 0)),
+          );
+          if (!empty($source->context)) {
+            $form['items'][$lid]['context'] = array(
+              '#type' => 'item',
+              '#title' => t('Context'),
+              '#markup' => check_plain($source->context),
+            );
+          }
+          $form['items'][$lid]['textgroup'] = array(
+            '#type' => 'value',
+            '#value' => $source->textgroup,
+          );
+          $form['items'][$lid]['location'] = array(
+            '#type' => 'value',
+            '#value' => $source->location,
+          );
+
+          $languages = $langs_enabled;
+          // We don't need the default language value, that value is in $source.
+          $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language();
+          unset($languages[($omit)]);
+          $form['items'][$lid]['translations-' . $lid] = array('#tree' => TRUE);
+          // Approximate the number of rows to use in the default textarea.
+          $rows = min(ceil(str_word_count($source->source) / 12), 10);
+          foreach ($languages as $langcode => $language) {
+            $form['items'][$lid]['translations-' . $lid][$langcode] = array(
+              '#type' => 'textarea',
+              '#title' => t($language->name),
+              '#rows' => $rows,
+              '#default_value' => '',
+            );
+          }
+
+          // Fetch translations and fill in default values in the form.
+          $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit));
+          foreach ($result as $translation) {
+            $form['items'][$lid]['translations-' . $lid][$translation->language]['#default_value'] = $translation->translation;
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Submit handler for translations form.
+ */
+function webform_localization_i18n_string_locale_translate_edit_form_submit($form, &$form_state) {
+  foreach ($form_state['values'] as $key => $value) {
+    if (preg_match("/translations-(.*)/", $key, $lid)) {
+      foreach ($value as $lang => $translation) {
+        $existing = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid[1], ':language' => $lang))->fetchField();
+        if (!empty($translation)) {
+          if (!empty($existing)) {
+            db_update('locales_target')
+                ->fields(array(
+                  'translation' => $translation,
+                  'i18n_status' => I18N_STRING_STATUS_CURRENT,
+                ))
+                ->condition('lid', $lid[1])
+                ->condition('language', $lang)
+                ->execute();
+          }
+          else {
+            db_insert('locales_target')
+                ->fields(array(
+                  'lid' => $lid[1],
+                  'translation' => $translation,
+                  'language' => $lang,
+                ))
+                ->execute();
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_preprocess_webform_components_form().
+ *
+ * Adds a translate link to each webform component.
+ */
+function webform_localization_preprocess_webform_components_form(&$variables) {
+  $form = $variables['form'];
+  if (!isset($form['#node']->webform['nid'])) {
+    return;
+  }
+
+  $header = $variables['header'];
+  $rows = $variables['rows'];
+  $node = $form['#node'];
+
+  $enabled_languages = locale_language_list('name');
+
+  $webform_localization_options = webform_localization_get_config($form['#node']->webform['nid']);
+  $row_data = array();
+  if ($webform_localization_options['expose_strings'] && count($enabled_languages) > 1) {
+    // Change colspan of header and footer.
+    $footer = array_pop($rows);
+    $header[6]['colspan'] = 4;
+    $footer['data'][6]['colspan'] = 4;
+    // Add translate link to rows.
+    foreach ($rows as $key => $row) {
+      $row_data[$key] = $row;
+      if (isset($row['data-cid'])) {
+        $name = webform_localization_i18n_string_name($node->nid, $row['data-cid'], '#title');
+        $string_source = i18n_string_get_string($name);
+        if (isset($string_source->lid)) {
+          $row_data[$key]['data'][] = l(t('Translate'), 'admin/config/regional/translate/edit/' . $string_source->lid, array('query' => drupal_get_destination()));
+        }
+      }
+    }
+    $variables['rows'] = $row_data;
+    $variables['rows'][] = $footer;
+    $variables['header'] = $header;
+    $variables['form'] = $form;
+  }
+}
+
+/**
+ * Implements hook_js_alter().
+ */
+function webform_localization_js_alter(&$javascript) {
+  // Only react on webform nodes.
+  if ($node = menu_get_object()) {
+    if (!isset($node->type) || !in_array($node->type, webform_variable_get('webform_node_types'))) {
+      return FALSE;
+    }
+
+    // Only react when we are not on the source node.
+    if ($node->nid != $node->tnid && $node->tnid > 0) {
+      // Gets webform localization options that match this node ID.
+      $webform_localization_options = webform_localization_get_config($node->tnid);
+
+      // Only react when keep a single webform across a translation set.
+      if ($webform_localization_options['single_webform'] > 0) {
+        foreach ($javascript['settings']['data'] as &$setting) {
+          if (isset($setting['webform']['conditionals']['webform-client-form-' . $node->tnid])) {
+            $setting['webform']['conditionals']['webform-client-form-' . $node->nid] = $setting['webform']['conditionals']['webform-client-form-' . $node->tnid];
+            unset($setting['webform']['conditionals']['webform-client-form-' . $node->tnid]);
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_entitycache_node_load().
+ */
+function webform_localization_entitycache_node_load($nodes) {
+  $webform_types = webform_variable_get('webform_node_types');
+  foreach ($nodes as $nid => &$node) {
+    if (in_array($node->type, $webform_types)) {
+      // Gets webform localization options that match this node ID.
+      $wl_options = webform_localization_get_config($nid);
+      if ($wl_options['expose_strings']) {
+        module_load_include('inc', 'webform_localization', 'includes/webform_localization.i18n');
+        // Translate custom strings.
+        webform_localization_translate_strings($node);
+        webform_localization_email_translate_strings($node);
+      }
+    }
+  }
+}

+ 3 - 0
sites/all/modules/contrib/form/webform_phone/webform_phone.components.inc

@@ -198,6 +198,9 @@ function _webform_render_phone($component, $value = NULL, $filter = TRUE) {
     '#weight'           => $component['weight'],
     '#translatable'     => array( 'title', 'description' ),
   );
+  if ($component['required']) {
+    $form_item['#attributes']['required'] = 'required';
+  }
   if (isset( $value )) {
     $form_item['#default_value'] = $value[0];
   }

+ 3 - 3
sites/all/modules/contrib/form/webform_phone/webform_phone.info

@@ -6,9 +6,9 @@ dependencies[] = phone (1.x)
 files[] = webform_phone.module
 core = 7.x
 
-; Information added by Drupal.org packaging script on 2015-01-06
-version = "7.x-1.21"
+; Information added by Drupal.org packaging script on 2015-09-05
+version = "7.x-1.23"
 core = "7.x"
 project = "webform_phone"
-datestamp = "1420529914"
+datestamp = "1441473543"
 

+ 1 - 1
sites/all/modules/contrib/form/webform_phone/webform_phone.module

@@ -31,7 +31,7 @@ function webform_phone_webform_component_info() {
       // If this component can be used as a conditional SOURCE. All components
       // may always be displayed conditionally, regardless of this setting.
       // Defaults to TRUE.
-      'conditional'   => FALSE,
+      'conditional'   => TRUE,
       // If this component allows other components to be grouped within it
       // (like a fieldset or tabs). Defaults to FALSE.
       'group'         => FALSE,

+ 40 - 24
sites/all/modules/contrib/taxonomy/term_merge/README.txt

@@ -6,48 +6,64 @@ by:
 
 Description
 -----------
-When using taxonomy for free tagging purposes, it's easy to end up with
-several terms having the same meaning. This may be due to spelling errors,
-or different users simply making up synonymous terms as they go.
+When using taxonomy for free tagging purposes, it's easy to end up with several
+terms having the same meaning. This may be due to spelling errors, or different
+users simply making up synonymous terms as they go.
 
 You, as an administrator, may then want to correct such errors or unify
-synonymous terms, thereby pruning the taxonomy to a more manageable set.
-This module allows you to merge multiple terms into one, while updating
-all fields referring to those terms to refer to the replacement term instead.
+synonymous terms, thereby pruning the taxonomy to a more manageable set. This
+module allows you to merge multiple terms into one, while updating all fields
+referring to those terms to refer to the replacement term instead.
 
 Currently, the module only acts on:
- * fields of 'taxonomy term reference' type
+ * fields of the following types: taxonomy term reference, entity reference, and
+   other fields that correctly define their foreign keys
  * Views Taxonomy Term filter handlers
  * Redirects
 
-It would be desirable to update other possible
-places where deleted terms are used.
+The term merging may happen in 2 flavors. You can either manually indicate what
+terms should be merged or you can use duplicate suggestion tool for this
+purpose. This tool intends to scan your vocabulary and detect such terms that
+are likely to be duplicates. You will then only review the list of suggested
+duplicates and will schedule for merging only those that actually are
+duplicates. The heuristics through which duplicate tool determines potential
+synonymous terms are made to be extendible by other modules. Refer to Term Merge
+advanced help if you want to write a custom one, though the module itself ships
+with the following heuristics:
+* search by the same name
+* search by the same description
+* search by the same parent
+
+You can indicate which specific heuristics should be used for searching
+duplicates within the UI of duplicate suggestion tool.
 
 Integration
 -------------
 Currently module integrates with the following core and contributed modules:
- * Redirect module (http://drupal.org/project/redirect). During term merging
- you may set up SEO friendly redirects from the branch terms to point to the
- trunk term
- * Synonyms module (http://drupal.org/project/synonyms). During term merging
- you will be able to choose a trunk term's field into which all the branch terms
- will be added as synonyms (until cardinality limit for that field is reached).
+ * Redirect module (http://drupal.org/project/redirect). During term merging you
+   may set up SEO friendly redirects from the branch terms to point to the trunk
+   term.
+ * Synonyms module (http://drupal.org/project/synonyms). During term merging you
+   will be able to choose a trunk term's field into which all the branch terms
+   will be added as synonyms (until cardinality limit for that field is
+   reached).
  * Hierarchical Select (http://drupal.org/project/hierarchical_select). If
- Hierarchical Select module is configured to be used for working with Taxonomy,
- its widget will be shown on the form, where you choose what terms to merge into
- what term.
+   Hierarchical Select module is configured to be used for working with
+   Taxonomy, its widget will be shown on the form, where you choose what terms
+   to merge.
  * Views (http://drupal.org/project/views). If the branch terms are to be
- deleted after the merging process, you could end up having some Views filters
- to filter on no longer existing terms. Term Merge module, while merging terms,
- will update those filters to filter not on the branch term, but on the trunk
- term. This way you will not have senseless filters and will not have to update
- them manually.
+   deleted after the merging process, you could end up having some Views filters
+   to filter on no longer existing terms. Term Merge module, while merging
+   terms, will update those filters to filter not on the branch term, but on the
+   trunk term. This way you will not have senseless filters and will not have to
+   update them manually.
 
 Requirements
 -------------
 The modules requires enabled the following modules:
  * Taxonomy module (ships with Drupal core)
- * Entity API (http://drupal.org/project/entity)
+ * Entity API (https://drupal.org/project/entity)
+ * cTools (https://www.drupal.org/project/ctools)
 
 Installation
 ------------

+ 9 - 0
sites/all/modules/contrib/taxonomy/term_merge/help/term_merge.help.ini

@@ -0,0 +1,9 @@
+[advanced help settings]
+line break = TRUE
+
+[term_merge]
+title = Term Merge
+
+[term_merge_duplicate_suggestion]
+title = Implementing custom duplicate suggestion
+parent = term_merge

File diff suppressed because it is too large
+ 20 - 0
sites/all/modules/contrib/taxonomy/term_merge/help/term_merge.html


+ 16 - 0
sites/all/modules/contrib/taxonomy/term_merge/help/term_merge_duplicate_suggestion.html

@@ -0,0 +1,16 @@
+If you want to define your own custom logic of what terms are to be considered possible duplicates and then to plug this custom logic into all Term Merge machinnery, then this page is for you.
+
+Duplicate detection heuristics are implemented through cTools plugins. If you have not worked with cTools plugins before, we highly recommend you to read documentation about them, such as <a href="&topic:ctools/plugins-implementing&">this one</a>.
+
+Term Merge module defines the cTools plugin type of <em>duplicate_suggestion</em>. This plugin type expects the following properties: <ul>
+    <li><b>title</b>: (string) Human readable translated title of your duplicate suggestion logic. The title will be inserted into the UI of duplicate tool whereafter you can enable it for duplicate detection</li>
+    <li><b>description</b>: (string) [optional] Human readable translated description of how your duplicate suggestion functions. Try including here description of the logic.</li>
+    <li><b>hash callback</b>: (string) Name of a function that hashes terms. Terms with the same hash will be considered possible duplicates. The function will receive the following input arguments: <ol>
+        <li>(object) Taxonomy term object whose hash is requested. The term will not include fields attached to it. They are excluded for performance and scaling considerations.</li>
+    </ol>Your hash function is expected to return string: hash of the provided Taxonomy term.</li>
+    <li><b>weight</b>: (int) [optional] Weight of your duplicate suggestion. On the duplicate tool UI the duplicate suggestions are displayed in the order according to their weights.</li>
+</ul>
+
+Having that said, core of your duplicate suggestion plugin should be the hash function. You can look at the exaples of hash function inside of Term Merge module, just study the <em>term_merge/plugins/duplicate_suggestion/*.inc</em> files. In fact your hash function does not necessarily have to be complicated to yield reasonable duplicate suggestions.
+
+Good luck coding! And if you feel like you have coded a duplicate suggestion that others might benefit from, please, be kind to open an issue against Term Merge module and share your duplicate suggestion there. I thank you beforehand on behalf of all the community!

+ 19 - 0
sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/description.inc

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for duplicate suggestion based on description.
+ */
+
+$plugin = array(
+  'title' => t('Terms description are the same'),
+  'description' => t('Mark terms as duplicates if they have the same description.'),
+  'hash callback' => 'term_merge_duplicate_suggestion_description_hash',
+);
+
+/**
+ * Hash term based on its description.
+ */
+function term_merge_duplicate_suggestion_description_hash($term) {
+  return drupal_strtoupper($term->description);
+}

+ 28 - 0
sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/name.inc

@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for duplicate suggestion based on names.
+ */
+
+$plugin = array(
+  'title' => t('Terms names are the same'),
+  'description' => t('Mark terms as duplicates if they have the same name.'),
+  'hash callback' => 'term_merge_duplicate_suggestion_name_hash',
+  'weight' => -10,
+);
+
+/**
+ * Hash term based on its name.
+ */
+function term_merge_duplicate_suggestion_name_hash($term) {
+  // Making upper case.
+  $name = drupal_strtoupper($term->name);
+  // Trying transliteration, if available.
+  if (module_exists('transliteration')) {
+    $name = transliteration_get($name);
+    // Keeping only ASCII chars.
+    $name = preg_replace('#\W#', '', $name);
+  }
+  return $name;
+}

+ 19 - 0
sites/all/modules/contrib/taxonomy/term_merge/plugins/duplicate_suggestion/parent.inc

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for duplicate suggestion based on parents.
+ */
+
+$plugin = array(
+  'title' => t('Terms parents are the same'),
+  'description' => t('Mark terms as duplicates if they have the same parent.'),
+  'hash callback' => 'term_merge_duplicate_suggestion_parent_hash',
+);
+
+/**
+ * Hash term based on its parent.
+ */
+function term_merge_duplicate_suggestion_parent_hash($term) {
+  return $term->parents[0];
+}

+ 4 - 3
sites/all/modules/contrib/taxonomy/term_merge/term_merge.info

@@ -7,10 +7,11 @@ files[] = term_merge.test
 
 dependencies[] = taxonomy
 dependencies[] = entity
+dependencies[] = ctools
 
-; Information added by Drupal.org packaging script on 2015-01-14
-version = "7.x-1.2"
+; Information added by Drupal.org packaging script on 2015-11-29
+version = "7.x-1.3"
 core = "7.x"
 project = "term_merge"
-datestamp = "1421194982"
+datestamp = "1448826550"
 

+ 126 - 41
sites/all/modules/contrib/taxonomy/term_merge/term_merge.module

@@ -14,16 +14,6 @@
  */
 define('TERM_MERGE_NO_REDIRECT', -1);
 
-/**
- * Constant to use in term merge action.
- *
- * Constant denotes "Create a new term and use it as the term trunk" logic
- * for term merge action.
- *
- * @var int
- */
-define('TERM_MERGE_NEW_TERM_TRUNK', -1);
-
 /**
  * Implements hook_menu().
  */
@@ -149,6 +139,38 @@ function term_merge_help($path, $arg) {
   }
 }
 
+/**
+ * Implements hook_ctools_plugin_type().
+ */
+function term_merge_ctools_plugin_type() {
+  $plugins = array();
+
+  $plugins['duplicate_suggestion'] = array(
+    'defaults' => array(
+      'title' => NULL,
+      'description' => NULL,
+      'hash callback' => NULL,
+      'weight' => 0,
+    ),
+  );
+
+  return $plugins;
+}
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function term_merge_ctools_plugin_directory($owner, $plugin_type) {
+  switch ($owner) {
+    case 'term_merge':
+      switch ($plugin_type) {
+        case 'duplicate_suggestion':
+          return 'plugins/' . $plugin_type;
+      }
+      break;
+  }
+}
+
 /**
  * Access callback for term merge action.
  *
@@ -297,7 +319,14 @@ function term_merge_action($object, $context) {
         if (!isset($term_branch->{$field_name}[$language])) {
           $term_branch->{$field_name}[$language] = array();
         }
-        $term_trunk->{$field_name}[$language] = array_merge($term_trunk->{$field_name}[$language], $term_branch->{$field_name}[$language]);
+        $items = array_merge($term_trunk->{$field_name}[$language], $term_branch->{$field_name}[$language]);
+        $unique_items = array();
+        foreach ($items as $item) {
+          $unique_items[serialize($item)] = $item;
+        }
+        $items = array_values($unique_items);
+
+        $term_trunk->{$field_name}[$language] = $items;
       }
     }
 
@@ -306,36 +335,26 @@ function term_merge_action($object, $context) {
     taxonomy_term_save($term_trunk);
   }
 
-  // Updating all the links to $term_branch to point now to $term_trunk
-  // firstly we go through the list of all fields searching for
-  // taxonomy_term_reference field type because potentially some of these fields
-  // values will have to be updated after merging terms.
-  $fields = field_info_field_map();
   $result = array();
-  foreach ($fields as $field_name => $v) {
-    // Additionally we group by field_name to know what field has to be updated
-    // in each found entity.
-    // @todo: Here would be nice to throw in a hook, allowing other modules to
-    // supply meta data about their field types if they also use taxonomy
-    // references, defining it in their own field types.
-    if ($v['type'] == 'taxonomy_term_reference') {
-      $result[$field_name] = array();
-      $query = new EntityFieldQuery();
-      // Making sure we search in the entire scope of entities.
-      $query->addMetaData('account', user_load(1));
-      $query->fieldCondition($field_name, 'tid', $term_branch->tid);
-      $_result = $query->execute();
-      $result[$field_name] = array_merge_recursive($result[$field_name], $_result);
-    }
+  foreach (term_merge_fields_with_foreign_key('taxonomy_term_data', 'tid') as $field) {
+    $result[$field['field_name']] = array();
+    $query = new EntityFieldQuery();
+    // Making sure we search in the entire scope of entities.
+    $query->addMetaData('account', user_load(1));
+
+    $query->fieldCondition($field['field_name'], $field['term_merge_field_column'], $term_branch->tid);
+    $_result = $query->execute();
+    $result[$field['field_name']]['entities'] = $_result;
+    $result[$field['field_name']]['column'] = $field['term_merge_field_column'];
   }
-  // Now we load all entities that have taxonomy_term_reference pointing to
-  // $term_branch.
-  foreach ($result as $field_name => $entity_types) {
-    foreach ($entity_types as $entity_type => $v) {
+
+  // Now we load all entities that have fields pointing to $term_branch.
+  foreach ($result as $field_name => $field_data) {
+    $column = $field_data['column'];
+    foreach ($field_data['entities'] as $entity_type => $v) {
       $ids = array_keys($v);
       $entities = entity_load($entity_type, $ids);
-      // After we have loaded it, we alter the taxonomy_term_reference
-      // to point to $term_trunk.
+      // After we have loaded it, we alter the field to point to $term_trunk.
       foreach ($entities as $entity) {
         // What is more, we have to do it for every available language.
         foreach ($entity->$field_name as $language => $items) {
@@ -344,7 +363,7 @@ function term_merge_action($object, $context) {
           // 'keep_only_unique'.
           $is_trunk_added = FALSE;
           foreach ($entity->{$field_name}[$language] as $delta => $item) {
-            if ($context['keep_only_unique'] && $is_trunk_added && in_array($item['tid'], array($term_trunk->tid, $term_branch->tid))) {
+            if ($context['keep_only_unique'] && $is_trunk_added && in_array($item[$column], array($term_trunk->tid, $term_branch->tid))) {
               // We are instructed to keep only unique references and we already
               // have term trunk in this field, so we just unset value for this
               // delta.
@@ -353,14 +372,14 @@ function term_merge_action($object, $context) {
             else {
               // Merging term references if necessary, and keep an eye on
               // whether we already have term trunk among this field values.
-              switch ($item['tid']) {
+              switch ($item[$column]) {
                 case $term_trunk->tid:
                   $is_trunk_added = TRUE;
                   break;
 
                 case $term_branch->tid:
                   $is_trunk_added = TRUE;
-                  $entity->{$field_name}[$language][$delta]['tid'] = $term_trunk->tid;
+                  $entity->{$field_name}[$language][$delta][$column] = $term_trunk->tid;
                   break;
               }
             }
@@ -371,6 +390,16 @@ function term_merge_action($object, $context) {
           // numbers, because it is what it is supposed to be.
           $entity->{$field_name}[$language] = array_values($entity->{$field_name}[$language]);
         }
+
+        // Integration with workbench_moderation module. Without this code, if
+        // we save the node for which workbench moderation is enabled, then
+        // it will go from "published" state into "draft". Though in fact we do
+        // not change anything in the node and therefore it should persist in
+        // published state.
+        if (module_exists('workbench_moderation') && $entity_type == 'node') {
+          $entity->workbench_moderation['updating_live_revision'] = TRUE;
+        }
+
         // After updating all the references, save the entity.
         entity_save($entity_type, $entity);
       }
@@ -570,6 +599,28 @@ function term_merge($term_branch, $term_trunk, $merge_settings = array()) {
   batch_set($batch);
 }
 
+/**
+ * Retrieve information about ctools plugin of type 'duplicate suggestion'.
+ *
+ * @param string $id
+ *   Supply here ID of the cTool plugin information about which you want to
+ *   retrieve. You may omit this argument and then information on all duplicate
+ *   suggestion plugins will be returned
+ *
+ * @return array
+ *   Array of information on all available duplicate suggestion plugins or if
+ *   $id was provided, then information on that plugin
+ */
+function term_merge_duplicate_suggestion($id = NULL) {
+  ctools_include('plugins');
+  $plugins = ctools_get_plugins('term_merge', 'duplicate_suggestion', $id);
+  if (!$id) {
+    // Sort the list of plugins by their weight.
+    uasort($plugins, 'drupal_sort_weight');
+  }
+  return $plugins;
+}
+
 /**
  * Generate and return form elements that control behavior of merge action.
  *
@@ -610,7 +661,7 @@ function term_merge_merge_options_elements($vocabulary) {
     $form['merge_fields'] = array(
       '#type' => 'checkboxes',
       '#title' => t('Merge Term Fields'),
-      '#description' => t('Check the fields whose values from branch terms you want to add to the values of corresponding fields of the trunk term. <b>Important note:</b> the values will be added until the cardinality limit for the selected fields is reached.'),
+      '#description' => t('Check the fields whose values from branch terms you want to add to the values of corresponding fields of the trunk term. <b>Important note:</b> the values will be added until the cardinality limit for the selected fields is reached and only unique values for each field will be saved.'),
       '#options' => $options,
     );
   }
@@ -619,6 +670,7 @@ function term_merge_merge_options_elements($vocabulary) {
     '#type' => 'checkbox',
     '#title' => t('Keep only unique terms after merging'),
     '#description' => t('Sometimes after merging you may end up having a node (or any other entity) pointing twice to the same taxonomy term, tick this checkbox if want to keep only unique terms in other entities after merging.'),
+    '#default_value' => TRUE,
   );
 
   if (module_exists('redirect')) {
@@ -709,3 +761,36 @@ function term_merge_merge_options_submit($merge_settings_element, &$form_state,
   );
   return $merge_settings;
 }
+
+/**
+ * Fetch all fields that have a foreign key to provided column.
+ *
+ * @param string $foreign_table
+ *   Name of the table for which to look among foreign keys of all the fields
+ * @param string $foreign_column
+ *   Name of the column for which to look among foreign keys of all the fields
+ *
+ * @return array
+ *   Array of all fields that have the specified table and column within their
+ *   foreign keys. Each of the fields array will be extended to include the
+ *   following additional keys:
+ *   - term_merge_field_column: (string) Name of the field column that holds
+ *     foreign key to the provided table and column
+ */
+function term_merge_fields_with_foreign_key($foreign_table, $foreign_column) {
+  $fields = field_info_fields();
+  $result = array();
+  foreach ($fields as $field_name => $field_info) {
+    foreach ($field_info['foreign keys'] as $foreign_key) {
+      if ($foreign_key['table'] == $foreign_table) {
+        $column = array_search($foreign_column, $foreign_key['columns']);
+        if ($column) {
+          $field_info['term_merge_field_column'] = $column;
+          $result[] = $field_info;
+        }
+      }
+    }
+  }
+
+  return $result;
+}

+ 209 - 188
sites/all/modules/contrib/taxonomy/term_merge/term_merge.pages.inc

@@ -217,34 +217,16 @@ function term_merge_form_submit($form, &$form_state) {
     $form_state['storage']['confirm'] = 0;
     $form_state['rebuild'] = TRUE;
 
-    // Before storing the submitted values we slightly preprocess them to make
-    // sure they correspond to what is expected by submit handler of taxonomy
-    // creation form.
-    if (isset($form_state['values']['relations'])) {
-      $form_state['values'] += $form_state['values']['relations'];
-    }
     $form_state['storage']['info'] = $form_state['values'];
     $form_state['storage']['merge_settings'] = term_merge_merge_options_submit($form, $form_state, $form);
-    $form_state['storage']['old_form'] = $form;
   }
   else {
     // The user has confirmed merging. We pull up the submitted values.
     $form_state['values'] = $form_state['storage']['info'];
 
-    // If necessary, create the term trunk.
-    if ($form_state['values']['term_trunk']['tid'] == TERM_MERGE_NEW_TERM_TRUNK) {
-      // We try to mimic normal form submission for taxonomy module.
-      module_load_include('inc', 'taxonomy', 'taxonomy.admin');
-      taxonomy_form_term_submit($form_state['storage']['old_form']['term_trunk']['term_create'], $form_state);
-      $term_trunk = $form_state['term'];
-    }
-    else {
-      $term_trunk = taxonomy_term_load($form_state['values']['term_trunk']['tid']);
-    }
+    term_merge(array_values($form_state['values']['term_branch']), $form_state['values']['term_trunk']['tid'], $form_state['storage']['merge_settings']);
 
-    term_merge(array_values($form_state['values']['term_branch']), $term_trunk->tid, $form_state['storage']['merge_settings']);
-
-    $form_state['redirect'] = array('taxonomy/term/' . $term_trunk->tid);
+    $form_state['redirect'] = array('taxonomy/term/' . $form_state['values']['term_trunk']['tid']);
   }
 }
 
@@ -283,7 +265,6 @@ function term_merge_form_term_trunk_widget_select(&$form, &$form_state, $vocabul
         unset($options[$child->tid]);
       }
     }
-    $options = array(TERM_MERGE_NEW_TERM_TRUNK => 'New Term') + $options;
   }
   else {
     // Term branch has not been selected yet.
@@ -295,58 +276,7 @@ function term_merge_form_term_trunk_widget_select(&$form, &$form_state, $vocabul
     '#required' => TRUE,
     '#description' => t('Choose into what term you want to merge.'),
     '#options' => $options,
-    '#ajax' => array(
-      'callback' => 'term_merge_form_term_trunk_term_create',
-      'wrapper' => 'term-merge-form-term-trunk-term-create',
-      'method' => 'replace',
-      'effect' => 'fade',
-    ),
-  );
-
-  $form['term_trunk']['term_create'] = array(
-    '#prefix' => '<div id="term-merge-form-term-trunk-term-create">',
-    '#suffix' => '</div>',
   );
-  // We throw in the Taxonomy native term create form only if the option for
-  // creation of a new term was selected by user.
-  if (isset($form_state['values']['term_trunk']['tid']) && $form_state['values']['term_trunk']['tid'] == TERM_MERGE_NEW_TERM_TRUNK) {
-    module_load_include('inc', 'taxonomy', 'taxonomy.admin');
-
-    $form['term_trunk']['term_create'] += array(
-      '#type' => 'fieldset',
-      '#title' => t('Create New Term'),
-    );
-
-    $form['term_trunk']['term_create'] += taxonomy_form_term($form['term_trunk']['term_create'], $form_state, array(), $vocabulary);
-    // We have our own submit button, so we unset the normal one from the term
-    // create form.
-    unset($form['term_trunk']['term_create']['actions']);
-    // Additionally we have to filter out from "Parent Terms" select the already
-    // selected branch terms and their children, because we can't merge into
-    // the term itself or its children.
-    // We do a trick here, since we know the 1st element is the <root> option
-    // and all others are normal taxonomy terms, we keep the 1st element as it
-    // is while all the other elements we substitute with our $options array
-    // which is basically identical but already has been filtered out unwanted
-    // terms. Plus we have to unset the 'New Term' option from $options.
-    unset($options[TERM_MERGE_NEW_TERM_TRUNK]);
-    if (is_array($form['term_trunk']['term_create']['relations']['parent']['#options'])) {
-      $form['term_trunk']['term_create']['relations']['parent']['#options'] = array_slice($form['term_trunk']['term_create']['relations']['parent']['#options'], 0, 1, TRUE) + $options;
-    }
-
-    // For each field attached to taxonomy term of this vocabulary that has
-    // unlimited cardinality we have to extra process the results, otherwise
-    // "Add another item" button doesn't work.
-    $instances = field_info_instances($form['term_trunk']['term_create']['#entity_type'], $form['term_trunk']['term_create']['#bundle']);
-    foreach ($instances as $instance) {
-      $field = field_info_field($instance['field_name']);
-      if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
-        if (isset($form['term_trunk']['term_create'][$field['field_name']][LANGUAGE_NONE]['add_more']['#limit_validation_errors'])) {
-          $form['term_trunk']['term_create'][$field['field_name']][LANGUAGE_NONE]['add_more']['#limit_validation_errors'] = array(array('term_branch'), array('term_trunk'));
-        }
-      }
-    }
-  }
 }
 
 /**
@@ -504,16 +434,6 @@ function term_merge_form_term_trunk($form, $form_state) {
   return $form['term_trunk'];
 }
 
-/**
- * Ajax callback function.
- *
- * Used in term_merge_term_merge_form() to replace the term create fieldset
- * depending on already selected term_branch values and the term_trunk value.
- */
-function term_merge_form_term_trunk_term_create($form, $form_state) {
-  return $form['term_trunk']['term_create'];
-}
-
 /**
  * Generate 'term_merge_duplicates_form'.
  *
@@ -521,16 +441,15 @@ function term_merge_form_term_trunk_term_create($form, $form_state) {
  *
  * @param object $vocabulary
  *   Fully loaded taxonomy vocabulary object inside of which term merging
- *   occurs, if this argument is omitted, then $term is required and will be
- *   used to obtain information about Taxonomy vocabulary
+ *   occurs, if this argument is omitted, then $parent_term is required and will
+ *   be used to obtain information about Taxonomy vocabulary
  * @param object $parent_term
  *   Fully loaded taxonomy term object using which the function will pull up
  *   the vocabulary inside of which term merging occurs. Duplicate terms will be
  *   sought only among children of this term
  */
 function term_merge_duplicates_form($form, &$form_state, $vocabulary = NULL, $parent_term = NULL) {
-  // TODO: make this JavaScript #attached.
-  drupal_add_js(drupal_get_path('module', 'term_merge') . '/js/duplicate.form.js');
+  $form['#attached']['js'][drupal_get_path('module', 'term_merge') . '/js/duplicate.form.js'] = array();
 
   // Checking if we were not given vocabulary object, we will use term object to
   // obtain the former.
@@ -546,34 +465,61 @@ function term_merge_duplicates_form($form, &$form_state, $vocabulary = NULL, $pa
     '#markup' => '<p>' . t('Here you can merge terms with the same names. It is a useful tool against term-duplicates. If this tool is invoked on a term (not on the entire vocabulary), duplicate terms will be sought only among children of that term. The terms are grouped by names. Term into which the merging will occur is selected manually by user, however you must know that it is impossible to merge a parent term into any of its children.') . '</p>',
   );
 
-  $form['scaling'] = array(
+  $form['settings'] = array(
     '#type' => 'fieldset',
-    '#title' => t('Scaling for large vocabularies'),
-    '#description' => t('Adjust these settings if your vocabulary is very large.'),
+    '#title' => t('Advanced settings'),
+    '#description' => t('Fine tune the duplicate search tool. You can adjust these settings if your vocabulary is very large. Also, you can control within these settings how the potential duplicates are presented below.'),
     '#tree' => TRUE,
     '#collapsible' => TRUE,
   );
 
-  $form['scaling']['help'] = array(
+  $form['settings']['help'] = array(
     '#markup' => '<p>' . format_plural(count($tree), 'Vocabulary %vocabulary has only 1 term. It is very unlikely you will merge anything here.', 'Vocabulary %vocabulary has @count terms. If this tool works slow, you may instruct the duplicate finder tool to terminate its work after it has found a specific number of possible duplicates.', array(
       '%vocabulary' => $vocabulary->name,
     )) . '</p>',
   );
 
-  $form['scaling']['max_duplicates'] = array(
+  $form['settings']['max_duplicates'] = array(
     '#type' => 'textfield',
     '#title' => t('Show N duplicates'),
-    '#description' => t('Input an integer here - this many duplicates will be show on the form. Once this amount of possible duplicates is found, the search process terminates.'),
+    '#description' => t('Input an integer here - this many duplicates will be shown on the form. Once this amount of possible duplicates is found, the search process terminates.'),
     '#required' => TRUE,
-    '#default_value' => isset($form_state['values']['scaling']['max_duplicates']) ? $form_state['values']['scaling']['max_duplicates'] : 300,
+    '#default_value' => isset($form_state['values']['settings']['max_duplicates']) ? $form_state['values']['settings']['max_duplicates'] : 300,
     '#element_validate' => array('element_validate_integer_positive'),
   );
 
-  $form['scaling']['update'] = array(
+  $options = array();
+  foreach (term_merge_duplicate_suggestion() as $plugin) {
+    $options[$plugin['name']] = $plugin['title'];
+  }
+  $form['settings']['duplicate_suggestion'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Mark terms as duplicate if all the checked conditions stand true'),
+    '#options' => $options,
+    '#default_value' => isset($form_state['values']['settings']['duplicate_suggestion']) ? $form_state['values']['settings']['duplicate_suggestion'] : array('name'),
+  );
+
+  $options = array();
+  $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
+  foreach (field_info_instances('taxonomy_term', $bundle) as $instance) {
+    $options[$instance['field_name']] = $instance['label'];
+  }
+
+  if (!empty($options)) {
+    $form['settings']['fields'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Display fields'),
+      '#description' => t('Check which fields you wish to display in the results below for each possible duplicate term.'),
+      '#options' => $options,
+      '#default_value' => isset($form_state['values']['settings']['fields']) ? array_values(array_filter($form_state['values']['settings']['fields'])) : array(),
+    );
+  }
+
+  $form['settings']['update'] = array(
     '#type' => 'button',
     '#value' => t('Re-run duplicate search'),
     '#ajax' => array(
-      'callback' => 'term_merge_duplicates_form_scaling',
+      'callback' => 'term_merge_duplicates_form_settings',
       'wrapper' => 'term-merge-duplicate-wrapper',
       'method' => 'replace',
       'effect' => 'fade',
@@ -588,24 +534,31 @@ function term_merge_duplicates_form($form, &$form_state, $vocabulary = NULL, $pa
   $groups = array();
 
   foreach ($tree as $term) {
-    if ($count >= $form['scaling']['max_duplicates']['#default_value']) {
+    if ($count >= $form['settings']['max_duplicates']['#default_value']) {
       // We have reached the limit of possible duplicates to be found.
       break;
     }
-    $name = term_merge_duplicates_process_name($term->name);
-    if (!isset($groups[$name])) {
-      $groups[$name] = array();
+    $hash = '';
+    foreach ($form['settings']['duplicate_suggestion']['#default_value'] as $duplicate_suggestion) {
+      $duplicate_suggestion = term_merge_duplicate_suggestion($duplicate_suggestion);
+      $function = ctools_plugin_get_function($duplicate_suggestion, 'hash callback');
+      if ($function) {
+        $hash .= $function($term);
+      }
+    }
+    if (!isset($groups[$hash])) {
+      $groups[$hash] = array();
     }
     else {
       // We increment count by one for the just encountered duplicate. Plus, if
       // it is the second duplicate in this group, we also increment it by one
       // for the 1st duplicate in the group.
       $count++;
-      if (count($groups[$name]) == 1) {
+      if (count($groups[$hash]) == 1) {
         $count++;
       }
     }
-    $groups[$name][$term->tid] = $term;
+    $groups[$hash][$term->tid] = $term;
   }
 
   $form['wrapper'] = array(
@@ -628,72 +581,156 @@ function term_merge_duplicates_form($form, &$form_state, $vocabulary = NULL, $pa
     '#tree' => TRUE,
   );
 
-  foreach ($groups as $i => $group) {
-    if (count($group) > 1) {
-      // Sorting terms by tid for better usage experience.
-      ksort($group);
+  $groups = array_filter($groups, 'term_merge_duplicates_form_filter');
 
-      $first_term = reset($group);
+  $tids = array();
+  foreach ($groups as $group) {
+    $tids = array_merge($tids, array_keys($group));
+  }
 
-      $options = array();
-      foreach ($group as $term) {
-        $parents = array();
-        // Adding Root to the hierarchy.
-        $parents[] = t('Vocabulary Root');
-        foreach (taxonomy_get_parents_all($term->tid) as $parent) {
-          // We do not include the current term in the hierarchy.
-          if ($parent->tid != $term->tid) {
-            $parents[] = $parent->name;
-          }
-        }
-        $language = isset($term->language) ? $term->language : LANGUAGE_NONE;
-        if ($language == LANGUAGE_NONE) {
-          $language = t('Not Specified');
+  // This array will be keyed by term tid and values will be counts of how many
+  // other entities reference to this term through values of fields attached to
+  // them.
+  $terms_count = array_fill_keys($tids, 0);
+  if (!empty($tids)) {
+    foreach (term_merge_fields_with_foreign_key('taxonomy_term_data', 'tid') as $referencing_field) {
+      if ($referencing_field['storage']['type'] == 'field_sql_storage') {
+        $table = array_keys($referencing_field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+        $table = reset($table);
+        $column = $referencing_field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table][$referencing_field['term_merge_field_column']];
+        $select = db_select($table, 'reference')
+          ->condition($column, $tids);
+        $select->addField('reference', $column, 'tid');
+        $select->addExpression('COUNT(1)', 'count');
+        $select->groupBy($column);
+        $select = $select->execute();
+        foreach ($select as $row) {
+          $terms_count[$row->tid] += $row->count;
         }
-
-        $options[$term->tid] = array(
-          'id' => $term->tid,
-          'title' => l($term->name, 'taxonomy/term/' . $term->tid),
-          'language' => $language,
-          'description' => check_markup($term->description, $term->format),
-          'parents' => implode(' &raquo; ', $parents),
-        );
       }
+    }
+  }
 
-      $form['wrapper']['group'][$i] = array(
-        '#type' => 'fieldset',
-        '#title' => check_plain($first_term->name),
-        '#collapsible' => TRUE,
-        '#pre_render' => array('term_merge_duplicates_fieldset_preprocess'),
-        '#element_validate' => array('term_merge_duplicates_fieldset_validate'),
-      );
+  if (!empty($form['settings']['fields']['#default_value'])) {
+    // We need to load full term entities, because we are requested to show
+    // fields.
+    $terms = taxonomy_term_load_multiple($tids);
+    foreach ($groups as $i => $group) {
+      $groups[$i] = array_intersect_key($terms, $group);
+    }
+  }
 
-      $form['wrapper']['group'][$i]['duplicates'] = array(
-        '#type' => 'tableselect',
-        '#title' => 'Duplicates',
-        '#header' => array(
-          'id' => t('ID'),
-          'title' => t('Title'),
-          'description' => t('Description'),
-          'language' => t('Language'),
-          'parents' => t('Parents'),
-        ),
-        '#options' => $options,
-      );
+  foreach ($groups as $i => $group) {
+    // Sorting terms by tid for better usage experience.
+    ksort($group);
 
-      $options = array();
-      foreach ($group as $term) {
-        $options[$term->tid] = $term->name;
+    $first_term = reset($group);
+
+    $options = array();
+    foreach ($group as $term) {
+      $parents = array();
+      // Adding Root to the hierarchy.
+      $parents[] = t('Vocabulary Root');
+      foreach (taxonomy_get_parents_all($term->tid) as $parent) {
+        // We do not include the current term in the hierarchy.
+        if ($parent->tid != $term->tid) {
+          $parents[] = $parent->name;
+        }
       }
-      $form['wrapper']['group'][$i]['trunk_tid'] = array(
-        '#type' => 'radios',
-        '#title' => t('Merge Into'),
-        '#options' => $options,
-        '#attributes' => array(
-          'class' => array('term-merge-duplicate-trunk'),
-        ),
+      $language = isset($term->language) ? $term->language : LANGUAGE_NONE;
+      if ($language == LANGUAGE_NONE) {
+        $language = t('Not Specified');
+      }
+
+      $options[$term->tid] = array(
+        'id' => $term->tid,
+        'title' => l($term->name, 'taxonomy/term/' . $term->tid),
+        'language' => $language,
+        'description' => check_markup($term->description, $term->format),
+        'parents' => implode(' &raquo; ', $parents),
+        'count' => format_plural($terms_count[$term->tid], '@count time', '@count times'),
       );
+
+      if (isset($form['settings']['fields'])) {
+        foreach ($form['settings']['fields']['#default_value'] as $instance) {
+          $field = field_info_field($instance);
+          $items = field_get_items('taxonomy_term', $term, $field['field_name']);
+          $options[$term->tid][$field['field_name']] = '';
+          if (is_array($items)) {
+            $options[$term->tid][$field['field_name']] = array(
+              '#theme' => 'item_list',
+              '#items' => array(),
+            );
+            foreach ($items as $item) {
+              switch ($field['type']) {
+                case 'image':
+                  $display = array();
+                  $image_style = image_style_load('thumbnail');
+                  if ($image_style) {
+                    $cache = _field_info_field_cache();
+                    $display = $cache->prepareInstanceDisplay($display, $field['type']);
+                    $display['settings']['image_style'] = $image_style['name'];
+                  }
+                  $rendered_item = drupal_render(field_view_value('taxonomy_term', $term, $field['field_name'], $item, $display));
+                  break;
+
+                default:
+                  $rendered_item = drupal_render(field_view_value('taxonomy_term', $term, $field['field_name'], $item));
+                  break;
+              }
+              $options[$term->tid][$field['field_name']]['#items'][] = $rendered_item;
+            }
+            if (count($options[$term->tid][$field['field_name']]['#items']) > 1) {
+              $options[$term->tid][$field['field_name']] = drupal_render($options[$term->tid][$field['field_name']]);
+            }
+            else {
+              $options[$term->tid][$field['field_name']] = $options[$term->tid][$field['field_name']]['#items'][0];
+            }
+          }
+        }
+      }
+    }
+
+    $form['wrapper']['group'][$i] = array(
+      '#type' => 'fieldset',
+      '#title' => check_plain($first_term->name),
+      '#collapsible' => TRUE,
+      '#pre_render' => array('term_merge_duplicates_fieldset_preprocess'),
+      '#element_validate' => array('term_merge_duplicates_fieldset_validate'),
+    );
+
+    $header = array(
+      'id' => t('ID'),
+      'title' => t('Title'),
+      'description' => t('Description'),
+      'language' => t('Language'),
+      'parents' => t('Parents'),
+      'count' => t('Referenced'),
+    );
+
+    if (isset($form['settings']['fields'])) {
+      $header += array_map('check_plain', array_intersect_key($form['settings']['fields']['#options'], drupal_map_assoc($form['settings']['fields']['#default_value'])));
+    }
+
+    $form['wrapper']['group'][$i]['duplicates'] = array(
+      '#type' => 'tableselect',
+      '#title' => 'Duplicates',
+      '#header' => $header,
+      '#options' => $options,
+    );
+
+    $options = array();
+    foreach ($group as $term) {
+      $options[$term->tid] = $term->name;
     }
+    $form['wrapper']['group'][$i]['trunk_tid'] = array(
+      '#type' => 'radios',
+      '#title' => t('Merge Into'),
+      '#options' => $options,
+      '#attributes' => array(
+        'class' => array('term-merge-duplicate-trunk'),
+      ),
+    );
   }
 
   if ($count > 0) {
@@ -770,31 +807,6 @@ function term_merge_duplicates_form_submit($form, &$form_state) {
   }
 }
 
-/**
- * String process function.
- *
- * Manipulate supplied var $name and by the output of this function terms in a
- * vocabulary are grouped as duplicates.
- *
- * @param string $name
- *   String that needs to be manipulated
- *
- * @return string
- *   Processed string (normally it implies making it upper case, stripping down
- *   any special chars, etc.)
- */
-function term_merge_duplicates_process_name($name) {
-  // Making upper case.
-  $name = drupal_strtoupper($name);
-  // Trying transliteration, if available.
-  if (module_exists('transliteration')) {
-    $name = transliteration_get($name);
-    // Keeping only ASCII chars.
-    $name = preg_replace('#\W#', '', $name);
-  }
-  return $name;
-}
-
 /**
  * Form element preprocess function.
  *
@@ -806,13 +818,13 @@ function term_merge_duplicates_fieldset_preprocess($element) {
   foreach ($options as $tid => $row) {
     $element['trunk_tid'][$tid]['#title_display'] = 'invisible';
     $options[$tid] = array(
-      'trunk' => drupal_render($element['trunk_tid'][$tid]),
-    ) + $options[$tid];
+        'trunk' => drupal_render($element['trunk_tid'][$tid]),
+      ) + $options[$tid];
   }
   $element['trunk_tid']['#title_display'] = 'invisible';
   $element['duplicates']['#header'] = array(
-    'trunk' => $element['trunk_tid']['#title'],
-  ) + $element['duplicates']['#header'];
+      'trunk' => $element['trunk_tid']['#title'],
+    ) + $element['duplicates']['#header'];
 
   return $element;
 }
@@ -837,8 +849,17 @@ function term_merge_duplicates_fieldset_validate($element, &$form_state, $form)
  * Ajax callback function.
  *
  * Used in term_merge_duplicates_form() to replace the duplicates tables with
- * new data per current scaling settings.
+ * new data per current settings.
  */
-function term_merge_duplicates_form_scaling($form, &$form_state) {
+function term_merge_duplicates_form_settings($form, &$form_state) {
   return $form['wrapper'];
 }
+
+/**
+ * Supportive array_filter() callback function.
+ *
+ * Eliminate all array elements, whose dimension is less than 1.
+ */
+function term_merge_duplicates_form_filter($array_element) {
+  return count($array_element) > 1;
+}

+ 279 - 172
sites/all/modules/contrib/taxonomy/term_merge/term_merge.test

@@ -89,7 +89,7 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * GetInfo method.
    */
-  public function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Term Merge',
       'description' => 'Ensure that the module Term Merge works correctly.',
@@ -176,6 +176,7 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
       'term_merge_test_single' => 1,
       'term_merge_test_unlimited' => FIELD_CARDINALITY_UNLIMITED,
       'term_merge_do_not_merge' => 10,
+      'term_merge_not_unique' => FIELD_CARDINALITY_UNLIMITED,
     );
 
     foreach ($fields_map as $field_name => $cardinality) {
@@ -191,8 +192,7 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
         'field_name' => $field_name,
         'entity_type' => 'taxonomy_term',
         'bundle' => $bundle,
-        'label' => $this->randomName(),
-        'description' => $this->randomName(),
+        'label' => $field_name,
       ));
     }
 
@@ -202,31 +202,32 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
     );
 
     foreach ($terms as $term_type => $tmp) {
-      $url = 'admin/structure/taxonomy/vocabulary/add';
-      $name = $this->randomName();
-      $edit = array(
-        'name' => $name,
+      $term = (object) array(
+        'vid' => $this->vocabulary->vid,
+        'name' => $this->randomName(),
       );
 
       foreach ($fields_map as $field_name => $cardinality) {
         switch ($field_name) {
           case 'term_merge_test_single':
-            $edit[$field_name . '[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
+            $term->{$field_name}[LANGUAGE_NONE][0]['value'] = $this->randomName();
             break;
 
           case 'term_merge_test_unlimited':
           case 'term_merge_do_not_merge':
             $count = rand(1, 3);
             for ($i = 0; $i < $count; $i++) {
-              $edit[$field_name . '[' . LANGUAGE_NONE . '][' . $i . '][value]'] = $this->randomName();
-              $this->drupalPost($url, $edit, 'Add another item');
-              $url = NULL;
+              $term->{$field_name}[LANGUAGE_NONE][$i]['value'] = $this->randomName();
             }
             break;
+
+          case 'term_merge_not_unique':
+            $term->{$field_name}[LANGUAGE_NONE][0]['value'] = 'term_merge_not_unique_value';
+            break;
         }
       }
 
-      $this->drupalPost($url, $edit, 'Save');
+      taxonomy_term_save($term);
       $terms[$term_type] = $this->getLastTerm($this->vocabulary);
     }
 
@@ -241,15 +242,23 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
     $this->drupalGet('taxonomy/term/' . $terms['trunk']->tid);
     foreach ($fields_map as $field_name => $cardinality) {
       foreach (field_get_items('taxonomy_term', $terms['branch'], $field_name) as $item) {
-        $this->assertNoText($item['value'], 'Values of field ' . $field_name . ' have not been added to the trunk term with disabled "merge_fields" option.');
+        if ($field_name != 'term_merge_not_unique') {
+          $this->assertNoText($item['value'], 'Values of field ' . $field_name . ' have not been added to the trunk term with disabled "merge_fields" option.');
+        }
       }
     }
 
-    // Now we try merging with merging 2 of 3 fields. The values of the branch
-    // term should be added to the trunk term's values only in those 2 fields.
+    // Now we try merging with merging fields. The values of the branch term
+    // should be added to the trunk term's values only in where we asked them
+    // to be added. Moreover, only unique values are to be kept in each of the
+    // merged fields.
     actions_do('term_merge_action', $terms['branch'], array(
       'term_trunk' => $terms['trunk']->tid,
-      'merge_fields' => array('term_merge_test_single', 'term_merge_test_unlimited'),
+      'merge_fields' => array(
+        'term_merge_test_single',
+        'term_merge_test_unlimited',
+        'term_merge_not_unique',
+      ),
       'term_branch_keep' => TRUE,
     ));
 
@@ -267,6 +276,13 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
           }
           break;
 
+        case 'term_merge_not_unique':
+          // Make sure only the unique values in merged field are kept.
+          foreach (field_get_items('taxonomy_term', $terms['trunk'], $field_name) as $item) {
+            $this->assertUniqueText($item['value'], 'Only unique field values are kept in the trunk term field after merging terms with enabled "merge_fields" option.');
+          }
+          break;
+
         case 'term_merge_test_unlimited':
           // Make sure values of fields that are instructed to be added to trunk
           // term's values are actually added.
@@ -517,7 +533,6 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
       'taxonomy_vocabulary_tab',
       'taxonomy_term_tab',
       'via_term_trunk_widget_select',
-      'via_term_trunk_widget_select_creating_new_term_trunk',
       'via_term_trunk_widget_autocomplete',
       'merge_fields',
       'do_not_merge_fields',
@@ -602,26 +617,6 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
           $this->assertIdentical(FALSE, $str_pos, 'Child is not available as option for term trunk if its parent is chosen among term branches.');
           $str_pos = strpos($term_trunk_options, $terms['parent']->name);
           $this->assertIdentical(FALSE, $str_pos, 'Selected branch term is not available as an option for term trunk.');
-          // At the same time asserting if we choose "New Term" option, among
-          // the available parents for the new term there are no children of the
-          // selected branch terms, nor the branch terms themselves.
-          $this->drupalPostAJAX(NULL, array(
-            'term_branch[]' => array($terms['parent']->tid),
-            'term_trunk[widget]' => $term_trunk_widget,
-            'term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK,
-          ), 'term_trunk[tid]');
-          $matches = array();
-          preg_match('#\<select[^>]+name="relations\[parent\]\[\]"[^>]*\>.+?\</select\>#si', $this->content, $matches);
-          $new_term_parent_options = $matches[0];
-          $str_pos = strpos($new_term_parent_options, $terms['child']->name);
-          $this->assertIdentical(FALSE, $str_pos, 'Child is not available as option for parent term for a new term trunk if its parent is chosen among term branches.');
-          $str_pos = strpos($new_term_parent_options, $terms['parent']->name);
-          $this->assertIdentical(FALSE, $str_pos, 'Selected branch term is not available as an option for parent term for a new term trunk.');
-          break;
-
-        case 'via_term_trunk_widget_select_creating_new_term_trunk':
-          $init_url = 'taxonomy/term/' . $terms['parent']->tid . '/merge';
-          $term_trunk_widget = 'select';
           break;
 
         case 'via_term_trunk_widget_autocomplete':
@@ -706,32 +701,6 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
       switch ($term_trunk_widget) {
         case 'select':
           $term_trunk_edit += array('term_trunk[tid]' => $terms['term_trunk']->tid);
-          if ($case == 'via_term_trunk_widget_select_creating_new_term_trunk') {
-            // This is special case, we are gonna create a new term trunk using
-            // taxonomy create term embedded form.
-            $term_trunk_edit = array('term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK) + $term_trunk_edit;
-            $this->drupalPostAJAX(NULL, array(
-              'term_branch[]' => $term_branches_edit,
-              'term_trunk[widget]' => $term_trunk_widget,
-            ) + $term_trunk_edit, 'term_trunk[tid]');
-
-            // Adding another delta for text field. This way we make sure Field
-            // API gets embedded into our form without errors.
-            $this->drupalPostAJAX(NULL, array(
-              'term_branch[]' => $term_branches_edit,
-              'term_trunk[widget]' => $term_trunk_widget,
-            ) + $term_trunk_edit, array('field_test_text_add_more' => 'Add another item'));
-            // We store into $term_trunk_edit array the info about the just
-            // created new trunk term for further assertions down below in the
-            // code.
-            $term_trunk_edit += array(
-              'name' => $this->randomName(),
-              'description[value]' => $this->randomName(),
-              'field_test_text[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(),
-              'field_test_text[' . LANGUAGE_NONE . '][1][value]' => $this->randomName(),
-              'relations[parent][]' => array($terms['term_trunk_parent']->tid),
-            );
-          }
           break;
 
         case 'autocomplete':
@@ -761,23 +730,6 @@ class TermMergeTermMergeWebTestCase extends TermMergeWebTestCase {
 
       // Adding any extra text assertions on per test-case basis.
       switch ($case) {
-        case 'via_term_trunk_widget_select_creating_new_term_trunk':
-          $term_trunk = $this->getLastTerm($this->vocabulary);
-
-          // Making sure the parent property of the just created trunk term is
-          // correct.
-          $parents = taxonomy_get_parents_all($term_trunk->tid);
-          $this->assertTrue(count($parents) == 2 && $parents[0]->tid == $term_trunk->tid && $parents[1]->tid == $terms['term_trunk_parent']->tid, 'Parent property of the just created new term trunk is correct.');
-
-          // Adding the submitted field values for further assertions too. This
-          // way we test whether the embedded form adds a new term along with
-          // any fields we have submitted into it.
-          $text_assertions['New Term name'] = $term_trunk_edit['name'];
-          $text_assertions['New Term description'] = $term_trunk_edit['description[value]'];
-          $text_assertions['New Term text field delta #0'] = $term_trunk_edit['field_test_text[' . LANGUAGE_NONE . '][0][value]'];
-          $text_assertions['New Term text field delta #1'] = $term_trunk_edit['field_test_text[' . LANGUAGE_NONE . '][1][value]'];
-          break;
-
         case 'merge_fields':
           // Making sure the term trunk has been merged all the fields from term
           // branches into itself.
@@ -841,7 +793,7 @@ class DuplicatesTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * GetInfo method.
    */
-  public function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Duplicate terms merge',
       'description' => 'Ensure that the feature <i>merge duplicate terms</i> of module Term Merge works correctly.',
@@ -901,6 +853,35 @@ class DuplicatesTermMergeWebTestCase extends TermMergeWebTestCase {
     $groups['triple_different_parent'][2]->parent = $groups['parent'][0]->tid;
     taxonomy_term_save($groups['triple_different_parent'][2]);
 
+    // Test duplicate suggestion plugin type. Make sure multiple duplicated
+    // suggestions are properly handed and make sure each of the duplicate
+    // suggestions does its function.
+    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/merge/duplicates');
+    $this->assertSuggestedDuplicates(array_merge($groups['triple_different_parent'], $groups['random']), 'Filtering only by term names yields expected results.');
+
+    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/merge/duplicates', array(
+      'settings[duplicate_suggestion][name]' => FALSE,
+      'settings[duplicate_suggestion][description]' => TRUE,
+    ), 'Re-run duplicate search');
+    $this->assertSuggestedDuplicates(array_merge($groups['triple_different_parent'], $groups['random']), 'Filtering only by term description yields expected results.');
+
+    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/merge/duplicates', array(
+      'settings[duplicate_suggestion][name]' => FALSE,
+      'settings[duplicate_suggestion][parent]' => TRUE,
+    ), 'Re-run duplicate search');
+    $expected_terms = array();
+    $expected_terms = array_merge($expected_terms, $groups['single'], $groups['random'], $groups['parent']);
+    $expected_terms[] = $groups['triple_different_parent'][0];
+    $this->assertSuggestedDuplicates($expected_terms, 'Filtering only by term parent yields expected results.');
+
+    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/merge/duplicates', array(
+      'settings[duplicate_suggestion][name]' => TRUE,
+      'settings[duplicate_suggestion][parent]' => TRUE,
+    ), 'Re-run duplicate search');
+    $expected_terms = $groups['triple_different_parent'];
+    unset($expected_terms[0]);
+    $this->assertSuggestedDuplicates($expected_terms, 'Filtering by term name and parent yields expected results, i.e. duplicate suggestions can be combined.');
+
     // Assuring the single term is not listed as duplicate.
     $this->drupaLGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/merge/duplicates');
     $this->assertNoText($groups['single'][0]->name, 'Single term is not listed as a duplicate.');
@@ -908,23 +889,23 @@ class DuplicatesTermMergeWebTestCase extends TermMergeWebTestCase {
     // Making sure the term in 'triple_different_parent' that does not have a
     // parent, is not listed when we invoke duplicate tool on a parent term.
     $this->drupalGet('taxonomy/term/' . $groups['parent'][0]->tid . '/merge/duplicates');
-    $this->assertNoFieldByName('group[' . $this->duplicateProcessName('triple_different_parent') . '][duplicates][' . $groups['triple_different_parent'][0]->tid . ']', 'Duplicate term is not listed when it is not among children of a term, on which Term Merge module was invoked.');
+    $this->assertNoFieldByName('group[' . $this->duplicateHashTerm($groups['triple_different_parent'][0]) . '][duplicates][' . $groups['triple_different_parent'][0]->tid . ']', 'Duplicate term is not listed when it is not among children of a term, on which Term Merge module was invoked.');
 
     $edit = array();
     // Trying to merge a term into another, invoking Duplicate tool on a parent
     // term of both. Important note: we do not test merging options, because
     // supposedly those are tested in the main test of this module.
-    $edit['group[' . $this->duplicateProcessName('triple_different_parent') . '][trunk_tid]'] = $groups['triple_different_parent'][1]->tid;
-    $edit['group[' . $this->duplicateProcessName('triple_different_parent') . '][duplicates][' . $groups['triple_different_parent'][2]->tid . ']'] = TRUE;
+    $edit['group[' . $this->duplicateHashTerm($groups['triple_different_parent'][1]) . '][trunk_tid]'] = $groups['triple_different_parent'][1]->tid;
+    $edit['group[' . $this->duplicateHashTerm($groups['triple_different_parent'][2]) . '][duplicates][' . $groups['triple_different_parent'][2]->tid . ']'] = TRUE;
     $groups['triple_different_parent'][2]->merged = TRUE;
     $this->drupalPost('taxonomy/term/' . $groups['parent'][0]->tid . '/merge/duplicates', $edit, 'Submit');
 
     //  Trying to merge multiple terms. We merge all but the 1st term.
     $edit = array();
-    $edit['group[' . $this->duplicateProcessName('random') . '][trunk_tid]'] = $groups['random'][0]->tid;
+    $edit['group[' . $this->duplicateHashTerm($groups['random'][0]) . '][trunk_tid]'] = $groups['random'][0]->tid;
     foreach ($groups['random'] as $k => $term) {
       if ($k != 0) {
-        $edit['group[' . $this->duplicateProcessName('random') . '][duplicates][' . $term->tid . ']'] = TRUE;
+        $edit['group[' . $this->duplicateHashTerm($groups['random'][$k]) . '][duplicates][' . $term->tid . ']'] = TRUE;
         $groups['random'][$k]->merged = TRUE;
       }
     }
@@ -956,19 +937,29 @@ class DuplicatesTermMergeWebTestCase extends TermMergeWebTestCase {
    *   Array of fully loaded taxonomy terms objects of the just created terms,
    *   grouped by their group name
    */
-  private function createTerms($groups) {
+  protected function createTerms($groups) {
     foreach ($groups as $name => $quantity) {
       $groups[$name] = array();
+      $description = $this->randomName();
       for ($i = 0; $i < $quantity; $i++) {
         $term_name = '';
+        $term_description = '';
         // Randomizing case of the group name.
         foreach (str_split($name) as $symbol) {
           $symbol = rand(0, 1) ? drupal_strtoupper($symbol) : drupal_strtolower($symbol);
           $term_name .= $symbol;
         }
-        $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/add', array(
+        // Getting description in different cases.
+        foreach (str_split($description) as $symbol) {
+          $symbol = rand(0, 1) ? drupal_strtoupper($symbol) : drupal_strtolower($symbol);
+          $term_description .= $symbol;
+        }
+        $term = (object) array(
+          'vid' => $this->vocabulary->vid,
           'name' => $term_name,
-        ), 'Save');
+          'description' => $description,
+        );
+        taxonomy_term_save($term);
         $groups[$name][] = $this->getLastTerm($this->vocabulary);
       }
     }
@@ -978,28 +969,61 @@ class DuplicatesTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * Supportive method.
    *
-   * Manipulate supplied var $name and by the output of this function terms in a
-   * vocabulary are grouped as duplicates. This method should be identical to
-   * the function term_merge_duplicates_process_name(), test case replies on the
-   * fact that both manipulate the string in the identical way.
+   * Calculate hash a term based on which it will be paired with other terms as
+   * possible duplicates of each other.
    *
-   * @param string $name
-   *   String that needs to be manipulated
+   * @param object $term
+   *   Term whose duplicate suggestion hash is to be calculated
+   * @param array $duplicate_suggestions
+   *   Array of duplicate suggestion names that to apply, when determining hash
+   *   of the provided term
    *
    * @return string
-   *   Processed string (normally it implies making it upper case, stripping
-   *   down any special chars, etc.)
+   *   Hash of the provided term according to enabled duplicate suggestions
    */
-  private function duplicateProcessName($name) {
-    // Making upper case.
-    $name = drupal_strtoupper($name);
-    // Trying transliteration, if available.
-    if (module_exists('transliteration')) {
-      $name = transliteration_get($name);
-      // Keeping only ASCII chars.
-      $name = preg_replace('#\W#', '', $name);
+  protected function duplicateHashTerm($term, $duplicate_suggestions = array('name')) {
+    $hash = '';
+
+    foreach  ($duplicate_suggestions as $duplicate_suggestion) {
+      $hash_chunk = '';
+      switch ($duplicate_suggestion) {
+        case 'name':
+          $hash_chunk = drupal_strtoupper($term->name);
+          // Trying transliteration, if available.
+          if (module_exists('transliteration')) {
+            $hash_chunk = transliteration_get($hash_chunk);
+            // Keeping only ASCII chars.
+            $hash_chunk = preg_replace('#\W#', '', $hash_chunk);
+          }
+          break;
+
+        case 'description':
+          $hash_chunk = drupal_strtoupper($term->description);
+          break;
+
+        case 'parent':
+          $hash_chunk = $term->parents[0];
+          break;
+      }
+      $hash .= $hash_chunk;
+    }
+    return $hash;
+  }
+
+  /**
+   * Assert expected terms indeed are suggested as duplicates.
+   *
+   * @param array $expected_terms
+   *   Array of terms that are expected to be suggested as duplicates
+   * @param string $message
+   *   Assertion message to display on the test results
+   */
+  protected function assertSuggestedDuplicates($expected_terms, $message = '') {
+    $i = 0;
+    foreach ($expected_terms as $term) {
+      $this->assertPattern('#\<input\s+[^>]*type="checkbox"\s+[^>]*name="[^"]+\[duplicates]\[' . $term->tid . '\]"#si', $message . ' (for term #' . $i . ')');
+      $i++;
     }
-    return $name;
   }
 }
 
@@ -1020,8 +1044,10 @@ class RedirectTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * SetUp method.
    */
-  public function setUp() {
-    parent::setUp(array('redirect', 'path'));
+  public function setUp(array $modules = array()) {
+    $modules[] = 'redirect';
+    $modules[] = 'path';
+    parent::setUp($modules);
 
     $this->superAdmin = $this->drupalCreateUser(array(
       'administer taxonomy',
@@ -1036,7 +1062,7 @@ class RedirectTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * GetInfo method.
    */
-  public function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Redirect module integration',
       'description' => 'Ensure that the module Term Merge integrates with ' . l('Redirect', 'http://drupal.org/project/redirect') . '/Path modules correctly.',
@@ -1140,9 +1166,9 @@ class RedirectTermMergeWebTestCase extends TermMergeWebTestCase {
       'redirect' => TERM_MERGE_NO_REDIRECT,
     ), 'Submit');
     $this->drupalPost(NULL, array(), 'Confirm');
-    $this->assertRedirectIntegration($terms, 'No redirections made after running merge batch when not instructed to make redirections.');
+    $this->assertRedirectIntegration($terms, 'No redirection made after running merge batch when not instructed to make redirection.');
 
-    // Trying to merge into an existing term with redirection.
+    // Trying to merge into a term with redirection.
     $this->drupalPost('taxonomy/term/' . $terms['branch']->tid . '/merge', array(
       'term_branch[]' => array($terms['branch']->tid),
       'term_trunk[widget]' => 'select',
@@ -1151,27 +1177,7 @@ class RedirectTermMergeWebTestCase extends TermMergeWebTestCase {
     ), 'Submit');
     $terms['branch']->redirect = $terms['trunk'];
     $this->drupalPost(NULL, array(), 'Confirm');
-    $this->assertRedirectIntegration($terms, 'Redirection is made after running merge batch merging into an existing term, when instructed to make redirections.');
-
-    // Trying to merge into a new term with redirection.
-    $terms = $this->createTerms(array('branch'));
-    $this->drupalPostAJAX('taxonomy/term/' . $terms['branch']->tid . '/merge', array(
-      'term_branch[]' => array($terms['branch']->tid),
-      'term_trunk[widget]' => 'select',
-      'term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK,
-    ), 'term_trunk[tid]');
-
-    $this->drupalPost(NULL, array(
-      'term_branch[]' => array($terms['branch']->tid),
-      'term_trunk[widget]' => 'select',
-      'term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK,
-      'name' => $this->randomName(),
-      'redirect' => 0,
-    ), 'Submit');
-    $this->drupalPost(NULL, array(), 'Confirm');
-    $terms['trunk'] = $this->getLastTerm($this->vocabulary);
-    $terms['branch']->redirect = $terms['trunk'];
-    $this->assertRedirectIntegration($terms, 'Redirection is made after running merge batch merging into a new term, when instructed to make redirections.');
+    $this->assertRedirectIntegration($terms, 'Redirection is made after running merge batch merging into an existing term, when instructed to make redirection.');
   }
 
   /**
@@ -1245,21 +1251,44 @@ class RedirectTermMergeWebTestCase extends TermMergeWebTestCase {
  */
 class SynonymsTermMergeWebTestCase extends TermMergeWebTestCase {
 
+  /**
+   * Field definition array within which the testing will happen.
+   *
+   * @var array
+   */
+  protected $field = array(
+    'field_name' => 'term_merge_synonyms_test',
+    'type' => 'text',
+  );
+
   /**
    * SetUp method.
    */
-  public function setUp() {
-    parent::setUp(array('synonyms'));
+  public function setUp(array $modules = array()) {
+    $modules[] = 'synonyms';
+    parent::setUp($modules);
     // Additionally we enable default synonyms field in the vocabulary.
-    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/edit', array(
-      'synonyms[synonyms][' . SYNONYMS_DEFAULT_FIELD_NAME . ']' => TRUE,
-    ), 'Save');
+    $this->field = field_create_field($this->field);
+    $instance = array(
+      'field_name' => $this->field['field_name'],
+      'label' => 'Testing term merge synonyms integration',
+      'entity_type' => 'taxonomy_term',
+      'bundle' => $this->vocabulary->machine_name,
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    );
+    $instance = field_create_instance($instance);
+    $instance = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
+    synonyms_behavior_settings_save(array(
+      'instance_id' => $instance['id'],
+      'behavior' => 'synonyms',
+      'settings' => array(),
+    ));
   }
 
   /**
    * GetInfo method.
    */
-  public function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Synonyms module integration',
       'description' => 'Ensure that the module Term Merge integrates with ' . l('Synonyms', 'http://drupal.org/project/synonyms') . ' module correctly.',
@@ -1303,7 +1332,7 @@ class SynonymsTermMergeWebTestCase extends TermMergeWebTestCase {
     // Testing adding as a synonym.
     actions_do('term_merge_action', $terms['branch'], array(
       'term_trunk' => $terms['trunk']->tid,
-      'synonyms' => array(SYNONYMS_DEFAULT_FIELD_NAME),
+      'synonyms' => array($this->field['field_name']),
     ));
 
     $terms['trunk']->synonyms = array($terms['branch']->name);
@@ -1321,44 +1350,22 @@ class SynonymsTermMergeWebTestCase extends TermMergeWebTestCase {
       'term_trunk[widget]' => 'select',
       'term_trunk[tid]' => $terms['trunk']->tid,
       'term_branch_keep' => TRUE,
-      'synonyms[' . SYNONYMS_DEFAULT_FIELD_NAME . ']' => FALSE,
+      'synonyms[' . $this->field['field_name'] . ']' => FALSE,
     ), 'Submit');
     $this->drupalPost(NULL, array(), 'Confirm');
     $this->assertSynonymsIntegration($terms, 'No synonyms are added after running merge batch when not instructed to add synonyms.');
 
-    // Trying to merge into an existing term with synonyms adding.
+    // Trying to merge into a term with synonyms adding.
     $this->drupalPost('taxonomy/term/' . $terms['branch']->tid . '/merge', array(
       'term_branch[]' => array($terms['branch']->tid),
       'term_trunk[widget]' => 'select',
       'term_trunk[tid]' => $terms['trunk']->tid,
       'term_branch_keep' => TRUE,
-      'synonyms[' . SYNONYMS_DEFAULT_FIELD_NAME . ']' => TRUE,
+      'synonyms[' . $this->field['field_name'] . ']' => TRUE,
     ), 'Submit');
     $terms['trunk']->synonyms = array($terms['branch']->name);
     $this->drupalPost(NULL, array(), 'Confirm');
     $this->assertSynonymsIntegration($terms, 'Synonyms are added after running merge batch merging into an existing term, when instructed to add synonyms.');
-
-    // Trying to merge into a new term with synonyms adding.
-    $terms = $this->createTerms(array('branch'));
-    $this->drupalPostAJAX('taxonomy/term/' . $terms['branch']->tid . '/merge', array(
-      'term_branch[]' => array($terms['branch']->tid),
-      'term_trunk[widget]' => 'select',
-      'term_branch_keep' => TRUE,
-      'term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK,
-    ), 'term_trunk[tid]');
-
-    $this->drupalPost(NULL, array(
-      'term_branch[]' => array($terms['branch']->tid),
-      'term_trunk[widget]' => 'select',
-      'term_trunk[tid]' => TERM_MERGE_NEW_TERM_TRUNK,
-      'term_branch_keep' => TRUE,
-      'name' => $this->randomName(),
-      'synonyms[' . SYNONYMS_DEFAULT_FIELD_NAME . ']' => TRUE,
-    ), 'Submit');
-    $this->drupalPost(NULL, array(), 'Confirm');
-    $terms['trunk'] = $this->getLastTerm($this->vocabulary);
-    $terms['trunk']->synonyms = array($terms['branch']->name);
-    $this->assertSynonymsIntegration($terms, 'Synonyms are added after running merge batch merging into a new term, when instructed to add synonyms.');
   }
 
   /**
@@ -1378,10 +1385,7 @@ class SynonymsTermMergeWebTestCase extends TermMergeWebTestCase {
     drupal_static_reset();
     foreach ($terms as $term) {
       // Getting an array of synonyms according to Synonyms module.
-      $synonyms = array();
-      foreach (synonyms_get_term_synonyms(taxonomy_term_load($term->tid)) as $tmp) {
-        $synonyms[] = $tmp['value'];
-      }
+      $synonyms = synonyms_get_raw(taxonomy_term_load($term->tid));
 
       $expected_synonyms = isset($term->synonyms) ? $term->synonyms : array();
       // Comparing $synonyms to $expected_synonyms.
@@ -1435,8 +1439,9 @@ class ViewsTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * SetUp method.
    */
-  public function setUp() {
-    parent::setUp(array('views'));
+  public function setUp(array $modules = array()) {
+    $modules[] = 'views';
+    parent::setUp($modules);
     // Additionally we create a view.
     $view = views_new_view();
     $view->name = 'term_merge_view_test';
@@ -1456,7 +1461,7 @@ class ViewsTermMergeWebTestCase extends TermMergeWebTestCase {
   /**
    * GetInfo method.
    */
-  public function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Views module integration',
       'description' => 'Ensure that the module Term Merge integrates with ' . l('Views', 'http://drupal.org/project/views') . ' module correctly.',
@@ -1516,7 +1521,8 @@ class ViewsTermMergeWebTestCase extends TermMergeWebTestCase {
     $this->view->set_display('default');
 
     // We use Field API info to look up necessary tables and columns.
-    $table = reset(array_keys($field['storage']['details']['sql']['FIELD_LOAD_CURRENT']));
+    $table = array_keys($field['storage']['details']['sql']['FIELD_LOAD_CURRENT']);
+    $table = reset($table);
     $columns = $field['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$table];
 
     $this->view->display_handler->display->display_options['filters'][$columns['tid']]['id'] = $columns['tid'];
@@ -1546,3 +1552,104 @@ class ViewsTermMergeWebTestCase extends TermMergeWebTestCase {
     $this->assertTrue(count($filter) == 1 && in_array($terms['trunk']->tid, array_keys($filter)), 'Views term reference filter gets updated to filter on trunk term instead of filtering on branch term if the branch term is instructed to be deleted during merging of terms.');
   }
 }
+
+/**
+ * Test integration with Entity Reference module.
+ */
+class EntityReferenceTermMergeWebTestCase extends TermMergeWebTestCase {
+
+  /**
+   * Content type used for testing the entity reference field integration.
+   *
+   * @var string
+   */
+  protected $content_type = 'term_merge_entity_reference';
+
+  /**
+   * Field definition array used for entity reference integration testing.
+   *
+   * @var array
+   */
+  protected $field = array(
+    'type' => 'entityreference',
+    'field_name' => 'term_merge_entity_reference',
+    'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    'settings' => array(
+      'target_type' => 'taxonomy_term',
+      'handler' => 'base',
+      'handler_settings' => array(),
+    ),
+  );
+
+  /**
+   * Instance definition array used for entity reference integration testing.
+   *
+   * @var array
+   */
+  protected $instance = array();
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Term Merge Entity Reference',
+      'description' => 'Ensure that the module Term Merge integrates with Entity Reference field type correctly.',
+      'group' => 'Term Merge',
+    );
+  }
+
+  public function setUp(array $modules = array()) {
+    $modules[] = 'entityreference';
+    parent::setUp($modules);
+
+    $this->drupalPost('admin/structure/types/add', array(
+      'name' => $this->randomName(),
+      'type' => $this->content_type,
+    ), 'Save content type');
+
+    $this->field = field_create_field($this->field);
+    $this->instance['field_name'] = $this->field['field_name'];
+    $this->instance['entity_type'] = 'node';
+    $this->instance['bundle'] = $this->content_type;
+    $this->instance['label'] = $this->randomName();
+    $this->instance = field_create_instance($this->instance);
+    $this->instance = field_info_instance($this->instance['entity_type'], $this->instance['field_name'], $this->instance['bundle']);
+  }
+
+  /**
+   * Verify that entity reference field values get update upon term merging.
+   */
+  public function testEntityReferenceField() {
+    $terms = array(
+      'trunk' => NULL,
+      'branch' => NULL,
+    );
+    $nodes = array();
+    foreach ($terms as $type => $v) {
+      $terms[$type] = (object) array(
+        'vid' => $this->vocabulary->vid,
+        'name' => $this->randomName(),
+      );
+      taxonomy_term_save($terms[$type]);
+      $nodes[$type] = (object) array(
+        'type' => $this->content_type,
+        'title' => $this->randomName(),
+        $this->field['field_name'] => array(LANGUAGE_NONE => array(
+          array('target_id' => $terms[$type]->tid),
+        )),
+      );
+      node_save($nodes[$type]);
+    }
+
+    actions_do('term_merge_action', $terms['branch'], array(
+      'term_trunk' => $terms['trunk']->tid,
+      'term_branch_keep' => FALSE,
+    ));
+
+    foreach ($nodes as $type => $node) {
+      $node = entity_load_unchanged('node', $node->nid);
+      $this->assertEqual($terms['trunk']->tid, $node->{$this->field['field_name']}[LANGUAGE_NONE][0]['target_id'], $type . ' node points to trunk term in the entity reference field after merging the terms.');
+    }
+  }
+}

+ 60 - 0
sites/all/modules/contrib/users/spambot/README.txt

@@ -0,0 +1,60 @@
+CONTENTS OF THIS FILE
+---------------------
+
+ * Introduction
+ * Recommended modules
+ * Installation
+ * Configuration
+ * Maintainers
+
+
+INTRODUCTION
+------------
+
+Spambot protects the user registration form from spammers and spambots by
+verifying registration attempts against the Stop Forum Spam
+(www.stopforumspam.com) online database.
+It also adds some useful features to help deal with spam accounts.
+
+This module works well for sites which require user registration
+before posting is allowed (which is most forums).
+
+
+RECOMMENDED MODULES
+-------------------
+
+ * User Stats (https://www.drupal.org/project/user_stats):
+   Allow to use a bit more statistics of users by IP address.
+
+ * Statistics (built-in core)
+   Allow to use a bit more statistics of users by IP address.
+
+
+INSTALLATION
+------------
+
+ * Install as you would normally install a contributed Drupal module. See:
+   https://drupal.org/documentation/install/modules-themes/modules-7
+   for further information.
+
+
+CONFIGURATION
+-------------
+
+ * Configure user permissions in Administration » People » Permissions:
+
+   - Protected from spambot scans
+
+     Users in roles with the "Protected from spambot scans" permission would not
+     be scanned by cron.
+
+ * Go to the '/admin/config/system/spambot' page and check additional settings.
+
+
+MAINTAINERS
+-----------
+
+Current maintainers:
+ * bengtan (bengtan) - https://www.drupal.org/u/bengtan
+ * Michael Moritz (miiimooo) - https://www.drupal.org/u/miiimooo
+ * Dmitry Kiselev (kala4ek) - https://www.drupal.org/u/kala4ek

+ 159 - 56
sites/all/modules/contrib/users/spambot/spambot.admin.inc

@@ -1,70 +1,110 @@
 <?php
 
-function spambot_settings_form($form, &$form_state) {
-  $numbers = array(0 => t('Never'), 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, 10 => 10, 15 => 15, 20 => 20, 30 => 30, 40 => 40, 50 => 50, 60 => 60, 70 => 70, 80 => 80, 90 => 90, 100 => 100, 150 => 150, 200 => 200);
+/**
+ * @file
+ * Administration part (forms and pages) for Spambot module.
+ */
 
+/**
+ * Form builder for settings form.
+ */
+function spambot_settings_form() {
+  $numbers = array(
+    1 => 1,
+    2 => 2,
+    3 => 3,
+    4 => 4,
+    5 => 5,
+    6 => 6,
+    7 => 7,
+    8 => 8,
+    9 => 9,
+    10 => 10,
+    15 => 15,
+    20 => 20,
+    30 => 30,
+    40 => 40,
+    50 => 50,
+    60 => 60,
+    70 => 70,
+    80 => 80,
+    90 => 90,
+    100 => 100,
+    150 => 150,
+    200 => 200,
+  );
+
+  // Fieldset for set up spam criteria.
   $form['criteria'] = array(
     '#type' => 'fieldset',
     '#title' => t('Spammer criteria'),
     '#description' => t('A user account or an attempted user registration will be deemed a spammer if the email, username, or IP address has been reported to www.stopforumspam.com more times than the following thresholds.'),
     '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
   );
   $form['criteria']['spambot_criteria_email'] = array(
     '#type' => 'select',
     '#title' => t('Number of times the email has been reported is equal to or more than'),
     '#description' => t('If the email address for a user or user registration has been reported to www.stopforumspam.com this many times, then it is deemed as a spammer.'),
-    '#options' => array(0 => t('Don\'t use email as a criteria')) + $numbers,
-    '#default_value' => variable_get('spambot_criteria_email', 1),
+    '#options' => array(0 => t("Don't use email as a criteria")) + $numbers,
+    '#default_value' => variable_get('spambot_criteria_email', SPAMBOT_DEFAULT_CRITERIA_EMAIL),
   );
   $form['criteria']['spambot_criteria_username'] = array(
     '#type' => 'select',
     '#title' => t('Number of times the username has been reported is equal to or more than'),
     '#description' => t('If the username for a user or user registration has been reported to www.stopforumspam.com this many times, then it is deemed as a spammer. Be careful about using this option as you may accidentally block genuine users who happen to choose the same username as a known spammer.'),
-    '#options' => array(0 => t('Don\'t use username as a criteria')) + $numbers,
-    '#default_value' => variable_get('spambot_criteria_username', 0),
+    '#options' => array(0 => t("Don't use username as a criteria")) + $numbers,
+    '#default_value' => variable_get('spambot_criteria_username', SPAMBOT_DEFAULT_CRITERIA_USERNAME),
   );
   $form['criteria']['spambot_criteria_ip'] = array(
     '#type' => 'select',
     '#title' => t('Number of times the IP address has been reported is equal to or more than'),
     '#description' => t('If the IP address for a user or user registration has been reported to www.stopforumspam.com this many times, then it is deemed as a spammer. Be careful about setting this threshold too low as IP addresses can change.'),
-    '#options' => array(0 => t('Don\'t use IP address as a criteria')) + $numbers,
-    '#default_value' => variable_get('spambot_criteria_ip', 20),
+    '#options' => array(0 => t("Don't use IP address as a criteria")) + $numbers,
+    '#default_value' => variable_get('spambot_criteria_ip', SPAMBOT_DEFAULT_CRITERIA_IP),
   );
 
+  // White lists.
+  $form['spambot_whitelist'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Whitelists'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  $form['spambot_whitelist']['spambot_whitelist_email'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Allowed email addresses'),
+    '#description' => t('Enter email addresses, one per line.'),
+    '#default_value' => variable_get('spambot_whitelist_email', ''),
+  );
+  $form['spambot_whitelist']['spambot_whitelist_username'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Allowed usernames'),
+    '#description' => t('Enter usernames, one per line.'),
+    '#default_value' => variable_get('spambot_whitelist_username', ''),
+  );
+  $form['spambot_whitelist']['spambot_whitelist_ip'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Allowed IP addresses'),
+    '#description' => t('Enter IP addresses, one per line.'),
+    '#default_value' => variable_get('spambot_whitelist_ip', ''),
+  );
+
+  // Fieldset for configure protecting at user register form.
   $form['register'] = array(
     '#type' => 'fieldset',
     '#title' => t('User registration'),
     '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
   );
   $form['register']['spambot_user_register_protect'] = array(
     '#type' => 'checkbox',
     '#title' => t('Protect the user registration form'),
     '#description' => t('If ticked, new user registrations will be tested if they match any known spammers and blacklisted.'),
     '#default_value' => variable_get('spambot_user_register_protect', TRUE),
-  ); 
-  $form['register']['spambot_blocked_message_email'] = array(
-    '#type' => 'textarea',
-    '#title' => t('User registration blocked message (blocked email address)'),
-    '#rows' => 1,
-    '#default_value' => variable_get('spambot_blocked_message_email', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)),
-    '#description' => t('Message to display when user registration is blocked due to email address. <br />Showing a specific reason why registration was blocked may make spambot easier to circumvent.<br />The following tokens are available: @email %email @username %username @ip %ip'),
-  );
-  $form['register']['spambot_blocked_message_username'] = array(
-    '#type' => 'textarea',
-    '#title' => t('User registration blocked message (blocked username)'),
-    '#rows' => 1,
-    '#default_value' => variable_get('spambot_blocked_message_username', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)),
-    '#description' => t('Message to display when user registration is blocked due to username.'),
   );
-  $form['register']['spambot_blocked_message_ip'] = array(
-    '#type' => 'textarea',
-    '#title' => t('User registration blocked message (blocked ip address)'),
-    '#rows' => 1,
-    '#default_value' => variable_get('spambot_blocked_message_ip', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)),
-    '#description' => t('Message to display when user registration is blocked due to ip address.'),
-  );
-  
-  $sleep_options = array(0 => t('Don\'t delay'), 1 => t('1 second'));
+
+  $sleep_options = array(t("Don't delay"), t('1 second'));
   foreach (array(2, 3, 4, 5, 10, 20, 30) as $num) {
     $sleep_options[$num] = t('@num seconds', array('@num' => $num));
   }
@@ -73,21 +113,23 @@ function spambot_settings_form($form, &$form_state) {
     '#title' => t('If blacklisted, delay for'),
     '#description' => t('If an attempted user registration is blacklisted, you can choose to deliberately delay the request. This can be useful for slowing them down if they continually try to register.<br />Be careful about choosing too large a value for this as it may exceed your PHP max_execution_time.'),
     '#options' => $sleep_options,
-    '#default_value' => variable_get('spambot_blacklisted_delay', 0),
+    '#default_value' => variable_get('spambot_blacklisted_delay', SPAMBOT_DEFAULT_DELAY),
   );
 
+  // Fieldset for set up scanning of existing accounts.
   $form['existing'] = array(
     '#type' => 'fieldset',
     '#title' => t('Scan existing accounts'),
-    '#description' => t('This module can also scan existing user accounts to see if they are known spammers. It works by checking user accounts with increasing uid\'s ie. user id 2, 3, 4 etc during cron.'),
+    '#description' => t("This module can also scan existing user accounts to see if they are known spammers. It works by checking user accounts with increasing uid's ie. user id 2, 3, 4 etc during cron."),
     '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
   );
   $form['existing']['spambot_cron_user_limit'] = array(
     '#type' => 'textfield',
     '#title' => t('Maximum number of user accounts to scan per cron'),
     '#description' => t('Enter the number of user accounts to scan for each cron. If you do not want to scan existing user accounts, leave this as 0.<br />Be careful not to make this value too large, as it will slow your cron execution down and may cause your site to query www.stopforumspam.com more times than allowed each day.'),
     '#size' => 10,
-    '#default_value' => variable_get('spambot_cron_user_limit', 0),
+    '#default_value' => variable_get('spambot_cron_user_limit', SPAMBOT_DEFAULT_CRON_USER_LIMIT),
   );
   $form['existing']['spambot_check_blocked_accounts'] = array(
     '#type' => 'checkbox',
@@ -107,39 +149,57 @@ function spambot_settings_form($form, &$form_state) {
     '#default_value' => variable_get('spambot_spam_account_action', SPAMBOT_ACTION_NONE),
   );
 
-  // Show scan status
-  $last_uid = variable_get('spambot_last_checked_uid', 0);
+  // Get scan status.
   $suffix = '';
-  if ($last_uid) {   
-    $num_checked = db_select('users')->fields('users')
-      ->condition('uid', '1', '>')->condition('uid', $last_uid, '<=')      
-      ->countQuery()->execute()->fetchField();
-
-    $num_left = db_select('users')->fields('users')
-      ->condition('uid', '1', '>')->condition('uid', $last_uid, '>')
-      ->countQuery()->execute()->fetchField();
-
-    $last_uid = db_select('users')->fields('users', array('uid'))
-      ->condition('uid', '1', '>')->condition('uid', $last_uid, '<=')
-      ->orderBy('uid', 'DESC')->range(0, 1)
-      ->execute()->fetchField();
-    
-    $account = user_load((int) $last_uid);
-    $suffix = '<br />' . t('The last checked user account is: !account (uid @uid)', array('!account' => l(check_plain($account->name), 'user/' . $account->uid), '@uid' => $account->uid));
+  if ($last_uid = variable_get('spambot_last_checked_uid', 0)) {
+    $num_checked = db_select('users', 'u')
+      ->fields('u', array('uid'))
+      ->condition('u.uid', 1, '>')
+      ->condition('u.uid', $last_uid, '<=')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
+    $num_left = db_select('users', 'u')
+      ->fields('u', array('uid'))
+      ->condition('u.uid', 1, '>')
+      ->condition('u.uid', $last_uid, '>')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
+    $last_uid = db_select('users', 'u')
+      ->fields('u', array('uid'))
+      ->condition('u.uid', 1, '>=')
+      ->condition('u.uid', $last_uid, '<=')
+      ->orderBy('u.uid', 'DESC')
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+
+    $account = user_load($last_uid);
+    $suffix = '<br />';
+    $suffix .= t('The last checked user account is: !account (uid %uid)', array(
+      '!account' => l($account->name, 'user/' . $account->uid),
+      '%uid' => $account->uid,
+    ));
   }
   else {
     $num_checked = 0;
-    $num_left = db_select('users')->fields('users')->condition('uid', 1, '>')
-      ->countQuery()->execute()->fetchField();
+    $num_left = db_select('users')
+      ->fields('users')
+      ->condition('uid', 1, '>')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
   }
 
-  $text = t('Accounts checked: @checked, Accounts remaining: @remaining', array('@checked' => $num_checked, '@remaining' => $num_left));
+  $text = t('Accounts checked: %checked, Accounts remaining: %remaining', array('%checked' => $num_checked, '%remaining' => $num_left));
   $form['existing']['message'] = array(
     '#type' => 'fieldset',
     '#title' => t('Scan status'),
     '#description' => $text . $suffix,
   );
-
   $form['existing']['spambot_last_checked_uid'] = array(
     '#type' => 'textfield',
     '#title' => t('Continue scanning after this user id'),
@@ -148,6 +208,49 @@ function spambot_settings_form($form, &$form_state) {
     '#default_value' => $last_uid,
   );
 
+  // Fieldset for set up messages which will be displayed for blocked users.
+  $form['messages'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Blocked messages'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  $form['messages']['spambot_blocked_message_email'] = array(
+    '#type' => 'textarea',
+    '#title' => t('User registration blocked message (blocked email address)'),
+    '#rows' => 1,
+    '#default_value' => variable_get('spambot_blocked_message_email', SPAMBOT_DEFAULT_BLOCKED_MESSAGE),
+    '#description' => t('Message to display when user action is blocked due to email address. <br />Showing a specific reason why registration was blocked may make spambot easier to circumvent.<br />The following tokens are available: <em>@email %email @username %username @ip %ip</em>'),
+  );
+  $form['messages']['spambot_blocked_message_username'] = array(
+    '#type' => 'textarea',
+    '#title' => t('User registration blocked message (blocked username)'),
+    '#rows' => 1,
+    '#default_value' => variable_get('spambot_blocked_message_username', SPAMBOT_DEFAULT_BLOCKED_MESSAGE),
+    '#description' => t('Message to display when user action is blocked due to username.<br />The following tokens are available: <em>@email %email @username %username @ip %ip</em>'),
+  );
+  $form['messages']['spambot_blocked_message_ip'] = array(
+    '#type' => 'textarea',
+    '#title' => t('User registration blocked message (blocked ip address)'),
+    '#rows' => 1,
+    '#default_value' => variable_get('spambot_blocked_message_ip', SPAMBOT_DEFAULT_BLOCKED_MESSAGE),
+    '#description' => t('Message to display when user action is blocked due to ip address.<br />The following tokens are available: <em>@email %email @username %username @ip %ip</em>'),
+  );
+
+  // Fieldset for configure log rules.
+  $form['logging'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Log information'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  $form['logging']['spambot_log_blocked_registration'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Log information about blocked registrations into Drupal log'),
+    '#default_value' => variable_get('spambot_log_blocked_registration', TRUE),
+  );
+
+  // StopFormSpam API key.
   $form['spambot_sfs_api_key'] = array(
     '#type' => 'textfield',
     '#title' => t('www.stopforumspam.com API key'),

+ 6 - 4
sites/all/modules/contrib/users/spambot/spambot.info

@@ -1,12 +1,14 @@
 name = Spambot
-description = Anti-spam module that uses data from www.stopforumspam.com to protect the user registration form against known spammers and spambots. 
+description = Anti-spam module that uses data from www.stopforumspam.com to protect the user registration form against known spammers and spambots.
 package = "Spam control"
+
 configure = admin/config/system/spambot
+
 core = 7.x
 
-; Information added by drupal.org packaging script on 2013-01-11
-version = "7.x-1.3"
+; Information added by Drupal.org packaging script on 2016-09-06
+version = "7.x-1.5"
 core = "7.x"
 project = "spambot"
-datestamp = "1357872222"
+datestamp = "1473137640"
 

+ 52 - 50
sites/all/modules/contrib/users/spambot/spambot.install

@@ -1,67 +1,56 @@
 <?php
 
 /**
- * Implementation of hook_schema().
+ * @file
+ * Install and update hooks for Spambot module.
+ */
+
+/**
+ * Implements hook_schema().
  */
 function spambot_schema() {
-  $schema = array();
   $schema['node_spambot'] = array(
-    'description' => t('Node table to track author IP addresses. For use by spambot only.'),
+    'description' => 'Node table to track author IP addresses. For use by spambot only.',
     'fields' => array(
-      'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-      'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-      'hostname' => array( 'type' => 'varchar', 'length' => 128, 'not null' => FALSE),
+      'nid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'hostname' => array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+      ),
     ),
     'primary key' => array('nid'),
     'indexes' => array(
       'uid' => array('uid'),
     ),
   );
+
   return $schema;
 }
 
 /**
- * Implementation of hook_uninstall().
+ * Implements hook_uninstall().
  */
 function spambot_uninstall() {
-  db_query("DELETE FROM {variable} WHERE name LIKE 'spambot_%'");
+  db_delete('variable')
+    ->condition('name', 'spambot_%', 'LIKE')
+    ->execute();
 }
 
 /**
- * Migrate settings from previous version of spambot (6.x-2.0)
+ * Update variables, create new table 'node_spambot'.
  */
-function spambot_update_6300() {
-  $ret = array();
-
-  // In previous versions of spambot, the default message was 'Blacklisted. Now go away!'
-  // If no custom message was configured, then configure it to 'Blacklisted. Now go away!'
-  $message = variable_get('spambot_blocked_message', FALSE);
-  if (!$message) {
-    variable_set('spambot_blocked_message', t('Blacklisted. Now go away!'));
-  }
-
-  // Previous versions of spambot blacklisted on any of the three criteria
-  variable_set('spambot_criteria_email', TRUE);
-  variable_set('spambot_criteria_username', TRUE);
-  variable_set('spambot_criteria_ip', TRUE);
-  return $ret;
-}
-
-function spambot_update_6301() {
-  $ret = array();
-  // Change criteria settings from booleans to numbers
-  if (variable_set('spambot_criteria_email', TRUE)) {
-    variable_set('spambot_criteria_email', 1);
-  }  
-  if (variable_set('spambot_criteria_username', FALSE)) {
-    variable_set('spambot_criteria_username', 1);
-  }
-  if (variable_set('spambot_criteria_ip', FALSE)) {
-    variable_set('spambot_criteria_ip', 1);
-  }
-  return $ret;
-}
-
 function spambot_update_7101() {
   $messages = array();
 
@@ -77,24 +66,37 @@ function spambot_update_7101() {
     $messages[] = t('Transferred user registration blocked message to new format.');
   }
 
-  // Create new table node_spambot
+  // Create new table node_spambot.
   if (!db_table_exists('node_spambot')) {
-    $schema = array();
-    $schema['node_spambot'] = array(
+    $node_spambot = array(
       'description' => t('Node table to track author IP addresses. For use by spambot only.'),
       'fields' => array(
-        'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-        'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-        'hostname' => array( 'type' => 'varchar', 'length' => 128, 'not null' => FALSE),
+        'nid' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'uid' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'hostname' => array(
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => FALSE,
+        ),
       ),
       'primary key' => array('nid'),
       'indexes' => array(
         'uid' => array('uid'),
       ),
     );
-    db_create_table('node_spambot', $schema['node_spambot']);
+    db_create_table('node_spambot', $node_spambot);
     $messages[] = t('Created new table <em>node_spambot</em>.');
   }
-  
-  return join('<br />', $messages);
+
+  return implode('<br />', $messages);
 }

+ 390 - 200
sites/all/modules/contrib/users/spambot/spambot.module

@@ -1,32 +1,28 @@
 <?php
+
 /**
-* @file
-* Anti-spam module that uses data from www.stopforumspam.com to protect the user registration form against known spammers and spambots. 
-* 
-*/
+ * @file
+ * Main module file.
+ *
+ * Anti-spam module that uses data from www.stopforumspam.com
+ * to protect the user registration form against known spammers and spambots.
+ */
 
 define('SPAMBOT_ACTION_NONE', 0);
 define('SPAMBOT_ACTION_BLOCK', 1);
 define('SPAMBOT_ACTION_DELETE', 2);
-
+define('SPAMBOT_DEFAULT_CRITERIA_EMAIL', 1);
+define('SPAMBOT_DEFAULT_CRITERIA_USERNAME', 0);
+define('SPAMBOT_DEFAULT_CRITERIA_IP', 20);
+define('SPAMBOT_DEFAULT_DELAY', 0);
+define('SPAMBOT_DEFAULT_CRON_USER_LIMIT', 0);
 define('SPAMBOT_DEFAULT_BLOCKED_MESSAGE', 'Your email address or username or IP address is blacklisted.');
-
-/**
- * Implements hook_permission()
- */
-function spambot_permission() {
-  return array(
-    'protected from spambot scans' => array(
-      'title' => t('Protected from spambot scans')
-    ),
-  );
-}
+define('SPAMBOT_MAX_EVIDENCE_LENGTH', 1024);
 
 /**
  * Implements hook_menu().
  */
 function spambot_menu() {
-  $items = array(); 
   $items['admin/config/system/spambot'] = array(
     'title' => 'Spambot',
     'description' => 'Configure the spambot module',
@@ -38,159 +34,179 @@ function spambot_menu() {
 
   $items['user/%user/spambot'] = array(
     'title' => 'Spam',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('spambot_user_spam_admin_form', 1),
+    'page callback' => 'spambot_user_spam',
+    'page arguments' => array(1),
     'access arguments' => array('administer users'),
     'type' => MENU_LOCAL_TASK,
     'file' => 'spambot.pages.inc',
   );
+
   return $items;
 }
 
 /**
- * Implementation of hook_form_FORM_ID_alter()
+ * Implements hook_permission().
  */
-function spambot_form_user_register_form_alter(&$form, &$form_state, $form_id) {
-  if (variable_get('spambot_user_register_protect', TRUE) && !user_access('administer users')) {
-    $form['#validate'][] = 'spambot_user_register_validate';
-  }
+function spambot_permission() {
+  return array(
+    'protected from spambot scans' => array(
+      'title' => t('Protected from spambot scans'),
+      'description' => t('Roles with this access permission would not be checked for spammer'),
+    ),
+  );
 }
 
 /**
- * Validate the user_register form
+ * Implements hook_admin_paths().
  */
-function spambot_user_register_validate($form, &$form_state) {
-  $email_threshold = variable_get('spambot_criteria_email', 1);
-  $username_threshold = variable_get('spambot_criteria_username', 0);
-  $ip_threshold = variable_get('spambot_criteria_ip', 20);
-
-  // Build request parameters according to the criteria to use
-  $request = array();
-  if (!empty($form_state['values']['mail']) && $email_threshold > 0) {
-    $request['email'] = $form_state['values']['mail'];
-  }  
+function spambot_admin_paths() {
+  $paths = array(
+    'user/*/spambot' => TRUE,
+  );
 
-  if (!empty($form_state['values']['name']) && $username_threshold > 0) {
-    $request['username'] = $form_state['values']['name'];
-  }  
+  return $paths;
+}
 
-  if ($ip_threshold > 0) {
-    $ip = ip_address();
-    // Don't check the loopback interface
-    if ($ip != '127.0.0.1') {
-      $request['ip'] = $ip;
-    }
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function spambot_form_user_register_form_alter(&$form, &$form_state) {
+  if (variable_get('spambot_user_register_protect', TRUE)) {
+    spambot_add_form_protection(
+      $form,
+      array(
+        'mail' => 'mail',
+        'name' => 'name',
+        'ip' => TRUE,
+      )
+    );
   }
+}
 
-  // Only do a remote API request if there is anything to check
-  if (count($request)) {
-    $data = array();
-    if (spambot_sfs_request($request, $data)) {
-      $substitutions = array(
-        '@email' => $form_state['values']['mail'], '%email' => $form_state['values']['mail'],
-        '@username' => $form_state['values']['name'], '%username' => $form_state['values']['name'],
-        '@ip' => ip_address(), '%ip' => ip_address(),
-      );
-
-      $reasons = array();
-      if ($email_threshold > 0 && !empty($data['email']['appears']) && $data['email']['frequency'] >= $email_threshold) {
-        form_set_error('mail', t(variable_get('spambot_blocked_message_email', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)), $substitutions));
-        $reasons[] = t('email=@value', array('@value' => $request['email']));
-      }
-      if ($username_threshold > 0 && !empty($data['username']['appears']) && $data['username']['frequency'] >= $username_threshold) {
-        form_set_error('name', t(variable_get('spambot_blocked_message_username', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)), $substitutions));
-        $reasons[] = t('username=@value', array('@value' => $request['username']));
-      }
-      if ($ip_threshold > 0 && !empty($data['ip']['appears']) && $data['ip']['frequency'] >= $ip_threshold) {
-        form_set_error('', t(variable_get('spambot_blocked_message_ip', t(SPAMBOT_DEFAULT_BLOCKED_MESSAGE)), $substitutions));
-        $reasons[] = t('ip=@value', array('@value' => $request['ip']));
-      }
-
-      if (count($reasons)) {
-        watchdog('spambot', 'Blocked registration: @reasons', array('@reasons' => join(',', $reasons)));
-
-        // Slow them down if configured
-        $delay = variable_get('spambot_blacklisted_delay', 0);
-        if ($delay) {
-          sleep($delay);
-        }
-      }
-    }
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function spambot_form_user_admin_account_alter(&$form, &$form_state, $form_id) {
+  foreach ($form['accounts']['#options'] as $uid => $user_options) {
+    // Change $form['accounts']['#options'][$uid]['operations']['data']
+    // into a multi-item render array so we can append to it.
+    $form['accounts']['#options'][$uid]['operations']['data'] = array(
+      'edit' => $form['accounts']['#options'][$uid]['operations']['data'],
+    );
+    $form['accounts']['#options'][$uid]['operations']['data']['spam'] = array(
+      '#type' => 'link',
+      '#title' => t('spam'),
+      '#href' => "user/$uid/spambot",
+      // Ugly hack to insert a space.
+      '#prefix' => ' ',
+    );
   }
 }
 
 /**
- * Implementation of hook_node_insert
- *
- * Keeps table node_spambot up to date
+ * Implements hook_node_insert().
  */
 function spambot_node_insert($node) {
-  db_insert('node_spambot')->fields(array('nid' => $node->nid, 'uid' => $node->uid, 'hostname' => ip_address()))->execute();
+  db_insert('node_spambot')
+    ->fields(array(
+      'nid' => $node->nid,
+      'uid' => $node->uid,
+      'hostname' => ip_address(),
+    ))
+    ->execute();
 }
 
 /**
- * Implementation of hook_node_delete
- *
- * Keeps table node_spambot up to date
+ * Implements hook_node_delete().
  */
 function spambot_node_delete($node) {
-  db_delete('node_spambot')->condition('nid', $node->nid)->execute();
+  db_delete('node_spambot')
+    ->condition('nid', $node->nid)
+    ->execute();
 }
 
 /**
- * Implementation of hook_cron
+ * Implements hook_cron().
  */
 function spambot_cron() {
-  $limit = variable_get('spambot_cron_user_limit', 0);
-  if ($limit) {
+  if ($limit = variable_get('spambot_cron_user_limit', SPAMBOT_DEFAULT_CRON_USER_LIMIT)) {
     $last_uid = variable_get('spambot_last_checked_uid', 0);
     if ($last_uid < 1) {
-      // Skip scanning the first account
+      // Skip scanning the anonymous and superadmin users.
       $last_uid = 1;
     }
 
-    $uids = db_select('users')->fields('users', array('uid'))
-      ->condition('uid', $last_uid, '>')->orderBy('uid')
-      ->range(0, $limit)->execute()->fetchCol();
+    $query = db_select('users')
+      ->fields('users', array('uid'))
+      ->condition('uid', $last_uid, '>')
+      ->orderBy('uid')
+      ->range(0, $limit);
+    if (!variable_get('spambot_check_blocked_accounts', FALSE)) {
+      $query->condition('status', 1);
+    }
+
+    $uids = $query
+      ->execute()
+      ->fetchCol();
 
-    $action = variable_get('spambot_spam_account_action', SPAMBOT_ACTION_NONE);
-    foreach ($uids as $uid) {
-      $account = user_load($uid);
-      
-      if ($account->status || variable_get('spambot_check_blocked_accounts', FALSE)) {
+    if ($uids) {
+      $action = variable_get('spambot_spam_account_action', SPAMBOT_ACTION_NONE);
+      $accounts = user_load_multiple($uids);
+
+      foreach ($accounts as $account) {
         $result = spambot_account_is_spammer($account);
+
         if ($result > 0) {
           $link = l(t('spammer'), 'user/' . $account->uid);
           switch (user_access('protected from spambot scans', $account) ? SPAMBOT_ACTION_NONE : $action) {
             case SPAMBOT_ACTION_BLOCK:
               if ($account->status) {
-                user_save($account, array('status' => 0));
-                watchdog('spambot', 'Blocked spam account: @name &lt;@email&gt; (uid @uid)', array('@name' => $account->name, '@email' => $account->mail, '@uid' => $account->uid), WATCHDOG_NOTICE, $link);
+                // Block spammer's account.
+                $account->status = 0;
+                user_save($account);
+                watchdog('spambot', 'Blocked spam account: @name &lt;@email&gt; (uid @uid)', array(
+                  '@name' => $account->name,
+                  '@email' => $account->mail,
+                  '@uid' => $account->uid,
+                ), WATCHDOG_NOTICE, $link);
               }
               else {
-                // Don't block an already blocked account
-                watchdog('spambot', t('Spam account already blocked: @name &lt;@email&gt; (uid @uid)', array('@name' => $account->name, '@email' => $account->mail, '@uid' => $account->uid)), array(), WATCHDOG_NOTICE, $link);
+                // Don't block an already blocked account.
+                watchdog('spambot', 'Spam account already blocked: @name &lt;@email&gt; (uid @uid)', array(
+                  '@name' => $account->name,
+                  '@email' => $account->mail,
+                  '@uid' => $account->uid,
+                ), WATCHDOG_NOTICE, $link);
               }
               break;
 
             case SPAMBOT_ACTION_DELETE:
               user_delete($account->uid);
-              watchdog('spambot', 'Deleted spam account: @name &lt;@email&gt; (uid @uid)', array('@name' => $account->name, '@email' => $account->mail, '@uid' => $account->uid), WATCHDOG_NOTICE, $link);
+              watchdog('spambot', 'Deleted spam account: @name &lt;@email&gt; (uid @uid)', array(
+                '@name' => $account->name,
+                '@email' => $account->mail,
+                '@uid' => $account->uid,
+              ), WATCHDOG_NOTICE, $link);
               break;
 
             default:
-              watchdog('spambot', 'Found spam account: @name &lt;@email&gt; (uid @uid)', array('@name' => $account->name, '@email' => $account->mail, '@uid' => $account->uid), WATCHDOG_NOTICE, $link);
+              watchdog('spambot', 'Found spam account: @name &lt;@email&gt; (uid @uid)', array(
+                '@name' => $account->name,
+                '@email' => $account->mail,
+                '@uid' => $account->uid,
+              ), WATCHDOG_NOTICE, $link);
               break;
           }
-          // Mark this uid as successfully checked
-          variable_set('spambot_last_checked_uid', $uid);
+
+          // Mark this uid as successfully checked.
+          variable_set('spambot_last_checked_uid', $account->uid);
         }
-        else if ($result == 0) {
-          // Mark this uid as successfully checked
-          variable_set('spambot_last_checked_uid', $uid);
+        elseif ($result == 0) {
+          // Mark this uid as successfully checked.
+          variable_set('spambot_last_checked_uid', $account->uid);
         }
-        else if ($result < 0) {
-          // Error contacting service, so pause processing
+        elseif ($result < 0) {
+          // Error contacting service, so pause processing.
           break;
         }
       }
@@ -199,42 +215,104 @@ function spambot_cron() {
 }
 
 /**
- * Invoke www.stopforumspam.com's api
- *
- * @param $query
- *   A keyed array of url parameters ie. array('email' => 'blah@blah.com')
- * @param $data
- *   An array that will be filled with the data from www.stopforumspam.com. 
+ * Validate callback for user_register form.
+ */
+function spambot_user_register_form_validate(&$form, &$form_state) {
+  $validation_field_names = $form['#spambot_validation'];
+  $values = $form_state['values'];
+  $form_errors = form_get_errors();
+
+  $email_threshold = variable_get('spambot_criteria_email', SPAMBOT_DEFAULT_CRITERIA_EMAIL);
+  $username_threshold = variable_get('spambot_criteria_username', SPAMBOT_DEFAULT_CRITERIA_USERNAME);
+  $ip_threshold = variable_get('spambot_criteria_ip', SPAMBOT_DEFAULT_CRITERIA_IP);
+
+  // Build request parameters according to the criteria to use.
+  $request = array();
+  if (!empty($values[$validation_field_names['mail']]) && $email_threshold > 0 && !spambot_check_whitelist('email', $values[$validation_field_names['mail']])) {
+    $request['email'] = $values[$validation_field_names['mail']];
+  }
+
+  if (!empty($values[$validation_field_names['name']]) && $username_threshold > 0 && !spambot_check_whitelist('username', $values[$validation_field_names['name']])) {
+    $request['username'] = $values[$validation_field_names['name']];
+  }
+
+  $ip = ip_address();
+  if ($ip_threshold > 0 && $ip != '127.0.0.1' && $validation_field_names['ip'] && !spambot_check_whitelist('ip', $ip)) {
+    // Make sure we have a valid IPv4 address (API doesn't support IPv6 yet).
+    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === FALSE) {
+      watchdog('spambot', 'Invalid IP address on registration: @ip. Spambot will not rely on it.', array('@ip' => $ip));
+    }
+    else {
+      $request['ip'] = $ip;
+    }
+  }
+
+  // Only do a remote API request if there is anything to check.
+  if ($request && !$form_errors) {
+    $data = array();
+    if (spambot_sfs_request($request, $data)) {
+      $substitutions = array(
+        '@email' => $values[$validation_field_names['mail']],
+        '%email' => $values[$validation_field_names['mail']],
+        '@username' => $values[$validation_field_names['name']],
+        '%username' => $values[$validation_field_names['name']],
+        '@ip' => $ip,
+        '%ip' => $ip,
+      );
+
+      $reasons = array();
+      if ($email_threshold > 0 && !empty($data['email']['appears']) && $data['email']['frequency'] >= $email_threshold) {
+        form_set_error('mail', format_string(variable_get('spambot_blocked_message_email', SPAMBOT_DEFAULT_BLOCKED_MESSAGE), $substitutions));
+        $reasons[] = t('email=@value', array('@value' => $request['email']));
+      }
+      if ($username_threshold > 0 && !empty($data['username']['appears']) && $data['username']['frequency'] >= $username_threshold) {
+        form_set_error('name', format_string(variable_get('spambot_blocked_message_username', SPAMBOT_DEFAULT_BLOCKED_MESSAGE), $substitutions));
+        $reasons[] = t('username=@value', array('@value' => $request['username']));
+      }
+      if ($ip_threshold > 0 && !empty($data['ip']['appears']) && $data['ip']['frequency'] >= $ip_threshold) {
+        form_set_error('', format_string(variable_get('spambot_blocked_message_ip', SPAMBOT_DEFAULT_BLOCKED_MESSAGE), $substitutions));
+        $reasons[] = t('ip=@value', array('@value' => $request['ip']));
+      }
+
+      if ($reasons) {
+        if (variable_get('spambot_log_blocked_registration', TRUE)) {
+          watchdog('spambot', 'Blocked registration: @reasons', array('@reasons' => implode(',', $reasons)));
+
+          $hook_args = array(
+            'request' => $request,
+            'reasons' => $reasons,
+          );
+          module_invoke_all('spambot_registration_blocked', $hook_args);
+        }
+
+        // Slow them down if configured.
+        if ($delay = variable_get('spambot_blacklisted_delay', SPAMBOT_DEFAULT_DELAY)) {
+          sleep($delay);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Invoke www.stopforumspam.com's api.
  *
- * @return
- *   TRUE on successful request (and $data will contain the data), FALSE if error
- * 
- * $data should be an array of the following form: 
- * Array
- * (
- *     [success] => 1
- *     [email] => Array
- *         (
- *             [lastseen] => 2010-01-10 08:41:26
- *             [frequency] => 2
- *             [appears] => 1
- *         )
- * 
- *     [username] => Array
- *         (
- *             [frequency] => 0
- *             [appears] => 0
- *         )
- * )
+ * @param array $query
+ *   A keyed array of url parameters ie. array('email' => 'blah@blah.com').
+ * @param array $data
+ *   An array that will be filled with the data from www.stopforumspam.com.
  *
+ * @return bool
+ *   TRUE on successful request (and $data will contain the data)
+ *   FALSE otherwise.
  */
-function spambot_sfs_request($query, &$data) {
-  // An empty request results in no match
+function spambot_sfs_request(array $query, array &$data) {
+  // An empty request results in no match.
   if (empty($query)) {
     return FALSE;
   }
 
-  // Use php serialisation format
+  // Use php serialisation format.
   $query['f'] = 'serial';
 
   $url = 'http://www.stopforumspam.com/api?' . http_build_query($query, '', '&');
@@ -245,11 +323,17 @@ function spambot_sfs_request($query, &$data) {
       return TRUE;
     }
     else {
-      watchdog('spambot', "Request unsuccessful: @url <pre>\n@dump</pre>", array('@url' => $url, '@dump' => print_r($data, TRUE)));
+      watchdog('spambot', "Request unsuccessful: %url <pre>\n@dump</pre>", array(
+        '%url' => $url,
+        '@dump' => print_r($data, TRUE),
+      ));
     }
   }
   else {
-    watchdog('spambot', "Error contacting service: @url <pre>\n@dump</pre>", array('@url' => $url, '@dump' => print_r($result, TRUE)));
+    watchdog('spambot', "Error contacting service: %url <pre>\n@dump</pre>", array(
+      '%url' => $url,
+      '@dump' => print_r($result, TRUE),
+    ));
   }
 
   return FALSE;
@@ -257,51 +341,69 @@ function spambot_sfs_request($query, &$data) {
 
 /**
  * Checks an account to see if it's a spammer.
- * This one uses configurable automated criteria checking of email and username only
  *
- * @return
- *   positive if spammer, 0 if not spammer, negative if error
+ * This one uses configurable automated criteria checking
+ * of email and username only.
+ *
+ * @param object $account
+ *   User account.
+ *
+ * @return int
+ *   Positive if spammer, 0 if not spammer, negative if error.
  */
 function spambot_account_is_spammer($account) {
-  $email_threshold = variable_get('spambot_criteria_email', 1);
-  $username_threshold = variable_get('spambot_criteria_username', 0);
-  $ip_threshold = variable_get('spambot_criteria_ip', 20);
-  
-  // Build request parameters according to the criteria to use
+  $email_threshold = variable_get('spambot_criteria_email', SPAMBOT_DEFAULT_CRITERIA_EMAIL);
+  $username_threshold = variable_get('spambot_criteria_username', SPAMBOT_DEFAULT_CRITERIA_USERNAME);
+  $ip_threshold = variable_get('spambot_criteria_ip', SPAMBOT_DEFAULT_CRITERIA_IP);
+
+  // Build request parameters according to the criteria to use.
   $request = array();
-  if (!empty($account->mail) && $email_threshold > 0) {
+  if (!empty($account->mail) && $email_threshold > 0 && !spambot_check_whitelist('email', $account->mail)) {
     $request['email'] = $account->mail;
   }
 
-  if (!empty($account->name) && $username_threshold > 0) {
+  if (!empty($account->name) && $username_threshold > 0 && !spambot_check_whitelist('username', $account->name)) {
     $request['username'] = $account->name;
-  }  
+  }
 
-  // Only do a remote API request if there is anything to check
-  if (count($request)) {
+  // Only do a remote API request if there is anything to check.
+  if ($request) {
     $data = array();
     if (spambot_sfs_request($request, $data)) {
-      if (($email_threshold > 0 && !empty($data['email']['appears']) && $data['email']['frequency'] >= $email_threshold) ||
-          ($username_threshold > 0 && !empty($data['username']['appears']) && $data['username']['frequency'] >= $username_threshold)) {
+      if (($email_threshold > 0 && !empty($data['email']['appears']) && $data['email']['frequency'] >= $email_threshold)
+        || ($username_threshold > 0 && !empty($data['username']['appears']) && $data['username']['frequency'] >= $username_threshold)) {
+
         return 1;
       }
     }
     else {
-      // Return error
+      // Return error.
       return -1;
     }
   }
 
   // Now check IP's
-  // If any IP matches the threshold, then flag as a spammer
+  // If any IP matches the threshold, then flag as a spammer.
   if ($ip_threshold > 0) {
     $ips = spambot_account_ip_addresses($account);
     foreach ($ips as $ip) {
-      // Skip the loopback interface
+      // Skip the loopback interface.
       if ($ip == '127.0.0.1') {
         continue;
       }
-      
+      // Make sure we have a valid IPv4 address
+      // (the API doesn't support IPv6 yet).
+      elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === FALSE) {
+        $link = l(t('user'), 'user/' . $account->uid);
+        watchdog('spambot', 'Invalid IP address: %ip (uid=%uid, name=%name, email=%email). Spambot will not rely on it.', array(
+          '%ip' => $ip,
+          '%name' => $account->name,
+          '%email' => $account->mail,
+          '%uid' => $account->uid,
+        ), WATCHDOG_NOTICE, $link);
+        continue;
+      }
+
       $request = array('ip' => $ip);
       $data = array();
       if (spambot_sfs_request($request, $data)) {
@@ -310,56 +412,76 @@ function spambot_account_is_spammer($account) {
         }
       }
       else {
-        // Abort on error
+        // Abort on error.
         return -1;
       }
     }
   }
-  
-  // Return no match
+
+  // Return no match.
   return 0;
 }
 
 /**
- * Retrieves a list of IP addresses for an account
+ * Retrieves a list of IP addresses for an account.
  *
- * @param $account
- *   Account to retrieve IP addresses for
+ * @param object $account
+ *   Account to retrieve IP addresses for.
  *
- * @return
+ * @return array
  *   An array of IP addresses, or an empty array if none found
  */
 function spambot_account_ip_addresses($account) {
   $hostnames = array();
 
-  // Retrieve IPs from node_spambot table
-  $items = db_select('node_spambot')->fields('node_spambot', array('hostname'))
-    ->condition('uid', $account->uid, '=')->distinct()->execute()->fetchCol();
+  // Retrieve IPs from node_spambot table.
+  $items = db_select('node_spambot')
+    ->distinct()
+    ->fields('node_spambot', array('hostname'))
+    ->condition('uid', $account->uid, '=')
+    ->execute()
+    ->fetchCol();
   $hostnames = array_merge($hostnames, $items);
 
-  // Retrieve IPs from any sessions which may still exist
-  $items = db_select('sessions')->fields('sessions', array('hostname'))
-    ->condition('uid', $account->uid, '=')->distinct()->execute()->fetchCol();  
+  // Retrieve IPs from any sessions which may still exist.
+  $items = db_select('sessions')
+    ->distinct()
+    ->fields('sessions', array('hostname'))
+    ->condition('uid', $account->uid, '=')
+    ->execute()
+    ->fetchCol();
   $hostnames = array_merge($hostnames, $items);
 
-  // Retrieve IPs from comments
+  // Retrieve IPs from comments.
   if (module_exists('comment')) {
-    $items = db_select('comment')->fields('comment', array('hostname'))
-      ->condition('uid', $account->uid, '=')->distinct()->execute()->fetchCol();  
+    $items = db_select('comment')
+      ->distinct()
+      ->fields('comment', array('hostname'))
+      ->condition('uid', $account->uid, '=')
+      ->execute()
+      ->fetchCol();
     $hostnames = array_merge($hostnames, $items);
   }
 
-  // Retrieve IPs from statistics
+  // Retrieve IPs from statistics.
   if (module_exists('statistics')) {
-    $items = db_select('accesslog')->fields('accesslog', array('hostname'))
-      ->condition('uid', $account->uid, '=')->distinct()->execute()->fetchCol();  
+    $items = db_select('accesslog')
+      ->distinct()
+      ->fields('accesslog', array('hostname'))
+      ->condition('uid', $account->uid, '=')
+      ->execute()
+      ->fetchCol();
     $hostnames = array_merge($hostnames, $items);
   }
 
-  // Retrieve IPs from user stats
+  // Retrieve IPs from user stats.
   if (module_exists('user_stats')) {
-    $items = db_select('user_stats_ips')->fields('user_stats_ips', array('ip_address'))
-      ->condition('uid', $account->uid, '=')->distinct()->execute()->fetchCol();  
+    $items = db_select('user_stats_ips')
+      ->distinct()
+      ->fields('user_stats_ips', array('ip_address'))
+      ->condition('uid', $account->uid, '=')
+      ->execute()
+      ->fetchCol();
     $hostnames = array_merge($hostnames, $items);
   }
 
@@ -368,43 +490,111 @@ function spambot_account_ip_addresses($account) {
 }
 
 /**
- * Reports an account as a spammer. Requires ip address and evidence of a single incident
+ * Reports an account as a spammer.
  *
- * @param $account
- *   Account to report
- * @param $ip
- *   IP address to report
- * @param $evidence
- *   Evidence to report
+ * Requires ip address and evidence of a single incident.
  *
- * @return
+ * @param object $account
+ *   Account to report.
+ * @param string $ip
+ *   IP address to report.
+ * @param string $evidence
+ *   Evidence to report.
+ *
+ * @return bool
  *   TRUE if successful, FALSE if error
  */
 function spambot_report_account($account, $ip, $evidence) {
   $success = FALSE;
-  $key = variable_get('spambot_sfs_api_key', FALSE);
 
-  if ($key) {
+  if ($key = variable_get('spambot_sfs_api_key', FALSE)) {
     $query['api_key'] = $key;
     $query['email'] = $account->mail;
     $query['username'] = $account->name;
     $query['ip_addr'] = $ip;
-    $query['evidence'] = $evidence;
+    $query['evidence'] = truncate_utf8($evidence, SPAMBOT_MAX_EVIDENCE_LENGTH);
+
+    $url = 'http://www.stopforumspam.com/add.php';
+    $options = array(
+      'headers' => array('Content-type' => 'application/x-www-form-urlencoded'),
+      'method' => 'POST',
+      'data' => http_build_query($query, '', '&'),
+    );
+    $result = drupal_http_request($url, $options);
 
-    $url = 'http://www.stopforumspam.com/add.php?' . http_build_query($query, '', '&');
-    $result = drupal_http_request($url);
     if (!empty($result->code) && $result->code == 200 && !empty($result->data) && stripos($result->data, 'data submitted successfully') !== FALSE) {
       $success = TRUE;
     }
-    else if (stripos($result->data, 'duplicate') !== FALSE) {
-      // www.stopforumspam.com can return a 503 code with data = '<p>recent duplicate entry</p>'
-      //   which we will treat as successful.
+    elseif (stripos($result->data, 'duplicate') !== FALSE) {
+      // www.stopforumspam.com can return a 503 code
+      // with data = '<p>recent duplicate entry</p>'
+      // which we will treat as successful.
       $success = TRUE;
     }
     else {
-      watchdog('spambot', "Error reporting account: @url <pre>\n@dump</pre>", array('@url' => $url, '@dump' => print_r($result, TRUE)));
+      watchdog('spambot', "Error reporting account: %url <pre>\n@dump</pre>", array(
+        '%url' => $url,
+        '@dump' => print_r($result, TRUE),
+      ));
     }
   }
 
   return $success;
 }
+
+/**
+ * Check if current data $type is whitelisted.
+ *
+ * @param string $type
+ *   Type can be one of these three values: 'ip', 'email' or 'username'.
+ * @param string $value
+ *   Value to be checked.
+ *
+ * @return bool
+ *   TRUE if data is whitelisted, FALSE otherwise.
+ */
+function spambot_check_whitelist($type, $value) {
+  switch ($type) {
+    case 'ip':
+      $whitelist_ips = variable_get('spambot_whitelist_ip', '');
+      $result = strpos($whitelist_ips, $value) !== FALSE;
+      break;
+
+    case 'email':
+      $whitelist_usernames = variable_get('spambot_whitelist_email', '');
+      $result = strpos($whitelist_usernames, $value) !== FALSE;
+      break;
+
+    case 'username':
+      $whitelist_emails = variable_get('spambot_whitelist_username', '');
+      $result = strpos($whitelist_emails, $value) !== FALSE;
+      break;
+
+    default:
+      $result = FALSE;
+      break;
+  }
+
+  return $result;
+}
+
+/**
+ * Form builder function to add spambot validations.
+ *
+ * @param array $form
+ *   Form array on which will be added spambot validation.
+ * @param array $options
+ *   Array of options to be added to form.
+ */
+function spambot_add_form_protection(array &$form, array $options = array()) {
+  // Don't add any protections if the user can bypass the Spambot.
+  if (!user_access('protected from spambot scans')) {
+    // Allow other modules to alter the protections applied to this form.
+    drupal_alter('spambot_form_protections', $options, $form);
+
+    $form['#spambot_validation']['name'] = !empty($options['name']) ? $options['name'] : '';
+    $form['#spambot_validation']['mail'] = !empty($options['mail']) ? $options['mail'] : '';
+    $form['#spambot_validation']['ip'] = isset($options['ip']) && is_bool($options['ip']) ? $options['ip'] : TRUE;
+    $form['#validate'][] = 'spambot_user_register_form_validate';
+  }
+}

+ 312 - 142
sites/all/modules/contrib/users/spambot/spambot.pages.inc

@@ -1,17 +1,48 @@
 <?php
 
+/**
+ * @file
+ * User available pages from Spambot module.
+ */
+
+/**
+ * Page callback for 'user/%user/spambot' path.
+ */
+function spambot_user_spam($account) {
+  // Check if current user isn't anonymous user.
+  if (!$account->uid) {
+    drupal_set_message(t("The Anonymous user account can't be reported for spam. If you intended to block a user account verify that the URL is /user/XXXX/spambot where XXXX is a valid UID"), 'warning');
+    return MENU_NOT_FOUND;
+  }
+
+  return drupal_get_form('spambot_user_spam_admin_form', $account);
+}
+
+/**
+ * Form builder for spambot_user_spam_admin_form form.
+ */
 function spambot_user_spam_admin_form($form, &$form_state, $account) {
-  $node_count = db_select('node')->condition('uid', $account->uid, '=')
-    ->countQuery()->execute()->fetchField();
-  
-  if (module_exists('comment')) {
-    $comment_count = db_select('comment')->condition('uid', $account->uid, '=')
-      ->countQuery()->execute()->fetchField();
+  $key = variable_get('spambot_sfs_api_key', FALSE);
+  $comments_enabled = module_exists('comment');
+
+  $node_count = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->condition('uid', $account->uid)
+    ->countQuery()
+    ->execute()
+    ->fetchField();
+
+  $status = t('This account has @n nodes.', array('@n' => $node_count));
+  if ($comments_enabled) {
+    $comment_count = db_select('comment', 'c')
+      ->fields('c', array('cid'))
+      ->condition('uid', $account->uid)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
     $status = t('This account has @n nodes and @c comments.', array('@n' => $node_count, '@c' => $comment_count));
   }
-  else {
-    $status = t('This account has @n nodes.', array('@n' => $node_count));
-  }
 
   $form['check'] = array(
     '#type' => 'submit',
@@ -40,47 +71,88 @@ function spambot_user_spam_admin_form($form, &$form_state, $account) {
     '#title' => t('Report this account to www.stopforumspam.com'),
     '#tree' => TRUE,
     '#collapsible' => TRUE,
-    '#description' => t('An API key from <a href="http://www.stopforumspam.com">www.stopforumspam.com</a> is required to report spammers.<br />Select one or more posts below to report them to www.stopforumspam.com.'),
   );
 
-  // Fetch a list of reportable nodes
+  // Fetch a list of reportable nodes.
   $form['action']['report']['nids'] = array();
-  $result = db_select('node_spambot')->fields('node_spambot', array('nid', 'hostname'))->condition('uid', $account->uid)->orderBy('nid', 'DESC')->range(0, 20)->execute();
+  $result = db_select('node_spambot', 'ns')
+    ->fields('ns', array('nid', 'hostname'))
+    ->condition('ns.uid', $account->uid)
+    ->orderBy('ns.nid', 'DESC')
+    ->range(0, 20)
+    ->execute();
+
   $nid_hostnames = array();
   foreach ($result as $record) {
     $nid_hostnames[$record->nid] = $record->hostname;
   }
 
   foreach ($nid_hostnames as $nid => $hostname) {
-    $node = node_load($nid);
-    if (!empty($node->nid)) {
+    if ($node = node_load($nid)) {
+      $title = truncate_utf8(check_plain($node->title), 128, TRUE, TRUE);
+
       $form['action']['report']['nids'][$nid] = array(
         '#type' => 'checkbox',
-        '#title' => l(mb_strimwidth($node->title, 0, 128, '...'), 'node/' . $nid, array('attributes' => array('title' => mb_strimwidth($node->body['und'][0]['summary'] . "\n\n" . $node->body['und'][0]['value'], 0, 256, '...')))) . ' ' . t('(node, ip=@ip)', array('@ip' => $hostname)),
+        '#title' => l(
+          $title,
+          "node/$nid",
+          array(
+            'attributes' => array(
+              'title' => $title,
+            ),
+          )
+        ) . ' ' . t('(node, ip=@ip)', array('@ip' => $hostname)),
+        '#disabled' => !$key,
       );
     }
   }
 
-  // Fetch a list of reportable comments
-  if (module_exists('comment')) {
+  // Fetch a list of reportable comments.
+  if ($comments_enabled) {
     $form['action']['report']['cids'] = array();
-    $result = db_select('comment')->fields('comment', array('cid'))->condition('uid', $account->uid)->orderBy('cid', 'DESC')->range(0, 20)->execute();
+    $result = db_select('comment')
+      ->fields('comment', array('cid'))
+      ->condition('uid', $account->uid)
+      ->orderBy('cid', 'DESC')
+      ->range(0, 20)
+      ->execute();
+
     $cids = array();
     foreach ($result as $record) {
       $cids[$record->cid] = $record->cid;
     }
 
     foreach ($cids as $cid) {
-      $comment = comment_load($cid);
-      if (!empty($comment->cid)) {
+      if ($comment = comment_load($cid)) {
+        $subject = truncate_utf8(check_plain($comment->subject), 128, TRUE, TRUE);
+
         $form['action']['report']['cids'][$cid] = array(
           '#type' => 'checkbox',
-          '#title' => l(mb_strimwidth($comment->subject, 0, 128, '...'), 'node/' . $comment->nid, array('fragment' => 'comment-'. $comment->cid, 'attributes' => array('title' => mb_strimwidth($comment->comment_body['und'][0]['value'], 0, 256, '...')))) . ' ' . t('(comment, ip=@ip)', array('@ip' => $comment->hostname)),
+          '#title' => l(
+            $subject,
+            "node/$comment->nid",
+            array(
+              'fragment' => "comment-$comment->cid",
+              'attributes' => array(
+                'title' => $subject,
+              ),
+            )
+          ) . ' ' . t('(comment, ip=@ip)', array('@ip' => $comment->hostname)),
+          '#disabled' => !$key,
         );
       }
     }
   }
 
+  if ($key) {
+    $comment_cids = $comments_enabled ? count($form['action']['report']['cids']) : 0;
+    $evidence_count = count($form['action']['report']['nids']) + $comment_cids;
+    $form['action']['report']['#description'] = $evidence_count ? t('Select one or more posts below to report them to www.stopforumspam.com.') : t('This account cannot be reported because no evidence or IP address is available.');
+  }
+  else {
+    $form['action']['report']['#description'] = t('An API key from <a href="http://www.stopforumspam.com">www.stopforumspam.com</a> must <a href="!admin-url">be configured</a> to report spammers.', array('!admin-url' => url('admin/config/system/spambot')));
+  }
+
   $form['action']['block_user'] = array(
     '#type' => 'checkbox',
     '#title' => t('Block this account'),
@@ -90,175 +162,273 @@ function spambot_user_spam_admin_form($form, &$form_state, $account) {
     '#type' => 'checkbox',
     '#title' => t('Delete this account'),
     '#default_value' => FALSE,
-  );  
+  );
   $form['action']['action'] = array(
     '#type' => 'submit',
     '#value' => t('Take action'),
   );
-  $form['uid'] = array('#type' => 'value', '#value' => $account->uid);
+  $form['uid'] = array(
+    '#type' => 'value',
+    '#value' => $account->uid,
+  );
+
+  $form['#validate'][] = 'spambot_user_spam_admin_form_validate';
+  $form['#submit'][] = 'spambot_user_spam_admin_form_submit';
+
   return $form;
 }
 
-function spambot_user_spam_admin_form_validate($form, &$form_state) {
-  $key_required = (!empty($form_state['values']['report']['nids']) && count(array_filter($form_state['values']['report']['nids']))) ? TRUE : FALSE;  
+/**
+ * Validate handler for spambot_user_spam_admin_form() form.
+ */
+function spambot_user_spam_admin_form_validate(&$form, &$form_state) {
+  $key_required = (!empty($form_state['values']['report']['nids']) && count(array_filter($form_state['values']['report']['nids']))) ? TRUE : FALSE;
   if (module_exists('comment')) {
     $key_required = (!empty($form_state['values']['report']['cids']) && count(array_filter($form_state['values']['report']['cids']))) || $key_required;
   }
 
-  if ($key_required && (!variable_get('spambot_sfs_api_key', FALSE))) {
-    form_set_error('', t('To report spammers to www.stopforumspam.com, you need to register for an API key at <a href="http://www.stopforumspam.com">www.stopforumspam.com</a> and enter it into the !page.', array('!page' => l('spambot settings', 'admin/config/system/spambot'))));
+  if ($key_required && !variable_get('spambot_sfs_api_key', FALSE)) {
+    form_set_error('', t('To report spammers to www.stopforumspam.com, you need to register for an API key at <a href="http://www.stopforumspam.com">www.stopforumspam.com</a> and enter it into the !page.', array(
+      '!page' => l(t('spambot settings'), 'admin/config/system/spambot'),
+    )));
   }
 }
 
-function spambot_user_spam_admin_form_submit($form, &$form_state) {
+/**
+ * Submit handler for spambot_user_spam_admin_form() form.
+ */
+function spambot_user_spam_admin_form_submit(&$form, &$form_state) {
   $account = user_load($form_state['values']['uid']);
+
   if ($form_state['values']['op'] == $form_state['values']['check']) {
-    // This is a more comprehensive check than the automated criteria checks.
-    // This tests everything.    
-    $messages = array();
-    $service_down = FALSE;
-
-    // Check email and username
-    $request = array('email' => $account->mail, 'username' => $account->name);
-    $data = array();
-    if (spambot_sfs_request($request, $data)) {
-      if (!empty($data['email']['appears'])) {
-        $messages[] = t('This account\'s email address matches @num times: !link', array('!link' => l($request['email'], 'http://www.stopforumspam.com/search?q=' . $request['email']), '@num' => $data['email']['frequency']));
-      }
+    _spambot_user_spam_admin_form_submit_check($form, $form_state, $account);
+  }
+  elseif ($form_state['values']['op'] == $form_state['values']['action']) {
+    _spambot_user_spam_admin_form_submit_action($form, $form_state, $account);
+  }
+}
 
-      if (!empty($data['username']['appears'])) {
-        $messages[] = t('This account\'s username matches @num times: !link', array('!link' => l($request['username'], 'http://www.stopforumspam.com/search?q=' . $request['username']), '@num' => $data['username']['frequency']));
-      }
-    }
-    else {
-      drupal_set_message(t('Error contacting service.'), 'warning');
-      $service_down = TRUE;
+/**
+ * Do complex checking at this user account.
+ */
+function _spambot_user_spam_admin_form_submit_check(&$form, &$form_state, $account) {
+  $messages = array();
+  $service_down = FALSE;
+
+  // Check email and username.
+  $data = array();
+  $request = array(
+    'email' => $account->mail,
+    'username' => $account->name,
+  );
+
+  if (spambot_sfs_request($request, $data)) {
+    if (!empty($data['email']['appears'])) {
+      $messages[] = array(
+        'text' => t("This account's email address matches %num times: !link", array(
+          '!link' => l($request['email'], 'http://www.stopforumspam.com/search?q=' . $request['email']),
+          '%num' => $data['email']['frequency'],
+        )),
+        'type' => 'warning',
+      );
     }
 
-    // Check IP addresses
-    if (!$service_down) {
-      $ips = spambot_account_ip_addresses($account);
-      foreach ($ips as $ip) {
-        // Skip the loopback interface
-        if ($ip == '127.0.0.1') {
-          continue;
-        }
-        
-        $request = array('ip' => $ip);
-        $data = array();
-        if (spambot_sfs_request($request, $data)) {
-          if (!empty($data['ip']['appears'])) {
-            $messages[] = t('An IP address !ip used by this account matches @num times.', array('!ip' => l($ip, 'http://www.stopforumspam.com/search?q=' . $ip), '@num' => $data['ip']['frequency']));
-          }
-        }
-        else {
-          drupal_set_message(t('Error contacting service.'), 'warning');
-          $service_down = TRUE;
-          break;
-        }
-      }
+    if (!empty($data['username']['appears'])) {
+      $messages[] = array(
+        'text' => t("This account's username matches %num times: !link", array(
+          '!link' => l($request['username'], 'http://www.stopforumspam.com/search?q=' . $request['username']),
+          '%num' => $data['username']['frequency'],
+        )),
+        'type' => 'warning',
+      );
     }
 
-    if (count($messages)) {
-      foreach ($messages as $message) {
-        drupal_set_message($message);
-      }
+    // Check data at whitelist.
+    if (spambot_check_whitelist('email', $account->mail)) {
+      $messages[] = array(
+        'text' => t("This account's email address placed at your whitelist."),
+        'type' => 'status',
+      );
     }
-    else {
-      drupal_set_message(t('No matches against known spammers found.'));
+    if (spambot_check_whitelist('username', $account->name)) {
+      $messages[] = array(
+        'text' => t("This account's username placed at your whitelist."),
+        'type' => 'status',
+      );
     }
   }
-  else if ($form_state['values']['op'] == $form_state['values']['action']) {
-    if ($account->uid == 1) {
-      drupal_set_message(t('Sorry, taking action against uid 1 is not allowed.'));
-      return;
-    }
+  else {
+    drupal_set_message(t('Error contacting service.'), 'warning');
+    $service_down = TRUE;
+  }
+
+  // Check IP addresses.
+  if (!$service_down) {
+    $ips = spambot_account_ip_addresses($account);
+    foreach ($ips as $ip) {
+      // Skip the loopback interface.
+      if ($ip == '127.0.0.1') {
+        continue;
+      }
+      elseif (spambot_check_whitelist('ip', $ip)) {
+        $whitelist_ips[] = $ip;
+        continue;
+      }
+      // Make sure we have a valid IPv4 address
+      // (the API doesn't support IPv6 yet).
+      elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === FALSE) {
+        $messages[] = array(
+          'text' => t('Invalid IP address: @ip. Spambot will not rely on it.', array('@ip' => $ip)),
+          'type' => 'warning',
+        );
+        continue;
+      }
 
-    // Block account
-    if (!empty($form_state['values']['block_user'])) {
-      if ($account->status) {
-        user_save($account, array('status' => 0));
-        drupal_set_message(t('Account blocked.'));
+      $request = array('ip' => $ip);
+      $data = array();
+      if (spambot_sfs_request($request, $data)) {
+        if (!empty($data['ip']['appears'])) {
+          $messages[] = array(
+            'text' => t('An IP address !ip used by this account matches %num times.', array(
+              '!ip' => l($ip, 'http://www.stopforumspam.com/search?q=' . $ip),
+              '%num' => $data['ip']['frequency'],
+            )),
+            'type' => 'warning',
+          );
+        }
       }
       else {
-        drupal_set_message(t('This account is already blocked.'));
+        drupal_set_message(t('Error contacting service.'), 'warning');
+        break;
       }
     }
 
-    // Prepare some data
-    $nodes = db_select('node')->fields('node', array('nid'))
-      ->condition('uid', $account->uid, '=')->orderBy('nid')->execute()->fetchCol();
+    if (!empty($whitelist_ips)) {
+      $messages[] = array(
+        'text' => t('These IP addresses placed at your whitelist: %ips', array('%ips' => implode(', ', $whitelist_ips))),
+        'type' => 'status',
+      );
+    }
+  }
 
-    $node_hostnames = array();
-    $result = db_select('node_spambot')->fields('node_spambot', array('nid', 'hostname'))->condition('uid', $account->uid)->orderBy('nid', 'DESC')->execute();
-    foreach ($result as $record) {
-      $node_hostnames[$record->nid] = $record->hostname;
+  if ($messages) {
+    foreach ($messages as $message) {
+      drupal_set_message($message['text'], $message['type']);
     }
+  }
+  else {
+    drupal_set_message(t('No matches against known spammers found.'));
+  }
+}
+
+/**
+ * Take action under this user account.
+ */
+function _spambot_user_spam_admin_form_submit_action(&$form, &$form_state, $account) {
+  $comments_enabled = module_exists('comment');
 
-    $comments = array();
-    if (module_exists('comment')) {
-      $comments = db_select('comment')->fields('comment', array('cid'))
-        ->condition('uid', $account->uid, '=')->orderBy('cid')->execute()->fetchCol();
+  if ($account->uid == 1) {
+    drupal_set_message(t('Sorry, taking action against uid 1 is not allowed.'), 'warning');
+    return;
+  }
+
+  // Block account.
+  if (!empty($form_state['values']['block_user'])) {
+    if ($account->status) {
+      user_save($account, array('status' => 0));
+      drupal_set_message(t('Account blocked.'));
+    }
+    else {
+      drupal_set_message(t('This account is already blocked.'));
     }
+  }
+
+  // Prepare some data.
+  $nodes = db_select('node')
+    ->fields('node', array('nid'))
+    ->condition('uid', $account->uid, '=')
+    ->orderBy('nid')
+    ->execute()
+    ->fetchCol();
+
+  $node_hostnames = array();
+  $result = db_select('node_spambot')
+    ->fields('node_spambot', array('nid', 'hostname'))
+    ->condition('uid', $account->uid)
+    ->orderBy('nid', 'DESC')
+    ->execute();
+  foreach ($result as $record) {
+    $node_hostnames[$record->nid] = $record->hostname;
+  }
 
-    // Report posts to www.stopforumspam.com
-    if (!empty($form_state['values']['report']['nids'])) {
-      foreach (array_filter($form_state['values']['report']['nids']) as $nid => $unused) {
-        $node = node_load($nid);
-        if (!empty($node->nid)) {
-          if (spambot_report_account($account, $node_hostnames[$nid], $node->title . "\n\n" . $node->body['und'][0]['summary'] . "\n\n" . $node->body['und'][0]['value'])) {
-            drupal_set_message(t('Node %title has been reported.', array('%title' => $node->title)));
-          }
-          else {
-            drupal_set_message(t('There was a problem reporting node %title.', array('%title' => $node->title)));
-          }
+  $comments = array();
+  if ($comments_enabled) {
+    $comments = db_select('comment')
+      ->fields('comment', array('cid'))
+      ->condition('uid', $account->uid, '=')
+      ->orderBy('cid')
+      ->execute()
+      ->fetchCol();
+  }
+
+  // Report posts to www.stopforumspam.com.
+  if (!empty($form_state['values']['report']['nids'])) {
+    foreach (array_filter($form_state['values']['report']['nids']) as $nid => $unused) {
+      $node = node_load($nid);
+      if (!empty($node->nid)) {
+        if (spambot_report_account($account, $node_hostnames[$nid], $node->title . "\n\n" . $node->body[LANGUAGE_NONE][0]['summary'] . "\n\n" . $node->body[LANGUAGE_NONE][0]['value'])) {
+          drupal_set_message(t('Node %title has been reported.', array('%title' => $node->title)));
+        }
+        else {
+          drupal_set_message(t('There was a problem reporting node %title.', array('%title' => $node->title)));
         }
       }
     }
+  }
 
-    if (module_exists('comment') && !empty($form_state['values']['report']['cids'])) {
-      foreach (array_filter($form_state['values']['report']['cids']) as $cid => $unused) {
-        $comment = comment_load($cid);
-        if (!empty($comment->cid)) {
-          if (spambot_report_account($account, $comment->hostname, $comment->subject . "\n\n" . $comment->comment_body['und'][0]['value'])) {
-            drupal_set_message(t('Comment %title has been reported.', array('%title' => $comment->subject)));
-          }
-          else {
-            drupal_set_message(t('There was a problem reporting comment %title.', array('%title' => $comment->subject)));
-          }
+  if ($comments_enabled && !empty($form_state['values']['report']['cids'])) {
+    foreach (array_filter($form_state['values']['report']['cids']) as $cid => $unused) {
+      $comment = comment_load($cid);
+      if (!empty($comment->cid)) {
+        if (spambot_report_account($account, $comment->hostname, $comment->subject . "\n\n" . $comment->comment_body[LANGUAGE_NONE][0]['value'])) {
+          drupal_set_message(t('Comment %title has been reported.', array('%title' => $comment->subject)));
+        }
+        else {
+          drupal_set_message(t('There was a problem reporting comment %title.', array('%title' => $comment->subject)));
         }
       }
     }
+  }
 
-    // Delete nodes and content
-    if (!empty($form_state['values']['delete_content'])) {
-      node_delete_multiple($nodes);
-
-      if (count($comments)) {
-        comment_delete_multiple($comments);
-      }
+  // Delete nodes and content.
+  if (!empty($form_state['values']['delete_content'])) {
+    node_delete_multiple($nodes);
 
-      drupal_set_message(t('Nodes and comments have been deleted.'));
+    if ($comments) {
+      comment_delete_multiple($comments);
     }
-    else if (!empty($form_state['values']['unpublish_content'])) {
-      // Unpublish nodes and content
-      if (count($nodes)) {
-        module_load_include('inc', 'node', 'node.admin');
-        node_mass_update($nodes, array('status' => 0));
-      }
 
-      if (count($comments)) {
-        db_update('comment')->fields(array('status' => COMMENT_NOT_PUBLISHED))
-          ->condition('uid', $account->uid)->execute();
-        cache_clear_all();
-      }
-      drupal_set_message(t('Nodes and comments have been unpublished.'));
+    drupal_set_message(t('Nodes and comments have been deleted.'));
+  }
+  elseif (!empty($form_state['values']['unpublish_content'])) {
+    // Unpublish nodes and content.
+    if ($nodes) {
+      module_load_include('inc', 'node', 'node.admin');
+      node_mass_update($nodes, array('status' => 0));
     }
 
-    // Delete user
-    if (!empty($form_state['values']['delete_user'])) {
-      // Redirect to user delete form
-      $form_state['redirect'] = 'user/' . $account->uid . '/cancel';
+    if ($comments) {
+      db_update('comment')
+        ->fields(array('status' => COMMENT_NOT_PUBLISHED))
+        ->condition('uid', $account->uid)
+        ->execute();
     }
+    drupal_set_message(t('Nodes and comments have been unpublished.'));
+  }
+
+  // Delete user.
+  if (!empty($form_state['values']['delete_user'])) {
+    // Redirect to user delete form.
+    $form_state['redirect'] = "user/$account->uid/cancel";
   }
 }

Some files were not shown because too many files changed in this diff