Sfoglia il codice sorgente

updated synonyms to 1.5

Bachir Soussi Chiadmi 7 anni fa
parent
commit
252abe9b0e
51 ha cambiato i file con 5749 aggiunte e 1393 eliminazioni
  1. 69 58
      sites/all/modules/contrib/taxonomy/synonyms/README.txt
  2. 4 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms.help.ini
  3. 4 4
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms.html
  4. 9 8
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behavior_implementation.html
  5. 20 0
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behavior_implementation_field_based.html
  6. 1 2
      sites/all/modules/contrib/taxonomy/synonyms/help/synonyms_behaviors.html
  7. 0 84
      sites/all/modules/contrib/taxonomy/synonyms/includes/EntityReferenceSynonymsBehavior.class.inc
  8. 85 99
      sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsBehavior.interface.inc
  9. 0 68
      sites/all/modules/contrib/taxonomy/synonyms/includes/TextSynonymsBehavior.class.inc
  10. 14 12
      sites/all/modules/contrib/taxonomy/synonyms/plugins/arguments/term_synonyms.inc
  11. 2 2
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/autocomplete.inc
  12. 1 1
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/select.inc
  13. 0 12
      sites/all/modules/contrib/taxonomy/synonyms/plugins/behavior/synonyms.inc
  14. 141 95
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.api.php
  15. 112 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.features.inc
  16. 11 6
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.info
  17. 138 15
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.install
  18. 455 379
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.module
  19. 341 38
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.pages.inc
  20. 738 358
      sites/all/modules/contrib/taxonomy/synonyms/synonyms.test
  21. 66 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/includes/CommerceProductReferenceSynonymsBehavior.class.inc
  22. 19 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.info
  23. 247 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.module
  24. 128 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.pages.inc
  25. 849 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.test
  26. 81 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/AbstractFieldSynonymsBehavior.class.inc
  27. 86 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/EntityReferenceSynonymsBehavior.class.inc
  28. 23 22
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/TaxonomySynonymsBehavior.class.inc
  29. 69 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/TextSynonymsBehavior.class.inc
  30. 72 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.api.php
  31. 20 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.info
  32. 97 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.module
  33. 873 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.test
  34. 56 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/AbstractPropertySynonymsBehavior.class.inc
  35. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/PropertySynonymsBehavior.class.inc
  36. 14 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/SearchPropertySynonymsBehavior.class.inc
  37. 18 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.info
  38. 98 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.module
  39. 112 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.test
  40. 12 0
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchCommerceProductReferenceSynonymsBehavior.class.inc
  41. 1 1
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/includes/SearchSynonymsBehavior.interface.inc
  42. 10 6
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/plugins/behavior/search.inc
  43. 9 4
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.info
  44. 20 9
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.module
  45. 14 2
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.pages.inc
  46. 108 18
      sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.test
  47. 57 13
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms.views.inc
  48. 102 0
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_field_synonyms.inc
  49. 144 0
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_entityreference_synonyms.inc
  50. 184 74
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_term_tid.inc
  51. 3 3
      sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_plugin_argument_validate_taxonomy_term.inc

+ 69 - 58
sites/all/modules/contrib/taxonomy/synonyms/README.txt

@@ -1,69 +1,84 @@
 
 -- SUMMARY --
 
-The Synonyms module extends the Drupal core Taxonomy features. Currently
-the module provides this additional functionality:
+The Synonyms module enriches Entities with the notion of synonyms. Currently the
+module provides the following functionality:
 * 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
+  implementation exists, can be enabled as source of synonyms.
+* support of synonyms through Entity properties. Entity properties stored in
+  database (as opposed to being calculated on-the-fly) can be enabled as source
+  of synonyms.
+* synonyms-friendly autocomplete and select widgets for taxonomy_term_reference,
+  entityreference, and commerce_product_reference (through Synonyms Commerce
+  submodule) 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.
+  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 entity synonyms 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 entities.
+* integration with Views. Synonyms module provides a few filters and contextual
+  filters that allow filtering not only by entity name but also by one of its
+  synonyms. Synonyms module also provides a Views field for all eligible
+  entities that contains a list of synonyms associated with the entity in
+  question.
 
 -- REQUIREMENTS --
 
 The Synonyms module requires the following modules:
-* Taxonomy module
-* CTools module
+* Entity API module
 
--- SYNONYMS BEHAVIOR, SUPPORTED FIELD TYPES --
-
-Module ships with ability to use the following field types as synonyms:
-* Text
-* Taxonomy Term Reference
+The Synonyms module integrates with (but does not require) the following
+modules:
+* Taxonomy
+* Search
+* Term Search
 * Entity Reference
-* Number
-* Float
-* Decimal
-
-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.
+* Views
+* Commerce
+* Features
+* Term Merge
+
+-- SUPPORTED SYNONYMS PROVIDERS --
+
+Module ships with ability to provide synonyms from the following locations:
+* "Text" field type
+* "Taxonomy Term Reference" field type
+* "Entity Reference" field type
+* "Commerce Product Reference" field type
+* "Number" field type
+* "Float" field type
+* "Decimal" field type
+* Entity properties stored in database
+
+Worth mentioning here: this list is easily extended further by implementing new
+synonyms providers in your code. Refer to Synonyms advanced help for more
+details on how to accomplish it.
 
 -- 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.
+granularity into what "synonyms" mean. This granularity is expressed via
+"synonyms behavior" idea whatsoever. Then you can enable different synonyms
+behaviors for different synonyms providers. 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):
+* Autocomplete - whether synonyms from this provider should participate in
+  autocomplete suggestions. This module ships with autocomplete synonyms
+  friendly widgets and their autocomplete suggestions will be filled in with the
+  synonyms of providers that have this behavior enabled.
+* Select - whether synonyms from this provider should be included in the
+  synonyms friendly select widgets.
 * 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.
+  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.
+Therefore, on the Synonyms configuration page you will see a table, where rows
+are synonym providers and columns are these "synonym behaviors" and you decide
+what synonym behaviors to activate on what synonym providers.
 
 -- INSTALLATION --
 
@@ -71,15 +86,11 @@ you decide what synonym behaviors to activate on what fields.
 
 -- CONFIGURATION --
 
-* The module itself does not provide any configuration as of the moment.
-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.
+* You can configure synonyms of all eligible entity types by going to Admin ->
+  Structure -> Synonyms (admin/structure/synonyms)
 
 -- FUTURE DEVELOPMENT --
 
-* If you are interested into converting this module from synonyms for Taxonomy
-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.
+* No good directions for future development are known at the moment. If you
+  would like to suggest one, report an issue (future request) against Synonyms
+  issue queue on Drupal.org.

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

@@ -12,3 +12,7 @@ weight = -1
 [synonyms_behavior_implementation]
 title = Creating a new behavior implementation
 parent = synonyms
+
+[synonyms_behavior_implementation_field_based]
+title = Creating a new behavior implementation from a field
+parent = synonyms_behavior_implementation

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


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


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


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


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

@@ -1,84 +0,0 @@
-<?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();
-  }
-}

+ 85 - 99
sites/all/modules/contrib/taxonomy/synonyms/includes/SynonymsBehavior.interface.inc

@@ -11,114 +11,61 @@
  * 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.
+   * Extract synonyms from an entity within a specific behavior implementation.
    *
-   * 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
+   *   Entity from which to extract synonyms
    *
    * @return array
-   *   Array of synonyms extracted from $items
+   *   Array of synonyms extracted from $entity
    */
-  public function extractSynonyms($items, $field, $instance, $entity, $entity_type);
+  public function extractSynonyms($entity);
 
   /**
-   * Add an entity as a synonym into a field of another entity.
+   * Add an entity as a synonym into 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.
+   * 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 update $trunk_entity in such a way that it
+   * holds $synonym_entity as a synonym (it all depends on how data is stored in
+   * your behavior implementation, but probably you will store entity label or
+   * its ID as you cannot literally store an entity inside of another entity).
+   * If entity of type $synonym_entity_type cannot be converted into a format
+   * expected by your behavior implementation, just do nothing.
    *
-   * @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 $trunk_entity
+   *   Entity into which another one should be added as synonym
    * @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);
+  public function mergeEntityAsSynonym($trunk_entity, $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.
+   * Look up entities by their synonyms within a behavior implementation.
    *
    * 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.
+   * of synonyms within the provided behavior implementation. 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
+   *   Condition that defines what to search for. Apart from normal SQL
+   *   conditions as known in Drupal, it may contain the following placeholders:
+   *   - AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER: to denote
+   *     synonyms column which you should replace with the actual column name
+   *     where the synonyms data for your provider is stored in plain text.
+   *   - AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER: to denote
+   *     column that holds entity ID. You are supposed to replace this
+   *     placeholder with actual column name that holds entity ID in your case.
+   *   For ease of work with these placeholders, you may extend the
+   *   AbstractSynonymsBehavior class and then just invoke the
+   *   AbstractSynonymsBehavior->synonymsFindProcessCondition() method, so you
+   *   won't have to worry much about it
    *
    * @return Traversable
    *   Traversable result set of found synonyms and entity IDs to which those
@@ -128,47 +75,86 @@ interface SynonymsSynonymsBehavior extends SynonymsBehavior {
    *     provided condition
    *   - entity_id: (int) ID of the entity to which the found synonym belongs
    */
-  public function synonymsFind(QueryConditionInterface $condition, $field, $instance);
+  public function synonymsFind(QueryConditionInterface $condition);
+
+  /**
+   * Collect info on features pipe during invocation of hook_features_export().
+   *
+   * If your synonyms provider depends on some other features components, this
+   * method should return them.
+   *
+   * @return array
+   *   Array of features pipe as per hook_features_export() specification
+   */
+  public function featuresExportPipe();
 }
 
 /**
- * Exception thrown by implementations of SynonymsSynonymsBehavior interface.
+ * Exception thrown by implementations of SynonymsBehavior interface.
  */
-class SynonymsSynonymsBehaviorException extends Exception {}
+class SynonymsBehaviorException extends Exception {}
 
 /**
- * Starting point for implementing SynonymsSynonymsBehavior interface.
+ * Starting point for implementing SynonymsBehavior interface.
  */
-abstract class AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior {
+abstract class AbstractSynonymsBehavior implements SynonymsBehavior {
 
   /**
    * Constant which denotes placeholder of a synonym column.
    *
    * @var string
    */
-  const COLUMN_PLACEHOLDER = '***COLUMN***';
+  const COLUMN_SYNONYM_PLACEHOLDER = '***COLUMN***';
+
+  /**
+   * Constant which denotes placeholder of an entity ID column.
+   *
+   * @var string
+   */
+  const COLUMN_ENTITY_ID_PLACEHOLDER = '***ENTITY_ID***';
+
+  /**
+   * Behavior implementation on which this class was initialized.
+   *
+   * @var array
+   */
+  protected $behavior_implementation;
+
+  public function __construct($behavior_implementation) {
+    $this->behavior_implementation = $behavior_implementation;
+  }
+
+  public function featuresExportPipe() {
+    return array();
+  }
 
   /**
    * Process condition in 'synonymsFind' method.
    *
    * Process condition in 'synonymsFind' method replacing all references of
-   * synonym column with the real name of that column.
+   * synonym and entity ID columns with the real names of those columns.
    *
    * @param QueryConditionInterface $condition
    *   Condition that should be processed
-   * @param string $column
+   * @param string $column_synonym
    *   Real name of the synonym column
+   * @param string $column_entity_id
+   *   Real name of the entity ID column
    */
-  protected function synonymsFindProcessCondition(QueryConditionInterface $condition, $column) {
+  protected function synonymsFindProcessCondition(QueryConditionInterface $condition, $column_synonym, $column_entity_id) {
     $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);
+          $this->synonymsFindProcessCondition($v['field'], $column_synonym, $column_entity_id);
         }
         else {
-          $v['field'] = str_replace(self::COLUMN_PLACEHOLDER, $column, $v['field']);
+          $replace = array(
+            self::COLUMN_SYNONYM_PLACEHOLDER => $column_synonym,
+            self::COLUMN_ENTITY_ID_PLACEHOLDER => $column_entity_id,
+          );
+          $v['field'] = str_replace(array_keys($replace), array_values($replace), $v['field']);
         }
       }
     }
@@ -178,11 +164,11 @@ abstract class AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehav
 /**
  * Interface of the autocomplete synonyms behavior.
  */
-interface AutocompleteSynonymsBehavior extends SynonymsSynonymsBehavior {
+interface AutocompleteSynonymsBehavior extends SynonymsBehavior {
 }
 
 /**
  * Interface of the synonyms friendly select behavior.
  */
-interface SelectSynonymsBehavior extends SynonymsSynonymsBehavior {
+interface SelectSynonymsBehavior extends SynonymsBehavior {
 }

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

@@ -1,68 +0,0 @@
-<?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();
-  }
-}

+ 14 - 12
sites/all/modules/contrib/taxonomy/synonyms/plugins/arguments/term_synonyms.inc

@@ -5,16 +5,18 @@
  * 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',
-);
+if (module_exists('taxonomy')) {
+  $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.
@@ -66,12 +68,12 @@ function synonyms_term_synonyms_context($arg = NULL, $conf = NULL, $empty = FALS
       foreach ($vocabularies as $vocabulary) {
         $condition = db_and();
         if ($conf['transform']) {
-          $condition->where("REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-') = :argument", array(
+          $condition->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :argument", array(
             ':argument' => $arg,
           ));
         }
         else {
-          $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $arg);
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $arg);
         }
         $rows = synonyms_synonyms_find($condition, 'taxonomy_term', $vocabulary->machine_name);
         if (!empty($rows)) {

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

@@ -23,8 +23,8 @@ function synonyms_behavior_autocomplete_settings_form($form, &$form_state, $sett
   $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>'),
+    '#default_value' => isset($settings['wording']) ? $settings['wording'] : '@synonym is a synonym of @entity',
+    '#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>@entity</em> to denote entity name</li><li><em>@field_name</em> to denote lowercase label of the field from where the synonym originates</li></ul>'),
     '#required' => TRUE,
   );
 

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

@@ -24,7 +24,7 @@ function synonyms_behavior_select_settings_form($form, &$form_state, $settings)
     '#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>'),
+    '#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>@entity</em> to denote entity name</li><li><em>@field_name</em> to denote lowercase label of the field from where the synonym originates</li></ul>'),
     '#required' => TRUE,
   );
 

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

@@ -1,12 +0,0 @@
-<?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',
-);

+ 141 - 95
sites/all/modules/contrib/taxonomy/synonyms/synonyms.api.php

@@ -6,124 +6,170 @@
  */
 
 /**
- * Hook to collect info about available synonyms behavior implementations.
+ * Collect info about available synonyms behavior implementations.
  *
- * Hook to collect info about what PHP classes implement provided synonyms
- * behavior for different field types.
+ * If your module ships a synonyms behavior implementation you probably want to
+ * implement this hook. However, exercise caution, if your synonyms behavior
+ * implementation is a field-based one, you might be better off implementing
+ * hook_synonyms_field_behavior_implementation_info().
  *
+ * @param string $entity_type
+ *   Entity type whose synonyms behavior implementations are requested
+ * @param string $bundle
+ *   Bundle name whose synonyms behavior implementations are requested
  * @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
+ *   Behavior name whose implementations are requested
  *
  * @return array
- *   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
+ *   Array of information about synonyms behavior implementations your module
+ *   exposes. Each sub array will represent a single synonyms behavior
+ *   implementation and should have the following structure:
+ *   - provider: (string) machine name of your synonyms behavior implementation.
+ *     Prefix it with your module name to make sure no name collision happens.
+ *     Also, provider must be unique within the namespace of behavior, entity
+ *     type and bundle. Basically, this is what distinguishes one behavior
+ *     implementation from another
+ *   - label: (string) Human friendly translated name of your synonyms behavior
+ *     implementation
+ *   - class: (string) Name of PHP class that implements synonyms behavior
+ *     interface, which is stated in synonyms behavior definition. This class
+ *     will do all the synonyms work. This hook serves pure declarative function
+ *     to map entity types, bundles with their synonym behavior implementations
+ *     whereas real "synonyms-related" work is implemented in your class
  */
-function hook_synonyms_behavior_implementation_info($behavior) {
-  switch ($behavior) {
-    case 'autocomplete':
-      return array(
-        'my-field-type' => 'MyFieldTypeAutocompleteSynonymsBehavior',
-      );
-      break;
+function hook_synonyms_behavior_implementation_info($entity_type, $bundle, $behavior) {
+  $providers = array();
+
+  switch ($entity_type) {
+    case 'entity_type_i_want':
+      switch ($bundle) {
+        case 'bundle_i_want':
+          switch ($behavior) {
+            case 'behavior_i_want':
+              $providers[] = array(
+                'provider' => 'my_module_synonyms_behavior_implementation_machine_name',
+                'label' => t('This is human friendly name of my synonyms behavior implementation. Put something meaningful here'),
+                'class' => 'MySynonymsSynonymsBehavior',
+              );
+              break;
+          }
+          break;
+      }
 
-    case 'another-behavior':
-      return array(
-        'my-field-type-or-yet-another-field-type' => 'MyFieldTypeAnotherBehaviorSynonymsBehavior',
-      );
       break;
   }
 
-  return array();
+  return $providers;
 }
 
 /**
- * Hook to alter info about available synonyms behavior implementations.
+ * Example of synonyms behavior implementation class.
  *
- * 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.
- *
- * @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
- */
-function hook_synonyms_behavior_implementation_info_alter(&$info, $behavior) {
-  switch ($behavior) {
-    case 'the-behavior-i-want':
-      $info['the-field-type-i-want'] = 'MyFieldTypeAutocompleteSynonymsBehavior';
-      break;
-  }
-}
-
-/**
- * Example of how to implement a synonyms behavior for an arbitrary field type.
+ * You are encouraged to extend AbstractSynonymsBehavior class as that one
+ * contains a few heuristic that make your implementation easier.
  */
-class MyFieldTypeAutocompleteSynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements AutocompleteSynonymsBehavior {
+class MySynonymsSynonymsBehavior extends AbstractSynonymsBehavior 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.
+  /**
+   * Extract synonyms from an entity within a specific behavior implementation.
+   *
+   * @param object $entity
+   *   Entity from which to extract synonyms
+   *
+   * @return array
+   *   Array of synonyms extracted from $entity
+   */
+  public function extractSynonyms($entity) {
     $synonyms = array();
-    foreach ($items as $item) {
-      $synonyms[] = $item['foo'];
-    }
+
+    // Do something with $entity in order to extract synonyms from it. Add all
+    // those synonyms into your $synonyms array.
+
     return $synonyms;
   }
 
-  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 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 update $trunk_entity in such a way that
+   * it holds $synonym_entity as a synonym (it all depends on how data is stored
+   * in your behavior implementation, but probably you will store entity label
+   * or its ID as you cannot literaly store an entity inside of another entity).
+   * If entity of type $synonym_entity_type cannot be converted into a format
+   * expected by your behavior implementation, just do nothing.
+   *
+   * @param object $trunk_entity
+   *   Entity into which another one should be added as synonym
+   * @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
+   */
+  public function mergeEntityAsSynonym($trunk_entity, $synonym_entity, $synonym_entity_type) {
+    // If you can add $synonym_entity into $trunk_entity, then do so.
+    // For example:
+    $trunk_entity->synonym_storage[] = $synonym_entity;
   }
 
-  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'];
+  /**
+   * Look up entities by their synonyms within a behavior implementation.
+   *
+   * You are provided with a SQL condition that you should apply to the storage
+   * of synonyms within the provided behavior implementation. And then return
+   * result: what entities match by the provided condition through what
+   * synonyms.
+   *
+   * @param QueryConditionInterface $condition
+   *   Condition that defines what to search for. Apart from normal SQL
+   *   conditions as known in Drupal, it may contain the following placeholders:
+   *   - AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER: to denote
+   *     synonyms column which you should replace with the actual column name
+   *     where the synonyms data for your provider is stored in plain text.
+   *   - AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER: to denote
+   *     column that holds entity ID. You are supposed to replace this placeholder
+   *     with actual column name that holds entity ID in your case.
+   *   For ease of work with these placeholders, you may extend the
+   *   AbstractSynonymsBehavior class and then just invoke the
+   *   AbstractSynonymsBehavior->synonymsFindProcessCondition() method, so you
+   *   won't have to worry much about it
+   *
+   * @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) {
+    // Here, as an example, we'll query an imaginary table where your module
+    // supposedly keeps synonyms. We'll also use helpful
+    // AbstractSynonymsBehavior::synonymsFindProcessCondition() to normalize
+    // $condition argument.
+    $query = db_select('my_synonyms_storage_table', 'table');
+    $query->addField('table', 'entity_id', 'entity_id');
+    $query->addField('table', 'synonym', 'synonym');
+    $this->synonymsFindProcessCondition($condition, 'table.synonym', 'table.entity_id');
+    $query->condition($condition);
+    return $query->execute();
   }
 
-  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'],
-      )));
-    }
-    // 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();
+  /**
+   * Collect info on features pipe during invocation of hook_features_export().
+   *
+   * If your synonyms provider depends on some other features components, this
+   * method should return them.
+   *
+   * @return array
+   *   Array of features pipe as per hook_features_export() specification
+   */
+  public function featuresExportPipe() {
+    $pipe = parent::featuresExportPipe();
+    // Here you can add any additional features components your provider
+    // depends on.
+    return $pipe;
   }
 }

+ 112 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms.features.inc

@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Synonyms module integration with Features.
+ */
+
+/**
+ * Implements hook_features_export().
+ */
+function synonyms_features_export($data, &$export, $module_name) {
+  $pipe = array();
+
+  $export['features']['synonyms'] = array();
+  foreach ($data as $v) {
+    list($entity_type, $bundle, $provider, $behavior) = explode(':', $v);
+    $behavior_definition = synonyms_behaviors();
+    $behavior_definition = $behavior_definition[$behavior];
+    $export['dependencies'][] = $behavior_definition['module'];
+
+    $export['features']['synonyms'][$v] = $v;
+
+    $behavior_implementation = synonyms_behavior_get_all_enabled($entity_type, $bundle, $provider, $behavior);
+    $behavior_implementation = reset($behavior_implementation);
+
+    $provider_info = synonyms_behavior_implementation_info($entity_type, $bundle, $behavior);
+    $provider_info = $provider_info[$provider];
+    $export['dependencies'][] = $provider_info['module'];
+
+    $pipe = array_merge_recursive($pipe, $behavior_implementation['object']->featuresExportPipe());
+  }
+
+  $export['dependencies'][] = 'synonyms';
+  $export['dependencies'] = drupal_map_assoc(array_unique($export['dependencies']));
+  return $pipe;
+}
+
+/**
+ * Implements hook_features_export_options().
+ */
+function synonyms_features_export_options() {
+  $options = array();
+
+  foreach (synonyms_behavior_get_all_enabled() as $behavior_implementation) {
+    $key = array(
+      $behavior_implementation['entity_type'],
+      $behavior_implementation['bundle'],
+      $behavior_implementation['provider'],
+      $behavior_implementation['behavior'],
+    );
+    if ($behavior_implementation['entity_type'] == $behavior_implementation['bundle']) {
+      $label = t('@entity_type @provider @behavior', array(
+        '@entity_type' => $behavior_implementation['entity_type'],
+        '@provider' => $behavior_implementation['label'],
+        '@behavior' => $behavior_implementation['behavior'],
+      ));
+    }
+    else {
+      $label = t('@entity_type @bundle @provider @behavior', array(
+        '@entity_type' => $behavior_implementation['entity_type'],
+        '@bundle' => $behavior_implementation['bundle'],
+        '@provider' => $behavior_implementation['label'],
+        '@behavior' => $behavior_implementation['behavior'],
+      ));
+    }
+    $options[implode(':', $key)] = $label;
+  }
+
+  return $options;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function synonyms_features_export_render($module_name, $data, $export = NULL) {
+  $code = array();
+  $code[] = '  $synonyms = array();';
+  foreach ($data as $name) {
+    list($entity_type, $bundle, $provider, $behavior) = explode(':', $name);
+    $behavior_implementation = synonyms_behavior_get_all_enabled($entity_type, $bundle, $provider, $behavior);
+    $behavior_implementation = reset($behavior_implementation);
+    if (is_array($behavior_implementation)) {
+      $behavior_implementation = array_intersect_key($behavior_implementation, drupal_map_assoc(array(
+        'entity_type', 'bundle', 'provider', 'settings', 'behavior',
+      )));
+    }
+    $code[] = "  \$synonyms['{$name}'] = " . features_var_export($behavior_implementation, '  ') .";";
+  }
+  $code[] = "  return \$synonyms;";
+  $code = implode("\n", $code);
+  return array('default_synonyms' => $code);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function synonyms_features_revert($module_name) {
+  $synonyms = module_invoke($module_name, 'default_synonyms');
+  foreach ($synonyms as $v) {
+    synonyms_behavior_implementation_save($v);
+  }
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function synonyms_features_rebuild($module_name) {
+  $synonyms = module_invoke($module_name, 'default_synonyms');
+  foreach ($synonyms as $v) {
+    synonyms_behavior_implementation_save($v);
+  }
+}

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

@@ -1,9 +1,12 @@
 name = Synonyms
-description = "Provides synonyms feature for working with Drupal Taxonomy"
-package = Taxonomy
+description = "Provides synonyms feature for Drupal entities."
+package = Synonyms
 core = 7.x
-dependencies[] = taxonomy
 dependencies[] = ctools
+dependencies[] = entity
+configure = admin/structure/synonyms
+
+test_dependencies[] = entityreference:entityreference
 
 files[] = synonyms.test
 
@@ -15,10 +18,12 @@ 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
+files[] = views/synonyms_views_handler_field_synonyms.inc
+files[] = views/synonyms_views_handler_filter_entityreference_synonyms.inc
 
-; Information added by Drupal.org packaging script on 2015-12-02
-version = "7.x-1.4"
+; Information added by Drupal.org packaging script on 2016-05-07
+version = "7.x-1.5"
 core = "7.x"
 project = "synonyms"
-datestamp = "1449079740"
+datestamp = "1462586641"
 

+ 138 - 15
sites/all/modules/contrib/taxonomy/synonyms/synonyms.install

@@ -12,13 +12,25 @@ 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.',
+    'description' => 'Stores synonyms settings for all the entities and providers. Only enabled synonyms behavior implementations 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',
+      'entity_type' => array(
+        'description' => 'Entity type whose behavior implementation is stored in this row.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'bundle' => array(
+        'description' => 'Bundle name whose behavior implementation is stored in this row.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'provider' => array(
+        'description' => 'Provider name whose behavior implementation is stored in this row.',
+        'type' => 'varchar',
+        'length' => 255,
         'not null' => TRUE,
-        'unsigned' => TRUE,
       ),
       'behavior' => array(
         'description' => 'Name of the synonyms behavior (ctools plugin), whose settings are stored in this row.',
@@ -34,16 +46,11 @@ function synonyms_schema() {
       ),
     ),
     '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'),
+      // We build 2 different indexes on the same column set because there are
+      // 2 different functions that may query this table and the columns they
+      // filter on may vary.
+      'behavior_implementation' => array('behavior', 'entity_type', 'bundle', 'provider'),
+      'all_enabled' => array('entity_type', 'bundle', 'provider', 'behavior'),
     ),
   );
 
@@ -64,6 +71,33 @@ function synonyms_uninstall() {
   }
 }
 
+/**
+ * Implements hook_requirements().
+ */
+function synonyms_requirements($phase) {
+  $requirements = array();
+  switch ($phase) {
+    case 'update':
+      // Synonyms has changed its dependencies. It used to be Taxonomy and now
+      // it is Entity module. We make sure Entity is enabled otherwise we halt
+      // the update for Synonyms.
+      // TODO: remove this requirement check at some point, when it becomes
+      // obsolete.
+      $t = get_t();
+      $requirements['synonyms_entity_dependency'] = array(
+        'title' => $t('Synonyms depends on Entity'),
+        'description' => $t('Synonyms module depends on Entity module since 4-Mar-2016. At the same time it no longer depends on Taxonomy module while does integrate with the latter. If you do not have installed/enabled Entity module, do so before running updates on Synonyms module. Until Entity module is enabled and updates are executed, Synonyms module performance may degrade and even trigger PHP errors. See <a href="@url">@url</a> for more info.', array(
+          '@url' => 'https://www.drupal.org/node/1194802',
+        )),
+        'value' => module_exists('entity') ? $t('Passed') : $t('Failed'),
+        'severity' => module_exists('entity') ? REQUIREMENT_INFO : REQUIREMENT_ERROR,
+      );
+      break;
+  }
+
+  return $requirements;
+}
+
 /**
  * Implements hook_update_N().
  *
@@ -188,3 +222,92 @@ function synonyms_update_7102() {
     variable_del($row->name);
   }
 }
+
+/**
+ * Expanding Synonyms module to work with all entity types.
+ *
+ * Namely, the following happens:
+ * - general "synonyms" behavior gets removed as too ambiguous.
+ * - alter schema of synonyms table
+ * - "select" and "autocomplete" behaviors undergo change in settings ("@term"
+ *   placeholder is replaced with "@entity" one)
+ * - taxonomy term reference widgets get renamed to their new names
+ * - all enabled behavior implementations are renamed to their new names in
+ *   order to support field- and property-based synonyms implementations
+ */
+function synonyms_update_7103() {
+  db_delete('synonyms_settings')
+    ->condition('behavior', 'synonyms')
+    ->execute();
+
+  db_drop_index('synonyms_settings', 'behavior');
+  db_drop_unique_key('synonyms_settings', 'instance_behavior');
+
+  db_add_field('synonyms_settings', 'entity_type', array(
+    'description' => 'Entity type whose behavior implementation is stored in this row.',
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+    'default' => '',
+  ));
+  db_add_field('synonyms_settings', 'bundle', array(
+    'description' => 'Bundle name whose behavior implementation is stored in this row.',
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+    'default' => '',
+  ));
+  db_add_field('synonyms_settings', 'provider', array(
+    'description' => 'Provider name whose behavior implementation is stored in this row.',
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+    'default' => '',
+  ));
+
+  db_query('UPDATE {synonyms_settings} s INNER JOIN {field_config_instance} i ON i.id = s.instance_id SET s.entity_type = i.entity_type, s.bundle = i.bundle, s.provider = i.field_name');
+
+  db_drop_field('synonyms_settings', 'instance_id');
+
+  db_add_unique_key('synonyms_settings', 'behavior_implementation', array('behavior', 'entity_type', 'bundle', 'provider'));
+  db_add_unique_key('synonyms_settings', 'all_enabled', array('entity_type', 'bundle', 'provider', 'behavior'));
+
+  module_enable(array('synonyms_provider_field'));
+
+  $alter_behaviors = array('select', 'autocomplete');
+  foreach (synonyms_behavior_get_all_enabled() as $behavior_implementation) {
+    db_delete('synonyms_settings')
+      ->condition('provider', $behavior_implementation['provider'])
+      ->execute();
+    $behavior_implementation['provider'] = synonyms_provider_field_provider_name(field_info_field($behavior_implementation['provider']));
+    if (in_array($behavior_implementation['behavior'], $alter_behaviors)) {
+      $behavior_implementation['settings']['wording'] = str_replace('@term', '@entity', $behavior_implementation['settings']['wording']);
+    }
+    synonyms_behavior_implementation_save($behavior_implementation);
+  }
+
+  // Keys are the old widget names, whereas corresponding values are the new
+  // widget names.
+  $migrate_map = array(
+    'synonyms_select' => 'synonyms_select_taxonomy_term',
+    'synonyms_autocomplete' => 'synonyms_autocomplete_taxonomy_term',
+  );
+  foreach (field_info_field_map() as $field_name => $field_info) {
+    if ($field_info['type'] == 'taxonomy_term_reference') {
+      foreach ($field_info['bundles'] as $entity_type => $bundles) {
+        foreach ($bundles as $bundle) {
+          $instance = field_read_instance($entity_type, $field_name, $bundle);
+          if (in_array($instance['widget']['type'], array_keys($migrate_map))) {
+            $instance['widget']['type'] = $migrate_map[$instance['widget']['type']];
+
+            if ($instance['widget']['type'] == 'synonyms_autocomplete_taxonomy_term' && $instance['widget']['settings']['synonyms_autocomplete_path'] == 'synonyms/autocomplete') {
+              $instance['widget']['settings']['synonyms_autocomplete_path'] = 'synonyms/autocomplete-taxonomy-term';
+            }
+
+            field_update_instance($instance);
+          }
+        }
+      }
+    }
+  }
+}

File diff suppressed because it is too large
+ 455 - 379
sites/all/modules/contrib/taxonomy/synonyms/synonyms.module


+ 341 - 38
sites/all/modules/contrib/taxonomy/synonyms/synonyms.pages.inc

@@ -26,7 +26,7 @@
  *   autocomplete form element. Only the last term is used for autocompletion.
  *   Defaults to '' (an empty string).
  */
-function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed = '') {
+function synonyms_autocomplete_taxonomy_term($field_name, $entity_type, $bundle, $tags_typed = '') {
   // If the request has a '/' in the search text, then the menu system will have
   // split it into multiple arguments, recover the intended $tags_typed.
   $args = func_get_args();
@@ -56,7 +56,7 @@ 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');
+  $widget = $instance['widget']['type'] == 'synonyms_autocomplete_taxonomy_term' ? $instance['widget']['settings'] : field_info_widget_settings('synonyms_autocomplete_taxonomy_term');
 
   // How many suggestions maximum we are able to output.
   $max_suggestions = $widget['suggestion_size'];
@@ -81,7 +81,14 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
     }
   }
 
-  $term_matches = array();
+  // Array of found suggestions. Each subarray of this array will represent a
+  // single suggestion entry.
+  // - tid: (int) tid of the suggested term
+  // - name: (string) name of the suggested term
+  // - synonym: (string) optional synonym string that matched this entry
+  // - behavior_implementation: (array) optional behavior implementation that
+  //   provided the synonym
+  $tags_return = array();
   if ($tag_last != '') {
     // Part of the criteria for the query come from the field's own settings.
     $vocabularies = array();
@@ -91,14 +98,6 @@ 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 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.
     $query = db_select('taxonomy_term_data', 't');
     $query->addTag('translatable');
@@ -116,9 +115,7 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
       ->range(0, $max_suggestions)
       ->execute();
     foreach ($result as $v) {
-      $v = (array) $v;
-      $v['wording'] = check_plain($v['name']);
-      $tags_return[] = $v;
+      $tags_return[] = (array) $v;
     }
 
     // Now we go vocabulary by vocabulary looking through synonym fields.
@@ -129,27 +126,27 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
       $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');
+        $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like($tag_last) . '%', 'LIKE');
 
         if (!empty($tags_typed_tids)) {
-          $condition->condition('entity_id', $tags_typed_tids, 'NOT IN');
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $tags_typed_tids, 'NOT IN');
         }
         if ($suggest_only_unique && !empty($tags_return)) {
           $tmp = array();
           foreach ($tags_return as $tag_return) {
             $tmp[] = $tag_return['tid'];
           }
-          $condition->condition('entity_id', $tmp, 'NOT IN');
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $tmp, 'NOT IN');
         }
 
         $new_tids = array();
-        foreach (synonyms_synonyms_find_behavior($condition, $implementation) as $synonym) {
+        foreach ($implementation['object']->synonymsFind($condition) 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,
+              'behavior_implementation' => $implementation,
             );
             $new_tids[] = $synonym->entity_id;
           }
@@ -168,36 +165,201 @@ function synonyms_autocomplete($field_name, $entity_type, $bundle, $tags_typed =
       $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']),
-          ));
         }
       }
     }
-    $prefix = empty($tags_typed) ? '' : drupal_implode_tags($tags_typed) . ', ';
-
     if (count($tags_return) > $max_suggestions) {
       $tags_return = array_slice($tags_return, 0, $max_suggestions);
     }
+  }
+  $prefix = empty($tags_typed) ? '' : drupal_implode_tags($tags_typed) . ', ';
+  drupal_json_output(synonyms_autocomplete_format($tags_return, $prefix));
+}
+
+/**
+ * Page callback: Outputs JSON for entity autocomplete suggestions.
+ *
+ * This callback outputs entity name suggestions in response to Ajax requests
+ * made by the synonyms autocomplete widget for entity reference fields. The
+ * output is a JSON object of plain-text entity suggestions, keyed by the
+ * user-entered value with the completed entity name appended. Entity names
+ * containing commas are wrapped in quotes. The search is made with
+ * consideration of synonyms.
+ *
+ * @param string $field_name
+ *   The name of the entity reference field.
+ * @param string $entity_type
+ *   Entity type to which the supplied $field_name is attached to
+ * @param string $bundle
+ *   Bundle name to which the supplied $field_name is attached to
+ * @param string $tags_typed
+ *   (optional) A comma-separated list of entity names entered in the
+ *   autocomplete form element. Only the last term is used for autocompletion.
+ *   Defaults to '' (an empty string).
+ */
+function synonyms_autocomplete_entity($field_name, $entity_type, $bundle, $tags_typed = '') {
+  // If the request has a '/' in the search text, then the menu system will have
+  // split it into multiple arguments, recover the intended $tags_typed.
+  $args = func_get_args();
+  // Shift off the $field_name argument.
+  array_shift($args);
+  // Shift off the $entity_type argument.
+  array_shift($args);
+  // Shift off the $bundle argument.
+  array_shift($args);
+  $tags_typed = implode('/', $args);
+
+  if (!($field = field_info_field($field_name)) || $field['type'] != 'entityreference') {
+    print t('Entity reference field @field_name not found.', array('@field_name' => $field_name));
+    exit;
+  }
+
+  if (!($instance = field_info_instance($entity_type, $field['field_name'], $bundle))) {
+    // Error string. The JavaScript handler will realize this is not JSON and
+    // will display it as debugging information.
+    print t('There was not found an instance of @field_name in @entity_type.', array(
+      '@field_name' => $field_name,
+      '@entity_type' => $entity_type,
+    ));
+    exit;
+  }
+
+  $widget = $instance['widget']['type'] == 'synonyms_autocomplete_entity' ? $instance['widget']['settings'] : field_info_widget_settings('synonyms_autocomplete_entity');
+
+  // How many suggestions maximum we are able to output.
+  $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 = $widget['suggest_only_unique'];
+
+  $tags_typed = drupal_explode_tags($tags_typed);
+  $tag_last = drupal_strtolower(array_pop($tags_typed));
+  $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
+
+  $handler = entityreference_get_selection_handler($field, $instance, $entity_type, NULL);
+
+  $tags_typed_entity_ids = array();
+  if (!empty($tags_typed)) {
+    foreach ($handler->getReferencableEntities($tags_typed, 'IN') as $target_entity_ids) {
+      $tags_typed_entity_ids = array_merge($tags_typed_entity_ids, array_keys($target_entity_ids));
+    }
+  }
+
+  $matches = array();
+  if ($tag_last) {
+    foreach ($handler->getReferencableEntities($tag_last) as $target_entity_ids) {
+      foreach (array_diff_key($target_entity_ids, drupal_map_assoc($tags_typed_entity_ids)) as $target_id => $label) {
+        $matches[] = array(
+          'target_id' => $target_id,
+          'name' => $label,
+        );
+        if (count($matches) == $max_suggestions) {
+          break (2);
+        }
+      }
+    }
+
+    if (count($matches) < $max_suggestions) {
+      $behavior_implementations = synonyms_behavior_get('autocomplete', $field['settings']['target_type'], synonyms_field_target_bundles($field), TRUE);
+      foreach ($behavior_implementations as $implementation) {
+        $condition = db_and();
+        $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like($tag_last) . '%', 'LIKE');
+
+        if (!empty($tags_typed_entity_ids)) {
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $tags_typed_entity_ids, 'NOT IN');
+        }
+        if ($suggest_only_unique && !empty($matches)) {
+          $tmp = array();
+          foreach ($matches as $match) {
+            $tmp[] = $match['target_id'];
+          }
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $tmp, 'NOT IN');
+        }
+
+        $new_target_ids = array();
+        foreach ($implementation['object']->synonymsFind($condition) as $synonym) {
+          if (!$suggest_only_unique || !in_array($synonym->entity_id, $new_target_ids)) {
+            $matches[] = array(
+              'target_id' => $synonym->entity_id,
+              'synonym' => $synonym->synonym,
+              'behavior_implementation' => $implementation,
+            );
+            $new_target_ids[] = $synonym->entity_id;
+            if (count($matches) == $max_suggestions) {
+              break (2);
+            }
+          }
+        }
+      }
+    }
 
-    // Now formatting the results.
-    foreach ($tags_return as $info) {
-      $n = $info['name'];
-      // Term names containing commas or quotes must be wrapped in quotes.
-      if (strpos($info['name'], ',') !== FALSE || strpos($info['name'], '"') !== FALSE) {
-        $n = '"' . str_replace('"', '""', $info['name']) . '"';
+    $synonym_entities = array();
+    foreach ($matches as $match) {
+      if (!isset($match['wording']) && isset($match['synonym'])) {
+        $synonym_entities[] = $match['target_id'];
       }
-      while (isset($term_matches[$prefix . $n])) {
-        $n .= ' ';
+    }
+    if (!empty($synonym_entities)) {
+      $synonym_entities = entity_load($field['settings']['target_type'], $synonym_entities);
+      foreach ($matches as $k => $match) {
+        if (!isset($match['name']) && isset($match['synonym'])) {
+          if (entity_access('view', $field['settings']['target_type'], $synonym_entities[$match['target_id']])) {
+            $matches[$k]['name'] = entity_label($field['settings']['target_type'], $synonym_entities[$match['target_id']]);
+          }
+          else {
+            unset($matches[$k]);
+          }
+        }
       }
-      $term_matches[$prefix . $n] = $info['wording'];
+      $matches = array_values($matches);
     }
   }
-  drupal_json_output($term_matches);
+
+  drupal_json_output(synonyms_autocomplete_format($matches, $prefix));
+}
+
+/**
+ * Supportive function to format autocomplete suggestions.
+ *
+ * @param array $matches
+ *   Array of matched entries. It should follow this structure:
+ *   - name: (string) String to be inserted into autocomplete textfield if user
+ *     chooses this autocomplete entry
+ *   - synonym: (string) If this entry is matched through a synonym, put that
+ *     synonym here
+ *   - behavior_implementation: (array) If this entry is matched through a
+ *     synonym, put here the behavior implementation array that provided this
+ *     match
+ * @param string $prefix
+ *   Any prefix to be appended to 'name' property of $matches array when
+ *   inserting into the autocomplete textfield. Normally it is the already
+ *   entered entries in the textfield
+ *
+ * @return array
+ *   Array of formatted autocomplete response entries ready to be returned to
+ *   the autocomplete JavaScript
+ */
+function synonyms_autocomplete_format($matches, $prefix) {
+  $output = array();
+
+  foreach ($matches as $match) {
+    $n = synonyms_autocomplete_escape($match['name']);
+    while (isset($output[$prefix . $n])) {
+      $n .= ' ';
+    }
+    $wording = check_plain($match['name']);
+    if (isset($match['synonym'])) {
+      $wording = format_string(filter_xss($match['behavior_implementation']['settings']['wording']), array(
+        '@entity' => $match['name'],
+        '@synonym' => $match['synonym'],
+        '@field_name' => drupal_strtolower($match['behavior_implementation']['label']),
+      ));
+    }
+    $output[$prefix . $n] = $wording;
+  }
+  return $output;
 }
 
 /**
@@ -211,7 +373,7 @@ function theme_synonyms_behaviors_settings($variables) {
   $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.'),
+    'empty' => t('Seems like there are no fields for which synonyms functionality available. Try adding a text field to get started.'),
   );
 
   $instance_ids = array();
@@ -238,3 +400,144 @@ function theme_synonyms_behaviors_settings($variables) {
 
   return '<div id="' . $element['#id'] . '">' . theme('table', $table) . drupal_render_children($element) . '</div>';
 }
+
+/**
+ * Page menu callback for managing Synonyms settings of entity types.
+ */
+function synonyms_settings_overview() {
+  $output = array();
+
+  $output['table'] = array(
+    '#theme' => 'table',
+    '#header' => array(t('Entity type'), t('Bundle'), t('Manage')),
+    '#rows' => array(),
+  );
+
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    if (synonyms_entity_type_load($entity_type)) {
+      foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+        $output['table']['#rows'][] = array(
+          $entity_info['label'],
+          $bundle == $entity_type ? '' : $bundle_info['label'],
+          l(t('Edit'), 'admin/structure/synonyms/' . $entity_type . '/' . $bundle),
+        );
+      }
+    }
+  }
+
+  return $output;
+}
+
+/**
+ * Synonyms settings form for a specific entity type and bundle name.
+ *
+ * @param string $entity_type
+ *   Entity type for which to generate synonyms settings form
+ * @param string $bundle
+ *   Bundle name for which to generate synonyms settings form
+ */
+function synonyms_settings_form($form, &$form_state, $entity_type, $bundle) {
+  $form['settings'] = array(
+    '#tree' => TRUE,
+    '#theme' => 'synonyms_behaviors_settings',
+    '#id' => 'synonyms-behaviors-settings-wrapper',
+    '#entity_type' => $entity_type,
+    '#bundle' => $bundle,
+  );
+
+  $behaviors = synonyms_behaviors();
+
+  foreach ($behaviors as $behavior => $behavior_info) {
+    $form['settings'][$behavior] = array(
+      '#title' => $behavior_info['title'],
+    );
+
+    $behavior_implementations = synonyms_behavior_get($behavior, $entity_type, $bundle);
+
+    foreach ($behavior_implementations as $implementation) {
+      $form['settings'][$behavior][$implementation['provider']]['#title'] = $implementation['label'];
+
+      if (isset($form_state['values']['settings'][$behavior][$implementation['provider']])) {
+        $behavior_settings = (bool) $form_state['values']['settings'][$behavior][$implementation['provider']]['enabled'];
+      }
+      else {
+        $behavior_settings = isset($implementation['settings']);
+      }
+
+      if ($behavior_settings) {
+        if (isset($form_state['values']['settings'][$behavior][$implementation['provider']]['settings'])) {
+          $behavior_settings = $form_state['values']['settings'][$behavior][$implementation['provider']]['settings'];
+        }
+        elseif (isset($implementation['settings'])) {
+          $behavior_settings = $implementation['settings'];
+        }
+        else {
+          $behavior_settings = array();
+        }
+      }
+
+      $form['settings'][$behavior][$implementation['provider']]['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['settings'][$behavior][$implementation['provider']]['enabled']['#ajax'] = array(
+          'callback' => 'synonyms_settings_form_ajax',
+          'wrapper' => $form['settings']['#id'],
+        );
+
+        if ($behavior_settings !== FALSE) {
+          $form['settings'][$behavior][$implementation['provider']]['settings'] = $settings_form($form, $form_state, $behavior_settings);
+        }
+      }
+    }
+  }
+
+  $form['actions'] = array(
+    '#type' => '#actions',
+  );
+
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler for 'synonyms_settings_form' form.
+ *
+ * Store synonyms behavior settings.
+ */
+function synonyms_settings_form_submit($form, &$form_state) {
+  foreach ($form_state['values']['settings'] as $behavior => $settings) {
+    foreach ($settings as $provider => $behavior_settings) {
+      $behavior_implementation = array(
+        'entity_type' => $form['settings']['#entity_type'],
+        'bundle' => $form['settings']['#bundle'],
+        'provider' => $provider,
+        'behavior' => $behavior,
+        'settings' => isset($behavior_settings['settings']) ? $behavior_settings['settings'] : NULL,
+      );
+      if ($behavior_settings['enabled']) {
+        synonyms_behavior_implementation_save($behavior_implementation);
+      }
+      else {
+        synonyms_behavior_implementation_delete($behavior_implementation);
+      }
+    }
+  }
+  drupal_set_message(t('Synonyms settings have been successfully saved.'));
+  $form_state['redirect'] = array('admin/structure/synonyms');
+}
+
+/**
+ * Ajax callback function for synonyms settings form.
+ */
+function synonyms_settings_form_ajax($form, &$form_state) {
+  return $form['settings'];
+}

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


+ 66 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/includes/CommerceProductReferenceSynonymsBehavior.class.inc

@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Enables Commerce Product Reference field type for synonyms integration.
+ */
+
+/**
+ * Definition of CommerceProductReferenceSynonymsBehavior class.
+ */
+class CommerceProductReferenceSynonymsBehavior extends AbstractFieldSynonymsBehavior {
+
+  public function extractSynonyms($entity) {
+    $synonyms = array();
+
+    $product_ids = array();
+    foreach ($this->entityItems($entity) as $item) {
+      $product_ids[] = $item['product_id'];
+    }
+    $entities = commerce_product_load_multiple($product_ids);
+    foreach ($entities as $entity) {
+      $synonyms[] = entity_label('commerce_product', $entity);
+    }
+
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($trunk_entity, $synonym_entity, $synonym_entity_type) {
+    if ('commerce_product' != $synonym_entity_type) {
+      return;
+    }
+
+    $items = $this->entityItems($trunk_entity);
+    $synonym_entity_id = entity_extract_ids($synonym_entity_type, $synonym_entity);
+    $items[] = array(
+      'product_id' => $synonym_entity_id[0],
+    );
+    $trunk_entity->{$this->field['field_name']}[LANGUAGE_NONE] = $this->uniqueItems($items, array('product_id'));
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition) {
+    if ($this->field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsBehaviorException(t('Not supported storage engine %type in @method() method.', array(
+        '%type' => $this->field['storage']['type'],
+        '@method' => __METHOD__,
+      )));
+    }
+    $table = array_keys($this->field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $this->field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['product_id'];
+
+    $query = db_select($table, 'field');
+
+    $product_entity_type_info = entity_get_info('commerce_product');
+
+    $product_entity_alias = $query->innerJoin($product_entity_type_info['base table'], 'product', 'field.' . $column . ' = %alias.' . $product_entity_type_info['entity keys']['id']);
+    $query->addField($product_entity_alias, $product_entity_type_info['entity keys']['label'], 'synonym');
+    $query->fields('field', array('entity_id'));
+    $query->condition('field.entity_type', $this->instance['entity_type']);
+    $query->condition('field.bundle', $this->instance['bundle']);
+
+    $this->synonymsFindProcessCondition($condition, $product_entity_alias . '.' . $product_entity_type_info['entity keys']['label'], 'field.entity_id');
+    $query->condition($condition);
+    return $query->execute();
+  }
+}

+ 19 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.info

@@ -0,0 +1,19 @@
+name = Synonyms Commerce
+description = "Provides synonyms integration with Commerce."
+package = Synonyms
+core = 7.x
+dependencies[] = synonyms_provider_field
+dependencies[] = commerce_product_reference
+
+test_dependencies[] = commerce:commerce_product_ui
+
+files[] = synonyms_commerce.test
+
+files[] = includes/CommerceProductReferenceSynonymsBehavior.class.inc
+
+; Information added by Drupal.org packaging script on 2016-05-07
+version = "7.x-1.5"
+core = "7.x"
+project = "synonyms"
+datestamp = "1462586641"
+

+ 247 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.module

@@ -0,0 +1,247 @@
+<?php
+
+/**
+ * @file
+ * Provides synonyms integration with Commerce.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function synonyms_commerce_menu() {
+  $items = array();
+
+  $items['synonyms-commerce/autocomplete'] = array(
+    'title' => 'Synonyms-friendly product autocomplete widget',
+    'page callback' => 'synonyms_commerce_autocomplete',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+    'file' => 'synonyms_commerce.pages.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function synonyms_commerce_field_widget_info() {
+  return array(
+    'synonyms_commerce_autocomplete' => array(
+      'label' => t('Synonyms friendly autocomplete'),
+      'field types' => array('commerce_product_reference'),
+      'settings' => array(
+        'size' => 60,
+        'synonyms_autocomplete_path' => 'synonyms-commerce/autocomplete',
+        'suggestion_size' => 10,
+        'suggest_only_unique' => FALSE,
+      ),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+      ),
+    ),
+    'synonyms_commerce_select' => array(
+      'label' => t('Synonyms friendly select list'),
+      'field types' => array('commerce_product_reference'),
+      'settings' => array(),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function synonyms_commerce_field_widget_settings_form($field, $instance) {
+  $widget = $instance['widget'];
+  $settings = $widget['settings'] + field_info_widget_settings($widget['type']);
+
+  $form = array();
+
+  switch  ($widget['type']) {
+    case 'synonyms_commerce_autocomplete':
+      $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 product'),
+        '#description' => t('If you want to include only product name or a single synonym, suggesting a particular product, while disregarding all ongoing ones, please, tick this checkbox on.'),
+        '#default_value' => $settings['suggest_only_unique'],
+      );
+      break;
+  }
+
+  return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function synonyms_commerce_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+  $default_value = synonyms_select_default_value($field, $instance, $items);
+  switch ($instance['widget']['type']) {
+    case 'synonyms_commerce_autocomplete':
+      $default_value_string = array();
+      foreach (commerce_product_load_multiple($default_value) as $product) {
+        $default_value_string[] = entity_label('commerce_product', $product);
+      }
+      $default_value_string = drupal_implode_tags($default_value_string);
+
+      $element += array(
+        '#type' => 'textfield',
+        '#default_value' => $default_value_string,
+        '#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('synonyms_commerce_autocomplete_validate'),
+        '#attached' => array(
+          'js' => array(
+            drupal_get_path('module', 'synonyms') . '/js/synonyms-autocomplete.js' => array(),
+          ),
+        ),
+        '#attributes' => array(
+          'class' => array('synonyms-autocomplete'),
+        ),
+      );
+      if (isset($instance['widget']['settings']['auto_creation'])) {
+        $element['#auto_creation'] = $instance['widget']['settings']['auto_creation'];
+      }
+      break;
+
+    case 'synonyms_commerce_select':
+      $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
+
+      $options = module_invoke('commerce_product_reference', 'options_list', $field, $instance);
+      $tmp = reset($options);
+      if (is_array($tmp)) {
+        // These options have optgroups.
+        foreach ($options as $k => $v) {
+          $options[$k] = synonyms_commerce_product_options_expand($v);
+        }
+      }
+      else {
+        $options = synonyms_commerce_product_options_expand($options);
+      }
+
+      $element += array(
+        '#type' => 'select',
+        '#empty_option' => t('- None -'),
+        '#options' => $options,
+        '#multiple' => $multiple,
+        '#element_validate' => array('synonyms_select_validate', 'synonyms_select_form_to_storage'),
+        '#default_value' => $default_value,
+      );
+      break;
+  }
+  return $element;
+}
+
+/**
+ * Implements hook_synonyms_provider_field_behavior_implementation_info().
+ */
+function synonyms_commerce_synonyms_provider_field_behavior_implementation_info($behavior) {
+  switch ($behavior) {
+    case 'select':
+    case 'autocomplete':
+      return array(
+        'commerce_product_reference' => 'CommerceProductReferenceSynonymsBehavior',
+      );
+      break;
+  }
+  return array();
+}
+
+/**
+ * Expand the options for commerce product select widget with synonyms.
+ *
+ * @param array $options
+ *   Array of commerce product reference widget options that should be expanded
+ *   with synonyms
+ *
+ * @return array
+ *   Expanded with synonyms version of the provided $options
+ */
+function synonyms_commerce_product_options_expand($options) {
+  $synonyms_options = array();
+
+  $behavior_implementations = array();
+
+  foreach (commerce_product_load_multiple(array_keys($options)) as $product) {
+    if (!isset($behavior_implementations[$product->type])) {
+      $behavior_implementations[$product->type] = synonyms_behavior_get('select', 'commerce_product', $product->type, TRUE);
+    }
+    $synonyms_options[] = synonyms_select_option_entity($product, 'commerce_product');
+
+    foreach ($behavior_implementations[$product->type] as $behavior_implementation) {
+      foreach ($behavior_implementation['object']->extractSynonyms($product) as $synonym) {
+        $synonyms_options[] = synonyms_select_option_entity($product, $behavior_implementation['entity_type'], $synonym, $behavior_implementation);
+      }
+    }
+  }
+  usort($synonyms_options , 'synonyms_select_sort_name');
+  return $synonyms_options;
+}
+
+/**
+ * Element validate for commerce product synonyms friendly autocomplete widget.
+ */
+function synonyms_commerce_autocomplete_validate($element, &$form_state) {
+  $input = drupal_map_assoc(drupal_explode_tags(drupal_strtolower($element['#value'])));
+
+  $value = array();
+
+  $field = field_info_field($element['#field_name']);
+  $instance = field_info_instance($element['#entity_type'], $field['field_name'], $element['#bundle']);
+
+  if (!empty($input)) {
+    $target_bundles = synonyms_bundle_normalize('commerce_product', array_filter($instance['settings']['referenceable_types']));
+
+    $efq = new EntityFieldQuery();
+    $efq->entityCondition('entity_type', 'commerce_product');
+    $efq->entityCondition('bundle', $target_bundles);
+    $efq->propertyCondition('title', $input, 'IN');
+    $result = $efq->execute();
+    if (isset($result['commerce_product'])) {
+      foreach (commerce_product_load_multiple(array_keys($result['commerce_product'])) as $product) {
+        $label = drupal_strtolower(entity_label('commerce_product', $product));
+        unset($input[$label]);
+        $entity_id = entity_extract_ids('commerce_product', $product);
+        $value[] = $entity_id[0];
+      }
+    }
+
+    if (!empty($input)) {
+      $behavior_implementations = synonyms_behavior_get('autocomplete', 'commerce_product', $target_bundles, TRUE);
+      foreach ($behavior_implementations as $implementation) {
+        $condition = db_and();
+        $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $input, 'IN');
+        foreach ($implementation['object']->synonymsFind($condition) as $synonym) {
+          $value[] = $synonym->entity_id;
+          unset($input[drupal_strtolower($synonym->synonym)]);
+          if (empty($input)) {
+            break(2);
+          }
+        }
+      }
+    }
+  }
+
+  $tmp = array_unique($value);
+  $value = array();
+  $column = array_keys($field['columns']);
+  $column = reset($column);
+  foreach ($tmp as $target_id) {
+    $value[] = array($column => $target_id);
+  }
+
+  form_set_value($element, $value, $form_state);
+}

+ 128 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.pages.inc

@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Menu page callbacks of the module.
+ */
+
+/**
+ * Menu page callback for synonyms commerce autocomplete widget.
+ */
+function synonyms_commerce_autocomplete($field_name, $entity_type, $bundle) {
+  // If the request has a '/' in the search text, then the menu system will have
+  // split it into multiple arguments, recover the intended $tags_typed.
+  $args = func_get_args();
+  // Shift off the $field_name argument.
+  array_shift($args);
+  // Shift off the $entity_type argument.
+  array_shift($args);
+  // Shift off the $bundle argument.
+  array_shift($args);
+  $tags_typed = implode('/', $args);
+
+  if (!($field = field_info_field($field_name)) || $field['type'] != 'commerce_product_reference') {
+    print t('Commerce product reference field @field_name not found.', array('@field_name' => $field_name));
+    exit;
+  }
+
+  if (!($instance = field_info_instance($entity_type, $field['field_name'], $bundle))) {
+    // Error string. The JavaScript handler will realize this is not JSON and
+    // will display it as debugging information.
+    print t('There was not found an instance of @field_name in @entity_type.', array(
+      '@field_name' => $field_name,
+      '@entity_type' => $entity_type,
+    ));
+    exit;
+  }
+
+  module_load_include('inc', 'synonyms', 'synonyms.pages');
+
+  $widget = $instance['widget']['type'] == 'synonyms_commerce_autocomplete' ? $instance['widget']['settings'] : field_info_widget_settings('synonyms_commerce_autocomplete');
+
+  // How many suggestions maximum we are able to output.
+  $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 = $widget['suggest_only_unique'];
+
+  $tags_typed = drupal_explode_tags($tags_typed);
+  $tag_last = drupal_strtolower(array_pop($tags_typed));
+  $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
+
+  $matches = array();
+
+  if ($tag_last) {
+    $tags_typed_entity_ids = array();
+
+    if (!empty($tags_typed)) {
+      foreach (commerce_product_load_multiple(array(), array('title' => $tags_typed)) as $product) {
+        $product_ids = entity_extract_ids('commerce_product', $product);
+        $tags_typed_entity_ids[] = $product_ids[0];
+      }
+    }
+
+    $new_target_ids = array();
+
+    foreach (commerce_product_match_products($field, $instance, $tag_last, 'contains', array(), $max_suggestions) as $product) {
+      $product = commerce_product_load_by_sku($product['sku']);
+      $product_id = entity_extract_ids('commerce_product', $product);
+      $product_id = $product_id[0];
+      if (!in_array($product_id, $tags_typed_entity_ids)) {
+        $matches[] = array(
+          'name' => entity_label('commerce_product', $product),
+        );
+        $new_target_ids[] = $product_id;
+      }
+    }
+
+    if (count($matches) < $max_suggestions) {
+      $target_bundles = synonyms_bundle_normalize('commerce_product', array_filter($instance['settings']['referenceable_types']));
+
+      $behavior_implementations = synonyms_behavior_get('autocomplete', 'commerce_product', $target_bundles, TRUE);
+      foreach ($behavior_implementations as $implementation) {
+        $condition = db_and();
+        $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like($tag_last) . '%', 'LIKE');
+
+        if (!empty($tags_typed_entity_ids)) {
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $tags_typed_entity_ids, 'NOT IN');
+        }
+        if ($suggest_only_unique && !empty($new_target_ids)) {
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_ENTITY_ID_PLACEHOLDER, $new_target_ids, 'NOT IN');
+        }
+
+        foreach ($implementation['object']->synonymsFind($condition) as $synonym) {
+          if (!$suggest_only_unique || !in_array($synonym->entity_id, $new_target_ids)) {
+            $matches[] = array(
+              'target_id' => $synonym->entity_id,
+              'synonym' => $synonym->synonym,
+              'behavior_implementation' => $implementation,
+            );
+            $new_target_ids[] = $synonym->entity_id;
+            if (count($matches) == $max_suggestions) {
+              break (2);
+            }
+          }
+        }
+      }
+    }
+
+    $synonym_entities = array();
+    foreach ($matches as $match) {
+      if (!isset($match['wording']) && isset($match['synonym'])) {
+        $synonym_entities[] = $match['target_id'];
+      }
+    }
+    if (!empty($synonym_entities)) {
+      $synonym_entities = commerce_product_load_multiple($synonym_entities);
+      foreach ($matches as $k => $match) {
+        if (!isset($match['name']) && isset($match['synonym'])) {
+          $matches[$k]['name'] = entity_label('commerce_product', $synonym_entities[$match['target_id']]);
+        }
+      }
+      $matches = array_values($matches);
+    }
+  }
+
+  drupal_json_output(synonyms_autocomplete_format($matches, $prefix));
+}

+ 849 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_commerce/synonyms_commerce.test

@@ -0,0 +1,849 @@
+<?php
+
+/**
+ * @file
+ * Tests for the Synonyms Commerce module.
+ */
+
+/**
+ * Test synonyms autocomplete widget for commerce product reference field type.
+ */
+class CommerceProductReferenceAutocompleteSynonymsWebTestCase extends AbstractAutocompleteSynonymsWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Commerce product reference synonyms autocomplete',
+      'description' => 'Ensure that the "synonym friendly autocomplete" widget works correctly with commerce product reference field type.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  public function setUp($modules = array()) {
+    $modules[] = 'commerce_product_reference';
+    $modules[] = 'commerce_product_ui';
+    $modules[] = 'synonyms_commerce';
+
+    $this->fields['enabled']['instance']['entity_type'] = 'commerce_product';
+    $this->fields['enabled']['instance']['bundle'] = 'product';
+    $this->fields['disabled']['instance']['entity_type'] = 'commerce_product';
+    $this->fields['disabled']['instance']['bundle'] = 'product';
+
+    $this->behavior_implementation['entity_type'] = $this->fields['enabled']['instance']['entity_type'];
+
+    parent::setUp($modules);
+
+    $this->reference_field = array(
+      'type' => 'commerce_product_reference',
+      'field_name' => 'synonyms_commerce',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'settings' => array(),
+    );
+    $this->reference_field = field_create_field($this->reference_field);
+
+    $this->reference_instance = array(
+      'field_name' => $this->reference_field['field_name'],
+      'entity_type' => $this->entity_type,
+      'bundle' => $this->bundle,
+      'label' => 'Synonym Commerce Autocomplete',
+      'settings' => array(
+        'referenceable_types' => drupal_map_assoc(array('product')),
+      ),
+      'widget' => array(
+        'type' => 'synonyms_commerce_autocomplete',
+      ),
+    );
+    $this->reference_instance = field_create_instance($this->reference_instance);
+    $this->reference_instance = field_info_instance($this->reference_instance['entity_type'], $this->reference_instance['field_name'], $this->reference_instance['bundle']);
+  }
+
+  protected function createTerms() {
+    $name = $this->randomName();
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $name,
+      'sku' => 'a' . drupal_strtolower($this->randomName()),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['term1'] = $product;
+
+    $name .= $this->randomName();
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $name,
+      'sku' => 'z' . drupal_strtolower($this->randomName()),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['term1_longer_name'] = $product;
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['no_synonyms'] = $product;
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($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()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['one_synonym'] = $product;
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($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()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['two_synonyms'] = $product;
+
+    $name = $this->randomName();
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $name,
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $name . $this->randomName()),
+        ),
+      ),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['name_similar_synonym'] = $product;
+
+    $name = 'similar_synonyms_';
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $name . $this->randomName()),
+          array('value' => $name . $this->randomName()),
+        ),
+      ),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['similar_synonyms'] = $product;
+
+    $name = 'one_term_name_another_synonym_';
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $name . $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['name_another_synonym'] = $product;
+
+    $product = (object) array(
+      'type' => 'product',
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $name . $this->randomName()),
+        ),
+      ),
+      $this->fields['disabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($product);
+    $this->terms['synonym_another_name'] = $product;
+
+    $another_product_type = commerce_product_ui_product_type_new();
+    $another_product_type['type'] = 'another_vocabulary';
+    $another_product_type['name'] = $this->randomName();
+    commerce_product_ui_product_type_save($another_product_type);
+
+    $product_similar_product = (object) array(
+      'type' => $another_product_type['type'],
+      'title' => $this->entityLabel($this->terms['no_synonyms']),
+      'sku' => drupal_strtolower($this->randomName()),
+    );
+    commerce_product_save($product_similar_product);
+
+    $product_similar_synonym = (object) array(
+      'type' => $another_product_type['type'],
+      'title' => $this->terms['one_synonym']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+      'sku' => drupal_strtolower($this->randomName()),
+    );
+    commerce_product_save($product_similar_synonym);
+
+    $synonym_similar_product = (object) array(
+      'type' => $another_product_type['type'],
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(LANGUAGE_NONE => array(
+        $this->entityLabel($this->terms['no_synonyms']),
+      )),
+    );
+    commerce_product_save($synonym_similar_product);
+
+    $synonym_similar_synonym = (object) array(
+      'type' => $another_product_type['type'],
+      'title' => $this->randomName(),
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(LANGUAGE_NONE => array(
+        $this->terms['one_synonym']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+      )),
+    );
+    commerce_product_save($synonym_similar_synonym);
+  }
+}
+
+
+/**
+ * Test synonyms-friendly select widget for commerce product reference field.
+ */
+class CommerceProductReferenceSelectSynonymsWebTestCase extends AbstractSelectSynonymsWebTestCase {
+
+  /**
+   * Array of products on which the testing is held.
+   *
+   * @var array
+   */
+  protected $products = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Commerce product reference synonyms select',
+      'description' => 'Ensure that the "synonym friendly select" widget works correctly with commerce product reference field.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  public function setUp($modules = array()) {
+    $modules[] = 'commerce_product_reference';
+    $modules[] = 'synonyms_commerce';
+
+    $this->fields['enabled']['instance']['entity_type'] = 'commerce_product';
+    $this->fields['enabled']['instance']['bundle'] = 'product';
+    $this->fields['disabled']['instance']['entity_type'] = 'commerce_product';
+    $this->fields['disabled']['instance']['bundle'] = 'product';
+
+    $this->behavior_implementation['entity_type'] = $this->fields['enabled']['instance']['entity_type'];
+
+    parent::setUp($modules);
+
+    $this->reference_field = array(
+      'type' => 'commerce_product_reference',
+      'field_name' => 'synonyms_commerce',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'settings' => array(),
+    );
+    $this->reference_field = field_create_field($this->reference_field);
+
+    $this->reference_instance = array(
+      'field_name' => $this->reference_field['field_name'],
+      'entity_type' => $this->entity_type,
+      'bundle' => $this->bundle,
+      'label' => 'Synonym Commerce Select',
+      'widget' => array(
+        'type' => 'synonyms_commerce_select',
+      ),
+    );
+    $this->reference_instance = field_create_instance($this->reference_instance);
+  }
+
+  public function testWidgetSorting() {
+    $cardinality = array(
+      1 => 1,
+      FIELD_CARDINALITY_UNLIMITED => 'unlimited',
+    );
+    $required = array(
+      TRUE => 'required',
+      FALSE => 'not required',
+    );
+
+    foreach ($cardinality as $cardinality_k => $cardinality_v) {
+      foreach ($required as $required_k => $required_v) {
+        $this->reference_field['cardinality'] = $cardinality_k;
+        field_update_field($this->reference_field);
+
+        $this->reference_instance['required'] = $required_k;
+        field_update_instance($this->reference_instance);
+
+        $options = array();
+        $options[] = array(
+          'entity' => $this->terms['product3'],
+          'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product3'],
+          'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product1'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product1'],
+          'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product1'],
+          'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product2'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product2'],
+          'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product2'],
+          'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+        );
+        $options[] = array(
+          'entity' => $this->terms['product3'],
+        );
+
+        $this->drupalGet('node/add/synonyms-test-content');
+        $this->assertSynonymsSelect($options, 'Synonyms select sorting by name works for the cardinality of ' . $cardinality_v . ' and ' . $required_v);
+      }
+    }
+  }
+
+  public function testWidget() {
+    $name = $this->randomName();
+    $this->drupalPost('node/add/synonyms-test-content', array(
+      'title' => $name,
+      $this->reference_field['field_name'] . '[' . LANGUAGE_NONE . '][]' => array(
+        $this->synonymSelectKey($this->terms['product1'], $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+        $this->terms['product2']->product_id,
+        $this->terms['product3']->product_id,
+        $this->synonymSelectKey($this->terms['product3'], $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+      ),
+    ), 'Save');
+
+    $node = $this->drupalGetNodeByTitle($name);
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertText($this->terms['product1']->title, 'Product is saved when its synonym is submitted through synonyms friendly select for the unlimited cardinality.');
+    $this->assertText($this->terms['product2']->title, 'Product is saved when it is submitted through synonyms friendly select for the unlimited cardinality.');
+    $this->assertText($this->terms['product3']->title, 'Product is saved only once when the term and its synonym are submitted through synonyms friendly select for the unlimited cardinality.');
+
+    $options = array();
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'selected' => TRUE,
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'selected' => TRUE,
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'selected' => TRUE,
+    );
+    $this->drupalGet('node/' . $node->nid . '/edit');
+    $this->assertSynonymsSelect($options, 'Default values are set correctly in the synonyms friendly select widget when working with field cardinality more than 1.');
+
+    $this->reference_field['cardinality'] = 2;
+    field_update_field($this->reference_field);
+    $name = $this->randomName();
+    $this->drupalPost('node/add/synonyms-test-content', array(
+      'title' => $name,
+      $this->reference_field['field_name'] . '[' . LANGUAGE_NONE . '][]' => array(
+        $this->synonymSelectKey($this->terms['product1'], $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+        $this->terms['product2']->product_id,
+        $this->terms['product3']->product_id,
+      ),
+    ), 'Save');
+    $this->assertText('this field cannot hold more than 2 values.', 'Submitting 3 entries into a field with cardinality of 2, that refer to 3 products, results in a form error.');
+
+    $this->drupalPost('node/add/synonyms-test-content', array(
+      'title' => $name,
+      $this->reference_field['field_name'] . '[' . LANGUAGE_NONE . '][]' => array(
+        $this->synonymSelectKey($this->terms['product1'], $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+        $this->terms['product2']->product_id,
+        $this->synonymSelectKey($this->terms['product2'], $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+      ),
+    ), 'Save');
+    $node = $this->drupalGetNodeByTitle($name);
+    $this->drupalGet('node/' .  $node->nid);
+    $this->assertText($this->terms['product1']->title, 'Submitting 3 entries into a field with cardinality of 2, that refer to only 2 products, results in form getting submitted. Product #1 is saved.');
+    $this->assertText($this->terms['product2']->title, 'Product #2 is saved.');
+
+    $this->reference_field['cardinality'] = 1;
+    field_update_field($this->reference_field);
+    // We need to invoke this one. Without it some caching within Entity
+    // metadata wrappers interferes. See https://www.drupal.org/node/2717019 for
+    // details.
+    entity_flush_caches();
+
+    $name = $this->randomName();
+    $this->drupalPost('node/add/synonyms-test-content', array(
+      'title' => $name,
+      $this->reference_field['field_name'] . '[' . LANGUAGE_NONE . ']' => $this->synonymSelectKey($this->terms['product1'], $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value']),
+    ), 'Save');
+    $node = $this->drupalGetNodeByTitle($name);
+
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertText($this->terms['product1']->title, 'Product is saved when its synonym is submitted through synonyms friendly select for the cardinality of 1.');
+
+    $options = array();
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'selected' => TRUE,
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+    );
+    $this->drupalGet('node/' .  $node->nid . '/edit');
+    $this->assertSynonymsSelect($options, 'Default values are set correctly in the synonyms friendly select widget when working with the field cardinality of 1.');
+
+    $this->drupalPost('node/' . $node->nid . '/edit', array(
+      $this->reference_field['field_name'] . '[' . LANGUAGE_NONE . ']' => $this->terms['product2']->product_id,
+    ), 'Save');
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertNoText($this->terms['product1']->title, 'After updating entity the old product is removed.');
+    $this->assertText($this->terms['product2']->title, 'Product is saved when it is submitted through synonyms friendly select for the cardinality of 1.');
+
+    $options = array();
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+      'synonym' => $this->terms['product3']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product1'],
+      'synonym' => $this->terms['product1']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'selected' => TRUE,
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][1]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product2'],
+      'synonym' => $this->terms['product2']->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE][0]['value'],
+    );
+    $options[] = array(
+      'entity' => $this->terms['product3'],
+    );
+    $this->drupalGet('node/' .  $node->nid . '/edit');
+    $this->assertSynonymsSelect($options, 'Default values are set correctly in the synonyms friendly select widget when working with the field cardinality of 1.');
+  }
+
+  protected function createTerms() {
+    $this->terms['product1'] = (object) array(
+      'type' => $this->behavior_implementation['bundle'],
+      'title' => 'Product 1',
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => 'Product 1A' . $this->randomName()),
+          array('value' => 'Product 1Z' . $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($this->terms['product1']);
+
+    $this->terms['product2'] = (object) array(
+      'type' => $this->behavior_implementation['bundle'],
+      'title' => 'Product 2',
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => 'Product 2Z' . $this->randomName()),
+          array('value' => 'Product 2A' . $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($this->terms['product2']);
+
+    $this->terms['product3'] = (object) array(
+      'type' => $this->behavior_implementation['bundle'],
+      'title' => 'Product 3',
+      'sku' => drupal_strtolower($this->randomName()),
+      $this->fields['enabled']['field']['field_name'] => array(
+        LANGUAGE_NONE => array(
+          array('value' => 'Another Product 3A' . $this->randomName()),
+          array('value' => 'Another Product 3Z' . $this->randomName()),
+        ),
+      ),
+    );
+    commerce_product_save($this->terms['product3']);
+  }
+}
+
+/**
+ * Test CommerceProductReferenceSynonymsBehavior class.
+ */
+class CommerceProductReferenceSynonymsBehaviorWebTestCase extends AbstractSynonymsProviderFieldWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'CommerceProductReferenceSynonymsBehavior',
+      'description' => 'Ensure that the synonyms module extracts synonyms from commerce product reference fields correctly.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    $modules[] = 'commerce_product_reference';
+    $modules[] = 'commerce_product_ui';
+    $modules[] = 'synonyms_commerce';
+
+    $this->fields['enabled']['field'] = array(
+      'field_name' => 'reference',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'type' => 'commerce_product_reference',
+    );
+
+    parent::setUp($modules);
+  }
+
+  /**
+   * Test synonyms extraction for 'commerce_product_reference' field type.
+   */
+  public function testCommerceProductReference() {
+    // Testing synonymsExtract().
+    $this->assertSynonymsExtract(array(), array(), 'on empty field.');
+
+    $synonym_entity = $this->createProduct();
+    $this->assertSynonymsExtract(array(
+      LANGUAGE_NONE => array(
+        0 => array(
+          'product_id' => entity_id('commerce_product', $synonym_entity),
+        ),
+      ),
+    ), array(entity_label('commerce_product', $synonym_entity)), 'on a field that holds one value.');
+
+    // Testing mergeEntityAsSynonym() method.
+    $product = $this->createProduct();
+    $this->assertMergeEntityAsSynonym(array(), $product, 'commerce_product', array(array('product_id' => $product->product_id)), 'on a product.');
+
+    // Testing synonymFind() method.
+    $this->assertSynonymsFind(array(), db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on empty field');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field without values');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field with a value but searching for another string');
+
+    $meta_data = array();
+    $synonym_entity = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity)),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('commerce_product', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity)), 'on a field with a single value searching for that string');
+
+    $meta_data = array();
+    $synonym_entity = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity)),
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('commerce_product', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity)), 'on a field with 2 values searching for one of those 2 values');
+
+    $meta_data = array();
+    $synonym_entity = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity)),
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('commerce_product', $synonym_entity)),
+    );
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity)), 'on 2 fields with 2 values each searching for one of those values');
+
+    $meta_data = array();
+    $synonym_entity = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity)),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('commerce_product', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('commerce_product', $synonym_entity), 1, -1)) . '%', 'LIKE'), 'on a field with a value searching for a string LIKE the %value%');
+
+    $meta_data = array();
+    $tag = $this->randomName();
+    $synonym_entity1 = $this->createProduct($tag . $this->randomName());
+    $synonym_entity2 = $this->createProduct($tag . $this->randomName());
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity1)),
+          array('product_id' => entity_id('commerce_product', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('commerce_product', $synonym_entity1),
+        entity_label('commerce_product', $synonym_entity2),
+      ),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like($tag) . '%', 'LIKE'), 'on a field with 2 similar values searching a string like %both values%');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createProduct();
+    $synonym_entity2 = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity1)),
+          array('product_id' => entity_id('commerce_product', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('commerce_product', $synonym_entity1),
+        entity_label('commerce_product', $synonym_entity2),
+      ),
+    );
+    $condition = db_or()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity1))
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity2));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for value1 or value2');
+
+    $meta_data = array();
+    $synonym_entity = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity)),
+          array('product_id' => entity_id('commerce_product', $this->createProduct())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('commerce_product', $synonym_entity)),
+    );
+    $condition = db_and()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity))
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('commerce_product', $synonym_entity), 1, -1)) . '%', 'LIKE');
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for value1 and LIKE value1%');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createProduct();
+    $synonym_entity2 = $this->createProduct();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity1)),
+          array('product_id' => entity_id('commerce_product', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('commerce_product', $synonym_entity1),
+        entity_label('commerce_product', $synonym_entity2),
+      ),
+    );
+    $condition = db_or()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity1))
+      ->condition(db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('commerce_product', $synonym_entity2))->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('commerce_product', $synonym_entity2), 1, -1)) . '%', 'LIKE'));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for (value1 or (value2 AND value2%))');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createProduct($this->randomName() . ' ' . $this->randomName() . ' ' . $this->randomName());
+    $synonym_entity2 = $this->createProduct(str_replace(' ', '-', entity_label('commerce_product', $synonym_entity1)));
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('product_id' => entity_id('commerce_product', $synonym_entity1)),
+          array('product_id' => entity_id('commerce_product', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('commerce_product', $synonym_entity1),
+        entity_label('commerce_product', $synonym_entity2),
+      ),
+    );
+    $condition = db_and()
+      ->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :synonym", array(
+        ':synonym' => entity_label('commerce_product', $synonym_entity2),
+      ));
+    $this->assertSynonymsFind($meta_data, $condition, "on a field with 2 values, where 2nd value replaces spaces with dashes in the 1st value, searching for REPLACE(column, ' ', '-') = value2");
+  }
+
+  /**
+   * Supportive function.
+   *
+   * Create an entity of necessary entity type (in our test it's commerce
+   * product).
+   *
+   * @param string $label
+   *   Label to use for the entity that is about to be created
+   * @param string $bundle
+   *   Bundle to use for the entity that is about to be created
+   *
+   * @return object
+   *   Fully loaded entity object of the just created entity
+   */
+  protected function createProduct($label = NULL, $bundle = 'product') {
+    if (is_null($label)) {
+      $label = $this->randomName();
+    }
+    $entity = commerce_product_new($bundle);
+    $entity->title = $label;
+    $entity->sku = drupal_html_id($label);
+    commerce_product_save($entity);
+    return $entity;
+  }
+}

+ 81 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/AbstractFieldSynonymsBehavior.class.inc

@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Definition of AbstractFieldSynonymsBehavior class.
+ */
+
+/**
+ * Abstract class for providing synonyms from fields attached to entities.
+ */
+abstract class AbstractFieldSynonymsBehavior extends AbstractSynonymsBehavior {
+
+  /**
+   * Field definition array on which this provider was initialized.
+   *
+   * @var array
+   */
+  protected $field;
+
+  /**
+   * Field instance definition on which this provider was initialized.
+   *
+   * @var array
+   */
+  protected $instance;
+
+  public function __construct($behavior_implementation) {
+    parent::__construct($behavior_implementation);
+
+    $this->field = field_info_field(synonyms_provider_field_field_name($behavior_implementation['provider']));
+    $this->instance = field_info_instance($behavior_implementation['entity_type'], $this->field['field_name'], $behavior_implementation['bundle']);
+  }
+
+  public function featuresExportPipe() {
+    $pipe = parent::featuresExportPipe();
+
+    // We include the Features component for the underlying field on which this
+    // synonyms provider is built.
+    $pipe['field'][] = implode('-', array($this->behavior_implementation['entity_type'], $this->behavior_implementation['bundle'], $this->field['field_name']));
+    return $pipe;
+  }
+
+  /**
+   * Retrieve items of the underlying field in this behavior implementation.
+   *
+   * @param object $entity
+   *   Entity whose items should be retrieved
+   *
+   * @return array
+   *   Array of items that provided entity has in the field on which behavior
+   *   implementation is set up
+   */
+  protected function entityItems($entity) {
+    $items = field_get_items($this->instance['entity_type'], $entity, $this->field['field_name']);
+    return is_array($items) ? $items : array();
+  }
+
+  /**
+   * Filter $items only to contain unique values.
+   *
+   * @param array $items
+   *   Array of field items that should be filtered to contain only unique
+   *   values
+   * @param array $unique_index
+   *   Array of column names that define uniqueness for an item
+   *
+   * @return array
+   *   Only unique items from the provided $items array
+   */
+  protected function uniqueItems($items, $unique_index) {
+    $index = array();
+    foreach ($items as $item) {
+      $item_index = array();
+      foreach ($unique_index as $column) {
+        $item_index[] = is_scalar($column) ? $column : serialize($column);
+      }
+      $index[serialize($item_index)] = $item;
+    }
+    return array_values($index);
+  }
+}

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

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

+ 23 - 22
sites/all/modules/contrib/taxonomy/synonyms/includes/TaxonomySynonymsBehavior.class.inc → sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/includes/TaxonomySynonymsBehavior.class.inc

@@ -8,13 +8,13 @@
 /**
  * Definition of TaxonomySynonymsBehavior class.
  */
-class TaxonomySynonymsBehavior extends AbstractSynonymsSynonymsBehavior implements SynonymsSynonymsBehavior, AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+class TaxonomySynonymsBehavior extends AbstractFieldSynonymsBehavior implements AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
 
-  public function extractSynonyms($items, $field, $instance, $entity, $entity_type) {
+  public function extractSynonyms($entity) {
     $synonyms = array();
 
     $terms = array();
-    foreach ($items as $item) {
+    foreach ($this->entityItems($entity) as $item) {
       $terms[] = $item['tid'];
     }
     $terms = taxonomy_term_load_multiple($terms);
@@ -24,16 +24,19 @@ class TaxonomySynonymsBehavior extends AbstractSynonymsSynonymsBehavior implemen
     return $synonyms;
   }
 
-  public function mergeEntityAsSynonym($items, $field, $instance, $synonym_entity, $synonym_entity_type) {
+  public function mergeEntityAsSynonym($trunk_entity, $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();
+      return;
     }
+
+    $items = $this->entityItems($trunk_entity);
+
     // Checking that $field is configured to reference the vocabulary of
     // $synonym_entity term.
     $is_allowed = FALSE;
-    foreach ($field['settings']['allowed_values'] as $setting) {
+    foreach ($this->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.
@@ -53,35 +56,33 @@ class TaxonomySynonymsBehavior extends AbstractSynonymsSynonymsBehavior implemen
     if (!$is_allowed) {
       // Synonym term is from a vocabulary that is not expected by this field,
       // or under unexpected parent.
-      return array();
+      return;
     }
-    return array(array(
+    $items[] = array(
       'tid' => $synonym_entity->tid,
-    ));
-  }
-
-  public function synonymItemHash($item, $field, $instance) {
-    return $item['tid'];
+    );
+    $trunk_entity->{$this->field['field_name']}[LANGUAGE_NONE] = $this->uniqueItems($items, array('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'],
+  public function synonymsFind(QueryConditionInterface $condition) {
+    if ($this->field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsBehaviorException(t('Not supported storage engine %type in @method() method.', array(
+        '%type' => $this->field['storage']['type'],
+        '@method' => __METHOD__,
       )));
     }
-    $table = array_keys($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = array_keys($this->field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
     $table = reset($table);
-    $column = $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['tid'];
+    $column = $this->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']);
+    $query->condition('field.entity_type', $this->instance['entity_type']);
+    $query->condition('field.bundle', $this->instance['bundle']);
 
-    $this->synonymsFindProcessCondition($condition, $term_alias . '.name');
+    $this->synonymsFindProcessCondition($condition, $term_alias . '.name', 'field.entity_id');
     $query->condition($condition);
     return $query->execute();
   }

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

@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Enables text and number field types to be source of synonyms.
+ */
+
+/**
+ * Definition of TextSynonymsBehavior class.
+ */
+class TextSynonymsBehavior extends AbstractFieldSynonymsBehavior implements AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+
+  public function extractSynonyms($entity) {
+    $synonyms = array();
+
+    foreach ($this->entityItems($entity) as $item) {
+      $synonyms[] = $item['value'];
+    }
+
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($trunk_entity, $synonym_entity, $synonym_entity_type) {
+    $synonym = entity_label($synonym_entity_type, $synonym_entity);
+
+    switch ($this->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;
+        }
+        break;
+
+    }
+    $items = $this->entityItems($trunk_entity);
+    $items[] = array(
+      'value' => $synonym,
+    );
+    $trunk_entity->{$this->field['field_name']}[LANGUAGE_NONE] = $this->uniqueItems($items, array('value'));
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition) {
+    if ($this->field['storage']['type'] != 'field_sql_storage') {
+      throw new SynonymsBehaviorException(t('Not supported storage engine %type in @method() method.', array(
+        '%type' => $this->field['storage']['type'],
+        '@method' => __METHOD__,
+      )));
+    }
+    $table = array_keys($this->field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
+    $table = reset($table);
+    $column = $this->field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['value'];
+
+    $this->synonymsFindProcessCondition($condition, $column, 'entity_id');
+
+    $query = db_select($table);
+    $query->fields($table, array('entity_id'));
+    $query->addField($table, $column, 'synonym');
+    return $query->condition($condition)
+      ->condition('entity_type', $this->instance['entity_type'])
+      ->condition('bundle', $this->instance['bundle'])
+      ->execute();
+  }
+
+}

+ 72 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.api.php

@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Documentation for Synonyms Provider Field module.
+ */
+
+/**
+ * Collect info about available field-based synonym behavior implementations.
+ *
+ * Hook to collect info about what PHP classes implement provided synonyms
+ * behavior for different field types. If you to create synonyms behavior
+ * implementation backed by some field type, this hook is for you. Information
+ * from this hook will be post-processed based on existing fields and instances
+ * and then inserted into hook_synonyms_behavior_implementation_info().
+ *
+ * @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 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_field_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();
+}
+
+/**
+ * Alter info about available field-based synonyms behavior implementations.
+ *
+ * This hook is invoked right after
+ * hook_synonyms_field_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.
+ *
+ * @param array $field_providers
+ *   Array of information about existing field-based synonyms behavior
+ *   implementations that was collected from modules
+ * @param string $behavior
+ *   Name of the behavior for which the field-based synonyms behavior
+ *   implementations are being generated
+ */
+function hook_synonyms_provider_field_behavior_implementation_info_alter(&$field_providers, $behavior) {
+  switch ($behavior) {
+    case 'the-behavior-i-want':
+      $field_providers['the-field-type-i-want'] = 'MyFieldTypeAutocompleteSynonymsBehavior';
+      break;
+  }
+}

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

@@ -0,0 +1,20 @@
+name = Field Synonyms Provider
+description = "Provides synonyms from entity fields."
+package = Synonyms
+core = 7.x
+dependencies[] = synonyms
+dependencies[] = field
+
+files[] = includes/AbstractFieldSynonymsBehavior.class.inc
+files[] = includes/EntityReferenceSynonymsBehavior.class.inc
+files[] = includes/TaxonomySynonymsBehavior.class.inc
+files[] = includes/TextSynonymsBehavior.class.inc
+
+files[] = synonyms_provider_field.test
+
+; Information added by Drupal.org packaging script on 2016-05-07
+version = "7.x-1.5"
+core = "7.x"
+project = "synonyms"
+datestamp = "1462586641"
+

+ 97 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.module

@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Provides synonyms from entity fields.
+ */
+
+/**
+ * Implements hook_field_delete_instance().
+ */
+function synonyms_provider_field_field_delete_instance($instance) {
+  // Remove, if necessary, any synonyms behaviors enabled on this instance.
+  foreach (synonyms_behavior_get_all_enabled($instance['entity_type'], $instance['bundle'], synonyms_provider_field_provider_name(field_info_field($instance['field_name']))) as $behavior_implementation) {
+    synonyms_behavior_implementation_delete($behavior_implementation);
+  }
+}
+
+/**
+ * Implements hook_synonyms_provider_info().
+ */
+function synonyms_provider_field_synonyms_behavior_implementation_info($entity_type, $bundle, $behavior) {
+  $providers = array();
+
+  // Proxy the request down to any module that ships a synonyms behavior
+  // implementation based on a field.
+  $field_providers = module_invoke_all('synonyms_provider_field_behavior_implementation_info', $behavior);
+  drupal_alter('synonyms_provider_field_behavior_implementation_info', $field_providers, $behavior);
+
+  $instances = field_info_instances($entity_type, $bundle);
+
+  foreach ($instances as $instance) {
+    $field = field_info_field($instance['field_name']);
+    if (isset($field_providers[$field['type']])) {
+      $providers[] = array(
+        'provider' => synonyms_provider_field_provider_name($field),
+        'label' => $instance['label'],
+        'class' => $field_providers[$field['type']],
+      );
+    }
+  }
+
+  return $providers;
+}
+
+/**
+ * Implements hook_synonyms_provider_field_behavior_implementation_info().
+ */
+function synonyms_provider_field_synonyms_provider_field_behavior_implementation_info($behavior) {
+  switch ($behavior) {
+    case 'autocomplete':
+    case 'select':
+      return array(
+        'number_integer' => 'TextSynonymsBehavior',
+        'number_decimal' => 'TextSynonymsBehavior',
+        'number_float' => 'TextSynonymsBehavior',
+        'text' => 'TextSynonymsBehavior',
+        'taxonomy_term_reference' => 'TaxonomySynonymsBehavior',
+        'entityreference' => 'EntityReferenceSynonymsBehavior',
+      );
+      break;
+  }
+  return array();
+}
+
+/**
+ * Construct synonyms provider name out of underlying field.
+ *
+ * This function is inverse of synonyms_provider_field_field_name().
+ *
+ * @param array $field
+ *   Field definition array whose provider name should be constructed
+ *
+ * @return string
+ *   Provider name that corresponds to the $field
+ *
+ * @see synonyms_provider_field_field_name()
+ */
+function synonyms_provider_field_provider_name($field) {
+  return 'synonyms_provider_field_' . $field['field_name'];
+}
+
+/**
+ * Reconstruct field name from the name of its provider.
+ *
+ * This function is inverse of synonyms_provider_field_provider_name().
+ *
+ * @param string $provider
+ *   Name of the provider whose underlying field should be reconstructed
+ *
+ * @return string
+ *   Field name that corresponds to the $provider
+ *
+ * @see synonyms_provider_field_provider_name()
+ */
+function synonyms_provider_field_field_name($provider) {
+  return drupal_substr($provider, drupal_strlen('synonyms_provider_field_'));
+}

+ 873 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_field/synonyms_provider_field.test

@@ -0,0 +1,873 @@
+<?php
+
+/**
+ * @file
+ * Tests for the Synonyms field provider module.
+ */
+
+/**
+ * Base class for all tests that test field-based Synonyms providers.
+ */
+abstract class AbstractSynonymsProviderFieldWebTestCase extends SynonymsWebTestCase {
+
+  /**
+   * Setup method.
+   */
+  public function setUp($modules = array()) {
+    if (!$this->behavior_implementation['behavior']) {
+      $this->behavior_implementation['behavior'] = 'autocomplete';
+    }
+    parent::setUp($modules);
+  }
+
+  /**
+   * Test synonymsExtract() method.
+   *
+   * @param array $items
+   *   Array of field items to be saved in tested term
+   * @param array $standard
+   *   Expected return of synonymsExtract() method
+   * @param string $message
+   *   Any custom message to be added to the standard one and passed to
+   *   SimpleTest assertion method
+   */
+  protected function assertSynonymsExtract($items, $standard, $message = '') {
+    $term = (object) array(
+      'name' => $this->randomName(),
+      'vid' => $this->vocabulary->vid,
+    );
+    $term->{$this->fields['enabled']['field']['field_name']} = $items;
+    taxonomy_term_save($term);
+    $synonyms = $this->behavior_implementation['object']->extractSynonyms($term);
+    $this->assertTrue(count(array_intersect($standard, $synonyms)) == count($standard), $this->behavior_implementation['class'] . '::extractSynonyms() passed: ' . $message);
+    // Cleaning up.
+    taxonomy_term_delete($term->tid);
+  }
+
+  /**
+   * Test mergeEntityAsSynonym method.
+   *
+   * @param array $items
+   *   Items array to place into the field on which synonyms provider is based
+   * @param object $synonym_entity
+   *   Parameter will be passed directly to the behavior implementation object
+   * @param string $synonym_entity_type
+   *   Parameter will be passed directly to the behavior implementation object
+   * @param array $standard
+   *   Array that is expected to be returned by the tested method
+   * @param string $message
+   *   Any custom message to be added to the standard one and passed to
+   *   SimpleTest assertion method
+   */
+  protected function assertMergeEntityAsSynonym($items, $synonym_entity, $synonym_entity_type, $standard, $message = '') {
+    $message = $this->behavior_implementation['class'] . '::mergeEntityAsSynonym() passed: ' . $message;
+
+    $term = (object) array(
+      'name' => $this->randomName(),
+      'vid' => $this->vocabulary->vid,
+    );
+    $term->{$this->fields['enabled']['field']['field_name']} = $items;
+    taxonomy_term_save($term);
+
+    $this->behavior_implementation['object']->mergeEntityAsSynonym($term, $synonym_entity, $synonym_entity_type);
+    $merged_items = $term->{$this->fields['enabled']['field']['field_name']}[LANGUAGE_NONE];
+    foreach ($standard as $k => $v) {
+      if (count(array_intersect($standard[$k], $merged_items[$k])) != count($standard[$k])) {
+        $this->fail($message);
+        return;
+      }
+    }
+    $this->pass($message);
+
+    taxonomy_term_delete($term->tid);
+  }
+
+
+  /**
+   * Test synonymFind method.
+   *
+   * @param array $meta_data
+   *   Array of meta data. Each subarray represents a single term and whether it
+   *   is expected to be included in the return of the method. Should have the
+   *   following structure:
+   *   - items: (array) Array of field items. Terms will be automatically
+   *     created with those items
+   *   - found_synonyms: (array) Array of synonyms that are expected to be found
+   *     for the given term, i.e. if "found_synonyms" is empty, it means the
+   *     term should not be found for the given $condition. If the
+   *     "found_synonyms" is not empty, then each element of this array should
+   *     trigger appearance of the term in the results for the given
+   *     $condition
+   * @param QueryConditionInterface $condition
+   *   Database condition that will be passed to the synonymsFind method
+   * @param string $message
+   *   Any custom message to be added to the standard one and passed to
+   *   SimpleTest assertion method
+   */
+  protected function assertSynonymsFind($meta_data, QueryConditionInterface $condition, $message = '') {
+    $message = $this->behavior_implementation['class'] . '::synonymsFind() pass: ' . $message;
+
+    $terms = array();
+    foreach ($meta_data as $v) {
+      $term = (object) array(
+        'name' => $this->randomName(),
+        'vid' => $this->vocabulary->vid,
+        $this->fields['enabled']['field']['field_name'] => $v['items'],
+      );
+      taxonomy_term_save($term);
+      $term->found_synonyms = $v['found_synonyms'];
+      $terms[] = $term;
+    }
+
+    $return = $this->behavior_implementation['object']->synonymsFind($condition);
+
+    $rows = array();
+    foreach ($return as $row) {
+      $rows[] = $row;
+    }
+
+    $success = TRUE;
+    $total_rows_standard = 0;
+    $total_rows = 0;
+    foreach ($terms as $term) {
+      foreach ($term->found_synonyms as $found_synonym) {
+        $total_rows_standard++;
+        $is_found = FALSE;
+        $total_rows = 0;
+        foreach ($rows as $row) {
+          $total_rows++;
+          if ($row->entity_id == $term->tid && $row->synonym == $found_synonym) {
+            $is_found = TRUE;
+          }
+        }
+        $success = $success && $is_found;
+      }
+    }
+
+    $success = $success && $total_rows_standard == $total_rows;
+
+    $this->assertTrue($success, $message);
+
+    // Cleaning up.
+    foreach ($terms as $term) {
+      taxonomy_term_delete($term->tid);
+    }
+  }
+}
+
+/**
+ * Test TextSynonymsBehavior class.
+ */
+class TextSynonymsBehaviorWebTestCase extends AbstractSynonymsProviderFieldWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'TextSynonymsBehavior',
+      'description' => 'Ensure that the synonyms module extracts synonyms from text and number fields correctly.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * Test synonyms extraction for 'text' field type.
+   */
+  public function testText() {
+    // Testing synonymsExtract().
+    $this->assertSynonymsExtract(array(), array(), 'on empty field.');
+
+    $synonym = $this->randomName();
+    $this->assertSynonymsExtract(array(
+      LANGUAGE_NONE => array(
+        0 => array('value' => $synonym),
+      ),
+    ), array($synonym), 'on a field that holds one value.');
+
+    // Testing mergeEntityAsSynonym() method.
+    $node = (object) array(
+      'title' => $this->randomName(),
+      'type' => 'page',
+    );
+    node_save($node);
+    $this->assertMergeEntityAsSynonym(array(), $node, 'node', array(array('value' => $node->title)), 'on a node entity.');
+
+    // Testing synonymFind() method.
+    $this->assertSynonymsFind(array(), db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on empty field.');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field without values.');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field with a value, but when searching for another string.');
+
+    $meta_data = array();
+    $synonym = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym),
+        ),
+      ),
+      'found_synonyms' => array($synonym),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym), 'on a field with a single value searching for that string');
+
+    $meta_data = array();
+    $synonym = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym),
+          array('value' => $this->randomName()),
+        ),
+      ),
+      'found_synonyms' => array($synonym),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym), 'on a field with 2 values searching for one of those 2 values');
+
+    $meta_data = array();
+    $synonym = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym),
+          array('value' => $this->randomName()),
+        ),
+      ),
+      'found_synonyms' => array($synonym),
+    );
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $this->randomName()),
+          array('value' => $this->randomName()),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym), 'on 2 fields with 2 values each searching for one of those values');
+
+    $meta_data = array();
+    $synonym = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym),
+        ),
+      ),
+      'found_synonyms' => array($synonym),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr($synonym, 1, -1)) . '%', 'LIKE'), 'on a field with a value searching for a string LIKE the %value%');
+
+    $meta_data = array();
+    $tag = $this->randomName();
+    $synonym1 = $tag . $this->randomName();
+    $synonym2 =  $tag . $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym1),
+          array('value' => $synonym2),
+        ),
+      ),
+      'found_synonyms' => array($synonym1, $synonym2),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like($tag) . '%', 'LIKE'), 'on a field with 2 similar values searching a string like %both values%');
+
+    $meta_data = array();
+    $synonym1 = $this->randomName();
+    $synonym2 = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym1),
+          array('value' => $synonym2),
+        ),
+      ),
+      'found_synonyms' => array($synonym1, $synonym2),
+    );
+    $this->assertSynonymsFind($meta_data, db_or()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym1)->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym2), 'on a field with 2 values searching for value1 or value2');
+
+    $meta_data = array();
+    $synonym = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym),
+          array('value' => $this->randomName()),
+        ),
+      ),
+      'found_synonyms' => array($synonym),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym)->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like(drupal_substr($synonym, 0, -1)) . '%', 'LIKE'), 'on a field with 2 values searching for value1 and LIKE value1%');
+
+    $meta_data = array();
+    $synonym1 = $this->randomName();
+    $synonym2 = $this->randomName();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym1),
+          array('value' => $synonym2),
+        ),
+      ),
+      'found_synonyms' => array($synonym1, $synonym2),
+    );
+    $condition = db_or();
+    $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym1);
+    $condition->condition(db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym2)->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like(drupal_substr($synonym2, 0, -1)) . '%', 'LIKE'));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for (value1 or (value2 AND value2%))');
+
+    $meta_data = array();
+    $synonym1 = $this->randomName() . ' ' . $this->randomName() . ' ' . $this->randomName();
+    $synonym2 = str_replace(' ', '-', $synonym1);
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('value' => $synonym1),
+          array('value' => $synonym2),
+        ),
+      ),
+      'found_synonyms' => array($synonym1, $synonym2),
+    );
+    $condition = db_and()
+      ->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :synonym", array(
+        ':synonym' => $synonym2,
+      ));
+    $this->assertSynonymsFind($meta_data, $condition, "on a field with 2 values, where 2nd value replaces spaces with dashes in the 1st value, searching for REPLACE(column, ' ', '-') = value2");
+  }
+}
+
+/**
+ * Test TaxonomySynonymsBehavior class.
+ */
+class TaxonomySynonymsBehaviorWebTestCase extends AbstractSynonymsProviderFieldWebTestCase {
+
+  /**
+   * Taxonomy vocabulary object terms.
+   *
+   * Terms of this vocabulary are synonyms of the main vocabulary terms.
+   *
+   * @var object
+   */
+  protected $vocabularySource;
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'TaxonomySynonymsBehavior',
+      'description' => 'Ensure that the synonyms module extracts synonyms from taxonomy term reference fields correctly.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    $this->fields['enabled']['field'] = array(
+      'field_name' => 'term',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'type' => 'taxonomy_term_reference',
+      'settings' => array(
+        'allowed_values' => array(
+          array(
+            'vocabulary' => 'source_vocabulary',
+            'parent' => 0,
+          ),
+        ),
+      ),
+    );
+    parent::setUp($modules);
+
+    $this->vocabularySource = (object) array(
+      'name' => $this->randomName(),
+      'machine_name' => 'source_vocabulary',
+    );
+    taxonomy_vocabulary_save($this->vocabularySource);
+  }
+
+  /**
+   * Test synonyms extraction for 'taxonomy_term_reference' field type.
+   */
+  public function testTaxonomyTermReference() {
+    // Testing synonymsExtract().
+    $this->assertSynonymsExtract(array(), array(), 'on empty field.');
+
+    $synonym_term = $this->createSynonymTerm();
+    $this->assertSynonymsExtract(array(
+      LANGUAGE_NONE => array(
+        0 => array(
+          'tid' => $synonym_term->tid,
+        ),
+      ),
+    ), array($synonym_term->name), 'on a field that holds one value.');
+
+    // Testing mergeEntityAsSynonym() method.
+    $synonym_term = $this->createSynonymTerm();
+    $this->assertMergeEntityAsSynonym(array(), $synonym_term, 'taxonomy_term', array(array('tid' => $synonym_term->tid)), 'on a term from referenced vocabulary.');
+
+    // Testing synonymFind() method.
+    $this->assertSynonymsFind(array(), db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on empty field');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field without values');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $this->createSynonymTerm()->tid),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field with a value but searching for another string');
+
+    $meta_data = array();
+    $synonym_term = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term->name), 'on a field with a single value searching for that string');
+
+    $meta_data = array();
+    $synonym_term = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $this->createSynonymTerm()->tid),
+          array('tid' => $synonym_term->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term->name), 'on a field with 2 values searching for one of those 2 values');
+
+    $meta_data = array();
+    $synonym_term = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term->tid),
+          array('tid' => $this->createSynonymTerm()->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term->name),
+    );
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $this->createSynonymTerm()->tid),
+          array('tid' => $this->createSynonymTerm()->tid),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term->name), 'on 2 fields with 2 values each searching for one of those values');
+
+    $meta_data = array();
+    $synonym_term = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr($synonym_term->name, 1, -1)) . '%', 'LIKE'), 'on a field with a value searching for a string LIKE the %value%');
+
+    $meta_data = array();
+    $tag = $this->randomName();
+    $synonym_term1 = $this->createSynonymTerm($tag . $this->randomName());
+    $synonym_term2 = $this->createSynonymTerm($tag . $this->randomName());
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term1->tid),
+          array('tid' => $synonym_term2->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term1->name, $synonym_term2->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like($tag) . '%', 'LIKE'), 'on a field with 2 similar values searching a string like %both values%');
+
+    $meta_data = array();
+    $synonym_term1 = $this->createSynonymTerm();
+    $synonym_term2 = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term1->tid),
+          array('tid' => $synonym_term2->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term1->name, $synonym_term2->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_or()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term1->name)->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term2->name), 'on a field with 2 values searching for value1 or value2');
+
+    $meta_data = array();
+    $synonym_term = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term->tid),
+          array('tid' => $this->createSynonymTerm()->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term->name),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term->name)->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr($synonym_term->name, 1, -1)) . '%', 'LIKE'), 'on a field with 2 values searching for value1 and LIKE value1%');
+
+    $meta_data = array();
+    $synonym_term1 = $this->createSynonymTerm();
+    $synonym_term2 = $this->createSynonymTerm();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term1->tid),
+          array('tid' => $synonym_term2->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term1->name, $synonym_term2->name),
+    );
+    $condition = db_or();
+    $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term1->name);
+    $condition->condition(db_and()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $synonym_term2->name)
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr($synonym_term2->name, 1 -1)) . '%', 'LIKE'));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for (value1 or (value2 AND value2%))');
+
+    $meta_data =  array();
+    $synonym_term1 = $this->createSynonymTerm($this->randomName() . ' ' . $this->randomName() . ' ' . $this->randomName());
+    $synonym_term2 = $this->createSynonymTerm(str_replace(' ', '-', $synonym_term1->name));
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('tid' => $synonym_term1->tid),
+          array('tid' => $synonym_term2->tid),
+        ),
+      ),
+      'found_synonyms' => array($synonym_term1->name, $synonym_term2->name),
+    );
+    $condition = db_and()
+      ->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :synonym", array(
+        ':synonym' => $synonym_term2->name,
+      ));
+    $this->assertSynonymsFind($meta_data, $condition, "on a field with 2 values, where 2nd value replaces spaces with dashes in the 1st value, searching for REPLACE(column, ' ', '-') = value2");
+  }
+
+  /**
+   * Supportive function.
+   *
+   * Create a taxonomy term in the synonyms source vocabulary with the specified
+   * name.
+   *
+   * @param string $name
+   *   Name of the term to be created. If nothing is supplied a random string
+   *   is used
+   *
+   * @return object
+   *   Fully loaded taxonomy term object of the just created term
+   */
+  protected function createSynonymTerm($name = NULL) {
+    if (is_null($name)) {
+      $name = $this->randomName();
+    }
+    $synonym_term = (object) array(
+      'name' => $name,
+      'vid' => $this->vocabularySource->vid,
+    );
+    taxonomy_term_save($synonym_term);
+    return $synonym_term;
+  }
+}
+
+/**
+ * Test EntityReferenceSynonymsBehavior class.
+ */
+class EntityReferenceSynonymsBehaviorWebTestCase extends AbstractSynonymsProviderFieldWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'EntityReferenceSynonymsBehavior',
+      'description' => 'Ensure that the synonyms module extracts synonyms from entity reference fields correctly.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  /**
+   * SetUp method.
+   */
+  public function setUp($modules = array()) {
+    $modules[] = 'entityreference';
+
+    $this->fields['enabled']['field'] = array(
+      'field_name' => 'reference',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'type' => 'entityreference',
+      // For the sake of experiment we use entityreference field that references
+      // nodes of a Drupal standard type to make things easier.
+      'settings' => array(
+        'target_type' => 'node',
+        'handler' => 'base',
+        'handler_settings' => array(
+          'target_bundles' => array('page' => 'page'),
+          'sort' => array('type' => 'none'),
+        ),
+      ),
+    );
+
+    parent::setUp($modules);
+  }
+
+  /**
+   * Test synonyms extraction for 'entityreference' field type.
+   */
+  public function testEntityReference() {
+    // Testing synonymsExtract().
+    $this->assertSynonymsExtract(array(), array(), 'on empty field.');
+
+    $synonym_entity = $this->createNode();
+    $this->assertSynonymsExtract(array(
+      LANGUAGE_NONE => array(
+        0 => array(
+          'target_id' => entity_id('node', $synonym_entity),
+        ),
+      ),
+    ), array(entity_label('node', $synonym_entity)), 'on a field that holds one value.');
+
+    // Testing mergeEntityAsSynonym() method.
+    $node = $this->createNode();
+    $this->assertMergeEntityAsSynonym(array(), $node, 'node', array(array('target_id' => $node->nid)), 'on a node.');
+
+    // Testing synonymFind() method.
+    $this->assertSynonymsFind(array(), db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on empty field');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field without values');
+
+    $meta_data = array();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $this->createNode())),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $this->randomName()), 'on a field with a value but searching for another string');
+
+    $meta_data = array();
+    $synonym_entity = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity)),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('node', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity)), 'on a field with a single value searching for that string');
+
+    $meta_data = array();
+    $synonym_entity = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity)),
+          array('target_id' => entity_id('node', $this->createNode())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('node', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity)), 'on a field with 2 values searching for one of those 2 values');
+
+    $meta_data = array();
+    $synonym_entity = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity)),
+          array('target_id' => entity_id('node', $this->createNode())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('node', $synonym_entity)),
+    );
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $this->createNode())),
+          array('target_id' => entity_id('node', $this->createNode())),
+        ),
+      ),
+      'found_synonyms' => array(),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity)), 'on 2 fields with 2 values each searching for one of those values');
+
+    $meta_data = array();
+    $synonym_entity = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity)),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('node', $synonym_entity)),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('node', $synonym_entity), 1, -1)) . '%', 'LIKE'), 'on a field with a value searching for a string LIKE the %value%');
+
+    $meta_data = array();
+    $tag = $this->randomName();
+    $synonym_entity1 = $this->createNode($tag . $this->randomName());
+    $synonym_entity2 = $this->createNode($tag . $this->randomName());
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity1)),
+          array('target_id' => entity_id('node', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('node', $synonym_entity1),
+        entity_label('node', $synonym_entity2),
+      ),
+    );
+    $this->assertSynonymsFind($meta_data, db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, db_like($tag) . '%', 'LIKE'), 'on a field with 2 similar values searching a string like %both values%');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createNode();
+    $synonym_entity2 = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity1)),
+          array('target_id' => entity_id('node', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('node', $synonym_entity1),
+        entity_label('node', $synonym_entity2),
+      ),
+    );
+    $condition = db_or()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity1))
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity2));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for value1 or value2');
+
+    $meta_data = array();
+    $synonym_entity = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity)),
+          array('target_id' => entity_id('node', $this->createNode())),
+        ),
+      ),
+      'found_synonyms' => array(entity_label('node', $synonym_entity)),
+    );
+    $condition = db_and()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity))
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('node', $synonym_entity), 1, -1)) . '%', 'LIKE');
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for value1 and LIKE value1%');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createNode();
+    $synonym_entity2 = $this->createNode();
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity1)),
+          array('target_id' => entity_id('node', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('node', $synonym_entity1),
+        entity_label('node', $synonym_entity2),
+      ),
+    );
+    $condition = db_or()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity1))
+      ->condition(db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, entity_label('node', $synonym_entity2))->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, '%' . db_like(drupal_substr(entity_label('node', $synonym_entity2), 1, -1)) . '%', 'LIKE'));
+    $this->assertSynonymsFind($meta_data, $condition, 'on a field with 2 values searching for (value1 or (value2 AND value2%))');
+
+    $meta_data = array();
+    $synonym_entity1 = $this->createNode($this->randomName() . ' ' . $this->randomName() . ' ' . $this->randomName());
+    $synonym_entity2 = $this->createNode(str_replace(' ', '-', entity_label('node', $synonym_entity1)));
+    $meta_data[] = array(
+      'items' => array(
+        LANGUAGE_NONE => array(
+          array('target_id' => entity_id('node', $synonym_entity1)),
+          array('target_id' => entity_id('node', $synonym_entity2)),
+        ),
+      ),
+      'found_synonyms' => array(
+        entity_label('node', $synonym_entity1),
+        entity_label('node', $synonym_entity2),
+      ),
+    );
+    $condition = db_and()
+      ->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :synonym", array(
+        ':synonym' => entity_label('node', $synonym_entity2),
+      ));
+    $this->assertSynonymsFind($meta_data, $condition, "on a field with 2 values, where 2nd value replaces spaces with dashes in the 1st value, searching for REPLACE(column, ' ', '-') = value2");
+  }
+
+  /**
+   * Supportive function.
+   *
+   * Create an entity of necessary entity type (in our test it's node).
+   *
+   * @param string $label
+   *   Label to use for the entity that is about to be created
+   * @param string $bundle
+   *   Bundle to use for the entity that is about to be created
+   *
+   * @return object
+   *   Fully loaded entity object of the just created entity
+   */
+  protected function createNode($label = NULL, $bundle = 'page') {
+    if (is_null($label)) {
+      $label = $this->randomName();
+    }
+    $entity = (object) array(
+      'type' => $bundle,
+      'title' => $label,
+    );
+    node_save($entity);
+    return $entity;
+  }
+}

+ 56 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/AbstractPropertySynonymsBehavior.class.inc

@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Abstract class for enabling entity properties to be source of synonyms.
+ */
+
+/**
+ * Definition of AbstractPropertySynonymsBehavior class.
+ */
+class AbstractPropertySynonymsBehavior extends AbstractSynonymsBehavior implements AutocompleteSynonymsBehavior, SelectSynonymsBehavior {
+
+  /**
+   * Name of the property on which this provider was initialized.
+   *
+   * @var string
+   */
+  protected $property;
+
+  /**
+   * Entity info of the entity type on which this provider was initialized.
+   *
+   * @var array
+   */
+  protected $entity_info;
+
+  public function __construct($behavior_implementation) {
+    parent::__construct($behavior_implementation);
+
+    $this->property = synonyms_provider_property_name($this->behavior_implementation['provider']);
+    $this->entity_info = entity_get_info($this->behavior_implementation['entity_type']);
+  }
+
+  public function extractSynonyms($entity) {
+    $synonyms = array();
+
+    if (isset($entity->{$this->property}) && $entity->{$this->property}) {
+      $synonyms[] = $entity->{$this->property};
+    }
+    return $synonyms;
+  }
+
+  public function mergeEntityAsSynonym($trunk_entity, $synonym_entity, $synonym_entity_type) {
+    // TODO: what to do ???
+  }
+
+  public function synonymsFind(QueryConditionInterface $condition) {
+    $query = db_select($this->entity_info['base table'], 'base');
+    $query->addField('base', $this->entity_info['entity keys']['id'], 'entity_id');
+    $query->addField('base', $this->property, 'synonym');
+
+    $this->synonymsFindProcessCondition($condition, 'base.' . $this->property, 'base.' . $this->entity_info['entity keys']['id']);
+    $query->condition($condition);
+    return $query->execute();
+  }
+}

+ 12 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/PropertySynonymsBehavior.class.inc

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Enables Entity properties to be source of synonyms.
+ */
+
+/**
+ * Definition of PropertySynonymsBehavior class.
+ */
+class PropertySynonymsBehavior extends AbstractPropertySynonymsBehavior {
+}

+ 14 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/includes/SearchPropertySynonymsBehavior.class.inc

@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Integrates Entity properties with synonyms "search" behavior.
+ */
+
+if (module_exists('synonyms_search')) {
+  /**
+   * Definition of SearchPropertySynonymsBehavior class.
+   */
+  class SearchPropertySynonymsBehavior extends PropertySynonymsBehavior implements SearchSynonymsBehavior {
+  }
+}

+ 18 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.info

@@ -0,0 +1,18 @@
+name = Entity Property Synonyms Provider
+description = "Provides synonyms from entity properties."
+package = Synonyms
+core = 7.x
+dependencies[] = synonyms
+
+files[] = includes/AbstractPropertySynonymsBehavior.class.inc
+files[] = includes/PropertySynonymsBehavior.class.inc
+files[] = includes/SearchPropertySynonymsBehavior.class.inc
+
+files[] = synonyms_provider_property.test
+
+; Information added by Drupal.org packaging script on 2016-05-07
+version = "7.x-1.5"
+core = "7.x"
+project = "synonyms"
+datestamp = "1462586641"
+

+ 98 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.module

@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Provides synonyms from entity properties.
+ */
+
+/**
+ * Implements hook_synonyms_behavior_implementation_info().
+ */
+function synonyms_provider_property_synonyms_behavior_implementation_info($entity_type, $bundle, $behavior) {
+  $providers = array();
+
+  switch ($behavior) {
+    case 'select':
+    case 'autocomplete':
+    case 'search':
+      $entity_info = entity_get_info($entity_type);
+      $class = $behavior == 'search' ? 'SearchPropertySynonymsBehavior' : 'PropertySynonymsBehavior';
+
+      if ($entity_info['base table'] && isset($entity_info['entity keys']['id'])) {
+        $entity_keys = array_filter($entity_info['entity keys']);
+        $properties = entity_get_property_info($entity_type);
+        $properties = $properties['properties'];
+
+        // Label is the label of an entity, therefore it may not be a synonym.
+        if (isset($entity_keys['label'])) {
+          unset($properties[$entity_keys['label']]);
+          unset($entity_keys['label']);
+        }
+
+        // Get all defined entity keys. We prefer them from entity keys rather
+        // than from entity properties, because in the former case we know what
+        // those keys represent (entity ID, bundle, etc), so we can give a bit
+        // more unified UI by maintaining the same labels for the same entity
+        // keys among different entity types.
+        foreach ($entity_keys as $entity_key => $column) {
+          if (in_array($column, $entity_info['schema_fields_sql']['base table'])) {
+            $providers[] = array(
+              'provider' => synonyms_provider_property_provider_name($column),
+              'label' => $entity_key,
+              'class' => $class,
+            );
+
+            unset($properties[$column]);
+          }
+        }
+
+        foreach ($properties as $property => $property_info) {
+          if (isset($property_info['schema field']) && $property_info['schema field']) {
+            $providers[] = array(
+              'provider' => synonyms_provider_property_provider_name($property_info['schema field']),
+              'label' => $property_info['label'],
+              'class' => $class,
+            );
+          }
+        }
+      }
+      break;
+  }
+
+  return $providers;
+}
+
+/**
+ * Construct synonyms provider name out of underlying property.
+ *
+ * This function is inverse of synonyms_provider_property_name().
+ *
+ * @param string $property
+ *   Entity property name whose provider name should be constructed
+ *
+ * @return string
+ *   Provider name that corresponds to the $property
+ *
+ * @see synonyms_provider_property_name()
+ */
+function synonyms_provider_property_provider_name($property) {
+  return 'synonyms_provider_property_' . $property;
+}
+
+/**
+ * Reconstruct entity property name from the name of its provider.
+ *
+ * This function is inverse of synonyms_provider_property_provider_name().
+ *
+ * @param string $provider
+ *   Name of the provider whose underlying entity property should be
+ *   reconstructed
+ *
+ * @return string
+ *   Entity property name that corresponds to the $provider
+ *
+ * @see synonyms_provider_property_provider_name()
+ */
+function synonyms_provider_property_name($provider) {
+  return drupal_substr($provider, drupal_strlen('synonyms_provider_property_'));
+}

+ 112 - 0
sites/all/modules/contrib/taxonomy/synonyms/synonyms_provider_property/synonyms_provider_property.test

@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Test cases for Property synonyms provider module.
+ */
+
+/**
+ * Test PropertySynonymsBehavior class.
+ */
+class SynonymsProviderPropertyWebTestCase extends SynonymsWebTestCase {
+
+  /**
+   * GetInfo method.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'PropertySynonymsBehavior',
+      'description' => 'Ensure that the synonyms module extracts synonyms from entity properties correctly.',
+      'group' => 'Synonyms',
+    );
+  }
+
+  public function setUp($modules = array()) {
+    $this->behavior_implementation['behavior'] = 'autocomplete';
+    $this->behavior_implementation['provider'] = 'whatever';
+
+    array_unshift($modules, 'synonyms_provider_property');
+    parent::setUp($modules);
+
+    // We can't initialize this before parent::setUp() because we depend on
+    // synonyms_provider_property_provider_name() function.
+    synonyms_behavior_implementation_delete($this->behavior_implementation);
+    $this->behavior_implementation['provider'] = synonyms_provider_property_provider_name('tid');
+    synonyms_behavior_implementation_save($this->behavior_implementation);
+    foreach (synonyms_behavior_get($this->behavior_implementation['behavior'], $this->behavior_implementation['entity_type'], $this->behavior_implementation['bundle'], TRUE) as $behavior_implementation) {
+      if ($behavior_implementation['provider'] == $this->behavior_implementation['provider']) {
+        $this->behavior_implementation = $behavior_implementation;
+        break;
+      }
+    }
+  }
+
+  /**
+   * Test property-based synonyms provider.
+   */
+  public function testProperty() {
+    $term = (object) array(
+      'vid' => $this->vocabulary->vid,
+      'name' => $this->randomName(),
+    );
+    taxonomy_term_save($term);
+
+    $term2 = (object) array(
+      'vid' => $this->vocabulary->vid,
+      'name' => $this->randomName(),
+    );
+    taxonomy_term_save($term2);
+
+    // Test extractSynonyms() method.
+    $synonyms = $this->behavior_implementation['object']->extractSynonyms($term);
+    $this->assertIdentical($synonyms, array($term->tid), $this->behavior_implementation['class'] . '::extractSynonyms() passed.');
+
+    // Test synonymsFind() method.
+    $condition = db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, 0);
+    $this->assertSynonymsFind($condition, array(), 'on non-existing synonym');
+
+    $condition = db_and()->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $term->tid);
+    $this->assertSynonymsFind($condition, array(array('entity_id' => $term->tid, 'synonym' => $term->tid)), 'on a synonym');
+
+    $condition = db_and()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $term->tid)
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $term2->tid);
+    $this->assertSynonymsFind($condition, array(), 'on a synonym AND on another synonym');
+
+    $condition = db_or()
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $term->tid)
+      ->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $term2->tid);
+    $this->assertSynonymsFind($condition, array(array('entity_id' => $term->tid, 'synonym' => $term->tid), array('entity_id' => $term2->tid, 'synonym' => $term2->tid)), 'on a synonym OR another synonym');
+  }
+
+  /**
+   * Supportive assert method.
+   *
+   * Assert results of synonymsFind() method.
+   *
+   * @param QueryConditionInterface $condition
+   *   Query condition to be passed on into synonymsFind() method
+   * @param array $standard
+   *   Expected result
+   * @param string $message
+   *   Optional additional assert message
+   */
+  protected function assertSynonymsFind(QueryConditionInterface $condition, $standard, $message = '') {
+    $map_standard = array();
+    foreach ($standard as $v) {
+      $map_standard[$v['entity_id']] = $v['synonym'];
+    }
+
+    $message = $this->behavior_implementation['class'] . '::synonymsFind() ' . $message;
+    $result = $this->behavior_implementation['object']->synonymsFind($condition);
+    $count = 0;
+    foreach ($result as $row) {
+      $count++;
+      if (!isset($map_standard[$row->entity_id]) || $map_standard[$row->entity_id] != $row->synonym) {
+        $this->fail($message);
+        return;
+      }
+    }
+    $this->assertEqual(count($map_standard), $count, $message);
+  }
+}

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

@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Enables Product Reference field type for synonyms "search" integration.
+ */
+
+/**
+ * Definition of SearchCommerceProductReferenceSynonymsBehavior class.
+ */
+class SearchCommerceProductReferenceSynonymsBehavior extends CommerceProductReferenceSynonymsBehavior implements SearchSynonymsBehavior {
+}

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

@@ -9,6 +9,6 @@
 /**
  * Interface of search integration synonyms behavior.
  */
-interface SearchSynonymsBehavior extends SynonymsSynonymsBehavior {
+interface SearchSynonymsBehavior extends SynonymsBehavior {
 }
 

+ 10 - 6
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/plugins/behavior/search.inc

@@ -19,9 +19,11 @@ $plugin = array(
  * 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']));
+function synonyms_search_behavior_search_enabled($behavior_definition, $behavior_implementation) {
+  if ($behavior_implementation['entity_type'] == 'taxonomy_term') {
+    module_load_include('inc', 'synonyms_search', 'synonyms_search.pages');
+    synonyms_search_reindex_nodes_by_vocabulary(taxonomy_vocabulary_machine_name_load($behavior_implementation['bundle']));
+  }
 }
 
 /**
@@ -30,7 +32,9 @@ function synonyms_search_behavior_search_enabled($behavior_definition, $settings
  * 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']));
+function synonyms_search_behavior_search_disabled($behavior_definition, $behavior_implementation) {
+  if ($behavior_implementation['entity_type'] == 'taxonomy_term') {
+    module_load_include('inc', 'synonyms_search', 'synonyms_search.pages');
+    synonyms_search_reindex_nodes_by_vocabulary(taxonomy_vocabulary_machine_name_load($behavior_implementation['bundle']));
+  }
 }

+ 9 - 4
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.info

@@ -1,9 +1,13 @@
 name = Synonyms Search
 description = "Provides synonyms integration with searching."
-package = Taxonomy
+package = Synonyms
 core = 7.x
 dependencies[] = synonyms
 dependencies[] = search
+dependencies[] = taxonomy
+
+test_dependencies[] = entityreference:entityreference
+test_dependencies[] = term_search:term_search
 
 files[] = synonyms_search.test
 
@@ -11,10 +15,11 @@ files[] = includes/SearchSynonymsBehavior.interface.inc
 files[] = includes/SearchTextSynonymsBehavior.class.inc
 files[] = includes/SearchTaxonomySynonymsBehavior.class.inc
 files[] = includes/SearchEntityReferenceSynonymsBehavior.class.inc
+files[] = includes/SearchCommerceProductReferenceSynonymsBehavior.class.inc
 
-; Information added by Drupal.org packaging script on 2015-12-02
-version = "7.x-1.4"
+; Information added by Drupal.org packaging script on 2016-05-07
+version = "7.x-1.5"
 core = "7.x"
 project = "synonyms"
-datestamp = "1449079740"
+datestamp = "1462586641"
 

+ 20 - 9
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.module

@@ -34,8 +34,8 @@ function synonyms_search_taxonomy_term_update($term) {
     $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));
+      $current_search_synonyms = array_merge($current_search_synonyms, $behavior_implementation['object']->extractSynonyms($term));
+      $previous_search_synonyms = array_merge($previous_search_synonyms, $behavior_implementation['object']->extractSynonyms($term->original));
     }
     $diff = array_diff($current_search_synonyms, $previous_search_synonyms);
     if (!empty($diff) || count($current_search_synonyms) != count($previous_search_synonyms)) {
@@ -51,11 +51,13 @@ function synonyms_search_taxonomy_term_update($term) {
 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.
+    // We go a field by field looking for taxonomy term reference or entity
+    // reference of taxonomy term type and if that vocabulary has enabled search
+    // synonyms, we add them to the search index.
+    // TODO: implement this through foreign keys information. See
+    // term_merge_fields_with_foreign_key() function.
     $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) {
@@ -63,6 +65,14 @@ function synonyms_search_node_update_index($node) {
         }
       }
     }
+    if ($field_info['type'] == 'entityreference' && $field_info['settings']['target_type'] == 'taxonomy_term') {
+      $terms = field_get_items('node', $node, $instance['field_name']);
+      if (is_array($terms) && !empty($terms)) {
+        foreach ($terms as $v) {
+          $output[] = $v['target_id'];
+        }
+      }
+    }
   }
 
   if (!empty($output)) {
@@ -72,7 +82,7 @@ function synonyms_search_node_update_index($node) {
       $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));
+        $output = array_merge($output, $implementation['object']->extractSynonyms($term));
       }
     }
   }
@@ -90,15 +100,15 @@ function synonyms_search_term_update_index($term) {
 
   $synonyms = array();
   foreach ($behavior_implementations as $implementation) {
-    $synonyms = array_merge($synonyms, synonyms_extract_synonyms($term, $implementation));
+    $synonyms = array_merge($synonyms, $implementation['object']->extractSynonyms($term));
   }
   return implode(', ', $synonyms);
 }
 
 /**
- * Implements hook_synonyms_behavior_implementation_info().
+ * Implements hook_synonyms_provider_field_behavior_implementation_info().
  */
-function synonyms_search_synonyms_behavior_implementation_info($behavior) {
+function synonyms_search_synonyms_provider_field_behavior_implementation_info($behavior) {
   switch ($behavior) {
     case 'search':
       return array(
@@ -108,6 +118,7 @@ function synonyms_search_synonyms_behavior_implementation_info($behavior) {
         'text' => 'SearchTextSynonymsBehavior',
         'taxonomy_term_reference' => 'SearchTaxonomySynonymsBehavior',
         'entityreference' => 'SearchEntityReferenceSynonymsBehavior',
+        'commerce_product_reference' => 'SearchCommerceProductReferenceSynonymsBehavior',
       );
       break;
   }

+ 14 - 2
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.pages.inc

@@ -20,9 +20,9 @@ function synonyms_search_reindex_nodes_by_terms($tids) {
   // that would imply a big amount of SQL queries on some websites.
   $found_nids = array();
   foreach (field_info_field_map() as $field_name => $v) {
+    // TODO: explore possibility of using foreign keys instead of hard coding
+    // fields types in here.
     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')
@@ -31,6 +31,18 @@ function synonyms_search_reindex_nodes_by_terms($tids) {
         $found_nids = array_merge($found_nids, array_keys($result['node']));
       }
     }
+    if ($v['type'] == 'entityreference' && isset($v['bundles']['node'])) {
+      $field = field_info_field($field_name);
+      if ($field['settings']['target_type'] == 'taxonomy_term') {
+        $query = new EntityFieldQuery();
+        $result = $query->entityCondition('entity_type', 'node')
+          ->fieldCondition($field_name, 'target_id', $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')

+ 108 - 18
sites/all/modules/contrib/taxonomy/synonyms/synonyms_search/synonyms_search.test

@@ -10,8 +10,6 @@
  */
 abstract class AbstractSearchSynonymsWebTestCase extends SynonymsWebTestCase {
 
-  protected $behavior = 'search';
-
   /**
    * What search type is being tested.
    *
@@ -30,7 +28,9 @@ abstract class AbstractSearchSynonymsWebTestCase extends SynonymsWebTestCase {
    * SetUp method.
    */
   public function setUp($modules = array()) {
-    $modules[] = 'synonyms_search';
+    $this->behavior_implementation['behavior'] = 'search';
+
+    array_unshift($modules, 'synonyms_search');
     parent::setUp($modules);
 
     // Create a few terms and synonyms.
@@ -121,6 +121,7 @@ class NodeSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
    * SetUp method.
    */
   public function setUp($modules = array()) {
+    $modules[] = 'entityreference';
     parent::setUp($modules);
 
     // Creating a test content type.
@@ -128,7 +129,18 @@ class NodeSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
       'name' => 'Synonyms Test Content',
       'type' => 'synonyms_test_content',
     ), 'Save content type');
+  }
 
+  /**
+   * 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() {
     // Attaching term reference field to the new content type.
     $field = array(
       'type' => 'taxonomy_term_reference',
@@ -150,32 +162,110 @@ class NodeSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
       'entity_type' => 'node',
       'bundle' => 'synonyms_test_content',
       'label' => 'Synonym Terms',
-      'widget' => array(
-        'type' => 'synonyms_autocomplete',
-      ),
     );
 
     field_create_instance($instance);
+
+    // 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_implementation_delete($this->behavior_implementation);
+    $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.');
+        }
+      }
+    }
   }
 
   /**
-   * Test searching nodes by a term synonym.
+   * Test searching nodes by a term synonym when referenced by entity reference.
    *
-   * 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.
+   * This test pretty much does the same thing as the testSearchTermSynonym()
+   * with the only different that the terms are referenced through entity
+   * reference field type.
    */
-  public function testSearchTermSynonym() {
+  public function testSearchTermSynonymEntityReference() {
+    // Attaching entity reference field to the new content type.
+    $field = array(
+      'type' => 'entityreference',
+      'field_name' => 'synonyms_term_enabled',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'settings' => array(
+        'target_type' => 'taxonomy_term',
+        'handler_settings' => array(
+          'target_bundles' => array($this->vocabulary->machine_name),
+        ),
+      ),
+    );
+    $field = field_create_field($field);
+
+    $instance = array(
+      'field_name' => $field['field_name'],
+      'entity_type' => 'node',
+      'bundle' => 'synonyms_test_content',
+      'label' => 'Synonym Terms',
+    );
+
+    field_create_instance($instance);
+
     // 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),
+        array('target_id' => $this->terms['no_synonyms']->tid),
+        array('target_id' => $this->terms['one_synonym']->tid),
+        array('target_id' => $this->terms['two_synonyms']->tid),
       )),
     );
     node_save($node);
@@ -217,7 +307,7 @@ class NodeSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
 
     // 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);
+    synonyms_behavior_implementation_delete($this->behavior_implementation);
     $this->cronRun();
     foreach ($this->terms as $k => $term) {
       $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);
@@ -326,7 +416,7 @@ class TermSearchSynonymsWebTestCase extends AbstractSearchSynonymsWebTestCase {
 
     // 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);
+    synonyms_behavior_implementation_delete($this->behavior_implementation);
     $this->cronRun();
     foreach ($this->terms as $k => $term) {
       $items = field_get_items('taxonomy_term', $term, $this->fields['enabled']['field']['field_name']);

+ 57 - 13
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms.views.inc

@@ -5,29 +5,73 @@
  * Views integration of Synonyms module.
  */
 
+/**
+ * Implements hook_views_data().
+ */
+function synonyms_views_data() {
+  $data = array();
+
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    $wrapper = entity_metadata_wrapper($entity_type);
+    $property_info = $wrapper->getPropertyInfo();
+    if (isset($property_info['synonyms']) && isset($entity_info['base table']) && $entity_info['base table']) {
+      $data[$entity_info['base table']]['synonyms'] = array(
+        'title' => t('All synonyms'),
+        'help' => t('All synonyms of @entity_type', array(
+          '@entity_type' => $entity_info['label'],
+        )),
+        'field' => array(
+          'handler' => 'synonyms_views_handler_field_synonyms',
+          'real field' => $entity_info['entity keys']['id'],
+          'click sortable' => FALSE,
+          'synonyms entity type' => $entity_type,
+        ),
+      );
+    }
+  }
+
+  return $data;
+}
+
 /**
  * Implements 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';
+  if (module_exists('taxonomy')) {
+    // 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';
+  switch ($field['type']) {
+    case 'taxonomy_term_reference':
+      // Add synonyms friendly filters.
+      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';
+        }
       }
-    }
+      break;
+
+    case 'entityreference':
+      foreach ($field['storage']['details']['sql'] as $table) {
+        $target_id_column = reset($table);
+        $target_id_column = $target_id_column['target_id'];
+        $table = array_keys($table);
+        $table = $table[0];
+        if (isset($result[$table][$target_id_column]['filter'])) {
+          $result[$table][$target_id_column]['filter']['handler'] = 'synonyms_views_handler_filter_entityreference_synonyms';
+        }
+      }
+      break;
   }
 }

+ 102 - 0
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_field_synonyms.inc

@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Definition of synonyms_handler_field_synonyms class.
+ */
+
+/**
+ * Views field handler for displaying synonyms of an entity.
+ */
+class synonyms_views_handler_field_synonyms extends views_handler_field {
+
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['list'] = array(
+      'default' => 'ul',
+    );
+
+    $options['separator'] = array(
+      'default' => '',
+      'translatable' => TRUE,
+    );
+
+    return $options;
+  }
+
+  function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    $form['list'] = array(
+      '#type' => 'radios',
+      '#title' => t('Display type'),
+      '#options' => array(
+        'ul' => t('Unordered list'),
+        'ol' => t('Ordered list'),
+        'separator' => t('Simple separator'),
+      ),
+      '#default_value' => $this->options['list'],
+    );
+
+    $form['separator'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Separator'),
+      '#default_value' => $this->options['separator'],
+      '#dependency' => array(
+        'radio:options[list]' => array('separator'),
+      ),
+    );
+  }
+
+  function pre_render(&$values) {
+    // Load all the queried entities in a single batch.
+    $entity_ids = array();
+    foreach ($values as $value) {
+      $entity_ids[] = $value->{$this->definition['real field']};
+    }
+    $entity_ids = array_unique($entity_ids);
+    if (!empty($entity_ids)) {
+      $entities = entity_load($this->definition['synonyms entity type'], $entity_ids);
+      foreach ($values as &$value) {
+        $value->synonyms_entity = $entities[$value->{$this->definition['real field']}];
+        unset($value);
+      }
+    }
+    parent::pre_render($values);
+  }
+
+  function get_value($values, $field = NULL) {
+    $property = $this->field;
+    $wrapper = entity_metadata_wrapper($this->definition['synonyms entity type'], $values->synonyms_entity);
+    $synonyms = $wrapper->$property->value(array('sanitize' => TRUE));
+
+    if (empty($synonyms)) {
+      $synonyms = '';
+    }
+    else {
+      switch ($this->options['list']) {
+        case 'ol':
+        case 'ul':
+          $synonyms = theme('item_list', array(
+            'type' => $this->options['list'],
+            'items' => $synonyms,
+          ));
+          break;
+
+        case 'separator':
+          $synonyms = implode($this->options['separator'], $synonyms);
+          break;
+      }
+    }
+
+    return $synonyms;
+  }
+
+  function sanitize_value($value, $type = NULL) {
+    if (is_null($type)) {
+      $type = 'xss_admin';
+    }
+    return parent::sanitize_value($value, $type);
+  }
+}

+ 144 - 0
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_entityreference_synonyms.inc

@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Definition of synonyms_views_handler_filter_entityreference_synonyms class.
+ */
+
+/**
+ * Definition of synonyms friendly entity reference field filter.
+ */
+class synonyms_views_handler_filter_entityreference_synonyms extends views_handler_filter_numeric {
+
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['type'] = array(
+      'default' => 'numeric',
+    );
+
+    return $options;
+  }
+
+  function extra_options_form(&$form, &$form_state) {
+    parent::extra_options_form($form, $form_state);
+
+    $form['type'] = array(
+      '#type' => 'radios',
+      '#title' => t('Type'),
+      '#options' => array(
+        'numeric' => t('Numeric'),
+        'synonyms_autocomplete' => t('Synonyms friendly autocomplete'),
+        'synonyms_select' => t('Synonyms friendly select list'),
+      ),
+      '#default_value' => $this->options['type'],
+    );
+  }
+
+  function has_extra_options() {
+    return TRUE;
+  }
+
+  function operators() {
+    $operators = parent::operators();
+    switch ($this->options['type']) {
+      case 'synonyms_autocomplete':
+      case 'synonyms_select':
+        // Only "equals" and "not equals" make sense, other operators are rather
+        // funky when it comes to IDs and not simple integers.
+        $operators = array_intersect_key($operators, drupal_map_assoc(array(
+          '=', '!=',
+        )));
+        break;
+    }
+    return $operators;
+  }
+
+  function value_form(&$form, &$form_state) {
+    parent::value_form($form, $form_state);
+
+    if (isset($form['value']['#type'])) {
+      $element = &$form['value'];
+    }
+    elseif (isset($form['value']['value']['#type'])) {
+      $element = &$form['value']['value'];
+    }
+
+    if (isset($element)) {
+      $field = field_info_field($this->definition['field_name']);
+      $entity_type = array_keys($field['bundles']);
+      $entity_type = reset($entity_type);
+      $bundle = reset($field['bundles'][$entity_type]);
+      $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
+
+      switch ($this->options['type']) {
+        case 'synonyms_autocomplete':
+          $widget = field_info_widget_settings('synonyms_autocomplete_entity');
+          $element['#autocomplete_path'] = $widget['synonyms_autocomplete_path'] . '/' . $field['field_name'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'];
+          $element['#attached']['js'][drupal_get_path('module', 'synonyms') . '/js/synonyms-autocomplete.js'] = array();
+          $element['#attributes']['class'][] = 'synonyms-autocomplete';
+          break;
+
+        case 'synonyms_select':
+          $element['#type'] = 'select';
+          $element['#options'] = synonyms_select_entity_options($field, $instance);
+          if (!$this->is_exposed()) {
+            $element['#empty_option'] = t('- None -');
+          }
+          $element['#element_validate'][] = 'synonyms_select_validate';
+          $element['#element_validate'][] = 'synonyms_select_views_entityreference_filter_validate';
+          unset($element['#size']);
+          break;
+      }
+    }
+  }
+
+  function query() {
+    switch ($this->options['type']) {
+      case 'synonyms_autocomplete':
+        $field = field_info_field($this->definition['field_name']);
+        $this->value['value'] = synonyms_get_entity_by_synonym($field['settings']['target_type'], $this->value['value'], synonyms_field_target_bundles($field));
+        break;
+    }
+    parent::query();
+  }
+
+  function admin_summary() {
+    if ($this->is_a_group()) {
+      return t('grouped');
+    }
+    if (!empty($this->options['exposed'])) {
+      return t('exposed');
+    }
+
+    switch ($this->options['type']) {
+      case 'numeric':
+      case 'synonyms_autocomplete':
+        return parent::admin_summary();
+        break;
+
+      case 'synonyms_select':
+        $field = field_info_field($this->definition['field_name']);
+        $entity = entity_load($field['settings']['target_type'], array($this->value['value']));
+        $entity = reset($entity);
+        if (is_object($entity)) {
+          $label = entity_label($field['settings']['target_type'], $entity);
+          $options = $this->operator_options('short');
+          return check_plain($options[$this->operator]) . ' ' . check_plain($label);
+        }
+        break;
+    }
+  }
+}
+
+/**
+ * Form element validate handler.
+ *
+ * Simply convert select value from an array (as mostly operated within Field
+ * module) to a single value.
+ */
+function synonyms_select_views_entityreference_filter_validate($element, &$form_state) {
+  $value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
+  $value = reset($value);
+  form_set_value($element, $value, $form_state);
+}

+ 184 - 74
sites/all/modules/contrib/taxonomy/synonyms/views/synonyms_views_handler_filter_term_tid.inc

@@ -13,97 +13,162 @@ class synonyms_views_handler_filter_term_tid extends views_handler_filter_term_n
     parent::extra_options_form($form, $form_state);
 
     $form['type']['#options']['synonyms_autocomplete'] = t('Synonyms friendly autocomplete');
+    $form['type']['#options']['synonyms_select'] = t('Synonyms friendly select');
   }
 
   function value_form(&$form, &$form_state) {
-    $restore_value = $this->options['type'] == 'synonyms_autocomplete';
-    if ($restore_value) {
-      $this->options['type'] = 'textfield';
+    $vocabulary = taxonomy_vocabulary_machine_name_load($this->options['vocabulary']);
+    if (empty($vocabulary) && $this->options['limit']) {
+      $form['markup'] = array(
+        '#markup' => '<div class="form-item">' . t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
+      );
+      return;
     }
-    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;
+
+    switch ($this->options['type']) {
+      case 'synonyms_autocomplete':
+        $tags = array();
+        if ($this->value) {
+          $result = taxonomy_term_load_multiple($this->value);
+          foreach ($result as $entity_term) {
+            $tags[] = entity_label('taxonomy_term', $entity_term);
+          }
         }
-      }
+        $tags = drupal_implode_tags($tags);
 
-      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];
-      }
+        $info = $this->synonyms_field_instance();
 
-      // We just grab the first instance of this field within the determined
-      // entity type.
-      $bundle = $field['bundles'][$field_entity_type][0];
+        if ($info['instance']['widget']['type'] == 'synonyms_autocomplete_taxonomy_term') {
+          $widget = $info['instance']['widget']['settings'];
+        }
+        else {
+          $widget = field_info_widget_settings('synonyms_autocomplete_taxonomy_term');
+        }
+        $autocomplete_path = $widget['synonyms_autocomplete_path'];
+        $size = $widget['size'];
 
-      $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';
+        $form['value'] = array(
+          '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'),
+          '#type' => 'textfield',
+          '#default_value' => $tags,
+          '#autocomplete_path' => $autocomplete_path . '/' . $this->definition['field_name'] . '/' . $info['instance']['entity_type'] . '/' . $info['instance']['bundle'],
+          '#size' => $size,
+          '#auto_creation' => FALSE,
+          '#attributes' => array('class' => array('synonyms-autocomplete')),
+          '#attached' => array('js' => array(
+            drupal_get_path('module', 'synonyms') . '/js/synonyms-autocomplete.js' => array(),
+          )),
+        );
+        break;
+
+      case 'synonyms_select':
+        $info = $this->synonyms_field_instance();
+
+        $options = array();
+        $widget = $info['instance']['widget']['type'] == 'synonyms_select_taxonomy_term' ? $info['instance']['widget']['settings'] : field_info_widget_settings('synonyms_select_taxonomy_term');
+        foreach ($info['field']['settings']['allowed_values'] as $tree) {
+          if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
+            switch ($widget['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_entity($term, 'taxonomy_term', NULL, NULL, array('depth'));
+                    foreach ($behavior_implementations as $implementation) {
+                      foreach ($implementation['object']->extractSynonyms($term) as $synonym) {
+                        $options[] = synonyms_select_option_entity($term, 'taxonomy_term', $synonym, $implementation, array('depth'));
+                      }
+                    }
+                  }
+                }
+                break;
+
+              case 'name':
+                // TODO: is there any way to leverage DB for the sorting routine?
+                $options = synonyms_select_taxonomy_term_sort_name_options_recursive($vocabulary, $tree['parent']);
+                break;
+            }
+          }
+        }
+
+        $form['value'] = array(
+          '#type' => 'select',
+          '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'),
+          '#multiple' => TRUE,
+          '#options' => $options,
+          '#size' => min(9, count($options)),
+          '#default_value' => (array) $this->value,
+          '#element_validate' => array('synonyms_select_validate'),
+        );
+        break;
+
+      default:
+        parent::value_form($form, $form_state);
+        break;
     }
   }
 
   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);
+    switch ($this->options['type']) {
+      case '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);
+        if ($tids) {
+          $form_state['values']['options']['value'] = $tids;
+        }
+        break;
+
+      case 'synonyms_select':
+        break;
+
+      default:
+        parent::value_validate($form, $form_state);
+        break;
     }
   }
 
   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;
-      }
+    switch ($this->options['type']) {
+      case '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);
+        $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);
+        if ($tids) {
+          $this->validated_exposed_input = $tids;
+        }
+        break;
+
+      case 'synonyms_select':
+        if (empty($this->options['exposed'])) {
+          return;
+        }
+        if (empty($this->options['expose']['identifier'])) {
+          return;
+        }
+
+        $all_key = array_search('All', $form_state['values'][$this->options['expose']['identifier']]);
+        if ($all_key !== FALSE) {
+          unset($form_state['values'][$this->options['expose']['identifier']][$all_key]);
+        }
+
+        if (!empty($form_state['values'][$this->options['expose']['identifier']])) {
+          return;
+        }
+        $this->validated_exposed_input = $form_state['values'][$this->options['expose']['identifier']];
+        break;
+
+      default:
+        parent::exposed_validate($form, $form_state);
+        break;
     }
   }
 
@@ -154,9 +219,9 @@ class synonyms_views_handler_filter_term_tid extends views_handler_filter_term_n
       if (!empty($missing)) {
         $condition = db_or();
         foreach ($missing as $tag => $v) {
-          $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $tag);
+          $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $tag);
         }
-        $synonyms = synonyms_synonyms_find_behavior($condition, $behavior_implementation);
+        $synonyms = $behavior_implementation['object']->synonymsFind($condition);
         foreach ($synonyms as $synonym) {
           $synonym->synonym = drupal_strtolower($synonym->synonym);
           unset($missing[$synonym->synonym]);
@@ -171,4 +236,49 @@ class synonyms_views_handler_filter_term_tid extends views_handler_filter_term_n
 
     return $tids;
   }
+
+  /**
+   * Collect info about field and instance that correspond to this filter.
+   *
+   * @return array
+   *   Array with the following structure:
+   *   - field: (array) Field definition array that corresponds to this filter
+   *   - instance: (array) Field instance definition array that corresponds to
+   *     this filter
+   */
+  protected function synonyms_field_instance() {
+    $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);
+
+    return array(
+      'field' => $field,
+      'instance' => $instance,
+    );
+  }
 }

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

@@ -57,18 +57,18 @@ class synonyms_views_plugin_argument_validate_taxonomy_term extends views_plugin
         if (empty($vocabularies)) {
           // 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());
+          $bundles = synonyms_bundle_normalize('taxonomy_term', $vocabularies);
         }
 
         foreach ($bundles as $bundle) {
           $condition = db_and();
           if ($transform) {
-            $condition->where("REPLACE(" . AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER . ", ' ', '-') = :argument", array(
+            $condition->where("REPLACE(" . AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER . ", ' ', '-') = :argument", array(
               ':argument' => $argument,
             ));
           }
           else {
-            $condition->condition(AbstractSynonymsSynonymsBehavior::COLUMN_PLACEHOLDER, $argument);
+            $condition->condition(AbstractSynonymsBehavior::COLUMN_SYNONYM_PLACEHOLDER, $argument);
           }
           $synonyms = synonyms_synonyms_find($condition, 'taxonomy_term', $bundle);
           if (!empty($synonyms)) {

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