Browse Source

updated search_api, search_api_solr_override, imce

Bachir Soussi Chiadmi 5 years ago
parent
commit
2e69e3fd4c
41 changed files with 1383 additions and 140 deletions
  1. 3 3
      sites/all/modules/contrib/files/imce/imce/imce.info
  2. 1 0
      sites/all/modules/contrib/files/imce/imce/imce.install
  3. 14 0
      sites/all/modules/contrib/files/imce/imce/imce.module
  4. 7 0
      sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc
  5. 7 2
      sites/all/modules/contrib/files/imce/imce/js/imce.js
  6. 80 0
      sites/all/modules/contrib/search/search_api/CHANGELOG.txt
  7. 3 3
      sites/all/modules/contrib/search/search_api/README.txt
  8. 1 1
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
  9. 6 7
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info
  10. 15 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt
  11. 2 2
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc
  12. 0 2
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc
  13. 41 7
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_date.inc
  14. 6 1
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
  15. 12 4
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc
  16. 248 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_numeric.inc
  17. 27 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc
  18. 5 1
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
  19. 8 3
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
  20. 146 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_content_cache.inc
  21. 4 2
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
  22. 8 7
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info
  23. 7 2
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module
  24. 14 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc
  25. 35 7
      sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
  26. 1 1
      sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc
  27. 57 0
      sites/all/modules/contrib/search/search_api/includes/callback_user_content.inc
  28. 88 46
      sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
  29. 3 4
      sites/all/modules/contrib/search/search_api/includes/processor_stemmer.inc
  30. 39 10
      sites/all/modules/contrib/search/search_api/search_api.admin.inc
  31. 80 0
      sites/all/modules/contrib/search/search_api/search_api.drush.inc
  32. 5 5
      sites/all/modules/contrib/search/search_api/search_api.info
  33. 45 0
      sites/all/modules/contrib/search/search_api/search_api.install
  34. 154 2
      sites/all/modules/contrib/search/search_api/search_api.module
  35. 7 2
      sites/all/modules/contrib/search/search_api/search_api.test
  36. 5 7
      sites/all/modules/contrib/search/search_api/tests/search_api_test.info
  37. 16 0
      sites/all/modules/contrib/search/search_api/tests/search_api_test_2.info
  38. 136 0
      sites/all/modules/contrib/search/search_api/tests/search_api_test_2.module
  39. 39 1
      sites/all/modules/contrib/search/search_api_solr_overrides/README.txt
  40. 3 3
      sites/all/modules/contrib/search/search_api_solr_overrides/search_api_solr_overrides.info
  41. 5 5
      sites/all/modules/contrib/search/search_api_solr_overrides/search_api_solr_overrides.module

+ 3 - 3
sites/all/modules/contrib/files/imce/imce/imce.info

@@ -4,9 +4,9 @@ core = "7.x"
 package = "Media"
 configure = "admin/config/media/imce"
 
-; Information added by Drupal.org packaging script on 2016-03-30
-version = "7.x-1.10"
+; Information added by Drupal.org packaging script on 2017-05-27
+version = "7.x-1.11"
 core = "7.x"
 project = "imce"
-datestamp = "1459346870"
+datestamp = "1495890787"
 

+ 1 - 0
sites/all/modules/contrib/files/imce/imce/imce.install

@@ -25,6 +25,7 @@ function imce_uninstall() {
   variable_del('imce_settings_replace');
   variable_del('imce_settings_thumb_method');
   variable_del('imce_settings_disable_private');
+  variable_del('imce_settings_admin_theme');
   variable_del('imce_custom_content');
   variable_del('imce_custom_process');
   variable_del('imce_custom_init');

+ 14 - 0
sites/all/modules/contrib/files/imce/imce/imce.module

@@ -46,6 +46,20 @@ function imce_menu() {
   return $items;
 }
 
+/**
+ * Implements hook_admin_paths().
+ */
+function imce_admin_paths() {
+  if (variable_get('imce_settings_admin_theme', FALSE)) {
+    return array(
+      'imce' => TRUE,
+      'imce/*' => TRUE,
+      'file/imce/*' => TRUE,
+      'imce-filefield/*' => TRUE,
+    );
+  }
+}
+
 /**
  * Implements hook_permission().
  */

+ 7 - 0
sites/all/modules/contrib/files/imce/imce/inc/imce.admin.inc

@@ -109,6 +109,12 @@ function imce_admin_form($form, &$form_state) {
     '#default_value' => variable_get('imce_settings_disable_private', 1),
     '#description' => t('IMCE serves all files under private files directory without applying any access restrictions. This allows anonymous access to any file(/system/files/filename) unless there is a module restricting access to the files. Here you can disable this feature.'),
   );
+  $form['common']['admin_theme'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Use admin theme for IMCE paths'),
+    '#default_value' => variable_get('imce_settings_admin_theme', FALSE),
+    '#description' => t('If you have user interface issues with the active theme you may consider switching to admin theme.'),
+  );
 
   $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
   $form['#theme'] = 'imce_admin';
@@ -183,6 +189,7 @@ function imce_admin_submit($form, &$form_state) {
   variable_set('imce_settings_replace', $form_state['values']['replace']);
   variable_set('imce_settings_thumb_method', $form_state['values']['thumb_method']);
   variable_set('imce_settings_disable_private', $form_state['values']['disable_private']);
+  variable_set('imce_settings_admin_theme', $form_state['values']['admin_theme']);
   drupal_set_message(t('Changes have been saved.'));
 }
 

+ 7 - 2
sites/all/modules/contrib/files/imce/imce/js/imce.js

@@ -801,12 +801,17 @@ updateUI: function() {
   if (furl.charAt(furl.length - 1) != '/') {
     furl = imce.conf.furl = furl + '/';
   }
-  imce.conf.modfix = imce.conf.clean && furl.indexOf(host + '/system/') > -1;
+  imce.conf.modfix = imce.conf.clean && furl.split('/')[3] === 'system';
   if (absurls && !isabs) {
     imce.conf.furl = baseurl + furl;
   }
   else if (!absurls && isabs && furl.indexOf(baseurl) == 0) {
-    imce.conf.furl = furl.substr(baseurl.length);
+    furl = furl.substr(baseurl.length);
+    // Server base url is defined with a port which is missing in current page url.
+    if (furl.charAt(0) === ':') {
+      furl = furl.replace(/^:\d*/, '');
+    }
+    imce.conf.furl = furl;
   }
   //convert button elements to input elements.
   imce.convertButtons(imce.FW);

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

@@ -1,3 +1,83 @@
+Search API 1.26 (2019-03-11):
+-----------------------------
+- #2324023 by drumm, drunken monkey: Changed Views field definition for to
+  float.
+- #3008849 by pamatt, drunken monkey: Fixed non-exposed numeric and date
+  filters in Views.
+- #3009744 by evgeny.chernyavskiy, drunken monkey: Fixed wrong "continue" in
+  search_api_server_tasks_check().
+- #3003742 by Jelle_S, drunken monkey: Fixed problems with Views date filters.
+- #3002043 by alonaoneill, drunken monkey: Fixed module name capitalization and
+  dependency namespacing in .info files.
+- #2990940 by drunken monkey: Fixed multi-byte handling of Highlight processor.
+- #3001424 by drunken monkey: Fixed notice when configuring the More Like This
+  contextual filter.
+
+Search API 1.25 (2018-09-17):
+-----------------------------
+- #2408727 by swim, drunken monkey: Added a batch operation for executing
+  pending tasks.
+- #2325917 by guillaumev, drunken monkey: Added a Views cache plugin based on
+  Views Content Cache.
+- #2989578 by KarlShea, drunken monkey: Fixed Views exposed form fields for
+  "not between" operator.
+- #2982167 by osopolar, drunken monkey: Added a Drush command for re-indexing
+  specific entities.
+- #1783746 by das-peter, sammys, SpadXIII, drunken monkey, ruloweb, KarlShea,
+  heshanlk, Anas_maw, pinkonomy, Damien Tournoud, rudiedirkx: Added support
+  for the "(not) between" operator.
+- #2408727 by drunken monkey, OliverColeman: Fixed out-of-memory errors when
+  executing pending tasks.
+-  Issue #2948820 by capysara, drunken monkey: Added a link to the "need to
+  reindex" message on the Filters tab.
+- #2828883 by JorgenSandstrom, drunken monkey: Fixed property type for
+  string-typed aggregated fields.
+- #2949899 by drunken monkey, DamienMcKenna: Added a warning against using
+  particular processors with Solr servers to the "Workflow" tab.
+
+Search API 1.24 (2018-04-05):
+-----------------------------
+- #2958201 by jcnventura, drunken monkey: Reverted issue #2566529: Added
+  support for the "Content access" processor for "Multiple types" indexes.
+
+Search API 1.23 (2018-03-31):
+-----------------------------
+- #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of multi-word
+  tokens.
+- #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors at feature
+  module installation in certain edge cases.
+- #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: Fixed
+  highlighting for text with multi-byte characters.
+- #1393064 by xlyz, drunken monkey, jannis: Fixed handling of empty facet
+  filters.
+- #2927692 by drunken monkey, Kristi Wachter: Fixed exposed grouped Views
+  options filters.
+- #2928769 by jannis, drunken monkey: Fixed Views cache not being cleared when
+  enabling indexes.
+- #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: Added support
+  for the "Content access" processor for "Multiple types" indexes.
+- #2905445 by ciss, drunken monkey: Fixed error handling in Views term filter
+  handler.
+- #2904268 by pobster, drunken monkey: Added support for language hierarchy in
+  Views.
+
+Search API 1.22 (2017-07-18):
+-----------------------------
+- #1710212 by drunken monkey: Added a data alteration for indexing a user's
+  content.
+- #2879892 by blacklabel_tom, drunken monkey: Fixed link in description of
+  "Stemmer" processor.
+- #2788593 by drunken monkey: Fixed error in Views query settings for specific
+  setups.
+- #2749963 by drunken monkey: Fixed "Index hierarchy" not having values
+  numerically indexed.
+- #2875793 by drunken monkey: Fixed buggy error handling in Views.
+- #2860624 by drunken monkey: Fixed problem with empty words in Views fulltext
+  filter.
+- #2855447 by mparker17, drunken monkey: Added "Separator" option for
+  aggregated fields of type "Fulltext".
+- #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.txt.
+
 Search API 1.21 (2017-02-23):
 -----------------------------
 - #2780341 by Berdir: Fixed passing of custom ranges to date facets.

+ 3 - 3
sites/all/modules/contrib/search/search_api/README.txt

@@ -31,9 +31,9 @@ Terms as used in this module.
   Sphinx or any other professional or simple indexing mechanism. Takes care of
   the details of all operations, especially indexing or searching content.
 - Server:
-  One specific place for indexing data, using a set service class. Can
-  e.g. be some tables in a database, a connection to a Solr server or other
-  external services, etc.
+  One specific place for indexing data, using a specific service class. For
+  example this could be some tables in a database, a connection to a Solr server
+  or other external services, etc.
 - Index:
   A configuration object for indexing data of a specific type. What and how data
   is indexed is determined by its settings. Also keeps track of which items

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

@@ -115,7 +115,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
     if ($filter == '!') {
       $query_filter->condition($field, NULL, $exclude ? '<>' : '=');
     }
-    elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
+    elseif ($filter && $filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
       $lower = trim(substr($filter, 1, $pos));
       $upper = trim(substr($filter, $pos + 4, -1));
       if ($lower == '*' && $upper == '*') {

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

@@ -1,7 +1,7 @@
-name = Search facets
+name = Search Facets
 description = "Integrate the Search API with the Facet API to provide facetted searches."
-dependencies[] = search_api
-dependencies[] = facetapi
+dependencies[] = search_api:search_api
+dependencies[] = facetapi:facetapi
 core = 7.x
 package = Search
 
@@ -9,9 +9,8 @@ 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 2017-02-23
-version = "7.x-1.21"
+; Information added by Drupal.org packaging script on 2019-03-11
+version = "7.x-1.26"
 core = "7.x"
 project = "search_api"
-datestamp = "1487844493"
-
+datestamp = "1552334832"

+ 15 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt

@@ -40,6 +40,21 @@ in that position. If the query is sorted in this way, then the
 random sort, as an associative array with any of the following keys:
 - seed: A numeric seed value to use for the random sort.
 
+"BETWEEN operator" feature
+--------------------------
+This module defines the "BETWEEN operator" feature (feature key:
+"search_api_between") that adds the "BETWEEN" and "NOT BETWEEN" filter
+operators to search queries. If your search server supports this feature, you
+can use the "Is between" and "Is not between" operators when adding Views
+filters for numeric, string or date types.
+
+For developers:
+A service class that wants to support this feature has to accept "BETWEEN" and
+"NOT BETWEEN" as additional $operator values in query conditions. The value in
+both cases is an array with the keys 0 and 1, with the value under key 0 being
+the lower and the value under key 1 being the upper bound for the range in which
+the field's value should ("BETWEEN") or should not ("NOT BETWEEN") be.
+
 "Facets block" display
 ----------------------
 Most features should be clear to users of Views. However, the module also

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

@@ -79,8 +79,8 @@ class SearchApiViewsHandlerArgument extends views_handler_argument {
   public function option_definition() {
     $options = parent::option_definition();
 
-    $options['break_phrase'] = array('default' => FALSE);
-    $options['not'] = array('default' => FALSE);
+    $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE);
+    $options['not'] = array('default' => FALSE, 'bool' => TRUE);
 
     return $options;
   }

+ 0 - 2
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc

@@ -16,8 +16,6 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
    */
   public function option_definition() {
     $options = parent::option_definition();
-    unset($options['break_phrase']);
-    unset($options['not']);
     $options['entity_type'] = array('default' => FALSE);
     $options['fields'] = array('default' => array());
     return $options;

+ 41 - 7
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_date.inc

@@ -6,9 +6,9 @@
  */
 
 /**
- * Views filter handler base class for handling all "normal" cases.
+ * Views filter handler base class for handling date fields.
  */
-class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
+class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilterNumeric {
 
   /**
    * Add a "widget type" option.
@@ -88,9 +88,22 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
   public function value_form(&$form, &$form_state) {
     parent::value_form($form, $form_state);
 
+    $is_date_popup = ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup'));
+
+    // If the operator is between
+    if ($this->operator == 'between') {
+      if ($is_date_popup) {
+        $form['value']['min']['#type'] = 'date_popup';
+        $form['value']['min']['#date_format'] =  $this->options['date_popup_format'];
+        $form['value']['min']['#date_year_range'] = $this->options['year_range'];
+        $form['value']['max']['#type'] = 'date_popup';
+        $form['value']['max']['#date_format'] =  $this->options['date_popup_format'];
+        $form['value']['max']['#date_year_range'] = $this->options['year_range'];
+      }
+    }
     // If we are using the date popup widget, overwrite the settings of the form
     // according to what date_popup expects.
-    if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
+    elseif ($is_date_popup) {
       $form['value']['#type'] = 'date_popup';
       $form['value']['#date_format'] =  $this->options['date_popup_format'];
       $form['value']['#date_year_range'] = $this->options['year_range'];
@@ -109,17 +122,38 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
    * Add this filter to the query.
    */
   public function query() {
+    $this->normalizeValue();
+
     if ($this->operator === 'empty') {
       $this->query->condition($this->real_field, NULL, '=', $this->options['group']);
     }
     elseif ($this->operator === 'not empty') {
       $this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
     }
-    else {
-      while (is_array($this->value)) {
-        $this->value = $this->value ? reset($this->value) : NULL;
+    elseif (in_array($this->operator, array('between', 'not between'), TRUE)) {
+      $min = $this->value['min'];
+      if ($min !== '') {
+        $min = is_numeric($min) ? $min : strtotime($min, REQUEST_TIME);
+      }
+      $max = $this->value['max'];
+      if ($max !== '') {
+        $max = is_numeric($max) ? $max : strtotime($max, REQUEST_TIME);
       }
-      $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
+
+      if (is_numeric($min) && is_numeric($max)) {
+        $this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']);
+      }
+      elseif (is_numeric($min)) {
+        $operator = $this->operator === 'between' ? '>=' : '<';
+        $this->query->condition($this->real_field, $min, $operator, $this->options['group']);
+      }
+      elseif (is_numeric($max)) {
+        $operator = $this->operator === 'between' ? '<=' : '>';
+        $this->query->condition($this->real_field, $min, $operator, $this->options['group']);
+      }
+    }
+    else {
+      $v = is_numeric($this->value['value']) ? $this->value['value'] : strtotime($this->value['value'], REQUEST_TIME);
       if ($v !== FALSE) {
         $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
       }

+ 6 - 1
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc

@@ -121,6 +121,11 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
     $words = preg_split('/\s+/', $input);
     $quoted = FALSE;
     foreach ($words as $i => $word) {
+      $word_length = drupal_strlen($word);
+      if (!$word_length) {
+        unset($words[$i]);
+        continue;
+      }
       // Protect quoted strings.
       if ($quoted && $word[strlen($word) - 1] === '"') {
         $quoted = FALSE;
@@ -130,7 +135,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
         $quoted = TRUE;
         continue;
       }
-      if (drupal_strlen($word) < $this->options['min_length']) {
+      if ($word_length < $this->options['min_length']) {
         unset($words[$i]);
       }
     }

+ 12 - 4
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc

@@ -18,10 +18,13 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt
    */
   protected function get_value_options() {
     parent::get_value_options();
-    $this->value_options = array(
-      'current' => t("Current user's language"),
-      'default' => t('Default site language'),
-    ) + $this->value_options;
+    $options = array();
+    if (module_exists('language_hierarchy')) {
+      $options['fallback'] = t("Current user's language with fallback");
+    }
+    $options['current'] = t("Current user's language");
+    $options['default'] = t('Default site language');
+    $this->value_options = $options + $this->value_options;
   }
 
   /**
@@ -40,6 +43,11 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt
       elseif ($v == 'default') {
         $this->value[$i] = language_default('language');
       }
+      elseif ($v == 'fallback' && module_exists('language_hierarchy')) {
+        $fallbacks = array($language_content->language => $language_content->language);
+        $fallbacks += array_keys(language_hierarchy_get_ancestors($language_content->language));
+        $this->value[$i] = drupal_map_assoc($fallbacks);
+      }
     }
     parent::query();
   }

+ 248 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_numeric.inc

@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterNumeric.
+ */
+
+/**
+ * Views filter handler class for handling numeric and "string" fields.
+ */
+class SearchApiViewsHandlerFilterNumeric extends SearchApiViewsHandlerFilter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init(&$view, &$options) {
+    parent::init($view, $options);
+
+    $this->normalizeValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+    $options['value'] = array(
+      'contains' => array(
+        'value' => array('default' => ''),
+        'min' => array('default' => ''),
+        'max' => array('default' => ''),
+      ),
+    );
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function operator_options() {
+    $operators = parent::operator_options();
+
+    $index = search_api_index_load(substr($this->table, 17));
+    $server = NULL;
+    try {
+      if ($index) {
+        $server = $index->server();
+      }
+    }
+    catch (SearchApiException $e) {
+      // Ignore.
+    }
+    if ($server && $server->supportsFeature('search_api_between')) {
+      $operators += array(
+        'between' => t('Is between'),
+        'not between' => t('Is not between'),
+      );
+    }
+
+    return $operators;
+  }
+
+  /**
+   * Provides a form for setting the filter value.
+   *
+   * Heavily borrowed from views_handler_filter_numeric.
+   *
+   * @see views_handler_filter_numeric::value_form()
+   */
+  public function value_form(&$form, &$form_state) {
+    $form['value']['#tree'] = TRUE;
+
+    $single_field_operators = $this->operator_options();
+    unset(
+      $single_field_operators['empty'],
+      $single_field_operators['not empty'],
+      $single_field_operators['between'],
+      $single_field_operators['not between']
+    );
+    $between_operators = array('between', 'not between');
+
+    // We have to make some choices when creating this as an exposed
+    // filter form. For example, if the operator is locked and thus
+    // not rendered, we can't render dependencies; instead we only
+    // render the form items we need.
+    $which = 'all';
+    $source = NULL;
+    if (!empty($form['operator'])) {
+      $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator';
+    }
+
+    $identifier = NULL;
+    if (!empty($form_state['exposed'])) {
+      $identifier = $this->options['expose']['identifier'];
+      if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
+        // Exposed and locked.
+        $which = in_array($this->operator, $between_operators) ? 'minmax' : 'value';
+      }
+      else {
+        $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']);
+      }
+    }
+
+    // Hide the value box if the operator is 'empty' or 'not empty'.
+    // Radios share the same selector so we have to add some dummy selector.
+    if ($which == 'all') {
+      $form['value']['value'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Value') : '',
+        '#size' => 30,
+        '#default_value' => $this->value['value'],
+        '#dependency' => array($source => array_keys($single_field_operators)),
+      );
+      if ($identifier && !isset($form_state['input'][$identifier]['value'])) {
+        $form_state['input'][$identifier]['value'] = $this->value['value'];
+      }
+    }
+    elseif ($which == 'value') {
+      // When exposed we drop the value-value and just do value if
+      // the operator is locked.
+      $form['value'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Value') : '',
+        '#size' => 30,
+        '#default_value' => isset($this->value['value']) ? $this->value['value'] : '',
+      );
+      if ($identifier && !isset($form_state['input'][$identifier])) {
+        $form_state['input'][$identifier] = isset($this->value['value']) ? $this->value['value'] : '';
+      }
+    }
+
+    if ($which == 'all' || $which == 'minmax') {
+      $form['value']['min'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Min') : '',
+        '#size' => 30,
+        '#default_value' => $this->value['min'],
+      );
+      $form['value']['max'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('And max') : t('And'),
+        '#size' => 30,
+        '#default_value' => $this->value['max'],
+      );
+
+      if ($which == 'all') {
+        $form['value']['min']['#dependency'] = array($source => $between_operators);
+        $form['value']['max']['#dependency'] = array($source => $between_operators);
+      }
+
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) {
+        $form_state['input'][$identifier]['min'] = $this->value['min'];
+      }
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) {
+        $form_state['input'][$identifier]['max'] = $this->value['max'];
+      }
+
+      if (!isset($form['value']['value'])) {
+        // Ensure there is something in the 'value'.
+        $form['value']['value'] = array(
+          '#type' => 'value',
+          '#value' => NULL,
+        );
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function admin_summary() {
+    if (!empty($this->options['exposed'])) {
+      return t('exposed');
+    }
+
+    if ($this->operator === 'empty') {
+      return t('is empty');
+    }
+    if ($this->operator === 'not empty') {
+      return t('is not empty');
+    }
+
+    if (in_array($this->operator, array('between', 'not between'), TRUE)) {
+      // This is of course wrong for translation purposes, but copied from
+      // views_handler_filter_numeric::admin_summary() so probably still better
+      // to re-use this than to do it correctly.
+      $operator = $this->operator === 'between' ? t('between') : t('not between');
+      $vars = array(
+        '@min' => (string) $this->value['min'],
+        '@max' => (string) $this->value['max'],
+      );
+      return $operator . ' ' . t('@min and @max', $vars);
+    }
+
+    return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value['value']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    $this->normalizeValue();
+
+    if (in_array($this->operator, array('between', 'not between'), TRUE)) {
+      $min = $this->value['min'];
+      $max = $this->value['max'];
+      if ($min !== '' && $max !== '') {
+        $this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']);
+      }
+      elseif ($min !== '') {
+        $operator = $this->operator === 'between' ? '>=' : '<';
+        $this->query->condition($this->real_field, $min, $operator, $this->options['group']);
+      }
+      elseif ($max !== '') {
+        $operator = $this->operator === 'between' ? '<=' : '>';
+        $this->query->condition($this->real_field, $min, $operator, $this->options['group']);
+      }
+    }
+    else {
+      // The parent handler doesn't expect our value structure, just pass the
+      // scalar value instead.
+      $this->value = $this->value['value'];
+      parent::query();
+    }
+  }
+
+  /**
+   * Sets $this->value to an array of options as defined by the filter.
+   *
+   * @see SearchApiViewsHandlerFilterNumeric::option_definition()
+   */
+  protected function normalizeValue() {
+    $value = $this->value;
+    if (is_array($value) && isset($value[0])) {
+      $value = $value[0];
+    }
+    if (!is_array($value)) {
+      $value = array('value' => $value);
+    }
+    $this->value = array(
+      'value' => isset($value['value']) ? $value['value'] : '',
+      'min' => isset($value['min']) ? $value['min'] : '',
+      'max' => isset($value['max']) ? $value['max'] : '',
+    );
+  }
+
+}

+ 27 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc

@@ -121,6 +121,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
    */
   public function option_definition() {
     $options = parent::option_definition();
+    $options['value'] = array('default' => '');
     $options['expose']['contains']['reduce'] = array('default' => FALSE);
     return $options;
   }
@@ -256,6 +257,32 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
     return $operator . (($values !== '') ? ' ' . $values : '');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  function accept_exposed_input($input) {
+    $accepted = parent::accept_exposed_input($input);
+
+    // Grouped filters will have the raw form values structure from the
+    // checkboxes as the value here. Convert that into the correct array of
+    // values instead.
+    if ($accepted && is_array($this->value) && $this->is_a_group()) {
+      // For some reason, Views thinks it's a good idea to nest the form values
+      // into a second array in some cases. That one will be numerically indexed
+      // with just a single entry, though, so it should be relatively easy to
+      // spot.
+      if (count($this->value) && isset($this->value[0])) {
+        $this->value = reset($this->value);
+      }
+      $this->value = array_keys(array_filter($this->value));
+      if (!$this->value) {
+        return FALSE;
+      }
+    }
+
+    return $accepted;
+  }
+
   /**
    * Add this filter to the query.
    */

+ 5 - 1
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc

@@ -321,9 +321,13 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte
    * {@inheritdoc}
    */
   protected function ids_to_strings(array $ids) {
+    $ids = array_filter($ids);
+    if (!$ids) {
+      return '';
+    }
     return implode(', ', db_select('taxonomy_term_data', 'td')
       ->fields('td', array('name'))
-      ->condition('td.tid', array_filter($ids))
+      ->condition('td.tid', $ids)
       ->execute()
       ->fetchCol());
   }

+ 8 - 3
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc

@@ -35,11 +35,16 @@ class SearchApiViewsCache extends views_plugin_cache_time {
     }
 
     $cid = $this->get_results_key();
+    $results = NULL;
+    $query_plugin = $this->view->query;
+    if ($query_plugin instanceof SearchApiViewsQuery) {
+      $results = $query_plugin->getSearchApiResults();
+    }
     $data = array(
       'result' => $this->view->result,
       'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0,
       'current_page' => $this->view->get_current_page(),
-      'search_api results' => $this->view->query->getSearchApiResults(),
+      'search_api results' => $results,
     );
     cache_set($cid, $data, $this->table, $this->cache_set_expire($type));
   }
@@ -80,7 +85,7 @@ class SearchApiViewsCache extends views_plugin_cache_time {
    * Overrides views_plugin_cache::get_cache_key().
    *
    * Use the Search API query as the main source for the key. Note that in
-   * Views < 3.8, this function does not exist.
+   * Views < 3.8, this method does not exist.
    */
   public function get_cache_key($key_data = array()) {
     global $user;
@@ -121,7 +126,7 @@ class SearchApiViewsCache extends views_plugin_cache_time {
   }
 
   /**
-   * Get the Search API query object associated with the current view.
+   * Retrieves the Search API query object associated with the current view.
    *
    * @return SearchApiQueryInterface|null
    *   The Search API query object associated with the current view; or NULL if

+ 146 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_content_cache.inc

@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiViewsContentCache class.
+ */
+
+/**
+ * Plugin class for caching Search API views, with additional invalidation.
+ */
+class SearchApiViewsContentCache extends views_content_cache_plugin_cache {
+
+  /**
+   * Static cache for get_results_key().
+   *
+   * @var string
+   */
+  protected $_results_key = NULL;
+
+  /**
+   * Static cache for getSearchApiQuery().
+   *
+   * @var SearchApiQueryInterface
+   */
+  protected $search_api_query = NULL;
+
+  /**
+   * Overrides views_plugin_cache::cache_set().
+   *
+   * Also stores Search API's internal search results.
+   */
+  public function cache_set($type) {
+    if ($type != 'results') {
+      return parent::cache_set($type);
+    }
+
+    $cid = $this->get_results_key();
+    $results = NULL;
+    $query_plugin = $this->view->query;
+    if ($query_plugin instanceof SearchApiViewsQuery) {
+      $results = $query_plugin->getSearchApiResults();
+    }
+    $data = array(
+      'result' => $this->view->result,
+      'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0,
+      'current_page' => $this->view->get_current_page(),
+      'search_api results' => $results,
+    );
+    cache_set($cid, $data, $this->table, $this->cache_set_expire($type));
+  }
+
+  /**
+   * Overrides views_plugin_cache::cache_get().
+   *
+   * Additionally stores successfully retrieved results with
+   * search_api_current_search().
+   */
+  public function cache_get($type) {
+    if ($type != 'results') {
+      return parent::cache_get($type);
+    }
+
+    // Values to set: $view->result, $view->total_rows, $view->execute_time,
+    // $view->current_page.
+    if ($cache = cache_get($this->get_results_key(), $this->table)) {
+      $cutoff = $this->cache_expire($type);
+      if (!$cutoff || $cache->created > $cutoff) {
+        $this->view->result = $cache->data['result'];
+        $this->view->total_rows = $cache->data['total_rows'];
+        $this->view->set_current_page($cache->data['current_page']);
+        $this->view->execute_time = 0;
+
+        // Trick Search API into believing a search happened, to make facetting
+        // et al. work.
+        $query = $this->getSearchApiQuery();
+        search_api_current_search($query->getOption('search id'), $query, $cache->data['search_api results']);
+
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Overrides views_plugin_cache::get_cache_key().
+   *
+   * Use the Search API query as the main source for the key. Note that in
+   * Views < 3.8, this method does not exist.
+   */
+  public function get_cache_key($key_data = array()) {
+    global $user;
+
+    if (!isset($this->_results_key)) {
+      $query = $this->getSearchApiQuery();
+      $query->preExecute();
+      $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
+      // Search API query object we use.
+      if (isset($_GET['exposed_info'])) {
+        $key_data['exposed_info'] = $_GET['exposed_info'];
+      }
+    }
+    $key = drupal_hash_base64(serialize($key_data));
+    return $key;
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Retrieves the Search API query object associated with the current view.
+   *
+   * @return SearchApiQueryInterface|null
+   *   The Search API query object associated with the current view; or NULL if
+   *   there is none.
+   */
+  protected function getSearchApiQuery() {
+    if (!isset($this->search_api_query)) {
+      $this->search_api_query = FALSE;
+      if (isset($this->view->query) && $this->view->query instanceof SearchApiViewsQuery) {
+        $this->search_api_query = $this->view->query->getSearchApiQuery();
+      }
+    }
+
+    return $this->search_api_query ? $this->search_api_query : NULL;
+  }
+
+}

+ 4 - 2
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc

@@ -135,7 +135,9 @@ class SearchApiViewsQuery extends views_plugin_query {
    *   The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
    */
   public function add_selector_orderby($selector, $order = 'ASC') {
-    $this->query->sort($selector, $order);
+    if (!$this->errors) {
+      $this->query->sort($selector, $order);
+    }
   }
 
   /**
@@ -213,7 +215,7 @@ class SearchApiViewsQuery extends views_plugin_query {
       '#default_value' => $this->options['search_api_bypass_access'],
     );
 
-    if ($this->index->getEntityType()) {
+    if ($this->index && $this->index->getEntityType()) {
       $form['entity_access'] = array(
         '#type' => 'checkbox',
         '#title' => t('Additional access checks on result entities'),

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

@@ -1,7 +1,7 @@
-name = Search views
+name = Search Views
 description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
-dependencies[] = search_api
-dependencies[] = views
+dependencies[] = search_api:search_api
+dependencies[] = views:views
 core = 7.x
 package = Search
 
@@ -19,17 +19,18 @@ files[] = includes/handler_filter_date.inc
 files[] = includes/handler_filter_entity.inc
 files[] = includes/handler_filter_fulltext.inc
 files[] = includes/handler_filter_language.inc
+files[] = includes/handler_filter_numeric.inc
 files[] = includes/handler_filter_options.inc
 files[] = includes/handler_filter_taxonomy_term.inc
 files[] = includes/handler_filter_text.inc
 files[] = includes/handler_filter_user.inc
 files[] = includes/handler_sort.inc
 files[] = includes/plugin_cache.inc
+files[] = includes/plugin_content_cache.inc
 files[] = includes/query.inc
 
-; Information added by Drupal.org packaging script on 2017-02-23
-version = "7.x-1.21"
+; Information added by Drupal.org packaging script on 2019-03-11
+version = "7.x-1.26"
 core = "7.x"
 project = "search_api"
-datestamp = "1487844493"
-
+datestamp = "1552334832"

+ 7 - 2
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module

@@ -27,8 +27,11 @@ function search_api_views_search_api_index_insert() {
  */
 function search_api_views_search_api_index_update(SearchApiIndex $index) {
   // Check whether index was disabled.
-  if (!$index->enabled && $index->original->enabled) {
+  $is_enabled = $index->enabled;
+  $was_enabled = $index->original->enabled;
+  if (!$is_enabled && $was_enabled) {
     _search_api_views_index_unavailable($index);
+    return;
   }
 
   // Check whether the indexed fields changed.
@@ -36,7 +39,9 @@ function search_api_views_search_api_index_update(SearchApiIndex $index) {
   $old_fields = $old_fields['fields'];
   $new_fields = $index->options + array('fields' => array());
   $new_fields = $new_fields['fields'];
-  if ($old_fields != $new_fields) {
+
+  // If the index was enabled or its fields changed, invalidate the Views cache.
+  if ($is_enabled != $was_enabled || $old_fields != $new_fields) {
     views_invalidate_cache();
   }
 }

+ 14 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc

@@ -99,6 +99,7 @@ function search_api_views_views_data() {
       $table['search_api_relevance']['title'] = t('Relevance');
       $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query.');
       $table['search_api_relevance']['field']['type'] = 'decimal';
+      $table['search_api_relevance']['field']['float'] = TRUE;
       $table['search_api_relevance']['field']['handler'] = 'entity_views_handler_field_numeric';
       $table['search_api_relevance']['field']['click sortable'] = TRUE;
       $table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort';
@@ -219,6 +220,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
       $table[$id]['filter']['vocabulary'] = $vocabulary;
     }
   }
+  elseif (in_array($inner_type, array('integer', 'decimal', 'duration', 'string'))) {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterNumeric';
+  }
   else {
     $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
   }
@@ -285,6 +289,16 @@ function search_api_views_views_plugins() {
     );
   }
 
+  if (module_exists('views_content_cache')) {
+    $ret['cache']['search_api_views_content_cache'] = array(
+      'title' => t('Search-specific content-based'),
+      'help' => t("Cache Search API views based on content updates. (Requires Views Content Cache)"),
+      'base' => $bases,
+      'handler' => 'SearchApiViewsContentCache',
+      'uses options' => TRUE,
+    );
+  }
+
   return $ret;
 }
 

+ 35 - 7
sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc

@@ -20,11 +20,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
    */
   protected $reductionType;
 
+  /**
+   * A separator to use when the aggregation type is 'fulltext'.
+   *
+   * Used to temporarily store a string separator when the aggregation type is
+   * "fulltext", for use in SearchApiAlterAddAggregation::reduce() with
+   * array_reduce().
+   *
+   * @var string
+   */
+  protected $fulltextReductionSeparator;
+
   public function configurationForm() {
     $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
 
     $fields = $this->index->getFields(FALSE);
     $field_options = array();
+    $field_properties = array();
     foreach ($fields as $name => $field) {
       $field_options[$name] = check_plain($field['name']);
       $field_properties[$name] = array(
@@ -79,9 +91,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
         '#required' => TRUE,
       );
       $form['fields'][$name]['type_descriptions'] = $type_descriptions;
+      $type_selector = ':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]';
       foreach (array_keys($types) as $type) {
-        $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
+        $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][$type_selector]['value'] = $type;
       }
+      $form['fields'][$name]['separator'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Fulltext separator'),
+        '#description' => t('For aggregation type "Fulltext", set the text that should be used to separate the aggregated field values. Use "\t" for tabs and "\n" for newline characters.'),
+        '#default_value' => addcslashes(isset($field['separator']) ? $field['separator'] : "\n\n", "\0..\37\\"),
+        '#states' => array(
+          'visible' => array(
+            $type_selector => array(
+              'value' => 'fulltext',
+            ),
+          ),
+        ),
+      );
       $form['fields'][$name]['fields'] = array_merge($field_properties, array(
         '#type' => 'checkboxes',
         '#title' => t('Contained fields'),
@@ -125,11 +151,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
       return;
     }
     foreach ($values['fields'] as $name => $field) {
-      $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
       unset($values['fields'][$name]['actions']);
+      $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
       if ($field['name'] && !$fields) {
         form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.'));
       }
+      $values['fields'][$name]['separator'] = stripcslashes($field['separator']);
     }
   }
 
@@ -176,6 +203,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
             $values = $this->flattenArray($values);
 
             $this->reductionType = $field['type'];
+            $this->fulltextReductionSeparator = isset($field['separator']) ? $field['separator'] : "\n\n";
             $item->$name = array_reduce($values, array($this, 'reduce'), NULL);
             if ($field['type'] == 'count' && !$item->$name) {
               $item->$name = 0;
@@ -192,7 +220,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
   public function reduce($a, $b) {
     switch ($this->reductionType) {
       case 'fulltext':
-        return isset($a) ? $a . "\n\n" . $b : $b;
+        return isset($a) ? $a . $this->fulltextReductionSeparator . $b : $b;
       case 'sum':
         return $a + $b;
       case 'count':
@@ -300,10 +328,10 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
           'count' => 'integer',
           'max' => 'integer',
           'min' => 'integer',
-          'first' => 'string',
-          'first_char' => 'string',
-          'last' => 'string',
-          'list' => 'list<string>',
+          'first' => 'token',
+          'first_char' => 'token',
+          'last' => 'token',
+          'list' => 'list<token>',
         );
       case 'description':
         return array(

+ 1 - 1
sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc

@@ -108,7 +108,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
         $this->extractHierarchy($child, $prop, $values[$key]);
       }
       foreach ($values as $key => $value) {
-        $item->$key = $value;
+        $item->$key = array_values($value);
       }
     }
   }

+ 57 - 0
sites/all/modules/contrib/search/search_api/includes/callback_user_content.inc

@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterAddUserContent.
+ */
+
+/**
+ * Adds the nodes created by the indexed user for indexing.
+ */
+class SearchApiAlterAddUserContent extends SearchApiAbstractAlterCallback {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return $index->getEntityType() === 'user';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function propertyInfo() {
+    return array(
+      'search_api_user_content' => array(
+        'label' => t('User content'),
+        'description' => t('The nodes created by this user'),
+        'type' => 'list<node>',
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterItems(array &$items) {
+    $uids = array();
+    foreach ($items as $item) {
+      $uids[] = $item->uid;
+    }
+
+    $sql = 'SELECT nid, uid FROM {node} WHERE uid IN (:uids)';
+    $nids = db_query($sql, array(':uids' => $uids));
+    $user_nodes = array();
+    foreach ($nids as $row) {
+      $user_nodes[$row->uid][] = $row->nid;
+    }
+
+    foreach ($items as $item) {
+      $item->search_api_user_content = array();
+      if (!empty($user_nodes[$item->uid])) {
+        $item->search_api_user_content = $user_nodes[$item->uid];
+      }
+    }
+  }
+
+}

+ 88 - 46
sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc

@@ -315,99 +315,141 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
    * @param array $keys
    *   Search keywords entered by the user.
    *
-   * @return string
-   *   A string containing HTML for the excerpt.
+   * @return string|null
+   *   A string containing HTML for the excerpt, or NULL if none could be
+   *   created.
    */
   protected function createExcerpt($text, array $keys) {
     // Prepare text by stripping HTML tags and decoding HTML entities.
     $text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text));
-    $text = ' ' . decode_entities($text);
+    $text = decode_entities($text);
+    $text = preg_replace('/\s+/', ' ', $text);
+    $text = trim($text, ' ');
+    $text_length = strlen($text);
 
-    // Extract fragments around keywords.
-    // First we collect ranges of text around each keyword, starting/ending
-    // at spaces, trying to get to the requested length.
-    // If the sum of all fragments is too short, we look for second occurrences.
+    // Try to reach the requested excerpt length with about two fragments (each
+    // with a keyword and some context).
     $ranges = array();
-    $included = array();
     $length = 0;
-    $work_keys = $keys;
-    while ($length < $this->options['excerpt_length'] && $work_keys) {
-      foreach ($work_keys as $k => $key) {
-        if ($length >= $this->options['excerpt_length']) {
+    $look_start = array();
+    $remaining_keys = $keys;
+
+    // Get the set excerpt length from the configuration. If the length is too
+    // small, only use one fragment.
+    $excerpt_length = $this->options['excerpt_length'];
+    $context_length = round($excerpt_length / 4) - 3;
+    if ($context_length < 32) {
+      $context_length = round($excerpt_length / 2) - 1;
+    }
+
+    while ($length < $excerpt_length && !empty($remaining_keys)) {
+      $found_keys = array();
+      foreach ($remaining_keys as $key) {
+        if ($length >= $excerpt_length) {
           break;
         }
-        // Remember occurrence of key so we can skip over it if more occurrences
-        // are desired.
-        if (!isset($included[$key])) {
-          $included[$key] = 0;
+
+        // Remember where we last found $key, in case we are coming through a
+        // second time.
+        if (!isset($look_start[$key])) {
+          $look_start[$key] = 0;
         }
-        // Locate a keyword (position $p, always >0 because $text starts with a
-        // space).
-        $p = 0;
-        if (empty($this->options['highlight_partial'])) {
-          $regex = '/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu';
-          if (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
-            $p = $match[0][1];
+
+        // See if we can find $key after where we found it the last time. Since
+        // we are requiring a match on a word boundary, make sure $text starts
+        // and ends with a space.
+        $matches = array();
+
+        if (!$this->options['highlight_partial']) {
+          $found_position = FALSE;
+          $regex = '/' . static::$boundary . preg_quote($key, '/') . static::$boundary . '/iu';
+          if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) {
+            $found_position = $matches[0][1];
           }
         }
         else {
-          $p = stripos($text, $key, $included[$key]);
+          $found_position = stripos($text, $key, $look_start[$key]);
         }
-        // Now locate a space in front (position $q) and behind it (position $s),
-        // leaving about 60 characters extra before and after for context.
-        // Note that a space was added to the front and end of $text above.
-        if ($p) {
-          if (($q = strpos(' ' . $text, ' ', max(0, $p - 61))) !== FALSE) {
-            $end = substr($text . ' ', $p, 80);
-            if (($s = strrpos($end, ' ')) !== FALSE) {
-              // Account for the added spaces.
-              $q = max($q - 1, 0);
-              $s = min($s, strlen($end) - 1);
-              $ranges[$q] = $p + $s;
-              $length += $p + $s - $q;
-              $included[$key] = $p + 1;
-              continue;
+        if ($found_position !== FALSE) {
+          $look_start[$key] = $found_position + 1;
+          // Keep track of which keys we found this time, in case we need to
+          // pass through again to find more text.
+          $found_keys[] = $key;
+
+          // Locate a space before and after this match, leaving some context on
+          // each end.
+          if ($found_position > $context_length) {
+            $before = strpos($text, ' ', $found_position - $context_length);
+            if ($before !== FALSE) {
+              ++$before;
+            }
+          }
+          else {
+            $before = 0;
+          }
+          if ($before !== FALSE && $before <= $found_position) {
+            if ($text_length > $found_position + $context_length) {
+              $after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position);
+            }
+            else {
+              $after = $text_length;
+            }
+            if ($after !== FALSE && $after > $found_position) {
+              if ($before < $after) {
+                // Save this range.
+                $ranges[$before] = $after;
+                $length += $after - $before;
+              }
             }
           }
         }
-        // Unless we got a match above, we don't need to look for this key any
-        // more.
-        unset($work_keys[$k]);
       }
+      // Next time through this loop, only look for keys we found this time,
+      // if any.
+      $remaining_keys = $found_keys;
     }
 
-    if (count($ranges) == 0) {
-      // We didn't find any keyword matches, so just return NULL.
+    if (!$ranges) {
+      // We didn't find any keyword matches, return NULL.
       return NULL;
     }
 
     // Sort the text ranges by starting position.
     ksort($ranges);
 
-    // Now we collapse overlapping text ranges into one. The sorting makes it O(n).
+    // Collapse overlapping text ranges into one. The sorting makes it O(n).
     $newranges = array();
+    $from1 = $to1 = NULL;
     foreach ($ranges as $from2 => $to2) {
-      if (!isset($from1)) {
+      if ($from1 === NULL) {
+        // This is the first time through this loop: initialize.
         $from1 = $from2;
         $to1 = $to2;
         continue;
       }
       if ($from2 <= $to1) {
+        // The ranges overlap: combine them.
         $to1 = max($to1, $to2);
       }
       else {
+        // The ranges do not overlap: save the working range and start a new
+        // one.
         $newranges[$from1] = $to1;
         $from1 = $from2;
         $to1 = $to2;
       }
     }
+    // Save the remaining working range.
     $newranges[$from1] = $to1;
 
-    // Fetch text
+    // Fetch text within the combined ranges we found.
     $out = array();
     foreach ($newranges as $from => $to) {
       $out[] = substr($text, $from, $to - $from);
     }
+    if (!$out) {
+      return NULL;
+    }
 
     // Let translators have the ... separator text as one chunk.
     $dots = explode('!excerpt', t('... !excerpt ... !excerpt ...'));

+ 3 - 4
sites/all/modules/contrib/search/search_api/includes/processor_stemmer.inc

@@ -24,12 +24,11 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor {
     $form = parent::configurationForm();
 
     $args = array(
-      '!algorithm' => url('https://github.com/markfullmer/porter2'),
-      '!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'),
+      '@algorithm' => url('http://snowball.tartarus.org/algorithms/english/stemmer.html'),
     );
     $form += array(
       'help' => array(
-        '#markup' => '<p>' . t('Optionally, provide an exclusion list to override the stemmer algorithm. Read about the <a href="@algorithm">algorithm</a> and <a href="@exclusions">exclusions</a>.', $args) . '</p>',
+        '#markup' => '<p>' . t('Optionally, provide an exclusion list to override the stemmer algorithm. (<a href="@algorithm">Read about the algorithm</a>.)', $args) . '</p>',
       ),
       'exceptions' => array(
         '#type' => 'textarea',
@@ -66,7 +65,7 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor {
         $stemmed[] = $word;
       }
     }
-    $value = implode('', $stemmed);
+    $value = implode(' ', $stemmed);
   }
 
   /**

+ 39 - 10
sites/all/modules/contrib/search/search_api/search_api.admin.inc

@@ -528,7 +528,7 @@ function theme_search_api_server(array $variables) {
 }
 
 /**
- * Form constructor for completely clearing a server.
+ * Form constructor for server operations.
  *
  * @param SearchApiServer $server
  *   The server for which the form is displayed.
@@ -543,15 +543,39 @@ function search_api_server_status_form(array $form, array &$form_state, SearchAp
   $form['clear'] = array(
     '#type' => 'submit',
     '#value' => t('Delete all indexed data on this server'),
+    '#submit' => array('search_api_server_status_form_clear_submit')
   );
 
+  $count = $server->enabled ? search_api_server_tasks_count($server) : 0;
+  if ($count) {
+    $message = format_plural($count, '@count pending task must be executed before indexing.', '@count pending tasks must be executed before indexing.');
+    drupal_set_message($message, 'warning', FALSE);
+    $form['execute_pending_tasks'] = array(
+      '#type' => 'submit',
+      '#value' => t('Execute all pending tasks on this server'),
+      '#submit' => array('search_api_server_status_form_execute_pending_tasks_submit')
+    );
+  }
+
   return $form;
 }
 
 /**
-* Form submission handler for search_api_server_status_form().
-*/
-function search_api_server_status_form_submit(array $form, array &$form_state) {
+ * Form submission handler for search_api_server_status_form().
+ *
+ * Used for the "Execute all pending tasks" button.
+ */
+function search_api_server_status_form_execute_pending_tasks_submit($form, &$form_state) {
+  $server_id = $form_state['server']->machine_name;
+  $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/execute-tasks";
+}
+
+/**
+ * Form submission handler for search_api_server_status_form().
+ *
+ * Used for the "Delete all indexed data" button.
+ */
+function search_api_server_status_form_clear_submit(array $form, array &$form_state) {
   $server_id = $form_state['server']->machine_name;
   $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/clear";
 }
@@ -1566,10 +1590,13 @@ function search_api_admin_index_workflow(array $form, array &$form_state, Search
   $form['processors'] = array(
     '#type' => 'fieldset',
     '#title' => t('Processors'),
-    '#description' => t('Select processors which will pre- and post-process data at index and search time, and their order. ' .
-        'Most processors will only influence fulltext fields, but refer to their individual descriptions for details regarding their effect.'),
+    '#description' => '<p>' . t("Select processors which will pre- and post-process data at index and search time, and their order. Most processors will only influence fulltext fields, but refer to their individual descriptions for details regarding their effect.<br />Also, some processors shouldn't be used with more advanced search engines (like Solr or Elasticsearch), since the search engine already provides this functionality.") . '</p>',
     '#collapsible' => TRUE,
   );
+  if ($index->server) {
+    $form['processors']['#description'] .= '<p>' . t("Check the <a href='@server-url'>server's</a> service class description for details.",
+        array('@server-url' => url('admin/config/search/search_api/server/' . $index->server . '/edit'))) . '</p>';
+  }
 
   // Processor status.
   $form['processors']['status'] = array(
@@ -1696,6 +1723,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
   unset($values['callbacks']['settings']);
   unset($values['processors']['settings']);
   $index = $form_state['index'];
+  $index_path = 'admin/config/search/search_api/index/' . $index->machine_name;
 
   $options = empty($index->options) ? array() : $index->options;
 
@@ -1761,13 +1789,14 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
 
     $index->save();
     $index->reindex();
-    drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."));
+    $vars = array('@url' => url($index_path));
+    drupal_set_message(t('The indexing workflow was successfully edited. All content was scheduled for <a href="@url">re-indexing</a> so the new settings can take effect.', $vars));
   }
   else {
     drupal_set_message(t('No values were changed.'));
   }
 
-  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
+  $form_state['redirect'] = $index_path . '/workflow';
 }
 
 /**
@@ -1822,8 +1851,8 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
         'In any case, fields of type "Fulltext" will always be fulltext-searchable.</p>'),
   );
   if ($index->server) {
-    $form['description']['#description'] .= '<p>' . t('Check the <a href="@server-url">' . "server's</a> service class description for details.",
-        array('@server-url' => url('admin/config/search/search_api/server/' . $index->server))) . '</p>';
+    $form['description']['#description'] .= '<p>' . t("Check the <a href='@server-url'>server's</a> service class description for details.",
+        array('@server-url' => url('admin/config/search/search_api/server/' . $index->server . '/edit'))) . '</p>';
   }
   foreach ($fields as $key => $info) {
     $form['fields'][$key]['title']['#markup'] = check_plain($info['name']);

+ 80 - 0
sites/all/modules/contrib/search/search_api/search_api.drush.inc

@@ -95,6 +95,18 @@ function search_api_drush_command() {
     'aliases' => array('sapi-r'),
   );
 
+  $items['search-api-reindex-items'] = array(
+    'description' => 'Force re-indexing of one or more specific items.',
+    'examples' => array(
+      'drush search-api-reindex-items node 12,34,56' => dt('Schedule the nodes with ID 12, 34 and 56 for re-indexing.'),
+    ),
+    'arguments' => array(
+      'entity_type' => dt('The entity type whose items should be re-indexed.'),
+      'entities' => dt('The entities of the given entity type to be re-indexed.'),
+    ),
+    'aliases' => array('sapi-ri'),
+  );
+
   $items['search-api-clear'] = array(
     'description' => 'Clear one or all search indexes and mark them for re-indexing.',
     'examples' => array(
@@ -109,6 +121,19 @@ function search_api_drush_command() {
     'aliases' => array('sapi-c'),
   );
 
+  $items['search-api-execute-tasks'] = array(
+    'description' => 'Execute all pending tasks or all for a given server.',
+    'examples' => array(
+      'drush search-api-execute-tasks my_solr_server' => dt('Execute all pending tasks on !server', array('!server' => 'my_solr_server')),
+      'drush sapi-et my_solr_server' => dt('Execute all pending tasks on !server', array('!server' => 'my_solr_server')),
+      'drush sapi-et' => dt('Execute all pending tasks on all servers.')
+    ),
+    'arguments' => array(
+      'server_id' => dt('The numeric ID or machine name of a server to execute tasks on.'),
+    ),
+    'aliases' => array('sapi-et')
+  );
+
   $items['search-api-set-index-server'] = array(
     'description' => 'Set the search server used by a given index.',
     'examples' => array(
@@ -448,6 +473,33 @@ function drush_search_api_reindex($index_id = NULL) {
   }
 }
 
+/**
+ * Marks the given entities as needing to be re-indexed.
+ */
+function drush_search_api_reindex_items($entity_type, $entities) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+
+  // Validate list of entity ids.
+  if (!empty($entities) && !preg_match('#^[0-9]*(,[0-9]*)*$#', $entities)) {
+    drush_log(dt('Entities should be a single numeric entity ID or a list with the numeric entity IDs separated by comma.'), 'error');
+    return;
+  }
+
+  $ids = explode(',', $entities);
+
+  if (!empty($ids)) {
+    search_api_track_item_change($entity_type, $ids);
+
+    $combined_ids = array();
+    foreach ($ids as $id) {
+      $combined_ids[] = $entity_type . '/' . $id;
+    }
+    search_api_track_item_change('multiple', $combined_ids);
+  }
+}
+
 /**
  * Clear an index.
  */
@@ -466,6 +518,34 @@ function drush_search_api_clear($index_id = NULL) {
   }
 }
 
+/**
+ * Execute all pending tasks or all for a given server.
+ */
+function drush_search_api_execute_tasks($server_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+
+  // Attempt to load the associated server.
+  $server = NULL;
+  if ($server_id) {
+    $servers = search_api_drush_get_server($server_id);
+    if (!$servers) {
+      return;
+    }
+    $server = reset($servers);
+  }
+
+  // Process batch op with drush.
+  try {
+    search_api_execute_pending_tasks($server);
+    drush_log(dt('!server tasks have been successfully executed.', array('!server' => $server->machine_name ? $server->machine_name : 'All')), 'ok');
+  }
+  catch (SearchApiException $e) {
+    drush_log($e->getMessage(), 'error');
+  }
+}
+
 /**
  * Set the server for a given index.
  */

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

@@ -1,6 +1,6 @@
 name = Search API
 description = "Provides a generic API for modules offering search capabilities."
-dependencies[] = entity
+dependencies[] = entity:entity
 core = 7.x
 package = Search
 
@@ -16,6 +16,7 @@ files[] = includes/callback_language_control.inc
 files[] = includes/callback_node_access.inc
 files[] = includes/callback_node_status.inc
 files[] = includes/callback_role_filter.inc
+files[] = includes/callback_user_content.inc
 files[] = includes/callback_user_status.inc
 files[] = includes/datasource.inc
 files[] = includes/datasource_entity.inc
@@ -37,9 +38,8 @@ files[] = includes/service.inc
 
 configure = admin/config/search/search_api
 
-; Information added by Drupal.org packaging script on 2017-02-23
-version = "7.x-1.21"
+; Information added by Drupal.org packaging script on 2019-03-11
+version = "7.x-1.26"
 core = "7.x"
 project = "search_api"
-datestamp = "1487844493"
-
+datestamp = "1552334832"

+ 45 - 0
sites/all/modules/contrib/search/search_api/search_api.install

@@ -264,6 +264,51 @@ function search_api_schema() {
   return $schema;
 }
 
+/**
+ * Implements hook_requirements().
+ */
+function search_api_requirements($phase) {
+  $requirements = array();
+
+  if ($phase == 'runtime') {
+    // Check whether at least one server has pending tasks.
+    if (search_api_server_tasks_count()) {
+      $items = array();
+
+      $conditions = array('enabled' => TRUE);
+      foreach (search_api_server_load_multiple(FALSE, $conditions) as $server) {
+        $count = search_api_server_tasks_count($server);
+        if ($count) {
+          $args = array(
+            '@name' => $server->name,
+          );
+          $text = format_plural($count, '@name has @count pending task.', '@name has @count pending tasks.', $args);
+          $items[] = l($text, "admin/config/search/search_api/server/{$server->machine_name}/execute-tasks");
+        }
+      }
+
+      if ($items) {
+        $text = t('There are pending tasks for the following servers:');
+        $text .= theme('item_list', array(
+          'type' => 'ul',
+          'items' => $items,
+        ));
+        if (count($items) > 1) {
+          $label = t('Execute pending tasks on all servers');
+          $text .= l($label, 'admin/config/search/search_api/execute-tasks');
+        }
+        $requirements['search_api_pending_tasks'] = array(
+          'title' => t('Search API'),
+          'value' => $text,
+          'severity' => REQUIREMENT_WARNING,
+        );
+      }
+    }
+  }
+
+  return $requirements;
+}
+
 /**
  * Implements hook_install().
  *

+ 154 - 2
sites/all/modules/contrib/search/search_api/search_api.module

@@ -72,6 +72,15 @@ function search_api_menu() {
     'type' => MENU_LOCAL_TASK,
     'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
   );
+  $items[$pre . '/server/%search_api_server/execute-tasks'] = array(
+    'title' => 'Execute pending tasks',
+    'description' => 'Attempt to process pending tasks for a given server.',
+    'page callback' => 'search_api_execute_pending_tasks',
+    'page arguments' => array(5),
+    'access callback' => 'search_api_access_execute_tasks_batch',
+    'access arguments' => array(5),
+    'type' => MENU_CALLBACK,
+  );
   $items[$pre . '/server/%search_api_server/disable'] = array(
     'title' => 'Disable',
     'description' => 'Disable index.',
@@ -98,6 +107,13 @@ function search_api_menu() {
     'context' => MENU_CONTEXT_INLINE,
     'weight' => 10,
   );
+  $items[$pre . '/execute-tasks'] = array(
+    'title' => 'Execute pending tasks',
+    'description' => 'Attempt to process pending server tasks.',
+    'page callback' => 'search_api_execute_pending_tasks',
+    'access callback' => 'search_api_access_execute_tasks_batch',
+    'type' => MENU_LOCAL_ACTION,
+  );
   $items[$pre . '/index/%search_api_index'] = array(
     'title' => 'View index',
     'title callback' => 'search_api_admin_item_title',
@@ -1025,6 +1041,28 @@ function search_api_search_api_item_type_info() {
   return $types;
 }
 
+/**
+ * Implements hook_module_implements_alter().
+ *
+ * Ensures the item type and service class static caches are invalidated at the
+ * right time.
+ */
+function search_api_module_implements_alter(array &$implementations, $hook) {
+  switch ($hook) {
+    case 'modules_enabled':
+      $group = $implementations['search_api'];
+      unset($implementations['search_api']);
+      $implementations = array('search_api' => $group) + $implementations;
+      break;
+
+    case 'modules_disabled':
+      $group = $implementations['search_api'];
+      unset($implementations['search_api']);
+      $implementations['search_api'] = $group;
+      break;
+  }
+}
+
 /**
  * Implements hook_modules_enabled().
  */
@@ -1103,6 +1141,11 @@ function search_api_search_api_alter_callback_info() {
     'description' => t('Exclude unpublished nodes from the index. <strong>Caution:</strong> This only affects the indexed nodes themselves. If an enabled node has references to disabled nodes, those will still be indexed (or displayed) normally.'),
     'class' => 'SearchApiAlterNodeStatus',
   );
+  $callbacks['search_api_alter_user_content'] = array(
+    'name' => t('Add user content'),
+    'description' => t('Allows indexing of nodes (and their fields) created by the indexed user. (Caution: This might lead to performance problems, or even errors during indexing, on larger sites.)'),
+    'class' => 'SearchApiAlterAddUserContent',
+  );
   $callbacks['search_api_alter_user_status'] = array(
     'name' => t('Exclude blocked users'),
     'description' => t('Exclude blocked users from the index. <strong>Caution:</strong> This only affects the indexed users themselves. If an active user account includes a reference to a disabled user, that reference will still be indexed (or displayed) normally.'),
@@ -1382,6 +1425,10 @@ function search_api_server_tasks_check(SearchApiServer $server = NULL) {
   // Sometimes the order of tasks might be important, so make sure to order by
   // the task ID (which should be in order of insertion).
   $select->orderBy('t.id');
+  // Only retrieve and execute 100 tasks at once, to avoid running out of memory
+  // or time. We just can't do anything else until all tasks have been resolved,
+  // but at least we shouldn't crash sites, or keep piling up tasks, that way.
+  $select->range(0, 100);
   $tasks = $select->execute();
 
   $executed_tasks = array();
@@ -1429,7 +1476,7 @@ function search_api_server_tasks_check(SearchApiServer $server = NULL) {
 
       default:
         // This should never happen.
-        continue;
+        continue 2;
     }
     $executed_tasks[] = $task->id;
   }
@@ -1438,11 +1485,116 @@ function search_api_server_tasks_check(SearchApiServer $server = NULL) {
   if (!$executed_tasks) {
     return TRUE;
   }
-  // Otherwise, delete the executed tasks and check if new tasks were created.
+  // Otherwise, delete the executed tasks and check if new tasks were created
+  // (or if we didn't even fetch all due to the 100 tasks limit).
   search_api_server_tasks_delete($executed_tasks);
   return $count_query->execute()->fetchField() === 0;
 }
 
+/**
+ * Provides a batch wrapper for search_api_server_tasks_check().
+ *
+ * @param SearchApiServer|null $server
+ *   (optional) The server whose tasks should be executed, or NULL to execute
+ *   tasks for all servers.
+ */
+function search_api_execute_pending_tasks(SearchApiServer $server = NULL) {
+  batch_set(array(
+    'title' => t('Processing pending tasks'),
+    'operations' => array(
+      array(
+        'search_api_execute_pending_tasks_batch',
+        array(
+          $server,
+        ),
+      ),
+    ),
+    'finished' => 'search_api_execute_pending_tasks_finished'
+  ));
+  if ($server) {
+    $path = 'admin/config/search/search_api/server/' . $server->machine_name;
+  }
+  else {
+    $path = 'admin/config/search/search_api';
+  }
+
+  if (function_exists('drush_backend_batch_process')) {
+    drush_backend_batch_process();
+  }
+  else {
+    batch_process($path);
+  }
+}
+
+/**
+ * Executes pending server tasks as part of a batch operation.
+ */
+function search_api_execute_pending_tasks_batch(SearchApiServer $server = NULL, &$context) {
+  if (!isset($context['results']['total'])) {
+    $context['results']['total'] = search_api_server_tasks_count($server);
+  }
+  $total = $context['results']['total'];
+
+  search_api_server_tasks_check($server);
+
+  $remaining = search_api_server_tasks_count($server);
+  $executed = max($total - $remaining, 0);
+
+  $args['@remaining'] = $remaining;
+  $context['message'] = format_plural($executed, 'Successfully executed @count task, @remaining remaining.', 'Successfully executed @count tasks, @remaining remaining.', $args);
+  $context['finished'] = $executed / $total;
+}
+
+/**
+ * Batch finish callback for pending server tasks.
+ */
+function search_api_execute_pending_tasks_finished($success, $results, $operations) {
+  if ($success) {
+    // Clear the previous warning.
+    drupal_get_messages('warning');
+
+    // Alert user to the number of tasks executed.
+    drupal_set_message(format_plural($results['total'], 'Successfully executed @count task.', 'Successfully executed @count tasks.'));
+  }
+}
+
+/**
+ * Return the number of pending tasks.
+ *
+ * @param SearchApiServer|null $server
+ *   (optional) The server for which tasks should be counted, or NULL to count
+ *   for all enabled servers.
+ *
+ * @return int
+ *   The number of pending tasks for the server, or in total.
+ */
+function search_api_server_tasks_count(SearchApiServer $server = NULL) {
+  $query = db_select('search_api_task', 't')
+    ->fields('t');
+
+  if ($server) {
+    $query->condition('server_id', $server->machine_name);
+  }
+  else {
+    $query->join('search_api_server', 's', 's.machine_name = t.server_id');
+    $query->condition('s.enabled', 1);
+  }
+
+  return $query->countQuery()->execute()->fetchField();
+}
+
+/**
+ * Access callback: Checks whether a user can execute pending tasks.
+ *
+ * @param SearchApiServer|null $server
+ *   (optional) The server for which tasks would be executed.
+ */
+function search_api_access_execute_tasks_batch(SearchApiServer $server = NULL) {
+  return user_access('administer search_api')
+      && search_api_server_tasks_count($server)
+      && (!$server || $server->enabled);
+}
+
 /**
  * Adds an entry into a server's list of pending tasks.
  *

+ 7 - 2
sites/all/modules/contrib/search/search_api/search_api.test

@@ -86,6 +86,7 @@ class SearchApiWebTest extends DrupalWebTestCase {
    * and then run tests on it.
    */
   public function testFramework() {
+    module_enable(array('search_api_test_2'));
     $this->drupalLogin($this->drupalCreateUser(array('administer search_api')));
     $this->insertItems();
     $this->createIndex();
@@ -730,13 +731,17 @@ class SearchApiWebTest extends DrupalWebTestCase {
    * deleteServer()) and that all associated tables and variables are removed.
    */
   protected function disableModules() {
+    module_disable(array('search_api_test_2'), FALSE);
+    $this->assertFalse(module_exists('search_api_test_2'), 'Second test module was successfully disabled.');
     module_disable(array('search_api_test'), FALSE);
-    $this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.');
+    $this->assertFalse(module_exists('search_api_test'), 'First test module was successfully disabled.');
     module_disable(array('search_api'), FALSE);
     $this->assertFalse(module_exists('search_api'), 'Search API module was successfully disabled.');
 
+    drupal_uninstall_modules(array('search_api_test_2'), FALSE);
+    $this->assertEqual(drupal_get_installed_schema_version('search_api_test_2', TRUE), SCHEMA_UNINSTALLED, 'Second test module was successfully uninstalled.');
     drupal_uninstall_modules(array('search_api_test'), FALSE);
-    $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'Test module was successfully uninstalled.');
+    $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'First test module was successfully uninstalled.');
     $this->assertFalse(db_table_exists('search_api_test'), 'Test module table was successfully removed.');
     drupal_uninstall_modules(array('search_api'), FALSE);
     $this->assertEqual(drupal_get_installed_schema_version('search_api', TRUE), SCHEMA_UNINSTALLED, 'Search API module was successfully uninstalled.');

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

@@ -1,18 +1,16 @@
-
-name = Search API test
+name = Search API Test
 description = "Some dummy implementations for testing the Search API."
 core = 7.x
 package = Search
 
-dependencies[] = search_api
+dependencies[] = search_api:search_api
 
 files[] = search_api_test.module
 
 hidden = TRUE
 
-; Information added by Drupal.org packaging script on 2017-02-23
-version = "7.x-1.21"
+; Information added by Drupal.org packaging script on 2019-03-11
+version = "7.x-1.26"
 core = "7.x"
 project = "search_api"
-datestamp = "1487844493"
-
+datestamp = "1552334832"

+ 16 - 0
sites/all/modules/contrib/search/search_api/tests/search_api_test_2.info

@@ -0,0 +1,16 @@
+name = Search API Test Service 2
+description = "A module providing a second test search service."
+core = 7.x
+package = Search
+
+dependencies[] = search_api:search_api
+
+files[] = search_api_test_service_2.module
+
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2019-03-11
+version = "7.x-1.26"
+core = "7.x"
+project = "search_api"
+datestamp = "1552334832"

+ 136 - 0
sites/all/modules/contrib/search/search_api/tests/search_api_test_2.module

@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * @file
+ * Provides a second test service and server for testing Search API.
+ */
+
+/**
+ * Implements hook_search_api_service_info().
+ */
+function search_api_test_2_search_api_service_info() {
+  $name = 'search_api_test_service_2';
+  $services[$name] = array(
+    'name' => $name,
+    'description' => 'search_api_test_service_2 description',
+    'class' => 'SearchApiDummyService',
+  );
+  return $services;
+}
+
+/**
+ * Implements hook_default_search_api_server().
+ */
+function search_api_test_2_default_search_api_server() {
+  $id = 'test_server_2';
+  $items[$id] = entity_create('search_api_server', array(
+    'name' => 'Search API test server 2',
+    'machine_name' => $id,
+    'enabled' => 1,
+    'description' => 'A server used for testing.',
+    'class' => 'search_api_test_service_2',
+  ));
+  return $items;
+}
+
+/**
+ * Dummy service for testing.
+ */
+class SearchApiDummyService implements SearchApiServiceInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(\SearchApiServer $server) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsFeature($feature) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewSettings() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postUpdate() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preDelete() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex(SearchApiIndex $index) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeIndex($index) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexItems(SearchApiIndex $index, array $items) {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query(SearchApiIndex $index, $options = array()) {
+    throw new SearchApiException("The dummy service doesn't support queries");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function search(SearchApiQueryInterface $query) {
+    return array();
+  }
+}

+ 39 - 1
sites/all/modules/contrib/search/search_api_solr_overrides/README.txt

@@ -1 +1,39 @@
-See major version branches.
+INTRODUCTION
+------------
+Allows you to override solr connection settings on an environment (site) basis,
+via your settings.php without editing servers managed in features.
+
+REQUIREMENTS
+------------
+* search_api_solr module
+
+CONFIGURATION
+-------------
+The module has no menu or modifiable settings. There is no configuration. When
+enabled, you can set your override values in your settings.php file.
+Search api will automatically pick up your values, but make sure to clear your
+cache first.
+
+EXAMPLE
+-------
+You can add following example to your settings.php file.
+
+$conf['search_api_solr_overrides'] = array(
+  'solr-server-id' => array(
+    'name' => 'Solr Server (Overridden)',
+    'options' => array(
+      'host' => '127.0.0.1',
+      'port' => 8983,
+      'path' => '/solr',
+    ),
+  ),
+);
+
+MAINTAINERS
+-----------
+Current maintainers:
+* nick_schuch - https://www.drupal.org/u/nick_schuch
+* cafuego - https://www.drupal.org/u/cafuego
+
+This project has been sponsored by:
+* PreviousNext - http://www.previousnext.com.au

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

@@ -3,9 +3,9 @@ description = Provides site specific overrides for search_api_solr configuration
 core = 7.x
 dependencies[] = search_api_solr
 
-; Information added by drupal.org packaging script on 2013-10-01
-version = "7.x-1.0-rc1+1-dev"
+; Information added by Drupal.org packaging script on 2017-06-13
+version = "7.x-1.0"
 core = "7.x"
 project = "search_api_solr_overrides"
-datestamp = "1380626863"
+datestamp = "1497319149"
 

+ 5 - 5
sites/all/modules/contrib/search/search_api_solr_overrides/search_api_solr_overrides.module

@@ -15,7 +15,7 @@
  * Example:
  * $conf['search_api_solr_overrides'] = array(
  *   'solr-server-id' => array(
- *     'name' => t('Solr Server (Overridden)'),
+ *     'name' => 'Solr Server (Overridden)',
  *       'options' => array(
  *         'host' => '127.0.0.1',
  *         'port' => 8983,
@@ -32,7 +32,7 @@ function search_api_solr_overrides_search_api_server_load($servers) {
   $overrides = variable_get('search_api_solr_overrides', FALSE);
 
   // Ensure the is information provided.
-  if (empty($overrides)) {
+  if (empty($overrides) || !is_array($overrides)) {
     return;
   }
 
@@ -41,12 +41,12 @@ function search_api_solr_overrides_search_api_server_load($servers) {
     // Check to see if the server config exists.
     if (!empty($servers[$id])) {
       foreach ($servers[$id] as $key => $field) {
-        // Ensure we need to override.
-        if (empty($override[$key])) {
+        // Ensure we need to override. User isset, so we can set FALSE values.
+        if (!isset($override[$key])) {
           continue;
         }
 
-        // Check for if the field is an array.
+        // Check if the field contains an array.
         if (is_array($field)) {
           $servers[$id]->$key = array_merge($servers[$id]->$key, $override[$key]);
         }