diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 08feb8b7..605484d8 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,5 +1,88 @@
-Search API 1.x, dev (xx/xx/xxxx):
----------------------------------
+Search API 1.11 (12/25/2013):
+-----------------------------
+- #1879196 by drunken monkey: Fixed invalid old indexes causing errors.
+- #2155127 by drunken monkey: Clarified the scope of the "Node access" and
+ "Exclude unpublished nodes" data alterations.
+- #2155575 by drunken monkey: Fixed incorrect "Server index status" warnings.
+- #2159011 by idebr, drunken monkey: Fixed highlighting of keywords with PCRE
+ special characters.
+- #2155721 by rjacobs, drunken monkey: Added support for Views' get_total_rows
+ property.
+- #2158873 by drumm, drunken monkey: Fixed "all of" operator of Views entity
+ filter handler.
+- #2156021 by jgullstr: Fixed confirm message when disabling servers.
+- #2146435 by timkang: Fixed Views paging with custom pager add-ons.
+- #2150347 by drunken monkey: Added access callbacks for indexes and servers.
+
+Search API 1.10 (12/09/2013):
+-----------------------------
+- #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs.
+- #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c.
+- #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too.
+- #2100671 by drunken monkey: Fixed stopwords processor to ignore missing
+ stopwords.
+- #2139239 by drunken monkey: Fixed highlighting for the last word of a field.
+- #1925114 by azinck: Fixed Views Facet Block integration with Panels.
+- #2139215 by drunken monkey: Fixed $context parameter of batch callback.
+- #2143659 by khiminrm: Fixed typo in update function 7116.
+- #2134509 by kscheirer, drunken monkey: Removed unused variables and
+ parameters.
+- #2136019 by drunken monkey: Fixed mapping callback for taxonomy term facets.
+- #2128001 by drunken monkey: Fixed the logic of the "contains none of these
+ words" fulltext operator.
+- #2128947 by stBorchert, drunken monkey: Fixed facet handling for multiple
+ searches on a page.
+- #2128529 by Frando, drunken monkey: Added a way for facet query type plugins
+ to pass options to the search query.
+- #1551302 by drunken monkey: Fixed the server tasks system.
+- #2135363 by drumm, drunken monkey: Added support for Views' use_count_query()
+ method.
+- #1390598 by Damien Tournoud, drunken monkey: Added the concept of query filter
+ tags.
+- #2135255 by dww: Fixed missing pager on first page of search results.
+- #1832334 by Damien Tournoud, drunken monkey: Fixed performance issues of
+ Views options filter handler for huge options lists.
+- #2118589 by mxr576, drunken monkey: Added node access for comment indexes.
+- #1961120 by drunken monkey: Fixed Views handling of short fulltext keywords.
+- #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters".
+- #2100193 by drunken monkey: Turned operations in overview into D8 dropbuttons.
+- #2100199 by drunken monkey: Merged index tabs for a cleaner look.
+- #2115127 by drunken monkey: Fixed cron indexing logic to keep the right order.
+- #1750144 by jsacksick, drunken monkey: Fixed missing Boost option for custom
+ fulltext field types.
+- #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not checking for
+ empty $item_ids.
+- #2100191 by drunken monkey, Bojhan: Added an admin description to the Search
+ API landing page.
+
+Search API 1.9 (10/23/2013):
+----------------------------
+- #2113277 by moonray, drunken monkey: Fixed date facet count for active item.
+- #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields.
+- #2114593 by drunken monkey: Added list of floats to test module.
+- #2109247 by mmikitka, drunken monkey: Exposed the status and module
+ properties to Entity API.
+- #2091499 by sammys, drunken monkey: Added Views contextual filter handler for
+ dates.
+- #2109537 by hefox, drunken monkey: Added alter hooks for workflow plugin
+ definitions.
+- #2102111 by sergei_brill: Added hook_search_api_views_query_alter().
+- #2110315 by drumm, drunken monkey: Added specialized Views filters for users
+ and terms.
+- #2111273 by drunken monkey: Fixed Javascript states for exposed filter
+ operator.
+- #2102353 by aaronbauman: Fixed "smaller than" to read "less than".
+- #2097559 by thijsvdanker: Fixed the language of created search excerpts.
+- #2096275 by andrewbelcher: Fixed calling of Views pager pre/post execute
+ callbacks.
+- #2093023 by maciej.zgadzaj: Added Drush commands to enable and disable
+ indexes.
+- #2088905 by queenvictoria, drunken monkey: Fixed handling of Views
+ override_path option.
+- #2083481 by drunken monkey, nickgs: Added "exclude" option for facets.
+- #2084953 by Yaron Tal: Fixed issue with theme initialization.
+- #2075839 by leeomara, drunken monkey: Added descriptions to field lists for
+ 'Aggregated Fields'.
Search API 1.8 (09/01/2013):
----------------------------
diff --git a/README.txt b/README.txt
index 9ea75ed0..2e2f581f 100644
--- a/README.txt
+++ b/README.txt
@@ -90,7 +90,7 @@ IMPORTANT: Access checks
results are displayed – either by only indexing such items, or by filtering
appropriately at search time.
For search on general site content (item type "Node"), this is already
- supported by the Search API. To enable this, go to the index's "Workflow" tab
+ supported by the Search API. To enable this, go to the index's "Filters" tab
and activate the "Node access" data alteration. This will add the necessary
field, "Node access information", to the index (which you have to leave as
"indexed"). If both this field and "Published" are set to be indexed, access
@@ -171,8 +171,8 @@ form at the bottom of the page. For instance, you might want to index the
author's username to the indexed data of a node, and you need to add the "Body"
entity to the node when you want to index the actual text it contains.
-- Index workflow
- (Configuration > Search API > [Index name] > Workflow)
+- Indexing workflow
+ (Configuration > Search API > [Index name] > Filters)
This page lets you customize how the created index works, and what metadata will
be available, by selecting data alterations and processors (see the glossary for
@@ -210,12 +210,6 @@ search_api_index_worker_callback_runtime:
API will spend indexing (for all indexes combined) in each cron run. The
default is 15 seconds.
-search_api_batch_per_cron:
- By changing this variable, you can define how many batch items are created on
- a single cron run. The value is per index, so on a site with 5 indexes with a
- cron limit of 100 each, the default value of 10 will load and queue up to 5000
- search items in up to 50 batch items.
-
Information for developers
--------------------------
diff --git a/boosts-and-queryconditon.patch b/boosts-and-queryconditon.patch
new file mode 100644
index 00000000..35edba50
--- /dev/null
+++ b/boosts-and-queryconditon.patch
@@ -0,0 +1,39 @@
+diff --git a/search_api.admin.inc b/search_api.admin.inc
+index 5fbc8d8..9a5122e 100644
+--- a/search_api.admin.inc
++++ b/search_api.admin.inc
+@@ -1480,8 +1480,8 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
+ $fulltext_type = array(0 => 'text');
+ $entity_types = entity_get_info();
+ $default_types = search_api_default_field_types();
+- $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
+-
++ // $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '5000', '10000', '20000', '40000', '80000', '160000', '320000'));
++ $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
+ $form_state['index'] = $index;
+ $form['#theme'] = 'search_api_admin_fields_table';
+ $form['#tree'] = TRUE;
+diff --git a/search_api.module b/search_api.module
+index bba0681..ba27465 100644
+--- a/search_api.module
++++ b/search_api.module
+@@ -1444,7 +1444,7 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
+ $query->filter($filter);
+ }
+ else {
+- $query->condition('status', NODE_PUBLISHED);
++ // $query->condition('status', NODE_PUBLISHED);
+ }
+ // Filter by node access grants.
+ $filter = $query->createFilter('OR');
+@@ -1636,6 +1636,10 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
+ foreach ($nested as $prefix => $nested_fields) {
+ if (isset($wrapper->$prefix)) {
+ $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options);
++ # http://drupal.org/node/1873910#comment-6876200
++ // $subwrapper = $wrapper->$prefix;
++ // $subwrapper->language( $wrapper->language->value() );
++ // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);
+ foreach ($nested_fields as $field => $info) {
+ $fields["$prefix:$field"] = $info;
+ }
diff --git a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
index b99f7dcf..cddcf56d 100644
--- a/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
+++ b/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
@@ -109,7 +109,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
public function addFacet(array $facet, SearchApiQueryInterface $query) {
if (isset($this->fields[$facet['name']])) {
$options = &$query->getOptions();
- $options['search_api_facets'][$facet['name']] = $this->fields[$facet['name']];
+ $facet_info = $this->fields[$facet['name']];
+ if (!empty($facet['query_options'])) {
+ // Let facet-specific query options override the set options.
+ $facet_info = $facet['query_options'] + $facet_info;
+ }
+ $options['search_api_facets'][$facet['name']] = $facet_info;
}
}
@@ -139,7 +144,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// I suspect that http://drupal.org/node/593658 would help.
// For now, just taking the first current search for this index. :-/
foreach (search_api_current_search() as $search) {
- list($query, $results) = $search;
+ list($query) = $search;
if ($query->getIndex()->machine_name == $index_id) {
$this->current_search = $search;
}
@@ -196,7 +201,6 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
*/
public function settingsForm(&$form, &$form_state) {
$facet = $form['#facetapi']['facet'];
- $realm = $form['#facetapi']['realm'];
$facet_settings = $this->getFacet($facet)->getSettings();
$options = $facet_settings->settings;
$search_ids = variable_get('search_api_facets_search_ids', array());
@@ -205,6 +209,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
$form['global']['default_true'] = array(
'#type' => 'select',
'#title' => t('Display for searches'),
+ '#prefix' => '
',
'#options' => array(
TRUE => t('For all except the selected'),
FALSE => t('Only for the selected'),
@@ -214,6 +219,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
$form['global']['facet_search_ids'] = array(
'#type' => 'select',
'#title' => t('Search IDs'),
+ '#suffix' => '
',
'#options' => $granularity_options,
'#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE,
);
}
+
+ // Add an "Exclude" option for terms.
+ 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(
+ '#type' => 'checkbox',
+ '#title' => t('Exclude'),
+ '#description' => t('Make the search exclude selected facets, instead of restricting it to them.'),
+ '#suffix' => '',
+ '#weight' => -1,
+ '#default_value' => !empty($options['exclude']),
+ );
+ }
}
}
diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
index ab3002c1..b045fc8f 100644
--- a/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
+++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
@@ -37,6 +37,17 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
public function execute($query) {
// Return terms for this facet.
$this->adapter->addFacet($this->facet, $query);
+
+ $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;
+
+ // First check if the facet is enabled for this search.
+ $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
+ $facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
+ if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
+ // Facet is not enabled for this search ID.
+ return;
+ }
+
// Change limit to "unlimited" (-1).
$options = &$query->getOptions();
if (!empty($options['search_api_facets'][$this->facet['name']])) {
@@ -121,7 +132,8 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
// Gets active facets, starts building hierarchy.
$parent = $gap = NULL;
- foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) {
+ $active_items = $this->adapter->getActiveItems($this->facet);
+ foreach ($active_items as $value => $item) {
// If the item is active, the count is the result set count.
$build[$value] = array('#count' => $total);
@@ -199,7 +211,9 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
if (!isset($build[$new_value])) {
$build[$new_value] = array('#count' => $count);
}
- else {
+ // Active items already have their value set because it's the current
+ // result count.
+ elseif (!isset($active_items[$new_value])) {
$build[$new_value]['#count'] += $count;
}
diff --git a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
index 1d0e8ebc..587598a3 100644
--- a/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
+++ b/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
@@ -30,53 +30,66 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
// Return terms for this facet.
$this->adapter->addFacet($this->facet, $query);
- $settings = $this->adapter->getFacet($this->facet)->getSettings();
- // Adds the operator parameter.
- $operator = $settings->settings['operator'];
+ $settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;
- // Add active facet filters.
+ // First check if the facet is enabled for this search.
+ $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
+ $facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
+ if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
+ // Facet is not enabled for this search ID.
+ return;
+ }
+
+ // Retrieve the active facet filters.
$active = $this->adapter->getActiveItems($this->facet);
if (empty($active)) {
return;
}
- if (FACETAPI_OPERATOR_OR == $operator) {
- // If we're dealing with an OR facet, we need to use a nested filter.
- $facet_filter = $query->createFilter('OR');
+ // Create the facet filter, and add a tag to it so that it can be easily
+ // identified down the line by services when they need to exclude facets.
+ $operator = $settings['operator'];
+ if ($operator == FACETAPI_OPERATOR_AND) {
+ $conjunction = 'AND';
+ }
+ elseif ($operator == FACETAPI_OPERATOR_OR) {
+ $conjunction = 'OR';
}
else {
- // Otherwise we set the conditions directly on the query.
- $facet_filter = $query;
+ throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator)));
}
+ $tags = array('facet:' . $this->facet['field']);
+ $facet_filter = $query->createFilter($conjunction, $tags);
foreach ($active as $filter => $filter_array) {
$field = $this->facet['field'];
$this->addFacetFilter($facet_filter, $field, $filter);
}
- // For OR facets, we now have to add the filter to the query.
- if (FACETAPI_OPERATOR_OR == $operator) {
- $query->filter($facet_filter);
- }
+ // Now add the filter to the query.
+ $query->filter($facet_filter);
}
/**
* Helper method for setting a facet filter on a query or query filter object.
*/
protected function addFacetFilter($query_filter, $field, $filter) {
+ // Test if this filter should be negated.
+ $settings = $this->adapter->getFacet($this->facet)->getSettings();
+ $exclude = !empty($settings->settings['exclude']);
// Integer (or other nun-string) filters might mess up some of the following
// comparison expressions.
$filter = (string) $filter;
if ($filter == '!') {
- $query_filter->condition($field, NULL);
+ $query_filter->condition($field, NULL, $exclude ? '<>' : '=');
}
elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
$lower = trim(substr($filter, 1, $pos));
$upper = trim(substr($filter, $pos + 4, -1));
if ($lower == '*' && $upper == '*') {
- $query_filter->condition($field, NULL, '<>');
+ $query_filter->condition($field, NULL, $exclude ? '=' : '<>');
}
- else {
+ elseif (!$exclude) {
if ($lower != '*') {
// Iff we have a range with two finite boundaries, we set two
// conditions (larger than the lower bound and less than the upper
@@ -92,9 +105,22 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
$query_filter->condition($field, $upper, '<=');
}
}
+ else {
+ // Same as above, but with inverted logic.
+ if ($lower != '*') {
+ if ($upper != '*' && ($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
+ $original_query_filter = $query_filter;
+ $query_filter = new SearchApiQueryFilter('OR');
+ }
+ $query_filter->condition($field, $lower, '<');
+ }
+ if ($upper != '*') {
+ $query_filter->condition($field, $upper, '>');
+ }
+ }
}
else {
- $query_filter->condition($field, $filter);
+ $query_filter->condition($field, $filter, $exclude ? '<>' : '=');
}
if (isset($original_query_filter)) {
$original_query_filter->filter($query_filter);
diff --git a/contrib/search_api_facetapi/search_api_facetapi.info b/contrib/search_api_facetapi/search_api_facetapi.info
index 1c6c24b8..a0b9dcba 100644
--- a/contrib/search_api_facetapi/search_api_facetapi.info
+++ b/contrib/search_api_facetapi/search_api_facetapi.info
@@ -9,9 +9,9 @@ files[] = plugins/facetapi/adapter.inc
files[] = plugins/facetapi/query_type_term.inc
files[] = plugins/facetapi/query_type_date.inc
-; Information added by drupal.org packaging script on 2013-09-01
-version = "7.x-1.8"
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
core = "7.x"
project = "search_api"
-datestamp = "1378025826"
+datestamp = "1387965506"
diff --git a/contrib/search_api_facetapi/search_api_facetapi.module b/contrib/search_api_facetapi/search_api_facetapi.module
index 64aec586..29a2dbd5 100644
--- a/contrib/search_api_facetapi/search_api_facetapi.module
+++ b/contrib/search_api_facetapi/search_api_facetapi.module
@@ -92,7 +92,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
// other modules.
$type_settings = array(
'taxonomy_term' => array(
- 'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy',
+ 'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
),
'date' => array(
'query type' => 'date',
@@ -226,6 +226,26 @@ function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
}
+/**
+ * Gets hierarchy information for taxonomy terms.
+ *
+ * Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
+ *
+ * Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
+ * our special "!" value is not passed.
+ *
+ * @param array $values
+ * An array containing the term IDs.
+ *
+ * @return array
+ * An associative array mapping term IDs to parent IDs (where parents could be
+ * found).
+ */
+function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
+ $values = array_filter($values, 'is_numeric');
+ return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
+}
+
/**
* Map callback for all search_api facet fields.
*
diff --git a/contrib/search_api_views/includes/display_facet_block.inc b/contrib/search_api_views/includes/display_facet_block.inc
index 39d256b3..35ad14f5 100644
--- a/contrib/search_api_views/includes/display_facet_block.inc
+++ b/contrib/search_api_views/includes/display_facet_block.inc
@@ -151,11 +151,9 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
}
- public function execute() {
- if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
- form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
- return NULL;
- }
+ public function query(){
+ parent::query();
+
$facet_field = $this->get_option('facet_field');
if (!$facet_field) {
return NULL;
@@ -165,7 +163,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
if (!$base_path) {
$base_path = $_GET['q'];
}
- $this->view->build();
+
$limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page'];
$query_options = &$this->view->query->getOptions();
if (!$this->get_option('hide_block')) {
@@ -179,6 +177,17 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
$query_options['search_api_base_path'] = $base_path;
$this->view->query->range(0, 0);
+ }
+
+ public function render() {
+ if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
+ form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
+ return NULL;
+ }
+ $facet_field = $this->get_option('facet_field');
+ if (!$facet_field) {
+ return NULL;
+ }
$this->view->execute();
@@ -229,7 +238,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
// Initializes variables passed to theme hook.
$variables = array(
'text' => $name,
- 'path' => $base_path,
+ 'path' => $this->view->query->getOption('search_api_base_path'),
'count' => $term['count'],
'options' => array(
'attributes' => array('class' => 'facetapi-inactive'),
@@ -249,10 +258,16 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
return NULL;
}
- $info['content']['facets'] = array(
+ return array(
+ 'facets' => array(
'#theme' => 'item_list',
'#items' => $facets,
+ )
);
+ }
+
+ public function execute(){
+ $info['content'] = $this->render();
$info['content']['more'] = $this->render_more_link();
$info['subject'] = filter_xss_admin($this->view->get_title());
return $info;
diff --git a/contrib/search_api_views/includes/handler_argument.inc b/contrib/search_api_views/includes/handler_argument.inc
index f3a97886..a11a662b 100644
--- a/contrib/search_api_views/includes/handler_argument.inc
+++ b/contrib/search_api_views/includes/handler_argument.inc
@@ -1,5 +1,10 @@
value)) {
+ $this->fillValue();
+ if ($this->value === FALSE) {
+ $this->abort();
+ return;
+ }
+ }
+
+ $outer_conjunction = strtoupper($this->operator);
+
+ if (empty($this->options['not'])) {
+ $operator = '=';
+ $inner_conjunction = 'OR';
+ }
+ else {
+ $operator = '<>';
+ $inner_conjunction = 'AND';
+ }
+
+ if (!empty($this->value)) {
+ if (!empty($this->value)) {
+ $outer_filter = $this->query->createFilter($outer_conjunction);
+ foreach ($this->value as $value) {
+ $value_filter = $this->query->createFilter($inner_conjunction);
+ $values = explode(';', $value);
+ $values = array_map(array($this, 'getTimestamp'), $values);
+ if (in_array(FALSE, $values, TRUE)) {
+ $this->abort();
+ return;
+ }
+ $is_range = (count($values) > 1);
+
+ $inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter);
+ $range_op = (empty($this->options['not']) ? '>=' : '<');
+ $inner_filter->condition($this->real_field, $values[0], $is_range ? $range_op : $operator);
+ if ($is_range) {
+ $range_op = (empty($this->options['not']) ? '<=' : '>');
+ $inner_filter->condition($this->real_field, $values[1], $range_op);
+ $value_filter->filter($inner_filter);
+ }
+ $outer_filter->filter($value_filter);
+ }
+
+ $this->query->filter($outer_filter);
+ }
+ }
+ }
+
+ /**
+ * Converts a value to a timestamp, if it isn't one already.
+ *
+ * @param string|int $value
+ * The value to convert. Either a timestamp, or a date/time string as
+ * recognized by strtotime().
+ *
+ * @return int|false
+ * The parsed timestamp, or FALSE if an illegal string was passed.
+ */
+ public function getTimestamp($value) {
+ if (is_numeric($value)) {
+ return $value;
+ }
+
+ return strtotime($value);
+ }
+
+ /**
+ * Fills $this->value with data from the argument.
+ */
+ protected function fillValue() {
+ if (!empty($this->options['break_phrase'])) {
+ // Set up defaults:
+ if (!isset($this->value)) {
+ $this->value = array();
+ }
+
+ if (!isset($this->operator)) {
+ $this->operator = 'OR';
+ }
+
+ if (empty($this->argument)) {
+ return;
+ }
+
+ if (preg_match('/^([-\d;:\s]+\+)*[-\d;:\s]+$/', $this->argument)) {
+ // The '+' character in a query string may be parsed as ' '.
+ $this->value = explode('+', $this->argument);
+ }
+ elseif (preg_match('/^([-\d;:\s]+,)*[-\d;:\s]+$/', $this->argument)) {
+ $this->operator = 'AND';
+ $this->value = explode(',', $this->argument);
+ }
+
+ // Keep an 'error' value if invalid strings were given.
+ if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) {
+ $this->value = FALSE;
+ }
+ }
+ else {
+ $this->value = array($this->argument);
+ }
+ }
+
+ /**
+ * Aborts the associated query due to an illegal argument.
+ */
+ protected function abort() {
+ $variables['!field'] = $this->definition['group'] . ': ' . $this->definition['title'];
+ $this->query->abort(t('Illegal argument passed to !field contextual filter.', $variables));
+ }
+
+ /**
+ * Computes the title this argument will assign the view, given the argument.
+ *
+ * @return string
+ * A title fitting for the passed argument.
+ */
+ public function title() {
+ if (!empty($this->argument)) {
+ if (empty($this->value)) {
+ $this->fillValue();
+ }
+ $dates = array();
+ foreach ($this->value as $date) {
+ $date_parts = explode(';', $date);
+
+ $ts = $this->getTimestamp($date_parts[0]);
+ $datestr = format_date($ts, 'short');
+ if (count($date_parts) > 1) {
+ $ts = $this->getTimestamp($date_parts[1]);
+ $datestr .= ' - ' . format_date($ts, 'short');
+ }
+
+ if ($datestr) {
+ $dates[] = $datestr;
+ }
+ }
+
+ return $dates ? implode(', ', $dates) : check_plain($this->argument);
+ }
+
+ return check_plain($this->argument);
+ }
+
+}
diff --git a/contrib/search_api_views/includes/handler_argument_fulltext.inc b/contrib/search_api_views/includes/handler_argument_fulltext.inc
index a2b3f553..a6f00e2d 100644
--- a/contrib/search_api_views/includes/handler_argument_fulltext.inc
+++ b/contrib/search_api_views/includes/handler_argument_fulltext.inc
@@ -1,5 +1,10 @@
t('Is smaller than'),
- '<=' => t('Is smaller than or equal to'),
+ '<' => t('Is less than'),
+ '<=' => t('Is less than or equal to'),
'=' => t('Is equal to'),
'<>' => t('Is not equal to'),
'>=' => t('Is greater than or equal to'),
@@ -46,8 +51,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
* Provide a form for setting the filter value.
*/
public function value_form(&$form, &$form_state) {
- while (is_array($this->value)) {
- $this->value = $this->value ? array_shift($this->value) : NULL;
+ while (is_array($this->value) && count($this->value) < 2) {
+ $this->value = $this->value ? reset($this->value) : NULL;
}
$form['value'] = array(
'#type' => 'textfield',
@@ -58,10 +63,19 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
// Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
- $form['value']['#states']['visible'] = array(
- ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
- ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
- );
+ if (empty($form_state['exposed'])) {
+ $form['value']['#states']['visible'] = array(
+ ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
+ ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
+ );
+ }
+ elseif (!empty($this->options['expose']['use_operator'])) {
+ $name = $this->options['expose']['operator_id'];
+ $form['value']['#states']['visible'] = array(
+ ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
+ ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'),
+ );
+ }
}
/**
diff --git a/contrib/search_api_views/includes/handler_filter_boolean.inc b/contrib/search_api_views/includes/handler_filter_boolean.inc
index b3b21727..2ff1f25e 100644
--- a/contrib/search_api_views/includes/handler_filter_boolean.inc
+++ b/contrib/search_api_views/includes/handler_filter_boolean.inc
@@ -1,5 +1,10 @@
$this->isMultiValued() ? t('Is one of') : t('Is'),
+ 'all of' => t('Is all of'),
+ '<>' => $this->isMultiValued() ? t('Is not one of') : t('Is not'),
+ 'empty' => t('Is empty'),
+ 'not empty' => t('Is not empty'),
+ );
+ if (!$this->isMultiValued()) {
+ unset($operators['all of']);
+ }
+ return $operators;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function option_definition() {
+ $options = parent::option_definition();
+
+ $options['expose']['multiple']['default'] = TRUE;
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function value_form(&$form, &$form_state) {
+ parent::value_form($form, $form_state);
+
+ if (!is_array($this->value)) {
+ $this->value = $this->value ? array($this->value) : array();
+ }
+
+ // Set the correct default value in case the admin-set value is used (and a
+ // value is present). The value is used if the form is either not exposed,
+ // or the exposed form wasn't submitted yet (there is
+ if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) {
+ $form['value']['#default_value'] = $this->ids_to_strings($this->value);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function value_validate($form, &$form_state) {
+ if (!empty($form['value'])) {
+ $value = &$form_state['values']['options']['value'];
+ $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
+ $ids = $this->validate_entity_strings($form['value'], $values);
+
+ if ($ids) {
+ $value = $ids;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function accept_exposed_input($input) {
+ $rc = parent::accept_exposed_input($input);
+
+ if ($rc) {
+ // If we have previously validated input, override.
+ if ($this->validated_exposed_input) {
+ $this->value = $this->validated_exposed_input;
+ }
+ }
+
+ return $rc;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exposed_validate(&$form, &$form_state) {
+ if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+ return;
+ }
+
+ $identifier = $this->options['expose']['identifier'];
+ $input = $form_state['values'][$identifier];
+
+ if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+ $this->operator = $this->options['group_info']['group_items'][$input]['operator'];
+ $input = $this->options['group_info']['group_items'][$input]['value'];
+ }
+
+ $values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input);
+
+ if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) {
+ $this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values);
+ }
+ else {
+ $this->validated_exposed_input = FALSE;
+ }
+ }
+
+ /**
+ * Determines whether multiple user names can be entered into this filter.
+ *
+ * This is either the case if the form isn't exposed, or if the " Allow
+ * multiple selections" option is enabled.
+ *
+ * @param array $options
+ * (optional) The options array to use. If not supplied, the options set on
+ * this filter will be used.
+ *
+ * @return bool
+ * TRUE if multiple values can be entered for this filter, FALSE otherwise.
+ */
+ protected function isMultiValued(array $options = array()) {
+ $options = $options ? $options : $this->options;
+ return empty($options['exposed']) || !empty($options['expose']['multiple']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function admin_summary() {
+ $value = $this->value;
+ $this->value = empty($value) ? '' : $this->ids_to_strings($value);
+ $ret = parent::admin_summary();
+ $this->value = $value;
+ return $ret;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ if ($this->operator === 'empty') {
+ $this->query->condition($this->real_field, NULL, '=', $this->options['group']);
+ }
+ elseif ($this->operator === 'not empty') {
+ $this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
+ }
+ elseif (is_array($this->value)) {
+ $all_of = $this->operator === 'all of';
+ $operator = $all_of ? '=' : $this->operator;
+ if (count($this->value) == 1) {
+ $this->query->condition($this->real_field, reset($this->value), $operator, $this->options['group']);
+ }
+ else {
+ $filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR');
+ foreach ($this->value as $value) {
+ $filter->condition($this->real_field, $value, $operator);
+ }
+ $this->query->filter($filter, $this->options['group']);
+ }
+ }
+ }
+
+}
diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc
index 952a81c5..55226218 100644
--- a/contrib/search_api_views/includes/handler_filter_fulltext.inc
+++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc
@@ -1,5 +1,10 @@
'keys');
+ $options['min_length'] = array('default' => '');
$options['fields'] = array('default' => array());
return $options;
@@ -75,6 +81,55 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
if (isset($form['expose'])) {
$form['expose']['#weight'] = -5;
}
+
+ $form['min_length'] = array(
+ '#title' => t('Minimum keyword length'),
+ '#description' => t('Minimum length of each word in the search keys. Leave empty to allow all words.'),
+ '#type' => 'textfield',
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#default_value' => $this->options['min_length'],
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exposed_validate(&$form, &$form_state) {
+ // Only validate exposed input.
+ if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+ return;
+ }
+
+ // We only need to validate if there is a minimum word length set.
+ if ($this->options['min_length'] < 2) {
+ return;
+ }
+
+ $identifier = $this->options['expose']['identifier'];
+ $input = &$form_state['values'][$identifier];
+
+ if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+ $this->operator = $this->options['group_info']['group_items'][$input]['operator'];
+ $input = &$this->options['group_info']['group_items'][$input]['value'];
+ }
+
+ // If there is no input, we're fine.
+ if (!trim($input)) {
+ return;
+ }
+
+ $words = preg_split('/\s+/', $input);
+ foreach ($words as $i => $word) {
+ if (drupal_strlen($word) < $this->options['min_length']) {
+ unset($words[$i]);
+ }
+ }
+ if (!$words) {
+ $vars['@count'] = $this->options['min_length'];
+ $msg = t('You must include at least one positive keyword with @count characters or more.', $vars);
+ form_error($form[$identifier], $msg);
+ }
+ $input = implode(' ', $words);
}
/**
@@ -108,9 +163,9 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
return;
}
- // If the operator was set to OR, set it as the conjunction. (AND is set by
- // default.)
- if ($this->operator === 'OR') {
+ // If the operator was set to OR or NOT, set OR as the conjunction. (It is
+ // also set for NOT since otherwise it would be "not all of these words".)
+ if ($this->operator != 'AND') {
$this->query->setOption('conjunction', $this->operator);
}
diff --git a/contrib/search_api_views/includes/handler_filter_language.inc b/contrib/search_api_views/includes/handler_filter_language.inc
index f95ddaf9..399f80c1 100644
--- a/contrib/search_api_views/includes/handler_filter_language.inc
+++ b/contrib/search_api_views/includes/handler_filter_language.inc
@@ -41,6 +41,10 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt
*/
public function query() {
global $language_content;
+
+ if (!is_array($this->value)) {
+ $this->value = $this->value ? array($this->value) : array();
+ }
foreach ($this->value as $i => $v) {
if ($v == 'current') {
$this->value[$i] = $language_content->language;
diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc
index 8e1361d1..c63c07e7 100644
--- a/contrib/search_api_views/includes/handler_filter_options.inc
+++ b/contrib/search_api_views/includes/handler_filter_options.inc
@@ -1,16 +1,82 @@
query) {
+ $index = $this->query->getIndex();
+ }
+ elseif (substr($this->view->base_table, 0, 17) == 'search_api_index_') {
+ $index = search_api_index_load(substr($this->view->base_table, 17));
+ }
+ else {
+ return NULL;
+ }
+ $wrapper = $index->entityWrapper(NULL, TRUE);
+ $parts = explode(':', $this->real_field);
+ foreach ($parts as $i => $part) {
+ if (!isset($wrapper->$part)) {
+ return NULL;
+ }
+ $wrapper = $wrapper->$part;
+ $info = $wrapper->info();
+ if ($i < count($parts) - 1) {
+ // Unwrap lists.
+ $level = search_api_list_nesting_level($info['type']);
+ for ($j = 0; $j < $level; ++$j) {
+ $wrapper = $wrapper[0];
+ }
+ }
+ }
+
+ return $wrapper;
+ }
+
+ /**
+ * Fills the value_options property with all possible options.
+ */
+ protected function get_value_options() {
+ if (isset($this->value_options)) {
+ return;
+ }
+
+ $wrapper = $this->get_wrapper();
+ if ($wrapper) {
+ $this->value_options = $wrapper->optionsList('view');
+ }
+ else {
+ $this->value_options = array();
+ }
+ }
+
/**
* Provide a list of options for the operator form.
*/
@@ -63,13 +129,12 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
* Reduce the options according to the selection.
*/
protected function reduce_value_options() {
- $options = array();
- foreach ($this->definition['options'] as $id => $option) {
- if (isset($this->options['value'][$id])) {
- $options[$id] = $option;
+ foreach ($this->value_options as $id => $option) {
+ if (!isset($this->options['value'][$id])) {
+ unset($this->value_options[$id]);
}
}
- return $options;
+ return $this->value_options;
}
/**
@@ -92,27 +157,38 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
* Provide a form for setting options.
*/
public function value_form(&$form, &$form_state) {
- $options = array();
+ $this->get_value_options();
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
- $options += $this->reduce_value_options($form_state);
+ $options = $this->reduce_value_options();
}
else {
- $options += $this->definition['options'];
+ $options = $this->value_options;
}
+
$form['value'] = array(
'#type' => $this->value_form_type,
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#options' => $options,
'#multiple' => TRUE,
- '#size' => min(4, count($this->definition['options'])),
+ '#size' => min(4, count($options)),
'#default_value' => is_array($this->value) ? $this->value : array(),
);
- // Hide the value box if operator is 'empty' or 'not empty'.
+
+ // Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
- $form['value']['#states']['visible'] = array(
- ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
- ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
- );
+ if (empty($form_state['exposed'])) {
+ $form['value']['#states']['visible'] = array(
+ ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
+ ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
+ );
+ }
+ elseif (!empty($this->options['expose']['use_operator'])) {
+ $name = $this->options['expose']['operator_id'];
+ $form['value']['#states']['visible'] = array(
+ ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
+ ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'),
+ );
+ }
}
/**
@@ -139,8 +215,9 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$values = '';
// Remove every element which is not known.
+ $this->get_value_options();
foreach ($this->value as $i => $value) {
- if (!isset($this->definition['options'][$value])) {
+ if (!isset($this->value_options[$value])) {
unset($this->value[$i]);
}
}
@@ -161,7 +238,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
}
// If there is only a single value, use just the plain operator, = or <>.
$operator = check_plain($operator);
- $values = check_plain($this->definition['options'][reset($this->value)]);
+ $values = check_plain($this->value_options[reset($this->value)]);
}
else {
foreach ($this->value as $value) {
@@ -172,7 +249,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$values .= '…';
break;
}
- $values .= check_plain($this->definition['options'][$value]);
+ $values .= check_plain($this->value_options[$value]);
}
}
@@ -197,28 +274,24 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$this->value = reset($this->value);
}
- // Determine operator and conjunction.
+ // Determine operator and conjunction. The defaults are already right for
+ // "all of".
+ $operator = '=';
+ $conjunction = 'AND';
switch ($this->operator) {
case '=':
- $operator = '=';
$conjunction = 'OR';
break;
- case 'all of':
- $operator = '=';
- $conjunction = 'AND';
- break;
-
case '<>':
$operator = '<>';
- $conjunction = 'AND';
break;
}
// If the value is an empty array, we either want no filter at all (for
- // "is none of", or want to find only items with no value for the field.
+ // "is none of"), or want to find only items with no value for the field.
if ($this->value === array()) {
- if ($this->operator != '<>') {
+ if ($operator != '<>') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
return;
diff --git a/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
new file mode 100644
index 00000000..02d30686
--- /dev/null
+++ b/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
@@ -0,0 +1,294 @@
+definition['vocabulary']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function option_definition() {
+ $options = parent::option_definition();
+
+ $options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select');
+ $options['hierarchy'] = array('default' => 0);
+ $options['error_message'] = array('default' => TRUE, 'bool' => TRUE);
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extra_options_form(&$form, &$form_state) {
+ $form['type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Selection type'),
+ '#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')),
+ '#default_value' => $this->options['type'],
+ );
+
+ $form['hierarchy'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show hierarchy in dropdown'),
+ '#default_value' => !empty($this->options['hierarchy']),
+ );
+ $form['hierarchy']['#states']['visible'][':input[name="options[type]"]']['value'] = 'select';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function value_form(&$form, &$form_state) {
+ parent::value_form($form, $form_state);
+
+ if (!empty($this->definition['vocabulary'])) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']);
+ $title = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name));
+ }
+ else {
+ $vocabulary = FALSE;
+ $title = t('Select terms');
+ }
+ $form['value']['#title'] = $title;
+
+ if ($vocabulary && $this->options['type'] == 'textfield') {
+ $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid;
+ }
+ else {
+ if ($vocabulary && !empty($this->options['hierarchy'])) {
+ $tree = taxonomy_get_tree($vocabulary->vid);
+ $options = array();
+
+ if ($tree) {
+ foreach ($tree as $term) {
+ $choice = new stdClass();
+ $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
+ $options[] = $choice;
+ }
+ }
+ }
+ else {
+ $options = array();
+ $query = db_select('taxonomy_term_data', 'td');
+ $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
+ $query->fields('td');
+ $query->orderby('tv.weight');
+ $query->orderby('tv.name');
+ $query->orderby('td.weight');
+ $query->orderby('td.name');
+ $query->addTag('term_access');
+ if ($vocabulary) {
+ $query->condition('tv.machine_name', $vocabulary->machine_name);
+ }
+ $result = $query->execute();
+ foreach ($result as $term) {
+ $options[$term->tid] = $term->name;
+ }
+ }
+
+ $default_value = (array) $this->value;
+
+ if (!empty($form_state['exposed'])) {
+ $identifier = $this->options['expose']['identifier'];
+
+ if (!empty($this->options['expose']['reduce'])) {
+ $options = $this->reduce_value_options($options);
+
+ if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
+ $default_value = array();
+ }
+ }
+
+ if (empty($this->options['expose']['multiple'])) {
+ if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
+ $default_value = 'All';
+ }
+ elseif (empty($default_value)) {
+ $keys = array_keys($options);
+ $default_value = array_shift($keys);
+ }
+ // Due to #1464174 there is a chance that array('') was saved in the
+ // admin ui. Let's choose a safe default value.
+ elseif ($default_value == array('')) {
+ $default_value = 'All';
+ }
+ else {
+ $copy = $default_value;
+ $default_value = array_shift($copy);
+ }
+ }
+ }
+ $form['value']['#type'] = 'select';
+ $form['value']['#multiple'] = TRUE;
+ $form['value']['#options'] = $options;
+ $form['value']['#size'] = min(9, count($options));
+ $form['value']['#default_value'] = $default_value;
+
+ if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) {
+ $form_state['input'][$identifier] = $default_value;
+ }
+ }
+ }
+
+ /**
+ * Reduces the available exposed options according to the selection.
+ */
+ protected function reduce_value_options(array $options) {
+ foreach ($options as $id => $option) {
+ if (empty($this->options['value'][$id])) {
+ unset($options[$id]);
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function value_validate($form, &$form_state) {
+ // We only validate if they've chosen the text field style.
+ if ($this->options['type'] != 'textfield') {
+ return;
+ }
+
+ parent::value_validate($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function accept_exposed_input($input) {
+ if (empty($this->options['exposed'])) {
+ return TRUE;
+ }
+
+ // If view is an attachment and is inheriting exposed filters, then assume
+ // exposed input has already been validated.
+ if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) {
+ $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
+ }
+
+ // If it's non-required and there's no value don't bother filtering.
+ if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
+ return FALSE;
+ }
+
+ return parent::accept_exposed_input($input);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exposed_validate(&$form, &$form_state) {
+ if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+ return;
+ }
+
+ // We only validate if they've chosen the text field style.
+ if ($this->options['type'] != 'textfield') {
+ $input = $form_state['values'][$this->options['expose']['identifier']];
+ if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+ $input = $this->options['group_info']['group_items'][$input]['value'];
+ }
+
+ if ($input != 'All') {
+ $this->validated_exposed_input = (array) $input;
+ }
+ return;
+ }
+
+ parent::exposed_validate($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function validate_entity_strings(array &$form, array $values) {
+ if (empty($values)) {
+ return array();
+ }
+
+ $tids = array();
+ $names = array();
+ $missing = array();
+ foreach ($values as $value) {
+ $missing[strtolower($value)] = TRUE;
+ $names[] = $value;
+ }
+
+ if (!$names) {
+ return FALSE;
+ }
+
+ $query = db_select('taxonomy_term_data', 'td');
+ $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
+ $query->fields('td');
+ $query->condition('td.name', $names);
+ if (!empty($this->definition['vocabulary'])) {
+ $query->condition('tv.machine_name', $this->definition['vocabulary']);
+ }
+ $query->addTag('term_access');
+ $result = $query->execute();
+ foreach ($result as $term) {
+ unset($missing[strtolower($term->name)]);
+ $tids[] = $term->tid;
+ }
+
+ if ($missing) {
+ if (!empty($this->options['error_message'])) {
+ form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing)))));
+ }
+ else {
+ // Add a bogus TID which will show an empty result for a positive filter
+ // and be ignored for an excluding one.
+ $tids[] = 0;
+ }
+ }
+
+ return $tids;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function expose_form(&$form, &$form_state) {
+ parent::expose_form($form, $form_state);
+ if ($this->options['type'] != 'select') {
+ unset($form['expose']['reduce']);
+ }
+ $form['error_message'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display error message'),
+ '#description' => t('Display an error message if one of the entered terms could not be found.'),
+ '#default_value' => !empty($this->options['error_message']),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function ids_to_strings(array $ids) {
+ return implode(', ', db_select('taxonomy_term_data', 'td')
+ ->fields('td', array('name'))
+ ->condition('td.tid', array_filter($ids))
+ ->execute()
+ ->fetchCol());
+ }
+
+}
diff --git a/contrib/search_api_views/includes/handler_filter_text.inc b/contrib/search_api_views/includes/handler_filter_text.inc
index a14d4a44..fae248a6 100644
--- a/contrib/search_api_views/includes/handler_filter_text.inc
+++ b/contrib/search_api_views/includes/handler_filter_text.inc
@@ -1,5 +1,10 @@
isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete';
+ $form['value']['#autocomplete_path'] = $path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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();
+ foreach ($ids as $uid) {
+ if (!$uid) {
+ $names[] = variable_get('anonymous', t('Anonymous'));
+ }
+ elseif (isset($result[$uid])) {
+ $names[] = $result[$uid];
+ }
+ }
+ return implode(', ', $names);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function validate_entity_strings(array &$form, array $values) {
+ $uids = array();
+ $missing = array();
+ foreach ($values as $value) {
+ if (drupal_strtolower($value) === drupal_strtolower(variable_get('anonymous', t('Anonymous')))) {
+ $uids[] = 0;
+ }
+ else {
+ $missing[strtolower($value)] = $value;
+ }
+ }
+
+ if (!$missing) {
+ return $uids;
+ }
+
+ $result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing)));
+ foreach ($result as $account) {
+ unset($missing[strtolower($account->name)]);
+ $uids[] = $account->uid;
+ }
+
+ if ($missing) {
+ form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing))));
+ }
+
+ return $uids;
+ }
+
+}
diff --git a/contrib/search_api_views/includes/handler_sort.inc b/contrib/search_api_views/includes/handler_sort.inc
index 463e6555..1b2cb195 100644
--- a/contrib/search_api_views/includes/handler_sort.inc
+++ b/contrib/search_api_views/includes/handler_sort.inc
@@ -1,5 +1,10 @@
_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));
diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc
index f695509b..36bc2326 100644
--- a/contrib/search_api_views/includes/query.inc
+++ b/contrib/search_api_views/includes/query.inc
@@ -1,5 +1,10 @@
array(),
'#default_value' => $this->options['parse_mode'],
);
- $modes = array();
foreach ($this->query->parseModes() as $key => $mode) {
$form['parse_mode']['#options'][$key] = $mode['name'];
if (!empty($mode['description'])) {
@@ -243,16 +247,6 @@ class SearchApiViewsQuery extends views_plugin_query {
$view->init_pager();
$this->pager->query();
- // Views passes sometimes NULL and sometimes the integer 0 for "All" in a
- // pager. If set to 0 items, a string "0" is passed. Therefore, we unset
- // the limit if an empty value OTHER than a string "0" was passed.
- if (!$this->limit && $this->limit !== '0') {
- $this->limit = NULL;
- }
- // Set the range. (We always set this, as there might even be an offset if
- // all items are shown.)
- $this->query->range($this->offset, $this->limit);
-
// Set the search ID, if it was not already set.
if ($this->query->getOption('search id') == get_class($this->query)) {
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
@@ -262,6 +256,20 @@ class SearchApiViewsQuery extends views_plugin_query {
if (!empty($this->options['search_api_bypass_access'])) {
$this->query->setOption('search_api_bypass_access', TRUE);
}
+
+ // If the View and the Panel conspire to provide an overridden path then
+ // pass that through as the base path.
+ 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);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alter(&$view) {
+ parent::alter($view);
+ drupal_alter('search_api_views_query', $view, $this);
}
/**
@@ -284,7 +292,28 @@ class SearchApiViewsQuery extends views_plugin_query {
return;
}
+ // Calculate the "skip result count" option, if it wasn't already set to
+ // FALSE.
+ $skip_result_count = $this->query->getOption('skip result count', TRUE);
+ if ($skip_result_count) {
+ $skip_result_count = !$this->pager->use_count_query() && empty($view->get_total_rows);
+ $this->query->setOption('skip result count', $skip_result_count);
+ }
+
try {
+ // Trigger pager pre_execute().
+ $this->pager->pre_execute($this->query);
+
+ // Views passes sometimes NULL and sometimes the integer 0 for "All" in a
+ // pager. If set to 0 items, a string "0" is passed. Therefore, we unset
+ // the limit if an empty value OTHER than a string "0" was passed.
+ if (!$this->limit && $this->limit !== '0') {
+ $this->limit = NULL;
+ }
+ // Set the range. (We always set this, as there might even be an offset if
+ // all items are shown.)
+ $this->query->range($this->offset, $this->limit);
+
$start = microtime(TRUE);
// Execute the search.
@@ -292,11 +321,13 @@ class SearchApiViewsQuery extends views_plugin_query {
$this->search_api_results = $results;
// Store the results.
- $this->pager->total_items = $view->total_rows = $results['result count'];
- if (!empty($this->pager->options['offset'])) {
- $this->pager->total_items -= $this->pager->options['offset'];
+ if (!$skip_result_count) {
+ $this->pager->total_items = $view->total_rows = $results['result count'];
+ if (!empty($this->pager->options['offset'])) {
+ $this->pager->total_items -= $this->pager->options['offset'];
+ }
+ $this->pager->update_page_info();
}
- $this->pager->update_page_info();
$view->result = array();
if (!empty($results['results'])) {
$this->addResults($results['results'], $view);
@@ -304,6 +335,9 @@ class SearchApiViewsQuery extends views_plugin_query {
// We shouldn't use $results['performance']['complete'] here, since
// extracting the results probably takes considerable time as well.
$view->execute_time = microtime(TRUE) - $start;
+
+ // Trigger pager post_execute().
+ $this->pager->post_execute($view->result);
}
catch (Exception $e) {
$this->errors[] = $e->getMessage();
@@ -317,8 +351,14 @@ class SearchApiViewsQuery extends views_plugin_query {
*
* Used by handlers to flag a fatal error which shouldn't be displayed but
* still lead to the view returning empty and the search not being executed.
+ *
+ * @param string|null $msg
+ * Optionally, a translated, unescaped error message to display.
*/
- public function abort() {
+ public function abort($msg = NULL) {
+ if ($msg) {
+ $this->errors[] = $msg;
+ }
$this->abort = TRUE;
}
@@ -520,9 +560,9 @@ class SearchApiViewsQuery extends views_plugin_query {
// Query interface methods (proxy to $this->query)
//
- public function createFilter($conjunction = 'AND') {
+ public function createFilter($conjunction = 'AND', $tags = array()) {
if (!$this->errors) {
- return $this->query->createFilter($conjunction);
+ return $this->query->createFilter($conjunction, $tags);
}
}
diff --git a/contrib/search_api_views/search_api_views.api.php b/contrib/search_api_views/search_api_views.api.php
new file mode 100644
index 00000000..95a92ca2
--- /dev/null
+++ b/contrib/search_api_views/search_api_views.api.php
@@ -0,0 +1,34 @@
+name == 'my_view' && is_numeric($view->exposed_raw_input['title']) && $view->exposed_raw_input['title'] > 0) {
+ // Traverse through the 'where' part of the query.
+ foreach ($query->where as &$condition_group) {
+ foreach ($condition_group['conditions'] as &$condition) {
+ // If this is the part of the query filtering on title, chang the
+ // condition to filter on node ID.
+ if (reset($condition) == 'node.title') {
+ $condition = array('node.nid', $view->exposed_raw_input['title'],'=');
+ }
+ }
+ }
+ }
+}
diff --git a/contrib/search_api_views/search_api_views.info b/contrib/search_api_views/search_api_views.info
index c1868098..0a4ad477 100644
--- a/contrib/search_api_views/search_api_views.info
+++ b/contrib/search_api_views/search_api_views.info
@@ -1,4 +1,3 @@
-
name = Search views
description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
dependencies[] = search_api
@@ -12,21 +11,25 @@ files[] = includes/handler_argument.inc
files[] = includes/handler_argument_fulltext.inc
files[] = includes/handler_argument_more_like_this.inc
files[] = includes/handler_argument_string.inc
+files[] = includes/handler_argument_date.inc
files[] = includes/handler_argument_taxonomy_term.inc
files[] = includes/handler_filter.inc
files[] = includes/handler_filter_boolean.inc
files[] = includes/handler_filter_date.inc
+files[] = includes/handler_filter_entity.inc
files[] = includes/handler_filter_fulltext.inc
files[] = includes/handler_filter_language.inc
files[] = includes/handler_filter_options.inc
+files[] = includes/handler_filter_taxonomy_term.inc
files[] = includes/handler_filter_text.inc
+files[] = includes/handler_filter_user.inc
files[] = includes/handler_sort.inc
files[] = includes/plugin_cache.inc
files[] = includes/query.inc
-; Information added by drupal.org packaging script on 2013-09-01
-version = "7.x-1.8"
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
core = "7.x"
project = "search_api"
-datestamp = "1378025826"
+datestamp = "1387965506"
diff --git a/contrib/search_api_views/search_api_views.install b/contrib/search_api_views/search_api_views.install
index 804d3079..03e610bf 100644
--- a/contrib/search_api_views/search_api_views.install
+++ b/contrib/search_api_views/search_api_views.install
@@ -1,4 +1,5 @@
$view) {
+ foreach (views_get_all_views() as $view) {
if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
continue;
}
@@ -32,7 +33,7 @@ function search_api_views_update_7101() {
$fields = $table_fields[$view->base_table];
$change |= _search_api_views_update_7101_helper($view->base_field, $fields);
if (!empty($view->display)) {
- foreach ($view->display as $key => &$display) {
+ foreach ($view->display as &$display) {
$options = &$display->display_options;
if (isset($options['style_options']['grouping'])) {
$change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
@@ -66,8 +67,15 @@ function search_api_views_update_7101() {
/**
* Helper function for replacing field identifiers.
*
- * @return
- * TRUE iff the identifier was changed.
+ * @param $field
+ * Some data to be searched for field names that should be altered. Passed by
+ * reference.
+ * @param array $fields
+ * An array mapping Search API field identifiers (as previously used by Views)
+ * to the new, sanitized Views field identifiers.
+ *
+ * @return bool
+ * TRUE if any data was changed, FALSE otherwise.
*/
function _search_api_views_update_7101_helper(&$field, array $fields) {
if (is_array($field)) {
diff --git a/contrib/search_api_views/search_api_views.module b/contrib/search_api_views/search_api_views.module
index f559714e..8a131c2a 100644
--- a/contrib/search_api_views/search_api_views.module
+++ b/contrib/search_api_views/search_api_views.module
@@ -1,5 +1,10 @@
entityWrapper(NULL, TRUE);
+ $wrapper = $index->entityWrapper(NULL, FALSE);
}
catch (EntityMetadataWrapperException $e) {
watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
@@ -43,6 +48,14 @@ function search_api_views_views_data() {
}
}
+ try {
+ $wrapper = $index->entityWrapper(NULL);
+ }
+ catch (EntityMetadataWrapperException $e) {
+ watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
+ continue;
+ }
+
// Add handlers for all indexed fields.
foreach ($index->getFields() as $key => $field) {
$tmp = $wrapper;
@@ -69,7 +82,7 @@ function search_api_views_views_data() {
if ($group) {
// @todo Entity type label instead of $group?
$table[$id]['group'] = $group;
- $name = t('@field (indexed)', array('@field' => $name));
+ $name = t('!field (indexed)', array('!field' => $name));
}
$table[$id]['title'] = $name;
$table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
@@ -145,8 +158,18 @@ function search_api_views_views_data() {
}
/**
- * Helper function that returns an array of handler definitions to add to a
- * views field definition.
+ * Adds handler definitions for a field to a Views data table definition.
+ *
+ * Helper method for search_api_views_views_data().
+ *
+ * @param $id
+ * The internal identifier of the field.
+ * @param array $field
+ * Information about the field.
+ * @param EntityMetadataWrapper $wrapper
+ * A wrapper providing further metadata about the field.
+ * @param array $table
+ * The existing Views data table definition, as a reference.
*/
function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) {
$type = $field['type'];
@@ -170,9 +193,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
return;
}
- if ($options = $wrapper->optionsList('view')) {
+ $info = $wrapper->info();
+ if (isset($info['options list']) && is_callable($info['options list'])) {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
- $table[$id]['filter']['options'] = $options;
$table[$id]['filter']['multi-valued'] = search_api_is_list_type($type);
}
elseif ($inner_type == 'boolean') {
@@ -181,6 +204,27 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
elseif ($inner_type == 'date') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
}
+ elseif (isset($field['entity_type']) && $field['entity_type'] === 'user') {
+ $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterUser';
+ }
+ elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
+ $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm';
+ $info = $wrapper->info();
+ $field_info = field_info_field($info['name']);
+ // For the "Parent terms" and "All parent terms" properties, we can
+ // extrapolate the vocabulary from the parent in the selector. (E.g.,
+ // for "field_tags:parent" we can use the information of "field_tags".)
+ // Otherwise, we can't include any vocabulary information.
+ if (!$field_info && ($info['name'] == 'parent' || $info['name'] == 'parents_all')) {
+ if (!empty($table[$id]['real field'])) {
+ $parts = explode(':', $table[$id]['real field']);
+ $field_info = field_info_field($parts[count($parts) - 2]);
+ }
+ }
+ if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
+ $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary'];
+ }
+ }
else {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
}
@@ -188,6 +232,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
if ($inner_type == 'string' || $inner_type == 'uri') {
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentString';
}
+ elseif ($inner_type == 'date') {
+ $table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentDate';
+ }
else {
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument';
}
diff --git a/disabled.png b/disabled.png
deleted file mode 100644
index 22477650..00000000
Binary files a/disabled.png and /dev/null differ
diff --git a/enabled.png b/enabled.png
deleted file mode 100644
index 95f8730e..00000000
Binary files a/enabled.png and /dev/null differ
diff --git a/includes/callback.inc b/includes/callback.inc
index c05260e9..ea161fbd 100644
--- a/includes/callback.inc
+++ b/includes/callback.inc
@@ -26,7 +26,7 @@ interface SearchApiAlterCallbackInterface {
/**
* Check whether this data-alter callback is applicable for a certain index.
*
- * This can be used for hiding the callback on the index's "Workflow" tab. To
+ * This can be used for hiding the callback on the index's "Filters" tab. To
* avoid confusion, you should only use criteria that are immutable, such as
* the index's entity type. Also, since this is only used for UI purposes, you
* should not completely rely on this to ensure certain index configurations
diff --git a/includes/callback_add_aggregation.inc b/includes/callback_add_aggregation.inc
index 15246863..31566c70 100644
--- a/includes/callback_add_aggregation.inc
+++ b/includes/callback_add_aggregation.inc
@@ -1,5 +1,10 @@
index->getFields(FALSE);
$field_options = array();
foreach ($fields as $name => $field) {
- $field_options[$name] = $field['name'];
+ $field_options[$name] = check_plain($field['name']);
+ $field_properties[$name] = array(
+ '#attributes' => array('title' => $name),
+ '#description' => check_plain($field['description']),
+ );
}
$additional = empty($this->options['fields']) ? array() : $this->options['fields'];
@@ -63,14 +72,14 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
foreach (array_keys($types) as $type) {
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
}
- $form['fields'][$name]['fields'] = array(
+ $form['fields'][$name]['fields'] = array_merge($field_properties, array(
'#type' => 'checkboxes',
'#title' => t('Contained fields'),
'#options' => $field_options,
'#default_value' => drupal_map_assoc($field['fields']),
'#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')),
'#required' => TRUE,
- );
+ ));
$form['fields'][$name]['actions'] = array(
'#type' => 'actions',
'remove' => array(
diff --git a/includes/callback_add_hierarchy.inc b/includes/callback_add_hierarchy.inc
index c21792ea..d69badb6 100644
--- a/includes/callback_add_hierarchy.inc
+++ b/includes/callback_add_hierarchy.inc
@@ -1,7 +1,12 @@
getHierarchicalFields();
}
/**
- * Display a form for configuring this callback.
- *
- * @return array
- * A form array for configuring this callback, or FALSE if no configuration
- * is possible.
+ * {@inheritdoc}
*/
public function configurationForm() {
$options = $this->getHierarchicalFields();
@@ -54,19 +51,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}
/**
- * Submit callback for the form returned by configurationForm().
- *
- * This method should both return the new options and set them internally.
- *
- * @param array $form
- * The form returned by configurationForm().
- * @param array $values
- * The part of the $form_state['values'] array corresponding to this form.
- * @param array $form_state
- * The complete form state.
- *
- * @return array
- * The new options array for this callback.
+ * {@inheritdoc}
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
// Change the saved type of fields in the index, if necessary.
@@ -74,7 +59,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
$fields = &$this->index->options['fields'];
$previous = drupal_map_assoc($this->options['fields']);
foreach ($values['fields'] as $field) {
- list($key, $prop) = explode(':', $field);
+ list($key) = explode(':', $field);
if (empty($previous[$field]) && isset($fields[$key]['type'])) {
$fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>';
$change = TRUE;
@@ -82,7 +67,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}
$new = drupal_map_assoc($values['fields']);
foreach ($previous as $field) {
- list($key, $prop) = explode(':', $field);
+ list($key) = explode(':', $field);
if (empty($new[$field]) && isset($fields[$key]['type'])) {
$w = $this->index->entityWrapper(NULL, FALSE);
if (isset($w->$key)) {
@@ -102,19 +87,11 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}
/**
- * Alter items before indexing.
- *
- * Items which are removed from the array won't be indexed, but will be marked
- * as clean for future indexing. This could for instance be used to implement
- * some sort of access filter for security purposes (e.g., don't index
- * unpublished nodes or comments).
- *
- * @param array $items
- * An array of items to be altered, keyed by item IDs.
+ * {@inheritdoc}
*/
public function alterItems(array &$items) {
if (empty($this->options['fields'])) {
- return array();
+ return;
}
foreach ($items as $item) {
$wrapper = $this->index->entityWrapper($item, FALSE);
@@ -137,16 +114,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}
/**
- * Declare the properties that are (or can be) added to items with this
- * callback. If a property with this name already exists for an entity it
- * will be overridden, so keep a clear namespace by prefixing the properties
- * with the module name if this is not desired.
- *
- * @see hook_entity_property_info()
- *
- * @return array
- * Information about all additional properties, as specified by
- * hook_entity_property_info() (only the inner "properties" array).
+ * {@inheritdoc}
*/
public function propertyInfo() {
if (empty($this->options['fields'])) {
@@ -188,7 +156,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}
/**
- * Helper method for finding all hierarchical fields of an index's type.
+ * Finds all hierarchical fields for the current index.
*
* @return array
* An array containing all hierarchical fields of the index, structured as
diff --git a/includes/callback_add_url.inc b/includes/callback_add_url.inc
index 097fd41e..cc76b33a 100644
--- a/includes/callback_add_url.inc
+++ b/includes/callback_add_url.inc
@@ -1,12 +1,17 @@
&$item) {
+ foreach ($items as &$item) {
$url = $this->index->datasource()->getItemUrl($item);
if (!$url) {
$item->search_api_url = NULL;
diff --git a/includes/callback_add_viewed_entity.inc b/includes/callback_add_viewed_entity.inc
index 2adcad0e..06b05c38 100644
--- a/includes/callback_add_viewed_entity.inc
+++ b/includes/callback_add_viewed_entity.inc
@@ -1,5 +1,10 @@
index->getEntityType();
$mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
- foreach ($items as $id => &$item) {
+ foreach ($items as &$item) {
// Since we can't really know what happens in entity_view() and render(),
// we use try/catch. This will at least prevent some errors, even though
// it's no protection against fatal errors and the like.
diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc
index e1072b6b..576fa608 100644
--- a/includes/callback_bundle_filter.inc
+++ b/includes/callback_bundle_filter.inc
@@ -1,15 +1,25 @@
getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info);
}
+ /**
+ * {@inheritdoc}
+ */
public function alterItems(array &$items) {
$info = entity_get_info($this->index->getEntityType());
if (self::hasBundles($info) && isset($this->options['bundles'])) {
@@ -24,6 +34,9 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
}
}
+ /**
+ * {@inheritdoc}
+ */
public function configurationForm() {
$info = entity_get_info($this->index->getEntityType());
if (self::hasBundles($info)) {
@@ -62,8 +75,13 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
}
/**
- * Helper method for figuring out if the entities with the given entity info
- * can be filtered by bundle.
+ * Determines whether a certain entity type has any bundles.
+ *
+ * @param array $entity_info
+ * The entity type's entity_get_info() array.
+ *
+ * @return bool
+ * TRUE if the entity type has bundles, FASLE otherwise.
*/
protected static function hasBundles(array $entity_info) {
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
diff --git a/includes/callback_comment_access.inc b/includes/callback_comment_access.inc
new file mode 100644
index 00000000..e6273530
--- /dev/null
+++ b/includes/callback_comment_access.inc
@@ -0,0 +1,46 @@
+getEntityType() === 'comment';
+ }
+
+ /**
+ * Overrides SearchApiAlterNodeAccess::getNode().
+ *
+ * Returns the comment's node, instead of the item (i.e., the comment) itself.
+ */
+ protected function getNode($item) {
+ return node_load($item->nid);
+ }
+
+ /**
+ * Overrides SearchApiAlterNodeAccess::configurationFormSubmit().
+ *
+ * Doesn't index the comment's "Author".
+ */
+ public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+ $old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_comment_access']['status']);
+ $new_status = !empty($form_state['values']['callbacks']['search_api_alter_comment_access']['status']);
+
+ if (!$old_status && $new_status) {
+ $form_state['index']->options['fields']['status']['type'] = 'boolean';
+ }
+
+ return parent::configurationFormSubmit($form, $values, $form_state);
+ }
+
+}
diff --git a/includes/callback_language_control.inc b/includes/callback_language_control.inc
index 0ac481f7..233852c1 100644
--- a/includes/callback_language_control.inc
+++ b/includes/callback_language_control.inc
@@ -1,5 +1,10 @@
&$item) {
diff --git a/includes/callback_node_access.inc b/includes/callback_node_access.inc
index 5acc76c1..8bfab494 100644
--- a/includes/callback_node_access.inc
+++ b/includes/callback_node_access.inc
@@ -10,15 +10,9 @@
class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
/**
- * Check whether this data-alter callback is applicable for a certain index.
+ * Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* Returns TRUE only for indexes on nodes.
- *
- * @param SearchApiIndex $index
- * The index to check for.
- *
- * @return boolean
- * TRUE if the callback can run on the given index; FALSE otherwise.
*/
public function supportsIndex(SearchApiIndex $index) {
// Currently only node access is supported.
@@ -26,15 +20,9 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
}
/**
- * Declare the properties that are (or can be) added to items with this callback.
+ * Overrides SearchApiAbstractAlterCallback::propertyInfo().
*
* Adds the "search_api_access_node" property.
- *
- * @see hook_entity_property_info()
- *
- * @return array
- * Information about all additional properties, as specified by
- * hook_entity_property_info() (only the inner "properties" array).
*/
public function propertyInfo() {
return array(
@@ -47,15 +35,7 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
}
/**
- * Alter items before indexing.
- *
- * Items which are removed from the array won't be indexed, but will be marked
- * as clean for future indexing. This could for instance be used to implement
- * some sort of access filter for security purposes (e.g., don't index
- * unpublished nodes or comments).
- *
- * @param array $items
- * An array of items to be altered, keyed by item IDs.
+ * {@inheritdoc}
*/
public function alterItems(array &$items) {
static $account;
@@ -65,30 +45,39 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
$account = drupal_anonymous_user();
}
- foreach ($items as $nid => &$item) {
+ foreach ($items as $id => $item) {
+ $node = $this->getNode($item);
// Check whether all users have access to the node.
- if (!node_access('view', $item, $account)) {
+ if (!node_access('view', $node, $account)) {
// Get node access grants.
- $result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $item->nid));
+ $result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->nid));
- // Store all grants together with it's realms in the item.
+ // Store all grants together with their realms in the item.
foreach ($result as $grant) {
- if (!isset($items[$nid]->search_api_access_node)) {
- $items[$nid]->search_api_access_node = array();
- }
- $items[$nid]->search_api_access_node[] = "node_access_$grant->realm:$grant->gid";
+ $items[$id]->search_api_access_node[] = "node_access_{$grant->realm}:{$grant->gid}";
}
}
else {
// Add the generic view grant if we are not using node access or the
// node is viewable by anonymous users.
- $items[$nid]->search_api_access_node = array('node_access__all');
+ $items[$id]->search_api_access_node = array('node_access__all');
}
}
}
/**
- * Submit callback for the configuration form.
+ * Retrieves the node related to a search item.
+ *
+ * In the default implementation for nodes, the item is already the node.
+ * Subclasses may override this to easily provide node access checks for
+ * items related to nodes.
+ */
+ protected function getNode($item) {
+ return $item;
+ }
+
+ /**
+ * Overrides SearchApiAbstractAlterCallback::configurationFormSubmit().
*
* If the data alteration is being enabled, set "Published" and "Author" to
* "indexed", because both are needed for the node access filter.
diff --git a/includes/datasource.inc b/includes/datasource.inc
index ba0d2ba8..cf0507fd 100644
--- a/includes/datasource.inc
+++ b/includes/datasource.inc
@@ -18,46 +18,49 @@
* aware that indexes' numerical IDs can change due to feature reverts. It is
* therefore recommended to use search_api_index_update_datasource(), or similar
* code, in a hook_search_api_index_update() implementation.
- *
- * All methods of the data source may throw exceptions of type
- * SearchApiDataSourceException if any exception or error state is encountered.
*/
interface SearchApiDataSourceControllerInterface {
/**
- * Constructor for a data source controller.
+ * Constructs a new data source controller.
*
- * @param $type
+ * @param string $type
* The item type for which this controller is created.
*/
public function __construct($type);
/**
- * Return information on the ID field for this controller's type.
+ * Returns information on the ID field for this controller's type.
*
* @return array
* An associative array containing the following keys:
* - key: The property key for the ID field, as used in the item wrapper.
* - type: The type of the ID field. Has to be one of the types from
* search_api_field_types(). List types ("list<*>") are not allowed.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getIdFieldInfo();
/**
- * Load items of the type of this data source controller.
+ * Loads items of the type of this data source controller.
*
* @param array $ids
* The IDs of the items to laod.
*
* @return array
* The loaded items, keyed by ID.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function loadItems(array $ids);
/**
- * Get a metadata wrapper for the item type of this data source controller.
+ * Creates a metadata wrapper for this datasource controller's type.
*
- * @param $item
+ * @param mixed $item
* Unless NULL, an item of the item type for this controller to be wrapped.
* @param array $info
* Optionally, additional information that should be used for creating the
@@ -67,151 +70,170 @@ interface SearchApiDataSourceControllerInterface {
* A wrapper for the item type of this data source controller, according to
* the info array, and optionally loaded with the given data.
*
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
+ *
* @see entity_metadata_wrapper()
*/
public function getMetadataWrapper($item = NULL, array $info = array());
/**
- * Get the unique ID of an item.
+ * Retrieves the unique ID of an item.
*
- * @param $item
+ * @param mixed $item
* An item of this controller's type.
*
- * @return
+ * @return mixed
* Either the unique ID of the item, or NULL if none is available.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getItemId($item);
/**
- * Get a human-readable label for an item.
+ * Retrieves a human-readable label for an item.
*
- * @param $item
+ * @param mixed $item
* An item of this controller's type.
*
- * @return
+ * @return string|null
* Either a human-readable label for the item, or NULL if none is available.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getItemLabel($item);
/**
- * Get a URL at which the item can be viewed on the web.
+ * Retrieves a URL at which the item can be viewed on the web.
*
- * @param $item
+ * @param mixed $item
* An item of this controller's type.
*
- * @return
+ * @return array|null
* Either an array containing the 'path' and 'options' keys used to build
* the URL of the item, and matching the signature of url(), or NULL if the
* item has no URL of its own.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getItemUrl($item);
/**
- * Initialize tracking of the index status of items for the given indexes.
+ * Initializes tracking of the index status of items for the given indexes.
*
* All currently known items of this data source's type should be inserted
* into the tracking table for the given indexes, with status "changed". If
* items were already present, these should also be set to "changed" and not
* be inserted again.
*
- * @param array $indexes
+ * @param SearchApiIndex[] $indexes
* The SearchApiIndex objects for which item tracking should be initialized.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function startTracking(array $indexes);
/**
- * Stop tracking of the index status of items for the given indexes.
+ * Stops tracking of the index status of items for the given indexes.
*
* The tracking tables of the given indexes should be completely cleared.
*
- * @param array $indexes
+ * @param SearchApiIndex[] $indexes
* The SearchApiIndex objects for which item tracking should be stopped.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function stopTracking(array $indexes);
/**
- * Start tracking the index status for the given items on the given indexes.
+ * Starts tracking the index status for the given items on the given indexes.
*
* @param array $item_ids
* The IDs of new items to track.
- * @param array $indexes
+ * @param SearchApiIndex[] $indexes
* The indexes for which items should be tracked.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function trackItemInsert(array $item_ids, array $indexes);
/**
- * Set the tracking status of the given items to "changed"/"dirty".
+ * Sets the tracking status of the given items to "changed"/"dirty".
*
* Unless $dequeue is set to TRUE, this operation is ignored for items whose
* status is not "indexed".
*
- * @param $item_ids
+ * @param array|false $item_ids
* Either an array with the IDs of the changed items. Or FALSE to mark all
* items as changed for the given indexes.
- * @param array $indexes
+ * @param SearchApiIndex[] $indexes
* The indexes for which the change should be tracked.
- * @param $dequeue
- * If set to TRUE, also change the status of queued items.
+ * @param bool $dequeue
+ * (deprecated) If set to TRUE, also change the status of queued items.
+ * The concept of queued items will be removed in the Drupal 8 version of
+ * this module.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE);
/**
- * Set the tracking status of the given items to "queued".
+ * Sets the tracking status of the given items to "queued".
*
* Queued items are not marked as "dirty" even when they are changed, and they
* are not returned by the getChangedItems() method.
*
- * @param $item_ids
+ * @param array|false $item_ids
* Either an array with the IDs of the queued items. Or FALSE to mark all
* items as queued for the given indexes.
* @param SearchApiIndex $index
* The index for which the items were queued.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
+ *
+ * @deprecated
+ * As of Search API 1.10, the cron queue is not used for indexing anymore,
+ * therefore this method has become useless. It will be removed in the
+ * Drupal 8 version of this module.
*/
public function trackItemQueued($item_ids, SearchApiIndex $index);
/**
- * Set the tracking status of the given items to "indexed".
+ * Sets the tracking status of the given items to "indexed".
*
* @param array $item_ids
* The IDs of the indexed items.
- * @param SearchApiIndex $indexes
+ * @param SearchApiIndex $index
* The index on which the items were indexed.
*
* @throws SearchApiDataSourceException
- * If the index doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function trackItemIndexed(array $item_ids, SearchApiIndex $index);
/**
- * Stop tracking the index status for the given items on the given indexes.
+ * Stops tracking the index status for the given items on the given indexes.
*
* @param array $item_ids
* The IDs of the removed items.
- * @param array $indexes
+ * @param SearchApiIndex[] $indexes
* The indexes for which the deletions should be tracked.
*
* @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function trackItemDelete(array $item_ids, array $indexes);
/**
- * Get a list of items that need to be indexed.
+ * Retrieves a list of items that need to be indexed.
*
* If possible, completely unindexed items should be returned before items
* that were indexed but later changed. Also, items that were changed longer
@@ -219,16 +241,19 @@ interface SearchApiDataSourceControllerInterface {
*
* @param SearchApiIndex $index
* The index for which changed items should be returned.
- * @param $limit
+ * @param int $limit
* The maximum number of items to return. Negative values mean "unlimited".
*
* @return array
* The IDs of items that need to be indexed for the given index.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getChangedItems(SearchApiIndex $index, $limit = -1);
/**
- * Get information on how many items have been indexed for a certain index.
+ * Retrieves information on how many items have been indexed for a certain index.
*
* @param SearchApiIndex $index
* The index whose index status should be returned.
@@ -240,22 +265,26 @@ interface SearchApiDataSourceControllerInterface {
* index.
*
* @throws SearchApiDataSourceException
- * If the index doesn't use the same item type as this controller.
+ * If any error state was encountered.
*/
public function getIndexStatus(SearchApiIndex $index);
/**
- * Get the entity type of items from this datasource.
+ * Retrieves the entity type of items from this datasource.
*
* @return string|null
* An entity type string if the items provided by this datasource are
* entities; NULL otherwise.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
public function getEntityType();
+
}
/**
- * Default base class for the SearchApiDataSourceControllerInterface.
+ * Provides a default base class for datasource controllers.
*
* Contains default implementations for a number of methods which will be
* similar for most data sources. Concrete data sources can decide to extend
@@ -330,10 +359,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
protected $changedColumn = 'changed';
/**
- * Constructor for a data source controller.
- *
- * @param $type
- * The item type for which this controller is created.
+ * {@inheritdoc}
*/
public function __construct($type) {
$this->type = $type;
@@ -345,30 +371,14 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get the entity type of items from this datasource.
- *
- * @return string|null
- * An entity type string if the items provided by this datasource are
- * entities; NULL otherwise.
+ * {@inheritdoc}
*/
public function getEntityType() {
return $this->entityType;
}
/**
- * Get a metadata wrapper for the item type of this data source controller.
- *
- * @param $item
- * Unless NULL, an item of the item type for this controller to be wrapped.
- * @param array $info
- * Optionally, additional information that should be used for creating the
- * wrapper. Uses the same format as entity_metadata_wrapper().
- *
- * @return EntityMetadataWrapper
- * A wrapper for the item type of this data source controller, according to
- * the info array, and optionally loaded with the given data.
- *
- * @see entity_metadata_wrapper()
+ * {@inheritdoc}
*/
public function getMetadataWrapper($item = NULL, array $info = array()) {
$info += $this->getPropertyInfo();
@@ -376,7 +386,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get the property info for this item type.
+ * Retrieves the property info for this item type.
*
* This is a helper method for getMetadataWrapper() that can be used by
* subclasses to specify the property information to use when creating a
@@ -384,7 +394,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
*
* The data structure uses largely the format specified in
* hook_entity_property_info(). However, the first level of keys (containing
- * the entity types) is omitted, and the "property" key is called
+ * the entity types) is omitted, and the "properties" key is called
* "property info" instead. So, an example return value would look like this:
*
* @code
@@ -413,6 +423,9 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
* @return array
* Property information as specified by entity_metadata_wrapper().
*
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
+ *
* @see getMetadataWrapper()
* @see hook_entity_property_info()
*/
@@ -425,13 +438,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get the unique ID of an item.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either the unique ID of the item, or NULL if none is available.
+ * {@inheritdoc}
*/
public function getItemId($item) {
$id_info = $this->getIdFieldInfo();
@@ -445,13 +452,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get a human-readable label for an item.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either a human-readable label for the item, or NULL if none is available.
+ * {@inheritdoc}
*/
public function getItemLabel($item) {
$label = $this->getMetadataWrapper($item)->label();
@@ -459,33 +460,14 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get a URL at which the item can be viewed on the web.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either an array containing the 'path' and 'options' keys used to build
- * the URL of the item, and matching the signature of url(), or NULL if the
- * item has no URL of its own.
+ * {@inheritdoc}
*/
public function getItemUrl($item) {
return NULL;
}
/**
- * Initialize tracking of the index status of items for the given indexes.
- *
- * All currently known items of this data source's type should be inserted
- * into the tracking table for the given indexes, with status "changed". If
- * items were already present, these should also be set to "changed" and not
- * be inserted again.
- *
- * @param array $indexes
- * The SearchApiIndex objects for which item tracking should be initialized.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function startTracking(array $indexes) {
if (!$this->table) {
@@ -499,27 +481,23 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Helper method that can be used by subclasses instead of implementing startTracking().
- *
* Returns the IDs of all items that are known for this controller's type.
*
+ * Helper method that can be used by subclasses instead of implementing
+ * startTracking().
+ *
* @return array
* An array containing all item IDs for this type.
+ *
+ * @throws SearchApiDataSourceException
+ * If any error state was encountered.
*/
protected function getAllItemIds() {
throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type)));
}
/**
- * Stop tracking of the index status of items for the given indexes.
- *
- * The tracking tables of the given indexes should be completely cleared.
- *
- * @param array $indexes
- * The SearchApiIndex objects for which item tracking should be stopped.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function stopTracking(array $indexes) {
if (!$this->table) {
@@ -529,22 +507,14 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
// will mostly be called with only one index.
foreach ($indexes as $index) {
$this->checkIndex($index);
- $query = db_delete($this->table)
+ db_delete($this->table)
->condition($this->indexIdColumn, $index->id)
->execute();
}
}
/**
- * Start tracking the index status for the given items on the given indexes.
- *
- * @param array $item_ids
- * The IDs of new items to track.
- * @param array $indexes
- * The indexes for which items should be tracked.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function trackItemInsert(array $item_ids, array $indexes) {
if (!$this->table) {
@@ -571,21 +541,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Set the tracking status of the given items to "changed"/"dirty".
- *
- * Unless $dequeue is set to TRUE, this operation is ignored for items whose
- * status is not "indexed".
- *
- * @param $item_ids
- * Either an array with the IDs of the changed items. Or FALSE to mark all
- * items as changed for the given indexes.
- * @param array $indexes
- * The indexes for which the change should be tracked.
- * @param $dequeue
- * If set to TRUE, also change the status of queued items.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
if (!$this->table) {
@@ -609,21 +565,10 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Set the tracking status of the given items to "queued".
- *
- * Queued items are not marked as "dirty" even when they are changed, and they
- * are not returned by the getChangedItems() method.
- *
- * @param $item_ids
- * Either an array with the IDs of the queued items. Or FALSE to mark all
- * items as queued for the given indexes.
- * @param SearchApiIndex $index
- * The index for which the items were queued.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function trackItemQueued($item_ids, SearchApiIndex $index) {
+ $this->checkIndex($index);
if (!$this->table) {
return;
}
@@ -639,15 +584,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Set the tracking status of the given items to "indexed".
- *
- * @param array $item_ids
- * The IDs of the indexed items.
- * @param SearchApiIndex $indexes
- * The index on which the items were indexed.
- *
- * @throws SearchApiDataSourceException
- * If the index doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
if (!$this->table) {
@@ -664,15 +601,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Stop tracking the index status for the given items on the given indexes.
- *
- * @param array $item_ids
- * The IDs of the removed items.
- * @param array $indexes
- * The indexes for which the deletions should be tracked.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function trackItemDelete(array $item_ids, array $indexes) {
if (!$this->table) {
@@ -690,19 +619,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get a list of items that need to be indexed.
- *
- * If possible, completely unindexed items should be returned before items
- * that were indexed but later changed. Also, items that were changed longer
- * ago should be favored.
- *
- * @param SearchApiIndex $index
- * The index for which changed items should be returned.
- * @param $limit
- * The maximum number of items to return. Negative values mean "unlimited".
- *
- * @return array
- * The IDs of items that need to be indexed for the given index.
+ * {@inheritdoc}
*/
public function getChangedItems(SearchApiIndex $index, $limit = -1) {
if ($limit == 0) {
@@ -721,16 +638,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Get information on how many items have been indexed for a certain index.
- *
- * @param SearchApiIndex $index
- * The index whose index status should be returned.
- *
- * @return array
- * An associative array containing two keys (in this order):
- * - indexed: The number of items already indexed in their latest version.
- * - total: The total number of items that have to be indexed for this
- * index.
+ * {@inheritdoc}
*/
public function getIndexStatus(SearchApiIndex $index) {
if (!$this->table) {
@@ -752,13 +660,16 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
/**
- * Helper method for ensuring that an index uses the same item type as this controller.
+ * Checks whether the given index is valid for this datasource controller.
+ *
+ * Helper method used by various methods in this class. By default only checks
+ * whether the types match.
*
* @param SearchApiIndex $index
* The index to check.
*
* @throws SearchApiDataSourceException
- * If the index doesn't use the same type as this controller.
+ * If the index doesn't fit to this datasource controller.
*/
protected function checkIndex(SearchApiIndex $index) {
if ($index->item_type != $this->type) {
diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc
index 6f15ec54..836f57e3 100644
--- a/includes/datasource_entity.inc
+++ b/includes/datasource_entity.inc
@@ -6,18 +6,12 @@
*/
/**
- * Data source for all entities known to the Entity API.
+ * Represents a datasource for all entities known to the Entity API.
*/
class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
/**
- * Return information on the ID field for this controller's type.
- *
- * @return array
- * An associative array containing the following keys:
- * - key: The property key for the ID field, as used in the item wrapper.
- * - type: The type of the ID field. Has to be one of the types from
- * search_api_field_types(). List types ("list<*>") are not allowed.
+ * {@inheritdoc}
*/
public function getIdFieldInfo() {
$info = entity_get_info($this->entityType);
@@ -43,13 +37,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Load items of the type of this data source controller.
- *
- * @param array $ids
- * The IDs of the items to laod.
- *
- * @return array
- * The loaded items, keyed by ID.
+ * {@inheritdoc}
*/
public function loadItems(array $ids) {
$items = entity_load($this->entityType, $ids);
@@ -65,32 +53,14 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Get a metadata wrapper for the item type of this data source controller.
- *
- * @param $item
- * Unless NULL, an item of the item type for this controller to be wrapped.
- * @param array $info
- * Optionally, additional information that should be used for creating the
- * wrapper. Uses the same format as entity_metadata_wrapper().
- *
- * @return EntityMetadataWrapper
- * A wrapper for the item type of this data source controller, according to
- * the info array, and optionally loaded with the given data.
- *
- * @see entity_metadata_wrapper()
+ * {@inheritdoc}
*/
public function getMetadataWrapper($item = NULL, array $info = array()) {
return entity_metadata_wrapper($this->entityType, $item, $info);
}
/**
- * Get the unique ID of an item.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either the unique ID of the item, or NULL if none is available.
+ * {@inheritdoc}
*/
public function getItemId($item) {
$id = entity_id($this->entityType, $item);
@@ -98,13 +68,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Get a human-readable label for an item.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either a human-readable label for the item, or NULL if none is available.
+ * {@inheritdoc}
*/
public function getItemLabel($item) {
$label = entity_label($this->entityType, $item);
@@ -112,15 +76,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Get a URL at which the item can be viewed on the web.
- *
- * @param $item
- * An item of this controller's type.
- *
- * @return
- * Either an array containing the 'path' and 'options' keys used to build
- * the URL of the item, and matching the signature of url(), or NULL if the
- * item has no URL of its own.
+ * {@inheritdoc}
*/
public function getItemUrl($item) {
if ($this->entityType == 'file') {
@@ -137,18 +93,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Initialize tracking of the index status of items for the given indexes.
- *
- * All currently known items of this data source's type should be inserted
- * into the tracking table for the given indexes, with status "changed". If
- * items were already present, these should also be set to "changed" and not
- * be inserted again.
- *
- * @param array $indexes
- * The SearchApiIndex objects for which item tracking should be initialized.
- *
- * @throws SearchApiDataSourceException
- * If any of the indexes doesn't use the same item type as this controller.
+ * {@inheritdoc}
*/
public function startTracking(array $indexes) {
if (!$this->table) {
@@ -190,14 +135,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
/**
- * Helper method that can be used by subclasses instead of implementing startTracking().
- *
- * Returns the IDs of all items that are known for this controller's type.
- *
- * Will be used when the entity type doesn't specify a "base table".
- *
- * @return array
- * An array containing all item IDs for this type.
+ * {@inheritdoc}
*/
protected function getAllItemIds() {
return array_keys(entity_load($this->entityType));
diff --git a/includes/exception.inc b/includes/exception.inc
index 4e7a0c83..f69ddd59 100644
--- a/includes/exception.inc
+++ b/includes/exception.inc
@@ -1,5 +1,10 @@
enabled) {
$this->queueItems();
}
- $server = $this->server();
- if ($server) {
+ if ($server = $this->server()) {
// Tell the server about the new index.
- if ($server->enabled) {
- $server->addIndex($this);
- }
- else {
- $tasks = variable_get('search_api_tasks', array());
- // When we add or remove an index, we can ignore all other tasks.
- $tasks[$server->machine_name][$this->machine_name] = array('add');
- variable_set('search_api_tasks', $tasks);
- }
+ $server->addIndex($this);
}
}
@@ -198,18 +194,7 @@ class SearchApiIndex extends Entity {
*/
public function postDelete() {
if ($server = $this->server()) {
- if ($server->enabled) {
- $server->removeIndex($this);
- }
- // Once the index is deleted, servers won't be able to tell whether it was
- // read-only. Therefore, we prefer to err on the safe side and don't call
- // the server method at all if the index is read-only and the server
- // currently disabled.
- elseif (empty($this->read_only)) {
- $tasks = variable_get('search_api_tasks', array());
- $tasks[$server->machine_name][$this->machine_name] = array('remove');
- variable_set('search_api_tasks', $tasks);
- }
+ $server->removeIndex($this);
}
// Stop tracking entities for indexing.
@@ -230,14 +215,14 @@ class SearchApiIndex extends Entity {
*/
public function dequeueItems() {
$this->datasource()->stopTracking(array($this));
- _search_api_empty_cron_queue($this);
}
/**
- * Saves this index to the database, either creating a new record or updating
- * an existing one.
+ * Saves this index to the database.
*
- * @return
+ * Either creates a new record or updates the existing one with the same ID.
+ *
+ * @return int|false
* Failure to save the index will return FALSE. Otherwise, SAVED_NEW or
* SAVED_UPDATED is returned depending on the operation performed. $this->id
* will be set if a new index was inserted.
@@ -253,6 +238,7 @@ class SearchApiIndex extends Entity {
// This will also throw an exception if the server doesn't exist – which is good.
elseif (!$this->server(TRUE)->enabled) {
$this->enabled = FALSE;
+ $this->server = NULL;
}
return parent::save();
@@ -267,7 +253,7 @@ class SearchApiIndex extends Entity {
* @param array $fields
* The new field values.
*
- * @return
+ * @return int|false
* SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
* the specified values.
*/
@@ -296,7 +282,7 @@ class SearchApiIndex extends Entity {
/**
* Schedules this search index for re-indexing.
*
- * @return
+ * @return bool
* TRUE on success, FALSE on failure.
*/
public function reindex() {
@@ -311,7 +297,7 @@ class SearchApiIndex extends Entity {
/**
* Clears this search index and schedules all of its items for re-indexing.
*
- * @return
+ * @return bool
* TRUE on success, FALSE on failure.
*/
public function clear() {
@@ -319,20 +305,7 @@ class SearchApiIndex extends Entity {
return TRUE;
}
- $server = $this->server();
- if ($server->enabled) {
- $server->deleteItems('all', $this);
- }
- else {
- $tasks = variable_get('search_api_tasks', array());
- // If the index was cleared or newly added since the server was last enabled, we don't need to do anything.
- if (!isset($tasks[$server->machine_name][$this->machine_name])
- || (array_search('add', $tasks[$server->machine_name][$this->machine_name]) === FALSE
- && array_search('clear', $tasks[$server->machine_name][$this->machine_name]) === FALSE)) {
- $tasks[$server->machine_name][$this->machine_name][] = 'clear';
- variable_set('search_api_tasks', $tasks);
- }
- }
+ $this->server()->deleteItems('all', $this);
_search_api_index_reindex($this);
module_invoke_all('search_api_index_reindex', $this, TRUE);
diff --git a/includes/processor.inc b/includes/processor.inc
index 1774bf19..08fb02cf 100644
--- a/includes/processor.inc
+++ b/includes/processor.inc
@@ -1,5 +1,10 @@
array());
$fulltext_fields = $this->index->getFulltextFields();
// We only need detailed fields data if $load is TRUE.
@@ -198,6 +199,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
return $data;
}
$wrapper = $this->index->entityWrapper($result['entity'], FALSE);
+ $wrapper->language($language->language);
$extracted = search_api_extract_fields($wrapper, $needs_extraction);
foreach ($extracted as $field => $info) {
@@ -292,7 +294,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
// If the sum of all fragments is too short, we look for second occurrences.
$ranges = array();
$included = array();
- $foundkeys = array();
$length = 0;
$workkeys = $keys;
while ($length < $this->options['excerpt_length'] && count($workkeys)) {
@@ -394,8 +395,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
*/
protected function highlightField($text, array $keys) {
$replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
- $text = preg_replace('/' . self::$boundary . '(' . implode('|', $keys) . ')' . self::$boundary . '/iu', $replace, ' ' . $text);
- return substr($text, 1);
+ $keys = implode('|', array_map('preg_quote', $keys));
+ $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
+ return substr($text, 1, -1);
}
}
diff --git a/includes/processor_html_filter.inc b/includes/processor_html_filter.inc
index f636e2f2..41777bd2 100644
--- a/includes/processor_html_filter.inc
+++ b/includes/processor_html_filter.inc
@@ -1,5 +1,10 @@
array(
'#type' => 'textfield',
- '#title' => t('Stopwords file URI'),
- '#title' => t('Enter the URI of your stopwords.txt file'),
+ '#title' => t('Stopwords file'),
'#description' => t('This must be a stream-type description like public://stopwords/stopwords.txt or http://example.com/stopwords.txt or private://stopwords.txt.'),
),
'stopwords' => array(
@@ -43,13 +47,8 @@ class SearchApiStopWords extends SearchApiAbstractProcessor {
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
parent::configurationFormValidate($form, $values, $form_state);
- $stopwords = trim($values['stopwords']);
$uri = $values['file'];
- if (empty($stopwords) && empty($uri)) {
- $el = $form['file'];
- form_error($el, $el['#title'] . ': ' . t('At stopwords file or words are required.'));
- }
- if (!empty($uri) && !file_get_contents($uri)) {
+ if (!empty($uri) && !@file_get_contents($uri)) {
$el = $form['file'];
form_error($el, t('Stopwords file') . ': ' . t('The file %uri is not readable or does not exist.', array('%uri' => $uri)));
}
@@ -57,7 +56,7 @@ class SearchApiStopWords extends SearchApiAbstractProcessor {
public function process(&$value) {
$stopwords = $this->getStopWords();
- if (empty($stopwords) && !is_string($value)) {
+ if (empty($stopwords) || !is_string($value)) {
return;
}
$words = preg_split('/\s+/', $value);
@@ -105,4 +104,4 @@ class SearchApiStopWords extends SearchApiAbstractProcessor {
$this->stopwords = array_flip(array_merge($file_words, $form_words));
return $this->stopwords;
}
-}
\ No newline at end of file
+}
diff --git a/includes/processor_tokenizer.inc b/includes/processor_tokenizer.inc
index f3972266..14834df0 100644
--- a/includes/processor_tokenizer.inc
+++ b/includes/processor_tokenizer.inc
@@ -1,5 +1,10 @@
options['filter class'];
- return new $filter_class($conjunction);
+ return new $filter_class($conjunction, $tags);
}
/**
@@ -616,6 +641,9 @@ class SearchApiQuery implements SearchApiQueryInterface {
*
* @param array $languages
* The languages for which results should be returned.
+ *
+ * @throws SearchApiException
+ * If there was a logical error in the combination of filters and languages.
*/
protected function addLanguages(array $languages) {
if (array_search(LANGUAGE_NONE, $languages) === FALSE) {
@@ -776,6 +804,13 @@ class SearchApiQuery implements SearchApiQueryInterface {
}
}
+ /**
+ * Implements the magic __clone() method to clone the filter, too.
+ */
+ public function __clone() {
+ $this->filter = clone $this->filter;
+ }
+
}
/**
@@ -790,9 +825,13 @@ interface SearchApiQueryFilterInterface {
* Constructs a new filter that uses the specified conjunction.
*
* @param string $conjunction
- * The conjunction to use for this filter - either 'AND' or 'OR'.
+ * (optional) The conjunction to use for this filter - either 'AND' or 'OR'.
+ * @param array $tags
+ * (optional) An arbitrary set of tags. Can be used to identify this filter
+ * down the line if necessary. This is primarily used by the facet system
+ * to support OR facet queries.
*/
- public function __construct($conjunction = 'AND');
+ public function __construct($conjunction = 'AND', array $tags = array());
/**
* Sets this filter's conjunction.
@@ -856,6 +895,25 @@ interface SearchApiQueryFilterInterface {
*/
public function &getFilters();
+ /**
+ * Checks whether a certain tag was set on this filter.
+ *
+ * @param string $tag
+ * A tag to check for.
+ *
+ * @return bool
+ * TRUE if the tag was set for this filter, FALSE otherwise.
+ */
+ public function hasTag($tag);
+
+ /**
+ * Retrieves the tags set on this filter.
+ *
+ * @return array
+ * The tags associated with this filter, as both the array keys and values.
+ */
+ public function &getTags();
+
}
/**
@@ -883,9 +941,10 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
/**
* {@inheritdoc}
*/
- public function __construct($conjunction = 'AND') {
+ public function __construct($conjunction = 'AND', array $tags = array()) {
$this->setConjunction($conjunction);
$this->filters = array();
+ $this->tags = drupal_map_assoc($tags);
}
/**
@@ -926,4 +985,29 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
return $this->filters;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTag($tag) {
+ return isset($this->tags[$tag]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getTags() {
+ return $this->tags;
+ }
+
+ /**
+ * Implements the magic __clone() method to clone nested filters, too.
+ */
+ public function __clone() {
+ foreach ($this->filters as $i => $filter) {
+ if (is_object($filter)) {
+ $this->filters[$i] = clone $filter;
+ }
+ }
+ }
+
}
diff --git a/includes/server_entity.inc b/includes/server_entity.inc
index 0436171f..be2a568c 100644
--- a/includes/server_entity.inc
+++ b/includes/server_entity.inc
@@ -1,5 +1,10 @@
ensureProxy();
return $this->proxy->configurationForm($form, $form_state);
}
+ /**
+ * Validation callback for the form returned by configurationForm().
+ *
+ * @see SearchApiServiceInterface::configurationFormValidate()
+ */
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
$this->ensureProxy();
return $this->proxy->configurationFormValidate($form, $values, $form_state);
}
+ /**
+ * Submit callback for the form returned by configurationForm().
+ *
+ * @see SearchApiServiceInterface::configurationFormSubmit()
+ */
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
$this->ensureProxy();
return $this->proxy->configurationFormSubmit($form, $values, $form_state);
}
+ /**
+ * Determines whether this service class supports a given feature.
+ *
+ * @see SearchApiServiceInterface::supportsFeature()
+ */
public function supportsFeature($feature) {
$this->ensureProxy();
return $this->proxy->supportsFeature($feature);
}
+ /**
+ * Displays this server's settings.
+ *
+ * @see SearchApiServiceInterface::viewSettings()
+ */
public function viewSettings() {
$this->ensureProxy();
return $this->proxy->viewSettings();
}
+ /**
+ * Reacts to the server's creation.
+ *
+ * @see SearchApiServiceInterface::postCreate()
+ */
public function postCreate() {
$this->ensureProxy();
return $this->proxy->postCreate();
}
+ /**
+ * Notifies this server that its fields are about to be updated.
+ *
+ * @see SearchApiServiceInterface::postUpdate()
+ */
public function postUpdate() {
$this->ensureProxy();
return $this->proxy->postUpdate();
}
+ /**
+ * Notifies this server that it is about to be deleted from the database.
+ *
+ * @see SearchApiServiceInterface::preDelete()
+ */
public function preDelete() {
$this->ensureProxy();
return $this->proxy->preDelete();
}
+ /**
+ * Adds a new index to this server.
+ *
+ * If an exception in the service class implementation of this method occcurs,
+ * it will be caught and the operation saved as an pending server task.
+ *
+ * @see SearchApiServiceInterface::addIndex()
+ * @see search_api_server_tasks_add()
+ */
public function addIndex(SearchApiIndex $index) {
$this->ensureProxy();
- return $this->proxy->addIndex($index);
+ try {
+ $this->proxy->addIndex($index);
+ }
+ catch (SearchApiException $e) {
+ $vars = array(
+ '%server' => $this->name,
+ '%index' => $index->name,
+ );
+ watchdog_exception('search_api', $e, '%type while adding index %index to server %server: !message in %function (line %line of %file).', $vars);
+ search_api_server_tasks_add($this, __FUNCTION__, $index);
+ }
}
+ /**
+ * Notifies the server that the field settings for the index have changed.
+ *
+ * If the service class implementation of the method returns TRUE, this will
+ * automatically take care of marking the items on the index for re-indexing.
+ *
+ * If an exception in the service class implementation of this method occcurs,
+ * it will be caught and the operation saved as an pending server task.
+ *
+ * @see SearchApiServiceInterface::fieldsUpdated()
+ * @see search_api_server_tasks_add()
+ */
public function fieldsUpdated(SearchApiIndex $index) {
$this->ensureProxy();
- return $this->proxy->fieldsUpdated($index);
+ try {
+ if ($this->proxy->fieldsUpdated($index)) {
+ _search_api_index_reindex($index);
+ return TRUE;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = array(
+ '%server' => $this->name,
+ '%index' => $index->name,
+ );
+ watchdog_exception('search_api', $e, '%type while updating the fields of index %index on server %server: !message in %function (line %line of %file).', $vars);
+ search_api_server_tasks_add($this, __FUNCTION__, $index, isset($index->original) ? $index->original : NULL);
+ }
+ return FALSE;
}
+ /**
+ * Removes an index from this server.
+ *
+ * If an exception in the service class implementation of this method occcurs,
+ * it will be caught and the operation saved as an pending server task.
+ *
+ * @see SearchApiServiceInterface::removeIndex()
+ * @see search_api_server_tasks_add()
+ */
public function removeIndex($index) {
+ // When removing an index from a server, it doesn't make any sense anymore to
+ // delete items from it, or react to other changes.
+ search_api_server_tasks_delete(NULL, $this, $index);
+
$this->ensureProxy();
- return $this->proxy->removeIndex($index);
+ try {
+ $this->proxy->removeIndex($index);
+ }
+ catch (SearchApiException $e) {
+ $vars = array(
+ '%server' => $this->name,
+ '%index' => is_object($index) ? $index->name : $index,
+ );
+ watchdog_exception('search_api', $e, '%type while removing index %index from server %server: !message in %function (line %line of %file).', $vars);
+ search_api_server_tasks_add($this, __FUNCTION__, $index);
+ }
}
+ /**
+ * Indexes the specified items.
+ *
+ * @see SearchApiServiceInterface::indexItems()
+ */
public function indexItems(SearchApiIndex $index, array $items) {
$this->ensureProxy();
return $this->proxy->indexItems($index, $items);
}
+ /**
+ * Deletes indexed items from this server.
+ *
+ * If an exception in the service class implementation of this method occcurs,
+ * it will be caught and the operation saved as an pending server task.
+ *
+ * @see SearchApiServiceInterface::deleteItems()
+ * @see search_api_server_tasks_add()
+ */
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
$this->ensureProxy();
- return $this->proxy->deleteItems($ids, $index);
+ try {
+ $this->proxy->deleteItems($ids, $index);
+ }
+ catch (SearchApiException $e) {
+ $vars = array(
+ '%server' => $this->name,
+ );
+ watchdog_exception('search_api', $e, '%type while deleting items from server %server: !message in %function (line %line of %file).', $vars);
+ search_api_server_tasks_add($this, __FUNCTION__, $index, $ids);
+ }
}
+ /**
+ * Creates a query object for searching on an index lying on this server.
+ *
+ * @see SearchApiServiceInterface::query()
+ */
public function query(SearchApiIndex $index, $options = array()) {
$this->ensureProxy();
return $this->proxy->query($index, $options);
}
+ /**
+ * Executes a search on the server represented by this object.
+ *
+ * @see SearchApiServiceInterface::search()
+ */
public function search(SearchApiQueryInterface $query) {
$this->ensureProxy();
return $this->proxy->search($query);
}
+ /**
+ * Retrieves additional information for the server, if available.
+ *
+ * Retrieving such information is only supported if the service class supports
+ * the "search_api_service_extra" feature.
+ *
+ * @return array
+ * An array containing additional, service class-specific information about
+ * the server.
+ *
+ * @see SearchApiAbstractService::getExtraInformation()
+ */
+ public function getExtraInformation() {
+ if ($this->proxy->supportsFeature('search_api_service_extra')) {
+ return $this->proxy->getExtraInformation();
+ }
+ return array();
+ }
+
}
diff --git a/includes/service.inc b/includes/service.inc
index c6edc957..b8d8ca88 100644
--- a/includes/service.inc
+++ b/includes/service.inc
@@ -1,10 +1,20 @@
- * listing all relevant settings is preferred.
+ * Displays this server's settings.
+ *
+ * Output can be HTML or a render array, a
listing all relevant settings
+ * is preferred.
*/
public function viewSettings();
/**
+ * Reacts to the server's creation.
+ *
* Called once, when the server is first created. Allows it to set up its
* necessary infrastructure.
*/
public function postCreate();
/**
- * Notifies this server that its fields are about to be updated. The server's
- * $original property can be used to inspect the old property values.
+ * Notifies this server that its fields are about to be updated.
*
- * @return
+ * The server's $original property can be used to inspect the old property
+ * values.
+ *
+ * @return bool
* TRUE, if the update requires reindexing of all content on the server.
*/
public function postUpdate();
/**
- * Notifies this server that it is about to be deleted from the database and
- * should therefore clean up, if appropriate.
+ * Notifies this server that it is about to be deleted from the database.
+ *
+ * This should execute any necessary cleanup operations.
*
* Note that you shouldn't call the server's save() method, or any
* methods that might do that, from inside of this method as the server isn't
@@ -112,18 +136,21 @@ interface SearchApiServiceInterface {
public function preDelete();
/**
- * Add a new index to this server.
+ * Adds a new index to this server.
*
* If the index was already added to the server, the object should treat this
* as if removeIndex() and then addIndex() were called.
*
* @param SearchApiIndex $index
* The index to add.
+ *
+ * @throws SearchApiException
+ * If an error occurred while adding the index.
*/
public function addIndex(SearchApiIndex $index);
/**
- * Notify the server that the field settings for the index have changed.
+ * Notifies the server that the field settings for the index have changed.
*
* If any user action is necessary as a result of this, the method should
* use drupal_set_message() to notify the user.
@@ -134,11 +161,14 @@ interface SearchApiServiceInterface {
* @return bool
* TRUE, if this change affected the server in any way that forces it to
* re-index the content. FALSE otherwise.
+ *
+ * @throws SearchApiException
+ * If an error occurred while reacting to the change of fields.
*/
public function fieldsUpdated(SearchApiIndex $index);
/**
- * Remove an index from this server.
+ * Removes an index from this server.
*
* This might mean that the index has been deleted, or reassigned to a
* different server. If you need to distinguish between these cases, inspect
@@ -152,11 +182,14 @@ interface SearchApiServiceInterface {
* @param $index
* Either an object representing the index to remove, or its machine name
* (if the index was completely deleted).
+ *
+ * @throws SearchApiException
+ * If an error occurred while removing the index.
*/
public function removeIndex($index);
/**
- * Index the specified items.
+ * Indexes the specified items.
*
* @param SearchApiIndex $index
* The search index for which items should be indexed.
@@ -187,7 +220,7 @@ interface SearchApiServiceInterface {
public function indexItems(SearchApiIndex $index, array $items);
/**
- * Delete items from an index on this server.
+ * Deletes indexed items from this server.
*
* Might be either used to delete some items (given by their ids) from a
* specified index, or all items from that index, or all items from all
@@ -200,11 +233,14 @@ interface SearchApiServiceInterface {
* @param SearchApiIndex $index
* The index from which items should be deleted, or NULL if all indexes on
* this server should be cleared (then, $ids has to be 'all').
+ *
+ * @throws SearchApiException
+ * If an error occurred while trying to delete the items.
*/
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL);
/**
- * Create a query object for searching on an index lying on this server.
+ * Creates a query object for searching on an index lying on this server.
*
* @param SearchApiIndex $index
* The index to search on.
@@ -334,6 +370,30 @@ abstract class SearchApiAbstractService implements SearchApiServiceInterface {
return $output ? "
\n$output
" : '';
}
+ /**
+ * Returns additional, service-specific information about this server.
+ *
+ * If a service class implements this method and supports the
+ * "search_api_service_extra" option, this method will be used to add extra
+ * information to the server's "View" tab.
+ *
+ * In the default theme implementation this data will be output in a table
+ * with two columns along with other, generic information about the server.
+ *
+ * @return array
+ * An array of additional server information, with each piece of information
+ * being an associative array with the following keys:
+ * - label: The human-readable label for this data.
+ * - info: The information, as HTML.
+ * - status: (optional) The status associated with this information. One of
+ * "info", "ok", "warning" or "error". Defaults to "info".
+ *
+ * @see supportsFeature()
+ */
+ public function getExtraInformation() {
+ return array();
+ }
+
/**
* Implements SearchApiServiceInterface::__construct().
*
diff --git a/search_api.admin.css b/search_api.admin.css
index 0c49e057..b82798df 100644
--- a/search_api.admin.css
+++ b/search_api.admin.css
@@ -1,44 +1,229 @@
+/**
+ * @file
+ * Styles for Search API admin pages.
+ */
-td.search-api-status {
+/*
+ * OVERVIEW
+ */
+
+.search-api-overview td.search-api-status {
text-align: center;
}
-div.search-api-edit-menu {
+.search-api-overview td {
+ vertical-align: top;
+}
+
+/*
+ * VIEW SERVER
+ */
+
+.search-api-server-summary ul.inline {
+ margin: 0;
+}
+
+.search-api-server-summary ul.inline li {
+ padding-left: 0;
+}
+
+/*
+ * VIEW INDEX
+ */
+.search-api-limit,
+.search-api-batch-size {
+ text-align: center;
+}
+
+.search-api-index-status .progress .filled {
+ background: #0074BD none;
+}
+
+/*
+ * DROPBUTTONS
+ *
+ * (Largely copied from D8's dropbutton.css.)
+ */
+
+/**
+ * When a dropbutton has only one option, it is simply a button.
+ */
+.dropbutton-wrapper,
+.dropbutton-wrapper div {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.js .dropbutton-wrapper {
+ display: block;
+ min-height: 2em;
+ position: relative;
+}
+
+.js .dropbutton-wrapper,
+.js .dropbutton-widget {
+ max-width: 100%;
+}
+
+@media screen and (max-width: 600px) {
+ .js .dropbutton-wrapper {
+ width: 100%;
+ }
+}
+
+.js .dropbutton-widget {
position: absolute;
- background-color: white;
- color: black;
- z-index: 999;
- border: 1px solid black;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- -khtml-border-radius: 4px;
- border-radius: 4px;
}
-div.search-api-edit-menu ul {
- margin: 0 0.5em;
- padding: 0;
-}
-
-div.search-api-edit-menu ul li {
- padding: 0;
+/* UL styles are over-scoped in core, so this selector needs weight parity. */
+.js .dropbutton-widget .dropbutton {
+ list-style-image: none;
list-style-type: none;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+}
+
+.js .dropbutton li,
+.js .dropbutton a {
display: block;
}
-div.search-api-edit-menu.collapsed {
+/**
+ * The dropbutton styling.
+ *
+ * A dropbutton is a widget that displays a list of action links as a button
+ * with a primary action. Secondary actions are hidden behind a click on a
+ * twisty arrow.
+ *
+ * The arrow is created using border on a zero-width, zero-height span.
+ * The arrow inherits the link color, but can be overridden with border colors.
+ */
+.js .dropbutton-multiple .dropbutton-widget {
+ padding-right: 2em; /* LTR */
+}
+
+.js[dir="rtl"] .dropbutton-multiple .dropbutton-widget {
+ padding-left: 2em;
+ padding-right: 0;
+}
+
+.dropbutton-multiple.open,
+.dropbutton-multiple.open .dropbutton-widget {
+ max-width: none;
+}
+
+.dropbutton-multiple.open {
+ z-index: 100;
+}
+
+.dropbutton-multiple .dropbutton .secondary-action {
display: none;
}
+.dropbutton-multiple.open .dropbutton .secondary-action {
+ display: block;
+}
+
+.dropbutton-toggle {
+ bottom: 0;
+ display: block;
+ position: absolute;
+ right: 0; /* LTR */
+ text-indent: 110%;
+ top: 0;
+ white-space: nowrap;
+ width: 2em;
+}
+
+[dir="rtl"] .dropbutton-toggle {
+ left: 0;
+ right: auto;
+}
+
+.dropbutton-toggle button {
+ background: none;
+ border: 0;
+ cursor: pointer;
+ display: block;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+.dropbutton-arrow {
+ border-bottom-color: transparent;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-style: solid;
+ border-width: 0.3333em 0.3333em 0;
+ display: block;
+ height: 0;
+ line-height: 0;
+ position: absolute;
+ right: 40%; /* 0.6667em; */
+ /* LTR */
+ top: 50%;
+ margin-top: -0.1666em;
+ width: 0;
+ overflow: hidden;
+}
+
+[dir="rtl"] .dropbutton-arrow {
+ left: 0.6667em;
+ right: auto;
+}
+
+.dropbutton-multiple.open .dropbutton-arrow {
+ border-bottom: 0.3333em solid;
+ border-top-color: transparent;
+ top: 0.6667em;
+}
+
+.js .dropbutton-widget {
+ background-color: white;
+ border: 1px solid #CCC;
+}
+
+.js .dropbutton-widget:hover {
+ border-color: #B8B8B8;
+}
+
+.dropbutton .dropbutton-action > * {
+ padding: 0.1em 0.5em;
+ white-space: nowrap;
+}
+
+.dropbutton .secondary-action {
+ border-top: 1px solid #E8E8E8;
+}
+
+.dropbutton-multiple .dropbutton {
+ border-right: 1px solid #E8E8E8; /* LTR */
+}
+
+[dir="rtl"] .dropbutton-multiple .dropbutton {
+ border-left: 1px solid #E8E8E8;
+ border-right: 0 none;
+}
+
+.dropbutton-multiple .dropbutton .dropbutton-action > * {
+ margin-right: 0.25em; /* LTR */
+}
+
+[dir="rtl"] .dropbutton-multiple .dropbutton .dropbutton-action > * {
+ margin-left: 0.25em;
+ margin-right: 0;
+}
+
+/*
+ * MISC
+ */
+
.search-api-alter-add-aggregation-fields,
.search-api-checkboxes-list {
max-height: 12em;
overflow: auto;
}
-
-/* Workaround for http://drupal.org/node/1015798 */
-.vertical-tabs fieldset div.fieldset-wrapper fieldset legend {
- display: block;
- margin-bottom: 2em;
-}
-
diff --git a/search_api.admin.inc b/search_api.admin.inc
index 05a68c2a..f4210f4f 100644
--- a/search_api.admin.inc
+++ b/search_api.admin.inc
@@ -1,7 +1,14 @@
server][$index->machine_name] = $index;
@@ -46,15 +54,10 @@ function search_api_admin_overview() {
'#title' => t('disabled'),
);
$t_disabled['class'] = array('search-api-status');
- $t_enable = t('enable');
- $t_disable = t('disable');
- $t_edit = t('edit');
+ $t_enable = t('Enable');
$pre_server = 'admin/config/search/search_api/server';
$pre_index = 'admin/config/search/search_api/index';
$enable = '/enable';
- $disable = '/disable';
- $edit = '/edit';
- $edit_link_options['attributes']['class'][] = 'search-api-edit-menu-toggle';
foreach ($servers as $server) {
$url = $pre_server . '/' . $server->machine_name;
$row = array();
@@ -64,10 +67,21 @@ function search_api_admin_overview() {
}
$row[] = $t_server;
$row[] = l($server->name, $url);
- $row[] = $server->enabled ? l($t_disable, $url . $disable) : l($t_enable, $url . $enable, array('query' => array('token' => drupal_get_token($server->machine_name))));
- $row[] = l($t_edit, $url . $edit);
- $row[] = _search_api_admin_delete_link($server);
- $rows[] = $row;
+ $links = array();
+ // The "Enable" function has no menu link, since a token is required. We add
+ // it as the first link, since it will most likely be the most useful link
+ // for a disabled server. (Same for indexes below.)
+ if (!$server->enabled) {
+ $links[] = array(
+ 'title' => $t_enable,
+ 'href' => $url . $enable,
+ 'query' => array('token' => drupal_get_token($server->machine_name))
+ );
+ }
+ $links = array_merge($links, menu_contextual_links('search-api-server', $pre_server, array($server->machine_name)));
+ $row[] = theme('search_api_dropbutton', array('links' => $links));
+ $rows[] = _search_api_deep_copy($row);
+
if (!empty($indexes[$server->machine_name])) {
foreach ($indexes[$server->machine_name] as $index) {
$url = $pre_index . '/' . $index->machine_name;
@@ -76,18 +90,20 @@ function search_api_admin_overview() {
if ($show_config_status) {
$row[] = theme('entity_status', array('status' => $index->status));
}
- $row[] = '';
+ $row[] = ' ';
$row[] = $t_index;
$row[] = l($index->name, $url);
- $row[] = $index->enabled
- ? l($t_disable, $url . $disable)
- : ($server->enabled ? l($t_enable, $url . $enable, array('query' => array('token' => drupal_get_token($index->machine_name)))) : '');
- $row[] = l($t_edit, $url . $edit, $edit_link_options) .
- '
';
- $row[] = _search_api_admin_delete_link($index);
- $rows[] = $row;
+ $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name));
+ $row[] = theme('search_api_dropbutton', array('links' => $links));
+ $rows[] = _search_api_deep_copy($row);
}
}
@@ -118,32 +130,41 @@ function search_api_admin_overview() {
}
$header[] = array('data' => t('Type'), 'colspan' => 2);
$header[] = t('Name');
- $header[] = array('data' => t('Operations'), 'colspan' => 3);
+ $header[] = array('data' => t('Operations'));
return array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
+ '#attributes' => array('class' => array('search-api-overview')),
'#empty' => t('There are no search servers or indexes defined yet.'),
);
}
/**
- * @param Entity $entity
- * The index or server for which a link should be generated.
+ * Returns HTML for a drobutton list of links.
+ *
+ * When using this, you have to
+ *
+ * @param array $variables
+ * An associative array containing the following keys:
+ * - links: An array of links, as expected by theme_links().
*
* @return string
- * A link to a delete form for the entity, if applicable.
+ * HTML for the dropbutton link list.
*/
-function _search_api_admin_delete_link(Entity $entity) {
- // Delete link only makes sense if entity is in the database (custom or overridden).
- if ($entity->hasStatus(ENTITY_CUSTOM)) {
- $type = $entity instanceof SearchApiServer ? 'server' : 'index';
- $url = 'admin/config/search/search_api/' . $type . '/' . $entity->machine_name . '/delete';
- $title = $entity->hasStatus(ENTITY_IN_CODE) ? t('revert') : t('delete');
- return l($title, $url);
- }
- return '';
+function theme_search_api_dropbutton(array &$variables) {
+ $base_path = drupal_get_path('module', 'search_api') . '/';
+ drupal_add_css($base_path . 'search_api.admin.css');
+ drupal_add_js($base_path . 'search_api.admin.js');
+
+ $variables['attributes']['class'][] = 'dropbutton';
+ $list = theme('links', $variables);
+ return "
+
+ $list
+
+
";
}
/**
@@ -240,15 +261,16 @@ function search_api_admin_add_server(array $form, array &$form_state) {
}
/**
- * AJAX callback that just returns the "options" array of the already built form
- * array.
+ * Form AJAX handler for search_api_admin_add_server().
+ *
+ * Just returns the "options" array of the already built form array.
*/
function search_api_admin_add_server_ajax_callback(array $form, array &$form_state) {
return $form['options'];
}
/**
- * Form validation callback for adding a server.
+ * Form validation handler for adding a server.
*
* Validates the machine name and calls the service class' validation handler.
*/
@@ -279,7 +301,7 @@ function search_api_admin_add_server_validate(array $form, array &$form_state) {
}
/**
- * Form submit callback for adding a server.
+ * Form submission handler for adding a server.
*/
function search_api_admin_add_server_submit(array $form, array &$form_state) {
form_state_values_clean($form_state);
@@ -315,12 +337,15 @@ function search_api_admin_item_title($object) {
}
/**
- * Displays a server's details.
+ * Page callback: Displays information about a server.
*
* @param SearchApiServer $server
* The server to display.
- * @param $action
- * One of 'enable', 'disable', 'delete'; or NULL if the server is only viewed.
+ * @param string|null $action
+ * (optional) An action to execute for the server. One of 'enable', 'disable'
+ * or 'clear'.
+ *
+ * @see search_api_menu()
*/
function search_api_admin_server_view(SearchApiServer $server, $action = NULL) {
if (!empty($action)) {
@@ -340,7 +365,7 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) {
}
else {
$ret = drupal_get_form('search_api_admin_confirm', 'server', $action, $server);
- if ($ret) {
+ if (!empty($ret['actions'])) {
return $ret;
}
}
@@ -349,22 +374,41 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) {
drupal_set_title(search_api_admin_item_title($server));
$class = search_api_get_service_info($server->class);
$options = $server->viewSettings();
- return array(
+ $indexes = array();
+ foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
+ if (!$indexes) {
+ $indexes['#theme'] = 'links';
+ $indexes['#attributes']['class'] = array('inline');
+ }
+ $indexes['#links'][] = array(
+ 'title' => $index->name,
+ 'href' => 'admin/config/search/search_api/index/' . $index->machine_name,
+ );
+ }
+ $render['view'] = array(
'#theme' => 'search_api_server',
'#id' => $server->id,
'#name' => $server->name,
'#machine_name' => $server->machine_name,
'#description' => $server->description,
'#enabled' => $server->enabled,
+ '#class_id' => $server->class,
'#class_name' => $class['name'],
'#class_description' => $class['description'],
+ '#indexes' => $indexes,
'#options' => $options,
'#status' => $server->status,
+ '#extra' => $server->getExtraInformation(),
);
+ $render['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
+ if ($server->enabled) {
+ $render['form'] = drupal_get_form('search_api_server_status_form', $server);
+ }
+ return $render;
}
/**
- * Theme function for displaying a server.
+ * Returns HTML for displaying a server.
*
* @param array $variables
* An associative array containing:
@@ -373,69 +417,154 @@ function search_api_admin_server_view(SearchApiServer $server, $action = NULL) {
* - machine_name: The server's machine name.
* - description: The server's description.
* - enabled: Boolean indicating whether the server is enabled.
+ * - class_id: The used service class' ID.
* - class_name: The used service class' display name.
* - class_description: The used service class' description.
+ * - indexes: A list of indexes associated with this server, either as an HTML
+ * string or a render array.
* - options: An HTML string or render array containing information about the
* server's service-specific settings.
* - status: The entity configuration status (in database, in code, etc.).
+ * - extra: An array of additional server information in the format specified
+ * by SearchApiAbstractService::getExtraInformation().
+ *
+ * @return string
+ * HTML for displaying a server.
+ *
+ * @ingroup themeable
*/
function theme_search_api_server(array $variables) {
- extract($variables);
+ $machine_name = $variables['machine_name'];
+ $description = $variables['description'];
+ $enabled = $variables['enabled'];
+ $class_id = $variables['class_id'];
+ $class_name = $variables['class_name'];
+ $indexes = $variables['indexes'];
+ $options = $variables['options'];
+ $status = $variables['status'];
+ $extra = $variables['extra'];
+
+ // First, output the index description if there is one set.
$output = '';
- $output .= '
' . check_plain($name) . '
' . "\n";
+ if ($description) {
+ $output .= '
' . nl2br(check_plain($description)) . '
';
+ }
- $output .= '
' . "\n";
+ // Then, display a table summarizing the index's status.
+ $rows = array();
+ // Create a row template with references so we don't have to deal with the
+ // complicated structure for each individual row.
+ $row = array(
+ 'data' => array(
+ array('header' => TRUE),
+ '',
+ ),
+ 'class' => array(''),
+ );
+ $label = & $row['data'][0]['data'];
+ $info = & $row['data'][1];
+ $class = & $row['class'][0];
- $output .= '
' . "\n";
+ $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status');
+ $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (More information)', 'There are @count items indexed on the server for this index. (More information)', $vars);
+ $class = '';
+ $label = t('Server index status');
+ $rows[] = _search_api_deep_copy($row);
}
- $output .= '
',
+ );
return $form;
}
/**
- * Validation function for search_api_admin_index_status_form.
+ * Form validation handler for search_api_admin_index_status_form().
+ *
+ * @see search_api_admin_index_status_form_submit()
*/
function search_api_admin_index_status_form_validate(array $form, array &$form_state) {
- if ($form_state['values']['op'] == t('Index now') && !$form_state['values']['limit']) {
- form_set_error('number', t('You have to set the number of items to index. Set to -1 for indexing all items.'));
+ $values = $form_state['values'];
+ if ($values['op'] == t('Index now')) {
+ $all_lower = drupal_strtolower($values['all']);
+ foreach (array('limit', 'batch_size') as $field) {
+ $val = trim($values[$field]);
+ if (drupal_strtolower($val) == $all_lower) {
+ $val = -1;
+ }
+ elseif (!$val || !is_numeric($val) || ((int) $val) != $val) {
+ form_error($form['index'][$field], t('Enter a non-zero integer. Use "-1" or "@all" for "all items".', array('@all' => $values['all'])));
+ }
+ else {
+ $val = (int) $val;
+ }
+ $form_state['values'][$field] = $val;
+ }
}
}
/**
- * Submit function for search_api_admin_index_status_form.
+ * Form submission handler for search_api_admin_index_status_form().
+ *
+ * @see search_api_admin_index_status_form_validate()
*/
function search_api_admin_index_status_form_submit(array $form, array &$form_state) {
- $redirect = &$form_state['redirect'];
$values = $form_state['values'];
$index = $form_state['index'];
- $pre = 'admin/config/search/search_api/index/' . $index->machine_name;
+ $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name;
+
+ // There is a Form API bug here that will let a user submit the form via the
+ // "Index now" button even if it is disabled, and then just set "op" to the
+ // value of an arbitrary other button. We therefore have to take care to spot
+ // this case ourselves.
+ if ($form_state['input']['op'] == t('Index now') && !empty($form['index']['button']['#disabled'])) {
+ drupal_set_message(t('All items have already been indexed.'), 'warning');
+ return;
+ }
+
switch ($values['op']) {
- case t('Enable'):
- $redirect = array(
- $pre . '/enable',
- array('query' => array('token' => drupal_get_token($index->machine_name))),
- );
- break;
- case t('Disable'):
- $redirect = $pre . '/disable';
- break;
case t('Index now'):
if (!_search_api_batch_indexing_create($index, $values['batch_size'], $values['limit'], $values['remaining'])) {
drupal_set_message(t("Couldn't create a batch, please check the batch size and limit."), 'warning');
}
- $redirect = $pre . '/status';
- break;
- case t('Re-index content'):
- if ($index->reindex()) {
- drupal_set_message(t('The index was successfully scheduled for re-indexing.'));
- }
- else {
- drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error');
- }
- $redirect = $pre . '/status';
- break;
- case t('Clear index'):
- if ($index->clear()) {
- drupal_set_message(t('The index was successfully cleared.'));
- }
- else {
- drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error');
- }
- $redirect = $pre . '/status';
break;
- default:
- throw new SearchApiException(t('Unknown action.'));
+ case t('Queue all items for reindexing'):
+ $form_state['redirect'] .= '/reindex';
+ break;
+
+ case t('Clear all indexed data'):
+ $form_state['redirect'] .= '/clear';
+ break;
}
}
/**
- * Edit an index' settings.
+ * Form constructor for editing an index's settings.
*
* @param SearchApiIndex $index
* The index to edit.
+ *
+ * @ingroup forms
+ *
+ * @see search_api_admin_index_edit_submit()
*/
function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) {
$form_state['index'] = $index;
@@ -1028,17 +1197,10 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI
'#default_value' => $index->server,
'#options' => array('' => t('< No server >'))
);
- $servers = search_api_server_load_multiple(FALSE);
+ $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1));
// List enabled servers first.
foreach ($servers as $server) {
- if ($server->enabled) {
- $form['server']['#options'][$server->machine_name] = $server->name;
- }
- }
- foreach ($servers as $server) {
- if (!$server->enabled) {
- $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name));
- }
+ $form['server']['#options'][$server->machine_name] = $server->name;
}
$form['read_only'] = array(
'#type' => 'checkbox',
@@ -1070,16 +1232,23 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI
),
);
- $form['submit'] = array(
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save settings'),
);
+ $form['actions']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete'),
+ '#submit' => array('search_api_admin_form_delete_submit'),
+ '#limit_validation_errors' => array(),
+ );
return $form;
}
/**
- * Submit callback for search_api_admin_index_edit.
+ * Form submission handler for search_api_admin_index_edit().
*/
function search_api_admin_index_edit_submit(array $form, array &$form_state) {
form_state_values_clean($form_state);
@@ -1099,13 +1268,17 @@ function search_api_admin_index_edit_submit(array $form, array &$form_state) {
}
/**
- * Edit an index' workflow (data alter callbacks, pre-/postprocessors, and their
- * order).
+ * Form constructor for editing an index's data alterations and processors.
*
* @param SearchApiIndex $index
* The index to edit.
+ *
+ * @ingroup forms
+ *
+ * @see search_api_admin_index_workflow_validate()
+ * @see search_api_admin_index_workflow_submit()
*/
-// Copied from filter_admin_format_form
+// Copied from filter_admin_format_form()
function search_api_admin_index_workflow(array $form, array &$form_state, SearchApiIndex $index) {
$callback_info = search_api_get_alter_callbacks();
$processor_info = search_api_get_processors();
@@ -1345,7 +1518,9 @@ function theme_search_api_admin_item_order(array $variables) {
}
/**
- * Validation callback for search_api_admin_index_workflow.
+ * Form validation handler for search_api_admin_index_workflow().
+ *
+ * @see search_api_admin_index_workflow_submit()
*/
function search_api_admin_index_workflow_validate(array $form, array &$form_state) {
// Call validation functions.
@@ -1362,7 +1537,9 @@ function search_api_admin_index_workflow_validate(array $form, array &$form_stat
}
/**
- * Submit callback for search_api_admin_index_workflow.
+ * Form submission handler for search_api_admin_index_workflow().
+ *
+ * @see search_api_admin_index_workflow_validate()
*/
function search_api_admin_index_workflow_submit(array $form, array &$form_state) {
$values = $form_state['values'];
@@ -1371,7 +1548,6 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
$index = $form_state['index'];
$options = empty($index->options) ? array() : $index->options;
- $fields_set = !empty($options['fields']);
// Store callback and processor settings.
foreach ($form_state['callbacks'] as $name => $callback) {
@@ -1397,7 +1573,8 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
$type = $field['type'];
$inner = search_api_extract_inner_type($type);
if ($inner != 'token' && empty($types[$inner])) {
- // Someone apparently added a structure or entity as a property in a data-alter callback.
+ // Someone apparently added a structure or entity as a property in
+ // a data alteration.
continue;
}
if ($inner == 'token' || (search_api_is_text_type($inner) && !empty($field['options list']))) {
@@ -1443,8 +1620,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
$index->save();
$index->reindex();
- drupal_set_message(t("The search index' workflow was successfully edited. " .
- 'All content was scheduled for re-indexing so the new settings can take effect.'));
+ drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."));
}
else {
drupal_set_message(t('No values were changed.'));
@@ -1456,7 +1632,7 @@ function search_api_admin_index_workflow_submit(array $form, array &$form_state)
/**
* Sort callback sorting array elements by their "weight" key, if present.
*
- * @see element_sort
+ * @see element_sort()
*/
function search_api_admin_element_compare($a, $b) {
$a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0;
@@ -1468,10 +1644,11 @@ function search_api_admin_element_compare($a, $b) {
}
/**
- * Select the indexed fields.
+ * Form constructor for setting the indexed fields.
*
- * @param SearchApiIndex $index
- * The index to edit.
+ * @ingroup forms
+ *
+ * @see search_api_admin_index_fields_submit()
*/
function search_api_admin_index_fields(array $form, array &$form_state, SearchApiIndex $index) {
$options = $index->getFields(FALSE, TRUE);
@@ -1480,10 +1657,17 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
// An array of option arrays for types, keyed by nesting level.
$types = array(0 => search_api_field_types());
- $fulltext_type = array(0 => 'text');
$entity_types = entity_get_info();
- $default_types = search_api_default_field_types();
- $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
+ $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
+
+ $fulltext_types = array(0 => array('text'));
+ // Add all custom data types with fallback "text" to fulltext types as well.
+ foreach (search_api_get_data_type_info() as $id => $type) {
+ if ($type['fallback'] != 'text') {
+ continue;
+ }
+ $fulltext_types[0][] = $id;
+ }
$form_state['index'] = $index;
$form['#theme'] = 'search_api_admin_fields_table';
@@ -1518,17 +1702,20 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
'#default_value' => $info['indexed'],
);
if (empty($info['entity_type'])) {
- // Determine the correct type options (i.e., with the correct nesting level).
+ // Determine the correct type options (with the correct nesting level).
$level = search_api_list_nesting_level($info['type']);
if (empty($types[$level])) {
$type_prefix = str_repeat('list<', $level);
$type_suffix = str_repeat('>', $level);
$types[$level] = array();
foreach ($types[0] as $type => $name) {
- // We use the singular name for list types, since the user usually doesn't care about the nesting level.
+ // We use the singular name for list types, since the user usually
+ // doesn't care about the nesting level.
$types[$level][$type_prefix . $type . $type_suffix] = $name;
}
- $fulltext_type[$level] = $type_prefix . 'text' . $type_suffix;
+ foreach ($fulltext_types[0] as $type) {
+ $fulltext_types[$level][] = $type_prefix . $type . $type_suffix;
+ }
}
$css_key = '#edit-fields-' . drupal_clean_css_identifier($key);
$form['fields'][$key]['type'] = array(
@@ -1548,10 +1735,19 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
'#states' => array(
'visible' => array(
$css_key . '-indexed' => array('checked' => TRUE),
- $css_key . '-type' => array('value' => $fulltext_type[$level]),
),
),
);
+ // Only add the multiple visible states if the VERSION string is >= 7.14.
+ // See https://drupal.org/node/1464758.
+ if (version_compare(VERSION, '7.14', '>=')) {
+ foreach ($fulltext_types[$level] as $type) {
+ $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'][] = array('value' => $type);
+ }
+ }
+ else {
+ $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'] = array('value' => reset($fulltext_types[$level]));
+ }
}
else {
// This is an entity.
@@ -1646,14 +1842,17 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp
$added[$key] = TRUE;
}
- // Then we walk through all properties and look if they are already contained in one of the arrays.
- // Since this uses an iterative instead of a recursive approach, it is a bit complicated, with three arrays tracking the current depth.
+ // Then we walk through all properties and look if they are already contained
+ // in one of the arrays. Since this uses an iterative instead of a recursive
+ // approach, it is a bit complicated, with three arrays tracking the current
+ // depth.
- // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper
+ // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user
+ // wrapper
$wrappers = array('' => $wrapper);
// Display names for the prefixes
$prefix_names = array('' => '');
- // The list nesting level for entities with a certain prefix
+ // The list nesting level for entities with a certain prefix
$nesting_levels = array('' => 0);
$types = search_api_default_field_types();
@@ -1679,7 +1878,8 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp
// We hide the complexity of multi-valued types from the user here.
$type = search_api_extract_inner_type($info['type']);
// Treat Entity API type "token" as our "string" type.
- // Also let text fields with limited options be of type "string" by default.
+ // Also let text fields with limited options be of type "string" by
+ // default.
if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) {
// Inner type is changed to "string".
$type = 'string';
@@ -1768,7 +1968,7 @@ function theme_search_api_admin_fields_table($variables) {
}
}
if (empty($form['fields'][$name]['description']['#value'])) {
- $rows[] = $row;
+ $rows[] = _search_api_deep_copy($row);
}
else {
$rows[] = array(
@@ -1794,7 +1994,7 @@ function theme_search_api_admin_fields_table($variables) {
}
/**
- * Submit function for search_api_admin_index_fields.
+ * Form submission handler for search_api_admin_index_fields().
*/
function search_api_admin_index_fields_submit(array $form, array &$form_state) {
$index = $form_state['index'];
@@ -1838,7 +2038,7 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) {
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
}
else {
- drupal_set_message(t('Please set up the index workflow.'));
+ drupal_set_message(t('Please set up the indexing workflow.'));
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
}
return;
@@ -1857,11 +2057,20 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) {
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
}
-
/**
- * Helper function for displaying a generic confirmation form.
+ * Form constructor for a generic confirmation form.
*
- * @return
+ * @param $type
+ * The type of entity (not the real "entity type"). Either "server" or
+ * "index".
+ * @param $action
+ * The action that would be executed for this entity after confirming. One of
+ * "reindex" ("index" type only), "clear", "disable" or "delete".
+ * @param Entity $entity
+ * The entity for which the action would be performed. Must have a "name"
+ * property.
+ *
+ * @return array|false
* Either a form array, or FALSE if this combination of type and action is
* not supported.
*/
@@ -1869,15 +2078,24 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio
switch ($type) {
case 'server':
switch ($action) {
+ case 'clear':
+ $text = array(
+ t('Clear server @name', array('@name' => $entity->name)),
+ t('Do you really want to clear all indexed data from this server?'),
+ t('This will permanently remove all data currently indexed on this server. Before the data is reindexed, searches on the indexes associated with this server will not return any results. This action cannot be undone. Use with caution!'),
+ t("The server's indexed data was successfully cleared."),
+ );
+ break;
+
case 'disable':
$text = array(
t('Disable server @name', array('@name' => $entity->name)),
t('Do you really want to disable this server?'),
- t('This will disable both the server and all associated indexes. ' .
- "Searches on these indexes won't be available until they are re-enabled."),
+ t('This will disconnect all indexes from this server and disable them. Searches on these indexes will not be available until they are added to another server and re-enabled. All indexed data (except for read-only indexes) on this server will be cleared.'),
t('The server and its indexes were successfully disabled.'),
);
break;
+
case 'delete':
if ($entity->hasStatus(ENTITY_OVERRIDDEN)) {
$text = array(
@@ -1897,12 +2115,31 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio
);
}
break;
+
default:
return FALSE;
}
break;
case 'index':
switch ($action) {
+ case 'reindex':
+ $text = array(
+ t('Re-index index @name', array('@name' => $entity->name)),
+ t('Do you really want to queue all items on this index for re-indexing?'),
+ t('This will mark all items for this index to be marked as needing to be indexed. Searches on this index will continue to yield results while the items are being re-indexed. This action cannot be undone.'),
+ t('The index was successfully marked for re-indexing.'),
+ );
+ break;
+
+ case 'clear':
+ $text = array(
+ t('Clear index @name', array('@name' => $entity->name)),
+ t('Do you really want to clear the indexed data of this index?'),
+ t('This will remove all data currently indexed for this index. Before the data is reindexed, searches on the index will not return any results. This action cannot be undone.'),
+ t('The index was successfully cleared.'),
+ );
+ break;
+
case 'disable':
$text = array(
t('Disable index @name', array('@name' => $entity->name)),
@@ -1911,6 +2148,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio
t('The index was successfully disabled.'),
);
break;
+
case 'delete':
if ($entity->hasStatus(ENTITY_OVERRIDDEN)) {
$text = array(
@@ -1930,6 +2168,7 @@ function search_api_admin_confirm(array $form, array &$form_state, $type, $actio
);
}
break;
+
default:
return FALSE;
}
diff --git a/search_api.admin.js b/search_api.admin.js
index d841ec16..9ff40ae0 100644
--- a/search_api.admin.js
+++ b/search_api.admin.js
@@ -1,7 +1,14 @@
+/**
+ * @file
+ * Javascript enhancements for the Search API admin pages.
+ */
-// Copied from filter.admin.js
(function ($) {
+/**
+ * Allows the re-ordering of enabled data alterations and processors.
+ */
+// Copied from filter.admin.js
Drupal.behaviors.searchApiStatus = {
attach: function (context, settings) {
$('.search-api-status-wrapper input.form-checkbox', context).once('search-api-status', function () {
@@ -43,19 +50,158 @@ Drupal.behaviors.searchApiStatus = {
}
};
-Drupal.behaviors.searchApiEditMenu = {
+/**
+ * Processes elements with the .dropbutton class on page load.
+ */
+Drupal.behaviors.searchApiDropButton = {
attach: function (context, settings) {
- $('.search-api-edit-menu-toggle', context).click(function (e) {
- $menu = $(this).parent().find('.search-api-edit-menu');
- if ($menu.is('.collapsed')) {
- $menu.removeClass('collapsed');
+ var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
+ if ($dropbuttons.length) {
+ //$('.dropbutton-toggle', $dropbuttons).click(dropbuttonClickHandler);
+ // Initialize all buttons.
+ for (var i = 0, il = $dropbuttons.length; i < il; i++) {
+ DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
}
- else {
- $menu.addClass('collapsed');
- }
- return false;
- });
+ // Adds the delegated handler that will toggle dropdowns on click.
+ $('.dropbutton-toggle', $dropbuttons).click(dropbuttonClickHandler);
+ }
}
};
+/**
+ * Delegated callback for opening and closing dropbutton secondary actions.
+ */
+function dropbuttonClickHandler(e) {
+ e.preventDefault();
+ $(e.target).closest('.dropbutton-wrapper').toggleClass('open');
+}
+
+/**
+ * A DropButton presents an HTML list as a button with a primary action.
+ *
+ * All secondary actions beyond the first in the list are presented in a
+ * dropdown list accessible through a toggle arrow associated with the button.
+ *
+ * @param {jQuery} dropbutton
+ * A jQuery element.
+ *
+ * @param {Object} settings
+ * A list of options including:
+ * - {String} title: The text inside the toggle link element. This text is
+ * hidden from visual UAs.
+ */
+function DropButton(dropbutton, settings) {
+ // Merge defaults with settings.
+ var options = $.extend({'title': Drupal.t('List additional actions')}, settings);
+ var $dropbutton = $(dropbutton);
+ this.$dropbutton = $dropbutton;
+ this.$list = $dropbutton.find('.dropbutton');
+ // Find actions and mark them.
+ this.$actions = this.$list.find('li').addClass('dropbutton-action');
+
+ // Add the special dropdown only if there are hidden actions.
+ if (this.$actions.length > 1) {
+ // Identify the first element of the collection.
+ var $primary = this.$actions.slice(0, 1);
+ // Identify the secondary actions.
+ var $secondary = this.$actions.slice(1);
+ $secondary.addClass('secondary-action');
+ // Add toggle link.
+ $primary.after(Drupal.theme('dropbuttonToggle', options));
+ // Bind mouse events.
+ this.$dropbutton
+ .addClass('dropbutton-multiple')
+ /**
+ * Adds a timeout to close the dropdown on mouseleave.
+ */
+ .bind('mouseleave.dropbutton', $.proxy(this.hoverOut, this))
+ /**
+ * Clears timeout when mouseout of the dropdown.
+ */
+ .bind('mouseenter.dropbutton', $.proxy(this.hoverIn, this))
+ /**
+ * Similar to mouseleave/mouseenter, but for keyboard navigation.
+ */
+ .bind('focusout.dropbutton', $.proxy(this.focusOut, this))
+ .bind('focusin.dropbutton', $.proxy(this.focusIn, this));
+ }
+}
+
+/**
+ * Extend the DropButton constructor.
+ */
+$.extend(DropButton, {
+ /**
+ * Store all processed DropButtons.
+ *
+ * @type {Array}
+ */
+ dropbuttons: []
+});
+
+/**
+ * Extend the DropButton prototype.
+ */
+$.extend(DropButton.prototype, {
+ /**
+ * Toggle the dropbutton open and closed.
+ *
+ * @param {Boolean} show
+ * (optional) Force the dropbutton to open by passing true or to close by
+ * passing false.
+ */
+ toggle: function (show) {
+ var isBool = typeof show === 'boolean';
+ show = isBool ? show : !this.$dropbutton.hasClass('open');
+ this.$dropbutton.toggleClass('open', show);
+ },
+
+ hoverIn: function () {
+ // Clear any previous timer we were using.
+ if (this.timerID) {
+ window.clearTimeout(this.timerID);
+ }
+ },
+
+ hoverOut: function () {
+ // Wait half a second before closing.
+ this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
+ },
+
+ open: function () {
+ this.toggle(true);
+ },
+
+ close: function () {
+ this.toggle(false);
+ },
+
+ focusOut: function (e) {
+ this.hoverOut.call(this, e);
+ },
+
+ focusIn: function (e) {
+ this.hoverIn.call(this, e);
+ }
+});
+
+$.extend(Drupal.theme, {
+ /**
+ * A toggle is an interactive element often bound to a click handler.
+ *
+ * @param {Object} options
+ * - {String} title: (optional) The HTML anchor title attribute and
+ * text for the inner span element.
+ *
+ * @return {String}
+ * A string representing a DOM fragment.
+ */
+ dropbuttonToggle: function (options) {
+ return '
';
+ }
+});
+
+// Expose constructor in the public space.
+Drupal.DropButton = DropButton;
+
})(jQuery);
diff --git a/search_api.api.php b/search_api.api.php
index 9c8857e9..c1af2a14 100644
--- a/search_api.api.php
+++ b/search_api.api.php
@@ -22,7 +22,8 @@
* - description: A translated string to be shown to administrators when
* selecting a service class. Should contain all peculiarities of the
* service class, like field type support, supported features (like facets),
- * the "direct" parse mode and other specific things to keep in mind.
+ * the "direct" parse mode and other specific things to keep in mind. The
+ * text can contain HTML.
* - class: The service class, which has to implement the
* SearchApiServiceInterface interface.
*
@@ -192,6 +193,8 @@ function hook_search_api_data_type_info_alter(array &$infos) {
}
/**
+ * Define available data alterations.
+ *
* Registers one or more callbacks that can be called at index time to add
* additional data to the indexed items (e.g. comments or attachments to nodes),
* alter the data in other forms or remove items from the array.
@@ -226,6 +229,21 @@ function hook_search_api_alter_callback_info() {
return $callbacks;
}
+/**
+ * Alter the available data alterations.
+ *
+ * @param array $callbacks
+ * The callback information to be altered, keyed by callback IDs.
+ *
+ * @see hook_search_api_alter_callback_info()
+ */
+function hook_search_api_alter_callback_info_alter(array &$callbacks) {
+ if (!empty($callbacks['example_random_alter'])) {
+ $callbacks['example_random_alter']['name'] = t('Even more random alteration');
+ $callbacks['example_random_alter']['class'] = 'ExampleUltraRandomAlter';
+ }
+}
+
/**
* Registers one or more processors. These are classes implementing the
* SearchApiProcessorInterface interface which can be used at index and search
@@ -261,6 +279,20 @@ function hook_search_api_processor_info() {
return $callbacks;
}
+/**
+ * Alter the available processors.
+ *
+ * @param array $processors
+ * The processor information to be altered, keyed by processor IDs.
+ *
+ * @see hook_search_api_processor_info()
+ */
+function hook_search_api_processor_info_alter(array &$processors) {
+ if (!empty($processors['example_processor'])) {
+ $processors['example_processor']['weight'] = -20;
+ }
+}
+
/**
* Allows you to log or alter the items that are indexed.
*
diff --git a/search_api.drush.inc b/search_api.drush.inc
index 8867995d..e1841e8f 100644
--- a/search_api.drush.inc
+++ b/search_api.drush.inc
@@ -22,6 +22,32 @@ function search_api_drush_command() {
'aliases' => array('sapi-l'),
);
+ $items['search-api-enable'] = array(
+ 'description' => 'Enable one or all disabled search_api indexes.',
+ 'examples' => array(
+ 'drush searchapi-enable' => dt('Enable all disabled indexes.'),
+ 'drush sapi-en' => dt('Alias to enable all disabled indexes.'),
+ 'drush sapi-en 1' => dt('Enable index with the ID !id.', array('!id' => 1)),
+ ),
+ 'arguments' => array(
+ 'index_id' => dt('The numeric ID or machine name of an index to enable.'),
+ ),
+ 'aliases' => array('sapi-en'),
+ );
+
+ $items['search-api-disable'] = array(
+ 'description' => 'Disable one or all enabled search_api indexes.',
+ 'examples' => array(
+ 'drush searchapi-disable' => dt('Disable all enabled indexes.'),
+ 'drush sapi-dis' => dt('Alias to disable all enabled indexes.'),
+ 'drush sapi-dis 1' => dt('Disable index with the ID !id.', array('!id' => 1)),
+ ),
+ 'arguments' => array(
+ 'index_id' => dt('The numeric ID or machine name of an index to disable.'),
+ ),
+ 'aliases' => array('sapi-dis'),
+ );
+
$items['search-api-status'] = array(
'description' => 'Show the status of one or all search indexes.',
'examples' => array(
@@ -73,8 +99,8 @@ function search_api_drush_command() {
'examples' => array(
'drush searchapi-clear' => dt('Clear all search indexes.'),
'drush sapi-c' => dt('Alias to clear all search indexes.'),
- 'drush sapi-r 1' => dt('Clear the search index with the ID !id.', array('!id' => 1)),
- 'drush sapi-r default_node_index' => dt('Clear the search index with the machine name !name.', array('!name' => 'default_node_index')),
+ 'drush sapi-c 1' => dt('Clear the search index with the ID !id.', array('!id' => 1)),
+ 'drush sapi-c default_node_index' => dt('Clear the search index with the machine name !name.', array('!name' => 'default_node_index')),
),
'arguments' => array(
'index_id' => dt('The numeric ID or machine name of an index.'),
@@ -127,6 +153,65 @@ function drush_search_api_list() {
drush_print_table($rows);
}
+/**
+ * Enable index(es).
+ *
+ * @param string|integer $index_id
+ * The index name or id which should be enabled.
+ */
+function drush_search_api_enable($index_id = NULL) {
+ if (search_api_drush_static(__FUNCTION__)) {
+ return;
+ }
+ $indexes = search_api_drush_get_index($index_id);
+ if (empty($indexes)) {
+ return;
+ }
+ foreach ($indexes as $index) {
+ if (!$index->enabled) {
+ drush_log(dt("Enabling index !index and queueing items for indexing.", array('!index' => $index->name)), 'notice');
+ if (search_api_index_enable($index->id)) {
+ drush_log(dt("The index !index was successfully enabled.", array('!index' => $index->name)), 'ok');
+ }
+ else {
+ drush_log(dt("Error enabling index !index.", array('!index' => $index->name)), 'error');
+ }
+ }
+ else {
+ drush_log(dt("The index !index is already enabled.", array('!index' => $index->name)), 'error');
+ }
+ }
+}
+
+/**
+ * Disable index(es).
+ *
+ * @param string|integer $index_id
+ * The index name or id which should be disabled.
+ */
+function drush_search_api_disable($index_id = NULL) {
+ if (search_api_drush_static(__FUNCTION__)) {
+ return;
+ }
+ $indexes = search_api_drush_get_index($index_id);
+ if (empty($indexes)) {
+ return;
+ }
+ foreach ($indexes as $index) {
+ if ($index->enabled) {
+ if (search_api_index_disable($index->id)) {
+ drush_log(dt("The index !index was successfully disabled.", array('!index' => $index->name)), 'ok');
+ }
+ else {
+ drush_log(dt("Error disabling index !index.", array('!index' => $index->name)), 'error');
+ }
+ }
+ else {
+ drush_log(dt("The index !index is already disabled.", array('!index' => $index->name)), 'error');
+ }
+ }
+}
+
/**
* Display index status.
*/
diff --git a/search_api.info b/search_api.info
index 04165795..37e52c83 100644
--- a/search_api.info
+++ b/search_api.info
@@ -11,6 +11,7 @@ files[] = includes/callback_add_hierarchy.inc
files[] = includes/callback_add_url.inc
files[] = includes/callback_add_viewed_entity.inc
files[] = includes/callback_bundle_filter.inc
+files[] = includes/callback_comment_access.inc
files[] = includes/callback_language_control.inc
files[] = includes/callback_node_access.inc
files[] = includes/callback_node_status.inc
@@ -33,9 +34,9 @@ files[] = includes/service.inc
configure = admin/config/search/search_api
-; Information added by drupal.org packaging script on 2013-09-01
-version = "7.x-1.8"
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
core = "7.x"
project = "search_api"
-datestamp = "1378025826"
+datestamp = "1387965506"
diff --git a/search_api.install b/search_api.install
index 8a3366cc..86812a4c 100644
--- a/search_api.install
+++ b/search_api.install
@@ -191,6 +191,47 @@ function search_api_schema() {
'primary key' => array('item_id', 'index_id'),
);
+ $schema['search_api_task'] = array(
+ 'description' => 'Stores pending tasks for servers.',
+ 'fields' => array(
+ 'id' => array(
+ 'description' => 'An integer identifying this task.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'server_id' => array(
+ 'description' => 'The {search_api_server}.machine_name for which this task should be executed.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ ),
+ 'type' => array(
+ 'description' => 'A keyword identifying the type of task that should be executed.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ ),
+ 'index_id' => array(
+ 'description' => 'The {search_api_index}.machine_name to which this task pertains, if applicable for this type.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => FALSE,
+ ),
+ 'data' => array(
+ 'description' => 'Some data needed for the task, might be optional depending on the type.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'serialize' => TRUE,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'server' => array('server_id'),
+ ),
+ 'primary key' => array('id'),
+ );
+
return $schema;
}
@@ -330,14 +371,12 @@ function search_api_disable() {
// Modules defining entity or item types might have been disabled. Ignore.
}
}
- DrupalQueue::get('search_api_indexing_queue')->deleteQueue();
}
/**
* Implements hook_uninstall().
*/
function search_api_uninstall() {
- variable_del('search_api_tasks');
variable_del('search_api_index_worker_callback_runtime');
}
@@ -612,7 +651,7 @@ function search_api_update_7106() {
$callbacks['search_api_alter_add_aggregation'] = $callbacks['search_api_alter_add_fulltext'];
unset($callbacks['search_api_alter_add_fulltext']);
if (!empty($callbacks['search_api_alter_add_aggregation']['settings']['fields'])) {
- foreach ($callbacks['search_api_alter_add_aggregation']['settings']['fields'] as $field => &$info) {
+ foreach ($callbacks['search_api_alter_add_aggregation']['settings']['fields'] as &$info) {
if (!isset($info['type'])) {
$info['type'] = 'fulltext';
}
@@ -813,3 +852,143 @@ function search_api_update_7114() {
}
}
}
+
+/**
+ * Switch to indexing without the use of a cron queue.
+ */
+function search_api_update_7115() {
+ variable_del('search_api_batch_per_cron');
+ DrupalQueue::get('search_api_indexing_queue')->deleteQueue();
+ db_update('search_api_item')
+ ->fields(array(
+ 'changed' => 1,
+ ))
+ ->condition('changed', 0, '<')
+ ->execute();
+}
+
+/**
+ * Transfers the tasks for disabled servers to a separate database table.
+ */
+function search_api_update_7116() {
+ // Create table.
+ $table = array(
+ 'description' => 'Stores pending tasks for servers.',
+ 'fields' => array(
+ 'id' => array(
+ 'description' => 'An integer identifying this task.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'server_id' => array(
+ 'description' => 'The {search_api_server}.machine_name for which this task should be executed.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ ),
+ 'type' => array(
+ 'description' => 'A keyword identifying the type of task that should be executed.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ ),
+ 'index_id' => array(
+ 'description' => 'The {search_api_index}.machine_name to which this task pertains, if applicable for this type.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => FALSE,
+ ),
+ 'data' => array(
+ 'description' => 'Some data needed for the task, might be optional depending on the type.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'serialize' => TRUE,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'server' => array('server_id'),
+ ),
+ 'primary key' => array('id'),
+ );
+ db_create_table('search_api_task', $table);
+
+ // Collect old tasks.
+ $tasks = array();
+ foreach (variable_get('search_api_tasks', array()) as $server => $indexes) {
+ foreach ($indexes as $index => $old_tasks) {
+ if (in_array('clear all', $old_tasks)) {
+ $tasks[] = array(
+ 'server_id' => $server,
+ 'type' => 'deleteItems',
+ );
+ }
+ if (in_array('remove', $old_tasks)) {
+ $tasks[] = array(
+ 'server_id' => $server,
+ 'type' => 'removeIndex',
+ 'index_id' => $index,
+ );
+ }
+ }
+ }
+ variable_del('search_api_tasks');
+
+ $select = db_select('search_api_index', 'i')
+ ->fields('i', array('machine_name', 'server'));
+ $select->innerJoin('search_api_server', 's', 'i.server = s.machine_name AND s.enabled = 0');
+ $index_ids = array();
+ foreach ($select->execute() as $index) {
+ $index_ids[] = $index->machine_name;
+ $tasks[] = array(
+ 'server_id' => $index->server,
+ 'type' => 'removeIndex',
+ 'index_id' => $index->machine_name,
+ );
+ }
+ if ($index_ids) {
+ db_update('search_api_index')
+ ->fields(array(
+ 'enabled' => 0,
+ 'server' => NULL,
+ ))
+ ->condition('machine_name', $index_ids)
+ ->execute();
+ }
+
+ if ($tasks) {
+ $insert = db_insert('search_api_task')
+ ->fields(array('server_id', 'type', 'index_id', 'data'));
+ foreach ($tasks as $task) {
+ $insert->values($task);
+ }
+ $insert->execute();
+ }
+}
+
+/**
+ * Checks the database for illegal {search_api_index}.server values.
+ */
+function search_api_update_7117() {
+ $servers = db_select('search_api_server', 's')
+ ->fields('s', array('machine_name'))
+ ->condition('enabled', 1);
+ $indexes = db_select('search_api_index', 'i')
+ ->fields('i', array('id'))
+ ->condition('server', $servers, 'NOT IN')
+ ->execute()
+ ->fetchCol();
+ if ($indexes) {
+ db_delete('search_api_item')
+ ->condition('index_id', $indexes)
+ ->execute();
+ db_update('search_api_index')
+ ->fields(array(
+ 'server' => NULL,
+ 'enabled' => 0,
+ ))
+ ->condition('id', $indexes)
+ ->execute();
+ }
+}
diff --git a/search_api.module b/search_api.module
index ae22df66..000cadb2 100644
--- a/search_api.module
+++ b/search_api.module
@@ -1,5 +1,10 @@
'View',
- 'weight' => -10,
'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
);
$items[$pre . '/server/%search_api_server/edit'] = array(
'title' => 'Edit',
@@ -65,6 +70,19 @@ function search_api_menu() {
'file' => 'search_api.admin.inc',
'weight' => -1,
'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+ );
+ $items[$pre . '/server/%search_api_server/disable'] = array(
+ 'title' => 'Disable',
+ 'description' => 'Disable index.',
+ 'page callback' => 'search_api_admin_server_view',
+ 'page arguments' => array(5, 6),
+ 'access callback' => 'search_api_access_disable_page',
+ 'access arguments' => array(5),
+ 'file' => 'search_api.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 8,
);
$items[$pre . '/server/%search_api_server/delete'] = array(
'title' => 'Delete',
@@ -77,6 +95,8 @@ function search_api_menu() {
'access arguments' => array(5),
'file' => 'search_api.admin.inc',
'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
);
$items[$pre . '/index/%search_api_index'] = array(
'title' => 'View index',
@@ -93,27 +113,16 @@ function search_api_menu() {
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
- $items[$pre . '/index/%search_api_index/status'] = array(
- 'title' => 'Status',
- 'description' => 'Display and work on index status.',
- 'page callback' => 'drupal_get_form',
- 'page arguments' => array('search_api_admin_index_status_form', 5),
- 'access arguments' => array('administer search_api'),
- 'file' => 'search_api.admin.inc',
- 'weight' => -8,
- 'type' => MENU_LOCAL_TASK,
- 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
- );
$items[$pre . '/index/%search_api_index/edit'] = array(
- 'title' => 'Settings',
+ 'title' => 'Edit',
'description' => 'Edit index settings.',
'page callback' => 'drupal_get_form',
'page arguments' => array('search_api_admin_index_edit', 5),
'access arguments' => array('administer search_api'),
'file' => 'search_api.admin.inc',
- 'weight' => -6,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+ 'weight' => -6,
);
$items[$pre . '/index/%search_api_index/fields'] = array(
'title' => 'Fields',
@@ -122,20 +131,32 @@ function search_api_menu() {
'page arguments' => array('search_api_admin_index_fields', 5),
'access arguments' => array('administer search_api'),
'file' => 'search_api.admin.inc',
- 'weight' => -4,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+ 'weight' => -4,
);
$items[$pre . '/index/%search_api_index/workflow'] = array(
- 'title' => 'Workflow',
- 'description' => 'Edit index workflow.',
+ 'title' => 'Filters',
+ 'description' => 'Edit indexing workflow.',
'page callback' => 'drupal_get_form',
'page arguments' => array('search_api_admin_index_workflow', 5),
'access arguments' => array('administer search_api'),
'file' => 'search_api.admin.inc',
- 'weight' => -2,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+ 'weight' => -2,
+ );
+ $items[$pre . '/index/%search_api_index/disable'] = array(
+ 'title' => 'Disable',
+ 'description' => 'Disable index.',
+ 'page callback' => 'search_api_admin_index_view',
+ 'page arguments' => array(5, 6),
+ 'access callback' => 'search_api_access_disable_page',
+ 'access arguments' => array(5),
+ 'file' => 'search_api.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 8,
);
$items[$pre . '/index/%search_api_index/delete'] = array(
'title' => 'Delete',
@@ -148,11 +169,37 @@ function search_api_menu() {
'access arguments' => array(5),
'file' => 'search_api.admin.inc',
'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
);
return $items;
}
+/**
+ * Implements hook_help().
+ */
+function search_api_help($path) {
+ switch ($path) {
+ case 'admin/help#search_api':
+ $classes = array();
+ foreach (search_api_get_service_info() as $id => $info) {
+ $id = drupal_clean_css_identifier($id);
+ $name = check_plain($info['name']);
+ $description = isset($info['description']) ? $info['description'] : '';
+ $classes[] = "
' . t('A search server and search index are used to execute searches. Several indexes can exist per server. You need at least one server and one index to create searches on your site.') . '
';
+ }
+}
+
/**
* Implements hook_hook_info().
*/
@@ -193,6 +240,12 @@ function search_api_hook_info() {
* Implements hook_theme().
*/
function search_api_theme() {
+ $themes['search_api_dropbutton'] = array(
+ 'variables' => array(
+ 'links' => array(),
+ ),
+ 'file' => 'search_api.admin.inc',
+ );
$themes['search_api_server'] = array(
'variables' => array(
'id' => NULL,
@@ -200,10 +253,13 @@ function search_api_theme() {
'machine_name' => '',
'description' => NULL,
'enabled' => NULL,
+ 'class_id' => NULL,
'class_name' => NULL,
'class_description' => NULL,
+ 'indexes' => array(),
'options' => array(),
'status' => ENTITY_CUSTOM,
+ 'extra' => array(),
),
'file' => 'search_api.admin.inc',
);
@@ -219,6 +275,7 @@ function search_api_theme() {
'options' => array(),
'fields' => array(),
'indexed_items' => 0,
+ 'on_server' => 0,
'total_items' => 0,
'status' => ENTITY_CUSTOM,
'read_only' => 0,
@@ -252,53 +309,78 @@ function search_api_permission() {
/**
* Implements hook_cron().
*
- * Will index $options['cron-limit'] items for each enabled index.
+ * This will first execute any pending server tasks. After that, items will
+ * be indexed on all enabled indexes with a non-zero cron limit. Indexing will
+ * run for the time set in the search_api_index_worker_callback_runtime variable
+ * (defaulting to 15 seconds), but will at least index one batch of items on
+ * each index.
+ *
+ * @see search_api_server_tasks_check()
*/
function search_api_cron() {
- $queue = DrupalQueue::get('search_api_indexing_queue');
- foreach (search_api_index_load_multiple(FALSE, array('enabled' => TRUE, 'read_only' => 0)) as $index) {
- $limit = isset($index->options['cron_limit'])
+ // Execute pending server tasks.
+ search_api_server_tasks_check();
+
+ // Load all enabled, not read-only indexes.
+ $conditions = array(
+ 'enabled' => TRUE,
+ 'read_only' => 0
+ );
+ $indexes = search_api_index_load_multiple(FALSE, $conditions);
+ if (!$indexes) {
+ return;
+ }
+ // Remember servers which threw an exception.
+ $ignored_servers = array();
+ // Continue indexing, one batch from each index, until the time is up, but at
+ // least index one batch per index.
+ $end = time() + variable_get('search_api_index_worker_callback_runtime', 15);
+ $first_pass = TRUE;
+ while (TRUE) {
+ if (!$indexes) {
+ break;
+ }
+ foreach ($indexes as $id => $index) {
+ if (!$first_pass && time() >= $end) {
+ break 2;
+ }
+ if (!empty($ignored_servers[$index->server])) {
+ continue;
+ }
+
+ $limit = isset($index->options['cron_limit'])
? $index->options['cron_limit']
: SEARCH_API_DEFAULT_CRON_LIMIT;
- if ($limit) {
- try {
- $task = array('index' => $index->machine_name);
- // Fetch items to index, do not fetch more than the configured amount
- // of batches to be created per cron run to avoid timeouts.
- $ids = search_api_get_items_to_index($index, $limit > 0 ? $limit * variable_get('search_api_batch_per_cron', 10) : -1);
- if (!$ids) {
- continue;
+ $num = 0;
+ if ($limit) {
+ try {
+ $num = search_api_index_items($index, $limit);
+ if ($num) {
+ $variables = array(
+ '@num' => $num,
+ '%name' => $index->name
+ );
+ watchdog('search_api', 'Indexed @num items for index %name.', $variables, WATCHDOG_INFO);
+ }
}
- $batches = $limit > 0 ? array_chunk($ids, $limit, TRUE) : array($ids);
- foreach ($batches as $batch) {
- $task['items'] = $batch;
- $queue->createItem($task);
+ catch (SearchApiException $e) {
+ // Exceptions will probably be caused by the server in most cases.
+ // Therefore, don't index for any index on this server.
+ $ignored_servers[$index->server] = TRUE;
+ $vars['%index'] = $index->name;
+ watchdog_exception('search_api', $e, '%type while trying to index items on %index: !message in %function (line %line of %file).', $vars);
}
- // Mark items as queued so they won't be inserted into the queue again
- // on the next cron run.
- search_api_track_item_queued($index, $ids);
}
- catch (SearchApiException $e) {
- watchdog_exception('search_api', $e);
+ if (!$num) {
+ // Couldn't index any items => stop indexing for this index in this
+ // cron run.
+ unset($indexes[$id]);
}
}
+ $first_pass = FALSE;
}
}
-/**
- * Implements hook_cron_queue_info().
- *
- * Defines a queue for saved searches that should be checked for new items.
- */
-function search_api_cron_queue_info() {
- return array(
- 'search_api_indexing_queue' => array(
- 'worker callback' => '_search_api_indexing_queue_process',
- 'time' => variable_get('search_api_index_worker_callback_runtime', 15),
- ),
- );
-}
-
/**
* Implements hook_entity_info().
*/
@@ -310,6 +392,7 @@ function search_api_entity_info() {
'entity class' => 'SearchApiServer',
'base table' => 'search_api_server',
'uri callback' => 'search_api_server_url',
+ 'access callback' => 'search_api_entity_access',
'module' => 'search_api',
'exportable' => TRUE,
'entity keys' => array(
@@ -325,6 +408,7 @@ function search_api_entity_info() {
'entity class' => 'SearchApiIndex',
'base table' => 'search_api_index',
'uri callback' => 'search_api_index_url',
+ 'access callback' => 'search_api_entity_access',
'module' => 'search_api',
'exportable' => TRUE,
'entity keys' => array(
@@ -383,6 +467,19 @@ function search_api_entity_property_info() {
'description' => t('A flag indicating whether the server is enabled.'),
'schema field' => 'enabled',
),
+ 'status' => array(
+ 'label' => t('Status'),
+ 'type' => 'integer',
+ 'description' => t('Search API server status property'),
+ 'schema field' => 'status',
+ 'options list' => 'search_api_status_options_list',
+ ),
+ 'module' => array(
+ 'label' => t('Module'),
+ 'type' => 'text',
+ 'description' => t('The name of the module from which this server originates.'),
+ 'schema field' => 'module',
+ ),
);
$info['search_api_index']['properties'] = array(
'id' => array(
@@ -444,6 +541,19 @@ function search_api_entity_property_info() {
'description' => t('A flag indicating whether the index is read-only.'),
'schema field' => 'read_only',
),
+ 'status' => array(
+ 'label' => t('Status'),
+ 'type' => 'integer',
+ 'description' => t('Search API index status property'),
+ 'schema field' => 'status',
+ 'options list' => 'search_api_status_options_list',
+ ),
+ 'module' => array(
+ 'label' => t('Module'),
+ 'type' => 'text',
+ 'description' => t('The name of the module from which this index originates.'),
+ 'schema field' => 'module',
+ ),
);
return $info;
@@ -481,50 +591,11 @@ function search_api_search_api_server_update(SearchApiServer $server) {
}
if (!empty($server->original) && $server->enabled != $server->original->enabled) {
if ($server->enabled) {
- // Were there any changes in the server's indexes while it was disabled?
- $tasks = variable_get('search_api_tasks', array());
- if (isset($tasks[$server->machine_name])) {
- foreach ($tasks[$server->machine_name] as $index_id => $index_tasks) {
- $index = search_api_index_load($index_id);
- foreach ($index_tasks as $task) {
- switch ($task) {
- case 'add':
- $server->addIndex($index);
- break;
- case 'clear':
- $server->deleteItems('all', $index);
- break;
- case 'clear all':
- // Would normally be used with a fake index ID of "", since it
- // doesn't matter.
- $server->deleteItems('all');
- break;
- case 'fields':
- if ($server->fieldsUpdated($index)) {
- _search_api_index_reindex($index);
- }
- break;
- case 'remove':
- $server->removeIndex($index ? $index : $index_id);
- break;
- default:
- if (substr($task, 0, 7) == 'delete-') {
- $id = substr($task, 7);
- $server->deleteItems(array($id), $index);
- }
- else {
- watchdog('search_api', t('Unknown task "@task" for server "@name".', array('@task' => $task, '@name' => $server->machine_name)), NULL, WATCHDOG_WARNING);
- }
- }
- }
- }
- unset($tasks[$server->machine_name]);
- variable_set('search_api_tasks', $tasks);
- }
+ search_api_server_tasks_check($server);
}
else {
- foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name, 'enabled' => 1)) as $index) {
- $index->update(array('enabled' => 0));
+ foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
+ $index->update(array('enabled' => 0, 'server' => NULL));
}
}
}
@@ -548,9 +619,7 @@ function search_api_search_api_server_delete(SearchApiServer $server) {
$index->update(array('server' => NULL, 'enabled' => FALSE));
}
- $tasks = variable_get('search_api_tasks', array());
- unset($tasks[$server->machine_name]);
- variable_set('search_api_tasks', $tasks);
+ search_api_server_tasks_delete(NULL, $server);
}
/**
@@ -588,31 +657,14 @@ function search_api_search_api_index_update(SearchApiIndex $index) {
$old_server = search_api_server_load($index->original->server);
// The server might have changed because the old one was deleted:
if ($old_server) {
- if ($old_server->enabled) {
- $old_server->removeIndex($index);
- }
- else {
- $tasks = variable_get('search_api_tasks', array());
- // When we add or remove an index, we can ignore all other tasks.
- $tasks[$old_server->machine_name][$index->machine_name] = array('remove');
- variable_set('search_api_tasks', $tasks);
- }
+ $old_server->removeIndex($index);
}
}
if ($index->server) {
$new_server = $index->server(TRUE);
// If the server is enabled, we call addIndex(); otherwise, we save the task.
- if ($new_server->enabled) {
- $new_server->addIndex($index);
- }
- else {
- $tasks = variable_get('search_api_tasks', array());
- // When we add or remove an index, we can ignore all other tasks.
- $tasks[$new_server->machine_name][$index->machine_name] = array('add');
- variable_set('search_api_tasks', $tasks);
- unset($new_server);
- }
+ $new_server->addIndex($index);
}
// We also have to re-index all content.
@@ -627,8 +679,8 @@ function search_api_search_api_index_update(SearchApiIndex $index) {
$new_fields = $new_fields['fields'];
if ($old_fields != $new_fields) {
cache_clear_all($index->getCacheId(), 'cache', TRUE);
- if ($index->server && $index->server()->fieldsUpdated($index)) {
- _search_api_index_reindex($index);
+ if ($index->server) {
+ $index->server()->fieldsUpdated($index);
}
}
@@ -660,15 +712,6 @@ function search_api_search_api_index_update(SearchApiIndex $index) {
$index->queueItems();
}
}
-
- // If the cron batch size changed, empty the cron queue for this index.
- $old_cron = $index->original->options + array('cron_limit' => NULL);
- $old_cron = $old_cron['cron_limit'];
- $new_cron = $index->options + array('cron_limit' => NULL);
- $new_cron = $new_cron['cron_limit'];
- if ($old_cron !== $new_cron) {
- _search_api_empty_cron_queue($index, TRUE);
- }
}
/**
@@ -692,7 +735,7 @@ function search_api_search_api_index_delete(SearchApiIndex $index) {
*
* Adds dependency information for exported servers.
*/
-function search_api_features_export_alter(&$export, $module_name) {
+function search_api_features_export_alter(&$export) {
if (isset($export['features']['search_api_server'])) {
// Get a list of the modules that provide storage engines.
$hook = 'search_api_service_info';
@@ -754,7 +797,8 @@ function search_api_system_info_alter(&$info, $file, $type) {
$links = array();
foreach ($indexes as $id => $name) {
- $links[] = l($name, "admin/config/search/search_api/index/$id");
+ $url = url("admin/config/search/search_api/index/$id");
+ $links[] = '' . check_plain($name) . '';
}
$args = array('!indexes' => implode(', ', $links));
@@ -778,7 +822,8 @@ function search_api_system_info_alter(&$info, $file, $type) {
$links = array();
foreach ($servers as $id => $name) {
- $links[] = l($name, "admin/config/search/search_api/server/$id");
+ $url = url("admin/config/search/search_api/server/$id");
+ $links[] = '' . check_plain($name) . '';
}
$args = array('!servers' => implode(', ', $links));
@@ -792,13 +837,11 @@ function search_api_system_info_alter(&$info, $file, $type) {
/**
* Implements hook_entity_insert().
*
- * Marks the new item as to-index for all indexes on entities of the specified
- * type.
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_insert() for the
+ * inserted items.
*
- * @param $entity
- * The new entity.
- * @param $type
- * The entity's type.
+ * @see search_api_search_api_item_type_info()
*/
function search_api_entity_insert($entity, $type) {
// When inserting a new search index, the new index was already inserted into
@@ -818,12 +861,11 @@ function search_api_entity_insert($entity, $type) {
/**
* Implements hook_entity_update().
*
- * Marks the item as changed for all indexes on entities of the specified type.
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_change() for the
+ * updated items.
*
- * @param $entity
- * The updated entity.
- * @param $type
- * The entity's type.
+ * @see search_api_search_api_item_type_info()
*/
function search_api_entity_update($entity, $type) {
// We only react on entity operations for types with property information, as
@@ -840,12 +882,11 @@ function search_api_entity_update($entity, $type) {
/**
* Implements hook_entity_delete().
*
- * Removes the item from the tracking table and deletes it from all indexes.
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_delete() for the
+ * deleted items.
*
- * @param $entity
- * The updated entity.
- * @param $type
- * The entity's type.
+ * @see search_api_search_api_item_type_info()
*/
function search_api_entity_delete($entity, $type) {
// We only react on entity operations for types with property information, as
@@ -865,7 +906,7 @@ function search_api_entity_delete($entity, $type) {
* Recalculates fields settings if the cardinality of the field has changed from
* or to 1.
*/
-function search_api_field_update_field($field, $prior_field, $has_data) {
+function search_api_field_update_field($field, $prior_field) {
$before = $prior_field['cardinality'];
$after = $field['cardinality'];
if ($before != $after && ($before == 1 || $after == 1)) {
@@ -909,7 +950,7 @@ function search_api_search_api_item_type_info() {
/**
* Implements hook_modules_enabled().
*/
-function search_api_modules_enabled(array $modules) {
+function search_api_modules_enabled() {
// New modules might offer additional item types or service classes,
// invalidating the cached information.
drupal_static_reset('search_api_get_item_type_info');
@@ -919,7 +960,7 @@ function search_api_modules_enabled(array $modules) {
/**
* Implements hook_modules_disabled().
*/
-function search_api_modules_disabled(array $modules) {
+function search_api_modules_disabled() {
// The disabled modules might have offered item types or service classes,
// invalidating the cached information.
drupal_static_reset('search_api_get_item_type_info');
@@ -971,12 +1012,17 @@ function search_api_search_api_alter_callback_info() {
);
$callbacks['search_api_alter_node_access'] = array(
'name' => t('Node access'),
- 'description' => t('Add node access information to the index.'),
+ 'description' => t('Add node access information to the index. Caution: This only affects the indexed nodes themselves, not any node reference fields that are indexed with them, or displayed in search results.'),
'class' => 'SearchApiAlterNodeAccess',
);
+ $callbacks['search_api_alter_comment_access'] = array(
+ 'name' => t('Access check'),
+ 'description' => t('Add node access information to the index. Caution: This only affects the indexed nodes themselves, not any node reference fields that are indexed with them, or displayed in search results.'),
+ 'class' => 'SearchApiAlterCommentAccess',
+ );
$callbacks['search_api_alter_node_status'] = array(
'name' => t('Exclude unpublished nodes'),
- 'description' => t('Exclude unpublished nodes from the index.'),
+ 'description' => t('Exclude unpublished nodes from the index. Caution: This only affects the indexed nodes themselves. If an enabled node has references to disabled nodes, those will still be indexed (or displayed) normally.'),
'class' => 'SearchApiAlterNodeStatus',
);
@@ -1036,9 +1082,9 @@ function search_api_search_api_processor_info() {
/**
* Inserts new unindexed items for all indexes on the specified type.
*
- * @param $type
+ * @param string $type
* The item type of the new items.
- * @param array $item_id
+ * @param array $item_ids
* The IDs of the new items.
*/
function search_api_track_item_insert($type, array $item_ids) {
@@ -1104,6 +1150,12 @@ function search_api_track_item_change($type, array $item_ids) {
* The index on which items were queued.
* @param array $item_ids
* The ids of the queued items.
+ *
+ * @deprecated
+ * As of Search API 1.10, the cron queue is not used for indexing anymore,
+ * therefore this function has become useless. It will, along with
+ * SearchApiDataSourceControllerInterface::trackItemQueued(), be removed in
+ * the Drupal 8 version of this module.
*/
function search_api_track_item_queued(SearchApiIndex $index, array $item_ids) {
$index->datasource()->trackItemQueued($item_ids, $index);
@@ -1148,20 +1200,158 @@ function search_api_track_item_delete($type, array $item_ids) {
foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) {
if ($index->server) {
$server = $index->server();
- if ($server->enabled) {
- $server->deleteItems($item_ids, $index);
- }
- else {
- $tasks = variable_get('search_api_tasks', array());
- foreach ($item_ids as $id) {
- $tasks[$server->machine_name][$index->machine_name][] = 'delete-' . $id;
- }
- variable_set('search_api_tasks', $tasks);
- }
+ $server->deleteItems($item_ids, $index);
}
}
}
+/**
+ * Checks for pending tasks on one or all enabled search servers.
+ *
+ * @param SearchApiServer|null $server
+ * (optional) The server whose tasks should be checked. If not given, the
+ * tasks for all enabled servers are checked.
+ *
+ * @return bool
+ * TRUE if all tasks (for the specific server, if $server was given) were
+ * executed successfully, or if there were no tasks. FALSE if there are still
+ * pending tasks.
+ */
+function search_api_server_tasks_check(SearchApiServer $server = NULL) {
+ $select = db_select('search_api_task', 't')
+ ->fields('t')
+ // Only retrieve tasks we can handle.
+ ->condition('t.type', array('addIndex', 'fieldsUpdated', 'removeIndex', 'deleteItems'));
+ if ($server) {
+ $select->condition('t.server_id', $server->machine_name);
+ }
+ else {
+ $select->innerJoin('search_api_server', 's', 't.server_id = s.machine_name AND s.enabled = 1');
+ // By ordering by the server, we can later just load them when we reach them
+ // while looping through the tasks. It is very unlikely there will be tasks
+ // for more than one or two servers, so a *_load_multiple() probably
+ // wouldn't bring any significant advantages, but complicate the code.
+ $select->orderBy('t.server_id');
+ }
+ // Store a count query for later checking whether all tasks were processed
+ // successfully.
+ $count_query = $select->countQuery();
+
+ // Sometimes the order of tasks might be important, so make sure to order by
+ // the task ID (which should be in order of insertion).
+ $select->orderBy('t.id');
+ $tasks = $select->execute();
+
+ $executed_tasks = array();
+ foreach ($tasks as $task) {
+ if (!$server || $server->machine_name != $task->server_id) {
+ $server = search_api_server_load($task->server_id);
+ if (!$server) {
+ continue;
+ }
+ }
+ switch ($task->type) {
+ case 'addIndex':
+ $index = search_api_index_load($task->index_id);
+ if ($index) {
+ $server->addIndex($index);
+ }
+ break;
+
+ case 'fieldsUpdated':
+ $index = search_api_index_load($task->index_id);
+ if ($index) {
+ if ($task->data) {
+ $index->original = unserialize($task->data);
+ }
+ $server->fieldsUpdated($index);
+ }
+ break;
+
+ case 'removeIndex':
+ $index = search_api_index_load($task->index_id);
+ if ($index) {
+ $server->removeIndex($index ? $index : $task->index_id);
+ }
+ break;
+
+ case 'deleteItems':
+ $ids = $task->data ? unserialize($task->data) : 'all';
+ $index = $task->index_id ? search_api_index_load($task->index_id) : NULL;
+ // Since a failed load returns (for stupid menu handler reasons) FALSE,
+ // not NULL, we have to make doubly sure here not to pass an invalid
+ // value (and cause a fatal error).
+ $index = $index ? $index : NULL;
+ $server->deleteItems($ids, $index);
+ break;
+
+ default:
+ // This should never happen.
+ continue;
+ }
+ $executed_tasks[] = $task->id;
+ }
+
+ // If there were no tasks (we recognized), return TRUE.
+ if (!$executed_tasks) {
+ return TRUE;
+ }
+ // Otherwise, delete the executed tasks and check if new tasks were created.
+ search_api_server_tasks_delete($executed_tasks);
+ return $count_query->execute()->fetchField() === 0;
+}
+
+/**
+ * Adds an entry into a server's list of pending tasks.
+ *
+ * @param SearchApiServer $server
+ * The server for which a task should be remembered.
+ * @param $type
+ * The type of task to perform.
+ * @param SearchApiIndex|string|null $index
+ * (optional) If applicable, the index to which the task pertains (or its
+ * machine name).
+ * @param mixed $data
+ * (optional) If applicable, some further data necessary for the task.
+ */
+function search_api_server_tasks_add(SearchApiServer $server, $type, $index = NULL, $data = NULL) {
+ db_insert('search_api_task')
+ ->fields(array(
+ 'server_id' => $server->machine_name,
+ 'type' => $type,
+ 'index_id' => $index ? (is_object($index) ? $index->machine_name : $index) : NULL,
+ 'data' => isset($data) ? serialize($data) : NULL,
+ ))
+ ->execute();
+}
+
+/**
+ * Removes pending server tasks from the list.
+ *
+ * @param array|null $ids
+ * (optional) The IDs of the pending server tasks to delete. Set to NULL
+ * to not filter by IDs.
+ * @param SearchApiServer|null $server
+ * (optional) A server for which the tasks should be deleted. Set to NULL to
+ * delete tasks from all servers.
+ * @param SearchApiIndex|string|null $index
+ * (optional) An index (or its machine name) for which the tasks should be
+ * deleted. Set to NULL to delete tasks for all indexes.
+ */
+function search_api_server_tasks_delete(array $ids = NULL, SearchApiServer $server = NULL, $index = NULL) {
+ $delete = db_delete('search_api_task');
+ if ($ids) {
+ $delete->condition('id', $ids);
+ }
+ if ($server) {
+ $delete->condition('server_id', $server->machine_name);
+ }
+ if ($index) {
+ $delete->condition('index_id', $index->machine_name);
+ }
+ $delete->execute();
+}
+
/**
* Recalculates the saved fields of an index.
*
@@ -1170,7 +1360,7 @@ function search_api_track_item_delete($type, array $item_ids) {
* index and, if a discrepancy is spotted, re-save that index with updated
* fields options (thus, of course, also triggering a re-indexing operation).
*
- * @param array|false $indexes
+ * @param SearchApiIndex[]|false $indexes
* An array of SearchApiIndex objects on which to perform the operation, or
* FALSE to perform it on all indexes.
*/
@@ -1240,53 +1430,31 @@ function _search_api_settings_equals($setting1, $setting2) {
}
/**
- * Indexes items for the specified index. Only items marked as changed are
- * indexed, in their order of change (if known).
+ * Indexes items for the specified index.
+ *
+ * Only items marked as changed are indexed, in their order of change (if
+ * known).
*
* @param SearchApiIndex $index
* The index on which items should be indexed.
- * @param $limit
- * The number of items which should be indexed at most. -1 means no limit.
+ * @param int $limit
+ * (optional) The number of items which should be indexed at most. Defaults to
+ * -1, which means that all changed items should be indexed.
+ *
+ * @return int
+ * Number of successfully indexed items.
*
* @throws SearchApiException
* If any error occurs during indexing.
- *
- * @return
- * Number of successfully indexed items.
*/
function search_api_index_items(SearchApiIndex $index, $limit = -1) {
- // Don't try to index read-only indexes.
+ // Don't try to index on read-only indexes.
if ($index->read_only) {
return 0;
}
- $queue = DrupalQueue::get('search_api_indexing_queue');
- $queue->createQueue();
- $indexed = 0;
- $unlimited = $limit < 0;
- $release_items = array();
- while (($unlimited || $indexed < $limit) && ($item = $queue->claimItem(30))) {
- if ($item->data['index'] === $index->machine_name) {
- $indexed += _search_api_indexing_queue_process($item->data);
- $queue->deleteItem($item);
- }
- else {
- $release_items[] = $item;
- }
- }
-
- foreach ($release_items as $item) {
- $queue->releaseItem($item);
- }
-
- if ($unlimited || $indexed < $limit) {
- $ids = search_api_get_items_to_index($index, $unlimited ? -1 : $limit - $indexed);
- if ($ids) {
- $indexed += count(search_api_index_specific_items($index, $ids));
- }
- }
-
- return $indexed;
+ $ids = search_api_get_items_to_index($index, $limit);
+ return $ids ? count(search_api_index_specific_items($index, $ids)) : 0;
}
/**
@@ -1299,13 +1467,20 @@ function search_api_index_items(SearchApiIndex $index, $limit = -1) {
* @param array $ids
* The IDs of the items which should be indexed.
*
+ * @return array
+ * The IDs of all successfully indexed items.
+ *
* @throws SearchApiException
* If any error occurs during indexing.
- *
- * @return
- * The IDs of all successfully indexed items.
*/
function search_api_index_specific_items(SearchApiIndex $index, array $ids) {
+ // Before doing anything else, check whether there are pending tasks that need
+ // to be executed on the server. It might be important that they are executed
+ // before any indexing occurs.
+ if (!search_api_server_tasks_check($index->server())) {
+ throw new SearchApiException(t('Could not index items since important pending server tasks could not be performed.'));
+ }
+
$items = $index->loadItems($ids);
// Clone items because data alterations may alter them.
$cloned_items = array();
@@ -1694,10 +1869,13 @@ function search_api_get_alter_callbacks() {
if (!isset($callbacks)) {
$callbacks = module_invoke_all('search_api_alter_callback_info');
- // Initialization of optional entries with default values
+ // Fill optional settings with default values.
foreach ($callbacks as $id => $callback) {
- $callbacks[$id] += array('enabled' => TRUE, 'weight' => 0);
+ $callbacks[$id] += array('weight' => 0);
}
+
+ // Invoke alter hook.
+ drupal_alter('search_api_alter_callback_info', $callbacks);
}
return $callbacks;
@@ -1717,10 +1895,13 @@ function search_api_get_processors() {
if (!isset($processors)) {
$processors = module_invoke_all('search_api_processor_info');
- // Initialization of optional entries with default values
+ // Fill optional settings with default values.
foreach ($processors as $id => $processor) {
- $processors[$id] += array('enabled pre' => TRUE, 'enabled post' => TRUE, 'weight' => 0);
+ $processors[$id] += array('weight' => 0);
}
+
+ // Invoke alter hook.
+ drupal_alter('search_api_processor_info', $processors);
}
return $processors;
@@ -1735,39 +1916,68 @@ function search_api_get_processors() {
* The SearchApiQueryInterface object representing the search query.
*/
function search_api_search_api_query_alter(SearchApiQueryInterface $query) {
+ global $user;
$index = $query->getIndex();
// Only add node access if the necessary fields are indexed in the index, and
// unless disabled explicitly by the query.
- $fields = $index->options['fields'];
- if (!empty($fields['search_api_access_node']) && !empty($fields['status']) && !empty($fields['author']) && !$query->getOption('search_api_bypass_access')) {
- $account = $query->getOption('search_api_access_account', $GLOBALS['user']);
+ $type = $index->getEntityType();
+ if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status']) && !$query->getOption('search_api_bypass_access')) {
+ $account = $query->getOption('search_api_access_account', $user);
if (is_numeric($account)) {
$account = user_load($account);
}
if (is_object($account)) {
try {
- _search_api_query_add_node_access($account, $query);
+ _search_api_query_add_node_access($account, $query, $type);
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
}
else {
- watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $GLOBALS['user'])), WATCHDOG_WARNING);
+ watchdog('search_api', 'An illegal user UID was given for node access: @uid.', array('@uid' => $query->getOption('search_api_access_account', $user)), WATCHDOG_WARNING);
}
}
}
/**
- * Build a node access subquery.
- *
- * @param $account
- * The user object, who searches.
- *
- * @return SearchApiQueryFilter
- */
-function _search_api_query_add_node_access($account, SearchApiQueryInterface $query) {
- if (!user_access('access content', $account)) {
+ * Adds a node access filter to a search query, if applicable.
+ *
+ * @param object $account
+ * The user object, who searches.
+ * @param SearchApiQueryInterface $query
+ * The query to which a node access filter should be added, if applicable.
+ * @param string $type
+ * (optional) The type of search – either "node" or "comment". Defaults to
+ * "node".
+ *
+ * @throws SearchApiException
+ * If not all necessary fields are indexed on the index.
+ */
+function _search_api_query_add_node_access($account, SearchApiQueryInterface $query, $type = 'node') {
+ // Don't do anything if the user can access all content.
+ if (user_access('bypass node access', $account)) {
+ return;
+ }
+
+ $is_comment = ($type == 'comment');
+
+ // Check whether the necessary fields are indexed.
+ $fields = $query->getIndex()->options['fields'];
+ $required = array('search_api_access_node', 'status');
+ if (!$is_comment) {
+ $required[] = 'author';
+ }
+ foreach ($required as $field) {
+ if (empty($fields[$field])) {
+ $vars['@field'] = $field;
+ $vars['@index'] = $query->getIndex()->name;
+ throw new SearchApiException(t('Required field @field not indexed on index @index. Could not perform access checks.', $vars));
+ }
+ }
+
+ // If the user cannot access content/comments at all, return no results.
+ if (!user_access('access content', $account) || ($is_comment && !user_access('access content', $account))) {
// Simple hack for returning no results.
$query->condition('status', 0);
$query->condition('status', 1);
@@ -1775,43 +1985,45 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
return;
}
- // Only filter for user which don't have full node access.
- if (!user_access('bypass node access', $account)) {
- // Filter by node "published" status.
- if (user_access('view own unpublished content')) {
- $filter = $query->createFilter('OR');
- $filter->condition('status', NODE_PUBLISHED);
- $filter->condition('author', $account->uid);
- $query->filter($filter);
- }
- else {
- // $query->condition('status', NODE_PUBLISHED);
- }
- // Filter by node access grants.
+ // Filter by the "published" status.
+ $published = $is_comment ? COMMENT_PUBLISHED : NODE_PUBLISHED;
+ if (!$is_comment && user_access('view own unpublished content')) {
$filter = $query->createFilter('OR');
- $grants = node_access_grants('view', $account);
- foreach ($grants as $realm => $gids) {
- foreach ($gids as $gid) {
- $filter->condition('search_api_access_node', "node_access_$realm:$gid");
- }
- }
- $filter->condition('search_api_access_node', 'node_access__all');
+ $filter->condition('status', $published);
+ $filter->condition('author', $account->uid);
$query->filter($filter);
}
+ else {
+ $query->condition('status', $published);
+ }
+
+ // Filter by node access grants.
+ $filter = $query->createFilter('OR');
+ $grants = node_access_grants('view', $account);
+ foreach ($grants as $realm => $gids) {
+ foreach ($gids as $gid) {
+ $filter->condition('search_api_access_node', "node_access_$realm:$gid");
+ }
+ }
+ $filter->condition('search_api_access_node', 'node_access__all');
+ $query->filter($filter);
}
/**
- * Utility function for determining whether a field of the given type contains
- * text data.
+ * Determines whether a field of the given type contains text data.
*
- * @param $type
- * A string containing the type to check.
+ * Can also be used to find other types.
+ *
+ * @param string $type
+ * The type for which to check.
* @param array $allowed
* Optionally, an array of allowed types.
*
* @return
* TRUE if $type is either one of the specified types, or a list of such
* values. FALSE otherwise.
+ *
+ * @see search_api_extract_inner_type()
*/
function search_api_is_text_type($type, array $allowed = array('text')) {
return array_search(search_api_extract_inner_type($type), $allowed) !== FALSE;
@@ -1926,33 +2138,31 @@ function search_api_index_update_datasource(SearchApiIndex $index, $table, $colu
}
/**
- * Utility function for extracting specific fields from an EntityMetadataWrapper
- * object.
+ * Extracts specific field values from an EntityMetadataWrapper object.
*
* @param EntityMetadataWrapper $wrapper
* The wrapper from which to extract fields.
* @param array $fields
* The fields to extract, as stored in an index. I.e., the array keys are
- * field names, the values are arrays with the keys "name", "type", "boost"
- * and "indexed" (although only "type" is used by this function).
+ * field names, the values are arrays with at least a "type" key present.
* @param array $value_options
* An array of options that should be passed to the
* EntityMetadataWrapper::value() method (see there).
*
- * @return
+ * @return array
* The $fields array with additional "value" and "original_type" keys set.
*/
function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields, array $value_options = array()) {
// If $wrapper is a list of entities, we have to aggregate their field values.
$wrapper_info = $wrapper->info();
if (search_api_is_list_type($wrapper_info['type'])) {
- foreach ($fields as $field => &$info) {
+ foreach ($fields as &$info) {
$info['value'] = array();
$info['original_type'] = $info['type'];
}
unset($info);
try {
- foreach ($wrapper as $i => $w) {
+ foreach ($wrapper as $w) {
$nested_fields = search_api_extract_fields($w, $fields, $value_options);
foreach ($nested_fields as $field => $info) {
if (isset($info['value'])) {
@@ -2016,7 +2226,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
}
}
else {
- foreach ($nested_fields as $field => &$info) {
+ foreach ($nested_fields as &$info) {
$info['value'] = NULL;
$info['original_type'] = $info['type'];
}
@@ -2095,7 +2305,7 @@ function search_api_server_load($id, $reset = FALSE) {
* @param bool $reset
* Whether to reset the internal entity_load cache.
*
- * @return array
+ * @return SearchApiServer[]
* An array of server objects keyed by machine name.
*/
function search_api_server_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) {
@@ -2127,6 +2337,19 @@ function search_api_title_delete_page(Entity $entity) {
return $entity->hasStatus(ENTITY_OVERRIDDEN) ? t('Revert') : t('Delete');
}
+/**
+ * Determines whether the current user can disable a server or index.
+ *
+ * @param Entity $entity
+ * The server or index for which the access to the "disable" page is checked.
+ *
+ * @return bool
+ * TRUE if the "disable" page can be accessed by the user, FALSE otherwise.
+ */
+function search_api_access_disable_page(Entity $entity) {
+ return user_access('administer search_api') && !empty($entity->enabled);
+}
+
/**
* Access callback for determining if a server's or index' "delete" page should
* be accessible.
@@ -2141,6 +2364,15 @@ function search_api_access_delete_page(Entity $entity) {
return user_access('administer search_api') && $entity->hasStatus(ENTITY_CUSTOM);
}
+/**
+ * Determines whether a user can access a certain search server or index.
+ *
+ * Used as an access callback in search_api_entity_info().
+ */
+function search_api_entity_access() {
+ return user_access('administer search_api');
+}
+
/**
* Inserts a new search server into the database.
*
@@ -2160,13 +2392,13 @@ function search_api_server_insert(array $values) {
/**
* Changes a server's settings.
*
- * @param $id
+ * @param string|int $id
* The ID or machine name of the server whose values should be changed.
* @param array $fields
* The new field values to set. The enabled field can't be set this way, use
* search_api_server_enable() and search_api_server_disable() instead.
*
- * @return
+ * @return int|false
* 1 if fields were changed, 0 if the fields already had the desired values.
* FALSE on failure.
*/
@@ -2177,13 +2409,14 @@ function search_api_server_edit($id, array $fields) {
}
/**
- * Enables a search server. Will also check for remembered tasks for this server
- * and execute them.
+ * Enables a search server.
*
- * @param $id
+ * Will also check for remembered tasks for this server and execute them.
+ *
+ * @param string|int $id
* The ID or machine name of the server to enable.
*
- * @return
+ * @return int|false
* 1 on success, 0 or FALSE on failure.
*/
function search_api_server_enable($id) {
@@ -2193,12 +2426,14 @@ function search_api_server_enable($id) {
}
/**
- * Disables a search server, along with all associated indexes.
+ * Disables a search server.
*
- * @param $id
+ * Will also disable all associated indexes and remove them from the server.
+ *
+ * @param string|int $id
* The ID or machine name of the server to disable.
*
- * @return
+ * @return int|false
* 1 on success, 0 or FALSE on failure.
*/
function search_api_server_disable($id) {
@@ -2207,6 +2442,30 @@ function search_api_server_disable($id) {
return $ret ? 1 : $ret;
}
+/**
+ * Clears a search server.
+ *
+ * Will delete all items stored on the server and mark all associated indexes
+ * for re-indexing.
+ *
+ * @param int|string $id
+ * The ID or machine name of the server to clear.
+ *
+ * @return bool
+ * TRUE on success, FALSE on failure.
+ */
+function search_api_server_clear($id) {
+ $server = search_api_server_load($id);
+ $success = TRUE;
+ foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
+ $success &= $index->reindex();
+ }
+ if ($success) {
+ $server->deleteItems();
+ }
+ return $success;
+}
+
/**
* Deletes a search server and disables all associated indexes.
*
@@ -2230,12 +2489,12 @@ function search_api_server_delete($id) {
* @param $reset
* Whether to reset the internal cache.
*
- * @return SearchApiIndex
- * A completely loaded index object, or NULL if no such index exists.
+ * @return SearchApiIndex|false
+ * A completely loaded index object, or FALSE if no such index exists.
*/
function search_api_index_load($id, $reset = FALSE) {
$ret = search_api_index_load_multiple(array($id), array(), $reset);
- return $ret ? reset($ret) : FALSE;
+ return reset($ret);
}
/**
@@ -2252,7 +2511,7 @@ function search_api_index_load($id, $reset = FALSE) {
* @param bool $reset
* Whether to reset the internal entity_load cache.
*
- * @return array
+ * @return SearchApiIndex[]
* An array of index objects keyed by machine name.
*/
function search_api_index_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) {
@@ -2289,16 +2548,39 @@ function search_api_index_url(SearchApiIndex $index) {
}
/**
- * Property callback.
+ * Returns an index's server.
+ *
+ * Used as a property getter callback for the index's "server_entity" prioperty
+ * in search_api_entity_property_info().
+ *
+ * @param SearchApiIndex $index
+ * The index whose server should be returned.
*
* @return SearchApiServer
- * The server this index currently resides on, or NULL if the index
- * is currently unassigned.
+ * The server this index currently resides on, or NULL if the index is
+ * currently unassigned.
*/
function search_api_index_get_server(SearchApiIndex $index) {
return $index->server();
}
+/**
+ * Returns an options list for the "status" property.
+ *
+ * Used as an options list callback in search_api_entity_property_info().
+ *
+ * @return array
+ * An array of options, as defined by hook_options_list().
+ */
+function search_api_status_options_list() {
+ return array(
+ ENTITY_CUSTOM => t('Custom'),
+ ENTITY_IN_CODE => t('Default'),
+ ENTITY_OVERRIDDEN => t('Overridden'),
+ ENTITY_FIXED => t('Fixed'),
+ );
+}
+
/**
* Inserts a new search index into the database.
*
@@ -2318,12 +2600,12 @@ function search_api_index_insert(array $values) {
/**
* Changes an index' settings.
*
- * @param $id
- * The edited index' id.
+ * @param int|string $id
+ * The edited index' ID or machine name.
* @param array $fields
* The new field values to set.
*
- * @return
+ * @return int|false
* 1 if fields were changed, 0 if the fields already had the desired values.
* FALSE on failure.
*/
@@ -2336,12 +2618,12 @@ function search_api_index_edit($id, array $fields) {
/**
* Changes an index' indexed field settings.
*
- * @param $id
+ * @param int|string $id
* The ID or machine name of the index whose fields should be changed.
* @param array $fields
* The new indexed field settings.
*
- * @return
+ * @return int|false
* 1 if the field settings were changed, 0 if they already had the desired
* values. FALSE on failure.
*/
@@ -2408,42 +2690,6 @@ function search_api_index_reindex($id) {
*/
function _search_api_index_reindex(SearchApiIndex $index) {
$index->datasource()->trackItemChange(FALSE, array($index), TRUE);
- _search_api_empty_cron_queue($index);
-}
-
-/**
- * Helper method for removing all of an index's jobs from the cron queue.
- *
- * @param SearchApiIndex $index
- * The index whose jobs should be removed.
- * @param $mark_changed
- * If TRUE, mark all items in the queue as "changed" again. Defaults to FALSE.
- */
-function _search_api_empty_cron_queue(SearchApiIndex $index, $mark_changed = FALSE) {
- $index_id = $index->machine_name;
- $queue = DrupalQueue::get('search_api_indexing_queue');
- $queue->createQueue();
- $ids = array();
- $release_items = array();
- while ($item = $queue->claimItem()) {
- if ($item->data['index'] === $index_id) {
- $queue->deleteItem($item);
- if ($mark_changed) {
- $ids = array_merge($ids, $item->data['items']);
- }
- }
- else {
- $release_items[] = $item;
- }
- }
-
- foreach ($release_items as $item) {
- $queue->releaseItem($item);
- }
-
- if ($ids) {
- $index->datasource()->trackItemChange($ids, array($index), TRUE);
- }
}
/**
@@ -2495,42 +2741,6 @@ function search_api_index_options_list() {
return $ret;
}
-/**
- * Cron queue worker callback for indexing some items.
- *
- * @param array $task
- * An associative array containing:
- * - index: The ID of the index on which items should be indexed.
- * - items: The items that should be indexed.
- *
- * @return
- * The number of successfully indexed items.
- */
-function _search_api_indexing_queue_process(array $task) {
- $index = search_api_index_load($task['index']);
- try {
- if ($index && $index->enabled && !$index->read_only && $task['items']) {
- $indexed = search_api_index_specific_items($index, $task['items']);
- $num = count($indexed);
- // If some items couldn't be indexed, mark them as dirty again.
- if ($num < count($task['items'])) {
- // Believe it or not but this is actually quite faster than the equivalent
- // $diff = array_diff($task['items'], $indexed);
- $diff = array_keys(array_diff_key(array_flip($task['items']), array_flip($indexed)));
- // Mark the items as dirty again.
- $index->datasource()->trackItemChange($diff, array($index), TRUE);
- }
- if ($num) {
- watchdog('search_api', t('Indexed @num items for index @name', array('@num' => $num, '@name' => $index->name)), NULL, WATCHDOG_INFO);
- }
- return $num;
- }
- }
- catch (SearchApiException $e) {
- watchdog_exception('search_api', $e);
- }
-}
-
/**
* Shutdown function which indexes all queued items, if any.
*/
@@ -2588,18 +2798,78 @@ function _search_api_convert_custom_type($callback, $value, $original_type, $typ
}
/**
- * Create and set a batch for indexing items.
+ * Determines the number of items indexed on a server for a certain index.
+ *
+ * Used as a helper function in search_api_admin_index_view().
+ *
+ * @param SearchApiIndex $index
+ * The index
+ *
+ * @return int
+ * The number of items found on the server for this index, if the latter is
+ * enabled. 0 otherwise.
+ */
+function _search_api_get_items_on_server(SearchApiIndex $index) {
+ if (!$index->enabled) {
+ return 0;
+ }
+ // We want the raw count, without facets or other filters. Therefore we don't
+ // use the query's execute() method but pass it straight to the server for
+ // evaluation. Since this circumvents the normal preprocessing, which sets the
+ // fields (on which some service classes might even rely when there are no
+ // keywords), we set them manually here.
+ $query = $index->query()
+ ->fields(array())
+ ->range(0, 0);
+ $response = $index->server()->search($query);
+ return $response['result count'];
+}
+
+/**
+ * Returns a deep copy of the input array.
+ *
+ * The behavior of PHP regarding arrays with references pointing to it is rather
+ * weird. Therefore, we use this helper function in theme_search_api_index() to
+ * create safe copies of such arrays.
+ *
+ * @param array $array
+ * The array to copy.
+ *
+ * @return array
+ * A deep copy of the array.
+ */
+function _search_api_deep_copy(array $array) {
+ $copy = array();
+ foreach ($array as $k => $v) {
+ if (is_array($v)) {
+ $copy[$k] = _search_api_deep_copy($v);
+ }
+ elseif (is_object($v)) {
+ $copy[$k] = clone $v;
+ }
+ elseif ($v) {
+ $copy[$k] = $v;
+ }
+ }
+ return $copy;
+}
+
+/**
+ * Creates and sets a batch for indexing items.
*
* @param SearchApiIndex $index
* The index for which items should be indexed.
- * @param $batch_size
+ * @param int $batch_size
* Number of items to index per batch.
- * @param $limit
- * Maximum number of items to index.
- * @param $remaining
+ * @param int $limit
+ * Maximum number of items to index. Negative values mean "no limit".
+ * @param int $remaining
* Remaining items to index.
- * @param $drush
+ * @param bool $drush
* Boolean specifying whether this was called from drush or not.
+ *
+ * @return bool
+ * Whether the batch was created and set successfully.
*/
function _search_api_batch_indexing_create(SearchApiIndex $index, $batch_size, $limit, $remaining, $drush = FALSE) {
if ($limit !== 0 && $batch_size !== 0) {
@@ -2637,10 +2907,10 @@ function _search_api_batch_indexing_create(SearchApiIndex $index, $batch_size, $
* Maximum number of items to index.
* @param boolean $drush
* Boolean specifying whether this was called from drush or not.
- * @param array $context
- * The batch context.
+ * @param $context
+ * An array (or object implementing ArrayAccess) containing the batch context.
*/
-function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, $limit, $drush = FALSE, array &$context) {
+function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size, $limit, $drush = FALSE, &$context) {
// Persistent data among batch runs.
if (!isset($context['sandbox']['limit'])) {
$context['sandbox']['limit'] = $limit;
@@ -2659,8 +2929,18 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size,
$to_index = min($context['sandbox']['limit'] - $context['sandbox']['progress'], $context['sandbox']['batch_size']);
// Index the items.
- $indexed = search_api_index_items($index, $to_index);
- $context['results']['indexed'] += $indexed;
+ try {
+ $indexed = search_api_index_items($index, $to_index);
+ $context['results']['indexed'] += $indexed;
+ }
+ catch (SearchApiException $e) {
+ watchdog_exception('search_api', $e);
+ $vars['@message'] = $e->getMessage();
+ $context['message'] = t('An error occurred during indexing: @message.', $vars);
+ $context['finished'] = 1;
+ $context['results']['not indexed'] += $context['sandbox']['limit'] - $context['sandbox']['progress'];
+ return;
+ }
// Display progress message.
if ($indexed > 0) {
@@ -2688,13 +2968,11 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size,
* Batch API finishing callback for the indexing functionality.
*
* @param boolean $success
- * Result of the batch operation.
+ * Whether the batch finished successfully.
* @param array $results
- * Results.
- * @param array $operations
- * Remaining batch operation to process.
+ * Detailed informations about the result.
*/
-function _search_api_batch_indexing_finished($success, $results, $operations) {
+function _search_api_batch_indexing_finished($success, $results) {
// Check if called from drush.
if (!empty($results['drush'])) {
$drupal_set_message = 'drush_log';
diff --git a/search_api.test b/search_api.test
index 1748627c..47479812 100644
--- a/search_api.test
+++ b/search_api.test
@@ -1,29 +1,66 @@
assertResponse(200, 'HTTP code 200 returned.');
return $ret;
}
+ /**
+ * Overrides DrupalWebTestCase::drupalPost().
+ *
+ * Additionally asserts that the HTTP request returned a 200 status code.
+ */
protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
$ret = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
$this->assertResponse(200, 'HTTP code 200 returned.');
return $ret;
}
+ /**
+ * Returns information about this test case.
+ *
+ * @return array
+ * An array with information about this test case.
+ */
public static function getInfo() {
return array(
'name' => 'Test search API framework',
@@ -32,24 +69,34 @@ class SearchApiWebTest extends DrupalWebTestCase {
);
}
+ /**
+ * {@inheritdoc}
+ */
public function setUp() {
parent::setUp('entity', 'search_api', 'search_api_test');
}
+ /**
+ * Tests correct admin UI, indexing and search behavior.
+ *
+ * We only use a single test method to avoid wasting ressources on setting up
+ * the test environment multiple times. This will be the only method called
+ * by the Simpletest framework (since the method name starts with "test"). It
+ * in turn calls other methdos that set up the environment in a certain way
+ * and then run tests on it.
+ */
public function testFramework() {
$this->drupalLogin($this->drupalCreateUser(array('administer search_api')));
- // @todo Why is there no default index?
- //$this->deleteDefaultIndex();
$this->insertItems();
- $this->checkOverview1();
$this->createIndex();
- $this->insertItems(5);
+ $this->insertItems();
$this->createServer();
- $this->checkOverview2();
+ $this->checkOverview();
$this->enableIndex();
$this->searchNoResults();
$this->indexItems();
$this->searchSuccess();
+ $this->checkIndexingOrder();
$this->editServer();
$this->clearIndex();
$this->searchNoResults();
@@ -57,57 +104,64 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->disableModules();
}
- protected function deleteDefaultIndex() {
- $this->drupalPost('admin/config/search/search_api/index/default_node_index/delete', array(), t('Confirm'));
+ /**
+ * Returns the test server in use by this test case.
+ *
+ * @return SearchApiServer
+ * The test server.
+ */
+ protected function server() {
+ return search_api_server_load($this->server_id, TRUE);
}
- protected function insertItems($offset = 0) {
+ /**
+ * Returns the test index in use by this test case.
+ *
+ * @return SearchApiIndex
+ * The test index.
+ */
+ protected function index() {
+ return search_api_index_load($this->index_id, TRUE);
+ }
+
+ /**
+ * Inserts some test items into the database, via the test module.
+ *
+ * @param int $number
+ * The number of items to insert.
+ *
+ * @see insertItem()
+ */
+ protected function insertItems($number = 5) {
$count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField();
- $this->insertItem(array(
- 'id' => $offset + 1,
- 'title' => 'Title 1',
- 'body' => 'Body text 1.',
- 'type' => 'Item',
- ));
- $this->insertItem(array(
- 'id' => $offset + 2,
- 'title' => 'Title 2',
- 'body' => 'Body text 2.',
- 'type' => 'Item',
- ));
- $this->insertItem(array(
- 'id' => $offset + 3,
- 'title' => 'Title 3',
- 'body' => 'Body text 3.',
- 'type' => 'Item',
- ));
- $this->insertItem(array(
- 'id' => $offset + 4,
- 'title' => 'Title 4',
- 'body' => 'Body text 4.',
- 'type' => 'Page',
- ));
- $this->insertItem(array(
- 'id' => $offset + 5,
- 'title' => 'Title 5',
- 'body' => 'Body text 5.',
- 'type' => 'Page',
- ));
+ for ($i = 1; $i <= $number; ++$i) {
+ $id = $count + $i;
+ $this->insertItem(array(
+ 'id' => $id,
+ 'title' => "Title $id",
+ 'body' => "Body text $id.",
+ 'type' => 'Item',
+ ));
+ }
$count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() - $count;
- $this->assertEqual($count, 5, '5 items successfully inserted.');
+ $this->assertEqual($count, $number, "$number items successfully inserted.");
}
- protected function insertItem($values) {
+ /**
+ * Helper function for inserting a single test item.
+ *
+ * @param array $values
+ * The property values of the test item.
+ *
+ * @see search_api_test_insert_item()
+ */
+ protected function insertItem(array $values) {
$this->drupalPost('search_api_test/insert', $values, t('Save'));
}
- protected function checkOverview1() {
- // This test fails for no apparent reason for drupal.org test bots.
- // Commenting them out for now.
- //$this->drupalGet('admin/config/search/search_api');
- //$this->assertText(t('There are no search servers or indexes defined yet.'), '"No servers" message is displayed.');
- }
-
+ /**
+ * Creates a test index via the UI and tests whether this works correctly.
+ */
protected function createIndex() {
$values = array(
'name' => '',
@@ -136,7 +190,7 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), 'The index was successfully created.');
$found = strpos($this->getUrl(), 'admin/config/search/search_api/index/' . $id) !== FALSE;
$this->assertTrue($found, 'Correct redirect.');
- $index = search_api_index_load($id, TRUE);
+ $index = $this->index();
$this->assertEqual($index->name, $values['name'], 'Name correctly inserted.');
$this->assertEqual($index->item_type, $values['item_type'], 'Index item type correctly inserted.');
$this->assertFalse($index->enabled, 'Status correctly inserted.');
@@ -210,18 +264,18 @@ class SearchApiWebTest extends DrupalWebTestCase {
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:body]' => 1,
);
$this->drupalPost(NULL, $values, t('Save configuration'));
- $this->assertText(t("The search index' workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.');
+ $this->assertText(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.');
$this->drupalGet("admin/config/search/search_api/index/$id");
$this->assertTitle('Search API test index | Drupal', 'Correct title when viewing index.');
$this->assertText('An index used for testing.', 'Description displayed.');
$this->assertText('Search API test entity', 'Item type displayed.');
- $this->assertText(format_plural(1, '1 item per cron batch.', '@count items per cron batch.'), 'Cron batch size displayed.');
-
- $this->drupalGet("admin/config/search/search_api/index/$id/status");
- $this->assertText(t('The index is currently disabled.'), '"Disabled" status displayed.');
+ $this->assertText(t('disabled'), '"Disabled" status displayed.');
}
+ /**
+ * Creates a test server via the UI and tests whether this works correctly.
+ */
protected function createServer() {
$values = array(
'name' => '',
@@ -251,7 +305,7 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText(t('The server was successfully created.'));
$found = strpos($this->getUrl(), 'admin/config/search/search_api/server/' . $id) !== FALSE;
$this->assertTrue($found, 'Correct redirect.');
- $server = search_api_server_load($id, TRUE);
+ $server = $this->server();
$this->assertEqual($server->name, $values['name'], 'Name correctly inserted.');
$this->assertTrue($server->enabled, 'Status correctly inserted.');
$this->assertEqual($server->description, $values['description'], 'Description correctly inserted.');
@@ -260,17 +314,22 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertTitle('Search API test server | Drupal', 'Correct title when viewing server.');
$this->assertText('A server used for testing.', 'Description displayed.');
$this->assertText('search_api_test_service', 'Service name displayed.');
- $this->assertText('search_api_test_service description', 'Service description displayed.');
$this->assertText('search_api_test foo bar', 'Service options displayed.');
}
- protected function checkOverview2() {
+ /**
+ * Checks whether the server and index are correctly listed in the overview.
+ */
+ protected function checkOverview() {
$this->drupalGet('admin/config/search/search_api');
$this->assertText('Search API test server', 'Server displayed.');
$this->assertText('Search API test index', 'Index displayed.');
$this->assertNoText(t('There are no search servers or indexes defined yet.'), '"No servers" message not displayed.');
}
+ /**
+ * Moves the index onto the server and enables it.
+ */
protected function enableIndex() {
$values = array(
'server' => $this->server_id,
@@ -283,24 +342,60 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText(t('The index was successfully enabled.'));
}
+ /**
+ * Asserts that a search on the index works but yields no results.
+ *
+ * This is the case since no items should have been indexed yet.
+ */
protected function searchNoResults() {
- $this->drupalGet('search_api_test/query/' . $this->index_id);
- $this->assertText('result count = 0', 'No search results returned without indexing.');
- $this->assertText('results = ()', 'No search results returned without indexing.');
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 0, 'No search results returned without indexing.');
+ $this->assertEqual(array_keys($results['results']), array(), 'No search results returned without indexing.');
}
+ /**
+ * Executes a search on the test index.
+ *
+ * Helper method used for testing search results.
+ *
+ * @param int|null $offset
+ * (optional) The offset for the returned results.
+ * @param int|null $limit
+ * (optional) The limit for the returned results.
+ *
+ * @return array
+ * Search results as specified by SearchApiQueryInterface::execute().
+ */
+ protected function doSearch($offset = NULL, $limit = NULL) {
+ // Since we change server and index settings via the UI (and, therefore, in
+ // different page requests), the static cache in this page request
+ // (executing the tests) will get stale. Therefore, we clear it before
+ // executing the search.
+ $this->index();
+ $this->server();
+
+ $query = search_api_query($this->index_id);
+ if ($offset || $limit) {
+ $query->range($offset, $limit);
+ }
+ return $query->execute();
+ }
+
+ /**
+ * Tests indexing via the UI "Index now" functionality.
+ *
+ * Asserts that errors during indexing are handled properly and that the
+ * status readings work.
+ */
protected function indexItems() {
- $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status");
- $this->assertText(t('The index is currently enabled.'), '"Enabled" status displayed.');
- $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.');
- $this->assertText(t('Index now'), '"Index now" button found.');
- $this->assertText(t('Clear index'), '"Clear index" button found.');
- $this->assertNoText(t('Re-index content'), '"Re-index" button not found.');
+ $this->checkIndexStatus();
// Here we test the indexing + the warning message when some items
- // can not be indexed.
- // The server refuses (for test purpose) to index items with IDs that are
- // multiples of 8 unless the "search_api_test_index_all" variable is set.
+ // cannot be indexed.
+ // The server refuses (for test purpose) to index the item that has the same
+ // ID as the "search_api_test_indexing_break" variable (default: 8).
+ // Therefore, if we try to index 8 items, only the first seven will be
+ // successfully indexed and a warning should be displayed.
$values = array(
'limit' => 8,
);
@@ -308,11 +403,14 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText(t('Successfully indexed @count items.', array('@count' => 7)));
$this->assertText(t('1 item could not be indexed. Check the logs for details.'), 'Index errors warning is displayed.');
$this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed.");
- $this->assertText(t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => 7, '@total' => 10, '@percentage' => 70)), 'Correct index status displayed.');
- $this->assertText(t('Re-indexing'), '"Re-index" button found.');
+ $this->checkIndexStatus(7);
// Here we're testing the error message when no item could be indexed.
- // The item with ID 8 is still not indexed.
+ // The item with ID 8 is still not indexed, but it will be the first to be
+ // indexed now. Therefore, if we try to index a single items, only item 8
+ // will be passed to the server, which will reject it and no items will be
+ // indexed. Since normally this signifies a more serious error than when
+ // only some items couldn't be indexed, this is handled differently.
$values = array(
'limit' => 1,
);
@@ -321,8 +419,10 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertNoText(t('1 item could not be indexed. Check the logs for details.'), "Index errors warning isn't displayed.");
$this->assertText(t("Couldn't index items. Check the logs for details."), 'Index error is displayed.');
- // Here we test the indexing of all the remaining items.
- variable_set('search_api_test_index_all', TRUE);
+ // No we set the "search_api_test_indexing_break" variable to 0, so all
+ // items will be indexed. The remaining items (8, 9, 10) should therefore
+ // be successfully indexed and no warning should show.
+ variable_set('search_api_test_indexing_break', 0);
$values = array(
'limit' => -1,
);
@@ -330,20 +430,249 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText(t('Successfully indexed @count items.', array('@count' => 3)));
$this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed.");
$this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed.");
- $this->assertText(t('All items have been indexed (@indexed / @total).', array('@indexed' => 10, '@total' => 10)), 'Correct index status displayed.');
- $this->assertNoText(t('Index now'), '"Index now" button no longer displayed.');
+ $this->checkIndexStatus(10);
+
+ // Reset the static cache for the server.
+ $this->server();
}
+ /**
+ * Checks whether the index's "Status" tab shows the correct values.
+ *
+ * Helper method used by indexItems() and others.
+ *
+ * The internal browser will point to the index's "Status" tab after this
+ * method is called.
+ *
+ * @param int $indexed
+ * (optional) The number of items that should be indexed at the moment.
+ * Defaults to 0.
+ * @param int $total
+ * (optional) The (correct) total number of items. Defaults to 10.
+ * @param bool $check_buttons
+ * (optional) Whether to check for the correct presence/absence of buttons.
+ * Defaults to TRUE.
+ * @param int|null $on_server
+ * (optional) The number of items actually on the server. Defaults to
+ * $indexed.
+ */
+ protected function checkIndexStatus($indexed = 0, $total = 10, $check_buttons = TRUE, $on_server = NULL) {
+ $url = "admin/config/search/search_api/index/{$this->index_id}";
+ if (strpos($this->url, $url) === FALSE) {
+ $this->drupalGet($url);
+ }
+
+ $index_status = t('@indexed/@total indexed', array('@indexed' => $indexed, '@total' => $total));
+ $this->assertText($index_status, 'Correct index status displayed.');
+
+ if (!isset($on_server)) {
+ $on_server = $indexed;
+ }
+ $info = format_plural($on_server, 'There is 1 item indexed on the server for this index.', 'There are @count items indexed on the server for this index.');
+ $this->assertText(t('Server index status'), 'Server index status displayed.');
+ $this->assertText($info, 'Correct server index status displayed.');
+
+ if (!$check_buttons) {
+ return;
+ }
+
+ $this->assertText(t('enabled'), '"Enabled" status displayed.');
+ if ($indexed == $total) {
+ $this->assertRaw('disabled="disabled"', '"Index now" form disabled.');
+ }
+ else {
+ $this->assertNoRaw('disabled="disabled"', '"Index now" form enabled.');
+ }
+ }
+
+ /**
+ * Tests whether searches yield the right results after indexing.
+ *
+ * The test server only implements range functionality, no kind of fulltext
+ * search capabilities, so we can only test for that.
+ */
protected function searchSuccess() {
- $this->drupalGet('search_api_test/query/' . $this->index_id);
- $this->assertText('result count = 10', 'Correct search result count returned after indexing.');
- $this->assertText('results = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)', 'Correct search results returned after indexing.');
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 10, 'Correct search result count returned after indexing.');
+ $this->assertEqual(array_keys($results['results']), array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 'Correct search results returned after indexing.');
- $this->drupalGet('search_api_test/query/' . $this->index_id . '/foo/2/4');
- $this->assertText('result count = 10', 'Correct search result count with ranged query.');
- $this->assertText('results = (3, 4, 5, 6)', 'Correct search results with ranged query.');
+ $results = $this->doSearch(2, 4);
+ $this->assertEqual($results['result count'], 10, 'Correct search result count with ranged query.');
+ $this->assertEqual(array_keys($results['results']), array(3, 4, 5, 6), 'Correct search results with ranged query.');
}
+ /**
+ * Tests whether items are indexed in the right order.
+ *
+ * The indexing order should always be that new items are indexed before
+ * changed ones, and only then the changed items in the order of their change.
+ *
+ * This method also assures that this behavior is even observed when indexing
+ * temporarily fails.
+ *
+ * @see https://drupal.org/node/2115127
+ */
+ protected function checkIndexingOrder() {
+ // Set cron batch size to 1 so not all items will get indexed right away.
+ // This also ensures that later, when indexing of a single item will be
+ // rejected by using the "search_api_test_indexing_break" variable, this
+ // will have the effect of rejecting "all" items of a batch (since that
+ // batch only consists of a single item).
+ $values = array(
+ 'options[cron_limit]' => 1,
+ );
+ $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $values, t('Save settings'));
+ $this->assertText(t('The search index was successfully edited.'));
+
+ // Manually clear the server's item storage – that way, the items will still
+ // count as indexed for the Search API, but won't be returned in searches.
+ // We do this so we have finer-grained control over the order in which items
+ // are indexed.
+ $this->server()->deleteItems();
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 0, 'Indexed items were successfully deleted from the server.');
+ $this->assertEqual(array_keys($results['results']), array(), 'Indexed items were successfully deleted from the server.');
+
+ // Now insert some new items, and mark others as changed. Make sure that
+ // each action has a unique timestamp, so the order will be correct.
+ $this->drupalGet('search_api_test/touch/8');
+ $this->insertItems(1);// item 11
+ sleep(1);
+ $this->drupalGet('search_api_test/touch/2');
+ $this->insertItems(1);// item 12
+ sleep(1);
+ $this->drupalGet('search_api_test/touch/5');
+ $this->insertItems(1);// item 13
+ sleep(1);
+ $this->drupalGet('search_api_test/touch/8');
+ $this->insertItems(1); // item 14
+
+ // Check whether the status display is right.
+ $this->checkIndexStatus(7, 14, FALSE, 0);
+
+ // Indexing order should now be: 11, 12, 13, 14, 8, 2, 4. Let's try it out!
+ // First manually index one item, and see if it's 11.
+ $values = array(
+ 'limit' => 1,
+ );
+ $this->drupalPost(NULL, $values, t('Index now'));
+ $this->assertText(t('Successfully indexed @count item.', array('@count' => 1)));
+ $this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), "Index errors warning isn't displayed.");
+ $this->assertNoText(t("Couldn't index items. Check the logs for details."), "Index error isn't displayed.");
+ $this->checkIndexStatus(8, 14, FALSE, 1);
+
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 1, 'Indexing order test 1: correct result count.');
+ $this->assertEqual(array_keys($results['results']), array(11), 'Indexing order test 1: correct results.');
+
+ // Now index with a cron run, but stop at item 8.
+ variable_set('search_api_test_indexing_break', 8);
+ $this->cronRun();
+ // Now just the four new items should have been indexed.
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 4, 'Indexing order test 2: correct result count.');
+ $this->assertEqual(array_keys($results['results']), array(11, 12, 13, 14), 'Indexing order test 2: correct results.');
+
+ // This time stop at item 5 (should be the last one).
+ variable_set('search_api_test_indexing_break', 5);
+ $this->cronRun();
+ // Now all new and changed items should have been indexed, except item 5.
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 6, 'Indexing order test 3: correct result count.');
+ $this->assertEqual(array_keys($results['results']), array(2, 8, 11, 12, 13, 14), 'Indexing order test 3: correct results.');
+
+ // Index the remaining item.
+ variable_set('search_api_test_indexing_break', 0);
+ $this->cronRun();
+ // Now all new and changed items should have been indexed.
+ $results = $this->doSearch();
+ $this->assertEqual($results['result count'], 7, 'Indexing order test 4: correct result count.');
+ $this->assertEqual(array_keys($results['results']), array(2, 5, 8, 11, 12, 13, 14), 'Indexing order test 4: correct results.');
+ }
+
+ /**
+ * Tests whether the server tasks system works correctly.
+ *
+ * Uses the "search_api_test_error_state" variable to trigger exceptions in
+ * the test service class and asserts that the Search API reacts correctly and
+ * re-attempts the operation on the next cron run.
+ */
+ protected function checkServerTasks() {
+ // Make sure none of the previous operations added any tasks.
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 0, 'No server tasks were previously saved.');
+
+ // Set error state for test service, so all operations will fail.
+ variable_set('search_api_test_error_state', TRUE);
+
+ // Delete some items.
+ $this->drupalGet('search_api_test/delete/8');
+ $this->drupalGet('search_api_test/delete/12');
+
+ // Assert that the indexed items haven't changed yet.
+ $results = $this->doSearch();
+ $this->assertEqual(array_keys($results['results']), array(2, 5, 8, 11, 12, 13, 14), 'During error state, no indexed items were deleted.');
+
+ // Check that tasks were correctly inserted.
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 2, 'Server tasks for deleted items were saved.');
+
+ // Now reset the error state variable and run cron to delete the items.
+ variable_set('search_api_test_error_state', FALSE);
+ $this->cronRun();
+
+ // Assert that the indexed items were indeed deleted from the server.
+ $results = $this->doSearch();
+ $this->assertEqual(array_keys($results['results']), array(2, 5, 11, 13, 14), 'Pending "delete item" server tasks were correctly executed during the cron run.');
+
+ // Check that the tasks were correctly deleted.
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 0, 'Server tasks were correctly deleted after being executed.');
+
+ // Now we first delete more items, then disable the server (thereby removing
+ // the index from it) – all while in error state.
+ variable_set('search_api_test_error_state', TRUE);
+ $this->drupalGet('search_api_test/delete/14');
+ $this->drupalGet('search_api_test/delete/2');
+ $settings['enabled'] = 0;
+ $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $settings, t('Save settings'));
+
+ // Check whether the index was correctly removed from the server.
+ $this->assertEqual($this->index()->server(), NULL, 'The index was successfully set to have no server.');
+ $exception = FALSE;
+ try {
+ $this->doSearch();
+ }
+ catch (SearchApiException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, 'Searching on the index failed with an exception.');
+
+ // Check that only one task – to remove the index from the server – is now
+ // present in the tasks table.
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 1, 'Only the "remove index" task is present in the server tasks.');
+
+ // Reset the error state variable, re-enable the server.
+ variable_set('search_api_test_error_state', FALSE);
+ $settings['enabled'] = 1;
+ $this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $settings, t('Save settings'));
+
+ // Check whether the index was really removed from the server now.
+ $server = $this->server();
+ $this->assertTrue(empty($server->options['indexes'][$this->index_id]), 'The index was removed from the server after cron ran.');
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 0, 'Server tasks were correctly deleted after being executed.');
+
+ // Put the index back on the server and index some items for the next tests.
+ $settings = array('server' => $this->server_id);
+ $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $settings, t('Save settings'));
+ $this->cronRun();
+ }
+
+ /**
+ * Tests whether editing the server works correctly.
+ */
protected function editServer() {
$values = array(
'name' => 'test-name-foo',
@@ -357,19 +686,49 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertText('test-test-baz', 'Service options changed.');
}
+ /**
+ * Tests whether clearing the index works correctly.
+ */
protected function clearIndex() {
- $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/status", array(), t('Clear index'));
+ $this->drupalPost("admin/config/search/search_api/index/{$this->index_id}", array(), t('Clear all indexed data'));
+ $this->drupalPost(NULL, array(), t('Confirm'));
$this->assertText(t('The index was successfully cleared.'));
- $this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), 'Correct index status displayed.');
+ $this->assertText(t('@indexed/@total indexed', array('@indexed' => 0, '@total' => 14)), 'Correct index status displayed.');
}
+ /**
+ * Tests whether deleting the server works correctly.
+ *
+ * The index still lying on the server should be disabled and removed from it.
+ * Also, any tasks with that server's ID should be deleted.
+ */
protected function deleteServer() {
+ // Insert some dummy tasks to check for.
+ $server = $this->server();
+ search_api_server_tasks_add($server, 'foo');
+ search_api_server_tasks_add($server, 'bar', $this->index());
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 2, 'Dummy tasks were added.');
+
+ // Delete the server.
$this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm'));
$this->assertNoText('test-name-foo', 'Server no longer listed.');
- $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status");
- $this->assertText(t('The index is currently disabled.'), 'The index was disabled and removed from the server.');
+ $this->drupalGet("admin/config/search/search_api/index/{$this->index_id}");
+ $this->assertNoText(t('Server'), 'The index was removed from the server.');
+ $this->assertText(t('disabled'), 'The index was disabled.');
+
+ // Check whether the tasks were correctly deleted.
+ $task_count = db_query('SELECT COUNT(id) FROM {search_api_task}')->fetchField();
+ $this->assertEqual($task_count, 0, 'Remaining server tasks were correctly deleted.');
}
+ /**
+ * Tests whether disabling and uninstalling the modules works correctly.
+ *
+ * This will disable and uninstall both the test module and the Search API. It
+ * asserts that this works correctly (since the server has been deleted in
+ * deleteServer()) and that all associated tables and variables are removed.
+ */
protected function disableModules() {
module_disable(array('search_api_test'), FALSE);
$this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.');
@@ -384,7 +743,7 @@ class SearchApiWebTest extends DrupalWebTestCase {
$this->assertFalse(db_table_exists('search_api_server'), 'Search server table was successfully removed.');
$this->assertFalse(db_table_exists('search_api_index'), 'Search index table was successfully removed.');
$this->assertFalse(db_table_exists('search_api_item'), 'Index items table was successfully removed.');
- $this->assertNull(variable_get('search_api_tasks'), 'Tasks variable was correctly removed.');
+ $this->assertFalse(db_table_exists('search_api_task'), 'Server tasks table was successfully removed.');
$this->assertNull(variable_get('search_api_index_worker_callback_runtime'), 'Worker runtime variable was correctly removed.');
}
@@ -398,8 +757,19 @@ class SearchApiWebTest extends DrupalWebTestCase {
*/
class SearchApiUnitTest extends DrupalWebTestCase {
+ /**
+ * The index used by these tests.
+ *
+ * @var SearchApIindex
+ */
protected $index;
+ /**
+ * Overrides DrupalTestCase::assertEqual().
+ *
+ * For arrays, checks whether all array keys are mapped the same in both
+ * arrays recursively, while ignoring their order.
+ */
protected function assertEqual($first, $second, $message = '', $group = 'Other') {
if (is_array($first) && is_array($second)) {
return $this->assertTrue($this->deepEquals($first, $second), $message, $group);
@@ -409,6 +779,20 @@ class SearchApiUnitTest extends DrupalWebTestCase {
}
}
+ /**
+ * Tests whether two values are equal.
+ *
+ * For arrays, this is done by comparing the key/value pairs recursively
+ * instead of checking for simple equality.
+ *
+ * @param mixed $first
+ * The first value.
+ * @param mixed $second
+ * The second value.
+ *
+ * @return bool
+ * TRUE if the two values are equal, FALSE otherwise.
+ */
protected function deepEquals($first, $second) {
if (!is_array($first) || !is_array($second)) {
return $first == $second;
@@ -424,6 +808,12 @@ class SearchApiUnitTest extends DrupalWebTestCase {
return empty($second);
}
+ /**
+ * Returns information about this test case.
+ *
+ * @return array
+ * An array with information about this test case.
+ */
public static function getInfo() {
return array(
'name' => 'Test search API components',
@@ -432,6 +822,9 @@ class SearchApiUnitTest extends DrupalWebTestCase {
);
}
+ /**
+ * {@inheritdoc}
+ */
public function setUp() {
parent::setUp('entity', 'search_api');
$this->index = entity_create('search_api_index', array(
@@ -455,6 +848,12 @@ class SearchApiUnitTest extends DrupalWebTestCase {
));
}
+ /**
+ * Tests the functionality of several components of the module.
+ *
+ * This is the single test method called by the Simpletest framework. It in
+ * turn calls other helper methods to test specific functionality.
+ */
public function testUnits() {
$this->checkQueryParseKeys();
$this->checkIgnoreCaseProcessor();
@@ -462,11 +861,13 @@ class SearchApiUnitTest extends DrupalWebTestCase {
$this->checkHtmlFilter();
}
- public function checkQueryParseKeys() {
+ /**
+ * Checks whether the keys are parsed correctly by the query class.
+ */
+ protected function checkQueryParseKeys() {
$options['parse mode'] = 'direct';
$mode = &$options['parse mode'];
$query = new SearchApiQuery($this->index, $options);
- $modes = $query->parseModes();
$query->keys('foo');
$this->assertEqual($query->getKeys(), 'foo', '"Direct query" parse mode, test 1.');
@@ -499,8 +900,10 @@ class SearchApiUnitTest extends DrupalWebTestCase {
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), '"Multiple terms" parse mode, test 4.');
}
- public function checkIgnoreCaseProcessor() {
- $types = search_api_field_types();
+ /**
+ * Tests the functionality of the "Ignore case" processor.
+ */
+ protected function checkIgnoreCaseProcessor() {
$orig = 'Foo bar BaZ, ÄÖÜÀÁ<>»«.';
$processed = drupal_strtolower($orig);
$items = array(
@@ -566,7 +969,10 @@ class SearchApiUnitTest extends DrupalWebTestCase {
$this->assertEqual($query->getFilter()->getFilters(), $filters2, 'Filters were processed correctly.');
}
- public function checkTokenizer() {
+ /**
+ * Tests the functionality of the "Tokenizer" processor.
+ */
+ protected function checkTokenizer() {
$orig = 'Foo bar1 BaZ, La-la-la.';
$processed1 = array(
array(
@@ -648,7 +1054,10 @@ class SearchApiUnitTest extends DrupalWebTestCase {
$this->assertEqual($query->getKeys(), 'foo"b r b z"foob r1', 'Search keys were processed correctly.');
}
- public function checkHtmlFilter() {
+ /**
+ * Tests the functionality of the "HTML filter" processor.
+ */
+ protected function checkHtmlFilter() {
$orig = <<a test.
diff --git a/tests/search_api_test.info b/tests/search_api_test.info
index a55fd3b7..07cf4ad3 100644
--- a/tests/search_api_test.info
+++ b/tests/search_api_test.info
@@ -10,9 +10,9 @@ files[] = search_api_test.module
hidden = TRUE
-; Information added by drupal.org packaging script on 2013-09-01
-version = "7.x-1.8"
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
core = "7.x"
project = "search_api"
-datestamp = "1378025826"
+datestamp = "1387965506"
diff --git a/tests/search_api_test.install b/tests/search_api_test.install
index 8dea6ebc..2db73bb2 100644
--- a/tests/search_api_test.install
+++ b/tests/search_api_test.install
@@ -39,7 +39,13 @@ function search_api_test_schema() {
'description' => 'A comma separated list of keywords.',
'type' => 'varchar',
'length' => 200,
- 'not null' => FALSE,
+ 'not null' => FALSE,
+ ),
+ 'prices' => array(
+ 'description' => 'A comma separated list of prices.',
+ 'type' => 'varchar',
+ 'length' => 200,
+ 'not null' => FALSE,
),
),
'primary key' => array('id'),
diff --git a/tests/search_api_test.module b/tests/search_api_test.module
index 67e33920..1b227f3d 100644
--- a/tests/search_api_test.module
+++ b/tests/search_api_test.module
@@ -1,5 +1,10 @@
array('search_api_test_insert_item'),
'access callback' => TRUE,
),
- 'search_api_test/%search_api_test' => array(
+ 'search_api_test/view/%search_api_test' => array(
'title' => 'View item',
'page callback' => 'search_api_test_view',
- 'page arguments' => array(1),
+ 'page arguments' => array(2),
'access callback' => TRUE,
),
- 'search_api_test/query/%search_api_index' => array(
- 'title' => 'Search query',
- 'page callback' => 'search_api_test_query',
+ 'search_api_test/touch/%search_api_test' => array(
+ 'title' => 'Mark item as changed',
+ 'page callback' => 'search_api_test_touch',
+ 'page arguments' => array(2),
+ 'access callback' => TRUE,
+ ),
+ 'search_api_test/delete/%search_api_test' => array(
+ 'title' => 'Delete items',
+ 'page callback' => 'search_api_test_delete',
'page arguments' => array(2),
'access callback' => TRUE,
),
@@ -46,6 +57,9 @@ function search_api_test_insert_item(array $form, array &$form_state) {
'keywords' => array(
'#type' => 'textfield',
),
+ 'prices' => array(
+ '#type' => 'textfield',
+ ),
'submit' => array(
'#type' => 'submit',
'#value' => t('Save'),
@@ -74,42 +88,22 @@ function search_api_test_load($id) {
* Menu callback for displaying search_api_test entities.
*/
function search_api_test_view($entity) {
- return array('text' => nl2br(check_plain(print_r($entity, TRUE))));
+ return nl2br(check_plain(print_r($entity, TRUE)));
}
/**
- * Menu callback for executing a search.
+ * Menu callback for marking a "search_api_test" entity as changed.
*/
-function search_api_test_query(SearchApiIndex $index, $keys = 'foo bar', $offset = 0, $limit = 10, $fields = NULL, $sort = NULL, $filters = NULL) {
- $query = $index->query()
- ->keys($keys ? $keys : NULL)
- ->range($offset, $limit);
- if ($fields) {
- $query->fields(explode(',', $fields));
- }
- if ($sort) {
- $sort = explode(',', $sort);
- $query->sort($sort[0], $sort[1]);
- }
- else {
- $query->sort('search_api_id', 'ASC');
- }
- if ($filters) {
- $filters = explode(',', $filters);
- foreach ($filters as $filter) {
- $filter = explode('=', $filter);
- $query->condition($filter[0], $filter[1]);
- }
- }
- $result = $query->execute();
+function search_api_test_touch($entity) {
+ module_invoke_all('entity_update', $entity, 'search_api_test');
+}
- $ret = '';
- $ret .= 'result count = ' . (int) $result['result count'] . ' ';
- $ret .= 'results = (' . (empty($result['results']) ? '' : implode(', ', array_keys($result['results']))) . ') ';
- $ret .= 'warnings = (' . (empty($result['warnings']) ? '' : '"' . implode('", "', $result['warnings']) . '"') . ') ';
- $ret .= 'ignored = (' . (empty($result['ignored']) ? '' : implode(', ', $result['ignored'])) . ') ';
- $ret .= nl2br(check_plain(print_r($result['performance'], TRUE)));
- return $ret;
+/**
+ * Menu callback for marking a "search_api_test" entity as changed.
+ */
+function search_api_test_delete($entity) {
+ db_delete('search_api_test')->condition('id', $entity->id)->execute();
+ module_invoke_all('entity_delete', $entity, 'search_api_test');
}
/**
@@ -169,6 +163,12 @@ function search_api_test_entity_property_info() {
'description' => 'An optional collection of keywords describing the item.',
'getter callback' => 'search_api_test_list_callback',
),
+ 'prices' => array(
+ 'label' => 'Prices',
+ 'type' => 'list',
+ 'description' => 'An optional list of prices.',
+ 'getter callback' => 'search_api_test_list_callback',
+ ),
);
return $info;
@@ -193,13 +193,17 @@ function search_api_test_parent($entity) {
/**
* List callback.
*/
-function search_api_test_list_callback($data) {
- //return is_array($entity->keywords) ? $entity->keywords : explode(',', $entity->keywords);
+function search_api_test_list_callback($data, array $options, $name) {
if (is_array($data)) {
- $res = is_array($data['keywords']) ? $data['keywords'] : explode(',', $data['keywords']);
+ $res = is_array($data[$name]) ? $data[$name] : explode(',', $data[$name]);
}
else {
- $res = is_array($data->keywords) ? $data->keywords : explode(',', $data->keywords);
+ $res = is_array($data->$name) ? $data->$name : explode(',', $data->$name);
+ }
+ if ($name == 'prices') {
+ foreach ($res as &$x) {
+ $x = (float) $x;
+ }
}
return array_filter($res);
}
@@ -221,6 +225,11 @@ function search_api_test_search_api_service_info() {
*/
class SearchApiTestService extends SearchApiAbstractService {
+ /**
+ * Overrides SearchApiAbstractService::configurationForm().
+ *
+ * Returns a single text field for testing purposes.
+ */
public function configurationForm(array $form, array &$form_state) {
$form = array(
'test' => array(
@@ -236,38 +245,72 @@ class SearchApiTestService extends SearchApiAbstractService {
return $form;
}
- public function indexItems(SearchApiIndex $index, array $items) {
- // Refuse to index items with IDs that are multiples of 8 unless the
- // "search_api_test_index_all" variable is set.
- if (variable_get('search_api_test_index_all', FALSE)) {
- return $this->index($index, array_keys($items));
- }
- $ret = array();
- foreach ($items as $id => $item) {
- if ($id % 8) {
- $ret[] = $id;
- }
- }
- return $this->index($index, $ret);
+ /**
+ * {@inheritdoc}
+ */
+ public function addIndex(SearchApiIndex $index) {
+ $this->checkErrorState();
}
- protected function index(SearchApiIndex $index, array $ids) {
+ /**
+ * {@inheritdoc}
+ */
+ public function fieldsUpdated(SearchApiIndex $index) {
+ $this->checkErrorState();
+ return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeIndex($index) {
+ $this->checkErrorState();
+ parent::removeIndex($index);
+ }
+
+ /**
+ * Implements SearchApiServiceInterface::indexItems().
+ *
+ * Indexes items by storing their IDs in the server's options.
+ *
+ * If the "search_api_test_indexing_break" variable is set, the item with
+ * that ID will not be indexed.
+ */
+ public function indexItems(SearchApiIndex $index, array $items) {
+ $this->checkErrorState();
+ // Refuse to index the item with the same ID as the
+ // "search_api_test_indexing_break" variable, if it is set.
+ $exclude = variable_get('search_api_test_indexing_break', 8);
+ foreach ($items as $id => $item) {
+ if ($id == $exclude) {
+ unset($items[$id]);
+ }
+ }
+ $ids = array_keys($items);
+
$this->options += array('indexes' => array());
$this->options['indexes'] += array($index->machine_name => array());
$this->options['indexes'][$index->machine_name] += drupal_map_assoc($ids);
- sort($this->options['indexes'][$index->machine_name]);
+ asort($this->options['indexes'][$index->machine_name]);
$this->server->save();
+
return $ids;
}
/**
- * Override so deleteItems() isn't called which would otherwise lead to the
+ * Overrides SearchApiAbstractService::preDelete().
+ *
+ * Overridden so deleteItems() isn't called which would otherwise lead to the
* server being updated and, eventually, to a notice because there is no
* server to be updated anymore.
*/
public function preDelete() {}
+ /**
+ * {@inheritdoc}
+ */
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
+ $this->checkErrorState();
if ($ids == 'all') {
if ($index) {
$this->options['indexes'][$index->machine_name] = array();
@@ -284,6 +327,12 @@ class SearchApiTestService extends SearchApiAbstractService {
$this->server->save();
}
+ /**
+ * Implements SearchApiServiceInterface::indexItems().
+ *
+ * Will ignore all query settings except the range, as only the item IDs are
+ * indexed.
+ */
public function search(SearchApiQueryInterface $query) {
$options = $query->getOptions();
$ret = array();
@@ -315,8 +364,16 @@ class SearchApiTestService extends SearchApiAbstractService {
return $ret;
}
- public function fieldsUpdated(SearchApiIndex $index) {
- return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0;
+ /**
+ * Throws an exception if the "search_api_test_error_state" variable is set.
+ *
+ * @throws SearchApiException
+ * If the "search_api_test_error_state" variable is set.
+ */
+ protected function checkErrorState() {
+ if (variable_get('search_api_test_error_state', FALSE)) {
+ throw new SearchApiException();
+ }
}
}