Sfoglia il codice sorgente

updated synonyms to 1.3

Bachir Soussi Chiadmi 7 anni fa
parent
commit
8d24211bf5
37 ha cambiato i file con 3322 aggiunte e 1456 eliminazioni
  1. 48 19
      sites/all/modules/contrib/taxonomy/synonyms/README.txt
  2. 14 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms.help.ini
  3. 6 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms.html
  4. 27 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behavior_implementation.html
  5. 19 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behaviors.html
  6. 0 116
      sites/all/modules/contrib/taxonomy/synonyms/includes/AbstractSynonymsExtractor.class.inc
  7. 84 0
      sites/all/modules/contrib/taxonomy/synonyms/includes/EntityReferenceSynonymsBehavior.class.inc
  8. 0 70
      sites/all/modules/contrib/taxonomy/synonyms/includes/EntityReferenceSynonymsExtractor.class.inc
  9. 188 0
      sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsBehavior.interface.inc
  10. 0 48
      sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsSynonymsExtractor.class.inc
  11. 88 0
      sites/all/modules/contrib/taxonomy/synonyms/includes/TaxonomySynonymsBehavior.class.inc
  12. 0 94
      sites/all/modules/contrib/taxonomy/synonyms/includes/TaxonomySynonymsExtractor.class.inc
  13. 68 0
      sites/all/modules/contrib/taxonomy/synonyms/includes/TextSynonymsBehavior.class.inc
  14. 24 0
      sites/all/modules/contrib/taxonomy/synonyms/js/synonyms-autocomplete.js
  15. 147 0
      sites/all/modules/contrib/taxonomy/synonyms/plugins/arguments/term_synonyms.inc
  16. 41 0
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/autocomplete.inc
  17. 41 0
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/select.inc
  18. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/synonyms.inc
  19. 98 123
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.api.php
  20. 1 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.css
  21. 11 8
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.info
  22. 146 4
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.install
  23. 849 326
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.module
  24. 95 144
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.pages.inc
  25. 443 426
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.test
  26. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchEntityReferenceSynonymsBehavior.class.inc
  27. 14 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchSynonymsBehavior.interface.inc
  28. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchTaxonomySynonymsBehavior.class.inc
  29. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchTextSynonymsBehavior.class.inc
  30. 36 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/plugins/behavior/search.inc
  31. 20 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.info
  32. 115 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.module
  33. 68 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.pages.inc
  34. 367 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.test
  35. 18 2
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms.views.inc
  36. 174 0
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_term_tid.inc
  37. 24 76
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_plugin_argument_validate_taxonomy_term.inc

+ 48 - 19
sites/all/modules/contrib/taxonomy/synonyms/README.txt

@@ -3,11 +3,15 @@
 
 The Synonyms module extends the Drupal core Taxonomy features. Currently
 the module provides this additional functionality:
-* support of synonyms through Field API. Any field, for which synonyms extractor
-   is created, attached to a term can be enabled as source of synonyms.
-* synonym-friendly autocomplete widget for taxonomy_term_reference fields
-* integration with Drupal search functionality enabling searching content by
-   synonyms of the terms that the content references
+* support of synonyms through Field API. Any field, for which synonyms behavior
+   exists, can be enabled as source of synonyms.
+* synonyms-friendly autocomplete and select widgets for taxonomy_term_reference
+   fields
+* integration with Drupal search functionality through Synonyms Search
+   submodule. It enables searching content by synonyms of the terms that the
+   content references. Synonyms Search submodule also integrates with Term
+   Search contributed module in a fashion that allows your terms to be found by
+   their synonyms.
 * integration with Search API. If you include synonyms of a term into your
    Search API search index, your clients will be able to find content with
    search keywords that contain synonyms and not actual names of terms.
@@ -16,11 +20,11 @@ the module provides this additional functionality:
 
 The Synonyms module requires the following modules:
 * Taxonomy module
-* Text module
+* CTools module
 
--- SYNONYMS EXTRACTORS, SUPPORTED FIELD TYPES --
+-- SYNONYMS BEHAVIOR, SUPPORTED FIELD TYPES --
 
-Module ships with ability to extract synonyms from the following field types:
+Module ships with ability to use the following field types as synonyms:
 * Text
 * Taxonomy Term Reference
 * Entity Reference
@@ -28,12 +32,38 @@ Module ships with ability to extract synonyms from the following field types:
 * Float
 * Decimal
 
-If you want to implement your own synonyms extractor that would enable support
-for any other field type, please, refer to synonyms.api.php file for
-instructions on how to do it, or file an issue against Synonyms module. We will
-try to implement support for your field type too. If you have written your
-synonyms extractor, please share by opening an issue, and it will be included
-into this module.
+If you want to implement your own synonyms behavior that would enable support
+for any other field type, refer to synonyms.api.php file for instructions on how
+to do it, or file an issue against Synonyms module. We will try to implement
+support for your field type too. If you have written your synonyms behavior
+implementation, please share by opening an issue, and it will be included into
+this module.
+
+-- GRANULATION WITHIN SYNONYMS BEHAVIOR --
+
+In order to achieve greater flexibility, this module introduced additional
+granularity into what "synonyms" mean. Then you can enable different synonyms
+behaviors for different fields. For example, field "Typos" can be part of
+autocomplete behavior, while field "Other spellings" can be part of search
+integration behavior. Currently the following synonym behaviors are recognized
+(other modules actually can extend this list):
+* General synonym - normally we suggest to enable this behavior for all fields
+   that have enabled at least one another behavior. In technical words, this
+   behavior is responsible for including content of the field into term synonyms
+   and also enables ability to add entities as synonym into this field.
+* Autocomplete - whether content of this field should participate in
+   autocomplete suggestions. This module ships an autocomplete synonyms friendly
+   widget and its autocomplete suggestions will be filled in with the content of
+   the fields that have enabled this behavior.
+* Select - whether content of this field should be included in the synonyms
+   friendly select widget.
+* Search integration (requires Synonyms Search enabled) - allows your content to
+   be found by synonyms of the terms it references. Your nodes  will be found by
+   all synonyms that have this behavior enabled.
+
+Therefore, on the vocabulary edit page you will see a table, where rows are
+fields that can become synonyms and columns are these "synonym behaviors" and
+you decide what synonym behaviors to activate on what fields.
 
 -- INSTALLATION --
 
@@ -42,15 +72,14 @@ into this module.
 -- CONFIGURATION --
 
 * The module itself does not provide any configuration as of the moment.
-Although during creation/editing of a Taxonomy vocabulary you will be able
-to enable/disable for that particular vocabulary the additional functionality
-this module provides, you will find additional fieldset at the bottom of
-vocabulary edit page.
+Although during editing of a Taxonomy vocabulary you will be specify for that
+particular vocabulary the additional functionality this module provides, you
+will find additional fieldset at the bottom of the vocabulary edit page.
 
 -- FUTURE DEVELOPMENT --
 
 * If you are interested into converting this module from synonyms for Taxonomy
-terms into synonyms for any entity types, please go to this issue
+terms into synonyms for any entity type, please go to this issue
 http://drupal.org/node/1194802 and leave a comment. Once we see some demand for
 this great feature and the Synonyms module gets a little more mature, we will
 try to make it happen.

+ 14 - 0
sites/all/modules/contrib/taxonomy/synonyms/help/synonyms.help.ini

@@ -0,0 +1,14 @@
+[advanced help settings]
+line break = TRUE
+
+[synonyms]
+title = Synonyms
+
+[synonyms_behaviors]
+title = Creating a new synonyms behavior
+parent = synonyms
+weight = -1
+
+[synonyms_behavior_implementation]
+title = Creating a new behavior implementation
+parent = synonyms

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


+ 27 - 0
sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behavior_implementation.html

@@ -0,0 +1,27 @@
+At this point you must possess good technical knowledge about what synonyms behaviors are. In this article we will show how you can implement an arbitrary behavior for an arbitrary field type.
+
+By implementing I mean to provide integration between a field type (between how that field type stores and encodes synonyms data in the database) and a synonym behavior (how that behavior requires to work with synonyms data). It must sound a bit too baked, but the ongoing paragraphs should shed more light onto it.
+
+Throughout writing your own synonyms behavior implementation you can always look into Synonyms module source code to get better understanding. You will find the behavior implementatios in <em>synonyms/includes/*SynonymsBehavior.class.inc</em> files.
+
+Creating a new implementation pretty much consists of 2 steps:
+<ol>
+    <li>Implementing behavior interface for a particular field type.</li>
+    <li>Notifying Synonyms module about your new PHP class and what field types and what behavior that PHP class is responsible for.</li>
+</ol>
+
+Now let us see each of the steps in further details.
+
+<h2>Implementing behavior interface</h2>
+
+Look up in behavior cTools plugin definition of your interest what interface it declares. The cTools plugin must be of type "behavior" owned by "synonyms" module. The interface is declared under the "interface" property. Read the documentation for that interface and write a PHP class that implements this interface. We cannot give more precise instructions about this step, because it all depends on the interface of the behavior.
+
+<h2>Notifying Synonyms module about your new implementation</h2>
+
+For the purposes of such notification we have 2 hooks in Synonyms module:
+<ul>
+    <li>hook_synonyms_behavior_implementation_info() to collect info from modules about existing behavior implementations</li>
+    <li>hook_synonyms_behavior_implementation_info_alter() to alter info about existing behavior implementations, for example, if you want to overwrite behavior implementation introduced in another module.</li>
+</ul>
+
+Implementing either of the 2 hooks is highly straight forward, you will just inform the Synonyms module for requested behavior for what field types you have implementations in what PHP classes. For more details, refer to synonyms.api.php file or look into synonyms_synonyms_behavior_implementation_info() function.

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


+ 0 - 116
sites/all/modules/contrib/taxonomy/synonyms/includes/AbstractSynonymsExtractor.class.inc

@@ -1,116 +0,0 @@
-<?php
-
-/**
- * @file
- * Define interface required for extracting synonyms from field types.
- */
-
-abstract class AbstractSynonymsExtractor {
-
-  /**
-   * Return array of supported field types for synonyms extraction.
-   *
-   * @return array
-   *   Array of Field API field types from which this class is able to extract
-   *   synonyms
-   */
-  public static function fieldTypesSupported() {
-    return array();
-  }
-
-  /**
-   * Extract synonyms from a field attached to an entity.
-   *
-   * We try to pass as many info about context as possible, however, normally
-   * you will only need $items to extract the synonyms.
-   *
-   * @param array $items
-   *   Array of items
-   * @param array $field
-   *   Array of field definition according to Field API
-   * @param array $instance
-   *   Array of instance definition according to Field API
-   * @param object $entity
-   *   Fully loaded entity object to which the $field and $instance with $item
-   *   values is attached to
-   * @param string $entity_type
-   *   Type of the entity $entity according to Field API definition of entity
-   *   types
-   *
-   * @return array
-   *   Array of synonyms extracted from $items
-   */
-  public static function synonymsExtract($items, $field, $instance, $entity, $entity_type) {
-    return array();
-  }
-
-  /**
-   * Allow you to hook in during autocomplete suggestions generation.
-   *
-   * Allow you to include entities for autocomplete suggestion that are possible
-   * candidates based on your field as a source of synonyms. This method is
-   * void, however, you have to alter and add your condition to $query
-   * parameter.
-   *
-   * @param string $tag
-   *   What user has typed in into autocomplete widget. Normally you would
-   *   run LIKE '%$tag%' on your column
-   * @param EntityFieldQuery $query
-   *   EntityFieldQuery object where you should put your conditions to
-   * @param array $field
-   *   Array of field definition according to Field API
-   * @param array $instance
-   *   Array of instance definition according to Field API
-   */
-  public static function processEntityFieldQuery($tag, EntityFieldQuery $query, $field, $instance) {}
-
-  /**
-   * Add an entity as a synonym into a field of another entity.
-   *
-   * Basically this method should be called when you want to add some entity
-   * as a synonym to another entity (for example when you merge one entity
-   * into another and besides merging want to add synonym of the merging
-   * entity into the trunk entity). You should extract synonym value (according
-   * to what value is expected in this field) and return it. We try to provide
-   * you with as much of context as possible, but normally you would only need
-   * $synonym_entity and $synonym_entity_type parameters. Return an empty array
-   * if entity of type $synonym_entity_type cannot be converted into a format
-   * expected by $field.
-   *
-   * @param array $items
-   *   Array items that already exist in the field into which new synonyms is to
-   *   be added
-   * @param array $field
-   *   Field array definition according to Field API of the field into which new
-   *   synonym is to be added
-   * @param array $instance
-   *   Instance array definition according to Field API of the instance into
-   *   which new synonym is to be added
-   * @param object $synonym_entity
-   *   Fully loaded entity object which has to be added as synonym
-   * @param string $synonym_entity_type
-   *   Entity type of $synonym_entity
-   *
-   * @return array
-   *   Array of extra items to be merged into the items that already exist in
-   *   field values
-   */
-  public static function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
-    return array();
-  }
-
-  /**
-   * Supportive method.
-   *
-   * Set such a condition on $query that it will always yield no results. Should
-   * be called from $this->processEntityFieldQuery() when for whatever reason
-   * the object can't alter $query to include matched synonyms. As a fallback it
-   * should call this method to make sure it filtered everything out.
-   *
-   * @param EntityFieldQuery $query
-   *   Query object passed to $this->processEntityFieldQuery() method
-   */
-  protected static function emptyResultsCondition(EntityFieldQuery $query) {
-    $query->range(0, 0);
-  }
-}

+ 84 - 0
sites/all/modules/contrib/taxonomy/synonyms/includes/EntityReferenceSynonymsBehavior.class.inc

@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Enables Entity Reference field type to be source of synonyms.
+ */
+
+/**
+ * Definition of EntityReferenceSynonymsBehavior class.
+ */
+class EntityReferenceSynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior, AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+
+  public function extractSynonyms($items, $field, $instance, $entity, $entity_type) {
+    $synonyms = array();
+
+    $target_tids = array();
+    foreach ($items as $item) {
+      $target_tids[] = $item['target_id'];
+    }
+    $entities = entity_load($field['settings']['target_type'], $target_tids);
+    foreach ($entities as $entity) {
+      $synonyms[] = entity_label($field['settings']['target_type'], $entity);
+    }
+
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
+    // Firstly validating that this entity reference is able to reference to
+    // that type of entity.
+    $expected_synonym_entity_type = $field['settings']['target_type'];
+    if ($expected_synonym_entity_type != $synonym_entity_type) {
+      return array();
+    }
+    $synonym_entity_id = entity_id($synonym_entity_type, $synonym_entity);
+    return array(array(
+      'target_id' => $synonym_entity_id,
+    ));
+  }
+
+  public function synonymItemHash($item, $field, $instance) {
+    return $field['settings']['target_type'] . $item['target_id'];
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition, $field, $instance) {
+    if ($field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsSynonymsBehaviorException(t('Not supported storage engine %type in synonymsFind() method.', array(
+        '%type' => $field['storage']['type'],
+      )));
+    }
+    $table = array_keys($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['target_id'];
+
+    $query = db_select($table, 'field');
+
+    $target_entity_type_info = entity_get_info($field['settings']['target_type']);
+    if (!isset($target_entity_type_info['base table']) || !$target_entity_type_info['base table']) {
+      throw new SynonymsSynonymsBehaviorException(t('Target entity type %entity_type is not stored in database.', array(
+        '%entity_type' => $field['settings']['target_type'],
+      )));
+    }
+    if (!isset($target_entity_type_info['entity keys']['id'])) {
+      throw new SynonymsSynonymsBehaviorException(t('Target entity type %entity_type does not declare primary key.', array(
+        '%entity_type' => $field['settings']['target_type'],
+      )));
+    }
+    if (!isset($target_entity_type_info['entity keys']['label'])) {
+      throw new SynonymsSynonymsBehaviorException(t('Target entity type %entity_type does not declare label column.', array(
+        '%entity_type' => $field['settings']['target_type'],
+      )));
+    }
+
+    $target_entity_alias = $query->innerJoin($target_entity_type_info['base table'], 'target_entity', 'field.' . $column . ' = target_entity.' . $target_entity_type_info['entity keys']['id']);
+    $query->addField($target_entity_alias, $target_entity_type_info['entity keys']['label'], 'synonym');
+    $query->fields('field', array('entity_id'));
+    $query->condition('field.entity_type', $instance['entity_type']);
+    $query->condition('field.bundle', $instance['bundle']);
+
+    $this->synonymsFindProcessCondition($condition, $target_entity_alias . '.' . $target_entity_type_info['entity keys']['label']);
+    $query->condition($condition);
+    return $query->execute();
+  }
+}

+ 0 - 70
sites/all/modules/contrib/taxonomy/synonyms/includes/EntityReferenceSynonymsExtractor.class.inc

@@ -1,70 +0,0 @@
-<?php
-
-/**
- * @file
- * Enables Entity Reference field type to be source of synonyms.
- */
-
-class EntityReferenceSynonymsExtractor extends AbstractSynonymsExtractor {
-
-  public static function fieldTypesSupported() {
-    return array('entityreference');
-  }
-
-  public static function synonymsExtract($items, $field, $instance, $entity, $entity_type) {
-    $synonyms = array();
-
-    // For speading up loading all the entities at once.
-    $target_tids = array();
-    foreach ($items as $item) {
-      $target_tids[] = $item['target_id'];
-    }
-    $entities = entity_load($field['settings']['target_type'], $target_tids);
-    foreach ($entities as $entity) {
-      $synonyms[] = entity_label($field['settings']['target_type'], $entity);
-    }
-
-    return $synonyms;
-  }
-
-  public static function processEntityFieldQuery($tag, EntityFieldQuery $query, $field, $instance) {
-    // Unfortunately EntityFieldQuery does not currently support INNER JOINing
-    // referenced entities via any field type.
-    // Thus, we use an ugly solution -- going through all entities that exist
-    // in such entity type trying to see if there is a match by entity's label.
-    $efq = new EntityFieldQuery();
-    $efq->entityCondition('entity_type', $field['settings']['target_type']);
-    // Additionally we have to figure out which column in the entity table
-    // represents entity label.
-    $entity_info = entity_get_info($field['settings']['target_type']);
-    if (!isset($entity_info['entity keys']['label'])) {
-      // We can't get any matches if we do not know what column to query
-      // against. So we add a condition to $query which will 100% yield empty
-      // results.
-      self::emptyResultsCondition($query);
-      return;
-    }
-    $efq->propertyCondition($entity_info['entity keys']['label'], '%' . $tag . '%', 'LIKE');
-    $result = $efq->execute();
-
-    if (!isset($result[$field['settings']['target_type']]) || !is_array($result[$field['settings']['target_type']])) {
-      self::emptyResultsCondition($query);
-      return;
-    }
-    $result = $result[$field['settings']['target_type']];
-    $query->fieldCondition($field, 'target_id', array_keys($result));
-  }
-
-  public static function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
-    // Firstly validating that this entity reference is able to reference to
-    // that type of entity.
-    $expected_synonym_entity_type = $field['settings']['target_type'];
-    if ($expected_synonym_entity_type != $synonym_entity_type) {
-      return array();
-    }
-    $synonym_entity_id = entity_id($synonym_entity_type, $synonym_entity);
-    return array(array(
-      'target_id' => $synonym_entity_id,
-    ));
-  }
-}

+ 188 - 0
sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsBehavior.interface.inc

@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * @file
+ * Interfaces of synonyms behaviors that are shipped with Synonyms module.
+ */
+
+/**
+ * General interface of a synonyms behavior.
+ *
+ * All synonyms behaviors must extend this interface.
+ */
+interface SynonymsBehavior {
+}
+
+/**
+ * Interface of "synonyms" behavior.
+ *
+ * The most basic synonyms behavior.
+ */
+interface SynonymsSynonymsBehavior extends SynonymsBehavior {
+
+  /**
+   * Extract synonyms from a field attached to an entity.
+   *
+   * We try to pass as many info about context as possible, however, normally
+   * you will only need $items to extract the synonyms.
+   *
+   * @param array $items
+   *   Array of items
+   * @param array $field
+   *   Array of field definition according to Field API
+   * @param array $instance
+   *   Array of instance definition according to Field API
+   * @param object $entity
+   *   Fully loaded entity object to which the $field and $instance with $item
+   *   values is attached to
+   * @param string $entity_type
+   *   Type of the entity $entity according to Field API definition of entity
+   *   types
+   *
+   * @return array
+   *   Array of synonyms extracted from $items
+   */
+  public function extractSynonyms($items, $field, $instance, $entity, $entity_type);
+
+  /**
+   * Add an entity as a synonym into a field of another entity.
+   *
+   * Basically this method should be called when you want to add some entity
+   * as a synonym to another entity (for example when you merge one entity
+   * into another and besides merging want to add synonym of the merged entity
+   * into the trunk entity). You should extract synonym value (according to what
+   * value is expected in this field) and return it. We try to provide you with
+   * as much of context as possible, but normally you would only need
+   * $synonym_entity and $synonym_entity_type parameters. Return an empty array
+   * if entity of type $synonym_entity_type cannot be converted into a format
+   * expected by $field.
+   *
+   * @param array $items
+   *   Array items that already exist in the field into which new synonyms is to
+   *   be added
+   * @param array $field
+   *   Field array definition according to Field API of the field into which new
+   *   synonym is to be added
+   * @param array $instance
+   *   Instance array definition according to Field API of the instance into
+   *   which new synonym is to be added
+   * @param object $synonym_entity
+   *   Fully loaded entity object which has to be added as synonym
+   * @param string $synonym_entity_type
+   *   Entity type of $synonym_entity
+   *
+   * @return array
+   *   Array of extra items to be merged into the items that already exist in
+   *   field values
+   */
+  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type);
+
+  /**
+   * Hash a field item that is enabled as synonym.
+   *
+   * Your hash function must return such hash that for 2 items that yield the
+   * same synonyms their hash must be the same. There is no limit on minimal or
+   * maximum hash length, but keep it reasonable, something below 512 symbols.
+   * Also, your hash function should strive to minimize hash collisions, i.e.
+   * when 2 different items yield the same hash.
+   *
+   * @param array $item
+   *   Field item whose hash is requested
+   * @param array $field
+   *   Field from which the $item comes from
+   * @param array $instance
+   *   Instance from which the $item comes from
+   *
+   * @return string
+   *   Hash of the provided $item
+   */
+  public function synonymItemHash($item, $field, $instance);
+
+  /**
+   * Look up entities by their synonyms within a provided field.
+   *
+   * You are provided with a SQL condition that you should apply to the storage
+   * of synonyms within the provided field. And then return result: what
+   * entities match by the provided condition through what synonyms.
+   *
+   * @param QueryConditionInterface $condition
+   *   Condition that defines what to search for. It may contain a placeholder
+   *   of AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER which you should
+   *   replace by the column name where the synonyms data for your field is
+   *   stored in plain text. For it to do, you may extend the
+   *   AbstractSynonymsSynonymsBehavior class and then just invoke the
+   *   AbstractSynonymsSynonymsBehavior->synonymsFindProcessCondition() method,
+   *   so you won't have to worry much about it
+   * @param array $field
+   *   Field API field definition array of the field within which the search
+   *   for synonyms should be performed
+   * @param array $instance
+   *   Field API instance definition array of the instance within which the
+   *   search for synonyms should be performed
+   *
+   * @return Traversable
+   *   Traversable result set of found synonyms and entity IDs to which those
+   *   belong. Each element in the result set should be an object and will have
+   *   the following structure:
+   *   - synonym: (string) Synonym that was found and which satisfies the
+   *     provided condition
+   *   - entity_id: (int) ID of the entity to which the found synonym belongs
+   */
+  public function synonymsFind(QueryConditionInterface $condition, $field, $instance);
+}
+
+/**
+ * Exception thrown by implementations of SynonymsSynonymsBehavior interface.
+ */
+class SynonymsSynonymsBehaviorException extends Exception {}
+
+/**
+ * Starting point for implementing SynonymsSynonymsBehavior interface.
+ */
+abstract class AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior {
+
+  /**
+   * Constant which denotes placeholder of a synonym column.
+   *
+   * @var string
+   */
+  const COLUMN_PLACEHOLDER = '***COLUMN***';
+
+  /**
+   * Process condition in 'synonymsFind' method.
+   *
+   * Process condition in 'synonymsFind' method replacing all references of
+   * synonym column with the real name of that column.
+   *
+   * @param QueryConditionInterface $condition
+   *   Condition that should be processed
+   * @param string $column
+   *   Real name of the synonym column
+   */
+  protected function synonymsFindProcessCondition(QueryConditionInterface $condition, $column) {
+    $condition_array = &$condition->conditions();
+    foreach ($condition_array as &$v) {
+      if (is_array($v) && isset($v['field'])) {
+        if ($v['field'] instanceof QueryConditionInterface) {
+          // Recursively process this condition too.
+          $this->synonymsFindProcessCondition($v['field'], $column);
+        }
+        else {
+          $v['field'] = str_replace(self::COLUMN_PLACEHOLDER, $column, $v['field']);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Interface of the autocomplete synonyms behavior.
+ */
+interface AutocompleteSynonymsBehavior extends SynonymsSynonymsBehavior {
+}
+
+/**
+ * Interface of the synonyms friendly select behavior.
+ */
+interface SelectSynonymsBehavior extends SynonymsSynonymsBehavior {
+}

+ 0 - 48
sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsSynonymsExtractor.class.inc

@@ -1,48 +0,0 @@
-<?php
-
-/**
- * @file
- * Default Synonyms Extractor class that ships together with the Synonym module.
- */
-
-class SynonymsSynonymsExtractor extends AbstractSynonymsExtractor {
-
-  public static function fieldTypesSupported() {
-    return array('text', 'number_integer', 'number_float', 'number_decimal');
-  }
-
-  public static function synonymsExtract($items, $field, $instance, $entity, $entity_type) {
-    $synonyms = array();
-
-    foreach ($items as $item) {
-      $synonyms[] = $item['value'];
-    }
-
-    return $synonyms;
-  }
-
-  public static function processEntityFieldQuery($tag, EntityFieldQuery $query, $field, $instance) {
-    $query->fieldCondition($field, 'value', '%' . $tag . '%', 'LIKE');
-  }
-
-  public static function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
-    $synonym = entity_label($synonym_entity_type, $synonym_entity);
-    switch ($field['type']) {
-      case 'text':
-        break;
-
-      // We add synonyms for numbers only if $synonym is a number.
-      case 'number_integer':
-      case 'number_float':
-      case 'number_decimal':
-        if (!is_numeric($synonym)) {
-          return array();
-        }
-        break;
-
-    }
-    return array(array(
-      'value' => $synonym,
-    ));
-  }
-}

+ 88 - 0
sites/all/modules/contrib/taxonomy/synonyms/includes/TaxonomySynonymsBehavior.class.inc

@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Enables Taxonomy Term Reference field type to be source of synonyms.
+ */
+
+/**
+ * Definition of TaxonomySynonymsBehavior class.
+ */
+class TaxonomySynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior, AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+
+  public function extractSynonyms($items, $field, $instance, $entity, $entity_type) {
+    $synonyms = array();
+
+    $terms = array();
+    foreach ($items as $item) {
+      $terms[] = $item['tid'];
+    }
+    $terms = taxonomy_term_load_multiple($terms);
+    foreach ($terms as $term) {
+      $synonyms[] = entity_label('taxonomy_term', $term);
+    }
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
+    // Taxonomy term reference supports only referencing of entity types
+    // 'taxonomy_term'.. duh.
+    if ($synonym_entity_type != 'taxonomy_term') {
+      return array();
+    }
+    // Checking that $field is configured to reference the vocabulary of
+    // $synonym_entity term.
+    $is_allowed = FALSE;
+    foreach ($field['settings']['allowed_values'] as $setting) {
+      if ($synonym_entity->vocabulary_machine_name == $setting['vocabulary']) {
+        if ($setting['parent'] == 0) {
+          // No need to check parent property as there is no limitation on it.
+          $is_allowed = TRUE;
+          break;
+        }
+        else {
+          foreach (taxonomy_get_parents_all($synonym_entity->tid) as $parent) {
+            if ($parent->tid == $setting['parent']) {
+              $is_allowed = TRUE;
+              break(2);
+            }
+          }
+        }
+      }
+    }
+    if (!$is_allowed) {
+      // Synonym term is from a vocabulary that is not expected by this field,
+      // or under unexpected parent.
+      return array();
+    }
+    return array(array(
+      'tid' => $synonym_entity->tid,
+    ));
+  }
+
+  public function synonymItemHash($item, $field, $instance) {
+    return $item['tid'];
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition, $field, $instance) {
+    if ($field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsSynonymsBehaviorException(t('Not supported storage engine %type in synonymsFind() method.', array(
+        '%type' => $field['storage']['type'],
+      )));
+    }
+    $table = array_keys($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['tid'];
+
+    $query = db_select($table, 'field');
+    $term_alias = $query->innerJoin('taxonomy_term_data', 'term', 'field.' . $column . ' = term.tid');
+    $query->addField($term_alias, 'name', 'synonym');
+    $query->fields('field', array('entity_id'));
+    $query->condition('field.entity_type', $instance['entity_type']);
+    $query->condition('field.bundle', $instance['bundle']);
+
+    $this->synonymsFindProcessCondition($condition, $term_alias . '.name');
+    $query->condition($condition);
+    return $query->execute();
+  }
+}

+ 0 - 94
sites/all/modules/contrib/taxonomy/synonyms/includes/TaxonomySynonymsExtractor.class.inc

@@ -1,94 +0,0 @@
-<?php
-
-/**
- * @file
- * Enables Taxonomy Term Reference field types to be source of synonyms.
- */
-
-class TaxonomySynonymsExtractor extends AbstractSynonymsExtractor {
-
-  public static function fieldTypesSupported() {
-    return array('taxonomy_term_reference');
-  }
-
-  public static function synonymsExtract($items, $field, $instance, $entity, $entity_type) {
-    $synonyms = array();
-
-    $terms = array();
-    foreach ($items as $item) {
-      $terms[] = $item['tid'];
-    }
-    $terms = taxonomy_term_load_multiple($terms);
-    foreach ($terms as $term) {
-      $synonyms[] = entity_label('taxonomy_term', $term);
-    }
-    return $synonyms;
-  }
-
-  public static function processEntityFieldQuery($tag, EntityFieldQuery $query, $field, $instance) {
-    // Unfortunately EntityFieldQuery does not currently support INNER JOINing
-    // term entity that is referenced via taxonomy_term_reference field type.
-    // Thus, we use an ugly solution -- going through all terms that exist in
-    // vocabulary and trying to see if there is a match by term's name.
-    $tids = array();
-
-    foreach ($field['settings']['allowed_values'] as $settings) {
-      $efd = new EntityFieldQuery();
-      $efd->entityCondition('bundle', $settings['vocabulary'])
-        ->entityCondition('entity_type', 'taxonomy_term')
-        ->propertyCondition('name', '%' . $tag . '%', 'LIKE');
-      $result = $efd->execute();
-      if (isset($result['taxonomy_term'])) {
-        foreach ($result['taxonomy_term'] as $tid) {
-          $tids[] = $tid->tid;
-        }
-      }
-    }
-
-    // Now we have tids of terms from the referenced vocabulary which names
-    // LIKE %$tag%, suggested are the terms that refer to any of these $tids.
-    if (empty($tids)) {
-      // No possible suggestions were found. We have to make sure $query yields
-      // no results.
-      self::emptyResultsCondition($query);
-      return;
-    }
-    $query->fieldCondition($field, 'tid', $tids);
-  }
-
-  public static function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
-    // Taxonomy term reference supports only referencing of entity types
-    // 'taxonomy_term'.. duh.
-    if ($synonym_entity_type != 'taxonomy_term') {
-      return array();
-    }
-    // Checking that $field is configured to reference the vocabulary of
-    // $synonym_entity term.
-    $is_allowed = FALSE;
-    foreach ($field['settings']['allowed_values'] as $setting) {
-      if ($synonym_entity->vocabulary_machine_name == $setting['vocabulary']) {
-        if ($setting['parent'] == 0) {
-          // No need to check parent property as there is no limitation on it.
-          $is_allowed = TRUE;
-          break;
-        }
-        else {
-          foreach (taxonomy_get_parents_all($synonym_entity->tid) as $parent) {
-            if ($parent->tid == $setting['parent']) {
-              $is_allowed = TRUE;
-              break(2);
-            }
-          }
-        }
-      }
-    }
-    if (!$is_allowed) {
-      // Synonym term is from a vocabulary that is not expected by this field,
-      // or under unexpected parent.
-      return array();
-    }
-    return array(array(
-      'tid' => $synonym_entity->tid,
-    ));
-  }
-}

+ 68 - 0
sites/all/modules/contrib/taxonomy/synonyms/includes/TextSynonymsBehavior.class.inc

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Enables text and number field types to be source of synonyms.
+ */
+
+/**
+ * Definition of TextSynonymsBehavior class.
+ */
+class TextSynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior, AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+
+  public function extractSynonyms($items, $field, $instance, $entity, $entity_type) {
+    $synonyms = array();
+
+    foreach ($items as $item) {
+      $synonyms[] = $item['value'];
+    }
+
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
+    $synonym = entity_label($synonym_entity_type, $synonym_entity);
+    switch ($field['type']) {
+      case 'text':
+        break;
+
+      // We add synonyms for numbers only if $synonym is a number.
+      case 'number_integer':
+      case 'number_float':
+      case 'number_decimal':
+        if (!is_numeric($synonym)) {
+          return array();
+        }
+        break;
+
+    }
+    return array(array(
+      'value' => $synonym,
+    ));
+  }
+
+  public function synonymItemHash($item, $field, $instance) {
+    return $item['value'];
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition, $field, $instance) {
+    if ($field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsSynonymsBehaviorException(t('Not supported storage engine %type in synonymsFind() method.', array(
+        '%type' => $field['storage']['type'],
+      )));
+    }
+    $table = array_keys($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['value'];
+
+    $this->synonymsFindProcessCondition($condition, $column);
+
+    $query = db_select($table);
+    $query->fields($table, array('entity_id'));
+    $query->addField($table, $column, 'synonym');
+    return $query->condition($condition)
+      ->condition('entity_type', $instance['entity_type'])
+      ->condition('bundle', $instance['bundle'])
+      ->execute();
+  }
+}

+ 24 - 0
sites/all/modules/contrib/taxonomy/synonyms/js/synonyms-autocomplete.js

@@ -0,0 +1,24 @@
+(function ($) {
+
+/**
+ * Fix the autocomplete core undesired behavior.
+ *
+ * The core autocomplete only allows 1 entry per suggestion, i.e. you can't have
+ * 2 suggestion entries suggest the same key. Synonyms module very well needs
+ * such ability, since multiple synonyms may point to the same entity. In order
+ * to bypass this limitation Synonyms module pads the suggestion entries with
+ * extra spaces on the right until it finds a "free" spot. This JavaScript
+ * right-trims the entries in order to cancel out the effect.
+ */
+Drupal.behaviors.synonymsAutocompleteWidget = {
+  attach: function (context, settings) {
+    $('input.form-autocomplete.synonyms-autocomplete', context).once('synonyms-autocomplete', function () {
+      $(this).bind('autocompleteSelect', function() {
+        var value = $(this).val();
+        value = value.replace(/\s+$/, '');
+        $(this).val(value);
+      });
+    });
+  }
+};
+})(jQuery);

+ 147 - 0
sites/all/modules/contrib/taxonomy/synonyms/plugins/arguments/term_synonyms.inc

@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @file
+ * Plugin to provide a synonyms-friendly argument handler for a Taxonomy term.
+ */
+
+$plugin = array(
+  'title' => t("Taxonomy term: ID (synonyms-friendly)"),
+  'keyword' => 'term',
+  'description' => t('Creates a single taxonomy term from a taxonomy term name or one of its synonyms.'),
+  'context' => 'synonyms_term_synonyms_context',
+  'default' => array('breadcrumb' => TRUE, 'transform' => FALSE),
+  'settings form' => 'synonyms_term_synonyms_settings_form',
+  'placeholder form' => 'synonyms_term_synonyms_ctools_argument_placeholder',
+  'breadcrumb' => 'synonyms_term_synonyms_breadcrumb',
+);
+
+/**
+ * Discover if this argument gives us the term we crave.
+ */
+function synonyms_term_synonyms_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('entity:taxonomy_term');
+  }
+
+  $conf['vids'] = is_array($conf['vids']) ? array_filter($conf['vids']) : array();
+
+  if (is_object($arg)) {
+    $term = $arg;
+
+    if (!empty($conf['vids']) && empty($conf['vids'][$term->vid])) {
+      return NULL;
+    }
+  }
+  else {
+    if ($conf['transform']) {
+      $tids = db_select('taxonomy_term_data', 't')
+        ->fields('t', array('tid'))
+        ->where("REPLACE(t.name, ' ', '-') = :argument", array(
+          ':argument' => $arg,
+        ));
+      if (!empty($conf['vids'])) {
+        $tids->condition('t.vid', $conf['vids']);
+      }
+      $tids = $tids->execute()->fetchCol();
+      $terms = taxonomy_term_load_multiple($tids);
+    }
+    else {
+      $terms = taxonomy_get_term_by_name($arg);
+    }
+
+    if (!empty($conf['vids'])) {
+      foreach ($terms as $k => $term) {
+        if (!isset($conf['vids'][$term->vid])) {
+          unset($terms[$k]);
+        }
+      }
+    }
+
+    if (empty($terms)) {
+      // We couldn't find the term by name, so we will look it up now by
+      // synonyms.
+      $vocabularies = taxonomy_vocabulary_load_multiple(empty($conf['vids']) ? FALSE : $conf['vids']);
+      foreach ($vocabularies as $vocabulary) {
+        $condition = db_and();
+        if ($conf['transform']) {
+          $condition->where("REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-') = :argument", array(
+            ':argument' => $arg,
+          ));
+        }
+        else {
+          $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $arg);
+        }
+        $rows = synonyms_synonyms_find($condition, 'taxonomy_term', $vocabulary->machine_name);
+        if (!empty($rows)) {
+          // We have found a match, no need to search further.
+          $terms[] = taxonomy_term_load($rows[0]->entity_id);
+          break;
+        }
+      }
+    }
+
+    if (empty($terms)) {
+      return NULL;
+    }
+    $term = array_shift($terms);
+  }
+
+  $context = ctools_context_create('entity:taxonomy_term', $term);
+  $context->original_argument = $arg;
+  return $context;
+}
+
+/**
+ * Settings form for the argument.
+ */
+function synonyms_term_synonyms_settings_form(&$form, &$form_state, $conf) {
+  $vocabularies = taxonomy_get_vocabularies();
+  $options = array();
+  foreach ($vocabularies as $vid => $vocab) {
+    $options[$vid] = $vocab->name;
+  }
+  $form['settings']['vids'] = array(
+    '#title' => t('Limit to these vocabularies'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#default_value' => !empty($conf['vids']) ? $conf['vids'] : array(),
+    '#description' => t('If no vocabularies are checked, terms from all vocabularies will be accepted.'),
+  );
+
+  $form['settings']['breadcrumb'] = array(
+    '#title' => t('Inject hierarchy into breadcrumb trail'),
+    '#type' => 'checkbox',
+    '#default_value' => !empty($conf['breadcrumb']),
+    '#description' => t('If checked, taxonomy term parents will appear in the breadcrumb trail.'),
+  );
+
+  $form['settings']['transform'] = array(
+    '#title' => t('Transform dashes in URL to spaces in term name filter values'),
+    '#type' => 'checkbox',
+    '#default_value' => !empty($conf['transform']),
+  );
+}
+
+/**
+ * Form fragment to get an argument to convert a placeholder for preview.
+ */
+function synonyms_term_synonyms_ctools_argument_placeholder($conf) {
+  return array(
+    '#type' => 'textfield',
+    '#description' => t('Enter a taxonomy term name.'),
+  );
+}
+
+/**
+ * Inject the breadcrumb trail if necessary.
+ */
+function synonyms_term_synonyms_breadcrumb($conf, $context) {
+  // Outsource the real implementation of breadcrumb to terms argument plugin.
+  $plugin = ctools_get_plugins('ctools', 'arguments', 'term');
+  $function = ctools_plugin_get_function($plugin, 'breadcrumb');
+  if ($function) {
+    call_user_func_array($function, func_get_args());
+  }
+}

+ 41 - 0
sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/autocomplete.inc

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for autocomplete synonyms behavior.
+ */
+
+$plugin = array(
+  'title' => t('Autocomplete'),
+  'description' => t('Synonyms friendly autocomplete'),
+  'settings form callback' => 'synonyms_behavior_autocomplete_settings_form',
+  'interface' => 'AutocompleteSynonymsBehavior',
+);
+
+/**
+ * Settings form for autocomplete behavior.
+ */
+function synonyms_behavior_autocomplete_settings_form($form, &$form_state, $settings) {
+  static $is_first_time = TRUE;
+
+  $element = array();
+
+  $element['wording'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Autocomplete wording'),
+    '#default_value' => isset($settings['wording']) ? $settings['wording'] : '@synonym is a synonym of @term',
+    '#description' => t('Specify with what wording the synonyms should be suggested in the autocomplete feature. You may use: <ul><li><em>@synonym</em> to denote value of the synonym</li><li><em>@term</em> to denote term name</li><li><em>@field_name</em> to denote lowercase label of the field from where the synonym originates</li></ul>'),
+    '#required' => TRUE,
+  );
+
+  if (!$is_first_time) {
+    // Remove the description, if the element is created more than once on the
+    // same form. Otherwise the whole form looks too clumsy.
+    unset($element['wording']['#description']);
+  }
+
+  $is_first_time = FALSE;
+
+  return $element;
+}
+

+ 41 - 0
sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/select.inc

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for synonyms friendly select behavior.
+ */
+
+$plugin = array(
+  'title' => t('Select'),
+  'description' => t('Synonyms friendly select'),
+  'settings form callback' => 'synonyms_behavior_select_settings_form',
+  'interface' => 'SelectSynonymsBehavior',
+);
+
+/**
+ * Settings form for select behavior.
+ */
+function synonyms_behavior_select_settings_form($form, &$form_state, $settings) {
+  static $is_first_time = TRUE;
+
+  $element = array();
+
+  $element['wording'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Select wording'),
+    '#default_value' => isset($settings['wording']) ? $settings['wording'] : '@synonym',
+    '#description' => t('Specify with what wording the synonyms should be placed in the select form element. You may use: <ul><li><em>@synonym</em> to denote value of the synonym</li><li><em>@term</em> to denote term name</li><li><em>@field_name</em> to denote lowercase label of the field from where the synonym originates</li></ul>'),
+    '#required' => TRUE,
+  );
+
+  if (!$is_first_time) {
+    // Remove the description, if the element is created more than once on the
+    // same form. Otherwise the whole form looks too clumsy.
+    unset($element['wording']['#description']);
+  }
+
+  $is_first_time = FALSE;
+
+  return $element;
+}
+

+ 12 - 0
sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/synonyms.inc

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for most general synonyms behavior.
+ */
+
+$plugin = array(
+  'title' => t('Include into term synonyms'),
+  'description' => t('Basic behavior that includes values of this field into the list of synonyms of its entity.'),
+  'interface' => 'SynonymsSynonymsBehavior',
+);

+ 98 - 123
sites/all/modules/contrib/taxonomy/synonyms/synonyms.api.php

@@ -6,149 +6,124 @@
  */
 
 /**
- * Provide Synonyms module with names of synonyms extractor classes.
+ * Hook to collect info about available synonyms behavior implementations.
  *
- * Provide Synonyms module with names of classes that are able to extract
- * synonyms from fields. Each of the provided classes should extend
- * AbstractSynonymsExtractor base class.
+ * Hook to collect info about what PHP classes implement provided synonyms
+ * behavior for different field types.
+ *
+ * @param string $behavior
+ *   Name of a synonyms behavior. This string will always be among the keys
+ *   of the return of synonyms_behaviors(), i.e. name of a ctools plugin
  *
  * @return array
- *   Array of strings, where each value is a name of synonyms extractor class
+ *   Array of information about what synonyms behavior implementations your
+ *   module supplies. The return array must contain field types as keys, whereas
+ *   corresponding values should be names of PHP classes that implement the
+ *   provided behavior for that field type. Read more about how to implement a
+ *   specific behavior in the advanced help of this module. In a few words: you
+ *   will have to implement an interface that is defined in the behavior
+ *   definition. Do not forget to make sure your PHP class is visible to Drupal
+ *   auto discovery mechanism
  */
-function hook_synonyms_extractor_info() {
-  return array(
-    // Please see below the definition of ApiSynonymsSynonymsExtractor class
-    // for your reference.
-    'ApiSynonymsSynonymsExtractor',
-  );
+function hook_synonyms_behavior_implementation_info($behavior) {
+  switch ($behavior) {
+    case 'autocomplete':
+      return array(
+        'my-field-type' => 'MyFieldTypeAutocompleteSynonymsBehavior',
+      );
+      break;
+
+    case 'another-behavior':
+      return array(
+        'my-field-type-or-yet-another-field-type' => 'MyFieldTypeAnotherBehaviorSynonymsBehavior',
+      );
+      break;
+  }
+
+  return array();
 }
 
 /**
- * Dummy synonyms extractor class for documentation purposes.
+ * Hook to alter info about available synonyms behavior implementations.
+ *
+ * This hook is invoked right after hook_synonyms_behavior_implementation_info()
+ * and is designed to let modules overwrite implementation info from some other
+ * modules. For example, if module A provides implementation for some field
+ * type, but your module has a better version of that implementation, you would
+ * need to implement this hook and to overwrite the implementation info.
  *
- * This is a copy of SynonymsSynonymsExtractor class providing an example of
- * how to write your own synonyms extractor class. See the definition of
- * AbstractSynonymsExtractor for reference and in code comments. For more
- * complicated examples take a look at EntityReferenceSynonymsExtractor class.
+ * @param array $info
+ *   Array of information about existing synonyms behavior implementations that
+ *   was collected from modules
+ * @param string $behavior
+ *   Name of the behavior for which the info about implementation is being
+ *   generated
  */
-class ApiSynonymsSynonymsExtractor extends AbstractSynonymsExtractor {
-
-  /**
-   * Return array of supported field types for synonyms extraction.
-   *
-   * @return array
-   *   Array of Field API field types from which this class is able to extract
-   *   synonyms
-   */
-  static public function fieldTypesSupported() {
-    return array('text', 'number_integer', 'number_float', 'number_decimal');
+function hook_synonyms_behavior_implementation_info_alter(&$info, $behavior) {
+  switch ($behavior) {
+    case 'the-behavior-i-want':
+      $info['the-field-type-i-want'] = 'MyFieldTypeAutocompleteSynonymsBehavior';
+      break;
   }
+}
 
-  /**
-   * Extract synonyms from a field attached to an entity.
-   *
-   * We try to pass as many info about context as possible, however, normally
-   * you will only need $items to extract the synonyms.
-   *
-   * @param array $items
-   *   Array of items
-   * @param array $field
-   *   Array of field definition according to Field API
-   * @param array $instance
-   *   Array of instance definition according to Field API
-   * @param object $entity
-   *   Fully loaded entity object to which the $field and $instance with $item
-   *   values is attached to
-   * @param string $entity_type
-   *   Type of the entity $entity according to Field API definition of entity
-   *   types
-   *
-   * @return array
-   *   Array of synonyms extracted from $items
-   */
-  static public function synonymsExtract($items, $field, $instance, $entity, $entity_type) {
-    $synonyms = array();
+/**
+ * Example of how to implement a synonyms behavior for an arbitrary field type.
+ */
+class MyFieldTypeAutocompleteSynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements AutocompleteSynonymsBehavior {
 
+  public function extractSynonyms($items, $field, $instance, $entity, $entity_type) {
+    // Let's say our synonyms is stored in the 'foo' column of the field.
+    $synonyms = array();
     foreach ($items as $item) {
-      $synonyms[] = $item['value'];
+      $synonyms[] = $item['foo'];
     }
-
     return $synonyms;
   }
 
-  /**
-   * Allow you to hook in during autocomplete suggestions generation.
-   *
-   * Allow you to include entities for autocomplete suggestion that are possible
-   * candidates based on your field as a source of synonyms. This method is
-   * void, however, you have to alter and add your condition to $query
-   * parameter.
-   *
-   * @param string $tag
-   *   What user has typed in into autocomplete widget. Normally you would
-   *   run LIKE '%$tag%' on your column
-   * @param EntityFieldQuery $query
-   *   EntityFieldQuery object where you should add your conditions to
-   * @param array $field
-   *   Array of field definition according to Field API, autocomplete on which
-   *   is fired
-   * @param array $instance
-   *   Array of instance definition according to Field API, autocomplete on
-   *   which is fired
-   */
-  static public function processEntityFieldQuery($tag, EntityFieldQuery $query, $field, $instance) {
-    $query->fieldCondition($field, 'value', '%' . $tag . '%', 'LIKE');
+  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
+    // Let's say we keep the synonyms as strings and under the 'foo' column, to
+    // keep it consistent with the extractSynonyms() method.
+    $label = entity_label($synonym_entity_type, $synonym_entity);
+    return array(array(
+      'foo' => $label,
+    ));
   }
 
-  /**
-   * Add an entity as a synonym into a field of another entity.
-   *
-   * Basically this method should be called when you want to add some entity
-   * as a synonym to another entity (for example when you merge one entity
-   * into another and besides merging want to add synonym of the merging
-   * entity into the trunk entity). You should extract synonym value (according
-   * to what value is expected in this field) and return it. We try to provide
-   * you with as much context as possible, but normally you would only need
-   * $synonym_entity and $synonym_entity_type parameters. Return an empty array
-   * if entity of type $synonym_entity_type cannot be converted into a format
-   * expected by $field.
-   *
-   * @param array $items
-   *   Array items that already exist in the field into which new synonyms is to
-   *   be added
-   * @param array $field
-   *   Field array definition according to Field API of the field into which new
-   *   synonym is to be added
-   * @param array $instance
-   *   Instance array definition according to Field API of the instance into
-   *   which new synonym is to be added
-   * @param object $synonym_entity
-   *   Fully loaded entity object which has to be added as synonym
-   * @param string $synonym_entity_type
-   *   Entity type of $synonym_entity
-   *
-   * @return array
-   *   Array of extra items to be merged into the items that already exist in
-   *   field values
-   */
-  static public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
-    $synonym = entity_label($synonym_entity_type, $synonym_entity);
-    switch ($field['type']) {
-      case 'text':
-        break;
-
-      // We add synonyms for numbers only if $synonym is a number.
-      case 'number_integer':
-      case 'number_float':
-      case 'number_decimal':
-        if (!is_numeric($synonym)) {
-          return array();
-        }
-        break;
+  public function synonymItemHash($item, $field, $instance) {
+    // Since we've agreed that the column that stores data in our imaginary
+    // field type is "foo". Then it suffices just to implement the hash function
+    // as the value of foo column.
+    return $item['foo'];
+  }
 
+  public function synonymsFind(QueryConditionInterface $condition, $field, $instance) {
+    // We only can find synonyms in SQL storage. If this field is not one, then
+    // we have full right to throw an exception.
+    if ($field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsSynonymsBehaviorException(t('Not supported storage engine %type in synonymsFind() method.', array(
+        '%type' => $field['storage']['type'],
+      )));
     }
-    return array(array(
-      'value' => $synonym,
-    ));
+    // Now we will figure out in what table $field is stored. We want to
+    // condition the 'foo' column within that field table.
+    $table = array_keys($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['foo'];
+
+    // Once we know full path to the column that stores synonyms as plain text,
+    // we can use a supplementary method from AbstractSynonymsSynonymsBehavior,
+    // which helps us to convert a column placeholder into its real value within
+    // the condition we have received from outside.
+    $this->synonymsFindProcessCondition($condition, $column);
+
+    // Now we are all set to build a SELECT query and return its result set.
+    $query = db_select($table);
+    $query->fields($table, array('entity_id'));
+    $query->addField($table, $column, 'synonym');
+    return $query->condition($condition)
+      ->condition('entity_type', $instance['entity_type'])
+      ->condition('bundle', $instance['bundle'])
+      ->execute();
   }
 }

+ 1 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms.css

@@ -0,0 +1 @@
+.synonyms-behavior-settings .form-item .description {white-space:normal;}

+ 11 - 8
sites/all/modules/contrib/taxonomy/synonyms/synonyms.info

@@ -3,19 +3,22 @@ description = "Provides synonyms feature for working with Drupal Taxonomy"
 package = Taxonomy
 core = 7.x
 dependencies[] = taxonomy
-dependencies[] = text
+dependencies[] = ctools
 
 files[] = synonyms.test
-files[] = includes/AbstractSynonymsExtractor.class.inc
-files[] = includes/SynonymsSynonymsExtractor.class.inc
-files[] = includes/TaxonomySynonymsExtractor.class.inc
-files[] = includes/EntityReferenceSynonymsExtractor.class.inc
+
+files[] = includes/SynonymsBehavior.interface.inc
+files[] = includes/TextSynonymsBehavior.class.inc
+files[] = includes/TaxonomySynonymsBehavior.class.inc
+files[] = includes/EntityReferenceSynonymsBehavior.class.inc
+
 files[] = views/synonyms.views.inc
 files[] = views/synonyms_views_plugin_argument_validate_taxonomy_term.inc
+files[] = views/synonyms_views_handler_filter_term_tid.inc
 
-; Information added by Drupal.org packaging script on 2014-09-15
-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 = "synonyms"
-datestamp = "1410753528"
+datestamp = "1448771941"
 

+ 146 - 4
sites/all/modules/contrib/taxonomy/synonyms/synonyms.install

@@ -5,13 +5,55 @@
  * Install, update, and uninstall functions for the Synonyms module.
  */
 
+/**
+ * Implements hook_schema().
+ */
+function synonyms_schema() {
+  $schema = array();
+
+  $schema['synonyms_settings'] = array(
+    'description' => 'Stores synonyms settings for all the entities and fields. Only enabled synonyms behaviors are included in this table.',
+    'fields' => array(
+      'instance_id' => array(
+        'description' => 'Reference to {field_config_instance}.id of the instance, whose synonyms settings are stored in this row.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+      ),
+      'behavior' => array(
+        'description' => 'Name of the synonyms behavior (ctools plugin), whose settings are stored in this row.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'settings_serialized' => array(
+        'description' => 'Settings of the specified synonyms behavior for the specified field instance.',
+        'type' => 'text',
+        'serialize' => TRUE,
+        'not null' => TRUE,
+      ),
+    ),
+    'unique keys' => array(
+      'instance_behavior' => array('instance_id', 'behavior'),
+    ),
+    'foreign keys' => array(
+      'field_config_instance' => array(
+        'table' => 'field_config_instance',
+        'columns' => array('instance_id' => 'id'),
+      ),
+    ),
+    'indexes' => array(
+      'behavior' => array('behavior'),
+    ),
+  );
+
+  return $schema;
+}
+
 /**
  * Implements hook_uninstall().
  */
 function synonyms_uninstall() {
-  // We rely on a constant defined in the main module's file, so we include it.
-  drupal_load('module', 'synonyms');
-  field_delete_field(SYNONYMS_DEFAULT_FIELD_NAME);
   // Cleaning all configure variables.
   $results = db_select('variable', 'var')
     ->fields('var', array('name'))
@@ -39,8 +81,108 @@ function synonyms_update_7101() {
     // Enabled synonyms now stored as field names, since the field independency
     // has been introduced. See issue http://drupal.org/node/1850748.
     drupal_load('module', 'synonyms');
-    $settings['synonyms'] = $settings['synonyms'] ? array(SYNONYMS_DEFAULT_FIELD_NAME) : array();
+    $settings['synonyms'] = $settings['synonyms'] ? array('synonyms_synonyms') : array();
     variable_set($var->name, $settings);
   }
   return t('Updated settings of synonyms.');
 }
+
+/**
+ * Multiple adjustments in the internal structures of the module.
+ *
+ * Unlock the 'synonyms_synonyms' field, because Synonyms module no longer uses
+ * it.
+ * Create 'synonyms_settings' table.
+ * Enable 'synonyms_search' module if the core Search module is enabled.
+ * Enable all available behaviors on all "source of synonyms" fields with the
+ * default settings.
+ */
+function synonyms_update_7102() {
+  $field = field_info_field('synonyms_synonyms');
+  $field['locked'] = FALSE;
+  field_update_field($field);
+
+  db_create_table('synonyms_settings', array(
+    'description' => 'Stores synonyms settings for all the entities and fields. Only enabled synonyms behaviors are included in this table.',
+    'fields' => array(
+      'instance_id' => array(
+        'description' => 'Reference to {field_config_instance}.id of the instance, whose synonyms settings are stored in this row.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+      ),
+      'behavior' => array(
+        'description' => 'Name of the synonyms behavior (ctools plugin), whose settings are stored in this row.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'settings_serialized' => array(
+        'description' => 'Settings of the specified synonyms behavior for the specified field instance.',
+        'type' => 'text',
+        'serialize' => TRUE,
+        'not null' => TRUE,
+      ),
+    ),
+    'unique keys' => array(
+      'instance_behavior' => array('instance_id', 'behavior'),
+    ),
+    'foreign keys' => array(
+      'field_config_instance' => array(
+        'table' => 'field_config_instance',
+        'columns' => array('instance_id' => 'id'),
+      ),
+    ),
+    'indexes' => array(
+      'behavior' => array('behavior'),
+    ),
+  ));
+
+  if (module_exists('search')) {
+    module_enable(array('synonyms_search'));
+  }
+
+  $vars = db_select('variable', 'v')
+    ->fields('v', array('name'))
+    ->condition('name', db_like('synonyms_settings_') . '%', 'LIKE')
+    ->execute();
+  foreach ($vars as $row) {
+    $var = variable_get($row->name);
+    $vid = substr($row->name, drupal_strlen('synonyms_settings_'));
+    $vocabulary = taxonomy_vocabulary_load($vid);
+    if ($vocabulary) {
+      $bundle = $vocabulary->machine_name;
+      foreach ($var['synonyms'] as $field) {
+        $instance = field_info_instance('taxonomy_term', $field, $bundle);
+        foreach (synonyms_behaviors() as $behavior) {
+          switch ($behavior['name']) {
+            case 'synonyms':
+            case 'search':
+            default:
+              $settings = array();
+              break;
+
+            case 'select':
+              $settings = array(
+                'wording' => '@synonym',
+              );
+              break;
+
+            case 'autocomplete':
+              $settings = array(
+                'wording' => '@synonym, synonym of @term',
+              );
+              break;
+          }
+          $settings = array(
+            'instance_id' => $instance['id'],
+            'behavior' => $behavior['name'],
+            'settings' => $settings,
+          );
+          synonyms_behavior_settings_save($settings);
+        }
+      }
+    }
+    variable_del($row->name);
+  }
+}

+ 849 - 326
sites/all/modules/contrib/taxonomy/synonyms/synonyms.module

@@ -5,11 +5,6 @@
  * Provide synonyms feature for Drupal Taxonomy.
  */
 
-/**
- * The default field name to be used as a source of synonyms for a term.
- */
-define('SYNONYMS_DEFAULT_FIELD_NAME', 'synonyms_synonyms');
-
 /**
  * Implements hook_menu().
  */
@@ -29,33 +24,58 @@ function synonyms_menu() {
 }
 
 /**
- * Implements hook_taxonomy_term_update().
+ * Implements hook_ctools_plugin_type().
  */
-function synonyms_taxonomy_term_update($term) {
-  // If we notice at least some change in synonyms of this term, we want to
-  // trigger search re-indexing of nodes, where this term is referenced, since
-  // change in term synonyms affects nodes ranking in the search.
-  if (isset($term->original)) {
-    $current_synonyms = synonyms_get_raw($term);
-    $previous_synonyms = synonyms_get_raw($term->original);
-    if (count($current_synonyms) != count($previous_synonyms)) {
-      // Schedule re-indexing, because amount of synonyms has changed.
-      module_load_include('inc', 'synonyms', 'synonyms.pages');
-      synonyms_reindex_nodes_by_terms(array($term->tid));
-    }
-    else {
-      foreach ($current_synonyms as $k => $current_synonym) {
-        if ($current_synonyms[$k] != $previous_synonyms[$k]) {
-          // Schedule re-indexing, because some synonym has changed.
-          module_load_include('inc', 'synonyms', 'synonyms.pages');
-          synonyms_reindex_nodes_by_terms(array($term->tid));
-          break;
-        }
+function synonyms_ctools_plugin_type() {
+  $plugins = array();
+
+  $plugins['behavior'] = array(
+    'defaults' => array(
+      'title' => NULL,
+      'description' => NULL,
+      'settings form callback' => NULL,
+      'interface' => NULL,
+      'enabled callback' => NULL,
+      'disabled callback' => NULL,
+    ),
+  );
+
+  return $plugins;
+}
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function synonyms_ctools_plugin_directory($owner, $plugin_type) {
+  switch ($owner) {
+    case 'synonyms':
+      switch ($plugin_type) {
+        case 'behavior':
+          return 'plugins/' . $plugin_type;
       }
-    }
+      break;
+
+    case 'ctools':
+      switch ($plugin_type) {
+        case 'arguments':
+          return 'plugins/' . $plugin_type;
+      }
+      break;
   }
 }
 
+/**
+ * Implements hook_theme().
+ */
+function synonyms_theme() {
+  return array(
+    'synonyms_behaviors_settings' => array(
+      'render element' => 'element',
+      'file' => 'synonyms.pages.inc',
+    ),
+  );
+}
+
 /**
  * Implements hook_entity_property_info().
  */
@@ -75,105 +95,38 @@ function synonyms_entity_property_info() {
 }
 
 /**
- * Implements hook_node_update_index().
+ * Implements hook_field_delete_instance().
  */
-function synonyms_node_update_index($node) {
-  $output = '';
-  foreach (field_info_instances('node', $node->type) as $instance) {
-    // We go a field by field looking for taxonomy term reference and
-    // if that vocabulary has enabled synonyms, we add term's synonyms
-    // to the search index.
-    $field_info = field_info_field($instance['field_name']);
-    if ($field_info['type'] == 'taxonomy_term_reference') {
-      // For each term referenced in this node we have to add synonyms.
-      $_terms = field_get_items('node', $node, $instance['field_name']);
-      if (is_array($_terms) && !empty($_terms)) {
-        $terms = array();
-        foreach ($_terms as $v) {
-          $terms[] = $v['tid'];
-        }
-        $terms = taxonomy_term_load_multiple($terms);
-        foreach ($terms as $term) {
-          $synonyms = synonyms_get_sanitized($term);
-          if (!empty($synonyms)) {
-            $output .= '<strong>' . implode(', ', $synonyms) . '</strong>';
-          }
-        }
-      }
-    }
+function synonyms_field_delete_instance($instance) {
+  // Remove, if necessary, any synonyms behaviors enabled on this instance.
+  $result = db_select('synonyms_settings', 's')
+    ->fields('s', array('behavior'))
+    ->condition('s.instance_id', $instance['id'])
+    ->execute();
+  foreach ($result as $row) {
+    synonyms_behavior_settings_delete($instance['id'], $row->behavior);
   }
-  return $output;
 }
 
 /**
- * Implements hook_synonyms_extractor_info().
+ * Implements hook_synonyms_behavior_implementation_info().
  */
-function synonyms_synonyms_extractor_info() {
-  // Here we provide synonyms extractors that come along with Synonyms module.
-  return array(
-    'SynonymsSynonymsExtractor',
-    'TaxonomySynonymsExtractor',
-    'EntityReferenceSynonymsExtractor',
-  );
-}
-
-/**
- * Public function.
- *
- * Provide info about what class reports ability to extract synonyms from
- * which field type. The output information of this function is collected via
- * hook_synonyms_extractor_info().
- *
- * @param string $field_type
- *   Optionally you may specify to get a class responsible for a specific field
- *   type only. If nothing is supplied, array of field_type => class_extractor
- *   relation is returned
- * @param bool $reset
- *   Whether collect all the info again from hooks, or cached info is fine
- *
- * @return array
- *   Key of this array denotes the field type while value of the array denotes
- *   which class reports ability to extract synonyms from such field type.
- */
-function synonyms_extractor_info($field_type = NULL, $reset = FALSE) {
-  $cache = &drupal_static(__FUNCTION__);
-
-  // Trying static cache.
-  if (!is_array($cache) || $reset) {
-    // Trying Drupal DB cache layer.
-    $cid = 'synonyms_extractor_info';
-    $cache = cache_get($cid);
-    if (!isset($cache->data) || $reset) {
-      // No cache has been found at all. So we call hooks and collect data.
-      $info = array();
-
-      $extractor_classes = module_invoke_all('synonyms_extractor_info');
-
-      if (is_array($extractor_classes)) {
-        foreach ($extractor_classes as $class) {
-          foreach (call_user_func(array($class, 'fieldTypesSupported')) as $_field_type) {
-            $info[$_field_type] = $class;
-          }
-        }
-      }
-
-      // Letting whoever wants to implement any changes after preprocessing the
-      // data.
-      drupal_alter('synonyms_extractor_info', $info);
-
-      $cache = $info;
-      cache_set($cid, $cache, 'cache', CACHE_TEMPORARY);
-    }
-    else {
-      $cache = $cache->data;
-    }
+function synonyms_synonyms_behavior_implementation_info($behavior) {
+  switch ($behavior) {
+    case 'autocomplete':
+    case 'select':
+    case 'synonyms':
+      return array(
+        'number_integer' => 'TextSynonymsBehavior',
+        'number_decimal' => 'TextSynonymsBehavior',
+        'number_float' => 'TextSynonymsBehavior',
+        'text' => 'TextSynonymsBehavior',
+        'taxonomy_term_reference' => 'TaxonomySynonymsBehavior',
+        'entityreference' => 'EntityReferenceSynonymsBehavior',
+      );
+      break;
   }
-
-  if (!is_null($field_type) && isset($cache[$field_type])) {
-    return $cache[$field_type];
-  }
-
-  return $cache;
+  return array();
 }
 
 /**
@@ -184,7 +137,9 @@ function synonyms_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
     return;
   }
 
-  module_load_include('inc', 'synonyms', 'synonyms.pages');
+  if (!isset($form['#vocabulary']->vid) || !$form['#vocabulary']->vid) {
+    return;
+  }
 
   $form['synonyms'] = array(
     '#type' => 'fieldset',
@@ -193,30 +148,96 @@ function synonyms_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
     '#tree' => TRUE,
   );
 
-  $options = array(
-    SYNONYMS_DEFAULT_FIELD_NAME => t('Default synonyms field'),
+  $behaviors = synonyms_behaviors();
+  $bundle = field_extract_bundle('taxonomy_term', $form['#vocabulary']);
+
+  $form['synonyms']['behaviors'] = array(
+    '#theme' => 'synonyms_behaviors_settings',
+    '#id' => 'synonyms-behaviors-settings-wrapper',
   );
-  if (isset($form['#vocabulary']->vid)) {
-    $instances = synonyms_instances_extract_applicable($form['#vocabulary']);
-    foreach ($instances as $instance) {
-      // Here we prefer some informative text for the default synonyms field
-      // rather its label.
-      if ($instance['field_name'] != SYNONYMS_DEFAULT_FIELD_NAME) {
-        $options[$instance['field_name']] = $instance['label'];
+
+  foreach ($behaviors as $behavior => $behavior_info) {
+    $form['synonyms']['behaviors'][$behavior] = array(
+      '#title' => $behavior_info['title'],
+    );
+
+    $behavior_implementations = synonyms_behavior_get($behavior, 'taxonomy_term', $bundle);
+
+    foreach ($behavior_implementations as $implementation) {
+      $instance = field_info_instance($implementation['entity_type'], $implementation['field_name'], $implementation['bundle']);
+      $form['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['#title'] = $instance['label'];
+
+      if (isset($form_state['values']['synonyms']['behaviors'][$behavior][$implementation['instance_id']])) {
+        $behavior_settings = (bool) $form_state['values']['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['enabled'];
+      }
+      else {
+        $behavior_settings = !is_null($implementation['settings']);
+      }
+      if ($behavior_settings) {
+        if (isset($form_state['values']['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['settings'])) {
+          $behavior_settings = $form_state['values']['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['settings'];
+        }
+        elseif ($implementation['settings']) {
+          $behavior_settings = $implementation['settings'];
+        }
+        else {
+          $behavior_settings = array();
+        }
+      }
+
+      $form['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['enabled'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Enable'),
+        '#default_value' => $behavior_settings !== FALSE,
+      );
+
+      $settings_form = ctools_plugin_get_function($behavior_info, 'settings form callback');
+      if ($settings_form) {
+        $form['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['enabled']['#ajax'] = array(
+          'callback' => 'synonyms_behaviors_settings_form_ajax',
+          'wrapper' => $form['synonyms']['behaviors']['#id'],
+        );
+
+        if ($behavior_settings !== FALSE) {
+          $form['synonyms']['behaviors'][$behavior][$implementation['instance_id']]['settings'] = $settings_form($form, $form_state, $behavior_settings);
+        }
       }
     }
   }
+  $form['#submit'][] = 'synonyms_taxonomy_form_vocabulary_submit';
+}
 
-  $form['synonyms']['synonyms'] = array(
-    '#type' => 'checkboxes',
-    '#title' => t('Synonyms Fields'),
-    '#options' => $options,
-    '#description' => t('<p>This option allows you to assign synonym fields for each term of the vocabulary, allowing to reduce the amount of duplicates.</p><p><b>Important note:</b> unticking %default_field on a production website will result in loss of your synonyms.</p>', array('%default_field' => $options[SYNONYMS_DEFAULT_FIELD_NAME])),
-    '#default_value' => synonyms_synonyms_fields($form['#vocabulary']),
-  );
+/**
+ * Submit handler for Taxonomy vocabulary edit form.
+ *
+ * Store synonyms behavior settings.
+ */
+function synonyms_taxonomy_form_vocabulary_submit($form, &$form_state) {
+  $values = $form_state['values'];
+
+  if ($values['op'] == $form['actions']['submit']['#value']) {
+    foreach ($values['synonyms']['behaviors'] as $behavior => $settings) {
+      foreach ($settings as $instance_id => $behavior_settings) {
+        if ($behavior_settings['enabled']) {
+          synonyms_behavior_settings_save(array(
+            'instance_id' => $instance_id,
+            'behavior' => $behavior,
+            'settings' => isset($behavior_settings['settings']) ? $behavior_settings['settings'] : NULL,
+          ));
+        }
+        else {
+          synonyms_behavior_settings_delete($instance_id, $behavior);
+        }
+      }
+    }
+  }
+}
 
-  // Adding our own submit handler.
-  $form['#submit'][] = 'synonyms_taxonomy_form_vocabulary_submit';
+/**
+ * Ajax callback function for synonyms behavior settings form.
+ */
+function synonyms_behaviors_settings_form_ajax($form, &$form_state) {
+  return $form['synonyms']['behaviors'];
 }
 
 /**
@@ -229,7 +250,7 @@ function synonyms_field_widget_info() {
       'field types' => array('taxonomy_term_reference'),
       'settings' => array(
         'size' => 60,
-        'autocomplete_path' => 'synonyms/autocomplete',
+        'synonyms_autocomplete_path' => 'synonyms/autocomplete',
         'suggestion_size' => 10,
         'suggest_only_unique' => FALSE,
         'auto_creation' => 1,
@@ -238,6 +259,16 @@ function synonyms_field_widget_info() {
         'multiple values' => FIELD_BEHAVIOR_CUSTOM,
       ),
     ),
+    'synonyms_select' => array(
+      'label' => t('Synonyms friendly select list'),
+      'field types' => array('taxonomy_term_reference'),
+      'settings' => array(
+        'sort' => 'weight',
+      ),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+      ),
+    ),
   );
 }
 
@@ -246,32 +277,49 @@ function synonyms_field_widget_info() {
  */
 function synonyms_field_widget_settings_form($field, $instance) {
   $widget = $instance['widget'];
-  $settings = $widget['settings'];
+  $settings = $widget['settings'] + field_info_widget_settings($widget['type']);
 
   $form = array();
 
-  $form['auto_creation'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Allow auto-creation?'),
-    '#description' => t('Whether users may create a new term by typing in a non-existing name into this field.'),
-    '#default_value' => $settings['auto_creation'],
-  );
-
-  $form['suggestion_size'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Suggestions Size'),
-    '#description' => t('Please, enter how many suggested entities to show in the autocomplete textfield.'),
-    '#required' => TRUE,
-    '#element_validate' => array('element_validate_integer_positive'),
-    '#default_value' => $settings['suggestion_size'],
-  );
-
-  $form['suggest_only_unique'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Suggest only one entry per term'),
-    '#description' => t('If you want to include only term name or a single synonym, suggesting a particular term, while disregarding all ongoing ones, please, tick this checkbox on.'),
-    '#default_value' => $settings['suggest_only_unique'],
-  );
+  switch  ($widget['type']) {
+    case 'synonyms_autocomplete':
+      $form['auto_creation'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Allow auto-creation?'),
+        '#description' => t('Whether users may create a new term by typing in a non-existing name into this field.'),
+        '#default_value' => $settings['auto_creation'],
+      );
+
+      $form['suggestion_size'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Suggestions Size'),
+        '#description' => t('Please, enter how many suggested entities to show in the autocomplete textfield.'),
+        '#required' => TRUE,
+        '#element_validate' => array('element_validate_integer_positive'),
+        '#default_value' => $settings['suggestion_size'],
+      );
+
+      $form['suggest_only_unique'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Suggest only one entry per term'),
+        '#description' => t('If you want to include only term name or a single synonym, suggesting a particular term, while disregarding all ongoing ones, please, tick this checkbox on.'),
+        '#default_value' => $settings['suggest_only_unique'],
+      );
+      break;
+
+    case 'synonyms_select':
+      $form['sort'] = array(
+        '#type' => 'radios',
+        '#title' => t('Sort'),
+        '#description' => t('Choose by what criterion the items within select should be sorted.'),
+        '#options' => array(
+          'weight' => t('As in taxonomy vocabulary (by weight)'),
+          'name' => t('By name of terms and their synonyms'),
+        ),
+        '#default_value' => $settings['sort'],
+      );
+      break;
+  }
 
   return $form;
 }
@@ -280,20 +328,76 @@ function synonyms_field_widget_settings_form($field, $instance) {
  * Implements hook_field_widget_form().
  */
 function synonyms_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
-  $tags = array();
+  $default_value = array();
   foreach ($items as $item) {
-    $tags[$item['tid']] = isset($item['taxonomy_term']) ? $item['taxonomy_term'] : taxonomy_term_load($item['tid']);
+    $default_value[] = $item['tid'];
   }
 
-  $element += array(
-    '#type' => 'textfield',
-    '#default_value' => taxonomy_implode_tags($tags),
-    '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $field['field_name'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'],
-    '#size' => $instance['widget']['settings']['size'],
-    '#maxlength' => 1024,
-    '#element_validate' => array('taxonomy_autocomplete_validate', 'synonyms_autocomplete_validate'),
-    '#auto_creation' => $instance['widget']['settings']['auto_creation'],
-  );
+  switch ($instance['widget']['type']) {
+    case 'synonyms_autocomplete':
+      $tags = taxonomy_term_load_multiple($default_value);
+
+      $element += array(
+        '#type' => 'textfield',
+        '#default_value' => taxonomy_implode_tags($tags),
+        '#autocomplete_path' => $instance['widget']['settings']['synonyms_autocomplete_path'] . '/' . $field['field_name'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'],
+        '#size' => $instance['widget']['settings']['size'],
+        '#maxlength' => 1024,
+        '#element_validate' => array('taxonomy_autocomplete_validate', 'synonyms_autocomplete_validate'),
+        '#auto_creation' => $instance['widget']['settings']['auto_creation'],
+        '#attached' => array(
+          'js' => array(
+            drupal_get_path('module', 'synonyms') . '/js/synonyms-autocomplete.js' => array(),
+          ),
+        ),
+        '#attributes' => array(
+          'class' => array('synonyms-autocomplete'),
+        ),
+      );
+      break;
+
+    case 'synonyms_select':
+      $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
+
+      $options = array();
+      foreach ($field['settings']['allowed_values'] as $tree) {
+        if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
+          switch ($instance['widget']['settings']['sort']) {
+            case 'weight':
+              if ($terms = taxonomy_get_tree($vocabulary->vid, $tree['parent'], NULL, TRUE)) {
+                $behavior_implementations = synonyms_behavior_get('select', 'taxonomy_term', field_extract_bundle('taxonomy_term', $vocabulary), TRUE);
+                foreach ($terms as $term) {
+                  $options[] = synonyms_select_option($term);
+                  foreach ($behavior_implementations as $implementation) {
+                    foreach (synonyms_extract_synonyms($term, $implementation) as $synonym) {
+                      $options[] = synonyms_select_option($term, $synonym, $implementation);
+                    }
+                  }
+                }
+              }
+              break;
+
+            case 'name':
+              // TODO: is there any way to leverage DB for the sorting routine?
+              $options = synonyms_select_sort_name_options_recursive($vocabulary, $tree['parent']);
+              break;
+          }
+        }
+      }
+
+      if (!$multiple && !$element['#required']) {
+        $options = array('' => t('- None -')) + $options;
+      }
+
+      $element += array(
+        '#type' => 'select',
+        '#multiple' => $multiple,
+        '#options' => $options,
+        '#default_value' => $default_value,
+        '#element_validate' => array('synonyms_select_form_to_storage'),
+      );
+      break;
+  }
 
   return $element;
 }
@@ -316,14 +420,26 @@ function synonyms_autocomplete_validate($element, &$form_state) {
   // (a) Double-check that those terms are not synonyms.
   // (b) Check that synonyms' configurable auto-creation option is enabled.
   $value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
+
+  $field = field_widget_field($element, $form_state);
   foreach ($value as $delta => $term) {
     if ($term['tid'] == 'autocreate') {
-      $vocabulary = taxonomy_vocabulary_load($term['vid']);
-      $synonym_tid = synonyms_get_term_by_synonym($term['name'], $vocabulary);
+      $synonym_tid = 0;
+      foreach ($field['settings']['allowed_values'] as $tree) {
+        $behavior_implementations = synonyms_behavior_get('autocomplete', 'taxonomy_term', $tree['vocabulary'], TRUE);
+        foreach ($behavior_implementations as $behavior_implementation) {
+          $synonyms = synonyms_synonyms_find_behavior(db_and()->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $term['name']), $behavior_implementation);
+          foreach ($synonyms as $synonym) {
+            $synonym_tid = $synonym->entity_id;
+            break(2);
+          }
+        }
+      }
+
       if ($synonym_tid != 0) {
         $value[$delta]['tid'] = $synonym_tid;
       }
-      elseif (empty($element['#auto_creation'])) {
+      elseif (!$element['#auto_creation']) {
         unset($value[$delta]);
       }
     }
@@ -335,8 +451,6 @@ function synonyms_autocomplete_validate($element, &$form_state) {
 /**
  * Try to find a term by its name or synonym.
  *
- * To maximize the match trimming and case-insensitive comparison is used.
- *
  * @param string $name
  *   The string to be searched for its {taxonomy_term_data}.tid
  * @param object $vocabulary
@@ -351,20 +465,28 @@ function synonyms_autocomplete_validate($element, &$form_state) {
  *   found term, otherwise returns 0
  */
 function synonyms_get_term_by_synonym($name, $vocabulary, $parent = 0) {
-  $name = mb_strtoupper(trim($name), 'UTF-8');
-
-  $tree = taxonomy_get_tree($vocabulary->vid, $parent, NULL, TRUE);
-  foreach ($tree as $term) {
-    if (mb_strtoupper($term->name, 'UTF-8') == $name) {
+  $name = trim($name);
+
+  $terms = taxonomy_get_term_by_name($name, $vocabulary->machine_name);
+  foreach ($terms as $term) {
+    if (!$parent || synonyms_taxonomy_term_is_child_of($term->tid, $parent)) {
+      // TODO: actually it could be so that there is more than 1 term that
+      // satisfies the search query, i.e. the name and parent constraints. At
+      // the moment we are going to return the first one we encounter, though
+      // something better could be thought of in the future.
       return $term->tid;
     }
+  }
 
-    // We additionally scan through the synonyms.
-    $synonyms = synonyms_get_sanitized($term);
-    foreach ($synonyms as $item) {
-      if (mb_strtoupper($item, 'UTF-8') == $name) {
-        return $term->tid;
-      }
+  // We have failed to find a term with the provided $name. So let's search now
+  // among the term synonyms.
+  $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
+  $synonyms = synonyms_synonyms_find(db_and()->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $name), 'taxonomy_term', $bundle);
+  foreach ($synonyms as $synonym) {
+    if (!$parent || synonyms_taxonomy_term_is_child_of($synonym->entity_id, $parent)) {
+      // TODO: similarly here, as above, we could have more than 1 match, but
+      // for now we will simply return the first one encountered.
+      return $synonym->entity_id;
     }
   }
 
@@ -374,7 +496,7 @@ function synonyms_get_term_by_synonym($name, $vocabulary, $parent = 0) {
 }
 
 /**
- * Look up a term considering synonyms and if nothing found add one.
+ * Look up a term considering synonyms and if nothing is found add one.
  *
  * This function is useful for automated creation of new terms as it won't
  * generate the same terms over and over again.
@@ -454,6 +576,9 @@ function synonyms_get_raw($item) {
 /**
  * Public function for retrieving synonyms of a taxonomy term.
  *
+ * You are encouraged to use synonyms_get_sanitized() or synonyms_get_raw()
+ * instead. This function soon will be removed from the source code.
+ *
  * @param object $term
  *   Fully loaded taxonomy term for which the synonyms are desired
  *
@@ -463,30 +588,151 @@ function synonyms_get_raw($item) {
  *   the following keys:
  *   - value: (string) the value of a synonym as it was input by user
  *   - safe_value: (string) a sanitized value of a synonym
+ *
+ * @deprecated
  */
 function synonyms_get_term_synonyms($term) {
   $synonyms = array();
   $vocabulary = taxonomy_vocabulary_load($term->vid);
-  foreach (synonyms_synonyms_fields($vocabulary) as $field) {
-    $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
-    $instance = field_info_instance('taxonomy_term', $field, $bundle);
-    $field = field_info_field($field);
-    $items = field_get_items('taxonomy_term', $term, $field['field_name']);
-
-    if (is_array($items) && !empty($items)) {
-      $class = synonyms_extractor_info($field['type']);
-      foreach (call_user_func(array($class, 'synonymsExtract'), $items, $field, $instance, $term, 'taxonomy_term') as $synonym) {
+  $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
+
+  $behavior_implementations = synonyms_behavior_get('synonyms', 'taxonomy_term', $bundle, TRUE);
+  foreach ($behavior_implementations as $implementation) {
+    foreach  (synonyms_extract_synonyms($term, $implementation) as $synonym) {
         $synonyms[] = array(
           'value' => $synonym,
           'safe_value' => check_plain($synonym),
         );
       }
     }
-  }
 
   return $synonyms;
 }
 
+/**
+ * Extract synonyms of an entity within a certain field and behavior.
+ *
+ * Do not use this function, if you want to get synonyms of an entity, unless
+ * you know what you are doing. This function extracts the synonyms from a field
+ * that is specified by $behavior_implementation parameter. The behavior may not
+ * necessarily be of 'synonyms' type, so it's not 100% valid to say that the
+ * entity has the returned array as its synonyms. However, this function is very
+ * useful for behaviors that "extend" the basic synonyms behavior.
+ *
+ * @param object $entity
+ *   Fully loaded entity, synonyms from which should be extracted
+ * @param array  $behavior_implementation
+ *   Fully loaded behavior implementation. Supply here one of the values from
+ *   the return of synonyms_behavior_get() function
+ *
+ * @return array
+ *   Array of synonyms that reside in the field dictated by
+ *   $behavior_implementation parameter
+ */
+function synonyms_extract_synonyms($entity, $behavior_implementation) {
+  $synonyms = array();
+  $field = field_info_field($behavior_implementation['field_name']);
+  $instance = field_info_instance($behavior_implementation['entity_type'], $behavior_implementation['field_name'], $behavior_implementation['bundle']);
+  $items = field_get_items($behavior_implementation['entity_type'], $entity, $field['field_name']);
+
+  if (is_array($items) && !empty($items)) {
+    $object = synonyms_behavior_implementation_class($behavior_implementation['behavior'], $field);
+    $object = new $object();
+
+    $synonyms = array_merge($synonyms, $object->extractSynonyms($items, $field, $instance, $entity, $behavior_implementation['entity_type']));
+  }
+  return $synonyms;
+}
+
+/**
+ * Look up entities by their synonyms.
+ *
+ * @param QueryConditionInterface $condition
+ *   Object of QueryConditionInterface that specifies conditions by which you
+ *   want to find synonyms. When building this condition object, use
+ *   AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER as a placeholder for
+ *   real column name that contains synonym as text. For example, if you were to
+ *   find all entities with synonyms that begin with "synonym-come-here"
+ *   substring, case insensitive and replacing all spaces in original synonym
+ *   string by a dash sign, then you would have to create the following
+ *   condition object:
+ *   db_and()
+ *     ->where("LOWER(REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-')) LIKE 'synonym-come-here%'")
+ *   And then just supply this object as an input parameter to this function
+ * @param string $entity_type
+ *   Among synonyms of what entity type to search
+ * @param string $bundle
+ *   Among synonyms of what bundle to search
+ *
+ * @return array
+ *   Array of found synonyms and entity IDs to which those belong. Each element
+ *   in the array will be an object and will have the following structure:
+ *   - synonym: (string) Synonym that was found and which satisfies the
+ *     $condition you specified
+ *   - entity_id: (int) ID of the entity to which the found synonym belongs
+ */
+function synonyms_synonyms_find(QueryConditionInterface $condition, $entity_type, $bundle) {
+  $rows = array();
+
+  $behavior_implementations = synonyms_behavior_get('synonyms', $entity_type, $bundle, TRUE);
+  foreach ($behavior_implementations as $behavior_implementation) {
+    foreach (synonyms_synonyms_find_behavior($condition, $behavior_implementation) as $row) {
+      $rows[] = $row;
+    }
+  }
+
+  return $rows;
+}
+
+/**
+ * Find entities with a provided synonym within certain behavior implementation.
+ *
+ * Do not use this function, if you want to find entities that have specific
+ * synonyms, unless you know what you are doing. This function searches for the
+ * entities with synonyms from a field that is specified by
+ * $behavior_implementation parameter. The behavior may not necessarily be of
+ * 'synonyms' type, so it's not 100% valid to say that the returned entities
+ * have the specified synonyms. However, this function is very useful for
+ * behaviors that "extend" the basic synonyms behavior.
+ *
+ * You have full SQL flexibility to specify parameters of how to search for
+ * synonyms. You can create arbitrary set of SQL conditions that will be plugged
+ * into specific SELECT queries by behavior implementations.
+ *
+ * @param QueryConditionInterface $condition
+ *   Object of QueryConditionInterface that specifies conditions by which you
+ *   want to find synonyms. When building this condition object, use
+ *   AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER as a placeholder for
+ *   real column name that contains synonym as text. For example, if you were to
+ *   find all entities with synonyms that begin with "synonym-come-here"
+ *   substring, case insensitive and replacing all spaces in original synonym
+ *   string by a dash sign, then you would have to create the following
+ *   condition object:
+ *   db_and()
+ *     ->where("LOWER(REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-')) LIKE 'synonym-come-here%'")
+ *   And then just supply this object as an input parameter to this function
+ * @param array $behavior_implementation
+ *   Fully loaded behavior implementation. Supply here one of the values from
+ *   the return of synonyms_behavior_get() function
+ *
+ * @return Traversable
+ *   Traversable result set of found synonyms and entity IDs to which those
+ *   belong. Each element in the result set will be an object and will have the
+ *   following structure:
+ *   - synonym: (string) Synonym that was found and which satisfies the
+ *     $condition you specified
+ *   - entity_id: (int) ID of the entity to which the found synonym belongs
+ */
+function synonyms_synonyms_find_behavior(QueryConditionInterface $condition, $behavior_implementation) {
+  $field = field_info_field($behavior_implementation['field_name']);
+  $instance = field_info_instance($behavior_implementation['entity_type'], $behavior_implementation['field_name'], $behavior_implementation['bundle']);
+
+  $object = synonyms_behavior_implementation_class($behavior_implementation['behavior'], $field);
+  $object = new $object();
+
+  return $object->synonymsFind($condition, $field, $instance);
+}
+
 /**
  * Allow to merge $synonym_entity as a synonym into $trunk_entity.
  *
@@ -502,8 +748,8 @@ function synonyms_get_term_synonyms($term) {
  * @param string $trunk_entity_type
  *   Entity type of $trunk_entity
  * @param string $field
- *   Field name that should exist in $trunk_entity and be enabled as a synonym
- *   source. Into this field synonym will be added
+ *   Field name that should exist in $trunk_entity and have enabled the
+ *   "synonyms" behavior. Into this field synonym will be added
  * @param object $synonym_entity
  *   Fully loaded entity object which will be added as a synonym
  * @param string $synonym_entity_type
@@ -517,26 +763,47 @@ function synonyms_add_entity_as_synonym($trunk_entity, $trunk_entity_type, $fiel
     // Currently synonyms module only operates on taxonomy terms.
     return FALSE;
   }
-  if (!in_array($field, synonyms_synonyms_fields(taxonomy_vocabulary_load($trunk_entity->vid)))) {
-    // $field either doesn't exist in the $trunk_entity or it's not enabled as
-    // a source of synonyms.
+
+  $bundle = entity_extract_ids($trunk_entity_type, $trunk_entity);
+  $bundle = $bundle[2];
+  $behavior_implementations = synonyms_behavior_get('synonyms', $trunk_entity_type, $bundle, TRUE);
+
+  $field = field_info_field($field);
+  $instance = field_info_instance($trunk_entity_type, $field['field_name'], $bundle);
+  if (!isset($behavior_implementations[$instance['id']])) {
+    // $field either doesn't exist in the $trunk_entity or it does not have
+    // enabled the behavior of synonyms.
     return FALSE;
   }
-  // Preparing arguments for calling a method of Extractor class.
-  $field = field_info_field($field);
-  $extractor = synonyms_extractor_info($field['type']);
+
   $items = field_get_items($trunk_entity_type, $trunk_entity, $field['field_name']);
   $items = is_array($items) ? $items : array();
-  $trunk_entity_ids = entity_extract_ids($trunk_entity_type, $trunk_entity);
-  $instance = field_info_instance($trunk_entity_type, $field['field_name'], $trunk_entity_ids[2]);
 
-  $extra_items = call_user_func(array($extractor, 'mergeEntityAsSynonym'), $items, $field, $instance, $synonym_entity, $synonym_entity_type);
+  $object = synonyms_behavior_implementation_class('synonyms', $field);
+  $object = new $object();
+  $extra_items = $object->mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type);
+
+  if (empty($extra_items)) {
+    // For some reason the behavior implementation couldn't merge it. Otherwise
+    // it would have not returned an empty array.
+    return FALSE;
+  }
 
   // Merging extracted synonym items into the values of the field that already
   // exist.
   // @todo: Currently we hardcode to LANGUAGE_NONE, but in future it would be
   // nice to have multilanguage support.
   $items = array_merge($items, $extra_items);
+
+  // Now we want to keep only unique values of the $items. Since we know nothing
+  // about what determines uniqueness of an item, we will ask the synonym
+  // behavior to hash each of them and then will compare hashes.
+  $unique_items = array();
+  foreach ($items as $item) {
+    $unique_items[$object->synonymItemHash($item, $field, $instance)] = $item;
+  }
+  $items = array_values($unique_items);
+
   $trunk_entity->{$field['field_name']}[LANGUAGE_NONE] = $items;
   // In future if this module eventually becomes a gateway for synonyms for any
   // entity types, we'll substitute it with entity_save().
@@ -547,177 +814,433 @@ function synonyms_add_entity_as_synonym($trunk_entity, $trunk_entity_type, $fiel
 /**
  * Return array of field names that are sources of synonyms.
  *
- * Return array of field names that are currently enabled as source of
- * synonyms in the supplied vocabulary.
+ * Return array of field names that are currently have enabled the synonyms
+ * behavior in the supplied vocabulary. This function is deprecated and shortly
+ * will be removed from the code. All clients of this function are encourage to
+ * use synonyms_behavior_get() function, which provides a richer set of
+ * functionality than this one.
  *
  * @param object $vocabulary
  *   Fully loaded taxonomy vocabulary object
  *
  * @return array
  *   Array of field names
+ *
+ * @deprecated
  */
 function synonyms_synonyms_fields($vocabulary) {
-  $settings = synonyms_vocabulary_settings($vocabulary);
-  if (!isset($settings['synonyms']) || !is_array($settings['synonyms'])) {
-    // Weird as normally we expect to see here at least an empty array but
-    // no problem. We simply initialize it.
-    $settings['synonyms'] = array();
-  }
-
-  // It's not just as easy as pulling up already saved value. After this
-  // we have to make sure that all the fields are still present and have not
-  // been deleted from the vocabulary.
+  $fields = array();
   $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
-  $instances = field_info_instances('taxonomy_term', $bundle);
-  $settings['synonyms'] = array_intersect($settings['synonyms'], array_keys($instances));
+  $behavior_implementations = synonyms_behavior_get('synonyms', 'taxonomy_term', $bundle, TRUE);
+  foreach ($behavior_implementations as $v) {
+    $fields[] = $v['field_name'];
+  }
+  return $fields;
+}
 
-  return $settings['synonyms'];
+/**
+ * Implements hook_views_api().
+ */
+function synonyms_views_api() {
+  return array(
+    'api' => 3,
+    'path' => drupal_get_path('module', 'synonyms') . '/views',
+  );
 }
 
 /**
- * Enforce the setting "synonyms".
+ * Load function for existing implementations of synonyms behaviors.
  *
- * @param object $vocabulary
- *   Fully loaded entity of a taxonomy vocabulary
- * @param array $fields
- *   Array of fields that are enabled as a source of synonyms
+ * @param string $behavior
+ *   Name of the synonyms behavior whose existing implementations should be
+ *   loaded. Basically it has to be name of a ctools plugin of "behavior" type.
+ * @param string $entity_type
+ *   Optional filter to limit the search for existing implementations only to
+ *   those that apply to the provided entity type
+ * @param string $bundle
+ *   Optional filter to limit the search for existing implementations only to
+ *   those that apply to the provided bundle and entity type (the $entity_type
+ *   argument)
+ * @param bool $only_enabled
+ *   Optional filter to limit the search for existing implementations only to
+ *   those that are currently enabled
+ * @param bool $include_deleted
+ *   Optional filter to include the behaviors from deleted instances
+ *
+ * @return array
+ *   Array of loaded existing synonyms behavior implementations. It is keyed
+ *   by ID of the field instance to which the behavior implementation applies.
+ *   The underlying array will have the following structure:
+ *   - behavior: (string) Behavior name of this behavior implementation, i.e.
+ *     name of a ctools plugin of "behavior" type
+ *   - settings: (mixed) Behavior settings, its internal structure depends on
+ *     the type of behavior. If this value is NULL, it means the behavior
+ *     implementation is currently disabled for the field instance
+ *   - entity_type: (string) Entity type to which this behavior implementation
+ *     applies
+ *   - bundle: (string) Bundle name to which this behavior implementation
+ *     applies
+ *   - field_name: (string) Name of a field to which this behavior
+ *     implementation applies
+ *   - instance_id: (int) ID of the instance to which this behavior
+ *     implementation applies
  */
-function synonyms_synonyms_enforce($vocabulary, $fields) {
-  $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
+function synonyms_behavior_get($behavior, $entity_type = NULL, $bundle = NULL, $only_enabled = FALSE, $include_deleted = FALSE) {
+  $behavior_implementation_info = synonyms_behavior_implementation_info($behavior);
+  $supported_field_types = array_keys($behavior_implementation_info);
 
-  // Normally all the fields already exist, we just need to assure that
-  // default synonyms field exists if it is enabled as a source of synonyms.
-  // Otherwise we make sure we delete instance of the default field in the
-  // vocabulary.
-  $instance = field_info_instance('taxonomy_term', SYNONYMS_DEFAULT_FIELD_NAME, $bundle);
-  if (in_array(SYNONYMS_DEFAULT_FIELD_NAME, $fields)) {
-    if (is_null($instance)) {
-      // Make sure the field exists.
-      synonyms_default_field_ensure();
-
-      field_create_instance(array(
-        'field_name' => SYNONYMS_DEFAULT_FIELD_NAME,
-        'entity_type' => 'taxonomy_term',
-        'bundle' => $bundle,
-        'label' => t('Synonyms'),
-        'description' => t('Please, enter the synonyms which should be related to this term.'),
-      ));
-    }
+  if (empty($supported_field_types)) {
+    return array();
+  }
 
+  $query = db_select('field_config_instance', 'i');
+  $field_alias = $query->innerJoin('field_config', 'f', 'f.id = i.field_id');
+  $query->condition($field_alias . '.type', $supported_field_types);
+  if ($entity_type) {
+    $query->condition('i.entity_type', $entity_type);
+  }
+  if ($bundle) {
+    $query->condition('i.bundle', $bundle);
   }
-  elseif (!is_null($instance)) {
-    // Deleting the instance, we will delete the field separately when the
-    // module gets uninstalled.
-    field_delete_instance($instance, FALSE);
+  if (!$include_deleted) {
+    $query->condition('i.deleted', 0);
   }
+
+  $settings_alias = $query->addJoin($only_enabled ? 'INNER' : 'LEFT OUTER', 'synonyms_settings', 's', 's.instance_id = i.id AND s.behavior = :behavior', array(
+    ':behavior' => $behavior,
+  ));
+  $query->fields($settings_alias, array('behavior', 'settings_serialized'));
+
+  $query->fields('i', array('entity_type', 'bundle', 'field_name'));
+  $query->addField('i', 'id', 'instance_id');
+  $result = $query->execute();
+  $result = $result->fetchAllAssoc('instance_id', PDO::FETCH_ASSOC);
+
+  return synonyms_behavior_settings_unpack($result);
 }
 
 /**
- * Return the current settings for the supplied $vocabulary.
+ * Retrieve information about all ctools plugins of type 'synonyms behavior'.
  *
- * @param object $vocabulary
- *   Fully loaded entity of a taxonomy vocabulary
+ * @return array
+ *   Array of information on all available synonyms behavior plugins
+ */
+function synonyms_behaviors() {
+  ctools_include('plugins');
+  return ctools_get_plugins('synonyms', 'behavior');
+}
+
+/**
+ * Fetch information about synonyms behaviors implementations per field type.
+ *
+ * Fetch the map between field types and the PHP classes that implement synonyms
+ * behaviors for them.
+ *
+ * @param string $behavior
+ *   What specific behavior is queried. Supply here keys from the return of
+ *   synonyms_behaviors() function
  *
  * @return array
- *   Array of current synonyms settings for the supplied vocabulary.
- *   Should include the following keys:
- *   - synonyms: (array) array of field names that are enabled as source of
- *     synonyms
+ *   Array of information about what field types implement the provided behavior
+ *   through what PHP classes. Keys of this array are field types, whereas their
+ *   values are names of PHP classes that implement the provided behavior for
+ *   that particular field type
  */
-function synonyms_vocabulary_settings($vocabulary) {
-  $settings = array();
+function synonyms_behavior_implementation_info($behavior) {
+  $info = module_invoke_all('synonyms_behavior_implementation_info', $behavior);
+  drupal_alter('synonyms_behavior_implementation_info', $info, $behavior);
+  return $info;
+}
 
-  if (isset($vocabulary->vid)) {
-    $settings = variable_get('synonyms_settings_' . $vocabulary->vid, array(
-      'synonyms' => array(),
-    ));
-  }
+/**
+ * Determine what PHP class implements specific behavior for specific field.
+ *
+ * @param string $behavior
+ *   Name of the behavior, implementation of which is requested. It should be
+ *   one of the keys of the return of synonyms_behaviors() function.
+ * @param array $field
+ *   Field definition array for which PHP class implementing $behavior is
+ *   requested
+ *
+ * @return string
+ *   Name of the PHP class that implements $behavior for $field field
+ */
+function synonyms_behavior_implementation_class($behavior, $field) {
+  $map = synonyms_behavior_implementation_info($behavior);
+  return $map[$field['type']];
+}
 
+/**
+ * Execute unpacking operation on the just loaded synonyms behavior settings.
+ *
+ * @param array $settings
+ *   Array of the just loaded settings. Each sub array should contain the
+ *   following keys:
+ *   - instance_id: (int) ID of the instance to which it applies
+ *   - behavior: (string) name of the synonyms behavior to which these settings
+ *     apply
+ *   - settings_serialized: (string) serialized content of the settings
+ *
+ * @return array
+ *   Unpacked version of the provided $settings
+ */
+function synonyms_behavior_settings_unpack($settings) {
+  foreach ($settings as &$setting) {
+    $setting['settings'] = $setting['settings_serialized'] ? unserialize($setting['settings_serialized']) : NULL;
+  }
   return $settings;
 }
 
 /**
- * Save the current settings for the supplied $vocabulary.
+ * Save the provided synonyms behavior settings into the database.
  *
- * @param object $vocabulary
- *   Fully loaded entity of a taxonomy vocabulary
  * @param array $settings
- *   Settings the have to be stored. The structure of this array has to be
- *   identical to the output array of the function
- *   synonyms_vocabulary_settings()
- */
-function synonyms_vocabulary_settings_save($vocabulary, $settings) {
-  // If source of synonyms has changed for this vocabulary, we have to trigger
-  // search re-indexing on all the nodes that reference at least 1 term from
-  // this vocabulary.
-  $previous_settings = synonyms_vocabulary_settings($vocabulary);
-  if (implode('', $settings['synonyms']) != implode('', $previous_settings['synonyms'])) {
-    module_load_include('inc', 'synonyms', 'synonyms.pages');
-    synonyms_reindex_nodes_by_vocabulary($vocabulary);
-  }
-
-  variable_set('synonyms_settings_' . $vocabulary->vid, $settings);
-
-  // Now enforcing each setting.
-  foreach ($settings as $k => $v) {
-    switch ($k) {
-      case 'synonyms':
-        synonyms_synonyms_enforce($vocabulary, $v);
-        break;
+ *   Array of settings. It must have the following structure:
+ *   - instance_id: (int) ID of the instance to which it applies
+ *   - behavior: (string) name of the synonyms behavior to which it applies
+ *   - settings: (mixed) the content of settings themselves
+ */
+function synonyms_behavior_settings_save($settings) {
+  if (!isset($settings['settings'])) {
+    $settings['settings'] = array();
+  }
+  $settings['settings_serialized'] = serialize($settings['settings']);
+  $result = db_merge('synonyms_settings')
+    ->key(array(
+      'instance_id' => $settings['instance_id'],
+      'behavior' => $settings['behavior'],
+    ))
+    ->fields(array(
+      'instance_id' => $settings['instance_id'],
+      'behavior' => $settings['behavior'],
+      'settings_serialized' => $settings['settings_serialized'],
+    ))
+    ->execute();
+
+  switch ($result) {
+    case MergeQuery::STATUS_INSERT:
+      $behavior_definition = synonyms_behaviors();
+      $behavior_definition = $behavior_definition[$settings['behavior']];
+      $enabled_callback = ctools_plugin_get_function($behavior_definition, 'enabled callback');
+      if ($enabled_callback) {
+        $enabled_callback($behavior_definition, $settings['settings'], synonyms_instance_id_load($settings['instance_id']));
+      }
+      break;
+  }
+}
+
+/**
+ * Delete settings for specific behavior and field instance.
+ *
+ * @param int $instance_id
+ *   ID of the instance for which settings should be removed
+ * @param string $behavior
+ *   Name of behavior for which settings should be removed
+ */
+function synonyms_behavior_settings_delete($instance_id, $behavior) {
+  $behavior_definition = synonyms_behaviors();
+  $behavior_definition = $behavior_definition[$behavior];
+  $disabled_callback = ctools_plugin_get_function($behavior_definition, 'disabled callback');
+  if ($disabled_callback) {
+    $instance = synonyms_instance_id_load($instance_id);
+    $behavior_implementation = synonyms_behavior_get($behavior, $instance['entity_type'], $instance['bundle'], FALSE, TRUE);
+    $behavior_implementation = $behavior_implementation[$instance_id];
+    $disabled_callback($behavior_definition, $behavior_implementation, $instance);
+  }
+  db_delete('synonyms_settings')
+    ->condition('instance_id', $instance_id)
+    ->condition('behavior', $behavior)
+    ->execute();
+}
+
+/**
+ * Supportive function to load a field instance by its ID.
+ *
+ * @param int $instance_id
+ *   ID of the instance that should be loaded
+ *
+ * @return array
+ *   Instance definition array
+ */
+function synonyms_instance_id_load($instance_id) {
+  $result = db_select('field_config_instance', 'i')
+    ->fields('i', array('entity_type', 'bundle', 'field_name'))
+    ->condition('id', $instance_id)
+    ->execute()
+    ->fetchAssoc();
+  return field_info_instance($result['entity_type'], $result['field_name'], $result['bundle']);
+}
+
+/**
+ * Convert synonyms friendly select widget values for storage friendly format.
+ *
+ * It acts similar to what the _options_form_to_storage() function does -
+ * bridges between how values are returned from form API to how they are
+ * expected by Field module.
+ */
+function synonyms_select_form_to_storage($element, &$form_state) {
+  $value = array();
+  if ($element['#multiple']) {
+    $value = $element['#value'];
+  }
+  else {
+    $value[] = $element['#value'];
+  }
+
+  foreach ($value as $k => $v) {
+    // For the cases when a synonym was selected and not a term option, we
+    // process the selected values stripping everything that goes after
+    // semicolon.
+    if (!is_numeric($v)) {
+      $tid = explode(':', $v);
+      $value[$k] = $tid[0];
+    }
+  }
+
+  // The user also might have selected multiple times the same term, given that
+  // a term can be represented by more than 1 option (a term and its synonym),
+  // then it's possible in theory, so we should be ready for this scenario.
+  $value = array_unique($value);
+
+  $form_state_value = array();
+  foreach ($value as $tid) {
+    $form_state_value[] = array('tid' => $tid);
+  }
+
+  form_set_value($element, $form_state_value, $form_state);
+}
+
+/**
+ * Check whether a taxonomy term $tid is a child of a taxonomy term $parent_tid.
+ *
+ * Supportive function, used throughout this module for parent constrains.
+ *
+ * @param int $tid
+ *   {taxonomy_term}.tid of the term that is tested for being a child of the
+ *   $parent_tid term
+ * @param int $parent_tid
+ *   {taxonomy_term}.tid of the term that is tested for being parent of the $tid
+ *   term
+ *
+ * @return bool
+ *   Whether $tid is a child of $parent_tid
+ */
+function synonyms_taxonomy_term_is_child_of($tid, $parent_tid) {
+  $term_parents = taxonomy_get_parents_all($tid);
+
+  // Dropping out the term itself from its array of parents.
+  array_shift($term_parents);
+
+  foreach ($term_parents as $term_parent) {
+    if ($term_parent->tid == $parent_tid) {
+      return TRUE;
     }
   }
+
+  return FALSE;
 }
 
 /**
- * Make sure the default synonyms field exists.
+ * Format an option for select form element.
  *
- * If the field doesn't exist function creates one, if the field exists,
- * the function does nothing.
+ * @param object $term
+ *   Fully loaded taxonomy term which is represented by this option
+ * @param string $synonym
+ *   If the provided term is represented in this option by a synonym, then
+ *   provide it here
+ * @param array $behavior_implementation
+ *   Behavior implementation array from which the $synonym comes from
+ *
+ * @return object
+ *   An option for select form element
  */
-function synonyms_default_field_ensure() {
-  $field = field_info_field(SYNONYMS_DEFAULT_FIELD_NAME);
-  if (is_null($field)) {
-    $field = array(
-      'field_name' => SYNONYMS_DEFAULT_FIELD_NAME,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'type' => 'text',
-    );
-    field_create_field($field);
+function synonyms_select_option($term, $synonym = NULL, $behavior_implementation = NULL) {
+  $key = $synonym ? $term->tid . ':' . drupal_html_class($synonym) : $term->tid;
+  $wording = $term->name;
+  if ($synonym) {
+    $instance = field_info_instance($behavior_implementation['entity_type'], $behavior_implementation['field_name'], $behavior_implementation['bundle']);
+    $wording = format_string($behavior_implementation['settings']['wording'], array(
+      '@synonym' => $synonym,
+      '@term' => $term->name,
+      '@field_name' => drupal_strtolower($instance['label']),
+    ));
   }
+  return (object) array(
+    'option' => array($key => str_repeat('-', $term->depth) . $wording),
+  );
 }
 
 /**
- * Extract instances that are applicable for being source of synonyms.
+ * Supportive function to build options array with sorting by name logic.
  *
- * Walk through all instances of a vocabulary and extract only valid candidates
- * for becoming a source of synonyms for the vocabulary terms.
+ * The function starts from the 0-depth level and starts to recursively build
+ * the options and to sort the labels on each level, then it merges the bottom
+ * to up all the levels maintaining correct order within the final options
+ * array.
  *
  * @param object $vocabulary
- *   Fully loaded vocabulary object
+ *   Within which vocabulary to execute the function. Supply here the fully
+ *   loaded taxonomy vocabulary object
+ * @param int $parent
+ *   Only children of this term will be included in the output. You can supply
+ *   0 which means to include all the terms from the vocabulary
+ * @param int $depth
+ *   Used for internal purposes. Clients of this function should supply here 0,
+ *   unless they know what they are doing. It is used internally to keep track
+ *   of the nesting level
  *
  * @return array
- *   Array of instance arrays
+ *   Array of options that can be inserted directly into 'select' form element.
+ *   The options will be sorted by name (term or synonym), respecting the
+ *   hierarchy restrictions
  */
-function synonyms_instances_extract_applicable($vocabulary) {
-  $applicable_field_types = array_keys(synonyms_extractor_info());
-  $applicable = array();
+function synonyms_select_sort_name_options_recursive($vocabulary, $parent = 0, $depth = 0) {
+  // We statically cache behavior implementations in order to not DDOS the data
+  // base.
+  $behavior_implementations = drupal_static(__FUNCTION__, array());
+
   $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
-  foreach (field_info_instances('taxonomy_term', $bundle) as $instance) {
-    $field = field_info_field($instance['field_name']);
-    if (in_array($field['type'], $applicable_field_types)) {
-      $applicable[] = $instance;
+  if (!isset($behavior_implementations[$bundle])) {
+    $behavior_implementations[$bundle] = synonyms_behavior_get('select', 'taxonomy_term', $bundle, TRUE);
+  }
+
+  $options = array();
+  if ($terms = taxonomy_get_tree($vocabulary->vid, $parent, 1, TRUE)) {
+    $options = array();
+    foreach ($terms as $term) {
+      $term->depth = $depth;
+      $options[] = synonyms_select_option($term);
+      foreach ($behavior_implementations[$bundle] as $implementation) {
+        foreach (synonyms_extract_synonyms($term, $implementation) as $synonym) {
+          $options[] = synonyms_select_option($term, $synonym, $implementation);
+        }
+      }
+    }
+
+    usort($options, 'synonyms_select_sort_name');
+
+    // Now recursively go one level nested into each of the terms that we have
+    // on this level.
+    $options_copy = $options;
+    $i = 0;
+    foreach ($options_copy as $v) {
+      $i++;
+      $tid = array_keys($v->option);
+      $tid = $tid[0];
+      if (is_numeric($tid)) {
+        $nested_options = synonyms_select_sort_name_options_recursive($vocabulary, $tid, $depth + 1);
+        $options = array_merge(array_slice($options, 0, $i), $nested_options, array_slice($options, $i));
+      }
     }
   }
-  return $applicable;
+  return $options;
 }
 
 /**
- * Implements hook_views_api().
+ * Supportive function.
+ *
+ * It is used for string comparison within synonyms friendly select widget.
  */
-function synonyms_views_api() {
-  return array(
-    'api' => 3,
-    'path' => drupal_get_path('module', 'synonyms') . '/views',
-  );
+function synonyms_select_sort_name($a, $b) {
+  return strcasecmp(reset($a->option), reset($b->option));
 }

+ 95 - 144
sites/all/modules/contrib/taxonomy/synonyms/synonyms.pages.inc

@@ -56,18 +56,31 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
     exit;
   }
 
+  $widget = $instance['widget']['type'] == 'synonyms_autocomplete' ? $instance['widget']['settings'] : field_info_widget_settings('synonyms_autocomplete');
+
   // How many suggestions maximum we are able to output.
-  $max_suggestions = $instance['widget']['settings']['suggestion_size'];
+  $max_suggestions = $widget['suggestion_size'];
 
   // Whether we are allowed to suggest more than one entry per term, shall that
   // entry be either term name itself or one of its synonyms.
-  $suggest_only_unique = $instance['widget']['settings']['suggest_only_unique'];
+  $suggest_only_unique = $widget['suggest_only_unique'];
 
   // The user enters a comma-separated list of tags. We only autocomplete the
   // last tag.
   $tags_typed = drupal_explode_tags($tags_typed);
   $tag_last = drupal_strtolower(array_pop($tags_typed));
 
+  $tags_typed_tids = array();
+  if (!empty($tags_typed)) {
+    $efq = new EntityFieldQuery();
+    $efq->entityCondition('entity_type', 'taxonomy_term');
+    $efq->propertyCondition('name', $tags_typed);
+    $tags_typed_tids = $efq->execute();
+    if (isset($tags_typed_tids['taxonomy_term'])) {
+      $tags_typed_tids = array_keys($tags_typed_tids['taxonomy_term']);
+    }
+  }
+
   $term_matches = array();
   if ($tag_last != '') {
     // Part of the criteria for the query come from the field's own settings.
@@ -79,14 +92,11 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
     $vocabularies = taxonomy_vocabulary_load_multiple(array_keys($vocabularies));
 
     // Array of found suggestions. Each subarray of this array will represent
-    // a single suggestion entry. The sub array must at least contain the
-    // following keys:
-    //   tid - tid of the suggested term
-    //   name - name of the suggested term
-    // And optionally, if the term is suggested based on its synonym, the sub
-    // array should also include the additional key:
-    //   synonym - string representation of the synonym based on which the
-    //     suggested term was included.
+    // a single suggestion entry. The sub array must contain the following keys:
+    // - tid: (int) tid of the suggested term
+    // - name: (string) name of the suggested term
+    // - wording: (string) human friendly XSS escaped text of the suggestion
+    //   entry
     $tags_return = array();
 
     // Firstly getting a list of tids that match by $term->name.
@@ -95,8 +105,8 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
     $query->addTag('term_access');
 
     // Do not select already entered terms.
-    if (!empty($tags_typed)) {
-      $query->condition('t.name', $tags_typed, 'NOT IN');
+    if (!empty($tags_typed_tids)) {
+      $query->condition('t.tid', $tags_typed_tids, 'NOT IN');
     }
     // Select rows that match by term name.
     $result = $query
@@ -104,78 +114,67 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
       ->condition('t.vid', array_keys($vocabularies))
       ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE')
       ->range(0, $max_suggestions)
-      ->execute()
-      ->fetchAllKeyed();
-
-    // Converting results into another format.
-    foreach ($result as $tid => $name) {
-      $tags_return[] = array(
-        'name' => $name,
-        'tid' => $tid,
-      );
+      ->execute();
+    foreach ($result as $v) {
+      $v = (array) $v;
+      $v['wording'] = check_plain($v['name']);
+      $tags_return[] = $v;
     }
 
-    $synonym_tids = array();
     // Now we go vocabulary by vocabulary looking through synonym fields.
     foreach ($vocabularies as $vocabulary) {
       // Now we go a synonym field by synonym field gathering suggestions.
-      // Since officially EntityFieldQuery doesn't support OR conditions
-      // we are enforced to go a field by field querying multiple times the DB.
       $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
-      foreach (synonyms_synonyms_fields($vocabulary) as $field) {
-        $field = field_info_field($field);
-        $instance = field_info_instance('taxonomy_term', $field['field_name'], $bundle);
-        $query = new EntityFieldQuery();
-        $query->entityCondition('entity_type', 'taxonomy_term')
-          ->entityCondition('bundle', $bundle);
 
-        // We let the class that defines this field type as a source of synonyms
-        // filter out and provide its suggestions based on this field.
-        $class = synonyms_extractor_info($field['type']);
-        call_user_func(array($class, 'processEntityFieldQuery'), $tag_last, $query, $field, $instance);
+      $behavior_implementations = synonyms_behavior_get('autocomplete', 'taxonomy_term', $bundle, TRUE);
+      foreach ($behavior_implementations as $implementation) {
+        $condition = db_and();
+        $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, '%' . db_like($tag_last) . '%', 'LIKE');
 
-        if (!empty($tags_typed)) {
-          $query->propertyCondition('name', $tags_typed, 'NOT IN');
+        if (!empty($tags_typed_tids)) {
+          $condition->condition('entity_id', $tags_typed_tids, 'NOT IN');
         }
         if ($suggest_only_unique && !empty($tags_return)) {
           $tmp = array();
           foreach ($tags_return as $tag_return) {
             $tmp[] = $tag_return['tid'];
           }
-          // We don't want to search among the terms already found by term name.
-          $query->entityCondition('entity_id', $tmp, 'NOT IN');
-        }
-        if ($suggest_only_unique && !empty($synonym_tids)) {
-          // We also don't want to search among the terms already matched by
-          // previous synonym fields.
-          $query->entityCondition('entity_id', $synonym_tids, 'NOT IN');
+          $condition->condition('entity_id', $tmp, 'NOT IN');
         }
-        $tmp = $query->execute();
-        if (!empty($tmp)) {
-          // Merging the results.
-          $tmp = array_keys($tmp['taxonomy_term']);
-          $synonym_tids = array_merge($synonym_tids, $tmp);
+
+        $new_tids = array();
+        foreach (synonyms_synonyms_find_behavior($condition, $implementation) as $synonym) {
+          if (!$suggest_only_unique || !in_array($synonym->entity_id, $new_tids)) {
+            $tags_return[] = array(
+              'tid' => $synonym->entity_id,
+              'name' => '',
+              'synonym' => $synonym->synonym,
+              'implementation' => $implementation,
+            );
+            $new_tids[] = $synonym->entity_id;
+          }
         }
       }
     }
 
-    if (!empty($synonym_tids)) {
-      foreach (taxonomy_term_load_multiple($synonym_tids) as $synonym_term) {
-        $tmp = array(
-          'name' => $synonym_term->name,
-          'tid' => $synonym_term->tid,
-        );
-        // Additionally we have to find out which synonym triggered inclusion of
-        // this term.
-        foreach (synonyms_get_sanitized($synonym_term) as $item) {
-          if (strpos(mb_strtoupper($item, 'UTF-8'), mb_strtoupper($tag_last, 'UTF-8')) !== FALSE) {
-            $tags_return[] = array('synonym' => $item) + $tmp;
-            if ($suggest_only_unique) {
-              // We just want to output 1 single suggestion entry per term, so
-              // one synonym is enough.
-              break;
-            }
-          }
+    $synonym_terms = array();
+    foreach ($tags_return as $v) {
+      if (isset($v['synonym'])) {
+        $synonym_terms[] = $v['tid'];
+      }
+    }
+
+    if (!empty($synonym_terms)) {
+      $synonym_terms = taxonomy_term_load_multiple($synonym_terms);
+      foreach ($tags_return as &$v) {
+        if (isset($v['synonym'])) {
+          $instance = field_info_instance($v['implementation']['entity_type'], $v['implementation']['field_name'], $v['implementation']['bundle']);
+          $v['name'] = $synonym_terms[$v['tid']]->name;
+          $v['wording'] = format_string(filter_xss($v['implementation']['settings']['wording']), array(
+            '@synonym' => $v['synonym'],
+            '@term' => $v['name'],
+            '@field_name' => drupal_strtolower($instance['label']),
+          ));
         }
       }
     }
@@ -192,98 +191,50 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
       if (strpos($info['name'], ',') !== FALSE || strpos($info['name'], '"') !== FALSE) {
         $n = '"' . str_replace('"', '""', $info['name']) . '"';
       }
-
-      if (isset($info['synonym'])) {
-        $display_name = t('@synonym, synonym of %term', array('@synonym' => $info['synonym'], '%term' => $info['name']));
-      }
-      else {
-        $display_name = check_plain($info['name']);
-      }
       while (isset($term_matches[$prefix . $n])) {
         $n .= ' ';
       }
-      $term_matches[$prefix . $n] = $display_name;
+      $term_matches[$prefix . $n] = $info['wording'];
     }
   }
   drupal_json_output($term_matches);
 }
 
 /**
- * Mark nodes that reference specific terms for search re-indexing.
- *
- * This is particularly useful, when the terms in question have been changed.
- *
- * @param $tids array
- *   Array of tids of the terms
+ * Default theme implementation for behavior settings form element.
  */
-function synonyms_reindex_nodes_by_terms($tids) {
-  // In order to speed up the process, we will query DB for nid's that reference
-  // handled to us tids, and at the end we'll trigger their re-indexing in just
-  // a single SQL query. Probably it is better to use search_touch_node(), but
-  // that would imply a big amount of SQL queries on some websites.
-  $found_nids = array();
-  foreach (field_info_field_map() as $field_name => $v) {
-    if ($v['type'] == 'taxonomy_term_reference' && isset($v['bundles']['node'])) {
-      // This field is taxonomy term reference and it is attached to nodes, so
-      // we will run EntityFieldQuery on it.
-      $query = new EntityFieldQuery();
-      $result = $query->entityCondition('entity_type', 'node')
-        ->fieldCondition($field_name, 'tid', $tids, 'IN')
-        ->execute();
-      if (isset($result['node'])) {
-        $found_nids = array_merge($found_nids, array_keys($result['node']));
-      }
-    }
-  }
-  if (!empty($found_nids)) {
-    db_update('search_dataset')
-      ->fields(array('reindex' => REQUEST_TIME))
-      ->condition('type', 'node')
-      ->condition('sid', $found_nids, 'IN')
-      ->execute();
-  }
-}
-
-/**
- * Mark nodes that reference terms from a vocabulary for search re-indexing.
- *
- * @param $vocabulary object
- *   Fully loaded vocabulary object
- */
-function synonyms_reindex_nodes_by_vocabulary($vocabulary) {
-  $tids = db_select('taxonomy_term_data', 't')
-    ->fields('t', array('tid'))
-    ->condition('vid', $vocabulary->vid)
-    ->execute()
-    ->fetchCol();
-  if (!empty($tids)) {
-    synonyms_reindex_nodes_by_terms($tids);
-  }
-}
+function theme_synonyms_behaviors_settings($variables) {
+  drupal_add_css(drupal_get_path('module', 'synonyms') . '/synonyms.css');
 
-/**
- * Submit handler for Taxonomy vocabulary edit form.
- *
- * Store extra values attached to form in this module.
- */
-function synonyms_taxonomy_form_vocabulary_submit($form, &$form_state) {
-  $values = $form_state['values'];
+  $element = &$variables['element'];
 
-  if ($values['op'] == $form['actions']['submit']['#value']) {
-    if (isset($form['#vocabulary']->vid)) {
-      $vocabulary = taxonomy_vocabulary_load($form['#vocabulary']->vid);
-    }
-    else {
-      // As a fallback, if this is a just created vocabulary, we try to pull it
-      // up by the just submitted machine name.
-      $vocabulary = taxonomy_vocabulary_machine_name_load($values['machine_name']);
-    }
+  $table = array(
+    'header' => array(t('Field')),
+    'rows' => array(),
+    'empty' => t('Seems like there are no fields for which synonyms functionality is available. Try adding a text field to get started.'),
+  );
 
-    // Preprocessing values keeping only field names of the checked fields.
-    $values['synonyms']['synonyms'] = array_values(array_filter($values['synonyms']['synonyms']));
+  $instance_ids = array();
+  foreach (element_children($element) as $behavior) {
+    $table['header'][] = check_plain($element[$behavior]['#title']);
+    $instance_ids = array_unique(array_merge($instance_ids, element_children($element[$behavior])));
+  }
 
-    $settings = synonyms_vocabulary_settings($vocabulary);
-    $settings = $values['synonyms'] + $settings;
-    synonyms_vocabulary_settings_save($vocabulary, $settings);
+  foreach ($instance_ids as $instance_id) {
+    $row = array();
+    $row_title = '';
+    foreach (element_children($element) as $behavior) {
+      if (isset($element[$behavior][$instance_id]['#title']) && !$row_title) {
+        $row_title = check_plain($element[$behavior][$instance_id]['#title']);
+      }
+      $row[] = array(
+        'data' => isset($element[$behavior][$instance_id]) ? drupal_render($element[$behavior][$instance_id]) : t('Not implemented'),
+        'class' => array('synonyms-behavior-settings', 'synonyms-behavior-settings-' . $behavior),
+      );
+    }
+    array_unshift($row, $row_title);
+    $table['rows'][] = $row;
   }
+
+  return '<div id="' . $element['#id'] . '">' . theme('table', $table) . drupal_render_children($element) . '</div>';
 }

File diff suppressed because it is too large
+ 443 - 426
sites/all/modules/contrib/taxonomy/synonyms/synonyms.test


+ 12 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchEntityReferenceSynonymsBehavior.class.inc

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Enables Entity Reference field type for search integration.
+ */
+
+/**
+ * Definition of SearchEntityReferenceSynonymsBehavior class.
+ */
+class SearchEntityReferenceSynonymsBehavior extends EntityReferenceSynonymsBehavior implements SearchSynonymsBehavior {
+}

+ 14 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchSynonymsBehavior.interface.inc

@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Interfaces of synonyms behaviors shipped with Synonyms Search module.
+ */
+
+
+/**
+ * Interface of search integration synonyms behavior.
+ */
+interface SearchSynonymsBehavior extends SynonymsSynonymsBehavior {
+}
+

+ 12 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchTaxonomySynonymsBehavior.class.inc

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Enables Taxonomy Term Reference field type for search integration.
+ */
+
+/**
+ * Definition of SearchTaxonomySynonymsBehavior class.
+ */
+class SearchTaxonomySynonymsBehavior extends TaxonomySynonymsBehavior implements SearchSynonymsBehavior {
+}

+ 12 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchTextSynonymsBehavior.class.inc

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Enables text and number field types for search integration.
+ */
+
+/**
+ * Definition of SearchTextSynonymsBehavior class.
+ */
+class SearchTextSynonymsBehavior extends TextSynonymsBehavior implements SearchSynonymsBehavior {
+}

+ 36 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/plugins/behavior/search.inc

@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Plugin definition for synonyms search behavior.
+ */
+
+$plugin = array(
+  'title' => t('Search'),
+  'description' => t('Integrate synonyms with Search module'),
+  'interface' => 'SearchSynonymsBehavior',
+  'enabled callback' => 'synonyms_search_behavior_search_enabled',
+  'disabled callback' => 'synonyms_search_behavior_search_disabled',
+);
+
+/**
+ * Callback for when the behavior is enabled.
+ *
+ * Trigger re-indexing of all the nodes that reference terms from the vocabulary
+ * where the change has taken place.
+ */
+function synonyms_search_behavior_search_enabled($behavior_definition, $settings, $instance) {
+  module_load_include('inc', 'synonyms_search', 'synonyms_search.pages');
+  synonyms_search_reindex_nodes_by_vocabulary(taxonomy_vocabulary_machine_name_load($instance['bundle']));
+}
+
+/**
+ * Callback for when the behavior is disabled.
+ *
+ * Trigger re-indexing of all the nodes that reference terms from the vocabulary
+ * where the change has taken place.
+ */
+function synonyms_search_behavior_search_disabled($behavior_definition, $behavior_implementation, $instance) {
+  module_load_include('inc', 'synonyms_search', 'synonyms_search.pages');
+  synonyms_search_reindex_nodes_by_vocabulary(taxonomy_vocabulary_machine_name_load($instance['bundle']));
+}

+ 20 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.info

@@ -0,0 +1,20 @@
+name = Synonyms Search
+description = "Provides synonyms integration with searching."
+package = Taxonomy
+core = 7.x
+dependencies[] = synonyms
+dependencies[] = search
+
+files[] = synonyms_search.test
+
+files[] = includes/SearchSynonymsBehavior.interface.inc
+files[] = includes/SearchTextSynonymsBehavior.class.inc
+files[] = includes/SearchTaxonomySynonymsBehavior.class.inc
+files[] = includes/SearchEntityReferenceSynonymsBehavior.class.inc
+
+; Information added by Drupal.org packaging script on 2015-11-29
+version = "7.x-1.3"
+core = "7.x"
+project = "synonyms"
+datestamp = "1448771941"
+

+ 115 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.module

@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Provides synonyms integration with searching.
+ */
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function synonyms_search_ctools_plugin_directory($owner, $plugin_type) {
+  switch ($owner) {
+    case 'synonyms':
+      switch ($plugin_type) {
+        case 'behavior':
+          return 'plugins/' . $plugin_type;
+      }
+      break;
+  }
+}
+
+/**
+ * Implements hook_taxonomy_term_update().
+ */
+function synonyms_search_taxonomy_term_update($term) {
+  // If we notice at least some change in synonyms of this term, we want to
+  // trigger search re-indexing of nodes, where this term is referenced, since
+  // change in term synonyms affects nodes ranking in the search.
+  if (isset($term->original)) {
+    $bundle = field_extract_bundle('taxonomy_term', $term);
+    $behavior_implementations = synonyms_behavior_get('search', 'taxonomy_term', $bundle, TRUE);
+
+    $current_search_synonyms = array();
+    $previous_search_synonyms = array();
+
+    foreach  ($behavior_implementations as $behavior_implementation) {
+      $current_search_synonyms = array_merge($current_search_synonyms, synonyms_extract_synonyms($term, $behavior_implementation));
+      $previous_search_synonyms = array_merge($previous_search_synonyms, synonyms_extract_synonyms($term->original, $behavior_implementation));
+    }
+    $diff = array_diff($current_search_synonyms, $previous_search_synonyms);
+    if (!empty($diff) || count($current_search_synonyms) != count($previous_search_synonyms)) {
+      module_load_include('inc', 'synonyms_search', 'synonyms_search.pages');
+      synonyms_search_reindex_nodes_by_terms(array($term->tid));
+    }
+  }
+}
+
+/**
+ * Implements hook_node_update_index().
+ */
+function synonyms_search_node_update_index($node) {
+  $output = array();
+  foreach (field_info_instances('node', $node->type) as $instance) {
+    // We go a field by field looking for taxonomy term reference and if that
+    // vocabulary has enabled search synonyms, we add them to the search index.
+    $field_info = field_info_field($instance['field_name']);
+    if ($field_info['type'] == 'taxonomy_term_reference') {
+      // For each term referenced in this node we have to add synonyms.
+      $terms = field_get_items('node', $node, $instance['field_name']);
+      if (is_array($terms) && !empty($terms)) {
+        foreach ($terms as $v) {
+          $output[] = $v['tid'];
+        }
+      }
+    }
+  }
+
+  if (!empty($output)) {
+    $terms = taxonomy_term_load_multiple($output);
+    $output = array();
+    foreach ($terms as $term) {
+      $bundle = field_extract_bundle('taxonomy_term', $term);
+      $behavior_implementations = synonyms_behavior_get('search', 'taxonomy_term', $bundle, TRUE);
+      foreach ($behavior_implementations as $implementation) {
+        $output = array_merge($output, synonyms_extract_synonyms($term, $implementation));
+      }
+    }
+  }
+
+  return empty($output) ? '' : '<strong>' . implode($output, ', ') . '</strong>';
+}
+
+/**
+ * Implements hook_term_update_index().
+ */
+function synonyms_search_term_update_index($term) {
+  $bundle = taxonomy_vocabulary_load($term->vid);
+  $bundle = field_extract_bundle('taxonomy_term', $bundle);
+  $behavior_implementations = synonyms_behavior_get('search', 'taxonomy_term', $bundle, TRUE);
+
+  $synonyms = array();
+  foreach ($behavior_implementations as $implementation) {
+    $synonyms = array_merge($synonyms, synonyms_extract_synonyms($term, $implementation));
+  }
+  return implode(', ', $synonyms);
+}
+
+/**
+ * Implements hook_synonyms_behavior_implementation_info().
+ */
+function synonyms_search_synonyms_behavior_implementation_info($behavior) {
+  switch ($behavior) {
+    case 'search':
+      return array(
+        'number_integer' => 'SearchTextSynonymsBehavior',
+        'number_decimal' => 'SearchTextSynonymsBehavior',
+        'number_float' => 'SearchTextSynonymsBehavior',
+        'text' => 'SearchTextSynonymsBehavior',
+        'taxonomy_term_reference' => 'SearchTaxonomySynonymsBehavior',
+        'entityreference' => 'SearchEntityReferenceSynonymsBehavior',
+      );
+      break;
+  }
+  return array();
+}

+ 68 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.pages.inc

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Supportive functions for the module.
+ */
+
+/**
+ * Mark nodes that reference specific terms for search re-indexing.
+ *
+ * This is particularly useful, when the terms in question have been changed.
+ *
+ * @param $tids array
+ *   Array of tids of the terms
+ */
+function synonyms_search_reindex_nodes_by_terms($tids) {
+  // In order to speed up the process, we will query DB for nid's that reference
+  // handled to us tids, and at the end we'll trigger their re-indexing just in
+  // a single SQL query. Probably it is better to use search_touch_node(), but
+  // that would imply a big amount of SQL queries on some websites.
+  $found_nids = array();
+  foreach (field_info_field_map() as $field_name => $v) {
+    if ($v['type'] == 'taxonomy_term_reference' && isset($v['bundles']['node'])) {
+      // This field is taxonomy term reference and it is attached to nodes, so
+      // we will run EntityFieldQuery on it.
+      $query = new EntityFieldQuery();
+      $result = $query->entityCondition('entity_type', 'node')
+        ->fieldCondition($field_name, 'tid', $tids, 'IN')
+        ->execute();
+      if (isset($result['node'])) {
+        $found_nids = array_merge($found_nids, array_keys($result['node']));
+      }
+    }
+  }
+  if (!empty($found_nids)) {
+    db_update('search_dataset')
+      ->fields(array('reindex' => REQUEST_TIME))
+      ->condition('type', 'node')
+      ->condition('sid', $found_nids, 'IN')
+      ->execute();
+  }
+
+  // Integration with Term Search module: reset terms index too.
+  if (module_exists('term_search')) {
+    db_update('search_dataset')
+      ->fields(array('reindex' => REQUEST_TIME))
+      ->condition('type', 'term')
+      ->condition('sid', $tids)
+      ->execute();
+  }
+}
+
+/**
+ * Mark nodes that reference terms from a vocabulary for search re-indexing.
+ *
+ * @param $vocabulary object
+ *   Fully loaded vocabulary object
+ */
+function synonyms_search_reindex_nodes_by_vocabulary($vocabulary) {
+  $tids = db_select('taxonomy_term_data', 't')
+    ->fields('t', array('tid'))
+    ->condition('vid', $vocabulary->vid)
+    ->execute()
+    ->fetchCol();
+  if (!empty($tids)) {
+    synonyms_search_reindex_nodes_by_terms($tids);
+  }
+}

+ 367 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.test

@@ -0,0 +1,367 @@
+<?php
+
+/**
+ * @file
+ * Tests for the Synonyms Search module.
+ */
+
+/**
+ * Base class for tests of Synonyms Search module.
+ */
+abstract class AbstractSearchSynonymsWebTestCase extends SynonymsWebTestCase {
+
+  protected $behavior = 'search';
+
+  /**
+   * What search type is being tested.
+   *
+   * @var string
+   */
+  protected $search_type = 'node';
+
+  /**
+   * Array of terms that will be used for testing.
+   *
+   * @var array
+   */
+  protected $terms = array();
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    $modules[] = 'synonyms_search';
+    parent::setUp($modules);
+
+    // Create a few terms and synonyms.
+    $term = (object) array(
+      'vid' => $this->vocabulary->vid,
+      'name' => $this->randomName(),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    taxonomy_term_save($term);
+    $this->terms['no_synonyms'] = $term;
+
+    $term = (object) array(
+      'vid' => $this->vocabulary->vid,
+      'name' => $this->randomName(),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    taxonomy_term_save($term);
+    $this->terms['one_synonym'] = $term;
+
+    $term = (object) array(
+      'vid' => $this->vocabulary->vid,
+      'name' => $this->randomName(),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+          array('value' => $this->randomName()),
+        ),
+      ),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    taxonomy_term_save($term);
+    $this->terms['two_synonyms'] = $term;
+  }
+
+  /**
+   * Retrieve search results.
+   *
+   * @param $keyword string
+   *   Keyword to supply to the search mechanism
+   *
+   * @return array
+   *   Array of HTML search results. Each element in this array is a single
+   *   search result represented in HTML code as Drupal search mechanism outputs
+   *   it
+   */
+  protected function getSearchResults($keyword) {
+    $response = $this->drupalGet('search/' . $this->search_type . '/' . $keyword);
+    $matches = array();
+    preg_match_all('#\<li[^>]+class="search-result"[^>]*\>(.*?)\</li\>#si', $response, $matches);
+    return $matches[1];
+  }
+}
+
+/**
+ * Test Synonyms module integration with Drupal search functionality for nodes.
+ */
+class NodeSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Synonyms node search integration',
+      'description' => 'Ensure that Synonyms module correctly integrates with the Drupal search functionality.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    parent::setUp($modules);
+
+    // Creating a test content type.
+    $this->drupalPost('admin/structure/types/add', array(
+      'name' => 'Synonyms Test Content',
+      'type' => 'synonyms_test_content',
+    ), 'Save content type');
+
+    // Attaching term reference field to the new content type.
+    $field = array(
+      'type' => 'taxonomy_term_reference',
+      'field_name' => 'synonyms_term_enabled',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'settings' => array(
+        'allowed_values' => array(
+          array(
+            'vocabulary' => $this->vocabulary->machine_name,
+            'parent' => 0,
+          ),
+        ),
+      ),
+    );
+    $field = field_create_field($field);
+
+    $instance = array(
+      'field_name' => $field['field_name'],
+      'entity_type' => 'node',
+      'bundle' => 'synonyms_test_content',
+      'label' => 'Synonym Terms',
+      'widget' => array(
+        'type' => 'synonyms_autocomplete',
+      ),
+    );
+
+    field_create_instance($instance);
+  }
+
+  /**
+   * Test searching nodes by a term synonym.
+   *
+   * Since logically term and its synonyms represent the same entity, the idea
+   * is that searching by a term synonym should trigger all content referencing
+   * that term to be included in search results. Additionally we test that when
+   * a synonym is deleted/edited in a term, corresponding content is no longer
+   * encountered when searched by ex-synonym.
+   */
+  public function testSearchTermSynonym() {
+    // Creating a node, which references all the terms we have.
+    $node = (object) array(
+      'type' => 'synonyms_test_content',
+      'title' => $this->randomName(),
+      'synonyms_term_enabled' => array(LANGUAGE_NONE => array(
+        array('tid' => $this->terms['no_synonyms']->tid),
+        array('tid' => $this->terms['one_synonym']->tid),
+        array('tid' => $this->terms['two_synonyms']->tid),
+      )),
+    );
+    node_save($node);
+
+    // Rebuilding Search index.
+    $this->cronRun();
+
+    foreach ($this->terms as $k => $term) {
+      $this->assertSearchResults($term->name, array($node), 'Searching by name of the term ' . $k);
+      $items = field_get_items('taxonomy_term', $term, $this->fields['disabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $delta => $item) {
+          $this->assertSearchResults($item['value'], array(), 'Searching by not enabled search integration field value #' . $delta . ' of term ' . $k);
+        }
+      }
+
+      $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $delta => $item) {
+          $this->assertSearchResults($item['value'], array($node), 'Searching by synonym #' . $delta . ' of the term ' . $k);
+        }
+      }
+    }
+
+    // Removing a synonym from the term. Then asserting node got re-indexed with
+    // new values of synonyms.
+    $deleted_synonym = array_pop($this->terms['one_synonym']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE]);
+    taxonomy_term_save($this->terms['one_synonym']);
+    $this->cronRun();
+    $this->assertSearchResults($deleted_synonym['value'], array(), 'Searching by recently deleted synonym of a taxonomy term yields no results.');
+
+    // Editing a synonym in a term. Then asserting node got re-indexed with new
+    // values of synonyms.
+    $ex_synonym = $this->terms['two_synonyms']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'];
+    $this->terms['two_synonyms']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'] = $this->randomName();
+    taxonomy_term_save($this->terms['two_synonyms']);
+    $this->cronRun();
+    $this->assertSearchResults($ex_synonym, array(), 'Searching by recently changed synonym of a taxonomy term yields no results.');
+
+    // We disable entire field from search integration and make sure for all
+    // synonyms search results are empty.
+    synonyms_behavior_settings_delete($this->fields['enabled']['instance']['id'], $this->behavior);
+    $this->cronRun();
+    foreach ($this->terms as $k => $term) {
+      $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $synonym) {
+          $this->assertSearchResults($synonym['value'], array(), 'Searching by ' . $k . ' term synonym, which field was recently disabled from search behavior yields no results.');
+        }
+      }
+    }
+  }
+
+  /**
+   * Assert search results.
+   *
+   * @param $keyword string
+   *   Keyword to supply to the search mechanism
+   * @param $results array
+   *   Array of fully loaded nodes that are expected to be on search results
+   * @param $message string
+   *   Drupal assertion message to display on test results page
+   */
+  protected function assertSearchResults($keyword, $results, $message) {
+    $matches = $this->getSearchResults($keyword);
+    if (count($matches) != count($results)) {
+      $this->fail($message);
+      return;
+    }
+    $matches = implode('', $matches);
+    foreach ($results as $node) {
+      if (strpos($matches, 'node/' . $node->nid) === FALSE) {
+        $this->fail($message);
+        return;
+      }
+    }
+    $this->pass($message);
+  }
+}
+
+/**
+ * Test Synonyms module integration with Drupal search for taxonomy terms.
+ */
+class TermSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
+
+  protected $search_type = 'term';
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Synonyms term search integration',
+      'description' => 'Ensure that Synonyms module correctly integrates with the Term Search module.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    $modules[] = 'term_search';
+    parent::setUp($modules);
+    $active_searches = variable_get('search_active_modules', array('node', 'user'));
+    $active_searches[] = 'term_search';
+    variable_set('search_active_modules', $active_searches);
+  }
+
+  /**
+   * Test searching terms by their synonyms.
+   */
+  public function testSearchTermSynonym() {
+    // Rebuilding Search index.
+    $this->cronRun();
+
+    foreach ($this->terms as $k => $term) {
+      $this->assertSearchResults($term->name, array($term), 'Searching by name of the term ' . $k);
+      $items = field_get_items('taxonomy_term', $term, $this->fields['disabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $delta => $item) {
+          $this->assertSearchResults($item['value'], array(), 'Searching by not enabled search integration field value #' . $delta . ' of term ' . $k);
+        }
+      }
+
+      $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $delta => $item) {
+          $this->assertSearchResults($item['value'], array($term), 'Searching by synonym #' . $delta . ' of the term ' . $k);
+        }
+      }
+    }
+
+    // Removing a synonym from the term. Then asserting it got re-indexed with
+    // new values of synonyms.
+    $deleted_synonym = array_pop($this->terms['one_synonym']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE]);
+    taxonomy_term_save($this->terms['one_synonym']);
+    $this->cronRun();
+    $this->assertSearchResults($deleted_synonym['value'], array(), 'Searching by recently deleted synonym of a taxonomy term yields no results.');
+
+    // Editing a synonym in a term. Then asserting it got re-indexed with new
+    // values of synonyms.
+    $ex_synonym = $this->terms['two_synonyms']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'];
+    $this->terms['two_synonyms']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'] = $this->randomName();
+    taxonomy_term_save($this->terms['two_synonyms']);
+    $this->cronRun();
+    $this->assertSearchResults($ex_synonym, array(), 'Searching by recently changed synonym of a taxonomy term yields no results.');
+
+    // We disable entire field from search integration and make sure for all
+    // synonyms search results are empty.
+    synonyms_behavior_settings_delete($this->fields['enabled']['instance']['id'], $this->behavior);
+    $this->cronRun();
+    foreach ($this->terms as $k => $term) {
+      $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);
+      if (is_array($items)) {
+        foreach ($items as $synonym) {
+          $this->assertSearchResults($synonym['value'], array(), 'Searching by ' . $k . ' term synonym, which field was recently disabled from search behavior yields no results.');
+        }
+      }
+    }
+  }
+
+  /**
+   * Assert search results.
+   *
+   * @param $keyword string
+   *   Keyword to supply to the search mechanism
+   * @param $results array
+   *   Array of fully loaded terms that are expected to be on search results
+   * @param $message string
+   *   Drupal assertion message to display on test results page
+   */
+  protected function assertSearchResults($keyword, $results, $message) {
+    $matches = $this->getSearchResults($keyword);
+    if (count($matches) != count($results)) {
+      $this->fail($message);
+      return;
+    }
+    $matches = implode('', $matches);
+    foreach ($results as $term) {
+      if (strpos($matches, 'taxonomy/term/' . $term->tid) === FALSE) {
+        $this->fail($message);
+        return;
+      }
+    }
+    $this->pass($message);
+  }
+
+}

+ 18 - 2
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms.views.inc

@@ -7,11 +7,27 @@
 
 /**
  * Implements hook_views_plugins_alter().
- *
- * @see hook_views_plugins_alter()
  */
 function synonyms_views_plugins_alter(&$plugins) {
   // Replace default taxonomy term argument validator with our extended version,
   // which can also handle a term synonym as an argument.
   $plugins['argument validator']['taxonomy_term']['handler'] = 'synonyms_views_plugin_argument_validate_taxonomy_term';
 }
+
+/**
+ * Implements hook_field_views_data_alter().
+ */
+function synonyms_field_views_data_alter(&$result, $field, $module) {
+  if ($field['type'] == 'taxonomy_term_reference') {
+    // Add synonyms friendly autocomplete filter.
+    foreach ($field['storage']['details']['sql'] as $table) {
+      $tid_column = reset($table);
+      $tid_column = $tid_column['tid'];
+      $table = array_keys($table);
+      $table = $table[0];
+      if (isset($result[$table][$tid_column]['filter'])) {
+        $result[$table][$tid_column]['filter']['handler'] = 'synonyms_views_handler_filter_term_tid';
+      }
+    }
+  }
+}

+ 174 - 0
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_term_tid.inc

@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * @file
+ * Definition of synonyms_views_handler_filter_term_tid class.
+ */
+
+/**
+ * Synonyms friendly taxonomy filter handler.
+ */
+class synonyms_views_handler_filter_term_tid extends views_handler_filter_term_node_tid {
+  function extra_options_form(&$form, &$form_state) {
+    parent::extra_options_form($form, $form_state);
+
+    $form['type']['#options']['synonyms_autocomplete'] = t('Synonyms friendly autocomplete');
+  }
+
+  function value_form(&$form, &$form_state) {
+    $restore_value = $this->options['type'] == 'synonyms_autocomplete';
+    if ($restore_value) {
+      $this->options['type'] = 'textfield';
+    }
+    parent::value_form($form, $form_state);
+
+    if ($restore_value) {
+      // We need to determine the entity type onto which this field is attached
+      // that is used in this view.
+      $entity_type_base_table = $this->view->base_table;
+      // TODO: it would be nice to consider the existence of relationships, but
+      // I just couldn't figure it out at that time.
+
+      $entity_info = entity_get_info();
+      $field_entity_type = FALSE;
+      $field = field_info_field($this->definition['field_name']);
+
+      foreach ($field['bundles'] as $entity_type => $bundles) {
+        if ($entity_info[$entity_type]['base table'] == $entity_type_base_table) {
+          $field_entity_type = $entity_type;
+          break;
+        }
+      }
+
+      if (!$field_entity_type) {
+        // Seems like we failed to determine the entity type which is used for
+        // this field in the view. Well, it's not a fatal fail, we'll just use
+        // whatever then.
+        $field_entity_type = array_keys($field['bundles']);
+        $field_entity_type = $field_entity_type[0];
+      }
+
+      // We just grab the first instance of this field within the determined
+      // entity type.
+      $bundle = $field['bundles'][$field_entity_type][0];
+
+      $instance = field_info_instance($field_entity_type, $field['field_name'], $bundle);
+      if ($instance['widget']['type'] == 'synonyms_autocomplete') {
+        $widget = $instance['widget']['settings'];
+      }
+      else {
+        $widget = field_info_widget_settings('synonyms_autocomplete');
+      }
+      $autocomplete_path = $widget['synonyms_autocomplete_path'];
+      $size = $widget['size'];
+
+      $form['value']['#autocomplete_path'] = $autocomplete_path . '/' . $this->definition['field_name'] . '/' . $field_entity_type . '/' . $bundle;
+      $form['value']['#size'] = $size;
+      $form['value']['#auto_creation'] = FALSE;
+      $form['value']['#attributes']['class'][] = 'synonyms-autocomplete';
+      $form['value']['#attached']['js'][drupal_get_path('module', 'synonyms') . '/js/synonyms-autocomplete.js'] = array();
+      $this->options['type'] = 'synonyms_autocomplete';
+    }
+  }
+
+  function value_validate($form, &$form_state) {
+    if ($this->options['type'] == 'synonyms_autocomplete') {
+      $values = drupal_explode_tags($form_state['values']['options']['value']);
+      $tids = $this->synonyms_validate_term_strings($form['value'], $values);
+
+      if ($tids) {
+        $form_state['values']['options']['value'] = $tids;
+      }
+    }
+    else {
+      parent::value_validate($form, $form_state);
+    }
+  }
+
+  function exposed_validate(&$form, &$form_state) {
+    if ($this->options['type'] == 'synonyms_autocomplete') {
+      if (empty($this->options['exposed'])) {
+        return;
+      }
+      if (empty($this->options['expose']['identifier'])) {
+        return;
+      }
+
+      $identifier = $this->options['expose']['identifier'];
+      $values = drupal_explode_tags($form_state['values'][$identifier]);
+      $tids = $this->synonyms_validate_term_strings($form[$identifier], $values);
+
+      if ($tids) {
+        $this->validated_exposed_input = $tids;
+      }
+    }
+    else {
+      parent::exposed_validate($form, $form_state);
+    }
+  }
+
+  /**
+   * Validate the user string.
+   *
+   * In a great extend it does the same job as parent::validate_term_strings(),
+   * just that this implementation is synonyms-aware.
+   *
+   * @param $element
+   *   The form element which is used, either the views ui or the exposed
+   *   filters.
+   * @param $values
+   *   The taxonomy names/synonyms which will be converted to tids.
+   *
+   * @return array
+   *   The taxonomy ids for all validated terms.
+   */
+  protected function synonyms_validate_term_strings($element, $values) {
+    if (empty($values)) {
+      return array();
+    }
+
+    $values = array_map('drupal_strtolower', $values);
+    $missing = array_flip($values);
+
+    $tids = array();
+
+    $vocabulary = taxonomy_vocabulary_machine_name_load($this->options['vocabulary']);
+
+    // Firstly looking up the entered tags as if they were term names. Then,
+    // the remaining tags are looked up as if they were synonyms of terms.
+    // Lastly, if any tags are left at this point, we mark form validation
+    // error.
+    $query = db_select('taxonomy_term_data', 'td');
+    $query->fields('td', array('tid', 'name'));
+    $query->condition('td.vid', $vocabulary->vid);
+    $query->condition('td.name', $values);
+    $query->addTag('term_access');
+    $result = $query->execute();
+    foreach ($result as $term) {
+      unset($missing[drupal_strtolower($term->name)]);
+      $tids[] = $term->tid;
+    }
+
+    $behavior_implementations = synonyms_behavior_get('autocomplete', 'taxonomy_term', $vocabulary->machine_name, TRUE);
+    foreach ($behavior_implementations as $behavior_implementation) {
+      if (!empty($missing)) {
+        $condition = db_or();
+        foreach ($missing as $tag => $v) {
+          $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $tag);
+        }
+        $synonyms = synonyms_synonyms_find_behavior($condition, $behavior_implementation);
+        foreach ($synonyms as $synonym) {
+          $synonym->synonym = drupal_strtolower($synonym->synonym);
+          unset($missing[$synonym->synonym]);
+          $tids[] = $synonym->entity_id;
+        }
+      }
+    }
+
+    if (!empty($missing) && !empty($this->options['error_message'])) {
+      form_error($element, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing)))));
+    }
+
+    return $tids;
+  }
+}

+ 24 - 76
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_plugin_argument_validate_taxonomy_term.inc

@@ -27,18 +27,10 @@ class synonyms_views_plugin_argument_validate_taxonomy_term extends views_plugin
     switch ($type) {
       case 'synonym':
       case 'synonym_tid':
-        // We are requested to do synonyms friendly validation. We will have to
-        // go through each of allowed vocabularies, collecting terms whose
-        // synonyms might look like the argument we have. However, it is not
-        // that easy to achieve due to the fact that synonyms may be kept in
-        // whatever format in whatever column and only corresponding Synonyms
-        // extractor class has the knowledge how it is organized. Firstly we are
-        // going to query database trying to find a term with our argument's
+        // We are requested to do synonyms friendly validation. Firstly we are
+        // going to query the database trying to find a term with our argument's
         // name. If we find one, it is great and we stop right there. Otherwise,
-        // the nightmare starts: we are going to use
-        // AbstractSynonymsExtractor::processEntityFieldQuery() method to find
-        // synonyms that are similar to our argument. Then we will load those
-        // terms and manually in PHP will determine whether it is a match.
+        // we look up by synonyms.
         $query = db_select('taxonomy_term_data', 't')
           ->fields('t', array('tid', 'name'));
         if (!empty($vocabularies)) {
@@ -46,7 +38,7 @@ class synonyms_views_plugin_argument_validate_taxonomy_term extends views_plugin
           $query->condition('v.machine_name', $vocabularies);
         }
         if ($transform) {
-          $query->where("replace(t.name, ' ', '-') = :name", array(':name' => $argument));
+          $query->where("REPLACE(t.name, ' ', '-') = :name", array(':name' => $argument));
         }
         else {
           $query->condition('t.name', $argument, '=');
@@ -61,76 +53,32 @@ class synonyms_views_plugin_argument_validate_taxonomy_term extends views_plugin
           return TRUE;
         }
 
+        $bundles = $vocabularies;
         if (empty($vocabularies)) {
-          // At this point we want to convert an empty $vocabulary (implicitly
-          // meaning "all") into actually a list of all fully loaded
-          // vocabularies.
-          $vocabularies = taxonomy_vocabulary_load_multiple(FALSE);
+          // At this point we want to convert an empty $vocabularies (implicitly
+          // meaning "all") into actually a list of all vocabularies.
+          $bundles = array_keys(taxonomy_vocabulary_get_names());
         }
-        else {
-          // We need to load the filtered vocabularies based on their machine
-          // names.
-          foreach ($vocabularies as $v) {
-            $vocabularies[$v] = taxonomy_vocabulary_machine_name_load($v);
-          }
-        }
-
-        // We haven't had much luck, seems like we have to do the hard part. We
-        // will pull up terms that are similar to our argument using the same
-        // functionality as for synonyms friendly autocomplete widget and then
-        // will find those who actually match among this narrowed set of terms.
-        // Since $match will be use as value for LIKE SQL operator, we are not
-        // sure whether we need to substitute dash (-) with spacebar, or keep
-        // the dash, to match both we will use underscore (_) since this symbol
-        // matches one symbol in LIKE operator.
-        $match = $transform ? str_replace('-', '_', $argument) : $argument;
-        $tids = array();
-        // Unfortunately we have to query multiple times the database for each
-        // vocabulary for each synonyms-source field.
-        foreach ($vocabularies as $vocabulary) {
-          $bundle = field_extract_bundle('taxonomy_term', $vocabulary);
-          foreach (synonyms_synonyms_fields($vocabulary) as $field) {
-            $field = field_info_field($field);
-            $instance = field_info_instance('taxonomy_term', $field['field_name'], $bundle);
-            $query = new EntityFieldQuery();
-            $query->entityCondition('entity_type', 'taxonomy_term')
-              ->entityCondition('bundle', $vocabulary->machine_name);
 
-            // We let the class that defines this field type as a source of
-            // synonyms filter out and provide its suggestions based on this
-            // field.
-            $class = synonyms_extractor_info($field['type']);
-            call_user_func(array($class, 'processEntityFieldQuery'), $match, $query, $field, $instance);
-            if (!empty($tids)) {
-              $query->entityCondition('entity_id', $tids, 'NOT IN');
-            }
-            $result = $query->execute();
-            if (isset($result['taxonomy_term'])) {
-              $tids = array_merge($tids, array_keys($result['taxonomy_term']));
-            }
+        foreach ($bundles as $bundle) {
+          $condition = db_and();
+          if ($transform) {
+            $condition->where("REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-') = :argument", array(
+              ':argument' => $argument,
+            ));
           }
-        }
-        $terms = taxonomy_term_load_multiple($tids);
-        // We have an array of terms whose synonyms look like they could be
-        // equal to our argument, let us iterate over each term's synonyms to
-        // actually see if they match.
-        $argument = mb_strtoupper($argument, 'UTF-8');
-        foreach ($terms as $term) {
-          foreach (synonyms_get_term_synonyms($term) as $synonym) {
-            $synonym = mb_strtoupper($synonym['safe_value'], 'UTF-8');
-            if ($transform) {
-              $synonym = str_replace(' ', '-', $synonym);
-            }
-            if ($synonym == $argument) {
-              // Finally we found a synonym that matches our argument. We are
-              // going to use the corresponding term and there is no point to
-              // continue searching.
-              $this->argument->argument = $this->synonyms_get_term_property($term);
-              $this->argument->validated_title = check_plain(entity_label('taxonomy_term', $term));
-              return TRUE;
-            }
+          else {
+            $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $argument);
+          }
+          $synonyms = synonyms_synonyms_find($condition, 'taxonomy_term', $bundle);
+          if (!empty($synonyms)) {
+            $term = taxonomy_term_load($synonyms[0]->entity_id);
+            $this->argument->argument = $this->synonyms_get_term_property($term);
+            $this->argument->validated_title = check_plain(entity_label('taxonomy_term', $term));
+            return TRUE;
           }
         }
+
         // We haven't found any synonym that matched our argument, so there is
         // no term to return.
         return FALSE;

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