updated to 7.x-1.11

This commit is contained in:
Bachir Soussi Chiadmi 2014-02-07 10:01:18 +01:00
parent a30917d1d2
commit cf03e9ca52
69 changed files with 4629 additions and 1557 deletions

View File

@ -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):
----------------------------

View File

@ -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
--------------------------

View File

@ -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;
}

View File

@ -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' => '<div class="facetapi-global-setting">',
'#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' => '</div>',
'#options' => $search_ids,
'#size' => min(4, count($search_ids)),
'#multiple' => TRUE,
@ -246,9 +252,25 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
'#type' => 'select',
'#title' => t('Granularity'),
'#description' => t('Determine the maximum drill-down level'),
'#prefix' => '<div class="facetapi-global-setting">',
'#suffix' => '</div>',
'#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' => '</div>',
'#weight' => -1,
'#default_value' => !empty($options['exclude']),
);
}
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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"

View File

@ -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.
*

View File

@ -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;

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerArgument.
*/
/**
* Views argument handler class for handling all non-fulltext types.
*/

View File

@ -0,0 +1,161 @@
<?php
/**
* @file
* Contains the SearchApiViewsHandlerArgumentDate class.
*/
/**
* Defines a contextual filter searching for a date or date range.
*/
class SearchApiViewsHandlerArgumentDate extends SearchApiViewsHandlerArgument {
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
if (empty($this->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);
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerArgumentFulltext.
*/
/**
* Views argument handler class for handling fulltext fields.
*/

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerArgumentMoreLikeThis.
*/
/**
* Views argument handler providing a list of related items for search servers
* supporting the "search_api_mlt" feature.

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerArgumentString.
*/
/**
* Views argument handler class for handling string fields.
*/

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilter.
*/
/**
* Views filter handler base class for handling all "normal" cases.
*/
@ -31,8 +36,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
*/
public function operator_options() {
return array(
'<' => 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'),
);
}
}
/**

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterBoolean.
*/
/**
* Views filter handler class for handling fulltext fields.
*/

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterDate.
*/
/**
* Views filter handler base class for handling all "normal" cases.
*/

View File

@ -0,0 +1,211 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterEntity.
*/
/**
* Views filter handler class for entities.
*
* Should be extended for specific entity types, such as
* SearchApiViewsHandlerFilterUser and SearchApiViewsHandlerFilterTaxonomyTerm.
*
* Based on views_handler_filter_term_node_tid.
*/
abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFilter {
/**
* If exposed form input was successfully validated, the entered entity IDs.
*
* @var array
*/
protected $validated_exposed_input;
/**
* Validates entered entity labels and converts them to entity IDs.
*
* Since this can come from either the form or the exposed filter, this is
* abstracted out a bit so it can handle the multiple input sources.
*
* @param array $form
* The form or form element for which any errors should be set.
* @param array $values
* The entered user names to validate.
*
* @return array
* The entity IDs corresponding to all entities that could be found.
*/
abstract protected function validate_entity_strings(array &$form, array $values);
/**
* Transforms an array of entity IDs into a comma-separated list of labels.
*
* @param array $ids
* The entity IDs to transform.
*
* @return string
* A string containing the labels corresponding to the IDs, separated by
* commas.
*/
abstract protected function ids_to_strings(array $ids);
/**
* {@inheritdoc}
*/
public function operator_options() {
$operators = array(
'=' => $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']);
}
}
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterFulltext.
*/
/**
* Views filter handler class for handling fulltext fields.
*/
@ -33,6 +38,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
$options['operator']['default'] = 'AND';
$options['mode'] = array('default' => '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);
}

View File

@ -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;

View File

@ -1,16 +1,82 @@
<?php
/**
* Views filter handler class for handling fields with a limited set of possible
* values.
*
* Definition items:
* - options: An array of possible values for this field.
* @file
* Contains the SearchApiViewsHandlerFilterOptions class.
*/
/**
* Views filter handler for fields with a limited set of possible values.
*/
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
/**
* Stores the values which are available on the form.
*
* @var array
*/
protected $value_options = NULL;
/**
* The type of form element used to display the options.
*
* @var string
*/
protected $value_form_type = 'checkboxes';
/**
* Retrieves a wrapper for this filter's field.
*
* @return EntityMetadataWrapper|null
* A wrapper for the field which this filter uses.
*/
protected function get_wrapper() {
if ($this->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;

View File

@ -0,0 +1,294 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterTaxonomyTerm.
*/
/**
* Views filter handler class for taxonomy term entities.
*
* Based on views_handler_filter_term_node_tid.
*/
class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilterEntity {
/**
* {@inheritdoc}
*/
public function has_extra_options() {
return !empty($this->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());
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterText.
*/
/**
* Views filter handler class for handling fulltext fields.
*/

View File

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerFilterUser.
*/
/**
* Views filter handler class for handling user entities.
*
* Based on views_handler_filter_user_name.
*/
class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity {
/**
* {@inheritdoc}
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);
// Set autocompletion.
$path = $this->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;
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsHandlerSort.
*/
/**
* Class for sorting results according to a specified field.
*/

View File

@ -98,7 +98,7 @@ class SearchApiViewsCache extends views_plugin_cache_time {
// other parameters used in the parent method are already reflected in the
// Search API query object we use.
if (isset($_GET['exposed_info'])) {
$key_data[$key] = $_GET[$key];
$key_data['exposed_info'] = $_GET['exposed_info'];
}
$this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiViewsQuery.
*/
/**
* Views query class using a Search API index as the data source.
*/
@ -180,7 +185,6 @@ class SearchApiViewsQuery extends views_plugin_query {
'#options' => 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);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @file
* Hooks provided by the Search Views module.
*/
/**
* Alter the query before executing the query.
*
* @param view $view
* The view object about to be processed.
* @param SearchApiViewsQuery $query
* The Search API Views query to be altered.
*
* @see hook_views_query_alter()
*/
function hook_search_api_views_query_alter(view &$view, SearchApiViewsQuery &$query) {
// (Example assuming a view with an exposed filter on node title.)
// If the input for the title filter is a positive integer, filter against
// node ID instead of node title.
if ($view->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'],'=');
}
}
}
}
}

View File

@ -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"

View File

@ -1,4 +1,5 @@
<?php
/**
* @file
* Install, update and uninstall functions for the search_api_views module.
@ -24,7 +25,7 @@ function search_api_views_update_7101() {
if (!$table_fields) {
return;
}
foreach (views_get_all_views() as $name => $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)) {

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Integrates the Search API with Views.
*/
/**
* Implements hook_views_api().
*/
@ -12,7 +17,7 @@ function search_api_views_views_api() {
/**
* Implements hook_search_api_index_insert().
*/
function search_api_views_search_api_index_insert(SearchApiIndex $index) {
function search_api_views_search_api_index_insert() {
// Make the new index available for views.
views_invalidate_cache();
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Views hook implementations for the Search API Views module.
*/
/**
* Implements hook_views_data().
*/
@ -28,7 +33,7 @@ function search_api_views_views_data() {
}
try {
$wrapper = $index->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';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

View File

@ -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

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiAlterAddAggregation.
*/
/**
* Search API data alteration callback that adds an URL field for all items.
*/
@ -11,7 +16,11 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
$fields = $this->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(

View File

@ -1,7 +1,12 @@
<?php
/**
* Search API data alteration callback that adds an URL field for all items.
* @file
* Contains SearchApiAlterAddHierarchy.
*/
/**
* Adds all ancestors for hierarchical fields.
*/
class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
@ -15,24 +20,16 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
protected $field_options;
/**
* Enable this data alteration only if any hierarchical fields are available.
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* @param SearchApiIndex $index
* The index to check for.
*
* @return boolean
* TRUE if the callback can run on the given index; FALSE otherwise.
* Returns TRUE only if any hierarchical fields are available.
*/
public function supportsIndex(SearchApiIndex $index) {
return (bool) $this->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

View File

@ -1,12 +1,17 @@
<?php
/**
* @file
* Contains SearchApiAlterAddUrl.
*/
/**
* Search API data alteration callback that adds an URL field for all items.
*/
class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback {
public function alterItems(array &$items) {
foreach ($items as $id => &$item) {
foreach ($items as &$item) {
$url = $this->index->datasource()->getItemUrl($item);
if (!$url) {
$item->search_api_url = NULL;

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiAlterAddViewedEntity.
*/
/**
* Search API data alteration callback that adds an URL field for all items.
*/
@ -64,7 +69,7 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {
$type = $this->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.

View File

@ -1,15 +1,25 @@
<?php
/**
* Search API data alteration callback that filters out items based on their
* bundle.
* @file
* Contains SearchApiAlterBundleFilter.
*/
/**
* Represents a data alteration that restricts entity indexes to some bundles.
*/
class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
/**
* {@inheritdoc}
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->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']);

View File

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains the SearchApiAlterCommentAccess class.
*/
/**
* Adds node access information to comment indexes.
*/
class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess {
/**
* Overrides SearchApiAlterNodeAccess::supportsIndex().
*
* Returns TRUE only for indexes on comments.
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->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);
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiAlterLanguageControl.
*/
/**
* Search API data alteration callback that filters out items based on their
* bundle.
@ -7,12 +12,7 @@
class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
/**
* Construct a data-alter callback.
*
* @param SearchApiIndex $index
* The index whose items will be altered.
* @param array $options
* The callback options set for this index.
* {@inheritdoc}
*/
public function __construct(SearchApiIndex $index, array $options = array()) {
$options += array(
@ -23,16 +23,10 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
}
/**
* Check whether this data-alter callback is applicable for a certain index.
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* Only returns TRUE if the system is multilingual.
*
* @param SearchApiIndex $index
* The index to check for.
*
* @return boolean
* TRUE if the callback can run on the given index; FALSE otherwise.
*
* @see drupal_multilingual()
*/
public function supportsIndex(SearchApiIndex $index) {
@ -40,10 +34,7 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
}
/**
* Display a form for configuring this data alteration.
*
* @return array
* A form array for configuring this data alteration.
* {@inheritdoc}
*/
public function configurationForm() {
$form = array();
@ -98,19 +89,7 @@ class SearchApiAlterLanguageControl 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) {
$values['languages'] = array_filter($values['languages']);
@ -118,15 +97,7 @@ class SearchApiAlterLanguageControl 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) {
foreach ($items as $i => &$item) {

View File

@ -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.

View File

@ -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) {

View File

@ -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));

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiException.
*/
/**
* Represents an exception or error that occurred in some part of the Search API
* framework.

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiIndex.
*/
/**
* Class representing a search index.
*/
@ -178,18 +183,9 @@ class SearchApiIndex extends Entity {
if ($this->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);

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiProcessorInterface and SearchApiAbstractProcessor.
*/
/**
* Interface representing a Search API pre- and/or post-processor.
*
@ -27,7 +32,7 @@ interface SearchApiProcessorInterface {
/**
* Check whether this processor is applicable for a certain index.
*
* This can be used for hiding the processor on the index's "Workflow" tab. To
* This can be used for hiding the processor on the index's "Filters" tab. To
* avoid confusion, you should only use criteria that are immutable, such as
* the index's item type. Also, since this is only used for UI purposes, you
* should not completely rely on this to ensure certain index configurations

View File

@ -150,8 +150,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
/**
* Retrieves the fulltext data of a result.
*
* @param array $result
* All results returned in the search.
* @param array $results
* All results returned in the search, by reference.
* @param int|string $i
* The index in the results array of the result whose data should be
* returned.
@ -164,11 +164,12 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* contained in them for the given result.
*/
protected function getFulltextFields(array &$results, $i, $load = TRUE) {
global $language;
$data = array();
// Act as if $load is TRUE if we have a loaded item.
$load |= !empty($result['entity']);
$result = &$results[$i];
// Act as if $load is TRUE if we have a loaded item.
$load |= !empty($result['entity']);
$result += array('fields' => array());
$fulltext_fields = $this->index->getFulltextFields();
// We only need detailed fields data if $load is TRUE.
@ -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);
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiHtmlFilter.
*/
/**
* Processor for stripping HTML from indexed fulltext data. Supports assigning
* custom boosts for any HTML element.

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiIgnoreCase.
*/
/**
* Processor for making searches case-insensitive.
*/

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiStopWords.
*/
/**
* Processor for removing stopwords from index and search terms.
*/
@ -21,8 +26,7 @@ class SearchApiStopWords extends SearchApiAbstractProcessor {
),
'file' => 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 <code>public://stopwords/stopwords.txt</code> or <code>http://example.com/stopwords.txt</code> or <code>private://stopwords.txt</code>.'),
),
'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;
}
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiTokenizer.
*/
/**
* Processor for tokenizing fulltext data by replacing (configurable)
* non-letters with spaces.

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiTransliteration.
*/
/**
* Processor for making searches insensitive to accents and other non-ASCII characters.
*/

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiQueryInterface and SearchApiQuery.
*/
/**
* Interface representing a search query on an Search API index.
*
@ -33,6 +38,10 @@ interface SearchApiQueryInterface {
* implementation to use.
* - 'search id': A string that will be used as the identifier when storing
* this search in the Search API's static cache.
* - 'skip result count': If present and set to TRUE, the result's
* "result count" key will not be needed. Service classes can check for
* this option to possibly avoid executing expensive operations to compute
* the result count in cases where it is not needed.
* - search_api_access_account: The account which will be used for entity
* access checks, if available and enabled for the index.
* - search_api_bypass_access: If set to TRUE, entity access checks will be
@ -62,11 +71,15 @@ interface SearchApiQueryInterface {
*
* @param string $conjunction
* The conjunction to use for the filter - either 'AND' or 'OR'.
* @param $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.
*
* @return SearchApiQueryFilterInterface
* A filter object that is set to use the specified conjunction.
*/
public function createFilter($conjunction = 'AND');
public function createFilter($conjunction = 'AND', $tags = array());
/**
* Sets the keys to search for.
@ -175,7 +188,9 @@ interface SearchApiQueryInterface {
* An associative array containing the search results. The following keys
* are standardized:
* - 'result count': The overall number of results for this query, without
* range restrictions. Might be approximated, for large numbers.
* range restrictions. Might be approximated, for large numbers, or
* skipped entirely if the "skip result count" option was set on this
* query.
* - results: An array of results, ordered as specified. The array keys are
* the items' IDs, values are arrays containing the following keys:
* - id: The item's ID.
@ -318,7 +333,8 @@ interface SearchApiQueryInterface {
* @param mixed $value
* The new value of the option.
*
* @return The option's previous value.
* @return mixed
* The option's previous value.
*/
public function setOption($name, $value);
@ -341,12 +357,21 @@ interface SearchApiQueryInterface {
class SearchApiQuery implements SearchApiQueryInterface {
/**
* The index.
* The index this query will use.
*
* @var SearchApiIndex
*/
protected $index;
/**
* The index's machine name.
*
* used during serialization to avoid serializing the whole index object.
*
* @var string
*/
protected $index_id;
/**
* The search keys. If NULL, this will be a filter-only search.
*
@ -503,9 +528,9 @@ class SearchApiQuery implements SearchApiQueryInterface {
/**
* {@inheritdoc}
*/
public function createFilter($conjunction = 'AND') {
public function createFilter($conjunction = 'AND', $tags = array()) {
$filter_class = $this->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;
}
}
}
}

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Contains SearchApiServer.
*/
/**
* Class representing a search server.
*
@ -82,7 +87,7 @@ class SearchApiServer 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.
*/
@ -136,6 +141,8 @@ class SearchApiServer extends Entity {
}
/**
* Reacts to calls of undefined methods on this object.
*
* If the service class defines additional methods, not specified in the
* SearchApiServiceInterface interface, then they are called via this magic
* method.
@ -148,81 +155,242 @@ class SearchApiServer extends Entity {
// Proxy methods
// For increased clarity, and since some parameters are passed by reference,
// we don't use the __call() magic method for those.
// we don't use the __call() magic method for those. This also gives us the
// opportunity to do additional error handling.
/**
* Form constructor for the server configuration form.
*
* @see SearchApiServiceInterface::configurationForm()
*/
public function configurationForm(array $form, array &$form_state) {
$this->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();
}
}

View File

@ -1,10 +1,20 @@
<?php
/**
* @file
* Contains SearchApiServiceInterface and SearchApiAbstractService.
*/
/**
* Interface defining the methods search services have to implement.
*
* Before a service object is used, the corresponding server's data will be read
* from the database (see SearchApiAbstractService for a list of fields).
*
* Most methods in this interface (where any change in data occurs) can throw a
* SearchApiException. The server entity class SearchApiServer catches these
* exceptions and uses the server tasks system to assure that the action is
* later retried.
*/
interface SearchApiServiceInterface {
@ -19,8 +29,15 @@ interface SearchApiServiceInterface {
public function __construct(SearchApiServer $server);
/**
* Form callback. Might be called on an uninitialized object - in this case,
* the form is for configuring a newly created server.
* Form constructor for the server configuration form.
*
* Might be called with an incomplete server (no ID). In this case, the form
* is displayed for the initial creation of the server.
*
* @param array $form
* The server options part of the form.
* @param array $form_state
* The current form state.
*
* @return array
* A form array for setting service-specific options.
@ -81,29 +98,36 @@ interface SearchApiServiceInterface {
public function supportsFeature($feature);
/**
* View this server's settings. Output can be HTML or a render array, a <dl>
* listing all relevant settings is preferred.
* Displays this server's settings.
*
* Output can be HTML or a render array, a <dl> 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 ? "<dl>\n$output</dl>" : '';
}
/**
* 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().
*

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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 '<li class="dropbutton-toggle"><button type="button" role="button"><span class="dropbutton-arrow"><span class="visually-hidden">' + options.title + '</span></span></button></li>';
}
});
// Expose constructor in the public space.
Drupal.DropButton = DropButton;
})(jQuery);

View File

@ -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.
*

View File

@ -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.
*/

View File

@ -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"

View File

@ -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();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,66 @@
<?php
/**
* Class for testing Search API web functionality.
* @file
* Contains the SearchApiWebTest and the SearchApiUnitTest classes.
*/
/**
* Class for testing Search API functionality via the UI.
*/
class SearchApiWebTest extends DrupalWebTestCase {
/**
* The machine name of the created test server.
*
* @var string
*/
protected $server_id;
/**
* The machine name of the created test index.
*
* @var string
*/
protected $index_id;
/**
* Overrides DrupalWebTestCase::assertText().
*
* Changes the default message to be just the text checked for.
*/
protected function assertText($text, $message = '', $group = 'Other') {
return parent::assertText($text, $message ? $message : $text, $group);
}
/**
* Overrides DrupalWebTestCase::drupalGet().
*
* Additionally asserts that the HTTP request returned a 200 status code.
*/
protected function drupalGet($path, array $options = array(), array $headers = array()) {
$ret = parent::drupalGet($path, $options, $headers);
$this->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 = <<<END
This is <em lang="en" title =
"something">a test</em>.

View File

@ -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"

View File

@ -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'),

View File

@ -1,5 +1,10 @@
<?php
/**
* @file
* Test functions and classes for testing the Search API.
*/
/**
* Implements hook_menu().
*/
@ -11,15 +16,21 @@ function search_api_test_menu() {
'page arguments' => 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'] . '<br/>';
$ret .= 'results = (' . (empty($result['results']) ? '' : implode(', ', array_keys($result['results']))) . ')<br/>';
$ret .= 'warnings = (' . (empty($result['warnings']) ? '' : '"' . implode('", "', $result['warnings']) . '"') . ')<br/>';
$ret .= 'ignored = (' . (empty($result['ignored']) ? '' : implode(', ', $result['ignored'])) . ')<br/>';
$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<decimal>',
'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();
}
}
}