Parcourir la source

updated and repatched search_api module
please check this thread : Decide on strategy for language aware search https://www.drupal.org/node/1393058

Bachir Soussi Chiadmi il y a 9 ans
Parent
commit
ca9413af4d
34 fichiers modifiés avec 1268 ajouts et 373 suppressions
  1. 28 0
      sites/all/modules/contrib/search/search_api/0001-re-added-own-boosts.patch
  2. 28 0
      sites/all/modules/contrib/search/search_api/0002-taxo-term-translation-bug-added-reference-to-bug-fix.patch
  3. 25 0
      sites/all/modules/contrib/search/search_api/0003-NODE_PUBLISED-in-previous-patches-i-commented-the-ne.patch
  4. 50 0
      sites/all/modules/contrib/search/search_api/0004-Icons.patch
  5. 67 0
      sites/all/modules/contrib/search/search_api/CHANGELOG.txt
  6. 0 39
      sites/all/modules/contrib/search/search_api/boosts-and-queryconditon.patch
  7. 105 47
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
  8. 16 5
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
  9. 3 3
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info
  10. 155 5
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module
  11. 7 1
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc
  12. 22 16
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc
  13. 15 8
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc
  14. 46 10
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
  15. 5 17
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc
  16. 16 1
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc
  17. 18 5
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
  18. 9 7
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
  19. 3 3
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info
  20. 33 6
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc
  21. 11 0
      sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
  22. 87 29
      sites/all/modules/contrib/search/search_api/includes/index_entity.inc
  23. 81 14
      sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
  24. 10 2
      sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc
  25. 54 3
      sites/all/modules/contrib/search/search_api/includes/query.inc
  26. 2 2
      sites/all/modules/contrib/search/search_api/includes/server_entity.inc
  27. 63 24
      sites/all/modules/contrib/search/search_api/search_api.admin.inc
  28. 1 1
      sites/all/modules/contrib/search/search_api/search_api.api.php
  29. 181 46
      sites/all/modules/contrib/search/search_api/search_api.drush.inc
  30. 3 3
      sites/all/modules/contrib/search/search_api/search_api.info
  31. 11 2
      sites/all/modules/contrib/search/search_api/search_api.install
  32. 106 71
      sites/all/modules/contrib/search/search_api/search_api.module
  33. 4 0
      sites/all/modules/contrib/search/search_api/search_api.rules.inc
  34. 3 3
      sites/all/modules/contrib/search/search_api/tests/search_api_test.info

+ 28 - 0
sites/all/modules/contrib/search/search_api/0001-re-added-own-boosts.patch

@@ -0,0 +1,28 @@
+From 6402fc7ab8f6343defbee7111dee7dd16a5082fc Mon Sep 17 00:00:00 2001
+From: Bachir Soussi Chiadmi <bachir@g-u-i.net>
+Date: Fri, 7 Feb 2014 10:10:15 +0100
+Subject: [PATCH 1/4] re-added own boosts
+
+---
+ search_api.admin.inc | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/search_api.admin.inc b/search_api.admin.inc
+index f4210f4..b2269d6 100644
+--- a/search_api.admin.inc
++++ b/search_api.admin.inc
+@@ -1658,8 +1658,9 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
+   // An array of option arrays for types, keyed by nesting level.
+   $types = array(0 => search_api_field_types());
+   $entity_types = entity_get_info();
+-  $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
+-
++  //$boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
++  $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
++   
+   $fulltext_types = array(0 => array('text'));
+   // Add all custom data types with fallback "text" to fulltext types as well.
+   foreach (search_api_get_data_type_info() as $id => $type) {
+-- 
+2.3.5
+

+ 28 - 0
sites/all/modules/contrib/search/search_api/0002-taxo-term-translation-bug-added-reference-to-bug-fix.patch

@@ -0,0 +1,28 @@
+From 54ee5c7b3a05850e15067d77a182cb8fe723d8e0 Mon Sep 17 00:00:00 2001
+From: Bachir Soussi Chiadmi <bachir@g-u-i.net>
+Date: Fri, 7 Feb 2014 10:29:08 +0100
+Subject: [PATCH 2/4] taxo term translation bug : added reference to bug fix in
+ comment
+
+---
+ search_api.module | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/search_api.module b/search_api.module
+index 000cadb..55bd54a 100644
+--- a/search_api.module
++++ b/search_api.module
+@@ -2221,6 +2221,10 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
+   foreach ($nested as $prefix => $nested_fields) {
+     if (isset($wrapper->$prefix)) {
+       $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options);
++      # http://drupal.org/node/1873910#comment-6876200
++      // $subwrapper = $wrapper->$prefix;
++      // $subwrapper->language( $wrapper->language->value() );
++      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);  
+       foreach ($nested_fields as $field => $info) {
+         $fields["$prefix:$field"] = $info;
+       }
+-- 
+2.3.5
+

+ 25 - 0
sites/all/modules/contrib/search/search_api/0003-NODE_PUBLISED-in-previous-patches-i-commented-the-ne.patch

@@ -0,0 +1,25 @@
+From 85251183d6ad5fadaa65154e33e1e8ac8ca7f9b0 Mon Sep 17 00:00:00 2001
+From: Bachir Soussi Chiadmi <bachir@g-u-i.net>
+Date: Fri, 7 Feb 2014 10:32:26 +0100
+Subject: [PATCH 3/4] NODE_PUBLISED : in previous patches i commented the next
+ line, why ? maybe will have to do it again
+
+---
+ search_api.module | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/search_api.module b/search_api.module
+index 55bd54a..17f611a 100644
+--- a/search_api.module
++++ b/search_api.module
+@@ -1994,6 +1994,7 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
+     $query->filter($filter);
+   }
+   else {
++    // /!\ in previous patches i commented the next line, why ? maybe will have to do it again
+     $query->condition('status', $published);
+   }
+ 
+-- 
+2.3.5
+

+ 50 - 0
sites/all/modules/contrib/search/search_api/0004-Icons.patch

@@ -0,0 +1,50 @@
+From c06be9a44ed0be31859a1800cf7f0ae6e8ae492a Mon Sep 17 00:00:00 2001
+From: Bachir Soussi Chiadmi <bachir@g-u-i.net>
+Date: Fri, 21 Feb 2014 19:49:45 +0100
+Subject: [PATCH 4/4] Icons re-added icons, why they didn't be here, i don't
+ know ...
+
+---
+ disabled.png | Bin 0 -> 384 bytes
+ enabled.png  | Bin 0 -> 383 bytes
+ 2 files changed, 0 insertions(+), 0 deletions(-)
+ create mode 100644 disabled.png
+ create mode 100644 enabled.png
+
+diff --git a/disabled.png b/disabled.png
+new file mode 100644
+index 0000000000000000000000000000000000000000..224776502046765ef7c083ffa3229fd206b9c975
+GIT binary patch
+literal 384
+zcmV-`0e}99P)<h;3K|Lk000e1NJLTq000aC000aK1^@s6R&`wG0000PbVXQnQ*UN;
+zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzB1uF+RCwBik-tg<K@i4gmNYiGco6Uz
+z1Rp_+BwY$iC2}^tfuaNqN)E0PY=wY@f3Z~1&O-&?K=37m)3r_F_{}jFE*3iQyZL7J
+zn_c#nMT9h-L*7E#2LVlo2k}xSM_RBBJcfJ*9ns%$zMRR1dkA@F3^O3`V!2Gwi`45z
+zLR~;$(8^>Hxo5S~v);h!F5lHi?8tY}Y=6k>{VeZk13H0TfJ{L>zr#&187PJtE1&YF
+z#chq}k)8^(h8y8iq7K@{qH60+Je8qL{fT(Z%i(p9?@Xp=Ap7MLyiFg&aBu-LbgHOE
+zFV;2lcsCYG0D;xhDo4mEm@`uJI=W__B!9S*DqmuQ&OZ-#wfQCMPL+ypp>5y+{X%=Y
+e>QV2H00RI@_ptZRUTXUQ0000<MNUMnLSTXuN2F!|
+
+literal 0
+HcmV?d00001
+
+diff --git a/enabled.png b/enabled.png
+new file mode 100644
+index 0000000000000000000000000000000000000000..95f8730e6955f1de7d244817db5ed7678bce0f72
+GIT binary patch
+literal 383
+zcmV-_0f7FAP)<h;3K|Lk000e1NJLTq000aC000aK1^@s6R&`wG0000PbVXQnQ*UN;
+zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzAxT6*RCwBA{Qv(y0|*Fj@h<{WbwJF|
+zfC@eWanoC$jeQ^vBS?eLCf`Lsb}R#au=t(d<~T-yb)Ka_P8S1lpp21kmFrs|LkN$e
+zvp{SB#LPhaj_LoOKSDsv0L0&c_%)Ob!<&HE3Wy_s_%BE;(?6gD5Pt+>Hz0Nf;@42I
+zO+Xy_DRSR0DE}{rX8QO0Hv<r}0WtHJ*h80rv@;M-2jWm5{}<Oh%P1gw1yl_KBl}E~
+z|4_Gn2V&13X{Qgu9M3V!GyzD>fw~_IKsJ1Y+QJFM+u5cX*n=d1bS#iR2V^r;9)v$K
+zvP{rM4_1&(vw%1sYp{YMj=5K3DUcIIAP$!OExr-W1Y&_0|Ns6i2I7xE%z%bLVr3vT
+dAhiGi1_1Nf-q(XP%8~#8002ovPDHLkV1gxRmaYH*
+
+literal 0
+HcmV?d00001
+
+-- 
+2.3.5
+

+ 67 - 0
sites/all/modules/contrib/search/search_api/CHANGELOG.txt

@@ -1,3 +1,70 @@
+Search API 1.14 (12/26/2014):
+-----------------------------
+- #2382385 by illusionuk, drunken monkey: Fixed error handling when using
+  invalid fulltext or sort field in Views.
+- #2371099 by drunken monkey: Fixed display of active "Exclude" facets.
+- #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexing on multiple
+  indexes with Drush.
+- #2347367 by drunken monkey, das-peter: Fixed forgotten usages of
+  $index->item_type.
+- #2359201 by drunken monkey: Added a "List" option to "Aggregated fields".
+- #2364247 by drunken monkey: Fixed documentation for
+  SearchApiQueryFilterInterface::getFilters().
+- #2364875 by Xano: Fixed Views argument handler for fulltext fields.
+- #2174163 by drunken monkey: Fixed detection of field type changes by data
+  alterations.
+- #2305755 by drunken monkey, pfrenssen: Fixed invalidation of the stored index
+  fields cache.
+- #2334727 by Alex Bukach, drunken monkey: Fixed Views caching does not take
+  items_per_page into account.
+- #1372092 by drunken monkey: Added an error message when no service class is
+  available when creating a server.
+- #2305627 by drunken monkey, cpliakas: Fixed date facets not displayed when
+  the configured granularity is larger than the calculated granularity.
+- #2319263 by solotandem: Added easier way to subclass entity classes.
+- #2278737 by drunken monkey: Fixed use of multiple Views fulltext search
+  filters.
+
+Search API 1.13 (07/23/2014):
+-----------------------------
+- #2281535 by areynolds, nicola85: Adapted to latest changes in Views cache
+  plugins.
+- #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) in Views.
+- #2146435 by alanmackenzie: Fixed Views paging with custom pager add-ons.
+- #2278791 by drunken monkey | tksmd: Fixed excerpt when searching single CJK
+  word.
+- #2272983 by idflood, drunken monkey: Fixed Highlighting processor for queries
+  without returned results.
+- #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed array to string
+  conversion in Highlighting processor.
+
+Search API 1.12 (05/23/2014):
+-----------------------------
+- #2265349 by drunken monkey: Marked _search_api_settings_equals() as
+  deprecated.
+- #2256891 by justanothermark: Fixed "0" entity labels.
+- #2233749 by rjacobs, drunken monkey: Added drush support to change the server
+  used by an index.
+- #2219553 by drunken monkey: Fixed Views fulltext filter operators.
+- #2135697 by drunken monkey: Fixed handling of HTML attributes in the
+  Highlighting processor.
+- #2179755 by drunken monkey, fago: Fixed whitespaces after HTML filter.
+- #2204847 by drunken monkey, alanmackenzie: Fixed Views caching issues with
+  pagination.
+- #2198791 by drunken monkey: Fixed empty Views entity filters.
+- #2195469 by freakalis, drunken monkey: Added "Exclude fields" options to
+  Highlighting processor.
+- #2169455 by drunken monkey: Fixed "undefined index" in
+  search_api_update_7116().
+- #2219563 by drunken monkey: Added __toString() methods for queries and
+  filters.
+- #1888174 by drunken monkey, ipallian: Fixed problems with date facets.
+- #2187487 by drunken monkey: Fixed admin summary of language filter.
+- #2198261 by drunken monkey: Fixed fatal error on view editing.
+- #2168713 by idebr: Fixed highlighting of keys containing slashes.
+- #2150779 by hefox: Fixed "Overridden" detection for index features.
+- #1227702 by drunken monkey: Improved error handling.
+
 Search API 1.11 (12/25/2013):
 -----------------------------
 - #1879196 by drunken monkey: Fixed invalid old indexes causing errors.

+ 0 - 39
sites/all/modules/contrib/search/search_api/boosts-and-queryconditon.patch

@@ -1,39 +0,0 @@
-diff --git a/search_api.admin.inc b/search_api.admin.inc
-index 5fbc8d8..9a5122e 100644
---- a/search_api.admin.inc
-+++ b/search_api.admin.inc
-@@ -1480,8 +1480,8 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
-   $fulltext_type = array(0 => 'text');
-   $entity_types = entity_get_info();
-   $default_types = search_api_default_field_types();
--  $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
--
-+  // $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '5000', '10000', '20000', '40000', '80000', '160000', '320000'));
-+  $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
-   $form_state['index'] = $index;
-   $form['#theme'] = 'search_api_admin_fields_table';
-   $form['#tree'] = TRUE;
-diff --git a/search_api.module b/search_api.module
-index bba0681..ba27465 100644
---- a/search_api.module
-+++ b/search_api.module
-@@ -1444,7 +1444,7 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
-       $query->filter($filter);
-     }
-     else {
--      $query->condition('status', NODE_PUBLISHED);
-+      // $query->condition('status', NODE_PUBLISHED);
-     }
-     // Filter by node access grants.
-     $filter = $query->createFilter('OR');
-@@ -1636,6 +1636,10 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
-   foreach ($nested as $prefix => $nested_fields) {
-     if (isset($wrapper->$prefix)) {
-       $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options);
-+      # http://drupal.org/node/1873910#comment-6876200
-+      // $subwrapper = $wrapper->$prefix;
-+      // $subwrapper->language( $wrapper->language->value() );
-+      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);
-       foreach ($nested_fields as $field => $info) {
-         $fields["$prefix:$field"] = $info;
-       }

+ 105 - 47
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc

@@ -57,14 +57,94 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
     if ($active = $this->adapter->getActiveItems($this->facet)) {
       $item = end($active);
       $field = $this->facet['field'];
-      $regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE);
-      $filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']);
+      $filter = $this->createRangeFilter($item['value']);
       $this->addFacetFilter($query, $field, $filter);
     }
   }
 
+  /**
+   * Rewrites the handler-specific date range syntax to the normal facet syntax.
+   *
+   * @param $value
+   *   The user-facing facet value.
+   *
+   * @return string
+   *   A facet to add as a filter, in the format used internally in this module.
+   */
+  protected function createRangeFilter($value) {
+    // Gets the granularity. Ignore any filters passed directly from the server
+    // (range or missing). We always create filters starting with a year.
+    if (!$value || !ctype_digit($value[0])) {
+      return $value;
+    }
+
+    $parts = explode('-', $value);
+    $date = new DateTime();
+    switch (count($parts)) {
+      case 1:
+        $date->setDate($parts[0], 1, 1);
+        $date->setTime(0, 0, 0);
+        $lower = $date->format('U');
+        $date->setDate($parts[0] + 1, 1, 1);
+        $date->setTime(0, 0, -1);
+        $upper = $date->format('U');
+        break;
+
+      case 2:
+        // Luckily, $month = 13 is treated as January of next year. (The same
+        // goes for all other parameters.) We use the inverse trick for the
+        // seconds of the upper bound, since that's inclusive and we want to
+        // stop at a second before the next segment starts.
+        $date->setDate($parts[0], $parts[1], 1);
+        $date->setTime(0, 0, 0);
+        $lower = $date->format('U');
+        $date->setDate($parts[0], $parts[1] + 1, 1);
+        $date->setTime(0, 0, -1);
+        $upper = $date->format('U');
+        break;
+
+      case 3:
+        $date->setDate($parts[0], $parts[1], $parts[2]);
+        $date->setTime(0, 0, 0);
+        $lower = $date->format('U');
+        $date->setDate($parts[0], $parts[1], $parts[2] + 1);
+        $date->setTime(0, 0, -1);
+        $upper = $date->format('U');
+        break;
+
+      case 4:
+        $date->setDate($parts[0], $parts[1], $parts[2]);
+        $date->setTime($parts[3], 0, 0);
+        $lower = $date->format('U');
+        $date->setTime($parts[3] + 1, 0, -1);
+        $upper = $date->format('U');
+        break;
+
+      case 5:
+        $date->setDate($parts[0], $parts[1], $parts[2]);
+        $date->setTime($parts[3], $parts[4], 0);
+        $lower = $date->format('U');
+        $date->setTime($parts[3], $parts[4] + 1, -1);
+        $upper = $date->format('U');
+        break;
+
+      case 6:
+        $date->setDate($parts[0], $parts[1], $parts[2]);
+        $date->setTime($parts[3], $parts[4], $parts[5]);
+        return $date->format('U');
+
+      default:
+        return $value;
+    }
+
+    return "[$lower TO $upper]";
+  }
+
   /**
    * Replacement callback for replacing ISO dates with timestamps.
+   *
+   * Not used anymore, but kept for backwards compatibility with potential
+   * subclasses.
    */
   public function replaceDateString($matches) {
     return strtotime($matches[0]);
@@ -86,15 +166,9 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
     $build = array();
     $search = search_api_current_search($search_id);
     $results = $search[1];
-    if (!$results['result count']) {
-      return array();
-    }
     // Gets total number of documents matched in search.
     $total = $results['result count'];
 
-    // Most of the code below is copied from search_facetapi's implementation of
-    // this method.
-
     // Executes query, iterates over results.
     if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
       $values = $results['search_api_facets'][$this->facet['name']];
@@ -113,13 +187,6 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
             }
           }
           else {
-            $filter = substr($value['filter'], 1, -1);
-            $pos = strpos($filter, ' ');
-            if ($pos !== FALSE) {
-              $lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY);
-              $upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY);
-              $filter = '[' . $lower . ' TO ' . $upper . ']';
-            }
             $build[$filter]['#count'] = $value['count'];
           }
         }
@@ -128,23 +195,28 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
 
     // Get the finest level of detail we're allowed to drill down to.
     $settings = $facet->getSettings()->settings;
-    $granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
+    $max_granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
 
     // Gets active facets, starts building hierarchy.
-    $parent = $gap = NULL;
+    $parent = $granularity = NULL;
     $active_items = $this->adapter->getActiveItems($this->facet);
     foreach ($active_items as $value => $item) {
       // If the item is active, the count is the result set count.
       $build[$value] = array('#count' => $total);
 
-      // Gets next "gap" increment.
-      if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
+      // Gets next "gap" increment. Ignore any filters passed directly from the
+      // server (range or missing). We always create filters starting with a
+      // year.
+      $value = "$value";
+      if (!$value || !ctype_digit($value[0])) {
+        continue;
+      }
+
+      $granularity = search_api_facetapi_date_get_granularity($value);
+      if (!$granularity) {
         continue;
       }
-      $start = substr($value, 1, $pos);
-      $end = substr($value, $pos + 4, -1);
-      $date_gap = facetapi_get_date_gap($start, $end);
-      $gap = facetapi_get_next_date_gap($date_gap, $granularity);
+      $granularity = facetapi_get_next_date_gap($granularity, $max_granularity);
 
       // If there is a previous item, there is a parent, uses a reference so the
       // arrays are populated when they are updated.
@@ -156,6 +228,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
       // Stores the last value iterated over.
       $parent = $value;
     }
+
     if (empty($raw_values)) {
       return $build;
     }
@@ -165,7 +238,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
     $timestamps = array_keys($raw_values);
     if (NULL === $parent) {
       if (count($raw_values) > 1) {
-        $gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
+        $granularity = facetapi_get_timestamp_gap(min($timestamps), max($timestamps), $max_granularity);
         // Array of numbers used to determine whether the next gap is smaller than
         // the minimum gap allowed in the drilldown.
         $gap_numbers = array(
@@ -178,36 +251,20 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
         );
         // Gets gap numbers for both the gap and minimum gap, checks if the gap
         // is within the limit set by the $granularity parameter.
-        if ($gap_numbers[$gap] < $gap_numbers[$granularity]) {
-          $gap = $granularity;
+        if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) {
+          $granularity = $max_granularity;
         }
       }
       else {
-        $gap = $granularity;
-      }
-    }
-
-    // Converts all timestamps to dates in ISO 8601 format.
-    $dates = array();
-    foreach ($timestamps as $timestamp) {
-      $dates[$timestamp] = facetapi_isodate($timestamp, $gap);
-    }
-
-    // Treat each date as the range start and next date as the range end.
-    $range_end = array();
-    $previous = NULL;
-    foreach (array_unique($dates) as $date) {
-      if (NULL !== $previous) {
-        $range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
+        $granularity = $max_granularity;
       }
-      $previous = $date;
     }
-    $range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
 
-    // Groups dates by the range they belong to, builds the $build array
-    // with the facet counts and formatted range values.
+    // Groups dates by the range they belong to, builds the $build array with
+    // the facet counts and formatted range values.
+    $format = search_api_facetapi_date_get_granularity_format($granularity);
     foreach ($raw_values as $value => $count) {
-      $new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
+      $new_value = date($format, $value);
       if (!isset($build[$new_value])) {
         $build[$new_value] = array('#count' => $count);
       }
@@ -226,4 +283,5 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
 
     return $build;
   }
+
 }

+ 16 - 5
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc

@@ -30,7 +30,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
     // Return terms for this facet.
     $this->adapter->addFacet($this->facet, $query);
 
-    $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;
+    $settings = $this->getSettings()->settings;
 
     // First check if the facet is enabled for this search.
     $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
@@ -56,7 +56,12 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
       $conjunction = 'OR';
     }
     else {
-      throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator)));
+      $vars = array(
+        '%operator' => $operator,
+        '%facet' => !empty($this->facet['label']) ? $this->facet['label'] : $this->facet['name'],
+      );
+      watchdog('search_api_facetapi', 'Unknown facet operator %operator used for facet %facet.', $vars, WATCHDOG_WARNING);
+      return;
     }
     $tags = array('facet:' . $this->facet['field']);
     $facet_filter = $query->createFilter($conjunction, $tags);
@@ -77,7 +82,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
     // Test if this filter should be negated.
     $settings = $this->adapter->getFacet($this->facet)->getSettings();
     $exclude = !empty($settings->settings['exclude']);
-    // Integer (or other nun-string) filters might mess up some of the following
+    // Integer (or other non-string) filters might mess up some of the following
     // comparison expressions.
     $filter = (string) $filter;
     if ($filter == '!') {
@@ -143,9 +148,15 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
       return array();
     }
     $search_id = $search_ids[$facet['name']];
-    $search = search_api_current_search($search_id);
+    list(, $results) = search_api_current_search($search_id);
     $build = array();
-    $results = $search[1];
+
+    // Always include the active facet items.
+    foreach ($this->adapter->getActiveItems($this->facet) as $filter)  {
+      $build[$filter['value']]['#count'] = $results['result count'];
+    }
+
+    // Then, add the facets returned by the server.
     if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
       $values = $results['search_api_facets'][$this->facet['name']];
       foreach ($values as $value) {

+ 3 - 3
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info

@@ -9,9 +9,9 @@ files[] = plugins/facetapi/adapter.inc
 files[] = plugins/facetapi/query_type_term.inc
 files[] = plugins/facetapi/query_type_date.inc
 
-; Information added by Drupal.org packaging script on 2013-12-25
-version = "7.x-1.11"
+; Information added by Drupal.org packaging script on 2014-12-26
+version = "7.x-1.14"
 core = "7.x"
 project = "search_api"
-datestamp = "1387965506"
+datestamp = "1419580682"
 

+ 155 - 5
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module

@@ -53,7 +53,7 @@ function search_api_facetapi_facetapi_searcher_info() {
   $info = array();
   $indexes = search_api_index_load_multiple(FALSE);
   foreach ($indexes as $index) {
-    if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
+    if (_search_api_facetapi_index_support_feature($index)) {
       $searcher_name = 'search_api@' . $index->machine_name;
       $info[$searcher_name] = array(
         'label' => t('Search service: @name', array('@name' => $index->name)),
@@ -97,7 +97,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
         'date' => array(
           'query type' => 'date',
           'map options' => array(
-            'map callback' => 'facetapi_map_date',
+            'map callback' => 'search_api_facetapi_map_date',
           ),
         ),
       );
@@ -116,7 +116,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
           'description' => t('Filter by @type.', array('@type' => $field['name'])),
           'allowed operators' => array(
             FACETAPI_OPERATOR_AND => TRUE,
-            FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
+            FACETAPI_OPERATOR_OR => _search_api_facetapi_index_support_feature($index, 'search_api_facets_operator_or'),
           ),
           'dependency plugins' => array('role'),
           'facet missing allowed' => TRUE,
@@ -218,7 +218,7 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
   if (!$index->enabled) {
     return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
   }
-  if (!$index->server()->supportsFeature('search_api_facets')) {
+  if (!_search_api_facetapi_index_support_feature($index)) {
     return array('#markup' => t('This index uses a server that does not support facet functionality.'));
   }
   $searcher_name = 'search_api@' . $index->machine_name;
@@ -226,6 +226,28 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
   return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
 }
 
+/**
+ * Checks whether a certain feature is supported for an index.
+ *
+ * @param SearchApiIndex $index
+ *   The search index which should be checked.
+ * @param string $feature
+ *   (optional) The feature to check for. Defaults to "search_api_facets".
+ *
+ * @return bool
+ *   TRUE if the feature is supported by the index's server (and the index is
+ *   currently enabled), FALSE otherwise.
+ */
+function _search_api_facetapi_index_support_feature(SearchApiIndex $index, $feature = 'search_api_facets') {
+  try {
+    $server = $index->server();
+    return $server && $server->supportsFeature($feature);
+  }
+  catch (SearchApiException $e) {
+    return FALSE;
+  }
+}
+
 /**
  * Gets hierarchy information for taxonomy terms.
  *
@@ -366,7 +388,7 @@ function _search_api_facetapi_facet_create_label(array $values, array $options)
     $entities = entity_load($type, $values);
     foreach ($entities as $id => $entity) {
       $label = entity_label($type, $entity);
-      if ($label) {
+      if ($label !== FALSE) {
         $map[$id] = $label;
       }
     }
@@ -432,3 +454,131 @@ function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_
   $cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
   cache_clear_all($cid, 'cache', TRUE);
 }
+
+/**
+ * Computes the granularity of a date facet filter.
+ *
+ * @param $filter
+ *   The filter value to examine.
+ *
+ * @return string|null
+ *   Either one of the FACETAPI_DATE_* constants corresponding to the
+ *   granularity of the filter, or NULL if it couldn't be computed.
+ */
+function search_api_facetapi_date_get_granularity($filter) {
+  // Granularity corresponds to number of dashes in filter value.
+  $units = array(
+    FACETAPI_DATE_YEAR,
+    FACETAPI_DATE_MONTH,
+    FACETAPI_DATE_DAY,
+    FACETAPI_DATE_HOUR,
+    FACETAPI_DATE_MINUTE,
+    FACETAPI_DATE_SECOND,
+  );
+  $count = substr_count($filter, '-');
+  return isset($units[$count]) ? $units[$count] : NULL;
+}
+
+/**
+ * Returns the date format used for a given granularity.
+ *
+ * @param $granularity
+ *   One of the FACETAPI_DATE_* constants.
+ *
+ * @return string
+ *   The date format used for the given granularity.
+ */
+function search_api_facetapi_date_get_granularity_format($granularity) {
+  $formats = array(
+    FACETAPI_DATE_YEAR => 'Y',
+    FACETAPI_DATE_MONTH => 'Y-m',
+    FACETAPI_DATE_DAY => 'Y-m-d',
+    FACETAPI_DATE_HOUR => 'Y-m-d-H',
+    FACETAPI_DATE_MINUTE => 'Y-m-d-H-i',
+    FACETAPI_DATE_SECOND => 'Y-m-d-H-i-s',
+  );
+  return $formats[$granularity];
+}
+
+/**
+ * Constructs labels for date facet filter values.
+ *
+ * @param array $values
+ *   The date facet filter values, as used in URL parameters.
+ * @param array $options
+ *   (optional) Options for creating the mapping. The following options are
+ *   recognized:
+ *   - format callback: A callback for creating a label for a timestamp. The
+ *     function signature is like search_api_facetapi_format_timestamp(),
+ *     receiving a timestamp and one of the FACETAPI_DATE_* constants as the
+ *     parameters and returning a human-readable label.
+ *
+ * @return array
+ *   An array of labels for the given facet filters.
+ */
+function search_api_facetapi_map_date(array $values, array $options = array()) {
+  $map = array();
+  foreach ($values as $value) {
+    // Ignore any filters passed directly from the server (range or missing). We
+    // always create filters starting with a year.
+    $value = "$value";
+    if (!$value || !ctype_digit($value[0])) {
+      continue;
+    }
+
+    // Get the granularity of the filter.
+    $granularity = search_api_facetapi_date_get_granularity($value);
+    if (!$granularity) {
+      continue;
+    }
+
+    // For years, the URL value is already the label.
+    if ($granularity == FACETAPI_DATE_YEAR) {
+      $map[$value] = $value;
+      continue;
+    }
+
+    // Otherwise, parse the timestamp from the known format and format it as a
+    // label.
+    $format = search_api_facetapi_date_get_granularity_format($granularity);
+    $date = DateTime::createFromFormat($format, $value);
+    if (!$date) {
+      continue;
+    }
+    $format_callback = 'search_api_facetapi_format_timestamp';
+    if (!empty($options['format callback']) && is_callable($options['format callback'])) {
+      $format_callback = $options['format callback'];
+    }
+    $map[$value] = call_user_func($format_callback, $date->format('U'), $granularity);
+  }
+  return $map;
+}
+
+/**
+ * Format a date according to the default timezone and the given precision.
+ *
+ * @param int $timestamp
+ *   An integer containing the Unix timestamp being formated.
+ * @param string $precision
+ *   A string containing the formatting precision. See the FACETAPI_DATE_*
+ *   constants for valid values.
+ *
+ * @return string
+ *   A human-readable representation of the timestamp.
+ */
+function search_api_facetapi_format_timestamp($timestamp, $precision = FACETAPI_DATE_YEAR) {
+  $formats = array(
+    FACETAPI_DATE_YEAR => 'Y',
+    FACETAPI_DATE_MONTH => 'F Y',
+    FACETAPI_DATE_DAY => 'F j, Y',
+    FACETAPI_DATE_HOUR => 'H:__',
+    FACETAPI_DATE_MINUTE => 'H:i',
+    FACETAPI_DATE_SECOND => 'H:i:s',
+  );
+
+  if (!isset($formats[$precision])) {
+    $precision = FACETAPI_DATE_YEAR;
+  }
+
+  return format_date($timestamp, 'custom', $formats[$precision]);
+}

+ 7 - 1
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc

@@ -66,7 +66,13 @@ class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumen
    */
   public function query($group_by = FALSE) {
     if ($this->options['fields']) {
-      $this->query->fields($this->options['fields']);
+      try {
+        $this->query->fields($this->options['fields']);
+      }
+      catch (SearchApiException $e) {
+        $this->query->abort($e->getMessage());
+        return;
+      }
     }
     if ($this->options['conjunction'] != 'AND') {
       $this->query->setOption('conjunction', $this->options['conjunction']);

+ 22 - 16
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc

@@ -62,24 +62,30 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
    * The argument sent may be found at $this->argument.
    */
   public function query($group_by = FALSE) {
-    $server = $this->query->getIndex()->server();
-    if (!$server->supportsFeature('search_api_mlt')) {
-      $class = search_api_get_service_info($server->class);
-      watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
+    try {
+      $server = $this->query->getIndex()->server();
+      if (!$server->supportsFeature('search_api_mlt')) {
+        $class = search_api_get_service_info($server->class);
+        watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
           array('@class' => $class['name']), WATCHDOG_ERROR);
-      $this->query->abort();
-      return;
-    }
-    $fields = $this->options['fields'] ? $this->options['fields'] : array();
-    if (empty($fields)) {
-      foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
-        $fields[] = $key;
+        $this->query->abort();
+        return;
+      }
+      $fields = $this->options['fields'] ? $this->options['fields'] : array();
+      if (empty($fields)) {
+        foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
+          $fields[] = $key;
+        }
       }
+      $mlt = array(
+        'id' => $this->argument,
+        'fields' => $fields,
+      );
+      $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
+    }
+    catch (SearchApiException $e) {
+      $this->query->abort($e->getMessage());
     }
-    $mlt = array(
-      'id' => $this->argument,
-      'fields' => $fields,
-    );
-    $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
   }
+
 }

+ 15 - 8
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc

@@ -90,7 +90,8 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi
 
     // Set the correct default value in case the admin-set value is used (and a
     // value is present). The value is used if the form is either not exposed,
-    // or the exposed form wasn't submitted yet (there is
+    // or the exposed form wasn't submitted yet. (There doesn't seem to be an
+    // easier way to check for that.)
     if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) {
       $form['value']['#default_value'] = $this->ids_to_strings($this->value);
     }
@@ -102,11 +103,13 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi
   public function value_validate($form, &$form_state) {
     if (!empty($form['value'])) {
       $value = &$form_state['values']['options']['value'];
-      $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
-      $ids = $this->validate_entity_strings($form['value'], $values);
+      if (strlen($value)) {
+        $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
+        $ids = $this->validate_entity_strings($form['value'], $values);
 
-      if ($ids) {
-        $value = $ids;
+        if ($ids) {
+          $value = $ids;
+        }
       }
     }
   }
@@ -135,6 +138,7 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi
       return;
     }
 
+    $this->validated_exposed_input = FALSE;
     $identifier = $this->options['expose']['identifier'];
     $input = $form_state['values'][$identifier];
 
@@ -143,14 +147,14 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi
       $input = $this->options['group_info']['group_items'][$input]['value'];
     }
 
+    if (!strlen($input)) {
+      return;
+    }
     $values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input);
 
     if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) {
       $this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values);
     }
-    else {
-      $this->validated_exposed_input = FALSE;
-    }
   }
 
   /**
@@ -175,6 +179,9 @@ abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFi
    * {@inheritdoc}
    */
   public function admin_summary() {
+    if (!is_array($this->value)) {
+      $this->value = $this->value ? array($this->value) : array();
+    }
     $value = $this->value;
     $this->value = empty($value) ? '' : $this->ids_to_strings($value);
     $ret = parent::admin_summary();

+ 46 - 10
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc

@@ -156,8 +156,9 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
 
     if ($filter) {
       $filter = $this->query->createFilter('OR');
+      $op = $this->operator === 'NOT' ? '<>' : '=';
       foreach ($fields as $field) {
-        $filter->condition($field, $this->value, $this->operator);
+        $filter->condition($field, $this->value, $op);
       }
       $this->query->filter($filter);
       return;
@@ -166,11 +167,18 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
     // If the operator was set to OR or NOT, set OR as the conjunction. (It is
     // also set for NOT since otherwise it would be "not all of these words".)
     if ($this->operator != 'AND') {
-      $this->query->setOption('conjunction', $this->operator);
+      $this->query->setOption('conjunction', 'OR');
     }
 
-    $this->query->fields($fields);
-    $old = $this->query->getOriginalKeys();
+    try {
+      $this->query->fields($fields);
+    }
+    catch (SearchApiException $e) {
+      $this->query->abort($e->getMessage());
+      return;
+    }
+    $old = $this->query->getKeys();
+    $old_original = $this->query->getOriginalKeys();
     $this->query->keys($this->value);
     if ($this->operator == 'NOT') {
       $keys = &$this->query->getKeys();
@@ -181,16 +189,44 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
         // We can't know how negation is expressed in the server's syntax.
       }
     }
+
+    // If there were fulltext keys set, we take care to combine them in a
+    // meaningful way (especially with negated keys).
     if ($old) {
       $keys = &$this->query->getKeys();
+      // Array-valued keys are combined.
       if (is_array($keys)) {
-        $keys[] = $old;
-      }
-      elseif (is_array($old)) {
-        // We don't support such nonsense.
+        // If the old keys weren't parsed into an array, we instead have to
+        // combine the original keys.
+        if (is_scalar($old)) {
+          $keys = "($old) ({$this->value})";
+        }
+        else {
+          // If the conjunction or negation settings aren't the same, we have to
+          // nest both old and new keys array.
+          if (!empty($keys['#negation']) != !empty($old['#negation']) || $keys['#conjunction'] != $old['#conjunction']) {
+            $keys = array(
+              '#conjunction' => 'AND',
+              $old,
+              $keys,
+            );
+          }
+          // Otherwise, just add all individual words from the old keys to the
+          // new ones.
+          else {
+            foreach (element_children($old) as $i) {
+              $keys[] = $old[$i];
+            }
+          }
+        }
       }
-      else {
-        $keys = "($old) ($keys)";
+      // If the parse mode was "direct" for both old and new keys, we
+      // concatenate them and set them both via method and reference (to also
+      // update the originalKeys property.
+      elseif (is_scalar($old_original)) {
+        $combined_keys = "($old_original) ($keys)";
+        $this->query->keys($combined_keys);
+        $keys = $combined_keys;
       }
     }
   }

+ 5 - 17
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc

@@ -14,26 +14,14 @@
 class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {
 
   /**
-   * Provide a form for setting options.
+   * {@inheritdoc}
    */
-  public function value_form(&$form, &$form_state) {
-    parent::value_form($form, $form_state);
-    $form['value']['#options'] = array(
+  protected function get_value_options() {
+    parent::get_value_options();
+    $this->value_options = array(
       'current' => t("Current user's language"),
       'default' => t('Default site language'),
-    ) + $form['value']['#options'];
-  }
-
-  /**
-   * Provides a summary of this filter's value for the admin UI.
-   */
-  public function admin_summary() {
-    $tmp = $this->definition['options'];
-    $this->definition['options']['current'] = t('current');
-    $this->definition['options']['default'] = t('default');
-    $ret = parent::admin_summary();
-    $this->definition['options'] = $tmp;
-    return $ret;
+    ) + $this->value_options;
   }
 
   /**

+ 16 - 1
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc

@@ -28,8 +28,23 @@ class SearchApiViewsHandlerSort extends views_handler_sort {
       unset($this->query->orderby);
       $sort = &$this->query->getSort();
       $sort = array();
+      unset($sort);
+    }
+
+    // If two of the same fields are used for sort, ignore the latter in order
+    // for the prior to take precedence. (Temporary workaround until
+    // https://www.drupal.org/node/2145547 is fixed in Views.)
+    $alreadySorted = $this->query->getSort();
+    if (is_array($alreadySorted) && isset($alreadySorted[$this->real_field])) {
+      return;
+    }
+
+    try {
+      $this->query->sort($this->real_field, $this->options['order']);
+    }
+    catch (SearchApiException $e) {
+      $this->query->abort($e->getMessage());
     }
-    $this->query->sort($this->real_field, $this->options['order']);
   }
 
 }

+ 18 - 5
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc

@@ -77,22 +77,24 @@ class SearchApiViewsCache extends views_plugin_cache_time {
   }
 
   /**
-   * Overrides views_plugin_cache::get_results_key().
+   * Overrides views_plugin_cache::get_cache_key().
    *
-   * Use the Search API query as the main source for the key.
+   * Use the Search API query as the main source for the key. Note that in
+   * Views < 3.8, this function does not exist.
    */
-  public function get_results_key() {
+  public function get_cache_key($key_data = array()) {
     global $user;
 
     if (!isset($this->_results_key)) {
       $query = $this->getSearchApiQuery();
       $query->preExecute();
-      $key_data = array(
+      $key_data += array(
         'query' => $query,
         'roles' => array_keys($user->roles),
         'super-user' => $user->uid == 1, // special caching for super user.
         'language' => $GLOBALS['language']->language,
         'base_url' => $GLOBALS['base_url'],
+        'offset' => $this->view->get_current_page() . '*' . $this->view->get_items_per_page() . '+' . $this->view->get_offset(),
       );
       // Not sure what gets passed in exposed_info, so better include it. All
       // other parameters used in the parent method are already reflected in the
@@ -100,8 +102,19 @@ class SearchApiViewsCache extends views_plugin_cache_time {
       if (isset($_GET['exposed_info'])) {
         $key_data['exposed_info'] = $_GET['exposed_info'];
       }
+    }
+    $key = md5(serialize($key_data));
+    return $key;
+  }
 
-      $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));
+  /**
+   * Overrides views_plugin_cache::get_results_key().
+   *
+   * This is unnecessary for Views >= 3.8.
+   */
+  public function get_results_key() {
+    if (!isset($this->_results_key)) {
+      $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . $this->get_cache_key();
     }
 
     return $this->_results_key;

+ 9 - 7
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc

@@ -169,7 +169,7 @@ class SearchApiViewsQuery extends views_plugin_query {
       '#default_value' => $this->options['search_api_bypass_access'],
     );
 
-    if (entity_get_info($this->index->item_type)) {
+    if ($this->index->getEntityType()) {
       $form['entity_access'] = array(
         '#type' => 'checkbox',
         '#title' => t('Additional access checks on result entities'),
@@ -342,7 +342,7 @@ class SearchApiViewsQuery extends views_plugin_query {
     catch (Exception $e) {
       $this->errors[] = $e->getMessage();
       // Recursion to get the same error behaviour as above.
-      return $this->execute($view);
+      $this->execute($view);
     }
   }
 
@@ -374,9 +374,9 @@ class SearchApiViewsQuery extends views_plugin_query {
     // First off, we try to gather as much field values as possible without
     // loading any items.
     foreach ($results as $id => $result) {
-      if (!empty($this->options['entity_access'])) {
-        $entity = entity_load($this->index->item_type, array($id));
-        if (!entity_access('view', $this->index->item_type, $entity[$id])) {
+      if (!empty($this->options['entity_access']) && ($entity_type = $this->index->getEntityType())) {
+        $entity = entity_load($entity_type, array($id));
+        if (!entity_access('view', $entity_type, $entity[$id])) {
           continue;
         }
       }
@@ -660,16 +660,18 @@ class SearchApiViewsQuery extends views_plugin_query {
     return $ret;
   }
 
-  public function getOption($name) {
+  public function getOption($name, $default = NULL) {
     if (!$this->errors) {
-      return $this->query->getOption($name);
+      return $this->query->getOption($name, $default);
     }
+    return $default;
   }
 
   public function setOption($name, $value) {
     if (!$this->errors) {
       return $this->query->setOption($name, $value);
     }
+    return NULL;
   }
 
   public function &getOptions() {

+ 3 - 3
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info

@@ -27,9 +27,9 @@ files[] = includes/handler_sort.inc
 files[] = includes/plugin_cache.inc
 files[] = includes/query.inc
 
-; Information added by Drupal.org packaging script on 2013-12-25
-version = "7.x-1.11"
+; Information added by Drupal.org packaging script on 2014-12-26
+version = "7.x-1.14"
 core = "7.x"
 project = "search_api"
-datestamp = "1387965506"
+datestamp = "1419580682"
 

+ 33 - 6
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc

@@ -134,8 +134,8 @@ function search_api_views_views_data() {
         if (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
           $field_id = ($pos = strrpos($key, ':')) ? substr($key, $pos + 1) : $key;
           $field_info = field_info_field($field_id);
-          if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
-            $vocabulary_fields[$field_info['settings']['allowed_values'][0]['vocabulary']][] = $key;
+          if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) {
+            $vocabulary_fields[$vocabulary][] = $key;
           }
           else {
             $vocabulary_fields[''][] = $key;
@@ -184,7 +184,7 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
   if ($inner_type == 'text') {
     $table[$id] += array(
       'argument' => array(
-        'handler' => 'SearchApiViewsHandlerArgument',
+        'handler' => 'SearchApiViewsHandlerArgumentString',
       ),
       'filter' => array(
         'handler' => 'SearchApiViewsHandlerFilterText',
@@ -209,7 +209,6 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
   }
   elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
     $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm';
-    $info = $wrapper->info();
     $field_info = field_info_field($info['name']);
     // For the "Parent terms" and "All parent terms" properties, we can
     // extrapolate the vocabulary from the parent in the selector. (E.g.,
@@ -221,8 +220,8 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
         $field_info = field_info_field($parts[count($parts) - 2]);
       }
     }
-    if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
-      $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary'];
+    if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) {
+      $table[$id]['filter']['vocabulary'] = $vocabulary;
     }
   }
   else {
@@ -293,3 +292,31 @@ function search_api_views_views_plugins() {
 
   return $ret;
 }
+
+/**
+ * Returns the vocabulary machine name of a term field.
+ *
+ * @param array|null $field_info
+ *   The field's field info array, or NULL if the field is not provided by the
+ *   Field API. See the return value of field_info_field().
+ *
+ * @return string|null
+ *   If the field contains taxonomy terms of a single vocabulary (which could be
+ *   determined), that vocabulary's machine name; NULL otherwise.
+ */
+function _search_api_views_get_field_vocabulary($field_info) {
+  // Test for "Term reference" fields.
+  if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
+    return $field_info['settings']['allowed_values'][0]['vocabulary'];
+  }
+  // Test for "Entity reference" fields.
+  elseif (isset($field_info['settings']['handler']) && $field_info['settings']['handler'] === 'base') {
+    if (!empty($field_info['settings']['handler_settings']['target_bundles'])) {
+      $bundles = $field_info['settings']['handler_settings']['target_bundles'];
+      if (count($bundles) == 1) {
+        return key($bundles);
+      }
+    }
+  }
+  return NULL;
+}

+ 11 - 0
sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc

@@ -193,6 +193,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
         return isset($a) ? min($a, $b) : $b;
       case 'first':
         return isset($a) ? $a : $b;
+      case 'list':
+        if (!isset($a)) {
+          $a = array();
+        }
+        $a[] = $b;
+        return $a;
     }
   }
 
@@ -261,6 +267,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
           'max' => t('Maximum'),
           'min' => t('Minimum'),
           'first' => t('First'),
+          'list' => t('List'),
         );
       case 'type':
         return array(
@@ -270,6 +277,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
           'max' => 'integer',
           'min' => 'integer',
           'first' => 'string',
+          'list' => 'list<string>',
         );
       case 'description':
         return array(
@@ -279,6 +287,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
           'max' => t('The Maximum aggregation computes the numerically largest contained field value.'),
           'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'),
           'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'),
+          'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
         );
     }
   }
@@ -289,6 +298,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
   public function formButtonSubmit(array $form, array &$form_state) {
     $button_name = $form_state['triggering_element']['#name'];
     if ($button_name == 'op') {
+      // Increment $i until the corresponding field is not set, then create the
+      // field with that number as suffix.
       for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
       }
       $this->options['fields']['search_api_aggregation_' . $i] = array(

+ 87 - 29
sites/all/modules/contrib/search/search_api/includes/index_entity.inc

@@ -172,20 +172,25 @@ class SearchApiIndex extends Entity {
   /**
    * Constructor as a helper to the parent constructor.
    */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'search_api_index');
+  public function __construct(array $values = array(), $entity_type = 'search_api_index') {
+    parent::__construct($values, $entity_type);
   }
 
   /**
    * Execute necessary tasks for a newly created index.
    */
   public function postCreate() {
-    if ($this->enabled) {
-      $this->queueItems();
+    try {
+      if ($server = $this->server()) {
+        // Tell the server about the new index.
+        $server->addIndex($this);
+        if ($this->enabled) {
+          $this->queueItems();
+        }
+      }
     }
-    if ($server = $this->server()) {
-      // Tell the server about the new index.
-      $server->addIndex($this);
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
     }
   }
 
@@ -193,8 +198,13 @@ class SearchApiIndex extends Entity {
    * Execute necessary tasks when the index is removed from the database.
    */
   public function postDelete() {
-    if ($server = $this->server()) {
-      $server->removeIndex($this);
+    try {
+      if ($server = $this->server()) {
+        $server->removeIndex($this);
+      }
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
     }
 
     // Stop tracking entities for indexing.
@@ -206,7 +216,12 @@ class SearchApiIndex extends Entity {
    */
   public function queueItems() {
     if (!$this->read_only) {
-      $this->datasource()->startTracking(array($this));
+      try {
+        $this->datasource()->startTracking(array($this));
+      }
+      catch (SearchApiException $e) {
+        watchdog_exception('search_api', $e);
+      }
     }
   }
 
@@ -214,7 +229,12 @@ class SearchApiIndex extends Entity {
    * Remove all records of entities to index.
    */
   public function dequeueItems() {
-    $this->datasource()->stopTracking(array($this));
+    try {
+      $this->datasource()->stopTracking(array($this));
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+    }
   }
 
   /**
@@ -231,16 +251,25 @@ class SearchApiIndex extends Entity {
     if (empty($this->description)) {
       $this->description = NULL;
     }
-    if (empty($this->server)) {
+    $server = FALSE;
+    if (!empty($this->server)) {
+      $server = search_api_server_load($this->server);
+      if (!$server) {
+        $vars['%server'] = $this->server;
+        $vars['%index'] = $this->name;
+        watchdog('search_api', 'Unknown server %server specified for index %index.', $vars, WATCHDOG_ERROR);
+      }
+    }
+    if (!$server) {
       $this->server = NULL;
       $this->enabled = FALSE;
     }
-    // This will also throw an exception if the server doesn't exist – which is good.
-    elseif (!$this->server(TRUE)->enabled) {
-      $this->enabled = FALSE;
-      $this->server = NULL;
+    if (!empty($this->options['fields'])) {
+      ksort($this->options['fields']);
     }
 
+    $this->resetCaches();
+
     return parent::save();
   }
 
@@ -305,7 +334,12 @@ class SearchApiIndex extends Entity {
       return TRUE;
     }
 
-    $this->server()->deleteItems('all', $this);
+    try {
+      $this->server()->deleteItems('all', $this);
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+    }
 
     _search_api_index_reindex($this);
     module_invoke_all('search_api_index_reindex', $this, TRUE);
@@ -350,7 +384,12 @@ class SearchApiIndex extends Entity {
    *   otherwise.
    */
   public function getEntityType() {
-    return $this->datasource()->getEntityType();
+    try {
+      return $this->datasource()->getEntityType();
+    }
+    catch (SearchApiException $e) {
+      return NULL;
+    }
   }
 
   /**
@@ -385,7 +424,7 @@ class SearchApiIndex extends Entity {
    *   SearchApiQueryInterface::__construct().
    *
    * @throws SearchApiException
-   *   If the index is currently disabled.
+   *   If the index is currently disabled or its server doesn't exist.
    *
    * @return SearchApiQueryInterface
    *   A query object for searching this index.
@@ -399,15 +438,20 @@ class SearchApiIndex extends Entity {
 
 
   /**
-   * Indexes items on this index. Will return an array of IDs of items that
-   * should be marked as indexed – i.e., items that were either rejected by a
-   * data-alter callback or were successfully indexed.
+   * Indexes items on this index.
+   *
+   * Will return an array of IDs of items that should be marked as indexed –
+   * i.e., items that were either rejected by a data-alter callback or were
+   * successfully indexed.
    *
    * @param array $items
-   *   An array of items to index.
+   *   An array of items to index, of this index's item type.
    *
    * @return array
    *   An array of the IDs of all items that should be marked as indexed.
+   *
+   * @throws SearchApiException
+   *   If an error occurred during indexing.
    */
   public function index(array $items) {
     if ($this->read_only) {
@@ -925,12 +969,18 @@ class SearchApiIndex extends Entity {
    * @return EntityMetadataWrapper
    *   A wrapper for the item type of this index, optionally loaded with the
    *   given data and having additional fields according to the data alterations
-   *   of this index.
+   *   of this index (if $alter wasn't set to FALSE).
    */
   public function entityWrapper($item = NULL, $alter = TRUE) {
-    $info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
-    $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
-    return $this->datasource()->getMetadataWrapper($item, $info);
+    try {
+      $info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
+      $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
+      return $this->datasource()->getMetadataWrapper($item, $info);
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+      return entity_metadata_wrapper($this->item_type);
+    }
   }
 
   /**
@@ -945,16 +995,24 @@ class SearchApiIndex extends Entity {
    * @see SearchApiDataSourceControllerInterface::loadItems()
    */
   public function loadItems(array $ids) {
-    return $this->datasource()->loadItems($ids);
+    try {
+      return $this->datasource()->loadItems($ids);
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+      return array();
+    }
   }
 
   /**
-   * Reset internal static caches.
+   * Reset internal caches.
    *
    * Should be used when things like fields or data alterations change to avoid
    * using stale data.
    */
   public function resetCaches() {
+    cache_clear_all($this->getCacheId(''), 'cache', TRUE);
+
     $this->datasource = NULL;
     $this->server_object = NULL;
     $this->callbacks = NULL;

+ 81 - 14
sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc

@@ -22,8 +22,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
   /**
    * PREG regular expression for splitting words.
    *
-   * We highlight around non-indexable or CJK characters.
-   *
    * @var string
    */
   protected static $split;
@@ -40,7 +38,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
         '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
         '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}';
     self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']))';
-    self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']+/iu';
+    self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/iu';
   }
 
   /**
@@ -53,6 +51,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
       'excerpt' => TRUE,
       'excerpt_length' => 256,
       'highlight' => 'always',
+      'exclude_fields' => array(),
     );
 
     $form['prefix'] = array(
@@ -87,6 +86,22 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
         ),
       ),
     );
+    // Exclude certain fulltextfields
+    $fields = $this->index->getFields();
+    $fulltext_fields = array();
+    foreach ($this->index->getFulltextFields() as $field) {
+      if (isset($fields[$field])) {
+        $fulltext_fields[$field] = $fields[$field]['name'] . ' (' . $field . ')';
+      }
+    }
+    $form['exclude_fields'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Exclude fields from excerpt'),
+      '#description' => t('Exclude certain fulltext fields from being displayed in the excerpt.'),
+      '#options' => $fulltext_fields,
+      '#default_value' => $this->options['exclude_fields'],
+      '#attributes' => array('class' => array('search-api-checkboxes-list')),
+    );
     $form['highlight'] = array(
       '#type' => 'select',
       '#title' => t('Highlight returned field data'),
@@ -106,21 +121,29 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
    * {@inheritdoc}
    */
   public function configurationFormValidate(array $form, array &$values, array &$form_state) {
-    // Overridden so $form['fields'] is not checked.
+    $values['exclude_fields'] = array_filter($values['exclude_fields']);
   }
 
   /**
    * {@inheritdoc}
    */
   public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
-    if (!$response['result count'] || !($keys = $this->getKeywords($query))) {
+    if (empty($response['results']) || !($keys = $this->getKeywords($query))) {
       return;
     }
 
+    $fulltext_fields = $this->index->getFulltextFields();
+    if (!empty($this->options['exclude_fields'])) {
+      $fulltext_fields = drupal_map_assoc($fulltext_fields);
+      foreach ($this->options['exclude_fields'] as $field) {
+        unset($fulltext_fields[$field]);
+      }
+    }
+
     foreach ($response['results'] as $id => &$result) {
       if ($this->options['excerpt']) {
         $text = array();
-        $fields = $this->getFulltextFields($response['results'], $id);
+        $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields);
         foreach ($fields as $data) {
           if (is_array($data)) {
             $text = array_merge($text, $data);
@@ -129,10 +152,11 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
             $text[] = $data;
           }
         }
-        $result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys);
+
+        $result['excerpt'] = $this->createExcerpt($this->flattenArrayValues($text), $keys);
       }
       if ($this->options['highlight'] != 'never') {
-        $fields = $this->getFulltextFields($response['results'], $id, $this->options['highlight'] == 'always');
+        $fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields, $this->options['highlight'] == 'always');
         foreach ($fields as $field => $data) {
           if (is_array($data)) {
             foreach ($data as $i => $text) {
@@ -155,6 +179,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
    * @param int|string $i
    *   The index in the results array of the result whose data should be
    *   returned.
+   * @param array $fulltext_fields
+   *   The fulltext fields from which the excerpt should be created.
    * @param bool $load
    *   TRUE if the item should be loaded if necessary, FALSE if only fields
    *   already returned in the results should be used.
@@ -163,7 +189,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
    *   An array containing fulltext field names mapped to the text data
    *   contained in them for the given result.
    */
-  protected function getFulltextFields(array &$results, $i, $load = TRUE) {
+  protected function getFulltextFields(array &$results, $i, array $fulltext_fields, $load = TRUE) {
     global $language;
     $data = array();
 
@@ -171,7 +197,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
     // Act as if $load is TRUE if we have a loaded item.
     $load |= !empty($result['entity']);
     $result += array('fields' => array());
-    $fulltext_fields = $this->index->getFulltextFields();
     // We only need detailed fields data if $load is TRUE.
     $fields = $load ? $this->index->getFields() : array();
     $needs_extraction = array();
@@ -309,7 +334,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
         // Locate a keyword (position $p, always >0 because $text starts with a
         // space).
         $p = 0;
-        if (preg_match('/' . self::$boundary . $key . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
+        if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
           $p = $match[0][1];
         }
         // Now locate a space in front (position $q) and behind it (position $s),
@@ -379,7 +404,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
     $text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2];
     $text = check_plain($text);
 
-    return $this->highlightField($text, $keys);
+    // Since we stripped the tags at the beginning, highlighting doesn't need to
+    // handle HTML anymore.
+    return $this->highlightField($text, $keys, FALSE);
   }
 
   /**
@@ -389,15 +416,55 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
    *   The text of the field.
    * @param array $keys
    *   Search keywords entered by the user.
+   * @param bool $html
+   *   Whether the text can contain HTML tags or not. In the former case, text
+   *   inside tags (i.e., tag names and attributes) won't be highlighted.
    *
    * @return string
    *   The field's text with all occurrences of search keywords highlighted.
    */
-  protected function highlightField($text, array $keys) {
+  protected function highlightField($text, array $keys, $html = TRUE) {
+    if (is_array($text)) {
+      $text = $this->flattenArrayValues($text);
+    }
+
+    if ($html) {
+      $texts = preg_split('#((?:</?[[:alpha:]](?:[^>"\']*|"[^"]*"|\'[^\']\')*>)+)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+      for ($i = 0; $i < count($texts); $i += 2) {
+        $texts[$i] = $this->highlightField($texts[$i], $keys, FALSE);
+      }
+      return implode('', $texts);
+    }
     $replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
-    $keys = implode('|', array_map('preg_quote', $keys));
+
+    $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/')));
     $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
     return substr($text, 1, -1);
   }
 
+  /**
+   * Flattens a (possibly multidimensional) array into a string.
+   *
+   * @param array $array
+   *   The array to flatten.
+   * @param string $glue
+   *   The separator to insert between individual array items.
+   *
+   * @return string
+   *   The glued string.
+   */
+  protected function flattenArrayValues(array $array, $glue = "\n\n") {
+    $ret = array();
+    foreach ($array as $item) {
+      if (is_array($item)) {
+        $ret[] = $this->flattenArrayValues($item, $glue);
+      }
+      else {
+        $ret[] = $item;
+      }
+    }
+
+    return implode($glue, $ret);
+  }
+
 }

+ 10 - 2
sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc

@@ -102,6 +102,8 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
     }
     else {
       $value = strip_tags($text);
+      // Remove any multiple or leading/trailing spaces we might have introduced.
+      $value = preg_replace('/\s\s+/', ' ', trim($value));
     }
   }
 
@@ -109,8 +111,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
     $ret = array();
     while (($pos = strpos($text, '<')) !== FALSE) {
       if ($boost && $pos > 0) {
+        $token = html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8');
+        // Remove any multiple or leading/trailing spaces we might have introduced.
+        $token = preg_replace('/\s\s+/', ' ', trim($token));
         $ret[] = array(
-          'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'),
+          'value' => $token,
           'score' => $boost,
         );
       }
@@ -130,8 +135,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
       }
     }
     if ($text) {
+      $token = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
+      // Remove any multiple or leading/trailing spaces we might have introduced.
+      $token = preg_replace('/\s\s+/', ' ', trim($token));
       $ret[] = array(
-        'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'),
+        'value' => $token,
         'score' => $boost,
       );
       $text = '';

+ 54 - 3
sites/all/modules/contrib/search/search_api/includes/query.inc

@@ -226,6 +226,9 @@ interface SearchApiQueryInterface {
    *
    * This method should always be called by execute() and contain all necessary
    * operations before the query is passed to the server's search() method.
+   *
+   * @throws SearchApiException
+   *   If any error occurred during the preparation of the query.
    */
   public function preExecute();
 
@@ -366,7 +369,7 @@ class SearchApiQuery implements SearchApiQueryInterface {
   /**
    * The index's machine name.
    *
-   * used during serialization to avoid serializing the whole index object.
+   * Used during serialization to avoid serializing the whole index object.
    *
    * @var string
    */
@@ -811,6 +814,31 @@ class SearchApiQuery implements SearchApiQueryInterface {
     $this->filter = clone $this->filter;
   }
 
+  /**
+   * Implements the magic __toString() method to simplify debugging.
+   */
+  public function __toString() {
+    $ret = 'Index: ' . $this->index->machine_name . "\n";
+    $ret .= 'Keys: ' . str_replace("\n", "\n  ", var_export($this->orig_keys, TRUE)) . "\n";
+    if (isset($this->keys)) {
+      $ret .= 'Parsed keys: ' . str_replace("\n", "\n  ", var_export($this->keys, TRUE)) . "\n";
+      $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
+    }
+    if ($filter = (string) $this->filter) {
+      $filter = str_replace("\n", "\n  ", $filter);
+      $ret .= "Filters:\n  $filter\n";
+    }
+    if ($this->sort) {
+      $sort = array();
+      foreach ($this->sort as $field => $order) {
+        $sort[] = "$field $order";
+      }
+      $ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
+    }
+    $ret .= 'Options: ' . str_replace("\n", "\n  ", var_export($this->options, TRUE)) . "\n";
+    return $ret;
+  }
+
 }
 
 /**
@@ -890,8 +918,11 @@ interface SearchApiQueryFilterInterface {
    * Return all conditions and nested filters contained in this filter.
    *
    * @return array
-   *   An array containing this filter's subfilters. Each of these is either an
-   *   array (field, value, operator), or another SearchApiFilter object.
+   *   An array containing this filter's subfilters. Each of these is either a
+   *   condition, represented as a numerically indexed array with the arguments
+   *   of a previous SearchApiQueryFilterInterface::condition() call (field,
+   *   value, operator); or a nested filter, represented by a
+   *   SearchApiQueryFilterInterface filter object.
    */
   public function &getFilters();
 
@@ -1010,4 +1041,24 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
     }
   }
 
+  /**
+   * Implements the magic __toString() method to simplify debugging.
+   */
+  public function __toString() {
+    // Special case for a single, nested filter:
+    if (count($this->filters) == 1 && is_object($this->filters[0])) {
+      return (string) $this->filters[0];
+    }
+    $ret = array();
+    foreach ($this->filters as $filter) {
+      if (is_object($filter)) {
+        $ret[] = "[\n  " . str_replace("\n", "\n    ", (string) $filter) . "\n  ]";
+      }
+      else {
+        $ret[] = "$filter[0] $filter[2] " . str_replace("\n", "\n    ", var_export($filter[1], TRUE));
+      }
+    }
+    return $ret ? '  ' . implode("\n{$this->conjunction}\n  ", $ret) : '';
+  }
+
 }

+ 2 - 2
sites/all/modules/contrib/search/search_api/includes/server_entity.inc

@@ -74,8 +74,8 @@ class SearchApiServer extends Entity {
   /**
    * Constructor as a helper to the parent constructor.
    */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'search_api_server');
+  public function __construct(array $values = array(), $entity_type = 'search_api_server') {
+    parent::__construct($values, $entity_type);
   }
 
   /**

+ 63 - 24
sites/all/modules/contrib/search/search_api/search_api.admin.inc

@@ -252,6 +252,13 @@ function search_api_admin_add_server(array $form, array &$form_state) {
   $form['options']['#prefix'] = '<div id="search-api-class-options">';
   $form['options']['#suffix'] = '</div>';
 
+  // If $info is not set, there are no service classes. Display an error message
+  // telling the user how to change that and return an empty form.
+  if (!isset($info)) {
+    drupal_set_message(t('There are no service classes available for the Search API. Please install a <a href="@url">module that provides a service class</a> to proceed.', array('@url' => url('https://www.drupal.org/node/1254698'))), 'error');
+    return array();
+  }
+
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Create server'),
@@ -834,6 +841,15 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) {
   }
 
   $status = search_api_index_status($index);
+  try {
+    $server = $index->server();
+  }
+  catch (SearchApiException $e) {
+    $server = NULL;
+    $vars['%server'] = $index->server;
+    $message = t('The index has an unknown server (ID: %server) set. Please check the index settings.', $vars);
+    drupal_set_message($message, 'error');
+  }
   $ret['view'] = array(
     '#theme' => 'search_api_index',
     '#id' => $index->id,
@@ -842,15 +858,21 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) {
     '#description' => $index->description,
     '#item_type' => $index->item_type,
     '#enabled' => $index->enabled,
-    '#server' => $index->server(),
+    '#server' => $server,
     '#options' => $index->options,
     '#fields' => $index->getFields(),
     '#indexed_items' => $status['indexed'],
-    '#on_server' => _search_api_get_items_on_server($index),
+    '#on_server' => NULL,
     '#total_items' => $status['total'],
     '#status' => $index->status,
     '#read_only' => $index->read_only,
   );
+  try{
+    $ret['view']['#on_server'] = _search_api_get_items_on_server($index);
+  }
+  catch (SearchApiException $e) {
+    watchdog_exception('search_api', $e);
+  }
   if ($index->enabled && !$index->read_only) {
     $ret['form'] = drupal_get_form('search_api_admin_index_status_form', $index, $status);
   }
@@ -873,7 +895,8 @@ function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) {
  *   - fields: All indexed fields of the index.
  *   - indexed_items: The number of items already indexed in their latest
  *     version on this index.
- *   - on_server: The number of items actually indexed on the server.
+ *   - on_server: The number of items actually indexed on the server. NULL if
+ *     the search for finding out the item count failed.
  *   - total_items: The total number of items that have to be indexed for this
  *     index.
  *   - status: The entity configuration status (in database, in code, etc.).
@@ -963,15 +986,21 @@ function theme_search_api_index(array $variables) {
     $rows[] = _search_api_deep_copy($row);
 
     $theme = array(
-      'percent' => (int) (100 * $indexed_items / $total_items),
+      'percent' => $total_items ? (int) (100 * $indexed_items / $total_items) : 100,
       'message' => t('@indexed/@total indexed', array('@indexed' => $indexed_items, '@total' => $total_items)),
     );
     $output .= '<h3>' . t('Index status') . '</h3>';
     $output .= '<div class="search-api-index-status">' . theme('progress_bar', $theme) . '</div>';
 
-    $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status');
-    $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (<a href="@url">More information</a>)', 'There are @count items indexed on the server for this index. (<a href="@url">More information</a>)', $vars);
-    $class = '';
+    if (!isset($on_server)) {
+      $info = t('An error occurred while trying to determine the server index status. Please check the logs for details.');
+      $class = 'error';
+    }
+    else {
+      $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status');
+      $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (<a href="@url">More information</a>)', 'There are @count items indexed on the server for this index. (<a href="@url">More information</a>)', $vars);
+      $class = '';
+    }
     $label = t('Server index status');
     $rows[] = _search_api_deep_copy($row);
   }
@@ -1178,12 +1207,21 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI
     '#default_value' => $index->name,
     '#required' => TRUE,
   );
+  try {
+    $enabled_fixed = !$index->enabled && !$index->server();
+  }
+  catch (Exception $e) {
+    watchdog_exception('search_api', $e);
+    // The exception only occurs if the index is disabled, and for an unknown
+    // server we of course want do prevent the index from being enabled.
+    $enabled_fixed = TRUE;
+  }
   $form['enabled'] = array(
     '#type' => 'checkbox',
     '#title' => t('Enabled'),
     '#default_value' => $index->enabled,
     // Can't enable an index lying on a disabled server, or no server at all.
-    '#disabled' => !$index->enabled && (!$index->server() || !$index->server()->enabled),
+    '#disabled' => $enabled_fixed,
   );
   $form['description'] = array(
     '#type' => 'textarea',
@@ -1565,6 +1603,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
   foreach ($form_state['callbacks'] as $name => $callback) {
     // Check whether callback status has changed.
     if ($values['callbacks'][$name]['status'] == empty($options['data_alter_callbacks'][$name]['status'])) {
+      $callbacks_changed = TRUE;
       if ($values['callbacks'][$name]['status']) {
         // Callback was just enabled, add its fields.
         $properties = $callback->propertyInfo();
@@ -1591,16 +1630,6 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
           }
         }
       }
-      else {
-        // Callback was just disabled, remove its fields.
-        $properties = $callback->propertyInfo();
-        if ($properties) {
-          foreach ($properties as $key => $field) {
-            unset($index->options['fields'][$key]);
-          }
-        }
-
-      }
     }
   }
 
@@ -1614,9 +1643,9 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
     uasort($index->options['data_alter_callbacks'], 'search_api_admin_element_compare');
     uasort($index->options['processors'], 'search_api_admin_element_compare');
 
-    // Reset the index's internal property cache to correctly incorporate the
-    // new data alterations.
-    $index->resetCaches();
+    // Re-calculate the fields, since they might have changed in hard-to-predict
+    // ways.
+    search_api_index_recalculate_fields(array($index));
 
     $index->save();
     $index->reindex();
@@ -1658,9 +1687,11 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
   // An array of option arrays for types, keyed by nesting level.
   $types = array(0 => search_api_field_types());
   $entity_types = entity_get_info();
-  //$boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
+  // $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
   $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
-   
+
+
+
   $fulltext_types = array(0 => array('text'));
   // Add all custom data types with fallback "text" to fulltext types as well.
   foreach (search_api_get_data_type_info() as $id => $type) {
@@ -2210,8 +2241,16 @@ function search_api_admin_confirm_submit(array $form, array &$form_state) {
   $action = $values['action'];
   $id = $values['id'];
 
+  $success = FALSE;
   $function = "search_api_{$type}_{$action}";
-  if ($function($id)) {
+  try {
+    // Some actions, like disabling, can actually throw an exception.
+    $success = $function($id);
+  }
+  catch (SearchApiException $e) {
+    watchdog_exception('search_api', $e);
+  }
+  if ($success) {
     drupal_set_message($values['message']);
   }
   else {

+ 1 - 1
sites/all/modules/contrib/search/search_api/search_api.api.php

@@ -335,7 +335,7 @@ function hook_search_api_items_indexed(SearchApiIndex $index, array $item_ids) {
  * Lets modules alter a search query before executing it.
  *
  * @param SearchApiQueryInterface $query
- *   The SearchApiQueryInterface object representing the search query.
+ *   The search query being executed.
  */
 function hook_search_api_query_alter(SearchApiQueryInterface $query) {
   // Exclude entities with ID 0. (Assume the ID field is always indexed.)

+ 181 - 46
sites/all/modules/contrib/search/search_api/search_api.drush.inc

@@ -108,6 +108,19 @@ function search_api_drush_command() {
     'aliases' => array('sapi-c'),
   );
 
+  $items['search-api-set-index-server'] = array(
+    'description' => 'Set the search server used by a given index.',
+    'examples' => array(
+      'drush search-api-set-index-server default_node_index my_solr_server' => dt('Set the !index index to use the !server server.', array('!index' => 'default_node_index', '!server' => 'my_solr_server')),
+      'drush sapi-sis default_node_index my_solr_server' => dt('Alias to set the !index index to use the !server server.', array('!index' => 'default_node_index', '!server' => 'my_solr_server')),
+    ),
+    'arguments' => array(
+      'index_id' => dt('The numeric ID or machine name of an index.'),
+      'server_id' => dt('The numeric ID or machine name of a server to set on the index.'),
+    ),
+    'aliases' => array('sapi-sis'),
+  );
+
   return $items;
 }
 
@@ -137,15 +150,21 @@ function drush_search_api_list() {
   foreach ($indexes as $index) {
     $type = search_api_get_item_type_info($index->item_type);
     $type = isset($type['name']) ? $type['name'] : $index->item_type;
-    $server = $index->server();
-    $server = $server ? $server->name : '(' . t('none') . ')';
+    try {
+      $server = $index->server();
+      $server = $server ? $server->name : '(' . dt('none') . ')';
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+      $server = '(' . dt('unknown: !server', array('server' => $index->server)) . ')';
+    }
     $row = array(
       $index->id,
       $index->name,
       $index->machine_name,
       $server,
       $type,
-      $index->enabled ? t('enabled') : t('disabled'),
+      $index->enabled ? dt('enabled') : dt('disabled'),
       $index->options['cron_limit'],
     );
     $rows[] = $row;
@@ -168,17 +187,24 @@ function drush_search_api_enable($index_id = NULL) {
     return;
   }
   foreach ($indexes as $index) {
+    $vars = array('!index' => $index->name);
     if (!$index->enabled) {
-      drush_log(dt("Enabling index !index and queueing items for indexing.", array('!index' => $index->name)), 'notice');
-      if (search_api_index_enable($index->id)) {
-        drush_log(dt("The index !index was successfully enabled.", array('!index' => $index->name)), 'ok');
+      drush_log(dt("Enabling index !index and queueing items for indexing.", $vars), 'notice');
+      $success = FALSE;
+      try {
+        if ($success = search_api_index_enable($index->id)) {
+          drush_log(dt("The index !index was successfully enabled.", $vars), 'ok');
+        }
+      }
+      catch (SearchApiException $e) {
+        drush_log($e->getMessage(), 'error');
       }
-      else {
-        drush_log(dt("Error enabling index !index.", array('!index' => $index->name)), 'error');
+      if (!$success) {
+        drush_log(dt("Error enabling index !index.", $vars), 'error');
       }
     }
     else {
-      drush_log(dt("The index !index is already enabled.", array('!index' => $index->name)), 'error');
+      drush_log(dt("The index !index is already enabled.", $vars), 'error');
     }
   }
 }
@@ -198,16 +224,23 @@ function drush_search_api_disable($index_id = NULL) {
     return;
   }
   foreach ($indexes as $index) {
+    $vars = array('!index' => $index->name);
     if ($index->enabled) {
-      if (search_api_index_disable($index->id)) {
-        drush_log(dt("The index !index was successfully disabled.", array('!index' => $index->name)), 'ok');
+      $success = FALSE;
+      try {
+        if ($success = search_api_index_disable($index->id)) {
+          drush_log(dt("The index !index was successfully disabled.", $vars), 'ok');
+        }
       }
-      else {
-        drush_log(dt("Error disabling index !index.", array('!index' => $index->name)), 'error');
+      catch (SearchApiException $e) {
+        drush_log($e->getMessage(), 'error');
+      }
+      if (!$success) {
+        drush_log(dt("Error disabling index !index.", $vars), 'error');
       }
     }
     else {
-      drush_log(dt("The index !index is already disabled.", array('!index' => $index->name)), 'error');
+      drush_log(dt("The index !index is already disabled.", $vars), 'error');
     }
   }
 }
@@ -264,40 +297,72 @@ function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = N
   if (empty($indexes)) {
     return;
   }
+
+  $process_batch = FALSE;
   foreach ($indexes as $index) {
-    // Get the number of remaing items to index.
-    $datasource = $index->datasource();
-    $index_status = $datasource->getIndexStatus($index);
-    $remaining = $index_status['total'] - $index_status['indexed'];
-    if ($remaining <= 0) {
-      drush_log(dt("The index !index is up to date.", array('!index' => $index->name)), 'ok');
-      continue;
+    if (_drush_search_api_batch_indexing_create($index, $limit, $batch_size)) {
+      $process_batch = TRUE;
     }
+  }
 
-    // Get the number of items to index per batch run.
-    if (!isset($batch_size)) {
-      $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
-    }
-    elseif ($batch_size <= 0) {
-      $batch_size = $remaining;
-    }
+  if ($process_batch) {
+    drush_backend_batch_process();
+  }
+}
 
-    // Get the number items to index.
-    if (!isset($limit) || !is_int($limit += 0) || $limit <= 0) {
-      $limit = $remaining;
-    }
+/**
+ * Creates and sets a batch for indexing items for a particular index.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be indexed.
+ * @param int $limit
+ *   (optional) The maximum number of items to index, or NULL to index all
+ *   items.
+ * @param int $batch_size
+ *   (optional) The number of items to index per batch, or NULL to index all
+ *   items at once.
+ *
+ * @return bool
+ *   TRUE if batch was created, FALSE otherwise.
+ */
+function _drush_search_api_batch_indexing_create(SearchApiIndex $index, $limit = NULL, $batch_size = NULL) {
+  // Get the number of remaining items to index.
+  try {
+    $datasource = $index->datasource();
+  }
+  catch (SearchApiException $e) {
+    drush_log($e->getMessage(), 'error');
+    return FALSE;
+  }
+  $index_status = $datasource->getIndexStatus($index);
+  $remaining = $index_status['total'] - $index_status['indexed'];
+  if ($remaining <= 0) {
+    drush_log(dt("The index !index is up to date.", array('!index' => $index->name)), 'ok');
+    return FALSE;
+  }
 
-    drush_log(dt("Indexing a maximum number of !limit items (!batch_size items per batch run) for the index !index.", array('!index' => $index->name, '!limit' => $limit, '!batch_size' => $batch_size)), 'ok');
+  // Get the number of items to index per batch run.
+  if (!isset($batch_size)) {
+    $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
+  }
+  elseif ($batch_size <= 0) {
+    $batch_size = $remaining;
+  }
 
-    // Create the batch.
-    if (!_search_api_batch_indexing_create($index, $batch_size, $limit, $remaining, TRUE)) {
-      drush_log(dt("Couldn't create a batch, please check the batch size and limit parameters."), 'error');
-    }
-    else {
-      // Launch the batch process.
-      drush_backend_batch_process();
-    }
+  // Get the total number of items to index.
+  if (!isset($limit) || !is_int($limit += 0) || $limit <= 0) {
+    $limit = $remaining;
   }
+
+  drush_log(dt("Indexing a maximum number of !limit items (!batch_size items per batch run) for the index !index.", array('!index' => $index->name, '!limit' => $limit, '!batch_size' => $batch_size)), 'ok');
+
+  // Create the batch.
+  if (!_search_api_batch_indexing_create($index, $batch_size, $limit, $remaining, TRUE)) {
+    drush_log(dt("Couldn't create a batch, please check the batch size and limit parameters."), 'error');
+    return FALSE;
+  }
+
+  return TRUE;
 }
 
 /**
@@ -367,12 +432,54 @@ function drush_search_api_clear($index_id = NULL) {
 }
 
 /**
- * Helper function to return an index or all indexes as an array.
+ * Set the server for a given index.
+ */
+function drush_search_api_set_index_server($index_id = NULL, $server_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  // Make sure we have parameters to work with.
+  if (empty($index_id) || empty($server_id)) {
+    drush_log(dt('You must specify both an index and server.'), 'error');
+    return;
+  }
+  // Fetch current index and server data.
+  $indexes = search_api_drush_get_index($index_id);
+  $servers = search_api_drush_get_server($server_id);
+  if (empty($indexes) || empty($servers)) {
+    // If the specified index or server can't be found, just return. An
+    // appropriate error message should have been printed already.
+    return;
+  }
+  // Set the new server on the index.
+  $success = FALSE;
+  $index = reset($indexes);
+  $server = reset($servers);
+  try {
+    $success = $index->update(array('server' => $server->machine_name));
+  }
+  catch (SearchApiException $e) {
+    drush_log($e->getMessage(), 'error');
+  }
+  if ($success === FALSE) {
+    drush_log(dt('There was an error setting index !index to use server !server.', array('!index' => $index->name, '!server' => $server->name)), 'error');
+  }
+  elseif (!$success) {
+    drush_log(dt('Index !index was already using server !server.', array('!index' => $index->name, '!server' => $server->name)), 'ok');
+  }
+  else {
+    drush_log(dt('Index !index has been set to use server !server and items have been queued for indexing.', array('!index' => $index->name, '!server' => $server->name)), 'ok');
+  }
+}
+
+/**
+ * Returns an index or all indexes as an array.
  *
- * @param $index_id
- *   (optional) The provided index id.
+ * @param string|int|null $index_id
+ *   (optional) The ID or machine name of the index to load. Defaults to
+ *   loading all available indexes.
  *
- * @return
+ * @return SearchApiIndex[]
  *   An array of indexes.
  */
 function search_api_drush_get_index($index_id = NULL) {
@@ -387,7 +494,34 @@ function search_api_drush_get_index($index_id = NULL) {
 }
 
 /**
- * Static lookup to prevent Drush 4 from running twice.
+ * Returns a server or all servers as an array.
+ *
+ * @param string|int|null $server_id
+ *   (optional) The ID or machine name of the server to load. Defaults to
+ *   loading all available servers.
+ *
+ * @return SearchApiServer[]
+ *   An array of servers.
+ */
+function search_api_drush_get_server($server_id = NULL) {
+  $ids = isset($server_id) ? array($server_id) : FALSE;
+  $servers = search_api_server_load_multiple($ids);
+  if (empty($servers)) {
+    drush_set_error(dt('Invalid server_id or no servers present.'));
+    // @todo: Maybe add logic to print table of all servers.
+  }
+  return $servers;
+}
+
+/**
+ * Does a static lookup to prevent Drush 4 from running twice.
+ *
+ * @param string $function
+ *   The Drush function being called.
+ *
+ * @return bool
+ *   TRUE if the function was already called in this Drush execution, FALSE
+ *   otherwise.
  *
  * @see http://drupal.org/node/704848
  */
@@ -397,4 +531,5 @@ function search_api_drush_static($function) {
     return TRUE;
   }
   $index[$function] = TRUE;
+  return FALSE;
 }

+ 3 - 3
sites/all/modules/contrib/search/search_api/search_api.info

@@ -34,9 +34,9 @@ files[] = includes/service.inc
 
 configure = admin/config/search/search_api
 
-; Information added by Drupal.org packaging script on 2013-12-25
-version = "7.x-1.11"
+; Information added by Drupal.org packaging script on 2014-12-26
+version = "7.x-1.14"
 core = "7.x"
 project = "search_api"
-datestamp = "1387965506"
+datestamp = "1419580682"
 

+ 11 - 2
sites/all/modules/contrib/search/search_api/search_api.install

@@ -349,8 +349,13 @@ function search_api_enable() {
     }
   }
   foreach ($types as $type => $indexes) {
-    $controller = search_api_get_datasource_controller($type);
-    $controller->startTracking($indexes);
+    try {
+      $controller = search_api_get_datasource_controller($type);
+      $controller->startTracking($indexes);
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+    }
   }
 }
 
@@ -961,6 +966,10 @@ function search_api_update_7116() {
     $insert = db_insert('search_api_task')
       ->fields(array('server_id', 'type', 'index_id', 'data'));
     foreach ($tasks as $task) {
+      $task += array(
+        'index_id' => NULL,
+        'data' => NULL,
+      );
       $insert->values($task);
     }
     $insert->execute();

+ 106 - 71
sites/all/modules/contrib/search/search_api/search_api.module

@@ -275,7 +275,7 @@ function search_api_theme() {
       'options' => array(),
       'fields' => array(),
       'indexed_items' => 0,
-      'on_server' => 0,
+      'on_server' => NULL,
       'total_items' => 0,
       'status' => ENTITY_CUSTOM,
       'read_only' => 0,
@@ -662,9 +662,21 @@ function search_api_search_api_index_update(SearchApiIndex $index) {
     }
 
     if ($index->server) {
-      $new_server = $index->server(TRUE);
-      // If the server is enabled, we call addIndex(); otherwise, we save the task.
-      $new_server->addIndex($index);
+      try {
+        $new_server = $index->server(TRUE);
+        // If the server is enabled, we call addIndex(); otherwise, we save the task.
+        $new_server->addIndex($index);
+      }
+      catch (SearchApiException $e) {
+        watchdog_exception('search_api', $e);
+        // If the new server doesn't exist, we remove the index from all
+        // servers. Note that saving an entity in its own update hook is usually
+        // a recipe for disaster, but since we are only doing this if a server
+        // is set and remove the server here before saving, it should be safe
+        // enough.
+        $index->server = NULL;
+        $index->save();
+      }
     }
 
     // We also have to re-index all content.
@@ -672,28 +684,17 @@ function search_api_search_api_index_update(SearchApiIndex $index) {
   }
 
   // If the fields were changed, call the appropriate service class hook method
-  // and re-index the content, if necessary. Also, clear the fields cache.
+  // and re-index the content, if necessary.
   $old_fields = $index->original->options + array('fields' => array());
   $old_fields = $old_fields['fields'];
   $new_fields = $index->options + array('fields' => array());
   $new_fields = $new_fields['fields'];
   if ($old_fields != $new_fields) {
-    cache_clear_all($index->getCacheId(), 'cache', TRUE);
     if ($index->server) {
       $index->server()->fieldsUpdated($index);
     }
   }
 
-  // If additional fields changed, clear the index's specific cache which
-  // includes them.
-  $old_additional = $index->original->options + array('additional fields' => array());
-  $old_additional = $old_additional['additional fields'];
-  $new_additional = $index->options + array('additional fields' => array());
-  $new_additional = $new_additional['additional fields'];
-  if ($old_additional != $new_additional) {
-    cache_clear_all($index->getCacheId() . '-0-1', 'cache');
-  }
-
   // If the index's enabled or read-only status is being changed, queue or
   // dequeue items for indexing.
   if (!$index->read_only && $index->enabled != $index->original->enabled) {
@@ -1098,7 +1099,14 @@ function search_api_track_item_insert($type, array $item_ids) {
     return;
   }
 
-  search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes);
+  try {
+    search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes);
+  }
+  catch (SearchApiException $e) {
+    $vars['%item_type'] = $type;
+    watchdog_exception('search_api', $e, '%type while inserting items of type %item_type: !message in %function (line %line of %file).', $vars);
+    return;
+  }
 
   foreach ($indexes as $index) {
     if (!empty($index->options['index_directly'])) {
@@ -1128,19 +1136,26 @@ function search_api_track_item_change($type, array $item_ids) {
   if (!$indexes) {
     return;
   }
-  search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes);
-  foreach ($indexes as $index) {
-    if (!empty($index->options['index_directly'])) {
-      // For indexes with the index_directly option set, queue the items to be
-      // indexed at the end of the request.
-      try {
-        search_api_index_specific_items_delayed($index, $item_ids);
-      }
-      catch (SearchApiException $e) {
-        watchdog_exception('search_api', $e);
+  try {
+    search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes);
+    foreach ($indexes as $index) {
+      if (!empty($index->options['index_directly'])) {
+        // For indexes with the index_directly option set, queue the items to be
+        // indexed at the end of the request.
+        try {
+          search_api_index_specific_items_delayed($index, $item_ids);
+        }
+        catch (SearchApiException $e) {
+          watchdog_exception('search_api', $e);
+        }
       }
     }
   }
+  catch (SearchApiException $e) {
+    $vars['%item_type'] = $type;
+    watchdog_exception('search_api', $e, '%type while updating items of type %item_type: !message in %function (line %line of %file).', $vars);
+    return;
+  }
 }
 
 /**
@@ -1158,7 +1173,12 @@ function search_api_track_item_change($type, array $item_ids) {
  *   the Drupal 8 version of this module.
  */
 function search_api_track_item_queued(SearchApiIndex $index, array $item_ids) {
-  $index->datasource()->trackItemQueued($item_ids, $index);
+  try {
+    $index->datasource()->trackItemQueued($item_ids, $index);
+  }
+  catch (SearchApiException $e) {
+    watchdog_exception('search_api', $e);
+  }
 }
 
 /**
@@ -1191,16 +1211,27 @@ function search_api_track_item_delete($type, array $item_ids) {
   );
   $indexes = search_api_index_load_multiple(FALSE, $conditions);
   if ($indexes) {
-    search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes);
+    try {
+      search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes);
+    }
+    catch (SearchApiException $e) {
+      $vars['%item_type'] = $type;
+      watchdog_exception('search_api', $e, '%type while deleting items of type %item_type: !message in %function (line %line of %file).', $vars);
+    }
   }
 
   // Then, delete it from all servers. Servers of disabled indexes have to be
   // considered, too!
   unset($conditions['enabled']);
   foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) {
-    if ($index->server) {
-      $server = $index->server();
-      $server->deleteItems($item_ids, $index);
+    try {
+      if ($server = $index->server()) {
+        $server->deleteItems($item_ids, $index);
+      }
+    }
+    catch (Exception $e) {
+      $vars['%item_type'] = $type;
+      watchdog_exception('search_api', $e, '%type while deleting items of type %item_type: !message in %function (line %line of %file).', $vars);
     }
   }
 }
@@ -1389,7 +1420,7 @@ function search_api_index_recalculate_fields($indexes = FALSE) {
     }
     // Use a more accurate method of determining if the fields settings are
     // equal to avoid needlessly re-indexing the whole index.
-    if (!_search_api_settings_equals($fields, $index->options['fields'])) {
+    if ($fields != $index->options['fields']) {
       $options = $index->options;
       $options['fields'] = $fields;
       $index->update(array('options' => $options));
@@ -1400,9 +1431,6 @@ function search_api_index_recalculate_fields($indexes = FALSE) {
 /**
  * Test two setting arrays (or individual settings) for equality.
  *
- * While a simple == also works in some cases, this function takes into account
- * that the order of keys (usually) doesn't matter in settings arrays.
- *
  * @param mixed $setting1
  *   The first setting (array).
  * @param mixed $setting2
@@ -1410,6 +1438,8 @@ function search_api_index_recalculate_fields($indexes = FALSE) {
  *
  * @return bool
  *   TRUE if both settings are identical, FALSE otherwise.
+ *
+ * @deprecated The simple "==" operator will achieve the same.
  */
 function _search_api_settings_equals($setting1, $setting2) {
   if (!is_array($setting1) || !is_array($setting2)) {
@@ -1495,10 +1525,7 @@ function search_api_index_specific_items(SearchApiIndex $index, array $ids) {
       // some specific setups.
       $type = search_api_get_item_type_info($index->item_type);
       $type = $type ? $type['name'] : $index->item_type;
-      watchdog('search_api',
-          "Error during indexing: invalid item loaded for @type with ID @id.",
-          array('@id' => $id, '@type' => $type),
-          WATCHDOG_WARNING);
+      watchdog('search_api', "Error during indexing: invalid item loaded for @type with ID @id.", array('@id' => $id, '@type' => $type), WATCHDOG_WARNING);
     }
   }
   $indexed = $items ? $index->index($cloned_items) : array();
@@ -1578,25 +1605,14 @@ function search_api_get_items_to_index(SearchApiIndex $index, $limit = -1) {
  * @param $id
  *   The ID or machine name of the index to execute the search on.
  * @param $options
- *   An associative array of options. The following are recognized:
- *   - filters: Either a SearchApiQueryFilterInterface object or an array of
- *     filters used to filter the search.
- *   - sort: An array of sort directives of the form $field => $order, where
- *     $order is either 'ASC' or 'DESC'.
- *   - offset: The position of the first returned search results relative to the
- *     whole result in the index.
- *   - limit: The maximum number of search results to return. -1 means no limit.
- *   - 'query class': The query class to use. Must be a subtype of
- *     SearchApiQueryInterface.
- *   - conjunction: The type of conjunction to use for this query - either
- *     'AND' or 'OR'. 'AND' by default.
- *   - 'parse mode': The mode with which to parse the $keys variable, if it
- *     is set and not already an array. See SearchApiQuery::parseModes() for
- *     parse modes recognized by the SearchApiQuery class.
- *     Subclasses might define additional modes.
+ *   An associative array of options to be passed to
+ *   SearchApiQueryInterface::__construct().
  *
  * @return SearchApiQueryInterface
  *   An object for searching on the specified index.
+ *
+ * @throws SearchApiException
+ *   If the index is unknown or disabled, or some other error was encountered.
  */
 function search_api_query($id, array $options = array()) {
   $index = search_api_index_load($id);
@@ -1607,9 +1623,10 @@ function search_api_query($id, array $options = array()) {
 }
 
 /**
- * Static store for the searches executed on the current page. Can either be
- * used to store an executed search, or to retrieve a previously stored
- * search.
+ * Stores or retrieves a search executed in this page request.
+ *
+ * Static storage for the searches executed during the current page request. Can
+ * used to store an executed search, or to retrieve a previously stored search.
  *
  * @param $search_id
  *   For pages displaying multiple searches, an optional ID identifying the
@@ -1935,7 +1952,14 @@ function search_api_search_api_query_alter(SearchApiQueryInterface $query) {
       }
     }
     else {
-      watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $user)), WATCHDOG_WARNING);
+      $account = $query->getOption('search_api_access_account', '(' . t('none') . ')');
+      if (is_object($account)) {
+        $account = $account->uid;
+      }
+      if (!is_scalar($account)) {
+        $account = var_export($account, TRUE);
+      }
+      watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $account), WATCHDOG_WARNING);
     }
   }
 }
@@ -1994,7 +2018,6 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
     $query->filter($filter);
   }
   else {
-    // /!\ in previous patches i commented the next line, why ? maybe will have to do it again
     $query->condition('status', $published);
   }
 
@@ -2225,7 +2248,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
       # http://drupal.org/node/1873910#comment-6876200
       // $subwrapper = $wrapper->$prefix;
       // $subwrapper->language( $wrapper->language->value() );
-      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);  
+      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);
       foreach ($nested_fields as $field => $info) {
         $fields["$prefix:$field"] = $info;
       }
@@ -2561,12 +2584,18 @@ function search_api_index_url(SearchApiIndex $index) {
  * @param SearchApiIndex $index
  *   The index whose server should be returned.
  *
- * @return SearchApiServer
+ * @return SearchApiServer|null
  *   The server this index currently resides on, or NULL if the index is
  * currently unassigned.
  */
 function search_api_index_get_server(SearchApiIndex $index) {
-  return $index->server();
+  try {
+    return $index->server();
+  }
+  catch (SearchApiException $e) {
+    watchdog_exception('search_api', $e);
+    return NULL;
+  }
 }
 
 /**
@@ -2643,14 +2672,14 @@ function search_api_index_edit_fields($id, array $fields) {
 /**
  * Enables a search index.
  *
- * @param $id
+ * @param string|int $id
  *   The ID or machine name of the index to enable.
  *
- * @throws SearchApiException
- *   If the index' server isn't enabled.
- *
- * @return
+ * @return int|false
  *   1 on success, 0 or FALSE on failure.
+ *
+ * @throws SearchApiException
+ *   If the index's server doesn't exist.
  */
 function search_api_index_enable($id) {
   $index = search_api_index_load($id, TRUE);
@@ -2661,11 +2690,14 @@ function search_api_index_enable($id) {
 /**
  * Disables a search index.
  *
- * @param $id
+ * @param string|int $id
  *   The ID or machine name of the index to disable.
  *
- * @return
+ * @return int|false
  *   1 on success, 0 or FALSE on failure.
+ *
+ * @throws SearchApiException
+ *   If the index's server doesn't exist.
  */
 function search_api_index_disable($id) {
   $index = search_api_index_load($id, TRUE);
@@ -2813,6 +2845,9 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ
  * @return int
  *   The number of items found on the server for this index, if the latter is
  *   enabled. 0 otherwise.
+ *
+ * @throws SearchApiException
+ *   If an error prevented the search from completing.
  */
 function _search_api_get_items_on_server(SearchApiIndex $index) {
   if (!$index->enabled) {

+ 4 - 0
sites/all/modules/contrib/search/search_api/search_api.rules.inc

@@ -52,6 +52,9 @@ function _search_api_rules_access() {
  * Rules action for indexing an item.
  */
 function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiIndex $index = NULL, $index_immediately = TRUE) {
+  // If we do not have an index, we need to guess the item type to use.
+  // @todo Since this can only be used with entities anyways, we can just loop
+  //   over the item type information and use all types with that entity type.
   $type = $wrapper->type();
   $item_ids = array($wrapper->getIdentifier());
 
@@ -61,6 +64,7 @@ function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiI
   }
 
   if ($index) {
+    $type = $index->item_type;
     $indexes = array($index);
   }
   else {

+ 3 - 3
sites/all/modules/contrib/search/search_api/tests/search_api_test.info

@@ -10,9 +10,9 @@ files[] = search_api_test.module
 
 hidden = TRUE
 
-; Information added by Drupal.org packaging script on 2013-12-25
-version = "7.x-1.11"
+; Information added by Drupal.org packaging script on 2014-12-26
+version = "7.x-1.14"
 core = "7.x"
 project = "search_api"
-datestamp = "1387965506"
+datestamp = "1419580682"