Mercurial > hg > solrsearch
diff solrsearch.module @ 0:a2b4f67e73dc default tip
initial
author | Dirk Wintergruen <dwinter@mpiwg-berlin.mpg.de> |
---|---|
date | Mon, 08 Jun 2015 10:21:54 +0200 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.module Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,2788 @@ +<?php + +/** + * @file + * Integration with the Solr Search search application. + */ + +define('solrsearch_READ_WRITE', 0); +define('solrsearch_READ_ONLY', 1); +define('solrsearch_API_VERSION', '3.0'); + +/** + * Implements hook_init(). + */ +function solrsearch_init() { + if (arg(0) == 'admin') { + // Add the CSS for this module + drupal_add_css(drupal_get_path('module', 'solrsearch') . '/solrsearch.css'); + } +} + +/** + * Implements hook_menu(). + */ +function solrsearch_menu() { + //$items = array(); + + $items['solrsearch-terms-change'] =array( + 'page callback' => 'solrsearch_term_select_field', + 'access arguments' => array('access content'), + 'file' => 'solrsearch_terms.inc', + ); + + $items['solrsearch-terms'] =array( + 'page callback' => 'solrsearch_term_list', + 'access arguments' => array('access content'), + 'file' => 'solrsearch_terms.inc', + ); + $items['solrsearch/node'] = array( + 'title' => 'Search', + 'page callback' => 'solrsearch_view', + 'access arguments' => array('access content'), + //'access callback' => 'solrsearch_is_active', + 'type' => MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch.pages.inc', + ); + $items['solrsearch/site'] = array( + 'title' => 'Search', + 'page callback' => 'solrsearch_view', + 'access arguments' => array('access content'), + //'access callback' => 'solrsearch_is_active', + 'type' => MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch.pages.inc', + ); + $items['solrsearch'] = array( + 'title' => 'Search', + 'page callback' => 'solrsearch_view', + 'access arguments' => array('access content'), + //'access callback' => 'solrsearch_is_active', + 'type' => MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch.pages.inc', + ); + + $items['solrsearchsimple'] = array( + 'title' => 'Simple Search', + 'page callback' => 'solrsearch_view', + 'access arguments' => array('access content'), + //'access callback' => 'solrsearch_is_active', + 'type' => MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch.pages.inc', + ); + + + $items['solrsearchsave'] = array( + 'title' => 'Search', + 'page callback' => 'solrsearch_save', + 'access arguments' => array('access content'), + //'access callback' => 'solrsearch_is_active', + 'type' => MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch.pages.inc', + ); + $items['admin/config/search/solrsearch'] = array( + 'title' => 'Solr Search search', + 'description' => 'Administer Solr Search.', + 'page callback' => 'solrsearch_status_page', + 'access arguments' => array('administer search'), + 'weight' => -8, + 'file' => 'solrsearch.admin.inc', + ); + + + $items['admin/config/search/solrsearch/settings'] = array( + 'title' => 'Settings', + 'weight' => 10, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_settings'), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + + $settings_path = 'admin/config/search/solrsearch/settings/'; + + $items[$settings_path . '%solrsearch_environment/edit'] = array( + 'title' => 'Edit', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_environment_edit_form', 5), + 'description' => 'Edit Solr Search search environment.', + 'access arguments' => array('administer search'), + 'weight' => 10, + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + $items[$settings_path . '%solrsearch_environment/clone'] = array( + 'title' => 'Solr Search search environment clone', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_environment_clone_form', 5), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch.admin.inc', + ); + $items[$settings_path . '%solrsearch_environment/delete'] = array( + 'title' => 'Solr Search search environment delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_environment_delete_form', 5), + 'access callback' => 'solrsearch_environment_delete_page_access', + 'access arguments' => array('administer search', 5), + 'file' => 'solrsearch.admin.inc', + ); + $items[$settings_path . 'add'] = array( + 'title' => 'Add search environment', + 'description' => 'Add Solr Search environment.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_environment_edit_form'), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_LOCAL_ACTION, + ); + + + $reports_path = 'admin/reports/solrsearch'; + $items[$reports_path] = array( + 'title' => 'Solr Search search index', + 'description' => 'Information about the contents of the index on the server', + 'page callback' => 'solrsearch_index_report', + 'access arguments' => array('access site reports'), + 'file' => 'solrsearch.admin.inc', + ); + $items[$reports_path . '/%solrsearch_environment'] = array( + 'title' => 'Solr Search search index', + 'description' => 'Information about the contents of the index on the server', + 'page callback' => 'solrsearch_index_report', + 'page arguments' => array(3), + 'access arguments' => array('access site reports'), + 'file' => 'solrsearch.admin.inc', + ); + $items[$reports_path . '/%solrsearch_environment/index'] = array( + 'title' => 'Search index', + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items[$reports_path . '/%solrsearch_environment/conf'] = array( + 'title' => 'Configuration files', + 'page callback' => 'solrsearch_config_files_overview', + 'access arguments' => array('access site reports'), + 'file' => 'solrsearch.admin.inc', + 'weight' => 5, + 'type' => MENU_LOCAL_TASK, + ); + $items[$reports_path . '/%solrsearch_environment/conf/%'] = array( + 'title' => 'Configuration file', + 'page callback' => 'solrsearch_config_file', + 'page arguments' => array(5, 3), + 'access arguments' => array('access site reports'), + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_CALLBACK, + ); + if (module_exists('devel')) { + $items['node/%node/devel/solrsearch'] = array( + 'title' => 'Solr Search', + 'page callback' => 'solrsearch_devel', + 'page arguments' => array(1), + 'access arguments' => array('access devel information'), + 'file' => 'solrsearch.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + } + + // We handle our own menu paths for facets + if (module_exists('facetapi')) { + $file_path = drupal_get_path('module', 'facetapi'); + $first = TRUE; + foreach (facetapi_get_realm_info() as $realm_name => $realm) { + if ($first) { + $first = FALSE; + $items[$settings_path . '%solrsearch_environment/facets'] = array( + 'title' => 'Facets', + 'page callback' => 'solrsearch_enabled_facets_page', + 'page arguments' => array($realm_name, 5), + 'weight' => -5, + 'access arguments' => array('administer search'), + 'file path' => $file_path, + 'file' => 'facetapi.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + ); + } + else { + $items[$settings_path . '%solrsearch_environment/facets/' . $realm_name] = array( + 'title' => $realm['label'], + 'page callback' => 'solrsearch_enabled_facets_page', + 'page arguments' => array($realm_name, 5), + 'weight' => -5, + 'access arguments' => array('administer search'), + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'file path' => $file_path, + 'file' => 'facetapi.admin.inc', + ); + } + } + } + return $items; +} + +/** + * Wrapper for facetapi settings forms. + */ +function solrsearch_enabled_facets_page($realm_name, $environment = NULL) { + $page = array(); + + if (isset($environment['env_id'])) { + $env_id = $environment['env_id']; + } + else { + $env_id = solrsearch_default_environment(); + } + $searcher = 'solrsearch@' . $env_id; + + // Initializes output with information about which environment's setting we are + // editing, as it is otherwise not transparent to the end user. + $page['solrsearch_environment'] = array( + '#theme' => 'solrsearch_settings_title', + '#env_id' => $env_id, + ); + + $page['settings'] = drupal_get_form('facetapi_realm_settings_form', $searcher, $realm_name); + return $page; +} + +/** + * Implements hook_facetapi_searcher_info(). + */ +function solrsearch_facetapi_searcher_info() { + $info = array(); + // TODO: is it needed to return all of them here? + foreach (solrsearch_load_all_environments() as $id => $environment) { + $info['solrsearch@' . $id] = array( + 'label' => t('Solr Search environment: @environment', array('@environment' => $environment['name'])), + 'adapter' => 'solrsearch', + 'instance' => $id, + 'path' => '', + 'supports facet mincount' => TRUE, + 'supports facet missing' => TRUE, + 'include default facets' => FALSE, + ); + } + return $info; +} + +/** + * Implements hook_facetapi_adapters(). + */ +function solrsearch_facetapi_adapters() { + return array( + 'solrsearch' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiAdapter', + ), + ), + ); +} + +/** + * Implements hook_facetapi_query_types(). + */ +function solrsearch_facetapi_query_types() { + return array( + 'solrsearch_term' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiTerm', + 'adapter' => 'solrsearch', + ), + ), + 'solrsearch_date' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiDate', + 'adapter' => 'solrsearch', + ), + ), + 'solrsearch_numeric_range' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiNumericRange', + 'adapter' => 'solrsearch', + ), + ), + 'solrsearch_integer' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiInteger', + 'adapter' => 'solrsearch', + ), + ), + 'solrsearch_geo' => array( + 'handler' => array( + 'class' => 'solrsearchFacetapiGeo', + 'adapter' => 'solrsearch', + ), + ), + ); +} + +/** + * Implements hook_facetapi_facet_info(). + * Currently it only supports the node entity type + */ +function solrsearch_facetapi_facet_info($searcher_info) { + $facets = array(); + //dpm("sorlsearch_facetapi"); + if ('solrsearch' == $searcher_info['adapter']) { + $environment = solrsearch_environment_load($searcher_info['instance']); + + if (!empty($environment['conf']['facet callbacks'])) { + foreach ($environment['conf']['facet callbacks'] as $callback) { + if (is_callable($callback)) { + $facets = array_merge($facets, call_user_func($callback, $searcher_info)); + } + } + } + elseif (isset($searcher_info['types']['node'])) { + $facets = solrsearch_default_node_facet_info(); + } + } + + return $facets; +} + +/** + * Returns an array of facets for node fields and attributes. + * + * @return + * An array of node facets. + */ +function solrsearch_default_node_facet_info() { + //dpm("sorlsearch_default_facetapi"); + return array_merge(solrsearch_common_node_facets(), solrsearch_entity_field_facets('node')); +} + +/** + * Returns an array of facets for the provided entity type's fields. + * + * @param string $entity_type + * An entity type machine name. + * @return + * An array of facets for the fields of the requested entity type. + */ +function solrsearch_entity_field_facets($entity_type) { + $facets = array(); + + foreach (solrsearch_entity_fields($entity_type) as $field_nm => $entity_fields) { + foreach ($entity_fields as $field_info) { + if (!empty($field_info['facets'])) { + $field = solrsearch_index_key($field_info); + $facets[$field] = array( + 'label' => check_plain($field_info['display_name']), + 'dependency plugins' => $field_info['dependency plugins'], + 'field api name' => $field_info['field']['field_name'], + 'description' => t('Filter by field of type @type.', array('@type' => $field_info['field']['type'])), + 'map callback' => $field_info['map callback'], + 'map options' => $field_info, + 'hierarchy callback' => $field_info['hierarchy callback'], + ); + if (!empty($field_info['facet mincount allowed'])) { + $facets[$field]['facet mincount allowed'] = $field_info['facet mincount allowed']; + } + if (!empty($field_info['facet missing allowed'])) { + $facets[$field]['facet missing allowed'] = $field_info['facet missing allowed']; + } + if (!empty($field_info['query types'])) { + $facets[$field]['query types'] = $field_info['query types']; + } + if (!empty($field_info['allowed operators'])) { + $facets[$field]['allowed operators'] = $field_info['allowed operators']; + } + // TODO : This is actually deprecated but we should still support + // older versions of facetapi. We should remove once facetapi has RC1 + // For reference : http://drupal.org/node/1161444 + if (!empty($field_info['query type'])) { + $facets[$field]['query type'] = $field_info['query type']; + } + if (!empty($field_info['min callback'])) { + $facets[$field]['min callback'] = $field_info['min callback']; + } + if (!empty($field_info['max callback'])) { + $facets[$field]['max callback'] = $field_info['max callback']; + } + if (!empty($field_info['map callback'])) { + $facets[$field]['map callback'] = $field_info['map callback']; + } + if (!empty($field_info['alter callbacks'])) { + $facets[$field]['alter callbacks'] = $field_info['alter callbacks']; + } + } + } + } + + return $facets; +} + +/** + * Helper function returning common facet definitions. + */ +function solrsearch_common_node_facets() { + + + + $facets['author'] = array( + 'label' => t('Author'), + 'description' => t('Filter by author.'), + 'field' => 'author_c', + 'facet mincount allowed' => TRUE, + + ); + + + $facets['title'] = array( + 'label' => t('Title'), + 'description' => t('Filter by title.'), + 'field' => 'title_s', + 'facet mincount allowed' => TRUE, + ); + + + $facets['doc-type'] = array( + 'label' => t('Type of item'), + 'description' => t('Filter by the type of item (field: doc-type).'), + 'field' => 'doc-type', + 'facet mincount allowed' => TRUE, + 'map callback' => 'solrsearch_map_doc_type', + ); + + $facets['access-type'] = array( + 'label' => t('Access restrictions'), + 'description' => t('Filter by Access Type.'), + 'field' => 'access-type', + 'facet mincount allowed' => TRUE, + ); + + + $facets['collection'] = array( + 'label' => t('Filter by collection'), + 'description' => t('Filter by Collection (field:collection).'), + 'field' => 'collection', + 'facet mincount allowed' => TRUE, + ); + + $facets['provider'] = array( + 'label' => t('Filter by provider'), + 'description' => t('Filter by Provider.'), + 'field' => 'provider', + 'facet mincount allowed' => TRUE, + ); + + $facets['data_provider'] = array( + 'label' => t('Filter by data provider'), + 'description' => t('Filter by Data provider .'), + 'field' => 'data_provider', + 'facet mincount allowed' => TRUE, + ); + + + $facets['type'] = array( + 'label' => t('Filter bytype'), + 'description' => t('Filter by type .'), + 'field' => 'type', + 'facet mincount allowed' => TRUE, + ); + + + $facets['year'] = array( + 'label' => t('Year'), + 'description' => t('Filter by the year.'), + 'field' => 'year', + 'query types' => array('integer'), + 'allowed operators' => array(FACETAPI_OPERATOR_AND => TRUE), + 'min callback' => 'solrsearch_get_min_integer', + 'max callback' => 'solrsearch_get_max_integer', + 'gap callback' => 'solrsearch_get_gap_integer', + 'default sorts' => array( + array('active', SORT_DESC), + array('indexed', SORT_ASC), + ), + ); + + + //dpm($facets); + return $facets; +} + + +/* + * solrsearch_map_doc_type + * map doc type to human readable + */ + +function solrsearch_map_doc_type($facets, $options) { + $map = array(); + $allowed_values = array(); + // @see list_field_formatter_view() + + $mapping = array( + 'institutesLibrary' => 'Printed Catalog', + 'indexMeta' => 'Digital Library', + 'echo2_collection' => 'Collections (ECHO)', + ); + foreach ($facets as $key) { + + if (isset( $mapping[$key])) { + $map[$key]['#markup'] = $mapping[$key]; + } else { + $map[$key]['#markup'] = $key; + } + + $map[$key]['#html'] = TRUE; + } + return $map; +} +/** + * Callback that returns the gap + * + * @param $facet + * An array containing the facet definition. + * + * @return + * + * + * @todo Cache this value. + */ +function solrsearch_get_gap_integer(array $facet) { + //TODO: should be dynamic or configurable + return 20; +} + +/** + * Callback that returns the minimum integer + * + * @param $facet + * An array containing the facet definition. + * + * @return + * + * + * @todo Cache this value. + */ +function solrsearch_get_min_integer(array $facet) { + //TODO: should be dynamic or configurable + return 1; +} + +/** + * Callback that returns the maximum integer + * + * @param $facet + * An array containing the facet definition. + * + * @return + * + * + * @todo Cache this value. + */ +function solrsearch_get_max_integer(array $facet) { + //TODO: should be dynamic or configurable + return 2020; +} + + +/** + * Determines Solr Search's behavior when searching causes an exception (e.g. Solr isn't available.) + * Depending on the admin settings, possibly redirect to Drupal's core search. + * + * @param $search_name + * The name of the search implementation. + * + * @param $querystring + * The search query that was issued at the time of failure. + */ +function solrsearch_failure($search_name, $querystring) { + $fail_rule = variable_get('solrsearch_failure', 'solrsearch:show_error'); + + switch ($fail_rule) { + case 'solrsearch:show_error': + // drupal_set_message(t('Search is temporarily unavailable. If the problem persists, please contact the site administrator.'), 'error'); + break; + case 'solrsearch:show_no_results': + // Do nothing. + break; + default: + // If we're failing over to another module make sure the search is available. + if (module_exists('search')) { + $search_info = search_get_info(); + if (isset($search_info[$fail_rule])) { + $search_info = $search_info[$fail_rule]; + drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name))); + drupal_goto('search/' . $search_info['path'] . '/' . rawurlencode($querystring)); + } + } + // if search is not enabled, break and do nothing + break; + } +} + +/** + * Like $site_key in _update_refresh() - returns a site-specific hash. + */ +function solrsearch_site_hash() { + if (!($hash = variable_get('solrsearch_site_hash', FALSE))) { + global $base_url; + // Set a random 6 digit base-36 number as the hash. + $hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6); + variable_set('solrsearch_site_hash', $hash); + } + return $hash; +} + +/** + * Generate a unique ID for an entity being indexed. + * + * @param $id + * An id number (or string) unique to this site, such as a node ID. + * @param $entity + * A string like 'node', 'file', 'user', or some other Drupal object type. + * + * @return + * A string combining the parameters with the site hash. + */ +function solrsearch_document_id($id, $entity_type = 'node') { + return solrsearch_site_hash() . "/{$entity_type}/" . $id; +} + +/** + * Mark one entity as needing re-indexing. + */ +function solrsearch_mark_entity($entity_type, $entity_id) { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $table = solrsearch_get_indexer_table($entity_type); + if (!empty($table)) { + db_update($table) + ->condition('entity_id', $entity_id) + ->fields(array('changed' => REQUEST_TIME)) + ->execute(); + } +} + +/** + * Implements hook_user_update(). + * + * Mark nodes as needing re-indexing if the author name changes. + * + * @see http://drupal.org/node/592522 + * Performance issue with Mysql + * @see http://api.drupal.org/api/drupal/includes--database--database.inc/function/db_update/7#comment-15459 + * To know why PDO in drupal does not support UPDATE and JOIN at once. + */ +function solrsearch_user_update(&$edit, $account, $category) { + if (isset($account->name) && isset($account->original) && isset($account->original->name) && $account->name != $account->original->name) { + $table = solrsearch_get_indexer_table('node'); + switch (db_driver()) { + case 'mysql' : + $table = db_escape_table($table); + $query = "UPDATE {{$table}} asn + INNER JOIN {node} n ON asn.entity_id = n.nid SET asn.changed = :changed + WHERE n.uid = :uid"; + $result = db_query($query, array(':changed' => REQUEST_TIME, + ':uid' => $account->uid, + )); + break; + default : + $nids = db_select('node') + ->fields('node', array('nid')) + ->where("uid = :uid", array(':uid' => $account->uid)); + $update = db_update($table) + ->condition('entity_id', $nids, 'IN') + ->fields(array('changed' => REQUEST_TIME)) + ->execute(); + } + } +} + +/** + * Implements hook_term_update(). + * + * Mark nodes as needing re-indexing if a term name changes. + * + * @see http://drupal.org/node/592522 + * Performance issue with Mysql + * @see http://api.drupal.org/api/drupal/includes--database--database.inc/function/db_update/7#comment-15459 + * To know why PDO in drupal does not support UPDATE and JOIN at once. + * @todo the rest, such as term deletion. + */ +function solrsearch_taxonomy_term_update($term) { + $table = solrsearch_get_indexer_table('node'); + switch (db_driver()) { + case 'mysql' : + $table = db_escape_table($table); + $query = "UPDATE {{$table}} asn + INNER JOIN {taxonomy_index} ti ON asn.entity_id = ti.nid SET asn.changed = :changed + WHERE ti.tid = :tid"; + $result = db_query($query, array(':changed' => REQUEST_TIME, + ':tid' => $term->tid, + )); + break; + default : + $nids = db_select('taxonomy_index') + ->fields('taxonomy_index', array('nid')) + ->where("tid = :tid", array(':tid' => $term->tid)); + $update = db_update($table) + ->condition('entity_id', $nids, 'IN') + ->fields(array('changed' => REQUEST_TIME)) + ->execute(); + } +} + + + +/** + * Convert date from timestamp into ISO 8601 format. + * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html + */ +function solrsearch_date_iso($date_timestamp) { + return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp); +} + +/** + * Function to flatten documents array recursively. + * + * @param array $documents + * The array of documents being indexed. + * @param array &$tmp + * A container variable that will contain the flattened array. + */ +function solrsearch_flatten_documents_array($documents, &$tmp) { + foreach ($documents AS $index => $item) { + if (is_array($item)) { + solrsearch_flatten_documents_array($item, $tmp); + } + elseif (is_object($item)) { + $tmp[] = $item; + } + } +} + +/** + * Implements hook_flush_caches(). + */ +function solrsearch_flush_caches() { + return array('cache_solrsearch'); +} + +/** + * A wrapper for cache_clear_all to be used as a submit handler on forms that + * require clearing Luke cache etc. + */ +function solrsearch_clear_cache($env_id) { + // Reset $env_id to NULL if call originates from a form submit handler. + if (is_array($env_id)) { + $env_id = NULL; + } + try { + $solr = solrsearch_get_solr($env_id); + $solr->clearCache(); + } + catch (Exception $e) { + watchdog('Solr Search', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), 'warning'); + } +} + +/** + * Call drupal_set_message() with the text. + * + * The text is translated with t() and substituted using Solr stats. + * @todo This is not according to drupal code standards + */ +function solrsearch_set_stats_message($text, $type = 'status', $repeat = FALSE) { + try { + $solr = solrsearch_get_solr(); + $stats_summary = $solr->getStatsSummary(); + drupal_set_message(check_plain(t($text, $stats_summary)), $type, FALSE); + } + catch (Exception $e) { + watchdog('Solr Search', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } +} + +/** + * Returns last changed and last ID for an environment and entity type. + */ +function solrsearch_get_last_index_position($env_id, $entity_type) { + $stored = variable_get('solrsearch_index_last', array()); + return isset($stored[$env_id][$entity_type]) ? $stored[$env_id][$entity_type] : array('last_changed' => 0, 'last_entity_id' => 0); +} + +/** + * Sets last changed and last ID for an environment and entity type. + */ +function solrsearch_set_last_index_position($env_id, $entity_type, $last_changed, $last_entity_id) { + $stored = variable_get('solrsearch_index_last', array()); + $stored[$env_id][$entity_type] = array('last_changed' => $last_changed, 'last_entity_id' => $last_entity_id); + variable_set('solrsearch_index_last', $stored); +} + +/** + * Clear a specific environment, or clear all. + */ +function solrsearch_clear_last_index_position($env_id = NULL, $entity_type = NULL) { + $stored = variable_get('solrsearch_index_last', array()); + if (empty($env_id)) { + $stored = array(); + } + elseif ($entity_type) { + unset($stored[$env_id][$entity_type]); + } + else { + unset($stored[$env_id]); + } + variable_set('solrsearch_index_last', $stored); +} + +/** + * Set the timestamp of the last index update + * @param $timestamp + * A timestamp or zero. If zero, the variable is deleted. + */ +function solrsearch_set_last_index_updated($env_id, $timestamp = 0) { + $updated = variable_get('solrsearch_index_updated', array()); + if ($timestamp > 0) { + $updated[$env_id] = $timestamp; + } + else { + unset($updated[$env_id]); + } + variable_set('solrsearch_index_updated', $updated); +} + +/** + * Get the timestamp of the last index update. + * @return integer (timestamp) + */ +function solrsearch_get_last_index_updated($env_id) { + $updated = variable_get('solrsearch_index_updated', array()); + return isset($updated[$env_id]) ? $updated[$env_id] : 0; +} + + + +/** + * Implements hook_form_[form_id]_alter(). + * + * Make sure to flush cache when content types are changed. + */ +function solrsearch_form_node_type_form_alter(&$form, $form_state) { + $form['#submit'][] = 'solrsearch_clear_cache'; +} + +/** + * Implements hook_form_[form_id]_alter(). (D7) + * + * Make sure to flush cache when fields are added. + */ +function solrsearch_form_field_ui_field_overview_form_alter(&$form, $form_state) { + $form['#submit'][] = 'solrsearch_clear_cache'; +} + +/** + * Implements hook_form_[form_id]_alter(). (D7) + * + * Make sure to flush cache when fields are updated. + */ +function solrsearch_form_field_ui_field_edit_form_alter(&$form, $form_state) { + $form['#submit'][] = 'solrsearch_clear_cache'; +} + +/** + * Sets breadcrumb trails for Facet API settings forms. + * + * @param FacetapiAdapter $adapter + * The Facet API adapter object. + * @param array $realm + * The realm definition. + */ +function solrsearch_set_facetapi_breadcrumb(FacetapiAdapter $adapter, array $realm) { + if ('solrsearch' == $adapter->getId()) { + // Hack here that depnds on our construction of the searcher name in this way. + list(, $env_id) = explode('@', $adapter->getSearcher()); + // Appends additional breadcrumb items. + $breadcrumb = drupal_get_breadcrumb(); + $breadcrumb[] = l(t('Solr Search search environment edit'), 'admin/config/search/solrsearch/settings/' . $env_id); + $breadcrumb[] = l($realm['label'], 'admin/config/search/solrsearch/settings/' . $env_id . '/facets/' . $realm['name']); + drupal_set_breadcrumb($breadcrumb); + } +} + +/** + * Implements hook_form_[form_id]_alter(). (D7) + */ +function solrsearch_form_facetapi_facet_settings_form_alter(&$form, $form_state) { + solrsearch_set_facetapi_breadcrumb($form['#facetapi']['adapter'], $form['#facetapi']['realm']); +} + +/** + * Implements hook_form_[form_id]_alter(). (D7) + */ +function solrsearch_form_facetapi_facet_dependencies_form_alter(&$form, $form_state) { + solrsearch_set_facetapi_breadcrumb($form['#facetapi']['adapter'], $form['#facetapi']['realm']); +} + +/** + * Semaphore that indicates whether a search has been done. Blocks use this + * later to decide whether they should load or not. + * + * @param $searched + * A boolean indicating whether a search has been executed. + * + * @return + * TRUE if a search has been executed. + * FALSE otherwise. + */ +function solrsearch_has_searched($env_id, $searched = NULL) { + $_searched = &drupal_static(__FUNCTION__, FALSE); + if (is_bool($searched)) { + $_searched[$env_id] = $searched; + } + // Return false if the search environment is not available in our array + if (!isset($_searched[$env_id])) { + return FALSE; + } + return $_searched[$env_id]; +} + +/** + * Semaphore that indicates whether Blocks should be suppressed regardless + * of whether a search has run. + * + * @param $suppress + * A boolean indicating whether to suppress. + * + * @return + * TRUE if a search has been executed. + * FALSE otherwise. + */ +function solrsearch_suppress_blocks($env_id, $suppress = NULL) { + $_suppress = &drupal_static(__FUNCTION__, FALSE); + if (is_bool($suppress)) { + $_suppress[$env_id] = $suppress; + } + // Return false if the search environment is not available in our array + if (!isset($_suppress[$env_id])) { + return FALSE; + } + return $_suppress[$env_id]; +} + +/** + * Get or set the default environment ID for the current page. + */ +function solrsearch_default_environment($env_id = NULL) { + $default_env_id = &drupal_static(__FUNCTION__, NULL); + + if (isset($env_id)) { + $default_env_id = $env_id; + } + if (empty($default_env_id)) { + $default_env_id = variable_get('solrsearch_default_environment', 'echosearch'); + } + return $default_env_id; +} + +/** + * Set the default environment and let other modules know about the change. + */ +function solrsearch_set_default_environment($env_id) { + $old_env_id = variable_get('solrsearch_default_environment', 'solr'); + variable_set('solrsearch_default_environment', $env_id); + module_invoke_all('solrsearch_default_environment', $env_id, $old_env_id); +} + +/** + * Factory method for solr singleton objects. Structure allows for an arbitrary + * number of solr objects to be used based on a name whie maps to + * the host, port, path combination. + * Get an instance like this: + * try { + * $solr = solrsearch_get_solr(); + * } + * catch (Exception $e) { + * watchdog('Solr Search', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + * } + * + * + * @param string $env_id + * + * @return DrupalApacheSolrServiceInterface $solr + * + * @throws Exception + */ +function solrsearch_get_solr($env_id = NULL) { + $solr_cache = &drupal_static(__FUNCTION__); + $environments = solrsearch_load_all_environments(); + + if (!interface_exists('DrupalApacheSolrServiceInterface')) { + require_once(dirname(__FILE__) . '/solrsearch.interface.inc'); + } + + if (empty($env_id)) { + $env_id = solrsearch_default_environment(); + } + elseif (empty($environments[$env_id])) { + throw new Exception(t('Invalid Solr Search environment: @env_id.', array('@env_id' => $env_id))); + } + + if (isset($environments[$env_id])) { + $class = $environments[$env_id]['service_class']; + + if (empty($solr_cache[$env_id])) { + // Use the default class if none is specified. + if (empty($class)) { + $class = variable_get('solrsearch_service_class', 'DrupalsolrsearchService'); + } + // Takes advantage of auto-loading. + $solr = new $class($environments[$env_id]['url'], $env_id); + $solr_cache[$env_id] = $solr; + } + return $solr_cache[$env_id]; + } + else { + throw new Exception('No default Solr Search environment.'); + } +} + +/** + * Function that loads all the environments + * + * @return $environments + * The environments in the database + */ +function solrsearch_load_all_environments() { + $environments = &drupal_static(__FUNCTION__); + + if (isset($environments)) { + return $environments; + } + // Use cache_get to avoid DB when using memcache, etc. + $cache = cache_get('solrsearch:environments', 'cache_solrsearch'); + if (isset($cache->data)) { + $environments = $cache->data; + } + elseif (!db_table_exists('solrsearch_index_bundles') || !db_table_exists('solrsearch_environment')) { + // Sometimes this function is called when the 'solrsearch_index_bundles' is + // not created yet. + $environments = array(); + } + else { + // If ctools is available use its crud functions to load the environments. + if (module_exists('ctools')) { + ctools_include('export'); + $environments = ctools_export_load_object('solrsearch_environment', 'all'); + // Convert environments to array. + foreach ($environments as &$environment) { + $environment = (array) $environment; + } + } + else { + $environments = db_query('SELECT * FROM {solrsearch_environment}')->fetchAllAssoc('env_id', PDO::FETCH_ASSOC); + } + + // Load conf and index bundles. We don't use 'subrecords callback' property + // of ctools export API. + solrsearch_environment_load_subrecords($environments); + + cache_set('solrsearch:environments', $environments, 'cache_solrsearch'); + } + + // Allow overrides of environments from settings.php + $conf_environments = variable_get('solrsearch_environments', array()); + if (!empty($conf_environments)) { + $environments = drupal_array_merge_deep($environments, $conf_environments); + } + + return $environments; +} + +/** + * Function that loads an environment + * + * @param $env_id + * The environment ID it needs to load. + * + * @return $environment + * The environment that was requested or FALSE if non-existent + */ +function solrsearch_environment_load($env_id) { + $environments = solrsearch_load_all_environments(); + return isset($environments[$env_id]) ? $environments[$env_id] : FALSE; +} + +/** + * Access callback for the delete page of an environment. + * + * @param $permission + * The permission that you allow access to + * @param $environment + * The environment you want to delete. Core environment cannot be deleted + */ +function solrsearch_environment_delete_page_access($permission, $environment) { + $is_default = $environment['env_id'] == solrsearch_default_environment(); + if ($is_default && !user_access($permission)) { + return FALSE; + } + return TRUE; +} + +/** + * Function that deletes an environment + * + * @param $env_id + * The environment ID it needs to delete. + * + */ +function solrsearch_environment_delete($env_id) { + $environment = solrsearch_environment_load($env_id); + if ($environment) { + db_delete('solrsearch_environment') + ->condition('env_id', $env_id) + ->execute(); + db_delete('solrsearch_environment_variable') + ->condition('env_id', $env_id) + ->execute(); + db_delete('solrsearch_index_bundles') + ->condition('env_id', $env_id) + ->execute(); + + module_invoke_all('solrsearch_environment_delete', $environment); + solrsearch_environments_clear_cache(); + } +} + +/** + * Function that clones an environment + * + * @param $env_id + * The environment ID it needs to clone. + * + */ +function solrsearch_environment_clone($env_id) { + $environment = solrsearch_environment_load($env_id); + $environments = solrsearch_load_all_environments(); + $environment['env_id'] = solrsearch_create_unique_id($environments, $env_id); + $environment['name'] = $environment['name'] . ' [cloned]'; + solrsearch_environment_save($environment); +} + +/** + * Generator for an unique ID of an environment + * + * @param $environments + * The environments that are available + * @param $original_environment + * The environment it needs to replicate an ID for. + * + * @return + * The new environment ID + */ +function solrsearch_create_unique_id($existing, $id) { + $count = 0; + $cloned_env_int = 0; + do { + $new_id = $id . '_' . $count; + $count++; + } while (isset($existing[$new_id])); + return $new_id; +} + +/** + * Function that saves an environment + * + * @param $environment + * The environment it needs to save. + * + */ +function solrsearch_environment_save($environment) { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $default = array('env_id' => '', 'name' => '', 'url' => '', 'service_class' => ''); + + $conf = isset($environment['conf']) ? $environment['conf'] : array(); + $index_bundles = isset($environment['index_bundles']) ? $environment['index_bundles'] : array(); + // Remove any unexpected fields. + // @todo - get this from the schema?. + $environment = array_intersect_key($environment, $default); + db_merge('solrsearch_environment') + ->key(array('env_id' => $environment['env_id'])) + ->fields($environment) + ->execute(); + // Update the environment variables (if any). + foreach ($conf as $name => $value) { + db_merge('solrsearch_environment_variable') + ->key(array('env_id' => $environment['env_id'], 'name' => $name)) + ->fields(array('value' => serialize($value))) + ->execute(); + } + // Update the index bundles (if any). + foreach ($index_bundles as $entity_type => $bundles) { + solrsearch_index_set_bundles($environment['env_id'], $entity_type, $bundles); + } + solrsearch_environments_clear_cache(); +} + +/** + * Clear all caches for environments. + */ +function solrsearch_environments_clear_cache() { + cache_clear_all('solrsearch:environments', 'cache_solrsearch'); + drupal_static_reset('solrsearch_load_all_environments'); + drupal_static_reset('solrsearch_get_solr'); + if (module_exists('ctools')) { + ctools_include('export'); + ctools_export_load_object_reset('solrsearch_environment'); + } +} + +/** + * Get a named variable, or return the default. + * + * @see variable_get() + */ +function solrsearch_environment_variable_get($env_id, $name, $default = NULL) { + $environment = solrsearch_environment_load($env_id); + if (isset($environment['conf'][$name])) { + return $environment['conf'][$name]; + } + return $default; +} + +/** + * Set a named variable, or return the default. + * + * @see variable_set() + */ +function solrsearch_environment_variable_set($env_id, $name, $value) { + db_merge('solrsearch_environment_variable') + ->key(array('env_id' => $env_id, 'name' => $name)) + ->fields(array('value' => serialize($value))) + ->execute(); + solrsearch_environments_clear_cache(); +} + +/** + * Get a named variable, or return the default. + * + * @see variable_del() + */ +function solrsearch_environment_variable_del($env_id, $name) { + db_delete('solrsearch_environment_variable') + ->condition('env_id', $env_id) + ->condition('name', $name) + ->execute(); + solrsearch_environments_clear_cache(); +} + +/** + * Checks if a specific Solr Search server is available. + * + * @return boolean TRUE if the server can be pinged, FALSE otherwise. + */ +function solrsearch_server_status($url, $class = NULL) { + + $status = &drupal_static(__FUNCTION__, array()); + + if (!interface_exists('DrupalApacheSolrServiceInterface')) { + require_once(dirname(__FILE__) . '/solrsearch.interface.inc'); + } + + if (empty($class)) { + $class = variable_get('solrsearch_service_class', 'DrupalsolrsearchService'); + } + + $key = $url . '|' . $class; + // Static store insures we don't ping the server more than once per page load. + if (!isset($status[$key])) { + $ping = FALSE; + try { + // Takes advantage of auto-loading. + // @Todo : Do we have to specify the env_id? + $solr = new $class($url); + $ping = @$solr->ping(variable_get('solrsearch_ping_timeout', 4)); + } + catch (Exception $e) { + watchdog('Solr Search', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + $status[$key] = $ping; + } + return $status[$key]; +} + +/** + * Execute a keyword search based on a query object. + * + * Normally this function is used with the default (dismax) handler for keyword + * searches. The $final_query that's returned will have been modified by + * both hook_solrsearch_query_prepare() and hook_solrsearch_query_alter(). + * + * @param $current_query + * A query object from solrsearch_drupal_query(). It will be modified by + * hook_solrsearch_query_prepare() and then cached in solrsearch_current_query(). + * @param $page + * For paging into results, using $current_query->params['rows'] results per page. + * + * @return array($final_query, $response) + * + * @throws Exception + */ +function solrsearch_do_query(DrupalSolrQueryInterface $current_query) { + + if (!is_object($current_query)) { + throw new Exception(t('NULL query object in function solrsearch_do_query()')); + } + + // Allow modules to alter the query prior to statically caching it. + // This can e.g. be used to add available sorts. + $searcher = $current_query->getSearcher(); + + if (module_exists('facetapi')) { + + // Gets enabled facets, adds filter queries to $params. + + $adapter = facetapi_adapter_load($searcher); + if ($adapter) { + + // Realm could be added but we want all the facets + $adapter->addActiveFilters($current_query); + } + } + + foreach (module_implements('solrsearch_query_prepare') as $module) { + $function_name = $module . '_solrsearch_query_prepare'; + $function_name($current_query); + } + + // Cache the original query. Since all the built queries go through + // this process, all the hook_invocations will happen later + $env_id = $current_query->solr('getId'); + + // Add our defType setting here. Normally this would be dismax or the setting + // from the solrconfig.xml. This allows the setting to be overridden. + $defType = solrsearch_environment_variable_get($env_id, 'solrsearch_query_type'); + if (!empty($defType)) { + $current_query->addParam('defType', $defType); + } + + $query = solrsearch_current_query($env_id, $current_query); + + // Verify if this query was already executed in the same page load + if ($response = solrsearch_static_response_cache($searcher)) { + // Return cached query object + + return array($query, $response); + } + + $query->addParam('start', $query->page * $query->getParam('rows')); + + // This hook allows modules to modify the query and params objects. + drupal_alter('solrsearch_query', $query); + + if ($query->abort_search) { + // A module implementing HOOK_solrsearch_query_alter() aborted the search. + return array(NULL, array()); + } + + + $keys = $query->getParam('q'); + + + //dont want this because not supported by my search module in drupal dwinter + /* if (strlen($keys) == 0 && ($filters = $query->getFilters())) { */ + /* // Move the fq params to q.alt for better performance. Only suitable */ + /* // when using dismax or edismax, so we keep this out of the query class itself */ + /* // for now. */ + /* $qalt = array(); */ + /* foreach ($filters as $delta => $filter) { */ + /* // Move the fq param if it has no local params and is not negative. */ + /* if (!$filter['#exclude'] && !$filter['#local']) { */ + /* $qalt[] = '(' . $query->makeFilterQuery($filter) . ')'; */ + /* $query->removeFilter($filter['#name'], $filter['#value'], $filter['#exclude']); */ + /* } */ + /* } */ + /* if ($qalt) { */ + /* $query->addParam('q.alt', implode(' ', $qalt)); */ + /* } */ + /* } */ + // We must run htmlspecialchars() here since converted entities are in the index. + // and thus bare entities &, > or < won't match. Single quotes are converted + // too, but not double quotes since the dismax parser looks at them for + // phrase queries. + $keys = htmlspecialchars($keys, ENT_NOQUOTES, 'UTF-8'); + $keys = str_replace("'", ''', $keys); + + + $response = $query->search($keys); + + + // The response is cached so that it is accessible to the blocks and anything + // else that needs it beyond the initial search. + solrsearch_static_response_cache($searcher, $response); + return array($query, $response); +} + +/** + * It is important to hold on to the Solr response object for the duration of the + * page request so that we can use it for things like building facet blocks. + * + * @param $searcher + * Name of the searcher - e.g. from $query->getSearcher(). + */ +function solrsearch_static_response_cache($searcher, $response = NULL) { + $_response = &drupal_static(__FUNCTION__, array()); + + if (is_object($response)) { + $_response[$searcher] = clone $response; + } + if (!isset($_response[$searcher])) { + $_response[$searcher] = NULL; + } + return $_response[$searcher]; +} + +/** + * Factory function for query objects. + * + * @param string $name + * The search name, used for finding the correct blocks and other config. + * Typically "solrsearch". + * @param array $params + * Array of params , such as 'q', 'fq' to be applied. + * @param string $solrsort + * Visible string telling solr how to sort. + * @param string $base_path + * The search base path (without the keywords) for this query. + * @param DrupalApacheSolrServiceInterface $solr + * An instance of DrupalApacheSolrServiceInterface. + * + * @return DrupalSolrQueryInterface + * DrupalSolrQueryInterface object. + * + * @throws Exception + */ +function solrsearch_drupal_query($name, array $params = array(), $solrsort = '', $base_path = '', DrupalApacheSolrServiceInterface $solr = NULL, $context = array()) { + + + if (!interface_exists('DrupalSolrQueryInterface')) { + require_once(dirname(__FILE__) . '/solrsearch.interface.inc'); + } + $class_info = variable_get('solrsearch_query_class', array( + 'file' => 'Solr_Base_Query', + 'module' => 'solrsearch', + 'class' => 'SolrBaseQuery')); + $class = $class_info['class']; + if (!class_exists($class_info['class']) && isset($class_info['file']) && isset($class_info['module'])) { + module_load_include('php', $class_info['module'], $class_info['file']); + } + if (empty($solr)) { + $solr = solrsearch_get_solr(); + } + + return new $class($name, $solr, $params, $solrsort, $base_path, $context); +} + +/** + * Factory function for query objects. + * + * @param $operator + * Whether the subquery should be added to another query as OR or AND + * + * @return DrupalSolrQueryInterface|false + * Subquery or error. + * + * @throws Exception + */ +function solrsearch_drupal_subquery($operator = 'OR') { + if (!interface_exists('DrupalSolrQueryInterface')) { + require_once(dirname(__FILE__) . '/solrsearch.interface.inc'); + } + + $class_info = variable_get('solrsearch_subquery_class', array( + 'file' => 'Solr_Base_Query', + 'module' => 'solrsearch', + 'class' => 'SolrFilterSubQuery')); + $class = $class_info['class']; + if (!class_exists($class_info['class']) && isset($class_info['file']) && isset($class_info['module'])) { + module_load_include('php', $class_info['module'], $class_info['file']); + } + $query = new $class($operator); + return $query; +} + +/** + * Static getter/setter for the current query. Only set once per page. + * + * @param $env_id + * Environment from which to save or get the current query + * @param DrupalSolrQueryInterface $query + * $query object to save in the static + * + * @return DrupalSolrQueryInterface|null + * return the $query object if it is available in the drupal_static or null otherwise + */ +function solrsearch_current_query($env_id, DrupalSolrQueryInterface $query = NULL) { + $saved_query = &drupal_static(__FUNCTION__, NULL); + if (is_object($query)) { + $saved_query[$env_id] = clone $query; + } + if (empty($saved_query[$env_id])) { + return NULL; + } + return is_object($saved_query[$env_id]) ? clone $saved_query[$env_id] : NULL; +} + +/** + * + */ + +/** + * Construct a dynamic index name based on information about a field. + * + * @param array $field + * array( + * 'index_type' => 'integer', + * 'multiple' => TRUE, + * 'name' => 'fieldname', + * ), + * @return string + * Fieldname as it appears in the solr index + */ +function solrsearch_index_key($field) { + $index_type = !empty($field['index_type']) ? $field['index_type'] : NULL; + switch ($index_type) { + case 'text': + $type_prefix = 't'; + break; + case 'text-omitNorms': + $type_prefix = 'to'; + break; + case 'text-unstemmed': + $type_prefix = 'tu'; + break; + case 'text-edgeNgram': + $type_prefix = 'te'; + break; + case 'text-whiteSpace': + $type_prefix = 'tw'; + break; + case 'integer': + $type_prefix = 'i'; // long integer + break; + case 'half-int': + $type_prefix = 'h'; // 32 bit integer + break; + case 'float': + $type_prefix = 'f'; // float; sortable. + break; + case 'double': + $type_prefix = 'p'; // double; sortable d was used for date. + break; + case 'boolean': + $type_prefix = 'b'; + break; + case 'tint': + $type_prefix = 'it'; // long integer trie; sortable, best for range queries + break; + case 'thalf-int': + $type_prefix = 'ht'; // 32 bit integer trie (sortable) + break; + case 'tfloat': + $type_prefix = 'ft'; // float trie; sortable, best for range queries. + break; + case 'tdouble': + $type_prefix = 'pt'; // double trie; + break; + case 'sint': + $type_prefix = 'is'; // long integer sortable (deprecated) + break; + case 'half-sint': + $type_prefix = 'hs'; // 32 bit integer long sortable (deprecated) + break; + case 'sfloat': + $type_prefix = 'fs'; // float, sortable (use for sorting missing last) (deprecated). + break; + case 'sdouble': + $type_prefix = 'ps'; // double sortable; (use for sorting missing last) (deprecated). + break; + case 'date': + $type_prefix = 'd'; // date trie (sortable) + break; + case 'date-deprecated': + $type_prefix = 'dd'; // date (regular) + break; + case 'binary': + $type_prefix = 'x'; // Anything that is base64 encoded + break; + case 'storage': + $type_prefix = 'z'; // Anything that just need to be stored, not indexed + break; + case 'point': + $type_prefix = 'point'; // PointType. "52.3672174,4.9126891" + break; + case 'location': + $type_prefix = 'loc'; // LatLonType. "52.3672174,4.9126891" + break; + case 'geohash': + $type_prefix = 'geo'; // GeohashField. "42.6" http://en.wikipedia.org/wiki/Geohash + break; + case 'string': + default: + $type_prefix = 's'; // String + } + $sm = !empty($field['multiple']) ? 'm_' : 's_'; + // Block deltas are limited to 32 chars. + return substr($type_prefix . $sm . $field['name'], 0, 32); +} + +/** + * Try to map a schema field name to a human-readable description. + */ +function solrsearch_field_name_map($field_name) { + $map = &drupal_static(__FUNCTION__); + + if (!isset($map)) { + $map = array( + 'content' => t('The full, rendered content (e.g. the rendered node body)'), + 'ts_comments' => t('The rendered comments associated with a node'), + 'tos_content_extra' => t('Extra rendered content or keywords'), + 'tos_name_formatted' => t('Author name (Formatted)'), + 'label' => t('Title or label'), + 'teaser' => t('Teaser or preview'), + 'tos_name' => t('Author name'), + 'path_alias' => t('Path alias'), + 'taxonomy_names' => t('All taxonomy term names'), + 'tags_h1' => t('Body text inside H1 tags'), + 'tags_h2_h3' => t('Body text inside H2 or H3 tags'), + 'tags_h4_h5_h6' => t('Body text inside H4, H5, or H6 tags'), + 'tags_inline' => t('Body text in inline tags like EM or STRONG'), + 'tags_a' => t('Body text inside links (A tags)'), + 'tid' => t('Taxonomy term IDs'), + 'is_uid' => t('User IDs'), + 'bundle' => t('Content type names eg. article'), + 'entity_type' => t('Entity type names eg. node'), + 'ss_language' => t('Language type eg. en or und (undefinded)'), + ); + if (module_exists('taxonomy')) { + foreach (taxonomy_get_vocabularies() as $vocab) { + $map['tm_vid_' . $vocab->vid . '_names'] = t('Taxonomy term names only from the %name vocabulary', array('%name' => $vocab->name)); + $map['im_vid_' . $vocab->vid] = t('Taxonomy term IDs from the %name vocabulary', array('%name' => $vocab->name)); + } + } + foreach (solrsearch_entity_fields('node') as $field_nm => $nodefields) { + foreach ($nodefields as $field_info) { + $map[solrsearch_index_key($field_info)] = t('Field of type @type: %label', array('@type' => $field_info['field']['type'], '%label' => $field_info['display_name'])); + } + } + drupal_alter('solrsearch_field_name_map', $map); + } + return isset($map[$field_name]) ? $map[$field_name] : $field_name; +} + +/** + * Validation function for the Facet API facet settings form. + * + * Solr Search does not support the combination of OR facets + * and facet missing, so catch that at validation. + */ +function solrsearch_facet_form_validate($form, &$form_state) { + if (($form_state['values']['global']['operator'] == FACETAPI_OPERATOR_OR) && $form_state['values']['global']['facet_missing']) { + form_set_error('operator', t('Solr Search does not support <em>facet missing</em> in combination with the OR operator.')); + } +} + +/** + * Implements hook_entity_info_alter(). + */ +function solrsearch_entity_info_alter(&$entity_info) { + // Load all environments + $environments = solrsearch_load_all_environments(); + + // Set those values that we know. Other modules can do so + // for their own entities if they want. + $default_entity_info = array(); + $default_entity_info['node']['indexable'] = TRUE; + $default_entity_info['node']['status callback'][] = 'solrsearch_index_node_status_callback'; + $default_entity_info['node']['document callback'][] = 'solrsearch_index_node_solr_document'; + $default_entity_info['node']['reindex callback'] = 'solrsearch_index_node_solr_reindex'; + $default_entity_info['node']['bundles changed callback'] = 'solrsearch_index_node_bundles_changed'; + $default_entity_info['node']['index_table'] = 'solrsearch_index_entities_node'; + $default_entity_info['node']['cron_check'] = 'solrsearch_index_node_check_table'; + // solrsearch_search implements a new callback for every entity type + // $default_entity_info['node']['solrsearch']['result callback'] = 'solrsearch_search_node_result'; + //Allow implementations of HOOK_solrsearch_entity_info to modify these default indexers + drupal_alter('solrsearch_entity_info', $default_entity_info); + + // First set defaults so that we don't need to worry about NULL keys. + foreach (array_keys($entity_info) as $type) { + if (!isset($entity_info[$type]['solrsearch'])) { + $entity_info[$type]['solrsearch'] = array(); + } + if (isset($default_entity_info[$type])) { + $entity_info[$type]['solrsearch'] += $default_entity_info[$type]; + } + $default = array( + 'indexable' => FALSE, + 'status callback' => '', + 'document callback' => '', + 'reindex callback' => '', + 'bundles changed callback' => '', + ); + $entity_info[$type]['solrsearch'] += $default; + } + + // For any supported entity type and bundle, flag it for indexing. + foreach ($entity_info as $entity_type => $info) { + if ($info['solrsearch']['indexable']) { + // Loop over each environment and check if any of them have other entity + // bundles of any entity type enabled and set the index value to TRUE + foreach ($environments as $env) { + // Skip if the environment is set to read only + if (empty($env['env_id']['conf']['solrsearch_read_only'])) { + // Get the supported bundles + $supported = solrsearch_get_index_bundles($env['env_id'], $entity_type); + // For each bundle in drupal, compare to the supported solrsearch + // bundles and enable where possible + foreach (array_keys($info['bundles']) as $bundle) { + if (in_array($bundle, $supported)) { + $entity_info[$entity_type]['bundles'][$bundle]['solrsearch']['index'] = TRUE; + } + } + } + } + } + } +} + +/** + * Gets a list of the bundles on the specified entity type that should be indexed. + * + * @param string $core + * The Solr environment for which to index entities. + * @param string $entity_type + * The entity type to index. + * @return array + * The bundles that should be indexed. + */ +function solrsearch_get_index_bundles($env_id, $entity_type) { + $environment = solrsearch_environment_load($env_id); + return !empty($environment['index_bundles'][$entity_type]) ? $environment['index_bundles'][$entity_type] : array(); +} + +/** + * Implements hook_entity_insert(). + */ +function solrsearch_entity_insert($entity, $type) { + // For our purposes there's really no difference between insert and update. + return solrsearch_entity_update($entity, $type); +} + +/** + * Determines if we should index the provided entity. + * + * Whether or not a given entity is indexed is determined on a per-bundle basis. + * Entities/Bundles that have no index flag are presumed to not get indexed. + * + * @param stdClass $entity + * The entity we may or may not want to index. + * @param string $type + * The type of entity. + * @return boolean + * TRUE if this entity should be indexed, FALSE otherwise. + */ +function solrsearch_entity_should_index($entity, $type) { + $info = entity_get_info($type); + list($id, $vid, $bundle) = entity_extract_ids($type, $entity); + + if ($bundle && isset($info['bundles'][$bundle]['solrsearch']['index']) && $info['bundles'][$bundle]['solrsearch']['index']) { + return TRUE; + } + return FALSE; +} + +/** + * Implements hook_entity_update(). + */ +function solrsearch_entity_update($entity, $type) { + // Include the index file for the status callback + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + if (solrsearch_entity_should_index($entity, $type)) { + $info = entity_get_info($type); + list($id, $vid, $bundle) = entity_extract_ids($type, $entity); + + // Check status callback before sending to the index + $status_callbacks = solrsearch_entity_get_callback($type, 'status callback', $bundle); + + $status = TRUE; + if (is_array($status_callbacks)) { + foreach($status_callbacks as $status_callback) { + if (is_callable($status_callback)) { + // by placing $status in front we prevent calling any other callback + // after one status callback returned false + $status = $status && $status_callback($id, $type); + } + } + } + + // Delete the entity from our index if the status callback returns FALSE + if (!$status) { + solrsearch_entity_delete($entity, $type); + return NULL; + } + + $indexer_table = solrsearch_get_indexer_table($type); + + // If we haven't seen this entity before it may not be there, so merge + // instead of update. + db_merge($indexer_table) + ->key(array( + 'entity_type' => $type, + 'entity_id' => $id, + )) + ->fields(array( + 'bundle' => $bundle, + 'status' => 1, + 'changed' => REQUEST_TIME, + )) + ->execute(); + } +} + +/** + * Retrieve the indexer table for an entity type. + */ +function solrsearch_get_indexer_table($type) { + $entity_info = entity_get_info(); + if (isset($entity_info[$type]['solrsearch']['index_table'])) { + $indexer_table = $entity_info[$type]['solrsearch']['index_table']; + } + else { + $indexer_table = 'solrsearch_index_entities'; + } + return $indexer_table; +} + +/** + * Implements hook_entity_delete(). + */ +function solrsearch_entity_delete($entity, $entity_type) { + $env_id = solrsearch_default_environment(); + + // Delete the entity's entry from a fictional table of all entities. + $info = entity_get_info($entity_type); + list($entity_id) = entity_extract_ids($entity_type, $entity); + solrsearch_remove_entity($env_id, $entity_type, $entity_id); +} + +function solrsearch_remove_entity($env_id, $entity_type, $entity_id) { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + + $indexer_table = solrsearch_get_indexer_table($entity_type); + if (solrsearch_index_delete_entity_from_index($env_id, $entity_type, $entity_id)) { + // There was no exception, so delete from the table. + db_delete($indexer_table) + ->condition('entity_type', $entity_type) + ->condition('entity_id', $entity_id) + ->execute(); + } + else { + // Set status 0 so we try to delete from the index again in the future. + db_update($indexer_table) + ->condition('entity_id', $entity_id) + ->fields(array('changed' => REQUEST_TIME, 'status' => 0)) + ->execute(); + } +} + +/** + * Returns array containing information about node fields that should be indexed + */ +function solrsearch_entity_fields($entity_type = 'node') { + $fields = &drupal_static(__FUNCTION__, array()); + + if (!isset($fields[$entity_type])) { + $fields[$entity_type] = array(); + + $mappings = module_invoke_all('solrsearch_field_mappings'); + foreach (array_keys($mappings) as $key) { + // Set all values with defaults. + $defaults = array( + 'dependency plugins' => array('bundle', 'role'), + 'map callback' => FALSE, + 'name callback' => '', + 'hierarchy callback' => FALSE, + 'indexing_callback' => '', + 'index_type' => 'string', + 'facets' => FALSE, + 'facet missing allowed' => FALSE, + 'facet mincount allowed' => FALSE, + // Field API allows any field to be multi-valued. + 'multiple' => FALSE, + ); + if ($key !== 'per-field') { + $mappings[$key] += $defaults; + } + else { + foreach (array_keys($mappings[$key]) as $field_key) { + $mappings[$key][$field_key] += $defaults; + } + } + } + + // Allow other modules to add or alter mappings. + drupal_alter('solrsearch_field_mappings', $mappings, $entity_type); + + $modules = system_get_info('module'); + $instances = field_info_instances($entity_type); + foreach (field_info_fields() as $field_name => $field) { + $row = array(); + if (isset($field['bundles'][$entity_type]) && (isset($mappings['per-field'][$field_name]) || isset($mappings[$field['type']]))) { + // Find the mapping. + if (isset($mappings['per-field'][$field_name])) { + $row = $mappings['per-field'][$field_name]; + } + else { + $row = $mappings[$field['type']]; + } + // The field info array. + $row['field'] = $field; + + // Cardinality: The number of values the field can hold. Legal values + // are any positive integer or FIELD_CARDINALITY_UNLIMITED. + if ($row['field']['cardinality'] != 1) { + $row['multiple'] = TRUE; + } + + // @todo: for fields like taxonomy we are indexing multiple Solr fields + // per entity field, but are keying on a single Solr field name here. + $function = !empty($row['name callback']) ? $row['name callback'] : NULL; + if ($function && is_callable($function)) { + $row['name'] = $function($field); + } + else { + $row['name'] = $field['field_name']; + } + $row['module_name'] = $modules[$field['module']]['name']; + // Set display name + $display_name = array(); + foreach ($field['bundles'][$entity_type] as $bundle) { + if (empty($instances[$bundle][$field_name]['display']['search_index']) || $instances[$bundle][$field_name]['display']['search_index'] != 'hidden') { + $row['display_name'] = $instances[$bundle][$field_name]['label']; + $row['bundles'][] = $bundle; + } + } + // Only add to the $fields array if some instances are displayed for the search index. + if (!empty($row['bundles'])) { + // Use the Solr index key as the array key. + $fields[$entity_type][solrsearch_index_key($row)][] = $row; + } + } + } + } + return $fields[$entity_type]; +} + +/** + * Implements hook_solrsearch_index_document_build(). + */ +function field_solrsearch_index_document_build(solrsearchDocument $document, $entity, $entity_type) { + $info = entity_get_info($entity_type); + if ($info['fieldable']) { + // Handle fields including taxonomy. + $indexed_fields = solrsearch_entity_fields($entity_type); + foreach ($indexed_fields as $index_key => $nodefields) { + foreach ($nodefields as $field_info) { + $field_name = $field_info['field']['field_name']; + // See if the node has fields that can be indexed + if (isset($entity->{$field_name})) { + // Got a field. + $function = $field_info['indexing_callback']; + if ($function && function_exists($function)) { + // NOTE: This function should always return an array. One + // entity field may be indexed to multiple Solr fields. + $fields = $function($entity, $field_name, $index_key, $field_info); + foreach ($fields as $field) { + // It's fine to use this method also for single value fields. + $document->setMultiValue($field['key'], $field['value']); + } + } + } + } + } + } +} + +/** + * Implements hook_solrsearch_index_document_build_node(). + * + * Adds book module support + */ +function solrsearch_solrsearch_index_document_build_node(solrsearchDocument $document, $entity, $env_id) { + // Index book module data. + if (!empty($entity->book['bid'])) { + // Hard-coded - must change if solrsearch_index_key() changes. + $document->is_book_bid = (int) $entity->book['bid']; + } +} + +/** + * Strip html tags and also control characters that cause Jetty/Solr to fail. + */ +function solrsearch_clean_text($text) { + // Remove invisible content. + $text = preg_replace('@<(applet|audio|canvas|command|embed|iframe|map|menu|noembed|noframes|noscript|script|style|svg|video)[^>]*>.*</\1>@siU', ' ', $text); + // Add spaces before stripping tags to avoid running words together. + $text = filter_xss(str_replace(array('<', '>'), array(' <', '> '), $text), array()); + // Decode entities and then make safe any < or > characters. + $text = htmlspecialchars(html_entity_decode($text, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8'); + // Remove extra spaces. + $text = preg_replace('/\s+/s', ' ', $text); + // Remove white spaces around punctuation marks probably added + // by the safety operations above. This is not a world wide perfect solution, + // but a rough attempt for at least US and Western Europe. + // Pc: Connector punctuation + // Pd: Dash punctuation + // Pe: Close punctuation + // Pf: Final punctuation + // Pi: Initial punctuation + // Po: Other punctuation, including ¿?¡!,.:; + // Ps: Open punctuation + $text = preg_replace('/\s(\p{Pc}|\p{Pd}|\p{Pe}|\p{Pf}|!|\?|,|\.|:|;)/s', '$1', $text); + $text = preg_replace('/(\p{Ps}|¿|¡)\s/s', '$1', $text); + return $text; +} + +/** + * Use the list.module's list_allowed_values() to format the + * field based on its value ($facet). + * + * @param $facet string + * The indexed value + * @param $options + * An array of options including the hook_block $delta. + */ +function solrsearch_fields_list_facet_map_callback($facets, $options) { + $map = array(); + $allowed_values = array(); + // @see list_field_formatter_view() + $fields = field_info_fields(); + $field_name = $options['field']['field_name']; + if (isset($fields[$field_name])) { + $allowed_values = list_allowed_values($fields[$field_name]); + } + foreach ($facets as $key) { + if (isset($allowed_values[$key])) { + $map[$key]['#markup'] = field_filter_xss($allowed_values[$key]); + } + elseif ($key == '_empty_' && $options['facet missing allowed']) { + // Facet missing. + $map[$key]['#markup'] = theme('facetapi_facet_missing', array('field_name' => $options['display_name'])); + } + else { + $map[$key]['#markup'] = field_filter_xss($key); + } + // The value has already been filtered. + $map[$key]['#html'] = TRUE; + } + return $map; +} + +/** + * @param $facet string + * The indexed value + * @param $options + * An array of options including the hook_block $delta. + * @see http://drupal.org/node/1059372 + */ +function solrsearch_nodereference_map_callback($facets, $options) { + $map = array(); + $allowed_values = array(); + // @see list_field_formatter_view() + $fields = field_info_fields(); + $field_name = $options['field']['field_name']; + if (isset($fields[$field_name])) { + $allowed_values = node_reference_potential_references($fields[$field_name]); + } + foreach ($facets as $key) { + if (isset($allowed_values[$key])) { + $map[$key]['#markup'] = field_filter_xss($allowed_values[$key]['title']); + } + elseif ($key == '_empty_' && $options['facet missing allowed']) { + // Facet missing. + $map[$key]['#markup'] = theme('facetapi_facet_missing', array('field_name' => $options['display_name'])); + } + else { + $map[$key]['#markup'] = field_filter_xss($key); + } + // The value has already been filtered. + $map[$key]['#html'] = TRUE; + } + return $map; +} + +/** + * @param $facet string + * The indexed value + * @param $options + * An array of options including the hook_block $delta. + * @see http://drupal.org/node/1059372 + */ +function solrsearch_userreference_map_callback($facets, $options) { + $map = array(); + $allowed_values = array(); + // @see list_field_formatter_view() + $fields = field_info_fields(); + $field_name = $options['field']['field_name']; + if (isset($fields[$field_name])) { + $allowed_values = user_reference_potential_references($fields[$field_name]); + } + foreach ($facets as $key) { + if (isset($allowed_values[$key])) { + $map[$key]['#markup'] = field_filter_xss($allowed_values[$key]['title']); + } + elseif ($key == '_empty_' && $options['facet missing allowed']) { + // Facet missing. + $map[$key]['#markup'] = theme('facetapi_facet_missing', array('field_name' => $options['display_name'])); + } + else { + $map[$key]['#markup'] = field_filter_xss($key); + } + // The value has already been filtered. + $map[$key]['#html'] = TRUE; + } + return $map; +} + +/** + * Mapping callback for entity references. + */ +function solrsearch_entityreference_facet_map_callback(array $values, array $options) { + $map = array(); + // Gathers entity ids so we can load multiple entities at a time. + $entity_ids = array(); + foreach ($values as $value) { + list($entity_type, $id) = explode(':', $value); + $entity_ids[$entity_type][] = $id; + } + // Loads and maps entities. + foreach ($entity_ids as $entity_type => $ids) { + $entities = entity_load($entity_type, $ids); + foreach ($entities as $id => $entity) { + $key = $entity_type . ':' . $id; + $map[$key] = entity_label($entity_type, $entity); + } + } + return $map; +} + +/** + * Returns the callback function appropriate for a given entity type/bundle. + * + * @param string $entity_type + * The entity type for which we want to know the approprite callback. + * @param string $callback + * The callback for which we want the appropriate function. + * @param string $bundle + * If specified, the bundle of the entity in question. Some callbacks may + * be overridden on a bundle-level. Not specified only the entity-level + * callback will be checked. + * @return string + * The function name for this callback, or NULL if not specified. + */ +function solrsearch_entity_get_callback($entity_type, $callback, $bundle = NULL) { + $info = entity_get_info($entity_type); + + // A bundle-specific callback takes precedence over the generic one for the + // entity type. + if ($bundle && isset($info['bundles'][$bundle]['solrsearch'][$callback])) { + $callback_function = $info['bundles'][$bundle]['solrsearch'][$callback]; + } + elseif (isset($info['solrsearch'][$callback])) { + $callback_function = $info['solrsearch'][$callback]; + } + else { + $callback_function = NULL; + } + return $callback_function; +} + + +/** + * Function to retrieve all the nodes to index. + * Deprecated but kept for backwards compatibility + * @param String $namespace + * @param type $limit + */ +function solrsearch_get_nodes_to_index($namespace, $limit) { + $env_id = solrsearch_default_environment(); + // Hardcode node as an entity type + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + solrsearch_index_get_entities_to_index($env_id, 'node', $limit); +} + + +/** + * Implements hook_theme(). + */ +function solrsearch_theme() { + return array( + 'solrsearch_result' => array( + 'variables' => array('result' => NULL, 'module' => NULL), + 'file' => 'solrsearch.pages.inc', + 'template' => 'solr-search-result', + ), + 'solrsearch_results' => array( + 'variables' => array('results' => NULL, 'module' => NULL), + 'file' => 'solrsearch.pages.inc', + 'template' => 'solr-search-results', + ), + 'solrsearch_term_list_author' => array( + 'variables' => array('authors' => null), + 'file' => 'solrsearch_terms.inc', + 'template' => 'solrsearch-term-list-author', + ), + + 'solrsearch_term_list_title' => array( + 'variables' => array('authors' => null), + 'file' => 'solrsearch_terms.inc', + 'template' => 'solrsearch-term-list-title', + ), + + 'solrsearch_term_selection_form' => array( + 'variables' => array('authors' => null,'cnt' => null, 'letter' => null), + 'file' => 'solrsearch_terms.inc', + 'template' => 'solrsearch-term-selection-form', + ), + + + /** + * Returns a list of links generated by solrsearch_sort_link + */ + 'solrsearch_sort_list' => array( + 'variables' => array('items' => NULL), + ), + /** + * Returns a link which can be used to search the results. + */ + 'solrsearch_sort_link' => array( + 'variables' => array('text' => NULL, 'path' => NULL, 'options' => NULL, 'active' => FALSE, 'direction' => ''), + ), + /** + * Themes the title links in admin settings pages. + */ + 'solrsearch_settings_title' => array( + 'variables' => array('env_id' => NULL), + ), + ); +} + +/** + * Implements hook_hook_info(). + */ +function solrsearch_hook_info() { + $hooks = array( + 'solrsearch_field_mappings' => array( + 'group' => 'solrsearch', + ), + 'solrsearch_field_mappings_alter' => array( + 'group' => 'solrsearch', + ), + 'solrsearch_query_prepare' => array( + 'group' => 'solrsearch', + ), + 'solrsearch_query_alter' => array( + 'group' => 'solrsearch', + ), + 'solrsearch_search_result_alter' => array( + 'group' => 'solrsearch', + ), + 'solrsearch_environment_delete' => array( + 'group' => 'solrsearch', + ) + ); + $hooks['solrsearch_index_document_build'] = array( + 'group' => 'solrsearch', + ); + return $hooks; +} + +/** + * Implements hook_solrsearch_field_mappings(). + */ +function field_solrsearch_field_mappings() { + $mappings = array( + 'list_integer' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'map callback' => 'solrsearch_fields_list_facet_map_callback', + 'index_type' => 'integer', + 'facets' => TRUE, + 'query types' => array('term', 'numeric_range'), + 'query type' => 'term', + 'facet missing allowed' => TRUE, + ), + 'list_float' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'map callback' => 'solrsearch_fields_list_facet_map_callback', + 'index_type' => 'float', + 'facets' => TRUE, + 'query types' => array('term', 'numeric_range'), + 'query type' => 'term', + 'facet missing allowed' => TRUE, + ), + 'list_text' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'map callback' => 'solrsearch_fields_list_facet_map_callback', + 'index_type' => 'string', + 'facets' => TRUE, + 'facet missing allowed' => TRUE, + ), + 'list_boolean' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'map callback' => 'solrsearch_fields_list_facet_map_callback', + 'index_type' => 'boolean', + 'facets' => TRUE, + 'facet missing allowed' => TRUE, + ), + 'number_integer' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'index_type' => 'tint', + 'facets' => TRUE, + 'query types' => array('term', 'numeric_range'), + 'query type' => 'term', + 'facet mincount allowed' => TRUE, + ), + 'number_decimal' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'index_type' => 'tfloat', + 'facets' => TRUE, + 'query types' => array('term', 'numeric_range'), + 'query type' => 'term', + 'facet mincount allowed' => TRUE, + ), + 'number_float' => array( + 'indexing_callback' => 'solrsearch_fields_default_indexing_callback', + 'index_type' => 'tfloat', + 'facets' => TRUE, + 'query types' => array('term', 'numeric_range'), + 'query type' => 'term', + 'facet mincount allowed' => TRUE, + ), + 'taxonomy_term_reference' => array( + 'map callback' => 'facetapi_map_taxonomy_terms', + 'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy', + 'indexing_callback' => 'solrsearch_term_reference_indexing_callback', + 'index_type' => 'integer', + 'facet_block_callback' => 'solrsearch_search_taxonomy_facet_block', + 'facets' => TRUE, + 'query types' => array('term'), + 'query type' => 'term', + 'facet mincount allowed' => TRUE, + ), + ); + + return $mappings; +} + +/** + * Implements hook_solrsearch_field_mappings() on behalf of date module. + */ +function date_solrsearch_field_mappings() { + $mappings = array(); + $default = array( + 'indexing_callback' => 'solrsearch_date_default_indexing_callback', + 'index_type' => 'date', + 'facets' => TRUE, + 'query types' => array('date'), + 'query type' => 'date', + 'min callback' => 'solrsearch_get_min_date', + 'max callback' => 'solrsearch_get_max_date', + 'map callback' => 'facetapi_map_date', + ); + + // DATE and DATETIME fields can use the same indexing callback. + $mappings['date'] = $default; + $mappings['datetime'] = $default; + + // DATESTAMP fields need a different callback. + $mappings['datestamp'] = $default; + $mappings['datestamp']['indexing_callback'] = 'solrsearch_datestamp_default_indexing_callback'; + + return $mappings; +} + + +/** + * Callback that returns the minimum date of the facet's datefield. + * + * @param $facet + * An array containing the facet definition. + * + * @return + * The minimum time in the node table. + * + * @todo Cache this value. + */ +function solrsearch_get_min_date(array $facet) { + // FieldAPI date fields. + $table = 'field_data_' . $facet['field api name']; + $column = $facet['field api name'] . '_value'; + $query = db_select($table, 't'); + $query->addExpression('MIN(' . $column . ')', 'min'); + $query_min = $query->execute()->fetch()->min; + // Update to unix timestamp if this is an ISO or other format. + if (!is_int($query_min)) { + $return = strtotime($query_min); + if ($return === FALSE) { + // Not a string that strtotime accepts (ex. '0000-00-00T00:00:00'). + // Return default start date of 1 as the date query type getDateRange() + // function expects a non-0 integer. + $return = 1; + } + } + return $return; +} + +/** + * Callback that returns the maximum value of the facet's date field. + * + * @param $facet + * An array containing the facet definition. + * + * @return + * The maximum time of the field. + * + * @todo Cache this value. + */ +function solrsearch_get_max_date(array $facet) { + + // FieldAPI date fields. + $table = 'field_data_' . $facet['field api name']; + $column = $facet['field api name'] . '_value'; + $query = db_select($table, 't'); + $query->addExpression('MAX(' . $column . ')', 'max'); + $query_max = $query->execute()->fetch()->max; + // Update to unix timestamp if this is an ISO or other format. + if (!is_int($query_max)) { + $return = strtotime($query_max); + if ($return === FALSE) { + // Not a string that strtotime accepts (ex. '0000-00-00T00:00:00'). + // Return default end date of 1 year from now. + $return = time() + (52 * 7 * 24 * 60 * 60); + } + } + return $return; +} + +/** + * Implements hook_solrsearch_field_mappings() on behalf of References (node_reference). + * @see http://drupal.org/node/1059372 + */ +function node_reference_solrsearch_field_mappings() { + $mappings = array( + 'node_reference' => array( + 'indexing_callback' => 'solrsearch_nodereference_indexing_callback', + 'index_type' => 'integer', + 'map callback' => 'solrsearch_nodereference_map_callback', + 'facets' => TRUE, + ) + ); + + return $mappings; +} + +/** + * Implements hook_solrsearch_field_mappings() on behalf of References (user_reference). + * @see http://drupal.org/node/1059372 + */ +function user_reference_solrsearch_field_mappings() { + $mappings = array( + 'user_reference' => array( + 'indexing_callback' => 'solrsearch_userreference_indexing_callback', + 'index_type' => 'integer', + 'map callback' => 'solrsearch_userreference_map_callback', + 'facets' => TRUE, + ), + ); + + return $mappings; +} +/** + * Implements hook_solrsearch_field_mappings() on behalf of EntityReferences (entityreference) + * @see http://drupal.org/node/1572722 + */ +function entityreference_solrsearch_field_mappings() { + $mappings = array( + 'entityreference' => array( + 'indexing_callback' => 'solrsearch_entityreference_indexing_callback', + 'map callback' => 'solrsearch_entityreference_facet_map_callback', + 'index_type' => 'string', + 'facets' => TRUE, + 'query types' => array('term'), + 'facet missing allowed' => TRUE, + ), + ); + + return $mappings; +} + +/** + * A replacement for l() + * - doesn't add the 'active' class + * - retains all $_GET parameters that solrsearch may not be aware of + * - if set, $options['query'] MUST be an array + * + * @see http://api.drupal.org/api/function/l/6 + * for parameters and options. + * + * @return + * an HTML string containing a link to the given path. + */ +function solrsearch_l($text, $path, $options = array()) { + // Merge in defaults. + $options += array( + 'attributes' => array(), + 'html' => FALSE, + 'query' => array(), + ); + + // Don't need this, and just to be safe. + unset($options['attributes']['title']); + + // Retain GET parameters that Solr Search knows nothing about. + $get = array_diff_key($_GET, array('q' => 1, 'page' => 1, 'solrsort' => 1), $options['query']); + $options['query'] += $get; + + return '<a href="' . check_url(url($path, $options)) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain(html_entity_decode($text))) . '</a>'; +} + +function theme_solrsearch_sort_link($vars) { + $icon = ''; + if ($vars['direction']) { + $icon = ' ' . theme('tablesort_indicator', array('style' => $vars['direction'])); + } + if ($vars['active']) { + if (isset($vars['options']['attributes']['class'])) { + $vars['options']['attributes']['class'] .= ' active'; + } + else { + $vars['options']['attributes']['class'] = 'active'; + } + } + return $icon . solrsearch_l($vars['text'], $vars['path'], $vars['options']); +} + +function theme_solrsearch_sort_list($vars) { + // theme('item_list') expects a numerically indexed array. + $vars['items'] = array_values($vars['items']); + return theme('item_list', array('items' => $vars['items'])); +} + +/** + * Themes the title for settings pages. + */ +function theme_solrsearch_settings_title($vars) { + $output = ''; + + // Gets environment information, builds header with nested link to the environment's + // edit page. Skips building title if environment info could not be retrieved. + if ($environment = solrsearch_environment_load($vars['env_id'])) { + $url = url( + 'admin/config/search/solrsearch/settings/', + array('query' => array('destination' => current_path())) + ); + $output .= '<h3>'; + $output .= t( + 'Settings for: @environment (<a href="@url">Overview</a>)', + array('@url' => $url, '@environment' => $environment['name']) + ); + $output .= "</h3>\n"; + } + + return $output; +} + +/** + * Export callback to load the view subrecords, which are the index bundles. + */ +function solrsearch_environment_load_subrecords(&$environments) { + if (empty($environments)) { + // Nothing to do. + return NULL; + } + + $all_index_bundles = db_select('solrsearch_index_bundles', 'ib') + ->fields('ib', array('env_id', 'entity_type', 'bundle')) + ->condition('env_id', array_keys($environments), 'IN') + ->orderBy('env_id') + ->orderBy('entity_type') + ->orderBy('bundle') + ->execute() + ->fetchAll(PDO::FETCH_ASSOC); + + $all_index_bundles_keyed = array(); + foreach ($all_index_bundles as $env_info) { + extract($env_info); + $all_index_bundles_keyed[$env_id][$entity_type][] = $bundle; + } + + $all_variables = db_select('solrsearch_environment_variable', 'v') + ->fields('v', array('env_id', 'name', 'value')) + ->condition('env_id', array_keys($environments), 'IN') + ->orderBy('env_id') + ->orderBy('name') + ->orderBy('value') + ->execute() + ->fetchAll(PDO::FETCH_ASSOC); + + $variables = array(); + foreach ($all_variables as $variable) { + extract($variable); + $variables[$env_id][$name] = unserialize($value); + } + + foreach ($environments as $env_id => &$environment) { + $index_bundles = !empty($all_index_bundles_keyed[$env_id]) ? $all_index_bundles_keyed[$env_id] : array(); + $conf = !empty($variables[$env_id]) ? $variables[$env_id] : array(); + if (is_array($environment)) { + // Environment is an array. + // If we have different values in the database compared with what we + // have in the given environment argument we allow the admin to revert + // the db values so we can stick with a consistent system + if (!empty($environment['index_bundles']) && !empty($index_bundles) && $environment['index_bundles'] !== $index_bundles) { + unset($environment['in_code_only']); + $environment['type'] = 'Overridden'; + } + if (!empty($environment['conf']) && !empty($conf) && $environment['conf'] !== $conf) { + unset($environment['in_code_only']); + $environment['type'] = 'Overridden'; + } + $environment['index_bundles'] = (empty($environment['index_bundles']) || !empty($index_bundles)) ? $index_bundles : $environment['index_bundles']; + $environment['conf'] = (empty($environment['conf']) || !empty($conf)) ? $conf : $environment['conf']; + } + elseif (is_object($environment)) { + // Environment is an object. + if ($environment->index_bundles !== $index_bundles && !empty($index_bundles)) { + unset($environment->in_code_only); + $environment->type = 'Overridden'; + } + if ($environment->conf !== $conf && !empty($conf)) { + unset($environment->in_code_only); + $environment->type = 'Overridden'; + } + $environment->index_bundles = (empty($environment->index_bundles) || !empty($index_bundles)) ? $index_bundles : $environment->index_bundles; + $environment->conf = (empty($environment->conf) || !empty($conf)) ? $conf : $environment->conf; + } + } +} + +/** + * Callback for saving Solr Search environment CTools exportables. + * + * CTools uses objects, while Solr Search uses arrays; turn CTools value into an + * array, then call the normal save function. + * + * @param stdclass $environment + * An environment object. + */ +function solrsearch_ctools_environment_save($environment) { + solrsearch_environment_save((array) $environment); +} + +/** + * Callback for reverting Solr Search environment CTools exportables. + * + * @param mixed $env_id + * An environment machine name. CTools may provide an id OR a complete + * environment object; Since Solr Search loads environments as arrays, this + * may also be an environment array. + */ +function solrsearch_ctools_environment_delete($env_id) { + if (is_object($env_id) || is_array($env_id)) { + $env_id = (object) $env_id; + $env_id = $env_id->env_id; + } + solrsearch_environment_delete($env_id); +} + +/** + * Callback for exporting Solr Search environments as CTools exportables. + * + * @param array $environment + * An environment array from Solr Search. + * @param string $indent + * White space for indentation from CTools. + */ +function solrsearch_ctools_environment_export($environment, $indent) { + ctools_include('export'); + $environment = (object) $environment; + // Re-load the enviroment, since in some cases the conf + // is stripped since it's not in the actual schema. + $environment = (object) solrsearch_environment_load($environment->env_id); + + $index_bundles = array(); + foreach (entity_get_info() as $type => $info) { + if ($bundles = solrsearch_get_index_bundles($environment->env_id, $type)) { + $index_bundles[$type] = $bundles; + } + } + $additions_top = array(); + $additions_bottom = array('conf' => $environment->conf, 'index_bundles' => $index_bundles); + return ctools_export_object('solrsearch_environment', $environment, $indent, NULL, $additions_top, $additions_bottom); +} + + + + +/** +* Performs a search by calling hook_search_execute(). +* +* @param $keys +* Keyword query to search on. +* @param $module +* Search module to search. +* @param $conditions +* Optional array of additional search conditions. +* +* @return +* Renderable array of search results. No return value if $keys are not +* supplied or if the given search module is not active. +*/ +function solrsearch_data($keys, $module, $conditions = NULL) { + if (module_hook($module, 'search_execute')) { + + + $results = module_invoke($module, 'search_execute', $keys, $conditions); + + #$params = $query ->getParams(); + #$description = t('Showing items @start through @end of @total.', array( + # '@start' => $params['start'] + 1, + # '@end' => $params['start'] + $params['rows'] - 1, + # '@total' => $total, + #)); + + + if (module_hook($module, 'search_page')) { + return module_invoke($module, 'search_page', $results); + } + else { + return array( + '#theme' => 'solrsearch_results', + '#results' => $results, + '#module' => $module, + ); + } + } +} + +function solrsearch_callback_user_values(array $facet){ + #ToDO is this used? +} + + +function solrsearch_permission() { + return array( + 'view restricted content' => array( + 'title' => t('View restricted content'), + 'description' => t('Allow users to view content which is restricted.'), + ), + ); +}