diff --git a/sites/all/modules/contrib/search/search_api/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api/CHANGELOG.txt
index 5e5ec734..0084fe0d 100644
--- a/sites/all/modules/contrib/search/search_api/CHANGELOG.txt
+++ b/sites/all/modules/contrib/search/search_api/CHANGELOG.txt
@@ -1,3 +1,41 @@
+Search API 1.21 (2017-02-23):
+-----------------------------
+- #2780341 by Berdir: Fixed passing of custom ranges to date facets.
+- #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation
+ type.
+- #2842856 by drunken monkey: Fixed language filters for "Multiple types"
+ indexes.
+- #2844990 by drunken monkey: Made the "Role filter" data alteration available
+ for multi-type indexes.
+- #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries.
+- #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets
+ module.
+- #2840261 by alan-ps: Fixed usage of outdated hash functions.
+- #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal
+ error in facet adapter's getSearchKeys() method.
+- #2838075 by dsnopek: Fixed possible race condition in
+ hook_system_info_alter().
+- #2836687 by sarthak drupal: Fixed one doc comment typo.
+- #2632880 by drunken monkey, donquixote: Added possibility to change indexed
+ bundles on disabled indexes.
+- #2828380 by jansete: Fixed taxonomy term access tag in Views filter.
+- #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views
+ when reverting an index.
+- #2822836 by prince_zyxware: Fixed some Drupal coding standards violations.
+- #2822145 by drunken monkey: Fixed problem with phrase search in Views
+ fulltext filter.
+- #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately"
+ functionality for unindexed items.
+- #2358065 by Jelle_S, graper, drunken monkey: Added the option for
+ highlighting of partial matches to the processor.
+- #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor.
+- #2649412 by relaxnow, GoZ: Added support for minimum granularity to date
+ facets.
+- #2769021 by Plazik, drunken monkey: Added the generated Search API query to
+ the Views preview.
+- #2769877 by mfernea: Fixed database exception when filtering for anonymous
+ user.
+
Search API 1.20 (2016-07-21):
-----------------------------
- #2731103 by drunken monkey: Fixed the default value for the taxonomy term
diff --git a/sites/all/modules/contrib/search/search_api/README.txt b/sites/all/modules/contrib/search/search_api/README.txt
index 2e2f581f..3943ceec 100644
--- a/sites/all/modules/contrib/search/search_api/README.txt
+++ b/sites/all/modules/contrib/search/search_api/README.txt
@@ -385,6 +385,10 @@ Included components
Enables the admin to specify a stopwords file, the words contained in which
will be filtered out of the text data indexed. This can be used to exclude
too common words from indexing, for servers not supporting this natively.
+ * Stem words
+ Uses the PorterStemmer method to reduce words to stems. A search for
+ "garden" will return results for "gardening" and "garden," as will a search
+ for "gardening."
- Additional modules
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
index 6251e32b..a5b5cdc0 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
@@ -192,6 +192,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
*/
public function getSearchKeys() {
$search = $this->getCurrentSearch();
+
+ // If the search is empty then there's no reason to continue.
+ if (!$search) {
+ return NULL;
+ }
+
$keys = $search[0]->getOriginalKeys();
if (is_array($keys)) {
// This will happen nearly never when displaying the search keys to the
@@ -281,10 +287,24 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// Date facets don't support the "OR" operator (for now).
$form['global']['operator']['#access'] = FALSE;
+
+ $default_value = FACETAPI_DATE_YEAR;
+ if (isset($options['date_granularity_min'])) {
+ $default_value = $options['date_granularity_min'];
+ }
+ $form['global']['date_granularity_min'] = array(
+ '#type' => 'select',
+ '#title' => t('Minimum granularity'),
+ '#description' => t('Determine the minimum drill-down level to start at'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#options' => $granularity_options,
+ '#default_value' => $default_value,
+ );
}
// Add an "Exclude" option for terms.
- if(!empty($facet['query types']) && in_array('term', $facet['query types'])) {
+ if (!empty($facet['query types']) && in_array('term', $facet['query types'])) {
$form['global']['operator']['#weight'] = -2;
unset($form['global']['operator']['#suffix']);
$form['global']['exclude'] = array(
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
index 6741cd81..6aeb8362 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
@@ -76,7 +76,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
*/
protected function createRangeFilter($value) {
// Ignore any filters passed directly from the server (range or missing).
- if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ [^ ]+[])]$/', $value))) {
+ if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ TO [^ ]+[\])]$/', $value))) {
return $value ? $value : NULL;
}
@@ -245,9 +245,19 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
}
}
- // Get the finest level of detail we're allowed to drill down to.
$settings = $facet->getSettings()->settings;
- $max_granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
+
+ // Get the finest level of detail we're allowed to drill down to.
+ $max_granularity = FACETAPI_DATE_MINUTE;
+ if (isset($settings['date_granularity'])) {
+ $max_granularity = $settings['date_granularity'];
+ }
+
+ // Get the coarsest level of detail we're allowed to start at.
+ $min_granularity = FACETAPI_DATE_YEAR;
+ if (isset($settings['date_granularity_min'])) {
+ $min_granularity = $settings['date_granularity_min'];
+ }
// Gets active facets, starts building hierarchy.
$parent = $granularity = NULL;
@@ -301,11 +311,14 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
FACETAPI_DATE_MINUTE => 2,
FACETAPI_DATE_SECOND => 1,
);
- // Gets gap numbers for both the gap and minimum gap, checks if the gap
- // is within the limit set by the $granularity parameter.
+ // Gets gap numbers for both the gap, minimum and maximum gap, checks if
+ // the gap is within the limit set by the $granularity parameters.
if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) {
$granularity = $max_granularity;
}
+ if ($gap_numbers[$granularity] > $gap_numbers[$min_granularity]) {
+ $granularity = $min_granularity;
+ }
}
else {
$granularity = $max_granularity;
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info
index 0c24621f..022eefc0 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info
+++ b/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 2016-07-21
-version = "7.x-1.20"
+; Information added by Drupal.org packaging script on 2017-02-23
+version = "7.x-1.21"
core = "7.x"
project = "search_api"
-datestamp = "1469117342"
+datestamp = "1487844493"
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install
index 5743e080..77c08026 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install
@@ -22,12 +22,14 @@ function search_api_facetapi_install() {
*/
function search_api_facetapi_uninstall() {
variable_del('search_api_facets_search_ids');
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR);
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH);
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY);
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR);
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE);
- variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND);
+ // We have to use the literal values here, as the Facet API module could have
+ // already been disabled at this point.
+ variable_del('date_format_search_api_facetapi_YEAR');
+ variable_del('date_format_search_api_facetapi_MONTH');
+ variable_del('date_format_search_api_facetapi_DAY');
+ variable_del('date_format_search_api_facetapi_HOUR');
+ variable_del('date_format_search_api_facetapi_MINUTE');
+ variable_del('date_format_search_api_facetapi_SECOND');
}
/**
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc
index e3e29191..00e80c29 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc
@@ -151,7 +151,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
}
- public function query(){
+ public function query() {
parent::query();
$facet_field = $this->get_option('facet_field');
@@ -291,7 +291,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
);
}
- public function execute(){
+ public function execute() {
$info['content'] = $this->render();
$info['content']['more'] = $this->render_more_link();
$info['subject'] = filter_xss_admin($this->view->get_title());
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
index de61c9bf..320adc36 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
@@ -119,7 +119,17 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
}
$words = preg_split('/\s+/', $input);
+ $quoted = FALSE;
foreach ($words as $i => $word) {
+ // Protect quoted strings.
+ if ($quoted && $word[strlen($word) - 1] === '"') {
+ $quoted = FALSE;
+ continue;
+ }
+ if ($quoted || $word[0] === '"') {
+ $quoted = TRUE;
+ continue;
+ }
if (drupal_strlen($word) < $this->options['min_length']) {
unset($words[$i]);
}
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
index b6db4f69..f3317cfa 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
@@ -93,7 +93,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte
$query->orderby('tv.name');
$query->orderby('td.weight');
$query->orderby('td.name');
- $query->addTag('term_access');
+ $query->addTag('taxonomy_term_access');
if ($vocabulary) {
$query->condition('tv.machine_name', $vocabulary->machine_name);
}
@@ -272,7 +272,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte
if (!empty($this->definition['vocabulary'])) {
$query->condition('tv.machine_name', $this->definition['vocabulary']);
}
- $query->addTag('term_access');
+ $query->addTag('taxonomy_term_access');
$result = $query->execute();
foreach ($result as $term) {
unset($missing[strtolower($term->name)]);
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc
index a2ef3ea2..6255274f 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc
@@ -29,8 +29,10 @@ class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity
protected function ids_to_strings(array $ids) {
$names = array();
$args[':uids'] = array_filter($ids);
- $result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args);
- $result = $result->fetchAllKeyed();
+ if ($args[':uids']) {
+ $result = db_query('SELECT uid, name FROM {users} u WHERE uid IN (:uids)', $args);
+ $result = $result->fetchAllKeyed();
+ }
foreach ($ids as $uid) {
if (!$uid) {
$names[] = variable_get('anonymous', t('Anonymous'));
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
index 5e0dff34..c6bd41d4 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
@@ -103,7 +103,7 @@ class SearchApiViewsCache extends views_plugin_cache_time {
$key_data['exposed_info'] = $_GET['exposed_info'];
}
}
- $key = md5(serialize($key_data));
+ $key = drupal_hash_base64(serialize($key_data));
return $key;
}
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
index bc9af4de..4394c5f3 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
@@ -310,6 +310,9 @@ class SearchApiViewsQuery extends views_plugin_query {
if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) {
$this->query->setOption('search_api_base_path', $this->view->override_path);
}
+
+ // Save query information for Views UI.
+ $view->build_info['query'] = (string) $this->query;
}
/**
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info
index f6f15245..2ba7ccaa 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info
+++ b/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 2016-07-21
-version = "7.x-1.20"
+; Information added by Drupal.org packaging script on 2017-02-23
+version = "7.x-1.21"
core = "7.x"
project = "search_api"
-datestamp = "1469117342"
+datestamp = "1487844493"
diff --git a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module
index 8a131c2a..62fcb944 100644
--- a/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module
+++ b/sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module
@@ -45,7 +45,10 @@ function search_api_views_search_api_index_update(SearchApiIndex $index) {
* Implements hook_search_api_index_delete().
*/
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
- _search_api_views_index_unavailable($index);
+ // Only do this if this is a "real" deletion, no revert.
+ if (!$index->hasStatus(ENTITY_IN_CODE)) {
+ _search_api_views_index_unavailable($index);
+ }
}
/**
diff --git a/sites/all/modules/contrib/search/search_api/includes/callback.inc b/sites/all/modules/contrib/search/search_api/includes/callback.inc
index ea161fbd..617aee37 100644
--- a/sites/all/modules/contrib/search/search_api/includes/callback.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/callback.inc
@@ -182,4 +182,19 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI
return array();
}
+ /**
+ * Determines whether the given index contains multiple types of entities.
+ *
+ * @param SearchApiIndex|null $index
+ * (optional) The index to examine. Defaults to the index set for this
+ * plugin.
+ *
+ * @return bool
+ * TRUE if the index is a multi-entity index, FALSE otherwise.
+ */
+ protected function isMultiEntityIndex(SearchApiIndex $index = NULL) {
+ $index = $index ? $index : $this->index;
+ return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController;
+ }
+
}
diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc b/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
index 9535be3c..069c4107 100644
--- a/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
@@ -209,6 +209,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return $a;
}
return drupal_substr($b, 0, 1);
+ case 'last':
+ return isset($b) ? $b : $a;
case 'list':
if (!isset($a)) {
$a = array();
@@ -288,6 +290,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'min' => t('Minimum'),
'first' => t('First'),
'first_char' => t('First letter'),
+ 'last' => t('Last'),
'list' => t('List'),
);
case 'type':
@@ -299,6 +302,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'min' => 'integer',
'first' => 'string',
'first_char' => 'string',
+ 'last' => 'string',
'list' => 'list',
);
case 'description':
@@ -310,6 +314,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'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.'),
'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'),
+ 'last' => t('The Last aggregation will simply keep the last encountered field value.'),
'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
);
}
diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc b/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc
index ab743d6c..cde8fe27 100644
--- a/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc
@@ -132,19 +132,4 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
}
- /**
- * Determines whether the given index contains multiple types of entities.
- *
- * @param SearchApiIndex|null $index
- * (optional) The index to examine. Defaults to the index set for this
- * plugin.
- *
- * @return bool
- * TRUE if the index is a multi-entity index, FALSE otherwise.
- */
- protected function isMultiEntityIndex(SearchApiIndex $index = NULL) {
- $index = $index ? $index : $this->index;
- return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController;
- }
-
}
diff --git a/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc b/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc
index ba126d1f..68b8d722 100644
--- a/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc
@@ -16,6 +16,9 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
* This plugin only supports indexes containing users.
*/
public function supportsIndex(SearchApiIndex $index) {
+ if ($this->isMultiEntityIndex($index)) {
+ return in_array('user', $index->options['datasource']['types']);
+ }
return $index->getEntityType() == 'user';
}
@@ -23,10 +26,20 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
* Implements SearchApiAlterCallbackInterface::alterItems().
*/
public function alterItems(array &$items) {
- $roles = $this->options['roles'];
+ $selected_roles = $this->options['roles'];
$default = (bool) $this->options['default'];
- foreach ($items as $id => $account) {
- $role_match = (count(array_diff_key($account->roles, $roles)) !== count($account->roles));
+ $multi_types = $this->isMultiEntityIndex($this->index);
+ foreach ($items as $id => $item) {
+ if ($multi_types) {
+ if ($item->item_type !== 'user') {
+ continue;
+ }
+ $item_roles = $item->user->roles;
+ }
+ else {
+ $item_roles = $item->roles;
+ }
+ $role_match = (count(array_diff_key($item_roles, $selected_roles)) !== count($item_roles));
if ($role_match === $default) {
unset($items[$id]);
}
diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource.inc b/sites/all/modules/contrib/search/search_api/includes/datasource.inc
index eec9a79c..9661092c 100644
--- a/sites/all/modules/contrib/search/search_api/includes/datasource.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/datasource.inc
@@ -626,8 +626,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
return NULL;
}
- $ret = array();
-
+ $indexes_by_id = array();
foreach ($indexes as $index) {
$this->checkIndex($index);
$update = db_update($this->table)
@@ -639,12 +638,26 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
if ($item_ids !== FALSE) {
$update->condition($this->itemIdColumn, $item_ids, 'IN');
}
- if ($update->execute()) {
- $ret[] = $index;
- }
+ $update->execute();
+ $indexes_by_id[$index->id] = $index;
}
- return $ret;
+ // Determine and return the indexes with any changed items. If $item_ids is
+ // FALSE, all items are marked as changed and, thus, all indexes will be
+ // affected (unless they don't have any items, but no real point in treating
+ // that special case).
+ if ($item_ids !== FALSE) {
+ $indexes_with_items = db_select($this->table, 't')
+ ->fields('t', array($this->indexIdColumn))
+ ->distinct()
+ ->condition($this->indexIdColumn, array_keys($indexes_by_id), 'IN')
+ ->condition($this->itemIdColumn, $item_ids, 'IN')
+ ->execute()
+ ->fetchCol();
+ return array_intersect_key($indexes_by_id, array_flip($indexes_with_items));
+ }
+
+ return NULL;
}
/**
@@ -715,7 +728,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
$this->checkIndex($index);
$select = db_select($this->table, 't');
- $select->addField('t', 'item_id');
+ $select->addField('t', $this->itemIdColumn);
$select->condition($this->indexIdColumn, $index->id);
$select->condition($this->changedColumn, 0, '>');
$select->orderBy($this->changedColumn, 'ASC');
diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc
index 3ebbab24..bbca0aa4 100644
--- a/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc
@@ -278,10 +278,10 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
$form['bundles'] = array(
'#type' => 'checkboxes',
'#title' => t('Bundles'),
- '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'),
+ '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for enabled indexes.'),
'#options' => array_map('check_plain', $options),
'#attributes' => array('class' => array('search-api-checkboxes-list')),
- '#disabled' => !empty($form_state['index']),
+ '#disabled' => !empty($form_state['index']) && $form_state['index']->enabled,
);
if (!empty($form_state['index']->options['datasource'])) {
$form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']);
diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc
index 1128f173..c3a9f1fc 100644
--- a/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/datasource_external.inc
@@ -49,7 +49,7 @@ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceC
* loadable, specify a function here.
*
* @param array $ids
- * The IDs of the items to laod.
+ * The IDs of the items to load.
*
* @return array
* The loaded items, keyed by ID.
diff --git a/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc b/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc
index 7e5d4de2..ea164963 100644
--- a/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/datasource_multiple.inc
@@ -44,6 +44,9 @@ class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataS
$item->item_type = $type;
$item->item_entity_id = $entity_id;
$item->item_bundle = NULL;
+ // Add the item language so the "search_api_language" field will work
+ // correctly.
+ $item->language = isset($entity->language) ? $entity->language : NULL;
try {
list(, , $bundle) = entity_extract_ids($type, $entity);
$item->item_bundle = $bundle ? "$type:$bundle" : NULL;
diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc b/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
index 8accf4de..5b419491 100644
--- a/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
@@ -51,6 +51,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'excerpt' => TRUE,
'excerpt_length' => 256,
'highlight' => 'always',
+ 'highlight_partial' => FALSE,
'exclude_fields' => array(),
);
@@ -114,6 +115,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'#default_value' => $this->options['highlight'],
);
+ $form['highlight_partial'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Highlight partial matches'),
+ '#description' => t('When enabled, matches in parts of words will be highlighted as well.'),
+ '#default_value' => $this->options['highlight_partial'],
+ );
+
return $form;
}
@@ -322,9 +330,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$ranges = array();
$included = array();
$length = 0;
- $workkeys = $keys;
- while ($length < $this->options['excerpt_length'] && count($workkeys)) {
- foreach ($workkeys as $k => $key) {
+ $work_keys = $keys;
+ while ($length < $this->options['excerpt_length'] && $work_keys) {
+ foreach ($work_keys as $k => $key) {
if ($length >= $this->options['excerpt_length']) {
break;
}
@@ -336,8 +344,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
// Locate a keyword (position $p, always >0 because $text starts with a
// space).
$p = 0;
- if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
- $p = $match[0][1];
+ 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];
+ }
+ }
+ else {
+ $p = stripos($text, $key, $included[$key]);
}
// Now locate a space in front (position $q) and behind it (position $s),
// leaving about 60 characters extra before and after for context.
@@ -352,18 +366,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$ranges[$q] = $p + $s;
$length += $p + $s - $q;
$included[$key] = $p + 1;
- }
- else {
- unset($workkeys[$k]);
+ continue;
}
}
- else {
- unset($workkeys[$k]);
- }
- }
- else {
- unset($workkeys[$k]);
}
+ // Unless we got a match above, we don't need to look for this key any
+ // more.
+ unset($work_keys[$k]);
}
}
@@ -437,10 +446,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
}
return implode('', $texts);
}
- $replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
$keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/')));
- $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
+ // If "Highlight partial matches" is disabled, we only want to highlight
+ // matches that are complete words. Otherwise, we want all of them.
+ $boundary = empty($this->options['highlight_partial']) ? self::$boundary : '';
+ $regex = '/' . $boundary . '(?:' . $keys . ')' . $boundary . '/iu';
+ $replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
+ $text = preg_replace($regex, $replace, ' ' . $text . ' ');
return substr($text, 1, -1);
}
diff --git a/sites/all/modules/contrib/search/search_api/includes/processor_stemmer.inc b/sites/all/modules/contrib/search/search_api/includes/processor_stemmer.inc
new file mode 100644
index 00000000..a6c05489
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/includes/processor_stemmer.inc
@@ -0,0 +1,732 @@
+ url('https://github.com/markfullmer/porter2'),
+ '!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'),
+ );
+ $form += array(
+ 'help' => array(
+ '#markup' => '' . t('Optionally, provide an exclusion list to override the stemmer algorithm. Read about the algorithm and exclusions.', $args) . '
',
+ ),
+ 'exceptions' => array(
+ '#type' => 'textarea',
+ '#title' => t('Exceptions'),
+ '#description' => t('Enter exceptions in the form of WORD=STEM, where "WORD" is the term entered and "STEM" is the resulting stem. List each exception on a separate line.'),
+ '#default_value' => "texan=texa",
+ ),
+ );
+
+ if (!empty($this->options['exceptions'])) {
+ $form['exceptions']['#default_value'] = $this->options['exceptions'];
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ // Load custom exceptions.
+ $exceptions = $this->getExceptions();
+
+ $words = preg_split('/[^\p{L}\p{N}]+/u', $value, -1 , PREG_SPLIT_DELIM_CAPTURE);
+ $stemmed = array();
+ foreach ($words as $i => $word) {
+ if ($i % 2 == 0 && strlen($word)) {
+ if (!isset($this->stems[$word])) {
+ $stem = new SearchApiPorter2($word, $exceptions);
+ $this->stems[$word] = $stem->stem();
+ }
+ $stemmed[] = $this->stems[$word];
+ }
+ else {
+ $stemmed[] = $word;
+ }
+ }
+ $value = implode('', $stemmed);
+ }
+
+ /**
+ * Retrieves the processor's configured exceptions.
+ *
+ * @return string[]
+ * An associative array of exceptions, with words as keys and stems as their
+ * replacements.
+ */
+ protected function getExceptions() {
+ if (!empty($this->options['exceptions'])) {
+ $exceptions = parse_ini_string($this->options['exceptions'], TRUE);
+ return is_array($exceptions) ? $exceptions : array();
+ }
+ return array();
+ }
+
+}
+
+/**
+ * Implements the Porter2 stemming algorithm.
+ *
+ * @see https://github.com/markfullmer/porter2
+ */
+class SearchApiPorter2 {
+
+ /**
+ * The word being stemmed.
+ *
+ * @var string
+ */
+ protected $word;
+
+ /**
+ * The R1 of the word.
+ *
+ * @var int
+ *
+ * @see http://snowball.tartarus.org/texts/r1r2.html.
+ */
+ protected $r1;
+
+ /**
+ * The R2 of the word.
+ *
+ * @var int
+ *
+ * @see http://snowball.tartarus.org/texts/r1r2.html.
+ */
+ protected $r2;
+
+ /**
+ * List of exceptions to be used.
+ *
+ * @var string[]
+ */
+ protected $exceptions = array();
+
+ /**
+ * Constructs a SearchApiPorter2 object.
+ *
+ * @param string $word
+ * The word to stem.
+ * @param string[] $custom_exceptions
+ * (optional) A custom list of exceptions.
+ */
+ public function __construct($word, $custom_exceptions = array()) {
+ $this->word = $word;
+ $this->exceptions = $custom_exceptions + array(
+ 'skis' => 'ski',
+ 'skies' => 'sky',
+ 'dying' => 'die',
+ 'lying' => 'lie',
+ 'tying' => 'tie',
+ 'idly' => 'idl',
+ 'gently' => 'gentl',
+ 'ugly' => 'ugli',
+ 'early' => 'earli',
+ 'only' => 'onli',
+ 'singly' => 'singl',
+ 'sky' => 'sky',
+ 'news' => 'news',
+ 'howe' => 'howe',
+ 'atlas' => 'atlas',
+ 'cosmos' => 'cosmos',
+ 'bias' => 'bias',
+ 'andes' => 'andes',
+ );
+
+ // Set initial y, or y after a vowel, to Y.
+ $inc = 0;
+ while ($inc <= $this->length()) {
+ if (substr($this->word, $inc, 1) === 'y' && ($inc == 0 || $this->isVowel($inc - 1))) {
+ $this->word = substr_replace($this->word, 'Y', $inc, 1);
+
+ }
+ $inc++;
+ }
+ // Establish the regions R1 and R2. See function R().
+ $this->r1 = $this->R(1);
+ $this->r2 = $this->R(2);
+ }
+
+ /**
+ * Computes the stem of the word.
+ *
+ * @return string
+ * The word's stem.
+ */
+ public function stem() {
+ // Ignore exceptions & words that are two letters or less.
+ if ($this->exceptions() || $this->length() <= 2) {
+ return strtolower($this->word);
+ }
+ else {
+ $this->step0();
+ $this->step1a();
+ $this->step1b();
+ $this->step1c();
+ $this->step2();
+ $this->step3();
+ $this->step4();
+ $this->step5();
+ }
+ return strtolower($this->word);
+ }
+
+ /**
+ * Determines whether the word is contained in our list of exceptions.
+ *
+ * If so, the $word property is changed to the stem listed in the exceptions.
+ *
+ * @return bool
+ * TRUE if the word was an exception, FALSE otherwise.
+ */
+ protected function exceptions() {
+ if (isset($this->exceptions[$this->word])) {
+ $this->word = $this->exceptions[$this->word];
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Searches for the longest among the "s" suffixes and removes it.
+ *
+ * Implements step 0 of the Porter2 algorithm.
+ */
+ protected function step0() {
+ $found = FALSE;
+ $checks = array("'s'", "'s", "'");
+ foreach ($checks as $check) {
+ if (!$found && $this->hasEnding($check)) {
+ $this->removeEnding($check);
+ $found = TRUE;
+ }
+ }
+ }
+
+ /**
+ * Handles various suffixes, of which the longest is replaced.
+ *
+ * Implements step 1a of the Porter2 algorithm.
+ */
+ protected function step1a() {
+ $found = FALSE;
+ if ($this->hasEnding('sses')) {
+ $this->removeEnding('sses');
+ $this->addEnding('ss');
+ $found = TRUE;
+ }
+ $checks = array('ied', 'ies');
+ foreach ($checks as $check) {
+ if (!$found && $this->hasEnding($check)) {
+ $length = $this->length();
+ $this->removeEnding($check);
+ if ($length > 4) {
+ $this->addEnding('i');
+ }
+ else {
+ $this->addEnding('ie');
+ }
+ $found = TRUE;
+ }
+ }
+ if ($this->hasEnding('us') || $this->hasEnding('ss')) {
+ $found = TRUE;
+ }
+ // Delete if preceding word part has a vowel not immediately before the s.
+ if (!$found && $this->hasEnding('s') && $this->containsVowel(substr($this->word, 0, -2))) {
+ $this->removeEnding('s');
+ }
+ }
+
+ /**
+ * Handles various suffixes, of which the longest is replaced.
+ *
+ * Implements step 1b of the Porter2 algorithm.
+ */
+ protected function step1b() {
+ $exceptions = array(
+ 'inning',
+ 'outing',
+ 'canning',
+ 'herring',
+ 'earring',
+ 'proceed',
+ 'exceed',
+ 'succeed',
+ );
+ if (in_array($this->word, $exceptions)) {
+ return;
+ }
+ $checks = array('eedly', 'eed');
+ foreach ($checks as $check) {
+ if ($this->hasEnding($check)) {
+ if ($this->r1 !== $this->length()) {
+ $this->removeEnding($check);
+ $this->addEnding('ee');
+ }
+ return;
+ }
+ }
+ $checks = array('ingly', 'edly', 'ing', 'ed');
+ $second_endings = array('at', 'bl', 'iz');
+ foreach ($checks as $check) {
+ // If the ending is present and the previous part contains a vowel.
+ if ($this->hasEnding($check) && $this->containsVowel(substr($this->word, 0, -strlen($check)))) {
+ $this->removeEnding($check);
+ foreach ($second_endings as $ending) {
+ if ($this->hasEnding($ending)) {
+ $this->addEnding('e');
+ return;
+ }
+ }
+ // If the word ends with a double, remove the last letter.
+ $found = $this->removeDoubles();
+ // If the word is short, add e (so hop -> hope).
+ if (!$found && ($this->isShort())) {
+ $this->addEnding('e');
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Replaces suffix y or Y with i if after non-vowel not @ word begin.
+ *
+ * Implements step 1c of the Porter2 algorithm.
+ */
+ protected function step1c() {
+ if (($this->hasEnding('y') || $this->hasEnding('Y')) && $this->length() > 2 && !($this->isVowel($this->length() - 2))) {
+ $this->removeEnding('y');
+ $this->addEnding('i');
+ }
+ }
+
+ /**
+ * Implements step 2 of the Porter2 algorithm.
+ */
+ protected function step2() {
+ $checks = array(
+ "ization" => "ize",
+ "iveness" => "ive",
+ "fulness" => "ful",
+ "ational" => "ate",
+ "ousness" => "ous",
+ "biliti" => "ble",
+ "tional" => "tion",
+ "lessli" => "less",
+ "fulli" => "ful",
+ "entli" => "ent",
+ "ation" => "ate",
+ "aliti" => "al",
+ "iviti" => "ive",
+ "ousli" => "ous",
+ "alism" => "al",
+ "abli" => "able",
+ "anci" => "ance",
+ "alli" => "al",
+ "izer" => "ize",
+ "enci" => "ence",
+ "ator" => "ate",
+ "bli" => "ble",
+ "ogi" => "og",
+ );
+ foreach ($checks as $find => $replace) {
+ if ($this->hasEnding($find)) {
+ if ($this->inR1($find)) {
+ $this->removeEnding($find);
+ $this->addEnding($replace);
+ }
+ return;
+ }
+ }
+ if ($this->hasEnding('li')) {
+ if ($this->length() > 4 && $this->validLi($this->charAt(-3))) {
+ $this->removeEnding('li');
+ }
+ }
+ }
+
+ /**
+ * Implements step 3 of the Porter2 algorithm.
+ */
+ protected function step3() {
+ $checks = array(
+ 'ational' => 'ate',
+ 'tional' => 'tion',
+ 'alize' => 'al',
+ 'icate' => 'ic',
+ 'iciti' => 'ic',
+ 'ical' => 'ic',
+ 'ness' => '',
+ 'ful' => '',
+ );
+ foreach ($checks as $find => $replace) {
+ if ($this->hasEnding($find)) {
+ if ($this->inR1($find)) {
+ $this->removeEnding($find);
+ $this->addEnding($replace);
+ }
+ return;
+ }
+ }
+ if ($this->hasEnding('ative')) {
+ if ($this->inR2('ative')) {
+ $this->removeEnding('ative');
+ }
+ }
+ }
+
+ /**
+ * Implements step 4 of the Porter2 algorithm.
+ */
+ protected function step4() {
+ $checks = array(
+ 'ement',
+ 'ment',
+ 'ance',
+ 'ence',
+ 'able',
+ 'ible',
+ 'ant',
+ 'ent',
+ 'ion',
+ 'ism',
+ 'ate',
+ 'iti',
+ 'ous',
+ 'ive',
+ 'ize',
+ 'al',
+ 'er',
+ 'ic',
+ );
+ foreach ($checks as $check) {
+ // Among the suffixes, if found and in R2, delete.
+ if ($this->hasEnding($check)) {
+ if ($this->inR2($check)) {
+ if ($check !== 'ion' || in_array($this->charAt(-4), array('s', 't'))) {
+ $this->removeEnding($check);
+ }
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Implements step 5 of the Porter2 algorithm.
+ */
+ protected function step5() {
+ if ($this->hasEnding('e')) {
+ // Delete if in R2, or in R1 and not preceded by a short syllable.
+ if ($this->inR2('e') || ($this->inR1('e') && !$this->isShortSyllable($this->length() - 3))) {
+ $this->removeEnding('e');
+ }
+ return;
+ }
+ if ($this->hasEnding('l')) {
+ // Delete if in R2 and preceded by l.
+ if ($this->inR2('l') && $this->charAt(-2) == 'l') {
+ $this->removeEnding('l');
+ }
+ }
+ }
+
+ /**
+ * Removes certain double consonants from the word's end.
+ *
+ * @return bool
+ * TRUE if a match was found and removed, FALSE otherwise.
+ */
+ protected function removeDoubles() {
+ $found = FALSE;
+ $doubles = array('bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt');
+ foreach ($doubles as $double) {
+ if (substr($this->word, -2) == $double) {
+ $this->word = substr($this->word, 0, -1);
+ $found = TRUE;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Checks whether a character is a vowel.
+ *
+ * @param int $position
+ * The character's position.
+ * @param string|null $word
+ * (optional) The word in which to check. Defaults to $this->word.
+ * @param string[] $additional
+ * (optional) Additional characters that should count as vowels.
+ *
+ * @return bool
+ * TRUE if the character is a vowel, FALSE otherwise.
+ */
+ protected function isVowel($position, $word = NULL, $additional = array()) {
+ if ($word === NULL) {
+ $word = $this->word;
+ }
+ $vowels = array_merge(array('a', 'e', 'i', 'o', 'u', 'y'), $additional);
+ return in_array($this->charAt($position, $word), $vowels);
+ }
+
+ /**
+ * Retrieves the character at the given position.
+ *
+ * @param int $position
+ * The 0-based index of the character. If a negative number is given, the
+ * position is counted from the end of the string.
+ * @param string|null $word
+ * (optional) The word from which to retrieve the character. Defaults to
+ * $this->word.
+ *
+ * @return string
+ * The character at the given position, or an empty string if the given
+ * position was illegal.
+ */
+ protected function charAt($position, $word = NULL) {
+ if ($word === NULL) {
+ $word = $this->word;
+ }
+ $length = strlen($word);
+ if (abs($position) >= $length) {
+ return '';
+ }
+ if ($position < 0) {
+ $position += $length;
+ }
+ return $word[$position];
+ }
+
+ /**
+ * Determines whether the word ends in a "vowel-consonant" suffix.
+ *
+ * Unless the word is only two characters long, it also checks that the
+ * third-last character is neither "w", "x" nor "Y".
+ *
+ * @param int|null $position
+ * (optional) If given, do not check the end of the word, but the character
+ * at the given position, and the next one.
+ *
+ * @return bool
+ * TRUE if the word has the described suffix, FALSE otherwise.
+ */
+ protected function isShortSyllable($position = NULL) {
+ if ($position === NULL) {
+ $position = $this->length() - 2;
+ }
+ // A vowel at the beginning of the word followed by a non-vowel.
+ if ($position === 0) {
+ return $this->isVowel(0) && !$this->isVowel(1);
+ }
+ // Vowel followed by non-vowel other than w, x, Y and preceded by
+ // non-vowel.
+ $additional = array('w', 'x', 'Y');
+ return !$this->isVowel($position - 1) && $this->isVowel($position) && !$this->isVowel($position + 1, NULL, $additional);
+ }
+
+ /**
+ * Determines whether the word is short.
+ *
+ * A word is called short if it ends in a short syllable and if R1 is null.
+ *
+ * @return bool
+ * TRUE if the word is short, FALSE otherwise.
+ */
+ protected function isShort() {
+ return $this->isShortSyllable() && $this->r1 == $this->length();
+ }
+
+ /**
+ * Determines the start of a certain "R" region.
+ *
+ * R is a region after the first non-vowel following a vowel, or end of word.
+ *
+ * @param int $type
+ * (optional) 1 or 2. If 2, then calculate the R after the R1.
+ *
+ * @return int
+ * The R position.
+ */
+ protected function R($type = 1) {
+ $inc = 1;
+ if ($type === 2) {
+ $inc = $this->r1;
+ }
+ elseif ($this->length() > 5) {
+ $prefix_5 = substr($this->word, 0, 5);
+ if ($prefix_5 === 'gener' || $prefix_5 === 'arsen') {
+ return 5;
+ }
+ if ($this->length() > 6 && substr($this->word, 0, 6) === 'commun') {
+ return 6;
+ }
+ }
+
+ while ($inc <= $this->length()) {
+ if (!$this->isVowel($inc) && $this->isVowel($inc - 1)) {
+ $position = $inc;
+ break;
+ }
+ $inc++;
+ }
+ if (!isset($position)) {
+ $position = $this->length();
+ }
+ else {
+ // We add one, as this is the position AFTER the first non-vowel.
+ $position++;
+ }
+ return $position;
+ }
+
+ /**
+ * Checks whether the given string is contained in R1.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the string is in R1, FALSE otherwise.
+ */
+ protected function inR1($string) {
+ $r1 = substr($this->word, $this->r1);
+ return strpos($r1, $string) !== FALSE;
+ }
+
+ /**
+ * Checks whether the given string is contained in R2.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the string is in R2, FALSE otherwise.
+ */
+ protected function inR2($string) {
+ $r2 = substr($this->word, $this->r2);
+ return strpos($r2, $string) !== FALSE;
+ }
+
+ /**
+ * Determines the string length of the current word.
+ *
+ * @return int
+ * The string length of the current word.
+ */
+ protected function length() {
+ return strlen($this->word);
+ }
+
+ /**
+ * Checks whether the word ends with the given string.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the word ends with the given string, FALSE otherwise.
+ */
+ protected function hasEnding($string) {
+ $length = strlen($string);
+ if ($length > $this->length()) {
+ return FALSE;
+ }
+ return (substr_compare($this->word, $string, -1 * $length, $length) === 0);
+ }
+
+ /**
+ * Appends a given string to the current word.
+ *
+ * @param string $string
+ * The ending to append.
+ */
+ protected function addEnding($string) {
+ $this->word = $this->word . $string;
+ }
+
+ /**
+ * Removes a given string from the end of the current word.
+ *
+ * Does not check whether the ending is actually there.
+ *
+ * @param string $string
+ * The ending to remove.
+ */
+ protected function removeEnding($string) {
+ $this->word = substr($this->word, 0, -strlen($string));
+ }
+
+ /**
+ * Checks whether the given string contains a vowel.
+ *
+ * @param string $string
+ * The string to check.
+ *
+ * @return bool
+ * TRUE if the string contains a vowel, FALSE otherwise.
+ */
+ protected function containsVowel($string) {
+ $inc = 0;
+ $return = FALSE;
+ while ($inc < strlen($string)) {
+ if ($this->isVowel($inc, $string)) {
+ $return = TRUE;
+ break;
+ }
+ $inc++;
+ }
+ return $return;
+ }
+
+ /**
+ * Checks whether the given string is a valid -li prefix.
+ *
+ * @param string $string
+ * The string to check.
+ *
+ * @return bool
+ * TRUE if the given string is a valid -li prefix, FALSE otherwise.
+ */
+ protected function validLi($string) {
+ return in_array($string, array(
+ 'c',
+ 'd',
+ 'e',
+ 'g',
+ 'h',
+ 'k',
+ 'm',
+ 'n',
+ 'r',
+ 't',
+ ));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/includes/query.inc b/sites/all/modules/contrib/search/search_api/includes/query.inc
index debed66a..640dcd20 100644
--- a/sites/all/modules/contrib/search/search_api/includes/query.inc
+++ b/sites/all/modules/contrib/search/search_api/includes/query.inc
@@ -856,10 +856,33 @@ class SearchApiQuery implements SearchApiQueryInterface {
}
$ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
}
- $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n";
+ $options = $this->sanitizeOptions($this->options);
+ $options = str_replace("\n", "\n ", var_export($options, TRUE));
+ $ret .= 'Options: ' . $options . "\n";
return $ret;
}
+ /**
+ * Sanitizes an array of options in a way that plays nice with var_export().
+ *
+ * @param array $options
+ * An array of options.
+ *
+ * @return array
+ * The sanitized options.
+ */
+ protected function sanitizeOptions(array $options) {
+ foreach ($options as $key => $value) {
+ if (is_object($value)) {
+ $options[$key] = 'object (' . get_class($value) . ')';
+ }
+ elseif (is_array($value)) {
+ $options[$key] = $this->sanitizeOptions($value);
+ }
+ }
+ return $options;
+ }
+
}
/**
@@ -1048,6 +1071,10 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
* {@inheritdoc}
*/
public function &getTags() {
+ // Tags can sometimes be NULL for old serialized query filter objects.
+ if (!isset($this->tags)) {
+ $this->tags = array();
+ }
return $this->tags;
}
diff --git a/sites/all/modules/contrib/search/search_api/search_api.info b/sites/all/modules/contrib/search/search_api/search_api.info
index dd9a210b..e7317531 100644
--- a/sites/all/modules/contrib/search/search_api/search_api.info
+++ b/sites/all/modules/contrib/search/search_api/search_api.info
@@ -27,6 +27,7 @@ files[] = includes/processor.inc
files[] = includes/processor_highlight.inc
files[] = includes/processor_html_filter.inc
files[] = includes/processor_ignore_case.inc
+files[] = includes/processor_stemmer.inc
files[] = includes/processor_stopwords.inc
files[] = includes/processor_tokenizer.inc
files[] = includes/processor_transliteration.inc
@@ -36,9 +37,9 @@ files[] = includes/service.inc
configure = admin/config/search/search_api
-; Information added by Drupal.org packaging script on 2016-07-21
-version = "7.x-1.20"
+; Information added by Drupal.org packaging script on 2017-02-23
+version = "7.x-1.21"
core = "7.x"
project = "search_api"
-datestamp = "1469117342"
+datestamp = "1487844493"
diff --git a/sites/all/modules/contrib/search/search_api/search_api.install b/sites/all/modules/contrib/search/search_api/search_api.install
index 34682d9c..5dc26895 100644
--- a/sites/all/modules/contrib/search/search_api/search_api.install
+++ b/sites/all/modules/contrib/search/search_api/search_api.install
@@ -362,7 +362,7 @@ function search_api_install() {
),
);
search_api_index_insert($values);
- drupal_set_message('The Search API module was installed. A new default node index was created.');
+ drupal_set_message(t('The Search API module was installed. A new default node index was created.'));
}
/**
diff --git a/sites/all/modules/contrib/search/search_api/search_api.module b/sites/all/modules/contrib/search/search_api/search_api.module
index 0890fca4..61385c5d 100644
--- a/sites/all/modules/contrib/search/search_api/search_api.module
+++ b/sites/all/modules/contrib/search/search_api/search_api.module
@@ -784,7 +784,7 @@ function search_api_features_export_alter(&$export) {
* @see hook_search_api_item_type_info()
*/
function search_api_system_info_alter(&$info, $file, $type) {
- if ($type != 'module' || $file->name == 'search_api') {
+ if ($type != 'module' || $file->name == 'search_api' || !module_exists($file->name)) {
return;
}
// Check for defined item types.
@@ -1152,11 +1152,17 @@ function search_api_search_api_processor_info() {
'class' => 'SearchApiStopWords',
'weight' => 30,
);
+ $processors['search_api_porter_stemmer'] = array(
+ 'name' => t('Stem words'),
+ 'description' => t('This processor reduces words to a stem (e.g., "talking" to "talk"). For best results, it should only be executed after tokenizing.'),
+ 'class' => 'SearchApiPorterStemmer',
+ 'weight' => 35,
+ );
$processors['search_api_highlighting'] = array(
'name' => t('Highlighting'),
'description' => t('Adds highlighting for search results.'),
'class' => 'SearchApiHighlight',
- 'weight' => 35,
+ 'weight' => 40,
);
return $processors;
diff --git a/sites/all/modules/contrib/search/search_api/search_api.rules.inc b/sites/all/modules/contrib/search/search_api/search_api.rules.inc
index 37143f87..0725f4ce 100644
--- a/sites/all/modules/contrib/search/search_api/search_api.rules.inc
+++ b/sites/all/modules/contrib/search/search_api/search_api.rules.inc
@@ -10,7 +10,7 @@
* Implements hook_rules_action_info().
*/
function search_api_rules_action_info() {
- $items['search_api_index'] = array (
+ $items['search_api_index'] = array(
'parameter' => array(
'entity' => array(
'type' => 'entity',
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test.info b/sites/all/modules/contrib/search/search_api/tests/search_api_test.info
index d974c14d..95c999ab 100644
--- a/sites/all/modules/contrib/search/search_api/tests/search_api_test.info
+++ b/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 2016-07-21
-version = "7.x-1.20"
+; Information added by Drupal.org packaging script on 2017-02-23
+version = "7.x-1.21"
core = "7.x"
project = "search_api"
-datestamp = "1469117342"
+datestamp = "1487844493"
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api_saved_searches/CHANGELOG.txt
index 4f92b7c6..d23224fe 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/CHANGELOG.txt
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/CHANGELOG.txt
@@ -1,11 +1,29 @@
-Search API Saved Searches 1.3 (05/24/2014):
+Search API Saved Searches 1.5 (2015-06-08):
+-------------------------------------------
+- #2346679 by drunken monkey, dcmul: Added display of saved search
+ enabled/disabled status to UI.
+- #2346677 by drunken monkey, dcmul: Fixed firing of saved searches for
+ disabled users.
+- #2387155 by Dimiona, drunken monkey: Fixed default value for custom
+ notification interval.
+- #2359003 by drunken monkey: Fixed removal of cron queue items of deleted
+ searches.
+- #2347243 by queenvictoria, drunken monkey: Added tokens from the first search
+ to the mail texts.
+- #1829678 by aschmoe: Fixed render order of "Save search" block in panels.
+- #2354863 by drunken monkey: Fixed alert intervals not being respected.
+- #2300175 by drunken monkey: Added database indexes to improve performance.
+- #2221683 by drunken monkey | rjacobs: Fixed duplicate mails due to overlaps
+ conditions in cron queue.
+
+Search API Saved Searches 1.3 (2014-05-24):
-------------------------------------------
- #2082325 by balajidharma, drunken monkey: Added classes to edit and delete
links.
- #2088045 by leeomara, drunken monkey: Added hook to override generated names.
- #2042299 by drunken monkey: Added access callbacks for both entity types.
-Search API Saved Searches 1.2 (07/21/2013):
+Search API Saved Searches 1.2 (2013-07-21):
-------------------------------------------
- #2040469 by drunken monkey: Fixed Views display of saved search results.
- #2027441 by drunken monkey: Added option to filter by date instead of IDs.
@@ -14,7 +32,7 @@ Search API Saved Searches 1.2 (07/21/2013):
- #2018983 by drunken monkey: Fixed error after deleting last saved search.
- #2012714 by drunken monkey: Fixed the $reset parameter for load functions.
-Search API Saved Searches 1.1 (03/27/2013):
+Search API Saved Searches 1.1 (2013-03-27):
-------------------------------------------
- #1371344 by leeomara, drunken monkey: Added a README.txt file.
- #1888140 by drunken monkey: Added extended Views integration.
@@ -36,6 +54,6 @@ Search API Saved Searches 1.1 (03/27/2013):
- #1398310 by drunken monkey: Fixed visibility of "custom time" field.
- #1857822 by sepgil: Added quantity tokens.
-Search API Saved Searches 1.0 (10/04/2012):
+Search API Saved Searches 1.0 (2012-10-04):
-------------------------------------------
First stable release of the Search API Saved Searches project.
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.admin.inc b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.admin.inc
index 57837c14..5f18bea9 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.admin.inc
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.admin.inc
@@ -280,7 +280,7 @@ You can configure your saved searches at the following address:
$form['options']['interval']['set_interval_custom'] = array(
'#type' => 'textfield',
'#description' => t('Enter the custom notification interval, in seconds. Use a negative value to disable notifications.'),
- '#default_value' => isset($interval_options[$options['set_interval']]) ? $options['set_interval'] : 0,
+ '#default_value' => $options['set_interval'],
'#parents' => array('options', 'set_interval_custom'),
'#states' => array(
'visible' => array(
@@ -343,7 +343,8 @@ You can configure your saved searches at the following address:
'#type' => 'textfield',
'#title' => t('Subject'),
'#description' => t("Enter the mail's subject.") . ' ' . t('Available variables are: @vars.',
- array('@vars' => '[site:name], [user:name], [user:mail]')),
+ array('@vars' => '[site:name], [user:name], [user:mail], [search-api-saved-search:name], [search-api-saved-search:created]')) . '
' .
+ t('The search-specific variables are taken from the first saved search of the mail. (Alerts from multiple searches are sent at the same time.)'),
'#default_value' => $options['mail']['notify']['title'],
'#required' => TRUE,
);
@@ -352,8 +353,12 @@ You can configure your saved searches at the following address:
'#title' => t('Body'),
'#description' => t("Enter the mail's body.") . ' ' . t('Available variables are: @vars.',
array('@vars' => '[site:name], [site:url], [user:name], [user:mail], [site:url-brief], ' .
- '[search-api-saved-searches:results], [user:search-api-saved-searches-url]')) . ' ' .
+ '[search-api-saved-searches:results], [user:search-api-saved-searches-url], ' .
+ '[search-api-saved-search:name], [search-api-saved-search:created], ' .
+ '[search-api-saved-search:results-capped], [search-api-saved-search:view-url], ' .
+ '[search-api-saved-search:edit-url], [search-api-saved-search:delete-url]')) . ' ' .
t('The replacement for @var can be configured below.', array('@var' => '[search-api-saved-searches:results]')) . '
' .
+ t('The search-specific variables are taken from the first saved search of the mail. (Alerts from multiple searches are sent at the same time.)') . '
' .
t('Please note: For anonymous users, the [user:*] variables will be empty.'),
'#default_value' => $options['mail']['notify']['body'],
'#rows' => 12,
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.info b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.info
index a40cc055..0ae321d1 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.info
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.info
@@ -10,9 +10,9 @@ files[] = views/handler_field_saved_search_interval.inc
files[] = views/handler_field_saved_search_link.inc
files[] = views/handler_field_saved_search_name.inc
-; Information added by Drupal.org packaging script on 2014-05-24
-version = "7.x-1.3"
+; Information added by Drupal.org packaging script on 2015-06-08
+version = "7.x-1.5"
core = "7.x"
project = "search_api_saved_searches"
-datestamp = "1400922530"
+datestamp = "1433745784"
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.install b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.install
index 056527de..dd4109f9 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.install
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.install
@@ -112,6 +112,11 @@ function search_api_saved_searches_schema() {
'type' => 'int',
'not null' => TRUE,
),
+ 'last_queued' => array(
+ 'description' => 'The Unix timestamp when the saved search was last queued.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ),
'last_execute' => array(
'description' => 'The Unix timestamp when the saved search was last executed.',
'type' => 'int',
@@ -141,8 +146,8 @@ function search_api_saved_searches_schema() {
),
),
'indexes' => array(
- 'uid' => array('uid'),
- 'mail' => array('mail'),
+ 'user' => array('mail', 'uid'),
+ 'notify' => array('enabled', 'notify_interval', 'last_queued', 'last_execute'),
),
'primary key' => array('id'),
);
@@ -172,3 +177,68 @@ function search_api_saved_searches_update_7101() {
->execute();
}
}
+
+/**
+ * Add {search_api_saved_search}.last_queued for more accurate queue tracking.
+ */
+function search_api_saved_searches_update_7102() {
+ $spec = array(
+ 'description' => 'The Unix timestamp when the saved search was last queued.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ );
+ db_change_field('search_api_saved_search', 'last_execute', 'last_queued', $spec);
+ $spec['description'] = 'The Unix timestamp when the saved search was last executed.';
+ $spec['not null'] = FALSE;
+ db_add_field('search_api_saved_search', 'last_execute', $spec);
+ db_update('search_api_saved_search')
+ ->expression('last_execute', 'last_queued')
+ ->execute();
+ $spec['not null'] = TRUE;
+ db_change_field('search_api_saved_search', 'last_execute', 'last_execute', $spec);
+}
+
+/**
+ * Optimize indexes on {search_api_saved_search} to improve performance.
+ */
+function search_api_saved_searches_update_7103() {
+ db_drop_index('search_api_saved_search', 'uid');
+ db_drop_index('search_api_saved_search', 'mail');
+ db_add_index('search_api_saved_search', 'user', array('mail', 'uid'));
+ db_add_index('search_api_saved_search', 'notify', array('enabled', 'notify_interval', 'last_queued', 'last_execute'));
+}
+
+/**
+ * Fix incorrect {search_api_saved_search}.last_queued values.
+ *
+ * This might lead to duplicate mails for saved searches in some rare cases, but
+ * is necessary to prevent saved searches from never being executed at all
+ * anymore in others.
+ *
+ * @see https://www.drupal.org/node/2354863
+ */
+function search_api_saved_searches_update_7104() {
+ db_update('search_api_saved_search')
+ ->expression('last_queued', 'last_execute')
+ ->where('last_queued > last_execute')
+ ->execute();
+}
+
+/**
+ * Disable saved searches whose users were disabled.
+ */
+function search_api_saved_searches_update_7105() {
+ // Get the UIDs of all inactive users.
+ $query = db_select('users', 'u');
+ $query->fields('u', array('uid'))
+ ->condition('u.status', 0)
+ ->condition('u.uid', 0, '<>');
+
+ // Then, disable all their searches.
+ db_update('search_api_saved_search')
+ ->condition('uid', $query, 'IN')
+ ->fields(array(
+ 'enabled' => 0,
+ ))
+ ->execute();
+}
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.module b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.module
index 2f1b8710..da6e531d 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.module
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.module
@@ -66,6 +66,24 @@ function search_api_saved_searches_menu() {
'access arguments' => array(NULL, 2, 4),
'file' => 'search_api_saved_searches.pages.inc',
);
+ $items['search-api/saved-search/%search_api_saved_search/enable'] = array(
+ 'title' => 'Enable/Disable saved search',
+ 'description' => 'Enable or disable a saved search.',
+ 'page callback' => 'search_api_saved_searches_search_enable',
+ 'page arguments' => array(2),
+ 'access callback' => 'search_api_saved_search_edit_access',
+ 'access arguments' => array(NULL, 2, 4),
+ 'file' => 'search_api_saved_searches.pages.inc',
+ );
+ $items['search-api/saved-search/%search_api_saved_search/disable'] = array(
+ 'title' => 'Enable/Disable saved search',
+ 'description' => 'Enable or disable a saved search.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('search_api_saved_searches_search_disable_form', 2),
+ 'access callback' => 'search_api_saved_search_edit_access',
+ 'access arguments' => array(NULL, 2, 4),
+ 'file' => 'search_api_saved_searches.pages.inc',
+ );
$items['search-api/saved-search/%search_api_saved_search/edit'] = array(
'title' => 'Edit saved search',
'description' => 'Edit a saved search.',
@@ -160,6 +178,7 @@ function search_api_saved_searches_entity_property_info_alter(array &$info) {
$searches['settings_id']['type'] = 'token';
$searches['enabled']['type'] = 'boolean';
$searches['created']['type'] = 'date';
+ $searches['last_queued']['type'] = 'date';
$searches['last_execute']['type'] = 'date';
// We can't assign "duration" until Entity API Views integration supports
// this.
@@ -290,11 +309,14 @@ function search_api_saved_searches_user_insert(&$edit, $account, $category) {
* If a user gets activated, associate saved searches with the same mail address
* with them.
*
+ * If a user gets deactivated, disable all related saved searches.
+ *
* Also, change mail address of saved searches when the user mail address
* changes.
*/
function search_api_saved_searches_user_update(&$edit, $account, $category) {
- // Changes of mail and status.
+ // For newly activated users, transfer all saved searches with their mail
+ // address to them.
if (!empty($account->status) && empty($account->original->status)) {
foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
$search->uid = $account->uid;
@@ -304,6 +326,15 @@ function search_api_saved_searches_user_update(&$edit, $account, $category) {
$search->save();
}
}
+ // If an account gets deactivated/banned, disable all associated searches.
+ if (empty($account->status) && !empty($account->original->status)) {
+ foreach (search_api_saved_search_load_multiple(FALSE, array('uid' => $account->uid)) as $search) {
+ $search->enabled = FALSE;
+ $search->save();
+ }
+ }
+ // If the user's mail address changed, also change the mail address of the
+ // user's saved searches.
if ($account->mail != $account->original->mail) {
foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => $account->uid)) as $search) {
$search->mail = $account->mail;
@@ -450,9 +481,8 @@ function search_api_saved_searches_settings_load($id, $reset = FALSE) {
* @param bool $reset
* If TRUE, will reset the internal entity cache.
*
- * @return array
- * An array of SearchApiSavedSearchesSettings objects matching the conditions,
- * keyed by delta.
+ * @return SearchApiSavedSearchesSettings[]
+ * All saved search settings matching the conditions, keyed by delta.
*/
function search_api_saved_searches_settings_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
$settings = entity_load('search_api_saved_searches_settings', $ids, $conditions, $reset);
@@ -478,16 +508,16 @@ function search_api_saved_search_load($id, $reset = FALSE) {
/**
* Loads multiple saved search objects.
*
- * @param $ids
+ * @param int[]|false $ids
* The saved search's IDs; or FALSE to load all saved searches.
* @param array $conditions
* Associative array of field => value conditions that returned objects must
* satisfy.
- * @param $reset
+ * @param bool $reset
* If TRUE, will reset the internal entity cache.
*
- * @return array
- * An array of SearchApiSavedSearch objects matching the conditions.
+ * @return SearchApiSavedSearch[]
+ * All saved searches matching the conditions, keyed by their IDs.
*/
function search_api_saved_search_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
return entity_load('search_api_saved_search', $ids, $conditions, $reset);
@@ -624,6 +654,15 @@ function search_api_saved_searches_block_info() {
return $blocks;
}
+/**
+ * Implements hook_ctools_block_info().
+ */
+function search_api_saved_searches_ctools_block_info($module, $delta, &$info) {
+ $info['category'] = t('Search API Saved Searches');
+ // Allow blocks to be used before the search results in Panels.
+ $info['render last'] = TRUE;
+}
+
/**
* Implements hook_block_configure().
*/
@@ -970,6 +1009,7 @@ function search_api_saved_searches_save_form_submit(array $form, array &$form_st
'name' => $values['name'],
'mail' => $values['mail'],
'created' => REQUEST_TIME,
+ 'last_queued' => REQUEST_TIME,
'last_execute' => REQUEST_TIME,
'notify_interval' => $values['notify_interval'],
'query' => $query,
@@ -1057,9 +1097,14 @@ function search_api_saved_searches_mail($key, array &$message, array $params) {
case 'notify':
$settings = $params['settings'];
+ $search = $params['searches'][0]['search'];
$data = array(
'user' => $params['user'],
'search_api_saved_searches' => $params['searches'],
+ 'search_api_saved_search_info' => array(
+ 'search' => $search,
+ 'results' => array(),
+ ),
);
$title = $settings->getTranslatedOption('mail.notify.title', $language->language);
$message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
@@ -1081,7 +1126,9 @@ function search_api_saved_searches_cron() {
$ids = db_select('search_api_saved_search', 's')
->fields('s', array('id'))
->condition('enabled', 1)
- ->where('notify_interval >= 0 AND last_execute + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
+ ->condition('notify_interval', 0, '>=')
+ ->where('last_execute >= last_queued')
+ ->where('last_queued + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
->execute()
->fetchCol();
if (!$ids) {
@@ -1093,21 +1140,18 @@ function search_api_saved_searches_cron() {
// Group the search according to mail and settings. Grouping by mail prevents
// a user from getting several mails at once, for different searches. Grouping
// by settings is necessary since the mails can differ between settings.
+ $user_searches = array();
foreach ($searches as $search) {
- $user_searches[$search->mail . ' ' . $search->settings_id][] = $search;
+ $user_searches[$search->mail . ' ' . $search->settings_id][] = $search->id;
+ // Set the last execution timestamp now, so the interval doesn't move and we
+ // don't get problems if the next cron run occurs before the queue is
+ // completely executed.
+ $search->last_queued = REQUEST_TIME;
+ $search->save();
}
foreach ($user_searches as $searches) {
$queue->createItem($searches);
}
- // Set the last execution timestamp now, so the interval doesn't move and we
- // don't get problems if the next cron run occurrs before the queue is
- // completely executed.
- db_update('search_api_saved_search')
- ->fields(array(
- 'last_execute' => REQUEST_TIME,
- ))
- ->condition('id', $ids, 'IN')
- ->execute();
}
/**
@@ -1127,17 +1171,44 @@ function search_api_saved_searches_cron_queue_info() {
/**
* Checks for new results for saved searches, and sends a mail if necessary.
*
- * @param array $searches
- * An array of SearchApiSavedSearch objects to check for new results. All of
- * these should have the same mail address and base settings.
+ * Used as a worker callback for the homonymous cron queue.
+ *
+ * @param int[] $search_ids
+ * The IDs of the saved searches to check for new results. All of these should
+ * have the same mail address and base settings.
+ *
+ * @throws SearchApiException
+ * If an error occurred in one of the searches.
+ *
+ * @see search_api_saved_searches_cron_queue_info()
*/
-function search_api_saved_searches_check_updates(array $searches) {
- try {
- $search = reset($searches);
- $settings = $search->settings();
- $index = $settings->index();
- $mail_params = array();
+function search_api_saved_searches_check_updates(array $search_ids) {
+ if (!$search_ids) {
+ return;
+ }
+ // Since in earlier versions this function got the loaded searches passed
+ // directly instead of just IDs, and there might still be some such items in
+ // the queue when updating to the new style, we have to stay
+ // backwards-compatible here. So, when an array of loaded searches is passed,
+ // we first replace them with their IDs and only then load them again.
+ if (!is_scalar(reset($search_ids))) {
+ /** @var SearchApiSavedSearch[] $searches */
+ $searches = $search_ids;
+ $search_ids = array();
foreach ($searches as $search) {
+ $search_ids[] = $search->id;
+ }
+ }
+ $searches = search_api_saved_search_load_multiple($search_ids, array('enabled' => 1));
+ if (!$searches) {
+ return;
+ }
+ $search = $searches[key($searches)];
+ $settings = $search->settings();
+ $index = $settings->index();
+ $mail_params = array();
+ foreach ($searches as $search) {
+ try {
// Make sure we run the query as the user who owns the saved search.
// Otherwise node access will not work properly.
$search->query['options']['search_api_access_account'] = $search->uid;
@@ -1150,6 +1221,7 @@ function search_api_saved_searches_check_updates(array $searches) {
}
$response = $query->execute();
if (!empty($response['results'])) {
+ $old = array();
$new = $results = drupal_map_assoc(array_keys($response['results']));
if (empty($settings->options['date_field'])) {
// ID-based method: Compare these results to the old ones.
@@ -1189,23 +1261,28 @@ function search_api_saved_searches_check_updates(array $searches) {
if (empty($settings->options['date_field']) && ($new || array_diff($old, $results))) {
// The results changed in some way: store the latest version.
$search->results = implode(',', $results);
- $search->save();
}
}
+ // Use time() instead of REQUEST_TIME to minimize the potential of sending
+ // duplicate results due to longer-running cron queue workers.
+ $search->last_execute = time();
+ $search->save();
}
- // If we set any searches in the mail parameters, send the mail.
- if ($mail_params) {
- $mail_params['user'] = user_load($search->uid);
- $mail_params['settings'] = $settings;
- $message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
- user_preferred_language($mail_params['user']), $mail_params);
- if ($message['result']) {
- watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
- array('@mail' => $search->mail), WATCHDOG_INFO);
- }
+ catch (SearchApiException $e) {
+ $args = _drupal_decode_exception($e);
+ $args['@id'] = $search->id;
+ throw new SearchApiException(t('%type while trying to check for new results on saved search @id: !message in %function (line %line of %file).', $args));
}
}
- catch (SearchApiException $e) {
- watchdog('search_api_saved_searches', $e->getMessage(), NULL, WATCHDOG_ERROR);
+ // If we set any searches in the mail parameters, send the mail.
+ if ($mail_params) {
+ $mail_params['user'] = user_load($search->uid);
+ $mail_params['settings'] = $settings;
+ $message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
+ user_preferred_language($mail_params['user']), $mail_params);
+ if ($message['result']) {
+ watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
+ array('@mail' => $search->mail), WATCHDOG_INFO);
+ }
}
}
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.pages.inc b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.pages.inc
index 81b94d40..eb2c84f5 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.pages.inc
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.pages.inc
@@ -45,16 +45,30 @@ function search_api_saved_searches_user_listing($account) {
$interval = format_interval($search->notify_interval, 1);
}
+ $enable_options['attributes']['class'][] = 'saved-search-enable';
$edit_options['attributes']['class'][] = 'saved-search-edit';
$delete_options['attributes']['class'][] = 'saved-search-delete';
+ if ($search->enabled) {
+ $enable_action = '/disable';
+ $enable_label = t('disable');
+ }
+ else {
+ $enable_action = '/enable';
+ $enable_label = t('enable');
+ $enable_options['query']['token'] = drupal_get_token($search->id);
+ }
+
$path = $base_path . $search->id;
+ $enable_link = l($enable_label, $path . $enable_action, $enable_options);
+ $edit_link = l(t('edit'), $path . '/edit', $edit_options);
+ $delete_link = l(t('delete'), $path . '/delete', $delete_options);
$rows[] = array(
$name,
$created,
$last_execute,
$interval,
- l(t('edit'), $path . '/edit', $edit_options) . ' | ' . l(t('delete'), $path . '/delete', $delete_options),
+ $enable_link . ' | ' . $edit_link . ' | ' . $delete_link,
);
}
@@ -170,6 +184,12 @@ function search_api_saved_searches_search_edit_form(array $form, array &$form_st
$form_state['destination'] = array($search->options['page']['path'], $search->options['page']);
}
+ $form['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enabled'),
+ '#description' => t('Disable to stop receiving notifications from this saved search.'),
+ '#default_value' => $search->enabled,
+ );
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
@@ -213,6 +233,7 @@ function search_api_saved_searches_search_edit_form_submit(array $form, array &$
$values = $form_state['values'];
$search = $form_state['search'];
$search->name = $values['name'];
+ $search->enabled = $values['enabled'];
if (isset($values['notify_interval'])) {
$search->notify_interval = $values['notify_interval'];
}
@@ -227,6 +248,56 @@ function search_api_saved_searches_search_edit_form_submit(array $form, array &$
}
}
+/**
+ * Page callback: Enables a saved search.
+ *
+ * @param SearchApiSavedSearch $search
+ * The search to enable.
+ */
+function search_api_saved_searches_search_enable(SearchApiSavedSearch $search = NULL) {
+ if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $search->id)) {
+ return MENU_ACCESS_DENIED;
+ }
+
+ $search->enabled = TRUE;
+ if ($search->save()) {
+ drupal_set_message(t('The saved search was successfully enabled.'));
+ }
+ else {
+ drupal_set_message(t('The saved search could not be enabled.'), 'error');
+ }
+
+ drupal_goto(_search_api_saved_searches_admin_redirect_url($search));
+ return NULL;
+}
+
+/**
+ * Page callback: Constructs a form for disabling a saved search.
+ *
+ * @param SearchApiSavedSearch $search
+ * The search to disable.
+ *
+ * @see search_api_saved_searches_search_disable_form_submit()
+ *
+ * @ingroup forms
+ */
+function search_api_saved_searches_search_disable_form(array $form, array &$form_state, SearchApiSavedSearch $search) {
+ $form_state['search'] = $search;
+ $url = _search_api_saved_searches_admin_redirect_url($search);
+ return confirm_form($form, t('Do you really want to disable this saved search?'), $url, t('You will not receive any updates for this saved search until it is re-enabled.'));
+}
+
+/**
+ * Form submission handler for search_api_saved_searches_search_disable_form().
+ */
+function search_api_saved_searches_search_disable_form_submit(array $form, array &$form_state) {
+ $search = $form_state['search'];
+ $search->enabled = FALSE;
+ $search->save();
+ drupal_set_message(t('The saved search was successfully disabled.'));
+ $form_state['redirect'] = _search_api_saved_searches_admin_redirect_url($search);
+}
+
/**
* Form builder for confirming the deletion of a saved search.
*
@@ -238,15 +309,7 @@ function search_api_saved_searches_search_edit_form_submit(array $form, array &$
*/
function search_api_saved_searches_search_delete_form(array $form, array &$form_state, SearchApiSavedSearch $search) {
$form_state['search'] = $search;
- if ($search->uid && search_api_saved_search_edit_access(user_load($search->uid))) {
- $url = 'user/' . $search->uid . '/saved-searches';
- }
- elseif (!empty($search->options['page'])) {
- $url = $search->options['page'];
- }
- else {
- $url = '';
- }
+ $url = _search_api_saved_searches_admin_redirect_url($search);
return confirm_form($form, t('Do you really want to delete this saved search?'), $url);
}
@@ -258,15 +321,29 @@ function search_api_saved_searches_search_delete_form(array $form, array &$form_
function search_api_saved_searches_search_delete_form_submit(array $form, array &$form_state) {
$search = $form_state['search'];
$search->delete();
+ drupal_set_message(t('The saved search was successfully deleted.'));
+ $form_state['redirect'] = _search_api_saved_searches_admin_redirect_url($search);
+}
+
+/**
+ * Returns the correct redirect URL after changing a saved search.
+ *
+ * This will be the user's "Saved searches" overview tab, if it is accessible;
+ * otherwise, if the search has a page associated with it, that page; and if
+ * none of the two are the case, the front page.
+ *
+ * @param SearchApiSavedSearch $search
+ * The saved search that was edited, deleted or otherwise changed.
+ *
+ * @return string
+ * The URL to redirect to.
+ */
+function _search_api_saved_searches_admin_redirect_url(SearchApiSavedSearch $search) {
if ($search->uid && search_api_saved_search_edit_access(user_load($search->uid))) {
- $url = 'user/' . $search->uid . '/saved-searches';
+ return 'user/' . $search->uid . '/saved-searches';
}
elseif (!empty($search->options['page'])) {
- $url = $search->options['page'];
+ return $search->options['page'];
}
- else {
- $url = '';
- }
- drupal_set_message(t('The saved search was successfully deleted.'));
- $form_state['redirect'] = $url;
+ return '';
}
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.search_entity.inc b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.search_entity.inc
index 8431686a..3bac4853 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.search_entity.inc
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.search_entity.inc
@@ -68,6 +68,11 @@ class SearchApiSavedSearch extends Entity {
*/
public $created;
+ /**
+ * @var integer
+ */
+ public $last_queued;
+
/**
* @var integer
*/
@@ -145,7 +150,7 @@ class SearchApiSavedSearch extends Entity {
}
/**
- * @return SearchApiIndex
+ * @return SearchApiSavedSearchesSettings
* The settings this saved search uses.
*
* @throws SearchApiException
diff --git a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches_i18n/search_api_saved_searches_i18n.info b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches_i18n/search_api_saved_searches_i18n.info
index d6f54848..595c94ac 100644
--- a/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches_i18n/search_api_saved_searches_i18n.info
+++ b/sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches_i18n/search_api_saved_searches_i18n.info
@@ -8,9 +8,9 @@ package = Multilingual - Internationalization
files[] = search_api_saved_searches_i18n.controller.inc
files[] = search_api_saved_searches_i18n.string_wrapper.inc
-; Information added by Drupal.org packaging script on 2014-05-24
-version = "7.x-1.3"
+; Information added by Drupal.org packaging script on 2015-06-08
+version = "7.x-1.5"
core = "7.x"
project = "search_api_saved_searches"
-datestamp = "1400922530"
+datestamp = "1433745784"
diff --git a/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt
index 268a91b0..68fe9882 100644
--- a/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt
+++ b/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt
@@ -1,5 +1,29 @@
-Search API Solr Search 1.x, dev (xxxx-xx-xx):
----------------------------------------------
+Search API Solr Search 1.12 (2017-02-23):
+-----------------------------------------
+- #2612770 by Temoor, drunken monkey: Fixed conjunction in complex filter
+ conditions.
+- #2711685 by berliner: Added support for Solr 6.
+- #2842661 by gabrielu, drunken monkey: Fixed duplicate code line in
+ clearCache().
+- #2834159 by zniki.ru: Fixed left-over explicit
+ SearchApiSolrConnection::escapeFieldName() call.
+- #2677912 by jts86, mian3010: Added option to disable committing for Solr.
+- #2828473 by detroz: Fixed incorrect variable initialization.
+- #2826565 by Sardis, drunken monkey: Fixed multi-valued field highlighting.
+- #2807327 by drunken monkey: Fixed configuration form descriptions.
+- #2772199 by drunken monkey: Added a warning to the description for the
+ highlighting server option.
+- #2733625 by joelstein, drunken monkey: Fixed notice in
+ Connection::getFields().
+
+Search API Solr Search 1.11 (2016-07-06):
+-----------------------------------------
+- #2710397 by drunken monkey: Fixed escaping of highlighting tags.
+- #2636016 by drunken monkey: Fixed location search distance facets.
+
+Search API Solr Search 1.10 (2016-03-14):
+-----------------------------------------
+- #2604322 by lex0r, drunken monkey: Added the option to log all Solr queries.
- #2645366 by tedfordgif, drunken monkey: Fixed browser incorrectly filling the
HTTP Auth form fields.
- #2611716 by aditya_anurag, drunken monkey: Improved the method documentation
diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/service.inc b/sites/all/modules/contrib/search/search_api_solr/includes/service.inc
index 1991f047..e80b35fa 100644
--- a/sites/all/modules/contrib/search/search_api_solr/includes/service.inc
+++ b/sites/all/modules/contrib/search/search_api_solr/includes/service.inc
@@ -80,6 +80,9 @@ class SearchApiSolrService extends SearchApiAbstractService {
'retrieve_data' => FALSE,
'highlight_data' => FALSE,
'skip_schema_check' => FALSE,
+ 'log_query' => FALSE,
+ 'log_response' => FALSE,
+ 'commits_disabled' => FALSE,
'solr_version' => '',
'http_method' => 'AUTO',
// Default to TRUE for new servers, but to FALSE for existing ones.
@@ -161,7 +164,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
$form['path'] = array(
'#type' => 'textfield',
'#title' => t('Solr path'),
- '#description' => t('The path that identifies the Solr instance to use on the server. (For Solr 4.x servers, this should include the name of the core to use.)'),
+ '#description' => t('The path that identifies the Solr instance to use on the server. (For Solr versions 4 and above, this should include the name of the core to use.)'),
'#default_value' => $options['path'],
);
@@ -211,19 +214,24 @@ class SearchApiSolrService extends SearchApiAbstractService {
$form['advanced']['highlight_data'] = array(
'#type' => 'checkbox',
'#title' => t('Highlight retrieved data'),
- '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields.'),
+ '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields. Note: Do not use the "Highlighting" processor for the index together with this option – use one or the other.'),
'#default_value' => $options['highlight_data'],
);
+ // Highlighting retrieved data only makes sense when we retrieve data.
+ // (Actually, internally it doesn't really matter. However, from a user's
+ // perspective, having to check both probably makes sense.)
+ $form['advanced']['highlight_data']['#states']['invisible']
+ [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE;
$form['advanced']['skip_schema_check'] = array(
'#type' => 'checkbox',
'#title' => t('Skip schema verification'),
- '#description' => t('Skip the automatic check for schema-compatibillity. Use this override if you are seeing an error-message about an incompatible schema.xml configuration file, and you are sure the configuration is compatible.'),
+ '#description' => t('Skip the automatic check for schema-compatibility. Use this override if you are seeing an error-message about an incompatible schema.xml configuration file, and you are sure the configuration is compatible.'),
'#default_value' => $options['skip_schema_check'],
);
$form['advanced']['solr_version'] = array(
'#type' => 'select',
'#title' => t('Solr version override'),
- '#description' => t('Specify the Solr version manually in case it cannot be retrived automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec"'),
+ '#description' => t('Specify the Solr version manually in case it cannot be retrieved automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec".'),
'#options' => array(
'' => t('Determine automatically'),
'3' => '3.x',
@@ -232,12 +240,6 @@ class SearchApiSolrService extends SearchApiAbstractService {
),
'#default_value' => $options['solr_version'],
);
- // Highlighting retrieved data only makes sense when we retrieve data.
- // (Actually, internally it doesn't really matter. However, from a user's
- // perspective, having to check both probably makes sense.)
- $form['advanced']['highlight_data']['#states']['invisible']
- [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE;
-
$form['advanced']['http_method'] = array(
'#type' => 'select',
'#title' => t('HTTP method'),
@@ -249,6 +251,24 @@ class SearchApiSolrService extends SearchApiAbstractService {
'GET' => 'GET',
),
);
+ $form['advanced']['log_query'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Log search requests'),
+ '#description' => t('Log all outgoing Solr search requests.'),
+ '#default_value' => $options['log_query'],
+ );
+ $form['advanced']['log_response'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Log search results'),
+ '#description' => t('Log all search result responses received from Solr. NOTE: This may slow down your site since all response data (including possible retrieved data) will be saved in the Drupal log.'),
+ '#default_value' => $options['log_response'],
+ );
+ $form['advanced']['commits_disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disable explicit committing'),
+ '#description' => t('Do not send any commit commands to the Solr server.'),
+ '#default_value' => $options['commits_disabled'],
+ );
if (module_exists('search_api_autocomplete')) {
$form['advanced']['autocomplete'] = array(
@@ -573,7 +593,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
// Now add all fields contained in the item, with dynamic fields. Also,
// gather the contents of all text fields to also add them to "content".
- $text_content = '';
+ $text_content = array();
foreach ($item as $key => $field) {
// If the field is not known for the index, something weird has
// happened. We refuse to index the items and hope that the others are
@@ -936,7 +956,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
unset($radius);
$field = $fields[$spatial['field']];
- $escaped_field = SearchApiSolrConnection::escapeFieldName($field);
+ $escaped_field = call_user_func(array($this->connection_class, 'escapeFieldName'), $field);
// If proper bbox coordinates were given use them to filter.
if (isset($spatial['bbox'])) {
@@ -970,7 +990,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
// If the fq consists only of a filter on this field, replace it with
// a range.
$preg_field = preg_quote($escaped_field, '/');
- if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $value, $m)) {
+ if (preg_match('/^(?:\{!tag[^}]+\})?' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $value, $m)) {
unset($fq[$key]);
if ($m[1] && is_numeric($m[1])) {
$min_radius = isset($min_radius) ? max($min_radius, $m[1]) : $m[1];
@@ -1354,6 +1374,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
foreach ($response->highlighting->$id->content as $snippet) {
$snippet = strip_tags($snippet);
$snippet = preg_replace('/^.*>|<.*$/', '', $snippet);
+ $snippet = check_plain($snippet);
$snippet = $this->formatHighlighting($snippet);
// The created fragments sometimes have leading or trailing punctuation.
// We remove that here for all common cases, but take care not to remove
@@ -1367,9 +1388,27 @@ class SearchApiSolrService extends SearchApiAbstractService {
$prefix_length = strlen($prefix);
foreach ($field_mapping as $search_api_property => $solr_property) {
if (substr($solr_property, 0, $prefix_length) == $prefix && !empty($response->highlighting->$id->$solr_property)) {
- // Contrary to above, we here want to preserve HTML, so we just
- // replace the [HIGHLIGHT] tags with the appropriate format.
- $fields[$search_api_property] = $this->formatHighlighting($response->highlighting->$id->$solr_property);
+ $value = $response->highlighting->$id->$solr_property;
+ $value = $this->sanitizeHighlightValue($value, $search_api_property);
+ // Remove highlight prefixes and suffixes so we could compare values
+ // in order to replace the correspond items.
+ $orig_value = preg_replace('#\[(/?)HIGHLIGHT\]#', '', $value);
+ $field_values = $this->sanitizeHighlightValue($fields[$search_api_property]);
+ foreach ($field_values as $delta => $field_value) {
+ foreach ($orig_value as $num => $item) {
+ if ($item === $field_value) {
+ $field_values[$delta] = $this->formatHighlighting($value[$num]);
+ $change = TRUE;
+ continue 2;
+ }
+ }
+ }
+ if (!empty($change)) {
+ $fields[$search_api_property] = array(
+ '#value' => $field_values,
+ '#sanitize_callback' => FALSE,
+ );
+ }
}
}
}
@@ -1378,12 +1417,33 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
- * Changes highlighting tags from our custom, HTML-safe ones to HTML.
+ * Sanitizes a highlighted field value.
*
- * @param string|array $snippet
- * The snippet(s) to format.
+ * @param string|array $value
+ * Either a highlighted field value, or an array of such values.
+ * @param string|null $field_id
+ * (optional) The ID of the field for which this sanitizing occurs, if any.
*
* @return string|array
+ * The sanitized input.
+ */
+ protected function sanitizeHighlightValue($value, $field_id = NULL) {
+ if (is_array($value)) {
+ foreach ($value as $i => $nested_value) {
+ $value[$i] = $this->sanitizeHighlightValue($nested_value, $field_id);
+ }
+ return $value;
+ }
+ return check_plain(strip_tags($value));
+ }
+
+ /**
+ * Changes highlighting tags from our custom, HTML-safe ones to HTML.
+ *
+ * @param string|string[] $snippet
+ * The snippet(s) to format.
+ *
+ * @return string|string[]
* The snippet(s), properly formatted as HTML.
*/
protected function formatHighlighting($snippet) {
@@ -1587,9 +1647,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
elseif ($f instanceof SearchApiQueryFilterInterface) {
$q = $this->createFilterQueries($f, $solr_fields, $fields);
if ($filter->getConjunction() != $f->getConjunction() && count($q) > 1) {
- // $or == TRUE means the nested filter has conjunction AND, and vice versa
- $sep = $or ? ' ' : ' OR ';
- $fq[] = '((' . implode(')' . $sep . '(', $q) . '))';
+ $fq[] = '((' . implode(') ' . $f->getConjunction() . ' (', $q) . '))';
}
else {
$fq = array_merge($fq, $q);
@@ -2315,6 +2373,10 @@ class SearchApiSolrService extends SearchApiAbstractService {
* Sends a commit command to the Solr server.
*/
public function commit() {
+ // If committing has been disabled altogether, do nothing here.
+ if (!empty($this->options['commits_disabled'])) {
+ return;
+ }
try {
$this->connect();
return $this->solr->commit(FALSE);
diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc
index a41ce05e..6279f05d 100644
--- a/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc
+++ b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc
@@ -367,8 +367,11 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
*/
public function getFields($num_terms = 0) {
$fields = array();
- foreach ($this->getLuke($num_terms)->fields as $name => $info) {
- $fields[$name] = new SearchApiSolrField($info);
+ $luke_data = $this->getLuke($num_terms);
+ if (isset($luke_data->fields)) {
+ foreach ($luke_data->fields as $name => $info) {
+ $fields[$name] = new SearchApiSolrField($info);
+ }
}
return $fields;
}
@@ -514,7 +517,6 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
public function clearCache() {
if ($cid = $this->getCacheId()) {
cache_clear_all($cid, 'cache_search_api_solr', TRUE);
- cache_clear_all($cid, 'cache_search_api_solr', TRUE);
}
$this->luke = array();
$this->stats = NULL;
@@ -944,10 +946,18 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
// built-in http_build_query() doesn't give us the format Solr wants.
$queryString = $this->httpBuildQuery($params);
+ if (!empty($this->options['log_query'])) {
+ watchdog('search_api_solr', 'Query: @query', array('@query' => $queryString), WATCHDOG_DEBUG);
+ }
+
if ($method == 'GET' || $method == 'AUTO') {
$searchUrl = $this->constructUrl(self::SEARCH_SERVLET, array(), $queryString);
if ($method == 'GET' || strlen($searchUrl) <= variable_get('search_api_solr_http_get_max_length', 4000)) {
- return $this->sendRawGet($searchUrl);
+ $response = $this->sendRawGet($searchUrl);
+ if (!empty($this->options['log_response'])) {
+ $this->logResponse($response);
+ }
+ return $response;
}
}
@@ -955,7 +965,28 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
$searchUrl = $this->constructUrl(self::SEARCH_SERVLET);
$options['data'] = $queryString;
$options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
- return $this->sendRawPost($searchUrl, $options);
+ $response = $this->sendRawPost($searchUrl, $options);
+
+ if (!empty($this->options['log_response'])) {
+ $this->logResponse($response);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Logs a Solr response object.
+ *
+ * @param object $response
+ * The response received from Solr.
+ */
+ protected function logResponse($response) {
+ $data = $response->code . ' ' . $response->status_message . "\n" . print_r($response->response, TRUE);
+ watchdog('search_api_solr', 'Response: @response
', array('@response' => $data), WATCHDOG_DEBUG);
+
+ if (!empty($response->facet_counts)) {
+ watchdog('search_api_solr', 'Facets: @facets
', array('@facets' => print_r($response->facet_counts, TRUE)), WATCHDOG_DEBUG);
+ }
}
}
diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info
index 291879e7..0ea0d30c 100644
--- a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info
+++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info
@@ -11,9 +11,9 @@ files[] = includes/solr_connection.interface.inc
files[] = includes/solr_field.inc
files[] = includes/spellcheck.inc
-; Information added by Drupal.org packaging script on 2016-02-23
-version = "7.x-1.9+10-dev"
+; Information added by Drupal.org packaging script on 2017-02-23
+version = "7.x-1.12"
core = "7.x"
project = "search_api_solr"
-datestamp = "1456233244"
+datestamp = "1487844794"
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/elevate.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/elevate.xml
new file mode 100644
index 00000000..71ea0006
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/elevate.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/mapping-ISOLatin1Accent.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/mapping-ISOLatin1Accent.txt
new file mode 100644
index 00000000..b92d03c5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/mapping-ISOLatin1Accent.txt
@@ -0,0 +1,14 @@
+# This file contains character mappings for the default fulltext field type.
+# The source characters (on the left) will be replaced by the respective target
+# characters before any other processing takes place.
+# Lines starting with a pound character # are ignored.
+#
+# For sensible defaults, use the mapping-ISOLatin1Accent.txt file distributed
+# with the example application of your Solr version.
+#
+# Examples:
+# "À" => "A"
+# "\u00c4" => "A"
+# "\u00c4" => "\u0041"
+# "æ" => "ae"
+# "\n" => " "
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/protwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/protwords.txt
new file mode 100644
index 00000000..cda85814
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/protwords.txt
@@ -0,0 +1,7 @@
+#-----------------------------------------------------------------------
+# This file blocks words from being operated on by the stemmer and word delimiter.
+&
+<
+>
+'
+"
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema.xml
new file mode 100644
index 00000000..d813d128
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema.xml
@@ -0,0 +1,693 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_fields.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_fields.xml
new file mode 100644
index 00000000..02b36555
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_fields.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_types.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_types.xml
new file mode 100644
index 00000000..bd716b82
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/schema_extra_types.xml
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig.xml
new file mode 100644
index 00000000..136ccfe5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig.xml
@@ -0,0 +1,1793 @@
+
+
+
+
+
+
+
+
+ ${solr.abortOnConfigurationError:true}
+
+
+ ${solr.luceneMatchVersion:LUCENE_60}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ ${solr.lock.type:native}
+
+
+
+
+
+ true
+
+
+
+
+ 1
+
+ 0
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.autoCommit.MaxDocs:10000}
+ ${solr.autoCommit.MaxTime:120000}
+
+
+
+
+ ${solr.autoSoftCommit.MaxDocs:2000}
+ ${solr.autoSoftCommit.MaxTime:10000}
+
+
+
+
+
+
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+ 1024
+
+
+ -1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+ 20
+
+
+ 200
+
+
+
+
+
+
+
+
+
+
+
+ solr rocks010
+
+
+
+
+
+ false
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ explicit
+ json
+ true
+ text
+
+
+
+
+
+
+
+ {!xport}
+ xsort
+ false
+
+
+
+ query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ edismax
+ content
+ explicit
+ true
+ 0.01
+
+ ${solr.pinkPony.timeAllowed:-1}
+ *:*
+
+
+ false
+
+ true
+ false
+
+ 1
+
+
+ spellcheck
+ elevator
+
+
+
+
+
+
+ content
+ 1
+ 1
+ 3
+ 15
+ 20
+ false
+
+ ${solr.mlt.timeAllowed:2000}
+
+
+
+
+
+
+ content
+ explicit
+ true
+
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+ _src_
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+ true
+ ignored_
+
+
+ true
+ links
+ ignored_
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ explicit
+ true
+
+
+
+
+
+
+ ${solr.replication.master:false}
+ commit
+ startup
+ ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml}
+
+
+ ${solr.replication.slave:false}
+ ${solr.replication.masterUrl:http://localhost:8983/solr}/replication
+ ${solr.replication.pollInterval:00:00:60}
+
+
+
+
+
+
+ true
+ json
+ true
+
+
+
+
+
+
+
+
+
+ default
+ wordbreak
+ false
+ false
+ 1
+ 5
+ 5
+ true
+ true
+ 10
+ 5
+
+
+ spellcheck
+
+
+
+
+
+
+ mySuggester
+ FuzzyLookupFactory
+ DocumentDictionaryFactory
+ cat
+ price
+ string
+
+
+
+
+
+ true
+ 10
+
+
+ suggest
+
+
+
+
+
+
+
+
+
+ true
+
+
+ tvComponent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ terms
+
+
+
+
+
+
+ string
+ elevate.xml
+
+
+
+
+
+ explicit
+
+
+ elevator
+
+
+
+
+
+
+
+
+
+
+ 100
+
+
+
+
+
+
+
+ 70
+
+ 0.5
+
+ [-\w ,/\n\"']{20,200}
+
+
+
+
+
+
+ ]]>
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,,
+ ,,
+ ,,
+ ,,
+ ,]]>
+ ]]>
+
+
+
+
+
+ 10
+ .,!?
+
+
+
+
+
+
+ WORD
+
+
+ en
+ US
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/plain; charset=UTF-8
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+ *:*
+
+
+
+
+
+
+
+
+
+
+
+ textSpell
+
+
+
+ default
+ spell
+ spellchecker
+ true
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig_extra.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig_extra.xml
new file mode 100644
index 00000000..c5bc3acf
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrconfig_extra.xml
@@ -0,0 +1,80 @@
+
+
+
+textSpell
+
+
+
+
+
+ default
+ spell
+ spellchecker
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrcore.properties b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrcore.properties
new file mode 100644
index 00000000..fb8d7d05
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/solrcore.properties
@@ -0,0 +1,20 @@
+# Defines Solr properties for this specific core.
+solr.replication.master=false
+solr.replication.slave=false
+solr.replication.pollInterval=00:00:60
+solr.replication.masterUrl=http://localhost:8983/solr
+solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml
+solr.mlt.timeAllowed=2000
+# You should not set your luceneMatchVersion to anything lower than your Solr
+# Version.
+solr.luceneMatchVersion=6.0
+solr.pinkPony.timeAllowed=-1
+# autoCommit after 10000 docs
+solr.autoCommit.MaxDocs=10000
+# autoCommit after 2 minutes
+solr.autoCommit.MaxTime=120000
+# autoSoftCommit after 2000 docs
+solr.autoSoftCommit.MaxDocs=2000
+# autoSoftCommit after 10 seconds
+solr.autoSoftCommit.MaxTime=10000
+solr.contrib.dir=../../../contrib
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/stopwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/stopwords.txt
new file mode 100644
index 00000000..d7f243e4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/stopwords.txt
@@ -0,0 +1,4 @@
+# Contains words which shouldn't be indexed for fulltext fields, e.g., because
+# they're too common. For documentation of the format, see
+# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.StopFilterFactory
+# (Lines starting with a pound character # are ignored.)
diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/synonyms.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/synonyms.txt
new file mode 100644
index 00000000..7d22eea6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/6.x/synonyms.txt
@@ -0,0 +1,3 @@
+# Contains synonyms to use for your index. For the format used, see
+# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.SynonymFilterFactory
+# (Lines starting with a pound character # are ignored.)