# HG changeset patch # User Dirk Wintergruen # Date 1433751714 -7200 # Node ID a2b4f67e73dcfc8127a0f21ee0b1fc5dda15326b initial diff -r 000000000000 -r a2b4f67e73dc Apache_Solr_Document.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Apache_Solr_Document.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,410 @@ + + */ + +/** + * Additional code Copyright (c) 2011 by Peter Wolanin, and + * additional contributors. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program as the file LICENSE.txt; if not, please see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + */ + +/** + * Holds Key / Value pairs that represent a Solr Document along with any associated boost + * values. Field values can be accessed by direct dereferencing such as: + * + * @code + * $document->title = 'Something'; + * echo $document->title; + * + * Additionally, the field values can be iterated with foreach + * + * @code + * foreach ($document as $fieldName => $fieldValue) { + * ... + * } + * + */ +class ApacheSolrDocument implements IteratorAggregate { + + /** + * Document boost value + * + * @var float + */ + protected $_documentBoost = FALSE; + + /** + * Document field values, indexed by name + * + * @var array + */ + protected $_fields = array(); + + /** + * Document field boost values, indexed by name + * + * @var array array of floats + */ + protected $_fieldBoosts = array(); + + /** + * Clear all boosts and fields from this document + */ + public function clear() { + $this->_documentBoost = FALSE; + + $this->_fields = array(); + $this->_fieldBoosts = array(); + } + + /** + * Get current document boost + * + * @return mixed + * will be false for default, or else a float + */ + public function getBoost() { + return $this->_documentBoost; + } + + /** + * Set document boost factor + * + * @param mixed $boost + * Use false for default boost, else cast to float that should be > 0 or will be treated as false + */ + public function setBoost($boost) { + $boost = (float) $boost; + + if ($boost > 0.0) { + $this->_documentBoost = $boost; + } + else { + $this->_documentBoost = FALSE; + } + } + + /** + * Add a value to a multi-valued field + * + * NOTE: the solr XML format allows you to specify boosts + * PER value even though the underlying Lucene implementation + * only allows a boost per field. To remedy this, the final + * field boost value will be the product of all specified boosts + * on field values - this is similar to SolrJ's functionality. + * + * @code + * $doc = new ApacheSolrDocument(); + * $doc->addField('foo', 'bar', 2.0); + * $doc->addField('foo', 'baz', 3.0); + * // resultant field boost will be 6! + * echo $doc->getFieldBoost('foo'); + * + * @param string $key + * @param mixed $value + * @param mixed $boost + * Use false for default boost, else cast to float that should be > 0 or will be treated as false + */ + public function addField($key, $value, $boost = FALSE) { + if (!isset($this->_fields[$key])) { + // create holding array if this is the first value + $this->_fields[$key] = array(); + } + else if (!is_array($this->_fields[$key])) { + // move existing value into array if it is not already an array + $this->_fields[$key] = array($this->_fields[$key]); + } + + if ($this->getFieldBoost($key) === FALSE) { + // boost not already set, set it now + $this->setFieldBoost($key, $boost); + } + else if ((float) $boost > 0.0) { + // multiply passed boost with current field boost - similar to SolrJ implementation + $this->_fieldBoosts[$key] *= (float) $boost; + } + + // add value to array + $this->_fields[$key][] = $value; + } + + /** + * Handle the array manipulation for a multi-valued field + * + * @param string $key + * @param string $value + * @param mixed $boost + * Use false for default boost, else cast to float that should be > 0 or will be treated as false + * + * @deprecated Use addField(...) instead + */ + public function setMultiValue($key, $value, $boost = FALSE) { + $this->addField($key, $value, $boost); + } + + /** + * Get field information + * + * @param string $key + * @return mixed associative array of info if field exists, false otherwise + */ + public function getField($key) { + if (isset($this->_fields[$key])) { + return array( + 'name' => $key, + 'value' => $this->_fields[$key], + 'boost' => $this->getFieldBoost($key) + ); + } + + return FALSE; + } + + /** + * Set a field value. Multi-valued fields should be set as arrays + * or instead use the addField(...) function which will automatically + * make sure the field is an array. + * + * @param string $key + * @param mixed $value + * @param mixed $boost + * Use false for default boost, else cast to float that should be > 0 or will be treated as false + */ + public function setField($key, $value, $boost = FALSE) { + $this->_fields[$key] = $value; + $this->setFieldBoost($key, $boost); + } + + /** + * Get the currently set field boost for a document field + * + * @param string $key + * @return float + * currently set field boost, false if one is not set + */ + public function getFieldBoost($key) { + return isset($this->_fieldBoosts[$key]) ? $this->_fieldBoosts[$key] : FALSE; + } + + /** + * Set the field boost for a document field + * + * @param string $key + * field name for the boost + * @param mixed $boost + * Use false for default boost, else cast to float that should be > 0 or will be treated as false + */ + public function setFieldBoost($key, $boost) { + $boost = (float) $boost; + + if ($boost > 0.0) { + $this->_fieldBoosts[$key] = $boost; + } + else { + $this->_fieldBoosts[$key] = FALSE; + } + } + + /** + * Return current field boosts, indexed by field name + * + * @return array + */ + public function getFieldBoosts() { + return $this->_fieldBoosts; + } + + /** + * Get the names of all fields in this document + * + * @return array + */ + public function getFieldNames() { + return array_keys($this->_fields); + } + + /** + * Get the values of all fields in this document + * + * @return array + */ + public function getFieldValues() { + return array_values($this->_fields); + } + + /** + * IteratorAggregate implementation function. Allows usage: + * + * @code + * foreach ($document as $key => $value) { + * ... + * } + * + */ + public function getIterator() { + $arrayObject = new ArrayObject($this->_fields); + + return $arrayObject->getIterator(); + } + + /** + * Magic get for field values + * + * @param string $key + * @return mixed + */ + public function __get($key) { + return $this->_fields[$key]; + } + + /** + * Magic set for field values. Multi-valued fields should be set as arrays + * or instead use the addField(...) function which will automatically + * make sure the field is an array. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) { + $this->setField($key, $value); + } + + /** + * Magic isset for fields values. Do not call directly. Allows usage: + * + * @code + * isset($document->some_field); + * + * @param string $key + * @return boolean + * Whether the given key is set in the document + */ + public function __isset($key) { + return isset($this->_fields[$key]); + } + + /** + * Magic unset for field values. Do not call directly. Allows usage: + * + * @code + * unset($document->some_field); + * + * @param string $key + */ + public function __unset($key) { + unset($this->_fields[$key]); + unset($this->_fieldBoosts[$key]); + } + + /** + * Create an XML fragment from a ApacheSolrDocument instance appropriate for use inside a Solr add call + * + * @param ApacheSolrDocument $document + * + * @return string + * an xml formatted string from the given document + */ + public static function documentToXml(ApacheSolrDocument $document) { + $xml = 'getBoost() !== FALSE) { + $xml .= ' boost="' . $document->getBoost() . '"'; + } + + $xml .= '>'; + + foreach ($document as $key => $value) { + $key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); + $fieldBoost = $document->getFieldBoost($key); + + if (is_array($value)) { + foreach ($value as $multivalue) { + $xml .= ''; + } + } + else { + $xml .= ''; + } + } + + $xml .= ''; + + // Remove any control characters to avoid Solr XML parser exception + return self::stripCtrlChars($xml); + } + + /** + * Replace control (non-printable) characters from string that are invalid to Solr's XML parser with a space. + * + * @param string $string + * @return string + */ + public static function stripCtrlChars($string) { + // See: http://w3.org/International/questions/qa-forms-utf-8.html + // Printable utf-8 does not include any of these chars below x7F + return preg_replace('@[\x00-\x08\x0B\x0C\x0E-\x1F]@', ' ', $string); + } +} \ No newline at end of file diff -r 000000000000 -r a2b4f67e73dc CopyOfsolrsearch_search.pages.OLD --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CopyOfsolrsearch_search.pages.OLD Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,128 @@ + 'container', + '#attributes' => array('class' => array('container-inline')), + ); + $form['basic']['keys'] = array( + '#type' => 'textfield', + '#title' => t('Enter terms'), + '#default_value' => $keys, + '#size' => 20, + '#maxlength' => 255, + ); + $form['basic']['submit'] = array( + '#type' => 'submit', + '#value' => t('Search'), + ); + + $form['basic']['get'] = array( + '#type' => 'hidden', + '#default_value' => json_encode(array_diff_key($_GET, array('q' => 1, 'page' => 1, 'solrsort' => 1, 'retain-filters' => 1))), + ); + + $fq = NULL; + + if (solrsearch_has_searched($search_page['env_id'])) { + $query = solrsearch_current_query($search_page['env_id']); + // We use the presence of filter query params as a flag for the retain filters checkbox. + $fq = $query->getParam('fq'); + } + + if ($fq || isset($form_state['input']['retain-filters'])) { + $form['basic']['retain-filters'] = array( + '#type' => 'checkbox', + '#title' => t('Retain current filters'), + '#default_value' => (int) !empty($_GET['retain-filters']), + ); + } + + return $form; +} + +/** + * Processes solrsearch_search_custom_page_search_form submissions. + */ +function solrsearch_search_custom_page_search_form_submit(&$form, &$form_state) { + $search_page = $form['#search_page']; + $redirect = $search_page['search_path']; + + // Also encode slashes so we don't get akward situations when obtaining the + // search key. We can't use drupal_encode_path because for "aestetic" reasons + // they don't encode slashes... + $redirect_value = rawurlencode($form_state['values']['keys']); + + if (strlen($form_state['values']['keys'])) { + $redirect .= '/' . $redirect_value; + } + + $get = array(); + if (isset($form_state['values']['get'])) { + $get = json_decode($form_state['values']['get'], TRUE); + } + if (!empty($form_state['values']['retain-filters'])) { + // Add our saved values + $get['retain-filters'] = '1'; + } + else { + // Remove all filters + if (!empty($search_page['settings']['solrsearch_search_allow_user_input'])) { + unset($get['fq']); + } + if (module_exists('facetapi')) { + unset($get['f']); + } + } + + // Add the query values into the redirect. + $form_state['redirect'] = array($redirect, array('query' => $get)); +} diff -r 000000000000 -r a2b4f67e73dc Drupal_Apache_Solr_Service.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Drupal_Apache_Solr_Service.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,913 @@ + + */ + +/** + * Additional code Copyright (c) 2008-2011 by Robert Douglass, James McKinney, + * Jacob Singh, Alejandro Garza, Peter Wolanin, and additional contributors. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program as the file LICENSE.txt; if not, please see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + */ + +/** + * Starting point for the Solr API. Represents a Solr server resource and has + * methods for pinging, adding, deleting, committing, optimizing and searching. + */ + +class DrupalsolrsearchService implements DrupalApacheSolrServiceInterface { + /** + * How NamedLists should be formatted in the output. This specifically effects facet counts. Valid values + * are 'map' (default) or 'flat'. + * + */ + const NAMED_LIST_FORMAT = 'map'; + + /** + * Servlet mappings + */ + const PING_SERVLET = 'admin/ping'; + const UPDATE_SERVLET = 'update'; + const SEARCH_SERVLET = 'select'; + const LUKE_SERVLET = 'admin/luke'; + const SYSTEM_SERVLET = 'admin/system'; + const STATS_SERVLET = 'admin/stats.jsp'; + const STATS_SERVLET_4 = 'admin/mbeans?wt=xml&stats=true'; + + /** + * Server url + * + * @var array + */ + protected $parsed_url; + + /** + * Constructed servlet full path URLs + * + * @var string + */ + protected $update_url; + + /** + * Default HTTP timeout when one is not specified (initialized to default_socket_timeout ini setting) + * + * var float + */ + protected $_defaultTimeout; + protected $env_id; + protected $luke; + protected $stats; + protected $system_info; + + /** + * Flag that denotes whether to use soft commits for Solr 4.x, defaults to FALSE. + * + * @var bool + */ + protected $soft_commit = FALSE; + + /** + * Call the /admin/ping servlet, to test the connection to the server. + * + * @param $timeout + * maximum time to wait for ping in seconds, -1 for unlimited (default 2). + * @return + * (float) seconds taken to ping the server, FALSE if timeout occurs. + */ + public function ping($timeout = 2) { + $start = microtime(TRUE); + + if ($timeout <= 0.0) { + $timeout = -1; + } + $pingUrl = $this->_constructUrl(self::PING_SERVLET); + // Attempt a HEAD request to the solr ping url. + $options = array( + 'method' => 'HEAD', + 'timeout' => $timeout, + ); + $response = $this->_makeHttpRequest($pingUrl, $options); + + if ($response->code == 200) { + // Add 0.1 ms to the ping time so we never return 0.0. + return microtime(TRUE) - $start + 0.0001; + } + else { + return FALSE; + } + } + + /** + * Flags whether to use soft commits for Solr 4.x. + * + * @param bool $soft_commit + * Whether or not to use soft commits for Solr 4.x. + */ + public function setSoftCommit($soft_commit) { + $this->soft_commit = (bool) $soft_commit; + } + + /** + * Returns the flag that denotes whether to use soft commits for Solr 4.x. + * + * @return bool + * Whether to use soft commits for Solr 4.x. + */ + public function getSoftCommit() { + return $this->soft_commit; + } + + /** + * Call the /admin/system servlet + * + * @return + * (array) With all the system info + */ + protected function setSystemInfo() { + $url = $this->_constructUrl(self::SYSTEM_SERVLET, array('wt' => 'json')); + if ($this->env_id) { + $this->system_info_cid = $this->env_id . ":system:" . drupal_hash_base64($url); + $cache = cache_get($this->system_info_cid, 'cache_solrsearch'); + if (isset($cache->data)) { + $this->system_info = json_decode($cache->data); + } + } + // Second pass to populate the cache if necessary. + if (empty($this->system_info)) { + $response = $this->_sendRawGet($url); + $this->system_info = json_decode($response->data); + if ($this->env_id) { + cache_set($this->system_info_cid, $response->data, 'cache_solrsearch'); + } + } + } + + /** + * Get information about the Solr Core. + * + * @return + * (string) system info encoded in json + */ + public function getSystemInfo() { + if (!isset($this->system_info)) { + $this->setSystemInfo(); + } + return $this->system_info; + } + + /** + * Sets $this->luke with the meta-data about the index from admin/luke. + */ + protected function setLuke($num_terms = 0) { + if (empty($this->luke[$num_terms])) { + $params = array( + 'numTerms' => "$num_terms", + 'wt' => 'json', + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + $url = $this->_constructUrl(self::LUKE_SERVLET, $params); + if ($this->env_id) { + $cid = $this->env_id . ":luke:" . drupal_hash_base64($url); + $cache = cache_get($cid, 'cache_solrsearch'); + if (isset($cache->data)) { + $this->luke = $cache->data; + } + } + } + // Second pass to populate the cache if necessary. + if (empty($this->luke[$num_terms])) { + $this->luke[$num_terms] = $this->_sendRawGet($url); + if ($this->env_id) { + cache_set($cid, $this->luke, 'cache_solrsearch'); + } + } + } + + /** + * Get just the field meta-data about the index. + */ + public function getFields($num_terms = 0) { + return $this->getLuke($num_terms)->fields; + } + + /** + * Get meta-data about the index. + */ + public function getLuke($num_terms = 0) { + if (!isset($this->luke[$num_terms])) { + $this->setLuke($num_terms); + } + return $this->luke[$num_terms]; + } + + /** + * Get the current solr version. This could be 1, 3 or 4 + * + * @return int + * 1, 3 or 4. Does not give a more details version, for that you need + * to get the system info. + */ + public function getSolrVersion() { + $system_info = $this->getSystemInfo(); + // Get our solr version number + if (isset($system_info->lucene->{'solr-spec-version'})) { + return $system_info->lucene->{'solr-spec-version'}[0]; + } + return 0; + } + + /** + * Sets $this->stats with the information about the Solr Core form + */ + protected function setStats() { + $data = $this->getLuke(); + $solr_version = $this->getSolrVersion(); + // Only try to get stats if we have connected to the index. + if (empty($this->stats) && isset($data->index->numDocs)) { + if ($solr_version >= 4) { + $url = $this->_constructUrl(self::STATS_SERVLET_4); + } + else { + $url = $this->_constructUrl(self::STATS_SERVLET); + } + if ($this->env_id) { + $this->stats_cid = $this->env_id . ":stats:" . drupal_hash_base64($url); + $cache = cache_get($this->stats_cid, 'cache_solrsearch'); + if (isset($cache->data)) { + $this->stats = simplexml_load_string($cache->data); + } + } + // Second pass to populate the cache if necessary. + if (empty($this->stats)) { + $response = $this->_sendRawGet($url); + $this->stats = simplexml_load_string($response->data); + if ($this->env_id) { + cache_set($this->stats_cid, $response->data, 'cache_solrsearch'); + } + } + } + } + + /** + * Get information about the Solr Core. + * + * Returns a Simple XMl document + */ + public function getStats() { + if (!isset($this->stats)) { + $this->setStats(); + } + return $this->stats; + } + + /** + * Get summary information about the Solr Core. + */ + public function getStatsSummary() { + $stats = $this->getStats(); + $solr_version = $this->getSolrVersion(); + + $summary = array( + '@pending_docs' => '', + '@autocommit_time_seconds' => '', + '@autocommit_time' => '', + '@deletes_by_id' => '', + '@deletes_by_query' => '', + '@deletes_total' => '', + '@schema_version' => '', + '@core_name' => '', + '@index_size' => '', + ); + + if (!empty($stats)) { + if ($solr_version <= 3) { + $docs_pending_xpath = $stats->xpath('//stat[@name="docsPending"]'); + $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath)); + $max_time_xpath = $stats->xpath('//stat[@name="autocommit maxTime"]'); + $max_time = (int) trim(current($max_time_xpath)); + // Convert to seconds. + $summary['@autocommit_time_seconds'] = $max_time / 1000; + $summary['@autocommit_time'] = format_interval($max_time / 1000); + $deletes_id_xpath = $stats->xpath('//stat[@name="deletesById"]'); + $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath)); + $deletes_query_xpath = $stats->xpath('//stat[@name="deletesByQuery"]'); + $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath)); + $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query']; + $schema = $stats->xpath('/solr/schema[1]'); + $summary['@schema_version'] = trim($schema[0]); + $core = $stats->xpath('/solr/core[1]'); + $summary['@core_name'] = trim($core[0]); + $size_xpath = $stats->xpath('//stat[@name="indexSize"]'); + $summary['@index_size'] = trim(current($size_xpath)); + } + else { + $system_info = $this->getSystemInfo(); + $docs_pending_xpath = $stats->xpath('//lst["stats"]/long[@name="docsPending"]'); + $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath)); + $max_time_xpath = $stats->xpath('//lst["stats"]/str[@name="autocommit maxTime"]'); + $max_time = (int) trim(current($max_time_xpath)); + // Convert to seconds. + $summary['@autocommit_time_seconds'] = $max_time / 1000; + $summary['@autocommit_time'] = format_interval($max_time / 1000); + $deletes_id_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesById"]'); + $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath)); + $deletes_query_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesByQuery"]'); + $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath)); + $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query']; + $schema = $system_info->core->schema; + $summary['@schema_version'] = $schema; + $core = $stats->xpath('//lst["core"]/str[@name="coreName"]'); + $summary['@core_name'] = trim(current($core)); + $size_xpath = $stats->xpath('//lst["core"]/str[@name="indexSize"]'); + $summary['@index_size'] = trim(current($size_xpath)); + } + } + + return $summary; + } + + /** + * Clear cached Solr data. + */ + public function clearCache() { + // Don't clear cached data if the server is unavailable. + if (@$this->ping()) { + $this->_clearCache(); + } + else { + throw new Exception('No Solr instance available when trying to clear the cache.'); + } + } + + protected function _clearCache() { + if ($this->env_id) { + cache_clear_all($this->env_id . ":stats:", 'cache_solrsearch', TRUE); + cache_clear_all($this->env_id . ":luke:", 'cache_solrsearch', TRUE); + } + $this->luke = array(); + $this->stats = NULL; + } + + /** + * Constructor + * + * @param $url + * The URL to the Solr server, possibly including a core name. E.g. http://localhost:8983/solr/ + * or https://search.example.com/solr/core99/ + * @param $env_id + * The machine name of a corresponding saved configuration used for loading + * data like which facets are enabled. + */ + public function __construct($url, $env_id = NULL) { + $this->env_id = $env_id; + $this->setUrl($url); + + // determine our default http timeout from ini settings + $this->_defaultTimeout = (int) ini_get('default_socket_timeout'); + + // double check we didn't get 0 for a timeout + if ($this->_defaultTimeout <= 0) { + $this->_defaultTimeout = 60; + } + } + + function getId() { + return $this->env_id; + } + + /** + * Check the reponse code and thow an exception if it's not 200. + * + * @param stdClass $response + * response object. + * + * @return + * response object + * @thows Exception + */ + protected function checkResponse($response) { + $code = (int) $response->code; + if ($code != 200) { + if ($code >= 400 && $code != 403 && $code != 404) { + // Add details, like Solr's exception message. + $response->status_message .= $response->data; + } + throw new Exception('"' . $code . '" Status: ' . $response->status_message); + } + return $response; + } + + /** + * Make a request to a servlet (a path) that's not a standard path. + * + * @param string $servlet + * A path to be added to the base Solr path. e.g. 'extract/tika' + * + * @param array $params + * Any request parameters when constructing the URL. + * + * @param array $options + * @see drupal_http_request() $options. + * + * @return + * response object + * + * @thows Exception + */ + public function makeServletRequest($servlet, $params = array(), $options = array()) { + // Add default params. + $params += array( + 'wt' => 'json', + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + + $url = $this->_constructUrl($servlet, $params); + $response = $this->_makeHttpRequest($url, $options); + return $this->checkResponse($response); + } + + /** + * Central method for making a GET operation against this Solr Server + */ + protected function _sendRawGet($url, $options = array()) { + $response = $this->_makeHttpRequest($url, $options); + return $this->checkResponse($response); + } + + /** + * Central method for making a POST operation against this Solr Server + */ + protected function _sendRawPost($url, $options = array()) { + $options['method'] = 'POST'; + // Normally we use POST to send XML documents. + if (!isset($options['headers']['Content-Type'])) { + $options['headers']['Content-Type'] = 'text/xml; charset=UTF-8'; + } + $response = $this->_makeHttpRequest($url, $options); + return $this->checkResponse($response); + } + + /** + * Central method for making the actual http request to the Solr Server + * + * This is just a wrapper around drupal_http_request(). + */ + protected function _makeHttpRequest($url, array $options = array()) { + if (!isset($options['method']) || $options['method'] == 'GET' || $options['method'] == 'HEAD') { + // Make sure we are not sending a request body. + $options['data'] = NULL; + } + + $result = drupal_http_request($url, $options); + + if (!isset($result->code) || $result->code < 0) { + $result->code = 0; + $result->status_message = 'Request failed'; + $result->protocol = 'HTTP/1.0'; + } + // Additional information may be in the error property. + if (isset($result->error)) { + $result->status_message .= ': ' . check_plain($result->error); + } + + if (!isset($result->data)) { + $result->data = ''; + $result->response = NULL; + } + else { + $response = json_decode($result->data); + if (is_object($response)) { + foreach ($response as $key => $value) { + $result->$key = $value; + } + } + } + return $result; + } + + + /** + * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. + * + * NOTE: inside a phrase fewer characters need escaped, use {@link DrupalsolrsearchService::escapePhrase()} instead + * + * @param string $value + * @return string + */ + static public function escape($value) + { + //list taken from http://lucene.apache.org/java/docs/queryparsersyntax.html#Escaping%20Special%20Characters + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } + + /** + * Escape a value meant to be contained in a phrase for special query characters + * + * @param string $value + * @return string + */ + static public function escapePhrase($value) + { + $pattern = '/("|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } + + /** + * Convenience function for creating phrase syntax from a value + * + * @param string $value + * @return string + */ + static public function phrase($value) + { + return '"' . self::escapePhrase($value) . '"'; + } + + /** + * Return a valid http URL given this server's host, port and path and a provided servlet name + * + * @param $servlet + * A string path to a Solr request handler. + * @param $params + * @param $parsed_url + * A url to use instead of the stored one. + * + * @return string + */ + protected function _constructUrl($servlet, $params = array(), $added_query_string = NULL) { + // PHP's built in http_build_query() doesn't give us the format Solr wants. + $query_string = $this->httpBuildQuery($params); + + if ($query_string) { + $query_string = '?' . $query_string; + if ($added_query_string) { + $query_string = $query_string . '&' . $added_query_string; + } + } + elseif ($added_query_string) { + $query_string = '?' . $added_query_string; + } + + $url = $this->parsed_url; + return $url['scheme'] . $url['user'] . $url['pass'] . $url['host'] . $url['port'] . $url['path'] . $servlet . $query_string; + } + + /** + * Get the Solr url + * + * @return string + */ + public function getUrl() { + return $this->_constructUrl(''); + } + + /** + * Set the Solr url. + * + * @param $url + * + * @return $this + */ + public function setUrl($url) { + $parsed_url = parse_url($url); + + if (!isset($parsed_url['scheme'])) { + $parsed_url['scheme'] = 'http'; + } + $parsed_url['scheme'] .= '://'; + + if (!isset($parsed_url['user'])) { + $parsed_url['user'] = ''; + } + else { + $parsed_url['host'] = '@' . $parsed_url['host']; + } + $parsed_url['pass'] = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; + $parsed_url['port'] = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + + if (isset($parsed_url['path'])) { + // Make sure the path has a single leading/trailing slash. + $parsed_url['path'] = '/' . ltrim($parsed_url['path'], '/'); + $parsed_url['path'] = rtrim($parsed_url['path'], '/') . '/'; + } + else { + $parsed_url['path'] = '/'; + } + // For now we ignore query and fragment. + $this->parsed_url = $parsed_url; + // Force the update url to be rebuilt. + unset($this->update_url); + return $this; + } + + /** + * Raw update Method. Takes a raw post body and sends it to the update service. Post body + * should be a complete and well formed xml document. + * + * @param string $rawPost + * @param float $timeout Maximum expected duration (in seconds) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function update($rawPost, $timeout = FALSE) { + // @todo: throw exception if updates are disabled. + if (empty($this->update_url)) { + // Store the URL in an instance variable since many updates may be sent + // via a single instance of this class. + $this->update_url = $this->_constructUrl(self::UPDATE_SERVLET, array('wt' => 'json')); + } + $options['data'] = $rawPost; + if ($timeout) { + $options['timeout'] = $timeout; + } + return $this->_sendRawPost($this->update_url, $options); + } + + /** + * Add an array of Solr Documents to the index all at once + * + * @param array $documents Should be an array of solrsearchDocument instances + * @param boolean $allowDups + * @param boolean $overwritePending + * @param boolean $overwriteCommitted + * + * @return response objecte + * + * @throws Exception If an error occurs during the service call + */ + public function addDocuments($documents, $overwrite = NULL, $commitWithin = NULL) { + $attr = ''; + + if (isset($overwrite)) { + $attr .= ' overwrite="' . empty($overwrite) ? 'false"' : 'true"'; + } + if (isset($commitWithin)) { + $attr .= ' commitWithin="' . intval($commitWithin) . '"'; + } + + $rawPost = ""; + foreach ($documents as $document) { + if (is_object($document) && ($document instanceof solrsearchDocument)) { + $rawPost .= solrsearchDocument::documentToXml($document); + } + } + $rawPost .= ''; + + return $this->update($rawPost); + } + + /** + * Send a commit command. Will be synchronous unless both wait parameters are set to false. + * + * @param boolean $optimize Defaults to true + * optimizes the index files. Only valid for solr versions <= 3 + * @param boolean $waitFlush + * block until index changes are flushed to disk. Only valid for solr versions <= 3 + * @param boolean $waitSearcher + * block until a new searcher is opened and registered as the main query searcher, making the changes visible. + * @param float $timeout + * Maximum expected duration of the commit operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function commit($optimize = TRUE, $waitFlush = TRUE, $waitSearcher = TRUE, $timeout = 3600) { + $optimizeValue = $optimize ? 'true' : 'false'; + $flushValue = $waitFlush ? 'true' : 'false'; + $searcherValue = $waitSearcher ? 'true' : 'false'; + $softCommit = $this->soft_commit ? 'true' : 'false'; + + $solr_version = $this->getSolrVersion(); + if ($solr_version <= 3) { + $rawPost = ''; + } + else { + $rawPost = ''; + } + + $response = $this->update($rawPost, $timeout); + $this->_clearCache(); + return $response; + } + + /** + * Create a delete document based on document ID + * + * @param string $id Expected to be utf-8 encoded + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function deleteById($id, $timeout = 3600) { + return $this->deleteByMultipleIds(array($id), $timeout); + } + + /** + * Create and post a delete document based on multiple document IDs. + * + * @param array $ids Expected to be utf-8 encoded strings + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function deleteByMultipleIds($ids, $timeout = 3600) { + $rawPost = ''; + + foreach ($ids as $id) { + $rawPost .= '' . htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8') . ''; + } + $rawPost .= ''; + + return $this->update($rawPost, $timeout); + } + + /** + * Create a delete document based on a query and submit it + * + * @param string $rawQuery Expected to be utf-8 encoded + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * @return stdClass response object + * + * @throws Exception If an error occurs during the service call + */ + public function deleteByQuery($rawQuery, $timeout = 3600) { + $rawPost = '' . htmlspecialchars($rawQuery, ENT_NOQUOTES, 'UTF-8') . ''; + + return $this->update($rawPost, $timeout); + } + + /** + * Send an optimize command. Will be synchronous unless both wait parameters are set + * to false. + * + * @param boolean $waitFlush + * block until index changes are flushed to disk Removed in Solr 4.0 + * @param boolean $waitSearcher + * block until a new searcher is opened and registered as the main query searcher, making the changes visible. + * @param float $timeout + * Maximum expected duration of the commit operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function optimize($waitFlush = TRUE, $waitSearcher = TRUE, $timeout = 3600) { + $flushValue = $waitFlush ? 'true' : 'false'; + $searcherValue = $waitSearcher ? 'true' : 'false'; + $softCommit = $this->soft_commit ? 'true' : 'false'; + + $solr_version = $this->getSolrVersion(); + if ($solr_version <= 3) { + $rawPost = ''; + } + else { + $rawPost = ''; + } + + return $this->update($rawPost, $timeout); + } + + /** + * Like PHP's built in http_build_query(), but uses rawurlencode() and no [] for repeated params. + */ + protected function httpBuildQuery(array $query, $parent = '') { + $params = array(); + + foreach ($query as $key => $value) { + $key = ($parent ? $parent : rawurlencode($key)); + + // Recurse into children. + if (is_array($value)) { + $params[] = $this->httpBuildQuery($value, $key); + } + // If a query parameter value is NULL, only append its key. + elseif (!isset($value)) { + $params[] = $key; + } + else { + $params[] = $key . '=' . rawurlencode($value); + } + } + + return implode('&', $params); + } + + /** + * Simple Search interface + * + * @param string $query The raw query string + * @param array $params key / value pairs for other query parameters (see Solr documentation), use arrays for parameter keys used more than once (e.g. facet.field) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + public function search($query = '', array $params = array(), $method = 'GET') { + // Always use JSON. See http://code.google.com/p/solr-php-client/issues/detail?id=6#c1 for reasoning + + + + if (!user_access("view restricted content")){ + $params['fq'][]='access-type:free'; + + + } + + $params['wt'] = 'json'; + // Additional default params. + $params += array( + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + if ($query) { + $params['q'] = $query; + } + // PHP's built in http_build_query() doesn't give us the format Solr wants. + $queryString = $this->httpBuildQuery($params); + // Check string length of the query string, change method to POST + $len = strlen($queryString); + // Fetch our threshold to find out when to flip to POST + $max_len = solrsearch_environment_variable_get($this->env_id, 'solrsearch_search_post_threshold', 3600); + + // if longer than $max_len (default 3600) characters + // we should switch to POST (a typical server handles 4096 max). + // If this class is used independently (without environments), we switch automatically to POST at an + // limit of 1800 chars. + if (($len > 1800) && (empty($this->env_id) || ($len > $max_len))) { + $method = 'POST'; + } + + if ($method == 'GET') { + $searchUrl = $this->_constructUrl(self::SEARCH_SERVLET, array(), $queryString); + + return $this->_sendRawGet($searchUrl); + } + else if ($method == 'POST') { + $searchUrl = $this->_constructUrl(self::SEARCH_SERVLET); + $options['data'] = $queryString; + $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + return $this->_sendRawPost($searchUrl, $options); + } + else { + throw new Exception("Unsupported method '$method' for search(), use GET or POST"); + } + } +} diff -r 000000000000 -r a2b4f67e73dc Solr_Base_Query.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Solr_Base_Query.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,657 @@ + 'is_uid', '#value' => 0) + * for anonymous content. + */ + protected $fields = array(); + + /** + * An array of subqueries. + */ + protected $subqueries = array(); + + function __construct($operator = 'OR') { + $this->operator = $operator; + $this->id = ++SolrFilterSubQuery::$idCount; + } + + function __clone() { + $this->id = ++SolrFilterSubQuery::$idCount; + } + + public function getFilters($name = NULL) { + if (empty($name)) { + return $this->fields; + } + reset($this->fields); + $matches = array(); + foreach ($this->fields as $filter) { + if ($filter['#name'] == $name) { + $matches[] = $filter; + } + } + return $matches; + } + + public function hasFilter($name, $value, $exclude = FALSE) { + foreach ($this->fields as $pos => $values) { + if ($values['#name'] == $name && $values['#value'] == $value && $values['#exclude'] == $exclude) { + return TRUE; + } + } + return FALSE; + } + + public function addFilter($name, $value, $exclude = FALSE, $local = '') { + // @todo - escape the value if it has spaces in it and is not a range query or parenthesized. + $filter = array( + '#exclude' => (bool) $exclude, + '#name' => trim($name), + '#value' => trim($value), + '#local' => trim($local), + ); + $this->fields[] = $filter; + return $this; + } + + public function removeFilter($name, $value = NULL, $exclude = FALSE) { + // Remove from the public list of filters. + $this->unsetFilter($this->fields, $name, $value, $exclude); + return $this; + } + + protected function unsetFilter(&$fields, $name, $value, $exclude) { + if (!isset($value)) { + foreach ($fields as $pos => $values) { + if ($values['#name'] == $name) { + unset($fields[$pos]); + } + } + } + else { + foreach ($fields as $pos => $values) { + if ($values['#name'] == $name && $values['#value'] == $value && $values['#exclude'] == $exclude) { + unset($fields[$pos]); + } + } + } + } + + public function getFilterSubQueries() { + return $this->subqueries; + } + + public function addFilterSubQuery(SolrFilterSubQuery $query) { + $this->subqueries[$query->id] = $query; + return $this; + } + + public function removeFilterSubQuery(SolrFilterSubQuery $query) { + unset($this->subqueries[$query->id]); + return $this; + } + + public function removeFilterSubQueries() { + $this->subqueries = array(); + return $this; + } + + public function makeFilterQuery(array $filter) { + $prefix = empty($filter['#exclude']) ? '' : '-'; + if ($filter['#local']) { + $prefix = '{!' . $filter['#local'] . '}' . $prefix; + } + // If the field value contains a colon or a space, wrap it in double quotes, + // unless it is a range query or is already wrapped in double quotes or + // parentheses. + if (preg_match('/[ :]/', $filter['#value']) && !preg_match('/^[\[\{]\S+ TO \S+[\]\}]$/', $filter['#value']) && !preg_match('/^["\(].*["\)]$/', $filter['#value'])) { + $filter['#value'] = '"' . $filter['#value'] . '"'; + } + return $prefix . $filter['#name'] . ':' . $filter['#value']; + } + + /** + * Make sure our query matches the pattern name:value or name:"value" + * Make sure that if we are ranges we use name:[ AND ] + * allowed inputs : + * a. bundle:article + * b. date:[1970-12-31T23:59:59Z TO NOW] + * Split the text in 4 different parts + * 1. name, eg.: bundle or date + * 2. The first opening bracket (or nothing), eg.: [ + * 3. The value of the field, eg. article or 1970-12-31T23:59:59Z TO NOW + * 4. The last closing bracket, eg.: ] + * @param string $filter + * The filter to validate + * @return boolean + */ + public static function validFilterValue($filter) { + $opening = 0; + $closing = 0; + $name = NULL; + $value = NULL; + + if (preg_match('/(?P[^:]+):(?P.+)?$/', $filter, $matches)) { + foreach ($matches as $match_id => $match) { + switch($match_id) { + case 'name' : + $name = $match; + break; + case 'value' : + $value = $match; + break; + } + } + + // For the name we allow any character that fits between the A-Z0-9 range and + // any alternative for this in other languages. No special characters allowed + if (!preg_match('/^[a-zA-Z0-9_\x7f-\xff]+$/', $name)) { + return FALSE; + } + + // For the value we allow anything that is UTF8 + if (!drupal_validate_utf8($value)) { + return FALSE; + } + + // Check our bracket count. If it does not match it is also not valid + $valid_brackets = TRUE; + $brackets['opening']['{'] = substr_count($value, '{'); + $brackets['closing']['}'] = substr_count($value, '}'); + $valid_brackets = ($brackets['opening']['{'] != $brackets['closing']['}']) ? FALSE : TRUE; + $brackets['opening']['['] = substr_count($value, '['); + $brackets['closing'][']'] = substr_count($value, ']'); + $valid_brackets = ($brackets['opening']['['] != $brackets['closing'][']']) ? FALSE : TRUE; + $brackets['opening']['('] = substr_count($value, '('); + $brackets['closing'][')'] = substr_count($value, ')'); + $valid_brackets = ($brackets['opening']['('] != $brackets['closing'][')']) ? FALSE : TRUE; + if (!$valid_brackets) { + return FALSE; + } + + // Check the date field inputs + if (preg_match('/\[(.+) TO (.+)\]$/', $value, $datefields)) { + // Only Allow a value in the form of + // http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html + // http://lucene.apache.org/solr/api/org/apache/solr/util/DateMathParser.html + // http://wiki.apache.org/solr/SolrQuerySyntax + // 1976-03-06T23:59:59.999Z (valid) + // * (valid) + // 1995-12-31T23:59:59.999Z (valid) + // 2007-03-06T00:00:00Z (valid) + // NOW-1YEAR/DAY (valid) + // NOW/DAY+1DAY (valid) + // 1976-03-06T23:59:59.999Z (valid) + // 1976-03-06T23:59:59.999Z+1YEAR (valid) + // 1976-03-06T23:59:59.999Z/YEAR (valid) + // 1976-03-06T23:59:59.999Z (valid) + // 1976-03-06T23::59::59.999Z (invalid) + if (!empty($datefields[1]) && !empty($datefields[2])) { + // Do not check to full value, only the splitted ones + unset($datefields[0]); + // Check if both matches are valid datefields + foreach ($datefields as $datefield) { + if (!preg_match('/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:[\d\.]{2,6}Z(\S)*)|(^([A-Z\*]+)(\A-Z0-9\+\-\/)*)/', $datefield, $datefield_match)) { + return FALSE; + } + } + } + } + } + return TRUE; + } + + /** + * Builds a set of filter queries from $this->fields and all subqueries. + * + * Returns an array of strings that can be combined into + * a URL query parameter or passed to Solr as fq paramters. + */ + protected function rebuildFq() { + $fq = array(); + foreach ($this->fields as $pos => $field) { + $fq[] = $this->makeFilterQuery($field); + } + foreach ($this->subqueries as $subquery) { + $subfq = $subquery->rebuildFq(); + if ($subfq) { + $operator = $subquery->operator; + $fq[] = "(" . implode(" $operator ", $subfq) . ")"; + } + } + return $fq; + } + +} + +class SolrBaseQuery extends SolrFilterSubQuery implements DrupalSolrQueryInterface { + + /** + * The parameters that get sent to Solr. + */ + protected $params = array('start' => 0, 'rows' => 10, 'fq' => array()); + + /** + * The search base path. + */ + protected $base_path; + protected $field_map = array(); + + /** + * DrupalApacheSolrService object + */ + protected $solr; + // The array keys must always be real Solr index fields. + protected $available_sorts; + + /** + * The query name is used to construct a searcher string. Mostly the + * environment id + */ + protected $name; + protected $context = array(); + // Makes sure we always have a valid sort. + protected $solrsort = array('#name' => 'score', '#direction' => 'desc'); + // A flag to allow the search to be aborted. + public $abort_search = FALSE; + + // A flag to check if need to retrieve another page of the result set + public $page = 0; + + /** + * @param $name + * The search name, used for finding the correct blocks and other config. + * Typically "apachesolr". + * + * @param $solr + * An instantiated DrupalApacheSolrService Object. + * Can be instantiated from apachesolr_get_solr(). + * + * @param $params + * Array of params to initialize the object (typically 'q' and 'fq'). + * + * @param $sortstring + * Visible string telling solr how to sort - added to GET query params. + * + * @param $base_path + * The search base path (without the keywords) for this query, without trailing slash. + */ + function __construct($name, $solr, array $params = array(), $sortstring = '', $base_path = '', $context = array()) { + parent::__construct(); + $this->name = $name; + $this->solr = $solr; + $this->addContext((array) $context); + $this->addParams((array) $params); + $this->available_sorts = $this->defaultSorts(); + $this->sortstring = trim($sortstring); + $this->parseSortString(); + $this->base_path = $base_path; + } + + protected function defaultSorts() { + return array( + 'score' => array('title' => t('Relevancy'), 'default' => 'desc'), + 'title' => array('title' => t('Title'), 'default' => 'asc'), + 'author_s' => array('title' => t('Author'), 'default' => 'asc'), + 'date' => array('title' => t('Date'), 'default' => 'desc'), + ); + } + + /** + * Get query name. + */ + public function getName() { + return $this->name; + } + + /** + * Get query searcher name (for facetapi, views, pages, etc). + */ + public function getSearcher() { + return $this->name . '@' . $this->solr->getId(); + } + + /** + * Get context values. + */ + public function getContext() { + return $this->context; + } + + /** + * Set context value. + */ + public function addContext(array $context) { + foreach ($context as $k => $v) { + $this->context[$k] = $v; + } + // The env_id must match that of the actual $solr object + $this->context['env_id'] = $this->solr->getId(); + return $this->context; + } + + protected $single_value_params = array( + 'q' => TRUE, // http://wiki.apache.org/solr/SearchHandler#q + 'q.op' => TRUE, // http://wiki.apache.org/solr/SearchHandler#q.op + 'q.alt' => TRUE, // http://wiki.apache.org/solr/SearchHandler#q + 'df' => TRUE, + 'qt' => TRUE, + 'defType' => TRUE, + 'timeAllowed' => TRUE, + 'omitHeader' => TRUE, + 'debugQuery' => TRUE, + 'start' => TRUE, + 'rows' => TRUE, + 'stats' => TRUE, + 'facet' => TRUE, + 'facet.prefix' => TRUE, + 'facet.limit' => TRUE, + 'facet.offset' => TRUE, + 'facet.mincount' => TRUE, + 'facet.missing' => TRUE, + 'facet.method' => TRUE, + 'facet.enum.cache.minDf' => TRUE, + 'facet.date.start' => TRUE, + 'facet.date.end' => TRUE, + 'facet.date.gap' => TRUE, + 'facet.date.hardend' => TRUE, + 'facet.date.other' => TRUE, + 'facet.date.include' => TRUE, + 'hl' => TRUE, + 'hl.snippets' => TRUE, + 'hl.fragsize' => TRUE, + 'hl.mergeContiguous' => TRUE, + 'hl.requireFieldMatch' => TRUE, + 'hl.maxAnalyzedChars' => TRUE, + 'hl.alternateField' => TRUE, + 'hl.maxAlternateFieldLength' => TRUE, + 'hl.formatter' => TRUE, + 'hl.simple.pre/hl.simple.post' => TRUE, + 'hl.fragmenter' => TRUE, + 'hl.fragListBuilder' => TRUE, + 'hl.fragmentsBuilder' => TRUE, + 'hl.useFastVectorHighlighter' => TRUE, + 'hl.usePhraseHighlighter' => TRUE, + 'hl.highlightMultiTerm' => TRUE, + 'hl.regex.slop' => TRUE, + 'hl.regex.pattern' => TRUE, + 'hl.regex.maxAnalyzedChars' => TRUE, + 'spellcheck' => TRUE, + ); + + public function getParam($name) { + if ($name == 'fq') { + return $this->rebuildFq(); + } + $empty = isset($this->single_value_params[$name]) ? NULL : array(); + return isset($this->params[$name]) ? $this->params[$name] : $empty; + } + + public function getParams() { + $params = $this->params; + $params['fq'] = $this->rebuildFq(); + return $params; + } + + public function getSolrParams() { + $params = $this->getParams(); + // For certain fields Solr prefers a comma separated list. + foreach (array('fl', 'hl.fl', 'sort', 'mlt.fl') as $name) { + if (isset($params[$name])) { + $params[$name] = implode(',', $params[$name]); + } + } + return $params; + } + + protected function addFq($string, $index = NULL) { + $string = trim($string); + $local = ''; + $exclude = FALSE; + $name = NULL; + $value = NULL; + + // Check if we are dealing with an exclude + if (preg_match('/^-(.*)/', $string, $matches)) { + $exclude = TRUE; + $string = $matches[1]; + } + + // If {!something} is found as first character then this is a local value + if (preg_match('/\{!([^}]+)\}(.*)/', $string, $matches)) { + $local = $matches[1]; + $string = $matches[2]; + } + + // Anything that has a name and value + // check if we have a : in the string + if (strstr($string, ':')) { + list($name, $value) = explode(":", $string, 2); + } + else { + $value = $string; + } + $this->addFilter($name, $value, $exclude, $local); + return $this; + } + + public function addParam($name, $value) { + if (isset($this->single_value_params[$name])) { + if (is_array($value)) { + $value = end($value); + } + $this->params[$name] = $this->normalizeParamValue($value); + return $this; + } + // We never actually populate $this->params['fq']. Instead + // we manage everything via the filter methods. + if ($name == 'fq') { + if (is_array($value)) { + array_walk_recursive($value, array($this, 'addFq')); + return $this; + } + else { + return $this->addFq($value); + } + } + + if (!isset($this->params[$name])) { + $this->params[$name] = array(); + } + + if (!is_array($value)) { + // Convert to array for array_map. + $param_values = array($value); + } + else { + // Convert to a numerically keyed array. + $param_values = array_values($value); + } + $this->params[$name] = array_merge($this->params[$name], array_map(array($this, 'normalizeParamValue'), $param_values)); + + return $this; + } + + protected function normalizeParamValue($value) { + // Convert boolean to string. + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + // Convert to trimmed string. + return trim($value); + } + + public function addParams(Array $params) { + foreach ($params as $name => $value) { + $this->addParam($name, $value); + } + return $this; + } + + public function removeParam($name) { + unset($this->params[$name]); + if ($name == 'fq') { + $this->fields = array(); + $this->subqueries = array(); + } + return $this; + } + + public function replaceParam($name, $value) { + $this->removeParam($name); + return $this->addParam($name, $value); + } + + /** + * Handles aliases for field to make nicer URLs. + * + * @param $field_map + * An array keyed with real Solr index field names with the alias as value. + * + * @return DrupalSolrQueryInterface + * The called object. + */ + public function addFieldAliases($field_map) { + $this->field_map = array_merge($this->field_map, $field_map); + // We have to re-parse the filters. + $this->parseSortString(); + return $this; + } + + public function getFieldAliases() { + return $this->field_map; + } + + public function clearFieldAliases() { + $this->field_map = array(); + // We have to re-parse the filters. + $this->parseSortString(); + return $this; + } + + protected function parseSortString() { + // Substitute any field aliases with real field names. + $sortstring = strtr($this->sortstring, $this->field_map); + // Score is a special case - it's the default sort for Solr. + if ('' == $sortstring || 'score desc' == $sortstring) { + $this->solrsort['#name'] = 'score'; + $this->solrsort['#direction'] = 'desc'; + unset($this->params['sort']); + } + else { + // Validate and set sort parameter + $fields = implode('|', array_keys($this->available_sorts)); + if (preg_match('/^(?:(' . $fields . ') (asc|desc),?)+$/', $sortstring, $matches)) { + // We only use the last match. + $this->solrsort['#name'] = $matches[1]; + $this->solrsort['#direction'] = $matches[2]; + $this->params['sort'] = array($sortstring); + } + } + } + + public function getAvailableSorts() { + return $this->available_sorts; + } + + public function setAvailableSort($name, $sort) { + // We expect non-aliased sorts to be added. + $this->available_sorts[$name] = $sort; + // Re-parse the sortstring. + $this->parseSortString(); + return $this; + } + + public function setAvailableSorts($sorts) { + // We expect a complete array of valid sorts. + $this->available_sorts = $sorts; + $this->parseSortString(); + return $this; + } + + public function removeAvailableSort($name) { + unset($this->available_sorts[$name]); + // Re-parse the sortstring. + $this->parseSortString(); + return $this; + } + + public function getSolrsort() { + return $this->solrsort; + } + + public function setSolrsort($name, $direction) { + $this->sortstring = trim($name) . ' ' . trim($direction); + $this->parseSortString(); + return $this; + } + + public function getPath($new_keywords = NULL) { + if (isset($new_keywords)) { + return $this->base_path . '/' . $new_keywords; + } + elseif ($this->getParam('q')) { + return $this->base_path . '/' . $this->getParam('q'); + } + else { + // Return with empty query (the slash). The path for a facet + // becomes $this->base_path . '//facetinfo'; + // We do this so we can have a consistent way of retrieving the query + + // additional parameters + return $this->base_path . '/'; + } + } + + public function getSolrsortUrlQuery() { + $queryvalues = array(); + $solrsort = $this->solrsort; + if ($solrsort && ($solrsort['#name'] != 'score')) { + if (isset($this->field_map[$solrsort['#name']])) { + $solrsort['#name'] = $this->field_map[$solrsort['#name']]; + } + $queryvalues['solrsort'] = $solrsort['#name'] . ' ' . $solrsort['#direction']; + } + else { + // Return to default relevancy sort. + unset($queryvalues['solrsort']); + } + return $queryvalues; + } + + public function search($keys = NULL) { + if ($this->abort_search) { + return NULL; + } + + return $this->solr->search($keys, $this->getSolrParams()); + } + + public function solr($method) { + return $this->solr->$method(); + } + +} diff -r 000000000000 -r a2b4f67e73dc apachesolr.admin.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/apachesolr.admin.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1342 @@ + 'value', + '#value' => $environment['env_id'], + ); + if (isset($environment['export_type']) && $environment['export_type'] == 3) { + $verb = t('Revert'); + } + else { + $verb = t('Delete'); + } + return confirm_form( + $form, + t('Are you sure you want to !verb search environment %name?', array('%name' => $environment['name'], '!verb' => strtolower($verb))), + 'admin/config/search/apachesolr', + t('This action cannot be undone.'), + $verb, + t('Cancel') + ); +} + +/** + * Submit handler for the delete form + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_delete_form_submit(array $form, array &$form_state) { + if (apachesolr_environment_delete($form_state['values']['env_id'])) { + drupal_set_message(t('The search environment was deleted')); + } + $form_state['redirect'] = 'admin/config/search/apachesolr/settings'; +} + +function apachesolr_environment_edit_delete_submit($form, &$form_state) { + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $form_state['values']['env_id'] . '/delete'; + + // Regardlessly of the destination parameter we want to go to another page + unset($_GET['destination']); + drupal_static_reset('drupal_get_destination'); + drupal_get_destination(); +} + +/** + * Settings page for a specific environment (or default one if not provided) + * + * @param array|bool $environment + * + * @return array Render array for a settings page + */ +function apachesolr_environment_settings_page(array $environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + $environment = apachesolr_environment_load($env_id); + } + $env_id = $environment['env_id']; + + // Initializes output with information about which environment's setting we are + // editing, as it is otherwise not transparent to the end user. + $output = array( + 'apachesolr_environment' => array( + '#theme' => 'apachesolr_settings_title', + '#env_id' => $env_id, + ), + ); + $output['form'] = drupal_get_form('apachesolr_environment_edit_form', $environment); + return $output; +} + +/** + * Form to clone a certain environment + * + * @param array $form + * @param array $form_state + * @param array $environment + * + * @return array output of confirm_form() + */ +function apachesolr_environment_clone_form(array $form, array &$form_state, array $environment) { + $form['env_id'] = array( + '#type' => 'value', + '#value' => $environment['env_id'], + ); + return confirm_form( + $form, + t('Are you sure you want to clone search environment %name?', array('%name' => $environment['name'])), + 'admin/config/search/apachesolr', + '', + t('Clone'), + t('Cancel') + ); +} + +/** + * Submit handler for the clone form + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_clone_form_submit(array $form, array &$form_state) { + if (apachesolr_environment_clone($form_state['values']['env_id'])) { + drupal_set_message(t('The search environment was cloned')); + } + $form_state['redirect'] = 'admin/config/search/apachesolr/settings'; +} + +/** + * Submit handler for the confirmation page of cloning an environment + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_clone_submit(array $form, array &$form_state) { + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $form_state['values']['env_id'] . '/clone'; +} + +/** + * Form builder for adding/editing a Solr environment used as a menu callback. + */ +function apachesolr_environment_edit_form(array $form, array &$form_state, array $environment = array()) { + if (empty($environment)) { + $environment = array(); + } + $environment += array('env_id' => '', 'name' => '', 'url' => '', 'service_class' => '', 'conf' => array()); + + $form['#environment'] = $environment; + $form['url'] = array( + '#type' => 'textfield', + '#title' => t('Solr server URL'), + '#default_value' => $environment['url'], + '#description' => t('Example: http://localhost:8983/solr'), + '#required' => TRUE, + ); + $is_default = $environment['env_id'] == apachesolr_default_environment(); + $form['make_default'] = array( + '#type' => 'checkbox', + '#title' => t('Make this Solr search environment the default'), + '#default_value' => $is_default, + '#disabled' => $is_default, + ); + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Description'), + '#default_value' => $environment['name'], + '#required' => TRUE, + ); + $form['env_id'] = array( + '#type' => 'machine_name', + '#title' => t('Environment id'), + '#machine_name' => array( + 'exists' => 'apachesolr_environment_load', + ), + '#default_value' => $environment['env_id'], + '#disabled' => !empty($environment['env_id']), // Cannot change it once set. + '#description' => t('Unique, machine-readable identifier for this Solr environment.'), + '#required' => TRUE, + ); + $form['service_class'] = array( + '#type' => 'value', + '#value' => $environment['service_class'], + ); + $form['conf'] = array( + '#tree' => TRUE, + ); + $form['conf']['apachesolr_read_only'] = array( + '#type' => 'radios', + '#title' => t('Index write access'), + '#default_value' => isset($environment['conf']['apachesolr_read_only']) ? $environment['conf']['apachesolr_read_only'] : APACHESOLR_READ_WRITE, + '#options' => array(APACHESOLR_READ_WRITE => t('Read and write (normal)'), APACHESOLR_READ_ONLY => t('Read only')), + '#description' => t('Read only stops this site from sending updates to this search environment. Useful for development sites.'), + ); + $form['actions'] = array( + '#type' => 'actions', + ); + $form['actions']['save'] = array( + '#type' => 'submit', + '#validate' => array('apachesolr_environment_edit_validate'), + '#submit' => array('apachesolr_environment_edit_submit'), + '#value' => t('Save'), + ); + $form['actions']['save_edit'] = array( + '#type' => 'submit', + '#validate' => array('apachesolr_environment_edit_validate'), + '#submit' => array('apachesolr_environment_edit_submit'), + '#value' => t('Save and edit'), + ); + $form['actions']['test'] = array( + '#type' => 'submit', + '#validate' => array('apachesolr_environment_edit_validate'), + '#submit' => array('apachesolr_environment_edit_test_submit'), + '#value' => t('Test connection'), + ); + if (!empty($environment['env_id']) && !$is_default) { + $form['actions']['delete'] = array( + '#type' => 'submit', + '#submit' => array('apachesolr_environment_edit_delete_submit'), + '#value' => t('Delete'), + ); + } + + // Ensures destination is an internal URL, builds "cancel" link. + if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) { + $destination = $_GET['destination']; + } + else { + $destination = 'admin/config/search/apachesolr/settings'; + } + $form['actions']['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#href' => $destination, + ); + + return $form; +} + +/** + * Submit handler for the test button in the environment edit page + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_edit_test_submit(array $form, array &$form_state) { + $ping = apachesolr_server_status($form_state['values']['url'], $form_state['values']['service_class']); + if ($ping) { + drupal_set_message(t('Your site has contacted the Apache Solr server.')); + } + else { + drupal_set_message(t('Your site was unable to contact the Apache Solr server.'), 'error'); + } + $form_state['rebuild'] = TRUE; +} + +/** + * Validate handler for the environment edit page + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_edit_validate(array $form, array &$form_state) { + $parts = parse_url($form_state['values']['url']); + foreach (array('scheme', 'host', 'path') as $key) { + if (empty($parts[$key])) { + form_set_error('url', t('The Solr server URL needs to include a !part', array('!part' => $key))); + } + } + if (isset($parts['port'])) { + // parse_url() should always give an integer for port. Since drupal_http_request() + // also uses parse_url(), we don't need to validate anything except the range. + $pattern = empty($parts['user']) ? '@://[^:]+:([^/]+)@' : '#://[^@]+@[^:]+:([^/]+)#'; + preg_match($pattern, $form_state['values']['url'], $m); + if (empty($m[1]) || !ctype_digit($m[1]) || $m[1] < 1 || $m[1] > 65535) { + form_set_error('port', t('The port has to be an integer between 1 and 65535.')); + } + else { + // Normalize the url by removing extra slashes and whitespace. + $form_state['values']['url'] = trim($form_state['values']['url'], "/ \t\r\n\0\x0B"); + } + } +} + +/** + * Submit handler for the environment edit page + * + * @param array $form + * @param array $form_state + */ +function apachesolr_environment_edit_submit(array $form, array &$form_state) { + apachesolr_environment_save($form_state['values']); + if (!empty($form_state['values']['make_default'])) { + apachesolr_set_default_environment($form_state['values']['env_id']); + } + cache_clear_all('apachesolr:environments', 'cache_apachesolr'); + drupal_set_message(t('The %name search environment has been saved.', array('%name' => $form_state['values']['name']))); + if ($form_state['values']['op'] == t('Save')) { + $form_state['redirect'] = 'admin/config/search/apachesolr/settings'; + } + else { + $form_state['redirect'] = current_path(); + } + // Regardlessly of the destination parameter we want to go to another page + unset($_GET['destination']); + drupal_static_reset('drupal_get_destination'); + drupal_get_destination(); +} + +/** + * Check to see if the facetapi module is installed, and if not put up + * a message. + * + * Only call this function if the user is already in a position for this to + * be useful. + */ +function apachesolr_check_facetapi() { + if (!module_exists('facetapi')) { + $filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'facetapi'", 0, 1) + ->fetchField(); + if ($filename && file_exists($filename)) { + drupal_set_message(t('If you enable the facetapi module, Apache Solr Search will provide you with configurable facets.', array('@modules' => url('admin/modules')))); + } + else { + drupal_set_message(t('If you install the facetapi module from !href, Apache Solr Search will provide you with configurable facets.', array('!href' => url('http://drupal.org/project/facetapi')))); + } + } +} + +/** + * Form builder for general settings used as a menu callback. + * + * @param array $form + * @param array $form_state + * + * @return array Output of the system_settings_form() + */ +function apachesolr_settings(array $form, array &$form_state) { + $form = array(); + $rows = array(); + + // Environment settings + $id = apachesolr_default_environment(); + $environments = apachesolr_load_all_environments(); + $default_environment = apachesolr_default_environment(); + apachesolr_check_facetapi(); + + // Reserve a row for the default one + $rows[$default_environment] = array(); + + foreach ($environments as $environment_id => $data) { + // Define all the Operations + $confs = array(); + $ops = array(); + // Whenever facetapi is enabled we also enable our operation link + if (module_exists('facetapi')) { + $confs['facets'] = array( + 'class' => 'operation', + 'data' => l(t('Facets'), + 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/facets', + array('query' => array('destination' => current_path())) + ), + ); + } + // These are our result and bias settings + if (module_exists('apachesolr_search')) { + $confs['result_bias'] = array( + 'class' => 'operation', + 'data' => l(t('Bias'), + 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/bias', + array('query' => array('destination' => current_path())) + ), + ); + } + $confs['index'] = array( + 'class' => 'operation', + 'data' => l(t('Index'), + 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/index' + ), + ); + $ops['edit'] = array( + 'class' => 'operation', + 'data' => l(t('Edit'), + 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/edit', + array('query' => array('destination' => current_path())) + ), + ); + + $ops['clone'] = array( + 'class' => 'operation', + 'data' => l(t('Clone'), + 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/clone', + array('query' => array('destination' => $_GET['q'])) + ), + ); + $env_name = l($data['name'], 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/edit', array('query' => array('destination' => $_GET['q']))); + + // Is this row our default environment? + if ($environment_id == $default_environment) { + $env_name = t('!environment (Default)', array('!environment' => $env_name)); + $env_class_row = 'default-environment'; + } + else { + $env_class_row = ''; + } + // For every non-default we add a delete link + // Allow to revert a search environment or to delete it + $delete_value = ''; + if (!isset($data['in_code_only'])) { + if ((isset($data['type']) && $data['type'] == 'Overridden')) { + $delete_value = array( + 'class' => 'operation', + 'data' => l(t('Revert'), 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/delete'), + ); + } + // don't allow the deletion of the default environment + elseif ($environment_id != $default_environment) { + $delete_value = array( + 'class' => 'operation', + 'data' => l(t('Delete'), 'admin/config/search/apachesolr/settings/' . $data['env_id'] . '/delete'), + ); + } + } + $ops['delete'] = $delete_value; + + // When we are receiving a http POST (so the page does not show) we do not + // want to check the statusses of any environment + $class = ''; + if (empty($form_state['input'])) { + $class = apachesolr_server_status($data['url'], $data['service_class']) ? 'ok' : 'error'; + } + + $headers = array( + array('data' => t('Name'), 'colspan' => 2), + t('URL'), + array('data' => t('Configuration'), 'colspan' => count($confs)), + array('data' => t('Operations'), 'colspan' => count($ops)), + ); + + $rows[$environment_id] = array('data' => + array( + // Cells + array( + 'class' => 'status-icon', + 'data' => '
' . $class . '
', + ), + array( + 'class' => $env_class_row, + 'data' => $env_name, + ), + check_plain($data['url']), + ), + 'class' => array(drupal_html_class($class)), + ); + // Add the links to the page + $rows[$environment_id]['data'] = array_merge($rows[$environment_id]['data'], $confs); + $rows[$environment_id]['data'] = array_merge($rows[$environment_id]['data'], $ops); + } + + $form['apachesolr_host_settings']['actions'] = array( + '#markup' => '', + ); + $form['apachesolr_host_settings']['table'] = array( + '#theme' => 'table', + '#header' => $headers, + '#rows' => array_values($rows), + '#attributes' => array('class' => array('admin-apachesolr')), + ); + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced configuration'), + '#collapsed' => TRUE, + '#collapsible' => TRUE, + ); + $form['advanced']['apachesolr_set_nodeapi_messages'] = array( + '#type' => 'radios', + '#title' => t('Extra help messages for administrators'), + '#description' => t('Adds notices to a page whenever Drupal changed content that needs reindexing'), + '#default_value' => variable_get('apachesolr_set_nodeapi_messages', 1), + '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), + ); + + // Number of Items to index + $numbers = drupal_map_assoc(array(1, 5, 10, 20, 50, 100, 200)); + $default_cron_limit = variable_get('apachesolr_cron_limit', 50); + + // apachesolr_cron_limit may be overridden in settings.php. If its current + // value is not among the default set of options, add it. + if (!isset($numbers[$default_cron_limit])) { + $numbers[$default_cron_limit] = $default_cron_limit; + } + $form['advanced']['apachesolr_cron_limit'] = array( + '#type' => 'select', + '#title' => t('Number of items to index per cron run'), + '#default_value' => $default_cron_limit, + '#options' => $numbers, + '#description' => t('Reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status'))) + ); + + $options = array('apachesolr:show_error' => t('Show error message')); + $system_info = system_get_info('module'); + if (module_exists('search')) { + foreach (search_get_info() as $module => $search_info) { + // Don't allow apachesolr to return results on failure of apachesolr. + if ($module == 'apachesolr_search') { + continue; + } + $options[$module] = t('Show @name search results', array('@name' => $system_info[$module]['name'])); + } + } + + $options['apachesolr:show_no_results'] = t('Show no results'); + $form['advanced']['apachesolr_failure'] = array( + '#type' => 'select', + '#title' => t('On failure'), + '#options' => $options, + '#default_value' => variable_get('apachesolr_failure', 'apachesolr:show_error'), + ); + + return system_settings_form($form); +} + +/** + * Gets information about the fields already in solr index. + * + * @param array $environment + * The environment for which we need to ask the status from + * + * @return array page render array + */ +function apachesolr_status_page($environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + $environment = apachesolr_environment_load($env_id); + } + else { + $env_id = $environment['env_id']; + } + + // Check for availability + if (!apachesolr_server_status($environment['url'], $environment['service_class'])) { + drupal_set_message(t('The server seems to be unavailable. Please verify the server settings at the settings page', array('!settings_page' => url("admin/config/search/apachesolr/settings/{$environment['env_id']}/edit", array('query' => drupal_get_destination())))), 'warning'); + return ''; + } + + try { + $solr = apachesolr_get_solr($environment["env_id"]); + $solr->clearCache(); + $data = $solr->getLuke(); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + $data = new stdClass; + $data->fields = array(); + } + + $messages = array(); + if (isset($data->index->numDocs)) { + try { + // Collect the stats + $stats_summary = $solr->getStatsSummary(); + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + $status = apachesolr_index_status($environment["env_id"]); + // We need a schema version greater than beta3. This is mostly to catch + // people using the Drupal 6 schema. + if (preg_match('/^drupal-[13]/', $stats_summary['@schema_version'])) { + $minimum = 'drupal-3.0-beta4'; + if (version_compare($stats_summary['@schema_version'], $minimum, '<')) { + drupal_set_message(t('Your schema.xml version is too old. You must update it to at least %minimum and re-index your content.', array('%minimum' => $minimum)), 'error'); + } + } + $pending_msg = $stats_summary['@pending_docs'] ? t('(@pending_docs sent but not yet processed)', $stats_summary) : ''; + $index_msg = $stats_summary['@index_size'] ? t('(@index_size on disk)', $stats_summary) : ''; + $indexed_message = t('@num Items !pending !index_msg', array( + '@num' => $data->index->numDocs, + '!pending' => $pending_msg, + '!index_msg' => $index_msg, + )); + $messages[] = array(t('Indexed'), $indexed_message); + + $remaining_message = t('@items (@percentage% has been sent to the server)', array( + '@items' => format_plural($status['remaining'], t('1 item'), t('@count items')), + '@percentage' => ((int)min(100, 100 * ($status['total'] - $status['remaining']) / max(1, $status['total']))), + ) + ); + $messages[] = array(t('Remaining'), $remaining_message); + + $messages[] = array(t('Schema'), t('@schema_version', $stats_summary)); + if (!empty($stats_summary['@core_name'])) { + $messages[] = array(t('Solr Core Name'), t('@core_name', $stats_summary)); + } + $messages[] = array(t('Delay'), t('@autocommit_time before updates are processed.', $stats_summary)); + $messages[] = array(t('Pending Deletions'), t('@deletes_total', $stats_summary)); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + } + if (empty($messages)) { + $messages[] = array(t('Error'), t('No data was returned from the server. Check your log messages.')); + } + // Initializes output with information about which server's setting we are + // editing, as it is otherwise not transparent to the end user. + $output['apachesolr_index_action_status'] = array( + '#prefix' => '

' . t('@environment: Search Index Content', array('@environment' => $environment['name'])) . '

', + '#theme' => 'table', + '#header' => array(t('Type'), t('Value')), + '#rows' => $messages, + ); + + $output['viewmore'] = array( + '#markup' => l(t('View more details on the search index contents'), 'admin/reports/apachesolr'), + ); + + $write_status = apachesolr_environment_variable_get($env_id, 'apachesolr_read_only', APACHESOLR_READ_WRITE); + if ($write_status == APACHESOLR_READ_WRITE) { + $output['index_action_form'] = drupal_get_form('apachesolr_index_action_form', $env_id); + $output['index_config_form'] = drupal_get_form('apachesolr_index_config_form', $env_id); + } + else { + drupal_set_message(t('Options for deleting and re-indexing are not available because the index is read-only. This can be changed on the settings page', array('!settings_page' => url('admin/config/search/apachesolr/settings/' . $env_id . '/edit', array('query' => drupal_get_destination())))), 'warning'); + } + + return $output; +} + +/** + * Get the report, eg.: some statistics and useful data from the Apache Solr index + * + * @param array $environment + * + * @return array page render array + */ +function apachesolr_index_report(array $environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + drupal_goto('admin/reports/apachesolr/' . $env_id); + } + $environments = apachesolr_load_all_environments(); + $environments_list = array(); + foreach ($environments as $env) { + $var_status = array('!name' =>$env['name']); + $environments_list[] = l(t('Statistics for !name', $var_status), 'admin/reports/apachesolr/' . $env['env_id']); + } + $output['environments_list'] = array( + '#theme' => 'item_list', + '#items' => $environments_list, + ); + + try { + $solr = apachesolr_get_solr($environment['env_id']); + $solr->clearCache(); + $data = $solr->getLuke(); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + return $output; + } + + $messages = array(); + $messages[] = array(t('Number of documents in index'), $data->index->numDocs); + + $limit = variable_get('apachesolr_luke_limit', 20000); + if (isset($data->index->numDocs) && $data->index->numDocs > $limit) { + $messages[] = array(t('Limit'), t('You have more than @limit documents, so term frequencies are being omitted for performance reasons.', array('@limit' => $limit))); + $not_found = t('Omitted'); + } + elseif (isset($data->index->numDocs)) { + $not_found = t('Not indexed'); + try { + $solr = apachesolr_get_solr($environment['env_id']); + // Note: we use 2 since 1 fails on Ubuntu Hardy. + $data = $solr->getLuke(2); + if (isset($data->index->numTerms)) { + $messages[] = array(t('# of terms in index'), $data->index->numTerms); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + $data->fields = array(); + } + } + // Initializes output with information about which server's setting we are + // editing, as it is otherwise not transparent to the end user. + $fields = (array)$data->fields; + if ($fields) { + $messages[] = array(t('# of fields in index'), count($fields)); + } + + // Output the messages we have for this page + $output['apachesolr_index_report'] = array( + '#theme' => 'table', + '#header' => array('type', 'value'), + '#rows' => $messages, + ); + + if ($fields) { + // Initializes table header. + $header = array( + 'name' => t('Field name'), + 'type' => t('Index type'), + 'terms' => t('Distinct terms'), + ); + + // Builds table rows. + $rows = array(); + foreach ($fields as $name => $field) { + // TODO: try to map the name to something more meaningful. + $rows[$name] = array( + 'name' => $name, + 'type' => $field->type, + 'terms' => isset($field->distinct) ? $field->distinct : $not_found + ); + } + ksort($rows); + // Output the fields we found for this environment + $output['field_table'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + } + else { + $output['field_table'] = array('#markup' => t('No data on indexed fields.')); + } + return $output; +} + +/** + * Page callback to show available conf files. + * + * @param array $environment + * + * @return string + * A non-render array but plain theme output for the config files overview. Could be done better probably + */ +function apachesolr_config_files_overview(array $environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + } + else { + $env_id = $environment['env_id']; + } + + $xml = NULL; + try { + $solr = apachesolr_get_solr($env_id); + $response = $solr->makeServletRequest('admin/file', array('wt' => 'xml')); + $xml = simplexml_load_string($response->data); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + } + + if ($xml) { + // Retrieve our items from the xml using xpath + $items = $xml->xpath('//lst[@name="files"]/lst'); + + // Add all the data of the file in a files array + $files = array(); + foreach ($items as $item_id => $item) { + // Do not list directories. Always a bool + if (isset($item->bool)) { + break; + } + // Get data from the files. + $name = $item->attributes(); + $name = ((string)$item->attributes()) ? (string)$item->attributes() : t('No name found'); + $files[$item_id]['name'] = l($name, 'admin/reports/apachesolr/' . $env_id . '/conf/' . $name); + + // Retrieve the date attribute + if (isset($item->date)) { + $modified = ((string)$item->date->attributes() == 'modified') ? (string) $item->date : t('No date found'); + $files[$item_id]['modified'] = format_date(strtotime($modified)); + } + + // Retrieve the size attribute + if (isset($item->long)) { + $size = ((string)$item->long->attributes() == 'size') ? (string) $item->long : t('No size found'); + $files[$item_id]['size'] = t('Size (bytes): @bytes', array('@bytes' => $size)); + } + } + // Sort our files alphabetically + ksort($files); + + // Initializes table header. + $header = array( + 'name' => t('File name'), + 'date' => t('Modified'), + 'size' => t('Size'), + ); + + // Display the table of field names, index types, and term counts. + $variables = array( + 'header' => $header, + 'rows' => $files, + ); + $output = theme('table', $variables); + } + else { + $output = '

' . t('No data about any file found.') . "

\n"; + } + return $output; +} + +/** + * Page callback to show one conf file. + * + * @param string $name + * @param array $environment + * + * @return string + * the requested config file + */ +function apachesolr_config_file($name, array $environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + } + else { + $env_id = $environment['env_id']; + } + + $output = ''; + try { + $solr = apachesolr_get_solr($env_id); + $response = $solr->makeServletRequest('admin/file', array('file' => $name)); + $raw_file = $response->data; + $output = '
' . check_plain($raw_file) . '
'; + drupal_set_title(check_plain($name)); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + } + return $output; +} + +/** + * Form builder for the Apachesolr Indexer actions form. + * + * @param array $form + * @param array $form_state + * @param string $env_id + * The machine name of the environment. + * @see apachesolr_index_action_form_delete_submit(). + * + * @return array $form + */ +function apachesolr_index_action_form(array $form, array $form_state, $env_id) { + $form = array(); + $form['action'] = array( + '#type' => 'fieldset', + '#title' => t('Actions'), + '#collapsible' => TRUE, + ); + + $form['action']['env_id'] = array( + '#type' => 'value', + '#value' => $env_id, + ); + + $form['action']['cron'] = array( + '#prefix' => '
', + '#type' => 'submit', + '#value' => t('Index queued content (!amount)', array('!amount' => variable_get('apachesolr_cron_limit', 50))), + '#submit' => array('apachesolr_index_action_form_cron_submit'), + ); + $form['action']['cron_description'] = array( + '#prefix' => '', + '#suffix' => '
', + '#markup' => t('Indexes just as many items as 1 cron run would do.'), + ); + + $form['action']['remaining'] = array( + '#prefix' => '
', + '#type' => 'submit', + '#value' => t('Index all queued content'), + '#submit' => array('apachesolr_index_action_form_remaining_submit'), + ); + $form['action']['remaining_description'] = array( + '#prefix' => '', + '#suffix' => '
', + '#markup' => t('Could take time and could put an increased load on your server.'), + ); + + $form['action']['reset'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'submit', + '#value' => t('Queue all content for reindexing'), + '#submit' => array('apachesolr_index_action_form_reset_submit'), + ); + $form['action']['delete'] = array( + '#prefix' => '
', + '#type' => 'submit', + '#value' => t('Delete the Search & Solr index'), + '#submit' => array('apachesolr_index_action_form_delete_submit'), + ); + $form['action']['delete_description'] = array( + '#prefix' => '', + '#suffix' => '
', + '#markup' => t('Useful with a corrupt index or a new schema.xml.'), + ); + + return $form; +} + +/** + * Submit handler for the Indexer actions form, delete button. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_remaining_submit(array $form, array &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $env_id = $form_state['values']['env_id']; + $form_state['redirect'] = array('admin/config/search/apachesolr/settings/' . $env_id . '/index/remaining', array('query' => $destination)); +} + +/** + * Submit handler for the Indexer actions form, delete button. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_delete_submit(array $form, array &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $env_id = $form_state['values']['env_id']; + $form_state['redirect'] = array('admin/config/search/apachesolr/settings/' . $env_id . '/index/delete', array('query' => $destination)); +} + +/** + * Submit handler for the Indexer actions form, delete button. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_reset_submit(array $form, array &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $env_id = $form_state['values']['env_id']; + $form_state['redirect'] = array('admin/config/search/apachesolr/settings/' . $env_id . '/index/reset', array('query' => $destination)); +} + +/** + * Submit handler for the deletion form. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_cron_submit(array $form, array &$form_state) { + if (!empty($form_state['build_info']['args'][0])) { + $env_id = $form_state['build_info']['args'][0]; + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $env_id . '/index'; + } + else { + $env_id = apachesolr_default_environment(); + $form_state['redirect'] = 'admin/config/search/apachesolr'; + } + apachesolr_cron($env_id); + drupal_set_message(t('Apachesolr cron succesfully executed')); +} + +/** + * Form builder for to reindex the remaining items left in the queue. + * + * @see apachesolr_index_action_form_delete_confirm_submit(). + * + * @param array $form + * @param array $form_state + * @param array $environment + * + * @return mixed + */ +function apachesolr_index_action_form_remaining_confirm(array $form, array &$form_state, array $environment) { + return confirm_form($form, + t('Are you sure you want index all remaining content?'), + 'admin/config/search/apachesolr/settings/' . $environment['env_id'] . '/index', + NULL, + t('Index all remaining') + ); +} + +/** + * Submit handler for the deletion form. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_remaining_confirm_submit(array $form, array &$form_state) { + if (!empty($form_state['build_info']['args'][0]['env_id'])) { + $env_id = $form_state['build_info']['args'][0]['env_id']; + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $env_id . '/index'; + } + else { + $env_id = apachesolr_default_environment(); + $form_state['redirect'] = 'admin/config/search/apachesolr'; + } + apachesolr_index_batch_index_remaining($env_id); +} + +/** + * Form builder for the index re-enqueue form. + * + * @see apachesolr_index_action_form_reset_confirm_submit(). + * + * @param array $form + * @param array $form_state + * @param array $environment + * + * @return mixed + */ +function apachesolr_index_action_form_reset_confirm(array $form, array &$form_state, array $environment) { + return confirm_form($form, + t('Are you sure you want to queue content for reindexing?'), + 'admin/config/search/apachesolr/settings/' . $environment['env_id'] . '/index', + t('All content on the site will be queued for indexing. The documents currently in the Solr index will remain searchable.'), + t('Queue all content') + ); +} + +/** + * Submit handler for the deletion form. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_reset_confirm_submit(array $form, array &$form_state) { + if (!empty($form_state['build_info']['args'][0]['env_id'])) { + $env_id = $form_state['build_info']['args'][0]['env_id']; + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $env_id . '/index'; + } + else { + $env_id = apachesolr_default_environment(); + $form_state['redirect'] = 'admin/config/search/apachesolr'; + } + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + apachesolr_index_mark_for_reindex($env_id); + drupal_set_message(t('All the content on your site is queued for indexing. You can wait for it to be indexed during cron runs, or you can manually reindex it.')); +} + +/** + * Form builder for the index delete/clear form. + * + * @see apachesolr_index_action_form_delete_confirm_submit(). + + * @param array $form + * @param array $form_state + * @param array $environment + * + * @return array output of confirm_form() + */ +function apachesolr_index_action_form_delete_confirm(array $form, array &$form_state, array $environment) { + return confirm_form($form, + t('Are you sure you want to clear your index?'), + 'admin/config/search/apachesolr/settings/' . $environment['env_id'] . '/index', + t('This will remove all data from your index and all search results will be incomplete until your site is reindexed.'), + t('Delete index') + ); +} + +/** + * Submit handler for the deletion form. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_action_form_delete_confirm_submit(array $form, array &$form_state) { + if (!empty($form_state['build_info']['args'][0]['env_id'])) { + $env_id = $form_state['build_info']['args'][0]['env_id']; + $form_state['redirect'] = 'admin/config/search/apachesolr/settings/' . $env_id . '/index'; + } + else { + $env_id = apachesolr_default_environment(); + $form_state['redirect'] = 'admin/config/search/apachesolr'; + } + // Rebuild our tracking table. + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + apachesolr_index_delete_index($env_id); + drupal_set_message(t('The index has been deleted.')); +} + +/** + * Submit a batch job to index the remaining, non-indexed content. + * + * @param string $env_id + * The environment ID where it needs to index the remaining items for + */ +function apachesolr_index_batch_index_remaining($env_id, $total_limit = null) { + $batch = array( + 'operations' => array( + array( + 'apachesolr_index_batch_index_entities', + array( + $env_id, + $total_limit, + ), + ), + ), + 'finished' => 'apachesolr_index_batch_index_finished', + 'title' => t('Indexing'), + 'init_message' => t('Preparing to submit content to Solr for indexing...'), + 'progress_message' => t('Submitting content to Solr...'), + 'error_message' => t('Solr indexing has encountered an error.'), + 'file' => drupal_get_path('module', 'apachesolr') . '/apachesolr.admin.inc', + ); + batch_set($batch); +} + + +/** + * Batch Operation Callback + * + * @param string $env_id + * The machine name of the environment. + * @param $total_limit + * The total number of items to index across all batches + * @param array $context + * + * @return false + * return false when an exception was caught + * + * @throws Exception + * When solr gives an error, throw an exception that solr is not available + */ +function apachesolr_index_batch_index_entities($env_id, $total_limit = NULL, &$context) { + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + if (empty($context['sandbox'])) { + try { + // Get the $solr object + $solr = apachesolr_get_solr($env_id); + // If there is no server available, don't continue. + if (!$solr->ping()) { + throw new Exception(t('No Solr instance available during indexing.')); + } + } + catch (Exception $e) { + watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR); + return FALSE; + } + + $status = apachesolr_index_status($env_id); + $context['sandbox']['progress'] = 0; + $context['sandbox']['submitted'] = 0; + + // How many items do we want to index? All or a limited set of items + if (empty($total_limit)) { + $context['sandbox']['max'] = $status['remaining']; + } + else { + $context['sandbox']['max'] = $total_limit; + } + } + + // We can safely process the apachesolr_cron_limit nodes at a time without a + // timeout or out of memory error. + $limit = variable_get('apachesolr_cron_limit', 50); + + // Reduce the limit for our final batch if we would be processing more than had been requested + if ($limit + $context['sandbox']['progress'] > $context['sandbox']['max']) { + $limit = $context['sandbox']['max'] - $context['sandbox']['progress']; + } + + if ($context['sandbox']['max'] >= $context['sandbox']['progress'] + $limit) { + $context['sandbox']['progress'] += $limit; + } + else { + $context['sandbox']['progress'] = $context['sandbox']['max']; + } + $context['sandbox']['submitted'] += apachesolr_index_entities($env_id, $limit); + + $arguments = array( + '@current' => $context['sandbox']['progress'], + '@total' => $context['sandbox']['max'], + '@submitted' => $context['sandbox']['submitted'], + ); + $context['message'] = t('Inspected @current of @total entities. Submitted @submitted documents to Solr', $arguments); + + // Inform the batch engine that we are not finished, and provide an + // estimation of the completion level we reached. + $context['finished'] = empty($context['sandbox']['max']) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max']; + + // Put the total into the results section when we're finished so we can + // show it to the admin. + if ($context['finished']) { + $context['results']['count'] = $context['sandbox']['progress']; + $context['results']['submitted'] = $context['sandbox']['submitted']; + } +} + +/** + * Batch 'finished' callback + * + * @param bool $success + * Whether the batch ended with success or not + * @param array $results + * @param array $operations + */ +function apachesolr_index_batch_index_finished($success, array $results, array $operations) { + $message = ''; + // $results['count'] will not be set if Solr is unavailable. + if (isset($results['count'])) { + $message .= format_plural($results['count'], '1 item processed successfully. ', '@count items successfully processed. '); + } + if (isset($results['submitted'])) { + $message .= format_plural($results['submitted'], '1 document successfully sent to Solr.', '@count documents successfully sent to Solr.'); + } + if ($success) { + $type = 'status'; + } + else { + // An error occurred. $operations contains the unprocessed operations. + $error_operation = reset($operations); + $message .= ' ' . t('An error occurred while processing @num with arguments: @args', array('@num' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))); + $type = 'error'; + } + drupal_set_message($message, $type); +} + +/** + * Form builder for the bundle configuration form. + * + * @see apachesolr_index_config_form_submit(). + * + * @param array $form + * @param array $form_state + * @param string $env_id + * The machine name of the environment. + * + * @return array $form + */ +function apachesolr_index_config_form(array $form, array $form_state, $env_id) { + $form['config'] = array( + '#type' => 'fieldset', + '#title' => t('Configuration'), + '#collapsible' => TRUE, + ); + + $form['config']['bundles'] = array( + '#type' => 'markup', + '#markup' => t('Select the entity types and bundles that should be indexed.'), + ); + + // For future extensibility, when we have multiple cores. + $form['config']['env_id'] = array( + '#type' => 'value', + '#value' => $env_id, + ); + + foreach (entity_get_info() as $entity_type => $entity_info) { + if (!empty($entity_info['apachesolr']['indexable'])) { + $options = array(); + foreach ($entity_info['bundles'] as $key => $info) { + $options[$key] = $info['label']; + } + + $form['config']['entities']['#tree'] = TRUE; + $form['config']['entities'][$entity_type] = array( + '#type' => 'checkboxes', + '#title' => check_plain($entity_info['label']), + '#options' => $options, + '#default_value' => apachesolr_get_index_bundles($env_id, $entity_type), + ); + } + } + + $form['config']['submit'] = array('#type' => 'submit', '#value' => t('Save')); + + return $form; +} + +/** + * Submit handler for the bundle configuration form. + * + * @param array $form + * @param array $form_state + */ +function apachesolr_index_config_form_submit(array $form, array &$form_state) { + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + $form_values = $form_state['values']; + $env_id = $form_values['env_id']; + + foreach ($form_values['entities'] as $entity_type => $bundles) { + $existing_bundles = apachesolr_get_index_bundles($env_id, $entity_type); + $all_bundles = array_keys($bundles); + $new_bundles = array_values(array_filter($bundles)); + apachesolr_index_set_bundles($env_id, $entity_type, $new_bundles); + + // Remove all excluded bundles - this happens on form submit + // even if there is no change so the admin can remove + // bundles if there was an error. + $excluded_bundles = array_diff($all_bundles, $new_bundles); + if (apachesolr_index_delete_bundles($env_id, $entity_type, $excluded_bundles)) { + $callback = apachesolr_entity_get_callback($entity_type, 'bundles changed callback'); + if (!empty($callback)) { + call_user_func($callback, $env_id, $existing_bundles, $new_bundles); + } + } + else { + drupal_set_message(t('Search is temporarily unavailable. If the problem persists, please contact the site administrator.'), 'error'); + } + } + + // Clear the entity cache, since we will be changing its data. + entity_info_cache_clear(); + cache_clear_all('apachesolr:environments', 'cache_apachesolr'); + drupal_set_message(t('Your settings have been saved.')); +} + +/** + * Page callback for node/%node/devel/apachesolr. + * + * @param object $node + * @return string debugging information + */ +function apachesolr_devel($node) { + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + $item = new stdClass(); + $item->entity_type = 'node'; + $item->entity_id = $node->nid; + $output = ''; + foreach (apachesolr_load_all_environments() as $env_id => $environment) { + $documents = apachesolr_index_entity_to_documents($item, $env_id); + $output .= '

' . t('Environment %name (%env_id)', array('%name' => $environment['name'], '%env_id' => $env_id)). '

'; + foreach ($documents as $document) { + $debug_data = array(); + foreach ($document as $key => $value) { + $debug_data[$key] = $value; + } + $output .= kdevel_print_object($debug_data); + } + } + return $output; +} diff -r 000000000000 -r a2b4f67e73dc facetapi.callbacks.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/facetapi.callbacks.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,164 @@ + $bundle_info) { + $names[$bundle_name] = $bundle_info['label']; + } + } + } + return array_intersect_key($names, array_flip($values)); +} + +/** + * Map callback for node authors. + */ +function facetapi_map_author(array $values) { + $map = array(); + $users = user_load_multiple($values); + foreach ($users as $user) { + $map[$user->uid] = format_username($user); + } + if (isset($map[0])) { + $map[0] = variable_get('anonymous', t('Anonymous')); + } + return $map; +} + +/** + * Map callback for languages. + */ +function facetapi_map_language(array $values) { + $map = array(); + $language_list = language_list(); + foreach ($values as $language) { + if (isset($language_list[$language])) { + $map[$language] = t($language_list[$language]->name); + } + else { + $map[$language] = t('Language neutral'); + } + } + return $map; +} + +/** + * Maps date ranges to human readable dates. + * + * @param array $values + * An array of indexed values being mapped. + * @param array $options + * An associative array of map options containing: + * - format callback: The callback used to format the date, defaults to + * "facetapi_format_timestamp". + * + * @return array + * An array mapping the indexed values to human readable values. + */ +function facetapi_map_date(array $values, array $options) { + if (empty($options['format callback'])) { + $options['format callback'] = 'facetapi_format_timestamp'; + } + $map = array(); + foreach ($values as $value) { + $range = explode(' TO ', trim($value, '{[]}')); + if (isset($range[1])) { + $gap = facetapi_get_date_gap($range[0], $range[1]); + $map[$value] = facetapi_format_date($range[0], $gap, $options['format callback']); + } + } + return $map; +} + +/** + * Callback that returns the minimum date in the node table. + * + * @param $facet + * An array containing the facet definition. + * + * @return + * The minimum time in the node table. + * + * @todo Cache this value. + */ +function facetapi_get_min_date(array $facet) { + $query = db_select('node', 'n')->condition('status', 1); + $query->addExpression('MIN(' . $facet['name'] . ')', 'max'); + return $query->execute()->fetch()->max; +} + +/** + * Callback that returns the minimum value in the node table. + * + * @param $facet + * An array containing the facet definition. + * + * @return + * The minimum time in the node table. + * + * @todo Cache this value. + */ +function facetapi_get_max_date(array $facet) { + $query = db_select('node', 'n')->condition('status', 1); + $query->addExpression('MAX(' . $facet['name'] . ')', 'max'); + return $query->execute()->fetch()->max; +} + +/** + * Map callback for taxonomy terms. + */ +function facetapi_map_taxonomy_terms(array $values) { + $map = array(); + $terms = taxonomy_term_load_multiple($values); + foreach ($terms as $term) { + $map[$term->tid] = entity_label('taxonomy_term', $term); + } + return $map; +} + +/** + * Gets parent information for taxonomy terms. + * + * @param array $values + * An array containing the term ids. + * + * @return + * An associative array keyed by term ID to parent ID. + */ +function facetapi_get_taxonomy_hierarchy(array $values) { + $result = db_select('taxonomy_term_hierarchy', 'th') + ->fields('th', array('tid', 'parent')) + ->condition('th.parent', '0', '>') + ->condition(db_or() + ->condition('th.tid', $values, 'IN') + ->condition('th.parent', $values, 'IN') + ) + ->execute(); + + $parents = array(); + foreach ($result as $record) { + $parents[$record->tid][] = $record->parent; + } + return $parents; +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/adapter.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/adapter.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,217 @@ +getSearcher(), '@'), '@'); + $path .= '/' . $env_id . '/facets'; + // Add the realm name to the path if it is not the first one in the list. + if (key(facetapi_get_realm_info()) != $realm_name) { + $path .= '/' . $realm_name; + } + } + return $path; + } + + /** + * Allows the backend to initialize its query object before adding the facet + * filters. + * + * @param mixed $query + * The backend's native object. + */ + function initActiveFilters($query) { + $enabled_facets = facetapi_get_enabled_facets($this->info['name']); + if ($enabled_facets) { + $query->addParam('facet', 'true'); + $query->addParam('facet.sort', 'count'); + $query->addParam('facet.mincount', '1'); + } + } + + /** + * Returns a boolean flagging whether $this->_searcher executed a search. + */ + public function searchExecuted() { + // Initial check - has ANY solr query run in our environment. + $env_id = $this->info['instance']; + $this_has_searched = solrsearch_has_searched($env_id); + // Secondary check - do we have results for this searcher? + $this_has_searched = $this_has_searched && solrsearch_static_response_cache($this->getSearcher()); + return $this_has_searched; + } + + /** + * Suppress output of the realm + * + * @param string $realm_name + * + * @return bool $flag + * Returns if it was suppressed or not + */ + public function suppressOutput($realm_name) { + $flag = FALSE; + //dpm("suppressoutput"); + if ($realm_name == 'block') { + //dpm($this->info); + $env_id = $this->info['instance']; + $flag = solrsearch_suppress_blocks($env_id); + //dpm($env_id); + //dpm($flag); + + } + return FALSE; + return $flag || !$this->searchExecuted(); } + + /** + * Returns the search keys. + * + * @return string + */ + public function getSearchKeys() { + if (NULL === $this->keys) { + $env_id = $this->info['instance']; + if ($query = solrsearch_current_query($env_id)) { + return $query->getParam('q'); + } + } + else { + return $this->keys; + } + return FALSE; + } + + /** + * Returns the search path. + * + * @return string + * A string containing the search path. + * + * @todo D8 should provide an API function for this. + */ + public function getSearchPath() { + $env_id = $this->info['instance']; + $query = solrsearch_current_query($env_id); + if (!$query || (NULL === $this->searchPath && NULL === $query->getPath())) { + if ($path = module_invoke($this->info['module'] . '_search', 'search_info')) { + $this->searchPath = 'search/' . $path['path']; + if (!isset($_GET['keys']) && ($keys = $this->getSearchKeys())) { + $this->searchPath .= '/' . $keys; + } + } + } + if (!$query || NULL === $query->getPath()) { + return $this->searchPath; + } + else { + return $query->getPath(); + } + + } + + /** + * Returns the number of total results found for the current search. + * + * @return bool|int + * Number of results or false if no search response was found + */ + public function getResultCount() { + $response = solrsearch_static_response_cache($this->getSearcher()); + if ($response) { + return $response->response->numFound; + } + return FALSE; + } + + /** + * Allows for backend specific overrides to the settings form. + * + * @param array $form + * @param array $form_state + */ + public function settingsForm(&$form, &$form_state) { + $form['#validate'][] = 'solrsearch_facet_form_validate'; + } + + + /** + * Allows the backend to add facet queries to its native query object. + * + * This method is called by the implementing module to initialize the facet + * display process. The following actions are taken: + * - FacetapiAdapter::initActiveFilters() hook is invoked. + * - Dependency plugins are instantiated and executed. + * - Query type plugins are executed. + * + * @param mixed $query + * The backend's native query object. + * + * @todo Should this method be deprecated in favor of one name init()? This + * might make the code more readable in implementing modules. + * + * @see FacetapiAdapter::initActiveFilters() + */ + function addActiveFilters($query) { + module_load_include('inc', 'solrsearch', 'facetapi.callbacks'); + facetapi_add_active_searcher($this->info['name']); + + // Invoke initActiveFilters hook. + $this->initActiveFilters($query); + + + foreach ($this->getEnabledFacets() as $facet) { + + $settings = $this->getFacet($facet)->getSettings(); + + // Instantiate and execute dependency plugins. + $display = TRUE; + foreach ($facet['dependency plugins'] as $id) { + $class = ctools_plugin_load_class('facetapi', 'dependencies', $id, 'handler'); + $plugin = new $class($id, $this, $facet, $settings, $this->activeItems['facet']); + if (NULL !== ($return = $plugin->execute())) { + $display = $return; + } + } + + // Store whether this facet passed its dependencies. + $this->dependenciesPassed[$facet['name']] = $display; + + // Execute query type plugin if dependencies were met, otherwise remove + // the facet's active items so they don't display in the current search + // block or appear as active in the breadcrumb trail. + if ($display && $this->queryTypes[$facet['name']]) { + $this->queryTypes[$facet['name']]->execute($query); + } + else { + foreach ($this->activeItems['facet'][$facet['name']] as $item) { + $this->urlProcessor->removeParam($item['pos']); + $filter = $item['field alias'] . ':' . $item['value']; + unset($this->activeItems['filter'][$filter]); + } + $this->activeItems['facet'][$facet['name']] = array(); + } + } + + } +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/query_type_date.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/query_type_date.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,185 @@ +getDateRange($query); + if (empty($date_range)) { + return NULL; + } + list($start, $end, $gap) = $date_range; + $query->addParam('facet.date', $this->facet['field']); + $query->addParam('f.' . $this->facet['field'] . '.facet.date.start', $start); + $query->addParam('f.' . $this->facet['field'] . '.facet.date.end', $end); + $query->addParam('f.' . $this->facet['field'] . '.facet.date.gap', $gap); + + // Adds "hard limit" parameter to prevent too many return values. + $settings = $this->adapter->getFacet($this->facet)->getSettings(); + $limit = empty($settings->settings['hard_limit']) ? 20 : (int) $settings->settings['hard_limit']; + $query->addParam('f.' . $this->facet['field'] . '.facet.limit', $limit); + + $active = $this->adapter->getActiveItems($this->facet); + // Date filters don't support OR operator. + foreach ($active as $value => $item) { + $query->addFilter($this->facet['field'], $value); + } + } + + /** + * Gets the range of dates we are using. + * + * @param DrupalSolrQueryInterface $query + * A SolrBaseQuery object. + * + * @return bool|array + * An array containing the gap and range information or false if not present + */ + function getDateRange(DrupalSolrQueryInterface $query) { + $return = NULL; + $gap = NULL; + + // Attempts to get next gap from passed date filters. + foreach ($this->adapter->getActiveItems($this->facet) as $item) { + if ($gap = facetapi_get_date_gap($item['start'], $item['end'])) { + $next_gap = facetapi_get_next_date_gap($gap, FACETAPI_DATE_SECOND); + if ($next_gap == $gap) { + $next_gap = NULL; + return NULL; + } + $return = array( + "{$item['start']}/$next_gap", + "{$item['end']}+1$next_gap/$next_gap", + "+1$next_gap", + ); + } + } + + // If no filters were passed, get default range. + if (NULL === $return) { + + // Builds SQL that gets minimum and maximum values from node table. + $minimum = $maximum = FALSE; + if ($this->facet['min callback'] && is_callable($this->facet['min callback'])) { + $minimum = $this->facet['min callback']($this->facet); + } + if ($this->facet['max callback'] && is_callable($this->facet['max callback'])) { + $maximum = $this->facet['max callback']($this->facet); + } + + // Gets the default gap. + //$gap = FACETAPI_DATE_YEAR; + if ($minimum && $maximum) { + $gap = facetapi_get_timestamp_gap($minimum, $maximum); + $minimum = facetapi_isodate($minimum, $gap); + $maximum = facetapi_isodate($maximum, $gap); + $return = array( + "$minimum/$gap", + "$maximum+1$gap/$gap", + "+1$gap", + ); + } + } + // Returns the range information. + return $return; + } + + /** + * Initializes the facet's build array. + * + * @return array + * The initialized render array. + */ + public function build() { + + // Initializes build and gets static response. + if (!$response = solrsearch_static_response_cache($this->adapter->getSearcher())) { + return array(); + } + $build = array(); + + // Gets total number of documents matched in search. + $total = $response->response->numFound; + + // Gets the active date facets, starts to builds the "parent - child" + // relationships. + $parent = NULL; + foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) { + // Builds the raw facet "value", the count for selected items will be the + // total number of rows returned in the query. + $build[$value] = array('#count' => $total); + + // If there is a previous item, there is a parent, uses a reference so the + // arrays are populated when they are updated. + if (NULL !== $parent) { + $build[$parent]['#item_children'][$value] = &$build[$value]; + $build[$value]['#item_parents'][$parent] = $parent; + } + + // Stores the last value iterated over. + $parent = $value; + } + + // Gets raw facet data from the Solr server. + if (isset($response->facet_counts->facet_dates) && isset($response->facet_counts->facet_dates->{$this->facet['field']})) { + $raw_data = (array) $response->facet_counts->facet_dates->{$this->facet['field']}; + } + else { + $raw_data = array(); + } + //$end = (!empty($raw_data['end'])) ? $raw_data['end'] : ''; + //$start = (!empty($raw_data['start'])) ? $raw_data['start'] : ''; + $gap = (!empty($raw_data['gap'])) ? $raw_data['gap'] : ''; + + // We cannot list anything below a minute (range of 00 seconds till 59 + // seconds. Milliseconds are not possible) + if ($gap != "+1SECOND") { + unset($raw_data['start']); + unset($raw_data['end']); + unset($raw_data['gap']); + + // Treat each date facet as a range start, and use the next date facet + // as range end. Use 'end' for the final end. + $previous = NULL; + + // Builds facet counts object used by the server. + foreach ($raw_data as $value => $count) { + if ($count) { + $from = $value; + $to = facetapi_isodate(strtotime($value . $gap)); + $new_value = '[' . $from . ' TO ' . $to . ']'; + $build[$new_value] = array('#count' => $count, '#active' => 0); + if (NULL !== $parent) { + $build[$parent]['#item_children'][$new_value] = &$build[$new_value]; + $build[$new_value]['#item_parents'][$parent] = $parent; + } + } + } + } + return $build; + } +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/query_type_geo.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/query_type_geo.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,84 @@ +adapter->getFacet($this->facet)->getSettings(); + + $facet_distances = explode(',', $this->facet_options); + + $active_items = $this->adapter->getActiveItems($this->facet); + + if (empty($active_items)) { + $distance = $this->default_radius; + } + else { + $active_item = array_pop($active_items); + $distance = substr($active_item['value'], 1); + // Add current selected distance to have possibility to unselect it. + $facet_distances[] = 1; + } + + // Search center point. + $query->addParam('pt', $this->center_point); + + // Set location field name. + $query->addParam('sfield', $this->facet['field']); + $query->addParam('fq', '{!geofilt sfield=' . $this->facet['field'] . '}'); + + // Set search radius. + $query->addParam('d', $distance); + + // Set facets. + foreach ($facet_distances as $facet_option) { + $facet_distance = $distance * $facet_option; + $query->addParam('facet.query', '{!geofilt d=' . $facet_distance . ' key=d' . $facet_distance . '}'); + } + } + + /** + * Initializes the facet's build array. + * + * @return array + * The initialized render array. + */ + public function build() { + $build = array(); + if ($response = apachesolr_static_response_cache($this->adapter->getSearcher())) { + if (isset($response->facet_counts->facet_queries)) { + foreach ($response->facet_counts->facet_queries as $value => $count) { + // Skip zero results values. + if ($count > 0) { + $build[$value] = array('#count' => $count); + } + } + } + } + return $build; + } +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/query_type_integer.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/query_type_integer.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,223 @@ +getIntegerRange($query); + + if (empty($integer_range)) { + return NULL; + } + list($start, $end, $gap) = $integer_range; + $query->addParam('facet.range', $this->facet['field']); + $query->addParam('f.' . $this->facet['field'] . '.facet.range.start', $start); + $query->addParam('f.' . $this->facet['field'] . '.facet.range.end', $end); + $query->addParam('f.' . $this->facet['field'] . '.facet.range.gap', $gap); + + // Adds "hard limit" parameter to prevent too many return values. + $settings = $this->adapter->getFacet($this->facet)->getSettings(); + $limit = empty($settings->settings['hard_limit']) ? 20 : (int) $settings->settings['hard_limit']; + $query->addParam('f.' . $this->facet['field'] . '.facet.limit', $limit); + + $active = $this->adapter->getActiveItems($this->facet); + // Date filters don't support OR operator. + foreach ($active as $value => $item) { + $query->addFilter($this->facet['field'], $value); + } + } + + /** + * Gets the range of integer rage we are using. + * + * @param DrupalSolrQueryInterface $query + * A SolrBaseQuery object. + * + * @return bool|array + * An array containing the gap and range information or false if not present + */ + function getIntegerRange(DrupalSolrQueryInterface $query) { + + $return = NULL; + $gap = NULL; + + // Attempts to get next gap from passed date filters. + foreach ($this->adapter->getActiveItems($this->facet) as $item) { + + if ($gap = ($item['end'] - $item['start'])) { + //$next_gap = facetapi_get_next_date_gap($gap, FACETAPI_DATE_SECOND); + //TODO: is something similar needed for integer range? + $next_gap =$gap; + if ($next_gap == $gap) { + $next_gap = NULL; + return NULL; + } + $return = array( + $item['start'], + $item['end'], + $next_gap, + ); + } + } + + // If no filters were passed, get default range. + if (NULL === $return) { + + // Builds SQL that gets minimum and maximum values from node table. + $minimum = $maximum =$gap = FALSE; + if ($this->facet['min callback'] && is_callable($this->facet['min callback'])) { + $minimum = $this->facet['min callback']($this->facet); + } + if ($this->facet['max callback'] && is_callable($this->facet['max callback'])) { + $maximum = $this->facet['max callback']($this->facet); + } + + if ($this->facet['gap callback'] && is_callable($this->facet['gap callback'])) { + $gap = $this->facet['gap callback']($this->facet); + } + + + // Gets the default gap. + //$gap = FACETAPI_DATE_YEAR; + if ($minimum && $maximum && $gap) { + + //$minimum = facetapi_isodate($minimum, $gap); + //$maximum = facetapi_isodate($maximum, $gap); + $return = array( + $minimum, + $maximum, + $gap , + ); + } + } + // Returns the range information. + return $return; + } + + /** + * Initializes the facet's build array. + * + * @return array + * The initialized render array. + */ + public function build() { + + // Initializes build and gets static response. + if (!$response = solrsearch_static_response_cache($this->adapter->getSearcher())) { + return array(); + } + $build = array(); + + // Gets total number of documents matched in search. + $total = $response->response->numFound; + + // Gets the active date facets, starts to builds the "parent - child" + // relationships. + $parent = NULL; + foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) { + // Builds the raw facet "value", the count for selected items will be the + // total number of rows returned in the query. + $build[$value] = array('#count' => $total); + + // If there is a previous item, there is a parent, uses a reference so the + // arrays are populated when they are updated. + if (NULL !== $parent) { + $build[$parent]['#item_children'][$value] = &$build[$value]; + $build[$value]['#item_parents'][$parent] = $parent; + } + + // Stores the last value iterated over. + $parent = $value; + } + + // Gets raw facet data from the Solr server. + if (isset($response->facet_counts->facet_ranges) && isset($response->facet_counts->facet_ranges->{$this->facet['field']})) { + $raw_data = (array) $response->facet_counts->facet_ranges->{$this->facet['field']}; + } + else { + $raw_data = array(); + } + //$end = (!empty($raw_data['end'])) ? $raw_data['end'] : ''; + //$start = (!empty($raw_data['start'])) ? $raw_data['start'] : ''; + $gap = (!empty($raw_data['gap'])) ? $raw_data['gap'] : ''; + + // We cannot list anything below a minute (range of 00 seconds till 59 + // seconds. Milliseconds are not possible) + + unset($raw_data['start']); + unset($raw_data['end']); + unset($raw_data['gap']); + + // Treat each date facet as a range start, and use the next date facet + // as range end. Use 'end' for the final end. + $previous = NULL; + + // Builds facet counts object used by the server. + + foreach ($raw_data as $value => $counts) { + foreach($counts as $val => $count){ + if ($count) { + $from = $val; + $to = $val+$gap; + $new_value = '[' . $from . ' TO ' . $to . ']'; + $build[$new_value] = array('#count' => $count, '#active' => 0); + if (NULL !== $parent) { + $build[$parent]['#item_children'][$new_value] = &$build[$new_value]; + $build[$new_value]['#item_parents'][$parent] = $parent; + } + } + } + } + return $build; + } +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/query_type_numeric_range.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/query_type_numeric_range.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,102 @@ +adapter->getFacet($this->facet)->getSettings(); + $active = $this->adapter->getActiveItems($this->facet); + + $singular_field_info = $this->facet['map options']; + $singular_field_info['multiple'] = FALSE; + + $this->single_key = solrsearch_index_key($singular_field_info); + // See: http://wiki.apache.org/solr/StatsComponent + $query->addParam('stats', 'true'); + $query->addParam('stats.field', $this->single_key); + $query->addParam('stats.facet', $this->single_key); + // Range filters don't support OR operator. + foreach ($active as $value => $item) { + $query->addFilter($this->single_key, $value); + } + } + + /** + * Initializes the facet's build array. + * + * Any calls to this method need to be wrapped in a try-catch block. + * + * @return array + * The initialized render array. + */ + public function build() { + $build = array(); + if (!isset($this->single_key)) { + return $build; + } + + // Per key we save our statistics result + $cache = cache_get('stats_' . $this->single_key, 'cache_solrsearch'); + $stats_minmax = array(); + + if (!isset($cache->data)) { + // we need an additional query for the statistics of the field + // We can optionally specify a Solr object. + $solr = solrsearch_get_solr(); + + // We possibly need some caching for this query + $query_stats = solrsearch_drupal_query('solrsearch_stats', array(), '', '', $solr); + $query_stats->addParam('stats', 'true'); + $query_stats->addParam('stats.field', $this->single_key); + $query_stats->addParam('stats.facet', $this->single_key); + $response_stats = $query_stats->search(); + + if ($response_stats->response) { + $stats_minmax = $response_stats->stats->stats_fields->{$this->single_key}; + cache_set('stats_' . $this->single_key, $stats_minmax, 'cache_solrsearch'); + } + } + else { + // Set our statistics from the cache + $stats_minmax = $cache->data; + } + + if ($response = solrsearch_static_response_cache($this->adapter->getSearcher())) { + if (isset($response->stats->stats_fields->{$this->single_key})) { + $stats = (array) $response->stats->stats_fields->{$this->single_key}; + foreach ($stats as $key => $val) { + $build[$this->facet['field']]['#range_' . $key] = $val; + $build[$this->facet['field']]['#global_range_' . $key] = $stats_minmax->$key; + } + } + } + return $build; + } +} diff -r 000000000000 -r a2b4f67e73dc plugins/facetapi/query_type_term.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/facetapi/query_type_term.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,106 @@ +adapter->getFacet($this->facet)->getSettings(); + // Adds the operator parameter. + $operator = $settings->settings['operator']; + $ex = (FACETAPI_OPERATOR_OR != $operator) ? '' : "{!ex={$this->facet['field']}}"; + $query->addParam('facet.field', $ex . $this->facet['field']); + + if (!empty($settings->settings['facet_missing'])) { + $query->addParam('f.' . $this->facet['field'] . '.facet.missing', 'true'); + } + // Adds "hard limit" parameter to prevent too many return values. + $limit = empty($settings->settings['hard_limit']) ? 20 : (int) $settings->settings['hard_limit']; + $query->addParam('f.' . $this->facet['field'] . '.facet.limit', $limit); + + // Adds "facet mincount" parameter to limit the number of facets. + if (isset($settings->settings['facet_mincount'])) { + $count = $settings->settings['facet_mincount']; + $query->addParam('f.' . $this->facet['field'] . '.facet.mincount', $count); + } + + $active = $this->adapter->getActiveItems($this->facet); + + // Adds filter based on the operator. + if (FACETAPI_OPERATOR_OR != $operator) { + foreach ($active as $value => $item) { + // Handle facet missing: + if ($value == '_empty_' && !empty($settings->settings['facet_missing'])) { + $query->addFilter($this->facet['field'], '[* TO *]', TRUE); + } + else { + $query->addFilter($this->facet['field'], $value); + } + } + } + else { + // OR facet. + $local = "tag={$this->facet['field']}"; + $values = array_keys($active); + if ($values) { + // Quote any values that have white space or colons. + foreach ($values as &$v) { + if (preg_match('/[:\s]/', $v)) { + $v = '"' . $v . '"'; + } + } + $query->addFilter($this->facet['field'], '(' . implode(' OR ', $values) . ')', FALSE, $local); + } + } + } + + /** + * Initializes the facet's build array. + * + * @return array + * The initialized render array. + */ + public function build() { + + + $build = array(); + if ($response = solrsearch_static_response_cache($this->adapter->getSearcher())) { + + $settings = $this->adapter->getFacet($this->facet)->getSettings(); + + if (isset($response->facet_counts->facet_fields->{$this->facet['field']})) { + $values = (array) $response->facet_counts->facet_fields->{$this->facet['field']}; + foreach ($values as $value => $count) { + // Facet missing may return 0 even if mincount is 1. + if (empty($settings->settings['facet_mincount']) || $count) { + $build[$value] = array('#count' => $count); + } + } + } + } + + return $build; + } +} diff -r 000000000000 -r a2b4f67e73dc solr-search-result.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solr-search-result.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,176 @@ + + + +
> +
+ + + +

internal

+
+ + + +
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + +
+ + +
> +
+ + + +
+ +
+
+ +
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+ + provided by: + + + +
+ +
+ + + + +
> +
+   +
+
+
+ + Signature Institute's Library: + + +
+
+ +
+
+ + + +
+
+ + + + + +
+
+ + +
> +
+   +
+
+
+ +
+
+ Collection: + + + +
+
+ + + + + +
+
+
+
+
+ + + + + diff -r 000000000000 -r a2b4f67e73dc solr-search-results.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solr-search-results.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,48 @@ + +
+ + +

+ +
+ +
+ +
+ +
+ +
+ + +
+ + +

+ + +
diff -r 000000000000 -r a2b4f67e73dc solr/schema.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solr/schema.xml Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + archive-path + title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 000000000000 -r a2b4f67e73dc solr/solrconfig.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solr/solrconfig.xml Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1940 @@ + + + + + + + + + LUCENE_40 + + + + + + + + + + + + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 15000 + false + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + static firstSearcher warming in solrconfig.xml + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + data-config.xml + + + + + + + + + + + + explicit + 10 + text + edismax + + title^10.0 IM_author^12.5 author^8.0 all-bib-data^3.0 author_c^1.0 content^1.0 + + on + doc-type + access-type + author + keyword + title + title_s + collection + + + + + + + + + + + + + + + explicit + json + true + text + + + + + + + + true + json + true + + + + + + + + explicit + + + velocity + browse + layout + MPIWG - search + + + edismax + + title^10.0 IM_author^8.5 author^8.0 all-bib-data^3.0 content^1.0 + + text + 100% + *:* + 10 + *,score + + + title^10.0 author^8.0 IM_author^8.5 ll-bib-data^3.0 + + IM_title, IM_author,author, IM_year, all-bib-data + 3 + + + on + doc-type + access-type + author + keyword + title + title_s + *:* + 1 + author,title_s + after + year + 1500 + 2020 + 30 + + + on + title author IM_author + html + <b> + </b> + 0 + title + 0 + title + 3 + 200 + content + 750 + + + on + false + 5 + 2 + 5 + true + true + 5 + 3 + + + + + spellcheck + + + + + + + explicit + + + velocity + authors + authors_layout + MPIWG - search + + + edismax + + title^10.0 IM_author^8.5 author^8.0 all-bib-data^3.0 + + text + 100% + *:* + 10 + *,score + + + title^10.0 author^8.0 IM_author^8.5 ll-bib-data^3.0 + + IM_title, IM_author,author, IM_year, all-bib-data + 3 + + + on + doc-type + author_c + + + title_s + + index + -1 + *:* + 1 + after + year + 1500 + 2020 + 30 + + + on + title author IM_author + html + <b> + </b> + 0 + title + 0 + title + 3 + 200 + content + 750 + + + on + false + 5 + 2 + 5 + true + true + 5 + 3 + + + + + spellcheck + + + + + + + explicit + + + velocity + titles + titles_layout + MPIWG - search - titles + + + edismax + + title^10.0 IM_author^8.5 author^8.0 all-bib-data^3.0 + + text + 100% + *:* + 10 + *,score + + + title^10.0 author^8.0 IM_author^8.5 ll-bib-data^3.0 + + IM_title, IM_author,author, IM_year, all-bib-data + 3 + + + on + doc-type + author_c + + + maintitle_s + + index + -1 + 500 + *:* + 1 + after + year + 1500 + 2020 + 30 + + + on + title author IM_author + html + <b> + </b> + 0 + title + 0 + title + 3 + 200 + content + 750 + + + on + false + 5 + 2 + 5 + true + true + 5 + 3 + + + + + spellcheck + + + + + + + + + + + + + application/json + + + + + application/csv + + + + + + + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + solrpingquery + + + all + + + + + + + + + explicit + true + + + + + + + + + + + + + + + + textSpell + + + + + + default + name + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + solr.WordBreakSolrSpellChecker + name + true + true + 10 + + + + + + + + + + + + + + + + text + + default + wordbreak + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + spellcheck + + + + + + + + + + text + true + + + tvComponent + + + + + + + + + default + + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + + 20 + + + clustering/carrot2 + + + GERMAN + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + + + true + default + true + + name + id + + features + + true + + + + false + + edismax + + title^2.0 author^3.0 year^1.2 + + *:* + 10 + *,score + + + clustering + + + + + + + + + + true + false + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + text + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + *:* + + + diff -r 000000000000 -r a2b4f67e73dc solrsearch-author-block-form.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-author-block-form.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,40 @@ + + *
+ * + *
+ * + * @endcode + * + * @see template_preprocess_search_block_form() + */ +?> +
+ subject)): ?> +

+ +

+
+ +
+
diff -r 000000000000 -r a2b4f67e73dc solrsearch-block-form.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-block-form.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,40 @@ + + *
+ * + *
+ * + * @endcode + * + * @see template_preprocess_search_block_form() + */ +?> +
+ subject)): ?> +

+ +

+
+ +
+
diff -r 000000000000 -r a2b4f67e73dc solrsearch-mpiwg.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-mpiwg.js Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,61 @@ +/* + * Javascript for MPIWG website + */ + +(function ($) { +$(document).ready(function() { + /* + * autosubmit forms + */ + + + $('form.autosubmit').find('.autosubmit').change(function() { + this.form.submit(); + }); + // hide submit button + $('form.autosubmit input[type="submit"].autosubmit').hide(); + + /* + * foldout divs + */ + $('.foldable').each(function() { + var $this = $(this); + var $head = $this.find('.fold_head'); + var $img = $head.find('img'); + var $body = $this.find('.fold_body'); + $head.on('click', function() { + $body.slideToggle('fast'); + $img.toggle(); + }).css('cursor', 'pointer'); + if (! $this.hasClass('initially_open')) { + // hide body initially + $body.hide(); + $img.toggle(); + } + }); + + +/* the following block was uncommented. it was interfering with the code from collapsiblock (M. Trognitz) */ +/* $('.collapsiblock-processed').each(function() { + var $this = $(this); + var $head = $this.children('h2'); + //var $img = $head.find('img'); + + $head.each(function(){ + var $body = $head.next(); + + $(this).on('click', function() { + $body.slideToggle('fast'); + //$img.toggle(); + }).css('cursor', 'pointer'); + if (! $(this).hasClass('initially_open')) { + // hide body initially + $body.hide(); + //$img.toggle(); + } + }); + });*/ + +}); +})(jQuery); + diff -r 000000000000 -r a2b4f67e73dc solrsearch-term-list-author.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-term-list-author.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,5 @@ +

authors found for:

+ + $hits):?> +
+ diff -r 000000000000 -r a2b4f67e73dc solrsearch-term-list-title.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-term-list-title.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,5 @@ +

titles found for:

+ + $hits):?> +
+ diff -r 000000000000 -r a2b4f67e73dc solrsearch-term-selection-form.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-term-selection-form.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,41 @@ + +

Browse all Sources

+ +
+
+ +
+
    +
  • + checked="checked" name="browseField" value="author_c"/> + Author A-Z +
  • +
  • + checked="checked" name="browseField" value="title_s"/> + Title A-Z +
  • +
  • + +
  • +
+
+
+ +

Choose a character

+
+
+
diff -r 000000000000 -r a2b4f67e73dc solrsearch-title-block-form.tpl.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch-title-block-form.tpl.php Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,40 @@ + + *
+ * + *
+ * + * @endcode + * + * @see template_preprocess_search_block_form() + */ +?> +
+ subject)): ?> +

+ +

+
+ +
+
diff -r 000000000000 -r a2b4f67e73dc solrsearch.admin.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.admin.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,817 @@ + 'value', + '#value' => $environment['env_id'], + ); + if (isset($environment['export_type']) && $environment['export_type'] == 3) { + $verb = t('Revert'); + } + else { + $verb = t('Delete'); + } + return confirm_form( + $form, + t('Are you sure you want to !verb search environment %name?', array('%name' => $environment['name'], '!verb' => strtolower($verb))), + 'admin/config/search/solrsearch', + t('This action cannot be undone.'), + $verb, + t('Cancel') + ); +} + +/** + * Submit handler for the delete form + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_delete_form_submit(array $form, array &$form_state) { + if (solrsearch_environment_delete($form_state['values']['env_id'])) { + drupal_set_message(t('The search environment was deleted')); + } + $form_state['redirect'] = 'admin/config/search/solrsearch/settings'; +} + +function solrsearch_environment_edit_delete_submit($form, &$form_state) { + $form_state['redirect'] = 'admin/config/search/solrsearch/settings/' . $form_state['values']['env_id'] . '/delete'; + + // Regardlessly of the destination parameter we want to go to another page + unset($_GET['destination']); + drupal_static_reset('drupal_get_destination'); + drupal_get_destination(); +} + +/** + * Settings page for a specific environment (or default one if not provided) + * + * @param array|bool $environment + * + * @return array Render array for a settings page + */ +function solrsearch_environment_settings_page(array $environment = array()) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + $environment = solrsearch_environment_load($env_id); + } + $env_id = $environment['env_id']; + + // Initializes output with information about which environment's setting we are + // editing, as it is otherwise not transparent to the end user. + $output = array( + 'solrsearch_environment' => array( + '#theme' => 'solrsearch_settings_title', + '#env_id' => $env_id, + ), + ); + $output['form'] = drupal_get_form('solrsearch_environment_edit_form', $environment); + return $output; +} + +/** + * Form to clone a certain environment + * + * @param array $form + * @param array $form_state + * @param array $environment + * + * @return array output of confirm_form() + */ +function solrsearch_environment_clone_form(array $form, array &$form_state, array $environment) { + $form['env_id'] = array( + '#type' => 'value', + '#value' => $environment['env_id'], + ); + return confirm_form( + $form, + t('Are you sure you want to clone search environment %name?', array('%name' => $environment['name'])), + 'admin/config/search/solrsearch', + '', + t('Clone'), + t('Cancel') + ); +} + +/** + * Submit handler for the clone form + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_clone_form_submit(array $form, array &$form_state) { + if (solrsearch_environment_clone($form_state['values']['env_id'])) { + drupal_set_message(t('The search environment was cloned')); + } + $form_state['redirect'] = 'admin/config/search/solrsearch/settings'; +} + +/** + * Submit handler for the confirmation page of cloning an environment + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_clone_submit(array $form, array &$form_state) { + $form_state['redirect'] = 'admin/config/search/solrsearch/settings/' . $form_state['values']['env_id'] . '/clone'; +} + +/** + * Form builder for adding/editing a Solr environment used as a menu callback. + */ +function solrsearch_environment_edit_form(array $form, array &$form_state, array $environment = array()) { + if (empty($environment)) { + $environment = array(); + } + $environment += array('env_id' => '', 'name' => '', 'url' => '', 'service_class' => '', 'conf' => array()); + + $form['#environment'] = $environment; + $form['url'] = array( + '#type' => 'textfield', + '#title' => t('Solr server URL'), + '#default_value' => $environment['url'], + '#description' => t('Example: http://localhost:8983/solr'), + '#required' => TRUE, + ); + $is_default = $environment['env_id'] == solrsearch_default_environment(); + $form['make_default'] = array( + '#type' => 'checkbox', + '#title' => t('Make this Solr search environment the default'), + '#default_value' => $is_default, + '#disabled' => $is_default, + ); + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Description'), + '#default_value' => $environment['name'], + '#required' => TRUE, + ); + $form['env_id'] = array( + '#type' => 'machine_name', + '#title' => t('Environment id'), + '#machine_name' => array( + 'exists' => 'solrsearch_environment_load', + ), + '#default_value' => $environment['env_id'], + '#disabled' => !empty($environment['env_id']), // Cannot change it once set. + '#description' => t('Unique, machine-readable identifier for this Solr environment.'), + '#required' => TRUE, + ); + $form['service_class'] = array( + '#type' => 'value', + '#value' => $environment['service_class'], + ); + $form['conf'] = array( + '#tree' => TRUE, + ); + $form['conf']['solrsearch_read_only'] = array( + '#type' => 'radios', + '#title' => t('Index write access'), + '#default_value' => isset($environment['conf']['solrsearch_read_only']) ? $environment['conf']['solrsearch_read_only'] : solrsearch_READ_WRITE, + '#options' => array(solrsearch_READ_WRITE => t('Read and write (normal)'), solrsearch_READ_ONLY => t('Read only')), + '#description' => t('Read only stops this site from sending updates to this search environment. Useful for development sites.'), + ); + $form['actions'] = array( + '#type' => 'actions', + ); + $form['actions']['save'] = array( + '#type' => 'submit', + '#validate' => array('solrsearch_environment_edit_validate'), + '#submit' => array('solrsearch_environment_edit_submit'), + '#value' => t('Save'), + ); + $form['actions']['save_edit'] = array( + '#type' => 'submit', + '#validate' => array('solrsearch_environment_edit_validate'), + '#submit' => array('solrsearch_environment_edit_submit'), + '#value' => t('Save and edit'), + ); + $form['actions']['test'] = array( + '#type' => 'submit', + '#validate' => array('solrsearch_environment_edit_validate'), + '#submit' => array('solrsearch_environment_edit_test_submit'), + '#value' => t('Test connection'), + ); + if (!empty($environment['env_id']) && !$is_default) { + $form['actions']['delete'] = array( + '#type' => 'submit', + '#submit' => array('solrsearch_environment_edit_delete_submit'), + '#value' => t('Delete'), + ); + } + + // Ensures destination is an internal URL, builds "cancel" link. + if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) { + $destination = $_GET['destination']; + } + else { + $destination = 'admin/config/search/solrsearch/settings'; + } + $form['actions']['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#href' => $destination, + ); + + return $form; +} + +/** + * Submit handler for the test button in the environment edit page + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_edit_test_submit(array $form, array &$form_state) { + $ping = solrsearch_server_status($form_state['values']['url'], $form_state['values']['service_class']); + if ($ping) { + drupal_set_message(t('Your site has contacted the Apache Solr server.')); + } + else { + drupal_set_message(t('Your site was unable to contact the Apache Solr server.'), 'error'); + } + $form_state['rebuild'] = TRUE; +} + +/** + * Validate handler for the environment edit page + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_edit_validate(array $form, array &$form_state) { + $parts = parse_url($form_state['values']['url']); + foreach (array('scheme', 'host', 'path') as $key) { + if (empty($parts[$key])) { + form_set_error('url', t('The Solr server URL needs to include a !part', array('!part' => $key))); + } + } + if (isset($parts['port'])) { + // parse_url() should always give an integer for port. Since drupal_http_request() + // also uses parse_url(), we don't need to validate anything except the range. + $pattern = empty($parts['user']) ? '@://[^:]+:([^/]+)@' : '#://[^@]+@[^:]+:([^/]+)#'; + preg_match($pattern, $form_state['values']['url'], $m); + if (empty($m[1]) || !ctype_digit($m[1]) || $m[1] < 1 || $m[1] > 65535) { + form_set_error('port', t('The port has to be an integer between 1 and 65535.')); + } + else { + // Normalize the url by removing extra slashes and whitespace. + $form_state['values']['url'] = trim($form_state['values']['url'], "/ \t\r\n\0\x0B"); + } + } +} + +/** + * Submit handler for the environment edit page + * + * @param array $form + * @param array $form_state + */ +function solrsearch_environment_edit_submit(array $form, array &$form_state) { + solrsearch_environment_save($form_state['values']); + if (!empty($form_state['values']['make_default'])) { + solrsearch_set_default_environment($form_state['values']['env_id']); + } + cache_clear_all('solrsearch:environments', 'cache_solrsearch'); + drupal_set_message(t('The %name search environment has been saved.', array('%name' => $form_state['values']['name']))); + if ($form_state['values']['op'] == t('Save')) { + $form_state['redirect'] = 'admin/config/search/solrsearch/settings'; + } + else { + $form_state['redirect'] = current_path(); + } + // Regardlessly of the destination parameter we want to go to another page + unset($_GET['destination']); + drupal_static_reset('drupal_get_destination'); + drupal_get_destination(); +} + +/** + * Check to see if the facetapi module is installed, and if not put up + * a message. + * + * Only call this function if the user is already in a position for this to + * be useful. + */ +function solrsearch_check_facetapi() { + if (!module_exists('facetapi')) { + $filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'facetapi'", 0, 1) + ->fetchField(); + if ($filename && file_exists($filename)) { + drupal_set_message(t('If you enable the facetapi module, Apache Solr Search will provide you with configurable facets.', array('@modules' => url('admin/modules')))); + } + else { + drupal_set_message(t('If you install the facetapi module from !href, Apache Solr Search will provide you with configurable facets.', array('!href' => url('http://drupal.org/project/facetapi')))); + } + } +} + +/** + * Form builder for general settings used as a menu callback. + * + * @param array $form + * @param array $form_state + * + * @return array Output of the system_settings_form() + */ +function solrsearch_settings(array $form, array &$form_state) { + $form = array(); + $rows = array(); + + // Environment settings // Take environment settings form solrsearch + $id = solrsearch_default_environment(); + + $environments = solrsearch_load_all_environments(); + $default_environment = solrsearch_default_environment(); + solrsearch_check_facetapi(); + + // Reserve a row for the default one + $rows[$default_environment] = array(); + + foreach ($environments as $environment_id => $data) { + // Define all the Operations + $confs = array(); + $ops = array(); + // Whenever facetapi is enabled we also enable our operation link + if (module_exists('facetapi')) { + $confs['facets'] = array( + 'class' => 'operation', + 'data' => l(t('Facets'), + 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/facets', + array('query' => array('destination' => current_path())) + ), + ); + } + // These are our result and bias settings + if (module_exists('solrsearch_search')) { + $confs['result_bias'] = array( + 'class' => 'operation', + 'data' => l(t('Bias'), + 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/bias', + array('query' => array('destination' => current_path())) + ), + ); + } + + $ops['edit'] = array( + 'class' => 'operation', + 'data' => l(t('Edit'), + 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/edit', + array('query' => array('destination' => current_path())) + ), + ); + + $ops['clone'] = array( + 'class' => 'operation', + 'data' => l(t('Clone'), + 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/clone', + array('query' => array('destination' => $_GET['q'])) + ), + ); + $env_name = l($data['name'], 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/edit', array('query' => array('destination' => $_GET['q']))); + + // Is this row our default environment? + if ($environment_id == $default_environment) { + $env_name = t('!environment (Default)', array('!environment' => $env_name)); + $env_class_row = 'default-environment'; + } + else { + $env_class_row = ''; + } + // For every non-default we add a delete link + // Allow to revert a search environment or to delete it + $delete_value = ''; + if (!isset($data['in_code_only'])) { + if ((isset($data['type']) && $data['type'] == 'Overridden')) { + $delete_value = array( + 'class' => 'operation', + 'data' => l(t('Revert'), 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/delete'), + ); + } + // don't allow the deletion of the default environment + elseif ($environment_id != $default_environment) { + $delete_value = array( + 'class' => 'operation', + 'data' => l(t('Delete'), 'admin/config/search/solrsearch/settings/' . $data['env_id'] . '/delete'), + ); + } + } + $ops['delete'] = $delete_value; + + // When we are receiving a http POST (so the page does not show) we do not + // want to check the statusses of any environment + $class = ''; + if (empty($form_state['input'])) { + $class = solrsearch_server_status($data['url'], $data['service_class']) ? 'ok' : 'error'; + } + + $headers = array( + array('data' => t('Name'), 'colspan' => 2), + t('URL'), + array('data' => t('Configuration'), 'colspan' => count($confs)), + array('data' => t('Operations'), 'colspan' => count($ops)), + ); + + $rows[$environment_id] = array('data' => + array( + // Cells + array( + 'class' => 'status-icon', + 'data' => '
' . $class . '
', + ), + array( + 'class' => $env_class_row, + 'data' => $env_name, + ), + check_plain($data['url']), + ), + 'class' => array(drupal_html_class($class)), + ); + // Add the links to the page + $rows[$environment_id]['data'] = array_merge($rows[$environment_id]['data'], $confs); + $rows[$environment_id]['data'] = array_merge($rows[$environment_id]['data'], $ops); + } + + $form['solrsearch_host_settings']['actions'] = array( + '#markup' => '', + ); + $form['solrsearch_host_settings']['table'] = array( + '#theme' => 'table', + '#header' => $headers, + '#rows' => array_values($rows), + '#attributes' => array('class' => array('admin-solrsearch')), + ); + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced configuration'), + '#collapsed' => TRUE, + '#collapsible' => TRUE, + ); + $form['advanced']['solrsearch_set_nodeapi_messages'] = array( + '#type' => 'radios', + '#title' => t('Extra help messages for administrators'), + '#description' => t('Adds notices to a page whenever Drupal changed content that needs reindexing'), + '#default_value' => variable_get('solrsearch_set_nodeapi_messages', 1), + '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), + ); + + // Number of Items to index + $numbers = drupal_map_assoc(array(1, 5, 10, 20, 50, 100, 200)); + $default_cron_limit = variable_get('solrsearch_cron_limit', 50); + + // solrsearch_cron_limit may be overridden in settings.php. If its current + // value is not among the default set of options, add it. + if (!isset($numbers[$default_cron_limit])) { + $numbers[$default_cron_limit] = $default_cron_limit; + } + $form['advanced']['solrsearch_cron_limit'] = array( + '#type' => 'select', + '#title' => t('Number of items to index per cron run'), + '#default_value' => $default_cron_limit, + '#options' => $numbers, + '#description' => t('Reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status'))) + ); + + $options = array('solrsearch:show_error' => t('Show error message')); + $system_info = system_get_info('module'); + if (module_exists('search')) { + foreach (search_get_info() as $module => $search_info) { + // Don't allow solrsearch to return results on failure of solrsearch. + if ($module == 'solrsearch_search') { + continue; + } + $options[$module] = t('Show @name search results', array('@name' => $system_info[$module]['name'])); + } + } + + $options['solrsearch:show_no_results'] = t('Show no results'); + $form['advanced']['solrsearch_failure'] = array( + '#type' => 'select', + '#title' => t('On failure'), + '#options' => $options, + '#default_value' => variable_get('solrsearch_failure', 'solrsearch:show_error'), + ); + + return system_settings_form($form); +} + +/** + * Gets information about the fields already in solr index. + * + * @param array $environment + * The environment for which we need to ask the status from + * + * @return array page render array + */ +function solrsearch_status_page($environment = array()) { + + if (empty($environment)) { + + $env_id = solrsearch_default_environment(); + $environment = solrsearch_environment_load($env_id); + } + else { + $env_id = $environment['env_id']; + } + + // Check for availability + if (!solrsearch_server_status($environment['url'], $environment['service_class'])) { + + drupal_set_message(t('The server seems to be unavailable. Please verify the server settings at the settings page', array('!settings_page' => url("admin/config/search/solrsearch/settings/{$environment['env_id']}/edit", array('query' => drupal_get_destination())))), 'warning'); + return ''; + } + + try { + $solr = solrsearch_get_solr($environment["env_id"]); + $solr->clearCache(); + $data = $solr->getLuke(); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + $data = new stdClass; + $data->fields = array(); + } + + $messages = array(); + + // Initializes output with information about which server's setting we are + // editing, as it is otherwise not transparent to the end user. + $output['solrsearch_index_action_status'] = array( + '#prefix' => '

' . t('@environment: Search Index Content', array('@environment' => $environment['name'])) . '

', + '#theme' => 'table', + '#header' => array(t('Type'), t('Value')), + '#rows' => $messages, + ); + + $output['viewmore'] = array( + '#markup' => l(t('View more details on the search index contents'), 'admin/reports/solrsearch'), + ); + + $write_status = solrsearch_environment_variable_get($env_id, 'solrsearch_read_only', solrsearch_READ_WRITE); + if ($write_status == solrsearch_READ_WRITE) { + /*$output['index_action_form'] = drupal_get_form('solrsearch_index_action_form', $env_id); + $output['index_config_form'] = drupal_get_form('solrsearch_index_config_form', $env_id);*/ + } + else { + drupal_set_message(t('Options for deleting and re-indexing are not available because the index is read-only. This can be changed on the settings page', array('!settings_page' => url('admin/config/search/solrsearch/settings/' . $env_id . '/edit', array('query' => drupal_get_destination())))), 'warning'); + } + + return $output; +} + +/** + * Get the report, eg.: some statistics and useful data from the Apache Solr index + * + * @param array $environment + * + * @return array page render array + */ +function solrsearch_index_report(array $environment = array()) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + drupal_goto('admin/reports/solrsearch/' . $env_id); + } + $environments = solrsearch_load_all_environments(); + $environments_list = array(); + foreach ($environments as $env) { + $var_status = array('!name' =>$env['name']); + $environments_list[] = l(t('Statistics for !name', $var_status), 'admin/reports/solrsearch/' . $env['env_id']); + } + $output['environments_list'] = array( + '#theme' => 'item_list', + '#items' => $environments_list, + ); + + try { + $solr = solrsearch_get_solr($environment['env_id']); + $solr->clearCache(); + $data = $solr->getLuke(); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + return $output; + } + + $messages = array(); + $messages[] = array(t('Number of documents in index'), $data->index->numDocs); + + $limit = variable_get('solrsearch_luke_limit', 20000); + if (isset($data->index->numDocs) && $data->index->numDocs > $limit) { + $messages[] = array(t('Limit'), t('You have more than @limit documents, so term frequencies are being omitted for performance reasons.', array('@limit' => $limit))); + $not_found = t('Omitted'); + } + elseif (isset($data->index->numDocs)) { + $not_found = t('Not indexed'); + try { + $solr = solrsearch_get_solr($environment['env_id']); + // Note: we use 2 since 1 fails on Ubuntu Hardy. + $data = $solr->getLuke(2); + if (isset($data->index->numTerms)) { + $messages[] = array(t('# of terms in index'), $data->index->numTerms); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + $data->fields = array(); + } + } + // Initializes output with information about which server's setting we are + // editing, as it is otherwise not transparent to the end user. + $fields = (array)$data->fields; + if ($fields) { + $messages[] = array(t('# of fields in index'), count($fields)); + } + + // Output the messages we have for this page + $output['solrsearch_index_report'] = array( + '#theme' => 'table', + '#header' => array('type', 'value'), + '#rows' => $messages, + ); + + if ($fields) { + // Initializes table header. + $header = array( + 'name' => t('Field name'), + 'type' => t('Index type'), + 'terms' => t('Distinct terms'), + ); + + // Builds table rows. + $rows = array(); + foreach ($fields as $name => $field) { + // TODO: try to map the name to something more meaningful. + $rows[$name] = array( + 'name' => $name, + 'type' => $field->type, + 'terms' => isset($field->distinct) ? $field->distinct : $not_found + ); + } + ksort($rows); + // Output the fields we found for this environment + $output['field_table'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + } + else { + $output['field_table'] = array('#markup' => t('No data on indexed fields.')); + } + return $output; +} + +/** + * Page callback to show available conf files. + * + * @param array $environment + * + * @return string + * A non-render array but plain theme output for the config files overview. Could be done better probably + */ +function solrsearch_config_files_overview(array $environment = array()) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + } + else { + $env_id = $environment['env_id']; + } + + $xml = NULL; + try { + $solr = solrsearch_get_solr($env_id); + $response = $solr->makeServletRequest('admin/file', array('wt' => 'xml')); + $xml = simplexml_load_string($response->data); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + } + + if ($xml) { + // Retrieve our items from the xml using xpath + $items = $xml->xpath('//lst[@name="files"]/lst'); + + // Add all the data of the file in a files array + $files = array(); + foreach ($items as $item_id => $item) { + // Do not list directories. Always a bool + if (isset($item->bool)) { + break; + } + // Get data from the files. + $name = $item->attributes(); + $name = ((string)$item->attributes()) ? (string)$item->attributes() : t('No name found'); + $files[$item_id]['name'] = l($name, 'admin/reports/solrsearch/' . $env_id . '/conf/' . $name); + + // Retrieve the date attribute + if (isset($item->date)) { + $modified = ((string)$item->date->attributes() == 'modified') ? (string) $item->date : t('No date found'); + $files[$item_id]['modified'] = format_date(strtotime($modified)); + } + + // Retrieve the size attribute + if (isset($item->long)) { + $size = ((string)$item->long->attributes() == 'size') ? (string) $item->long : t('No size found'); + $files[$item_id]['size'] = t('Size (bytes): @bytes', array('@bytes' => $size)); + } + } + // Sort our files alphabetically + ksort($files); + + // Initializes table header. + $header = array( + 'name' => t('File name'), + 'date' => t('Modified'), + 'size' => t('Size'), + ); + + // Display the table of field names, index types, and term counts. + $variables = array( + 'header' => $header, + 'rows' => $files, + ); + $output = theme('table', $variables); + } + else { + $output = '

' . t('No data about any file found.') . "

\n"; + } + return $output; +} + +/** + * Page callback to show one conf file. + * + * @param string $name + * @param array $environment + * + * @return string + * the requested config file + */ +function solrsearch_config_file($name, array $environment = array()) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + } + else { + $env_id = $environment['env_id']; + } + + $output = ''; + try { + $solr = solrsearch_get_solr($env_id); + $response = $solr->makeServletRequest('admin/file', array('file' => $name)); + $raw_file = $response->data; + $output = '
' . check_plain($raw_file) . '
'; + drupal_set_title(check_plain($name)); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + } + return $output; +} + + + +/** + * Page callback for node/%node/devel/solrsearch. + * + * @param object $node + * @return string debugging information + */ +function solrsearch_devel($node) { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $item = new stdClass(); + $item->entity_type = 'node'; + $item->entity_id = $node->nid; + $output = ''; + foreach (solrsearch_load_all_environments() as $env_id => $environment) { + $documents = solrsearch_index_entity_to_documents($item, $env_id); + $output .= '

' . t('Environment %name (%env_id)', array('%name' => $environment['name'], '%env_id' => $env_id)). '

'; + foreach ($documents as $document) { + $debug_data = array(); + foreach ($document as $key => $value) { + $debug_data[$key] = $value; + } + $output .= kdevel_print_object($debug_data); + } + } + return $output; +} diff -r 000000000000 -r a2b4f67e73dc solrsearch.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.css Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,22 @@ +p.solrext-help, +div.solrext { +display: block; +color: white; +padding: 5px; +background-color: black; +border: 4px solid white; +-webkit-border-radius: 8px; +-moz-border-radius: 8px; +border-radius: 8px; +} + + +div.solrsearch_search_form div input.form-submit { + +background: url("http://localhost/drupal-dev/themes/bartik/images/search-button.png") no-repeat scroll center top transparent; +text-indent: -9999px; + } + +.solrsearch_internal{ +background-color: red; +} \ No newline at end of file diff -r 000000000000 -r a2b4f67e73dc solrsearch.index.inc_unused --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.index.inc_unused Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1469 @@ + $info) { + // With each pass through the callback, retrieve the next group of nids. + $rows = solrsearch_index_get_entities_to_index($env_id, $entity_type, $limit); + $documents = array(); + foreach ($rows as $row) { + $row_documents = solrsearch_index_entities_document($row, $entity_type, $env_id); + $documents = array_merge($documents, $row_documents); + } + + $indexed = solrsearch_index_send_to_solr($env_id, $documents); + if ($indexed !== FALSE) { + $documents_submitted += count($documents); + $index_position = solrsearch_get_last_index_position($env_id, $entity_type); + $max_changed = $index_position['last_changed']; + $max_entity_id = $index_position['last_entity_id']; + foreach ($rows as $row) { + if (!empty($row->status)) { + if ($row->changed > $max_changed) { + $max_changed = $row->changed; + } + if ($row->entity_id > $max_entity_id) { + $max_entity_id = $row->entity_id; + } + } + } + solrsearch_set_last_index_position($env_id, $entity_type, $max_changed, $max_entity_id); + solrsearch_set_last_index_updated($env_id, REQUEST_TIME); + } + } + return $documents_submitted; +} + +/** + * Convert a certain entity from the solrsearch index table to a set of documents. 1 entity + * can be converted in multiple documents if the solrsearch_index_entity_to_documents decides to do so. + * + * @param array $row + * A row from the indexing table + * @param string $entity_type + * The type of the entity + * @param string $env_id + * The machine name of the environment. + * + * @return array of solrsearchDocument(s) + */ +function solrsearch_index_entities_document($row, $entity_type, $env_id) { + $documents = array(); + if (!empty($row->status)) { + // Let any module exclude this entity from the index. + $build_document = TRUE; + foreach (module_implements('solrsearch_exclude') as $module) { + $exclude = module_invoke($module, 'solrsearch_exclude', $row->entity_id, $entity_type, $row, $env_id); + // If the hook returns TRUE we should exclude the entity + if (!empty($exclude)) { + $build_document = FALSE; + } + } + foreach (module_implements('solrsearch_' . $entity_type . '_exclude') as $module) { + $exclude = module_invoke($module, 'solrsearch_' . $entity_type . '_exclude', $row->entity_id, $row, $env_id); + // If the hook returns TRUE we should exclude the entity + if (!empty($exclude)) { + $build_document = FALSE; + } + } + if ($build_document) { + $documents = array_merge($documents, solrsearch_index_entity_to_documents($row, $env_id)); + } + } + else { + // Delete the entity from our index if the status callback returned 0 + solrsearch_remove_entity($env_id, $row->entity_type, $row->entity_id); + } + // Clear entity cache for this specific entity + entity_get_controller($row->entity_type)->resetCache(array($row->entity_id)); + return $documents; +} +/** + * Returns the total number of documents that are able to be indexed and the + * number of documents left to be indexed. + * + * This is a helper function for modules that implement hook_search_status(). + * + * @param string $env_id + * The machine name of the environment. + * + * @return array + * An associative array with the key-value pairs: + * - remaining: The number of items left to index. + * - total: The total number of items to index. + * + * @see hook_search_status() + */ +function solrsearch_index_status($env_id) { + $remaining = 0; + $total = 0; + + foreach (entity_get_info() as $entity_type => $info) { + $bundles = solrsearch_get_index_bundles($env_id, $entity_type); + if (empty($bundles)) { + continue; + } + + $table = solrsearch_get_indexer_table($entity_type); + $query = db_select($table, 'asn')->condition('asn.status', 1)->condition('asn.bundle', $bundles); + $total += $query->countQuery()->execute()->fetchField(); + + // Get $last_entity_id and $last_changed. + $last_index_position = solrsearch_get_last_index_position($env_id, $entity_type); + $last_entity_id = $last_index_position['last_entity_id']; + $last_changed = $last_index_position['last_changed']; + + // Find the remaining entities to index for this entity type. + $query = db_select($table, 'aie') + ->condition('aie.bundle', $bundles) + ->condition('aie.status', 1) + ->condition(db_or() + ->condition('aie.changed', $last_changed, '>') + ->condition(db_and() + ->condition('aie.changed', $last_changed, '<=') + ->condition('aie.entity_id', $last_entity_id, '>'))) + ->addTag('solrsearch_index_' . $entity_type); + + + if ($table == 'solrsearch_index_entities') { + // Other, entity-specific tables don't need this condition. + $query->condition('aie.entity_type', $entity_type); + } + $remaining += $query->countQuery()->execute()->fetchField(); + } + return array('remaining' => $remaining, 'total' => $total); +} + +/** + * Worker callback for solrsearch_index_entities(). + * + * Loads and proccesses the entity queued for indexing and converts into one or + * more documents that are sent to the Apache Solr server for indexing. + * + * The entity is loaded as the user specified in the "solrsearch_index_user" + * system variable in order to prevent sentive data from being indexed and + * displayed to underprivileged users in search results. The index user defaults + * to a user ID of "0", which is the anonymous user. + * + * After the entity is loaded, it is converted to an array via the callback + * specified in the entity type's info array. The array that the entity is + * converted to is the model of the document sent to the Apache Solr server for + * indexing. This function allows develoeprs to modify the document by + * implementing the following hooks: + * - solrsearch_index_document_build() + * - solrsearch_index_document_build_ENTITY_TYPE() + * - solrsearch_index_documents_alter() + * + * @param stdClass $item + * The data returned by the queue table containing: + * - entity_id: An integer containing the unique identifier of the entity, for + * example a node ID or comment ID. + * - entity_type: The unique identifier for the entity, i.e. "node", "file". + * - bundle: The machine-readable name of the bundle the passed entity is + * associated with. + * - status: The "published" status of the entity. The status will also be set + * to "0" when entity is deleted but the Apache Solr server is unavailable. + * - changed: A timestamp flagging when the entity was last modified. + * @param string $env_id + * The machine name of the environment. + * + * @return array + * An associative array of documents that are sent to the Apache Solr server + * for indexing. + * + * @see solrsearch_index_nodes() for the old-skool version. + */ +function solrsearch_index_entity_to_documents($item, $env_id) { + global $user; + drupal_save_session(FALSE); + $saved_user = $user; + // build the content for the index as an anonymous user to avoid exposing restricted fields and such. + // By setting a variable, indexing can take place as a different user + $uid = variable_get('solrsearch_index_user', 0); + if ($uid == 0) { + $user = drupal_anonymous_user(); + } + else { + $user = user_load($uid); + } + // Pull out all of our pertinent data. + $entity_type = $item->entity_type; + + // Entity cache will be reset at the end of the indexing algorithm, to use the cache properly whenever + // the code does another entity_load + $entity = entity_load($entity_type, array($item->entity_id)); + $entity = $entity ? reset($entity) : FALSE; + + if (empty($entity)) { + // If the object failed to load, just stop. + return FALSE; + } + + list($entity_id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // Create a new document, and do the bare minimum on it. + $document = _solrsearch_index_process_entity_get_document($entity, $entity_type); + + //Get the callback array to add stuff to the document + $callbacks = solrsearch_entity_get_callback($entity_type, 'document callback', $bundle); + $documents = array(); + foreach ($callbacks as $callback) { + // Call a type-specific callback to add stuff to the document. + $documents = array_merge($documents, $callback($document, $entity, $entity_type, $env_id)); + } + + //do this for all possible documents that were returned by the callbacks + foreach ($documents as $document) { + // Call an all-entity hook to add stuff to the document. + module_invoke_all('solrsearch_index_document_build', $document, $entity, $entity_type, $env_id); + + // Call a type-specific hook to add stuff to the document. + module_invoke_all('solrsearch_index_document_build_' . $entity_type, $document, $entity, $env_id); + + // Final processing to ensure that the document is properly structured. + // All records must have a label field, which is used for user-friendly labeling. + if (empty($document->label)) { + $document->label = ''; + } + + // All records must have a "content" field, which is used for fulltext indexing. + // If we don't have one, enter an empty value. This does mean that the entity + // will not be fulltext searchable. + if (empty($document->content)) { + $document->content = ''; + } + + // All records must have a "teaser" field, which is used for abbreviated + // displays when no highlighted text is available. + if (empty($document->teaser)) { + $document->teaser = truncate_utf8($document->content, 300, TRUE); + } + + // Add additional indexing based on the body of each record. + solrsearch_index_add_tags_to_document($document, $document->content); + } + + // Now allow modules to alter each other's additions for maximum flexibility. + + // Hook to allow modifications of the retrieved results + foreach (module_implements('solrsearch_index_documents_alter') as $module) { + $function = $module . '_solrsearch_index_documents_alter'; + $function($documents, $entity, $entity_type, $env_id); + } + + // Restore the user. + $user = $saved_user; + drupal_save_session(TRUE); + + return $documents; +} + +/** + * Index an array of documents to solr. + * + * @param $env_id + * @param array $documents + * + * @return bool|int number indexed, or FALSE on failure. + * @throws Exception + */ +function solrsearch_index_send_to_solr($env_id, array $documents) { + try { + // Get the $solr object + $solr = solrsearch_get_solr($env_id); + // If there is no server available, don't continue. + if (!$solr->ping(variable_get('solrsearch_ping_timeout', 4))) { + throw new Exception(t('No Solr instance available during indexing.')); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + return FALSE; + } + // Do not index when we do not have any documents to send + // Send TRUE because this is not an error + if (empty($documents)) { + return TRUE; + } + // Send the document off to Solr. + watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents))); + try { + $docs_chunk = array_chunk($documents, 20); + foreach ($docs_chunk as $docs) { + $solr->addDocuments($docs); + } + watchdog('Apache Solr', 'Indexing succeeded on @count documents', array( + '@count' => count($documents), + ), WATCHDOG_INFO); + return count($documents); + } + catch (Exception $e) { + if (!empty($docs)) { + foreach ($docs as $doc) { + $eids[] = $doc->entity_type . '/' . $doc->entity_id; + } + } + watchdog('Apache Solr', 'Indexing failed on one of the following entity ids: @eids
!message', array( + '@eids' => implode(', ', $eids), + '!message' => nl2br(strip_tags($e->getMessage())), + ), WATCHDOG_ERROR); + return FALSE; + } +} + +/** + * Extract HTML tag contents from $text and add to boost fields. + * + * @param solrsearchDocument $document + * @param string $text + * must be stripped of control characters before hand. + * + */ +function solrsearch_index_add_tags_to_document(solrsearchDocument $document, $text) { + $tags_to_index = variable_get('solrsearch_tags_to_index', array( + 'h1' => 'tags_h1', + 'h2' => 'tags_h2_h3', + 'h3' => 'tags_h2_h3', + 'h4' => 'tags_h4_h5_h6', + 'h5' => 'tags_h4_h5_h6', + 'h6' => 'tags_h4_h5_h6', + 'u' => 'tags_inline', + 'b' => 'tags_inline', + 'i' => 'tags_inline', + 'strong' => 'tags_inline', + 'em' => 'tags_inline', + 'a' => 'tags_a' + )); + + // Strip off all ignored tags. + $text = strip_tags($text, '<' . implode('><', array_keys($tags_to_index)) . '>'); + + preg_match_all('@<(' . implode('|', array_keys($tags_to_index)) . ')[^>]*>(.*)@Ui', $text, $matches); + foreach ($matches[1] as $key => $tag) { + $tag = drupal_strtolower($tag); + // We don't want to index links auto-generated by the url filter. + if ($tag != 'a' || !preg_match('@(?:http://|https://|ftp://|mailto:|smb://|afp://|file://|gopher://|news://|ssl://|sslv2://|sslv3://|tls://|tcp://|udp://|www\.)[a-zA-Z0-9]+@', $matches[2][$key])) { + if (!isset($document->{$tags_to_index[$tag]})) { + $document->{$tags_to_index[$tag]} = ''; + } + $document->{$tags_to_index[$tag]} .= ' ' . solrsearch_clean_text($matches[2][$key]); + } + } +} + +/** + * Returns a generic Solr document object for this entity. + * + * This function will do the basic processing for the document that is common + * to all entities, but virtually all entities will need their own additional + * processing. + * + * @param object $entity + * The entity for which we want a document. + * @param string $entity_type + * The type of entity we're processing. + * @return solrsearchDocument + */ +function _solrsearch_index_process_entity_get_document($entity, $entity_type) { + list($entity_id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + $document = new solrsearchDocument(); + + // Define our url options in advance. This differs depending on the + // language + $languages = language_list(); + $url_options = array('absolute' => TRUE); + if (isset($entity->language) && isset($languages[$entity->language])) { + $url_options = $url_options + array('language' => $languages[$entity->language]); + } + + $document->id = solrsearch_document_id($entity_id, $entity_type); + $document->site = url(NULL, $url_options); + $document->hash = solrsearch_site_hash(); + + $document->entity_id = $entity_id; + $document->entity_type = $entity_type; + $document->bundle = $bundle; + $document->bundle_name = entity_bundle_label($entity_type, $bundle); + + if (empty($entity->language)) { + // 'und' is the language-neutral code in Drupal 7. + $document->language = LANGUAGE_NONE; + } + else { + $document->language = $entity->language; + } + + $path = entity_uri($entity_type, $entity); + // A path is not a requirement of an entity + if (!empty($path)) { + $document->path = $path['path']; + $document->url = url($path['path'], $path['options'] + $url_options); + // Path aliases can have important information about the content. + // Add them to the index as well. + if (function_exists('drupal_get_path_alias')) { + // Add any path alias to the index, looking first for language specific + // aliases but using language neutral aliases otherwise. + $output = drupal_get_path_alias($document->path, $document->language); + if ($output && $output != $document->path) { + $document->path_alias = $output; + } + } + } + return $document; +} + +/** + * Returns an array of rows from a query based on an indexing environment. + * @todo Remove the read only because it is not environment specific + * + * @param $env_id + * @param $entity_type + * @param $limit + * + * @return array list of row to index + */ +function solrsearch_index_get_entities_to_index($env_id, $entity_type, $limit) { + $rows = array(); + if (variable_get('solrsearch_read_only', 0)) { + return $rows; + } + $bundles = solrsearch_get_index_bundles($env_id, $entity_type); + if (empty($bundles)) { + return $rows; + } + + $table = solrsearch_get_indexer_table($entity_type); + // Get $last_entity_id and $last_changed. + $last_index_position = solrsearch_get_last_index_position($env_id, $entity_type); + $last_entity_id = $last_index_position['last_entity_id']; + $last_changed = $last_index_position['last_changed']; + + // Find the next batch of entities to index for this entity type. Note that + // for ordering we're grabbing the oldest first and then ordering by ID so + // that we get a definitive order. + // Also note that we fetch ALL fields from the indexer table + $query = db_select($table, 'aie') + ->fields('aie') + ->condition('aie.bundle', $bundles) + ->condition(db_or() + ->condition('aie.changed', $last_changed, '>') + ->condition(db_and() + ->condition('aie.changed', $last_changed, '<=') + ->condition('aie.entity_id', $last_entity_id, '>'))) + ->orderBy('aie.changed', 'ASC') + ->orderBy('aie.entity_id', 'ASC') + ->addTag('solrsearch_index_' . $entity_type); + + if ($table == 'solrsearch_index_entities') { + // Other, entity-specific tables don't need this condition. + $query->condition('aie.entity_type', $entity_type); + } + $query->range(0, $limit); + $records = $query->execute(); + + $status_callbacks = solrsearch_entity_get_callback($entity_type, 'status callback'); + foreach ($records as $record) { + // Check status and status callbacks before sending to the index + 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 + $record->status = $record->status && $status_callback($record->entity_id, $record->entity_type); + } + } + } + $rows[] = $record; + } + return $rows; +} + +/** + * Delete the whole index for an environment. + * + * @param string $env_id + * The machine name of the environment. + * @param string $entity_type + * (optional) specify to remove just this entity_type from the index. + * @param string $bundle + * (optional) also specify a bundle to remove just the bundle from + * the index. + */ +function solrsearch_index_delete_index($env_id, $entity_type = NULL, $bundle = NULL) { + // Instantiate a new Solr object. + try { + $solr = solrsearch_get_solr($env_id); + $query = '*:*'; + + if (!empty($entity_type) && !empty($bundle)) { + $query = "(bundle:$bundle AND entity_type:$entity_type) OR sm_parent_entity_bundle:{$entity_type}-{$bundle}"; + } + elseif (!empty($bundle)) { + $query = "(bundle:$bundle)"; + } + + // Allow other modules to modify the delete query. + // For example, use the site hash so that you only delete this site's + // content: $query = 'hash:' . solrsearch_site_hash() + drupal_alter('solrsearch_delete_by_query', $query); + $solr->deleteByQuery($query); + $solr->commit(); + + if (!empty($entity_type)) { + $rebuild_callback = solrsearch_entity_get_callback($entity_type, 'reindex callback'); + if (is_callable($rebuild_callback)) { + $rebuild_callback($env_id, $bundle); + } + } + else { + solrsearch_index_mark_for_reindex($env_id); + } + + solrsearch_set_last_index_updated($env_id, REQUEST_TIME); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } +} + +/** + * Delete from the index documents with the entity type and any of the excluded bundles. + * + * Also deletes all documents that have the entity type and bundle as a parent. + * + * @param string $env_id + * The machine name of the environment. + * @param string $entity_type + * @param array $excluded_bundles + * + * @return true on success, false on failure. + */ +function solrsearch_index_delete_bundles($env_id, $entity_type, array $excluded_bundles) { + // Remove newly omitted bundles. + try { + $solr = solrsearch_get_solr($env_id); + foreach ($excluded_bundles as $bundle) { + $query = "(bundle:$bundle AND entity_type:$entity_type) OR sm_parent_entity_bundle:{$entity_type}-{$bundle}"; + + // Allow other modules to modify the delete query. + // For example, use the site hash so that you only delete this site's + // content: $query = 'hash:' . solrsearch_site_hash() + drupal_alter('solrsearch_delete_by_query', $query); + $solr->deleteByQuery($query); + } + if ($excluded_bundles) { + $solr->commit(); + } + return TRUE; + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + return FALSE; + } +} + +/** + * Delete an entity from the index. + * + * Also deletes all documents that have the deleted document as a parent. + * + * @param string $env_id + * The machine name of the environment. + * @param string $entity_type + * @param string $entity_id + * + * @return true on success, false on failure. + */ +function solrsearch_index_delete_entity_from_index($env_id, $entity_type, $entity_id) { + static $failed = FALSE; + if ($failed) { + return FALSE; + } + try { + $solr = solrsearch_get_solr($env_id); + $document_id = solrsearch_document_id($entity_id, $entity_type); + $query = "id:\"$document_id\" OR sm_parent_document_id:\"$document_id\""; + $solr->deleteByQuery($query); + solrsearch_set_last_index_updated($env_id, REQUEST_TIME); + return TRUE; + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + // Don't keep trying queries if they are failing. + $failed = TRUE; + return FALSE; + } +} + +/** + * Mark a certain entity type for a specific environment for reindexing. + * + * @param $env_id + * @param null $entity_type + */ +function solrsearch_index_mark_for_reindex($env_id, $entity_type = NULL) { + foreach (entity_get_info() as $type => $entity_info) { + if (($type == $entity_type) || ($entity_type == NULL)) { + if (isset($entity_info['solrsearch']) && ($entity_info['solrsearch']['indexable'])) { + $reindex_callback = solrsearch_entity_get_callback($type, 'reindex callback'); + if (!empty($reindex_callback)) { + call_user_func($reindex_callback, $env_id); + } + } + } + } + solrsearch_clear_last_index_position($env_id, $entity_type); + cache_clear_all('*', 'cache_solrsearch', TRUE); +} + +/** + * Sets what bundles on the specified entity type should be indexed. + * + * @param string $env_id + * The machine name of the environment. + * @param string $entity_type + * The entity type to index. + * @param array $bundles + * The machine names of the bundles to index. + * + * @throws Exception + */ +function solrsearch_index_set_bundles($env_id, $entity_type, array $bundles) { + $transaction = db_transaction(); + try { + db_delete('solrsearch_index_bundles') + ->condition('env_id', $env_id) + ->condition('entity_type', $entity_type) + ->execute(); + + if ($bundles) { + $insert = db_insert('solrsearch_index_bundles') + ->fields(array('env_id', 'entity_type', 'bundle')); + + foreach ($bundles as $bundle) { + $insert->values(array( + 'env_id' => $env_id, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + )); + } + $insert->execute(); + } + } + catch (Exception $e) { + $transaction->rollback(); + // Re-throw the exception so we are aware of the failure. + throw $e; + } +} + +// This really should be in core, but it isn't yet. When it gets added to core, +// we can remove this version. +// @see http://drupal.org/node/969180 +if (!function_exists('entity_bundle_label')) { + +/** + * Returns the label of a bundle. + * + * @param string $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param string $bundle_name + * The bundle for which we want the label from + * + * @return + * A string with the human-readable name of the bundle, or FALSE if not specified. + */ +function entity_bundle_label($entity_type, $bundle_name) { + $labels = &drupal_static(__FUNCTION__, array()); + + if (empty($labels)) { + foreach (entity_get_info() as $type => $info) { + foreach ($info['bundles'] as $bundle => $bundle_info) { + $labels[$type][$bundle] = !empty($bundle_info['label']) ? $bundle_info['label'] : FALSE; + } + } + } + + return $labels[$entity_type][$bundle_name]; +} + +} + +/** + * Builds the node-specific information for a Solr document. + * + * @param solrsearchDocument $document + * The Solr document we are building up. + * @param object $node + * The entity we are indexing. + * @param string $entity_type + * The type of entity we're dealing with. + * @param string $env_id + * The type of entity we're dealing with. + * + * @return array A set of solrsearchDocument documents + */ +function solrsearch_index_node_solr_document(solrsearchDocument $document, $node, $entity_type, $env_id) { + // None of these get added unless they are explicitly in our schema.xml + $document->label = solrsearch_clean_text($node->title); + + // Build the node body. + $build = node_view($node, 'search_index', !empty($node->language) ? $node->language : LANGUAGE_NONE); + // Remove useless html crap out of the render. + unset($build['#theme']); + $text = drupal_render($build); + $document->content = solrsearch_clean_text($text); + + // Adding the teaser + if (isset($node->teaser)) { + $document->teaser = solrsearch_clean_text($node->teaser); + } + else { + $document->teaser = truncate_utf8($document->content, 300, TRUE); + } + + // Path aliases can have important information about the content. + // Add them to the index as well. + if (function_exists('drupal_get_path_alias')) { + // Add any path alias to the index, looking first for language specific + // aliases but using language neutral aliases otherwise. + $language = empty($node->language) ? NULL : $node->language; + $path = 'node/' . $node->nid; + $output = drupal_get_path_alias($path, $language); + if ($output && $output != $path) { + $document->path_alias = $output; + } + } + + // Author information + $document->ss_name = $node->name; + // We want the name to be searchable for keywords. + $document->tos_name = $node->name; + + // Index formatted username so it can be searched and sorted on. + $account = (object) array('uid' => $node->uid, 'name' => $node->name); + $username = format_username($account); + $document->ss_name_formatted = $username; + $document->tos_name_formatted = $username; + $document->is_uid = $node->uid; + $document->bs_status = $node->status; + $document->bs_sticky = $node->sticky; + $document->bs_promote = $node->promote; + $document->is_tnid = $node->tnid; + $document->bs_translate = $node->translate; + + // Language specific checks + if (empty($node->language)) { + // 'und' is the language-neutral code in Drupal 7. + $document->ss_language = LANGUAGE_NONE; + } + else { + $document->ss_language = $node->language; + } + + // Timestamp of the node + $document->ds_created = solrsearch_date_iso($node->created); + $document->ds_changed = solrsearch_date_iso($node->changed); + + // Comment counts + time + if (isset($node->last_comment_timestamp) && !empty($node->comment_count)) { + $document->ds_last_comment_timestamp = solrsearch_date_iso($node->last_comment_timestamp); + $document->ds_last_comment_or_change = solrsearch_date_iso(max($node->last_comment_timestamp, $node->changed)); + $document->is_comment_count = $node->comment_count; + } + else { + $document->ds_last_comment_or_change = solrsearch_date_iso($node->changed); + } + + // Fetch extra data normally not visible, including comments. + // We do this manually (with module_implements instead of node_invoke_nodeapi) + // because we want a keyed array to come back. Only in this way can we decide + // whether to index comments or not. + $extra = array(); + $excludes = variable_get('solrsearch_exclude_nodeapi_types', array()); + $exclude_nodeapi = isset($excludes[$node->type]) ? $excludes[$node->type] : array(); + + foreach (module_implements('node_update_index') as $module) { + // Invoke nodeapi if this module has not been excluded, for example, + // exclude 'comment' for a type to skip indexing its comments. + if (empty($exclude_nodeapi[$module])) { + $function = $module . '_node_update_index'; + if ($output = $function($node)) { + $extra[$module] = $output; + } + } + } + + // Adding the text of the comments + if (isset($extra['comment'])) { + $comments = $extra['comment']; + // Remove comments from the extra fields + unset($extra['comment']); + $document->ts_comments = solrsearch_clean_text($comments); + // @todo: do we want to reproduce solrsearch_add_tags_to_document() for comments? + } + // If there are other extra fields, add them to the document + if (!empty($extra)) { + // Use an omit-norms text field since this is generally going to be short; not + // really a full-text field. + $document->tos_content_extra = solrsearch_clean_text(implode(' ', $extra)); + } + + // Generic use case for future reference. Callbacks can + // allow you to send back multiple documents + $documents = array(); + $documents[] = $document; + return $documents; +} + +/** + * Function that will be executed if the node bundles were updated. + * Currently it does nothing, but it could potentially do something later on. + * + * @param $env_id + * @param $existing_bundles + * @param $new_bundles + */ +function solrsearch_index_node_bundles_changed($env_id, $existing_bundles, $new_bundles) { + // Nothing to do for now. +} + +/** + * Reindexing callback for solrsearch, for nodes. + * + * @param string $env_id + * The machine name of the environment. + * @param string|null $bundle + * (optional) The bundle type to reindex. If not used + * all bundles will be re-indexed. + * + * @return null + * returns NULL if the specified bundle is not in the indexable bundles list + * + * @throws Exception + */ +function solrsearch_index_node_solr_reindex($env_id, $bundle = NULL) { + $indexer_table = solrsearch_get_indexer_table('node'); + $transaction = db_transaction(); + try { + $indexable_bundles = solrsearch_get_index_bundles($env_id, 'node'); + + if ($bundle && !empty($indexable_bundles) && !in_array($bundle, $indexable_bundles)) { + // The bundle specified is not in the indexable bundles list. + return NULL; + } + + // Leave status 0 rows - those need to be + // removed from the index later. + $delete = db_delete($indexer_table); + $delete->condition('status', 1); + + if (!empty($bundle)) { + $delete->condition('bundle', $bundle); + } + elseif (!empty($indexable_bundles)) { + $delete->condition('bundle', $indexable_bundles, 'IN'); + } + + $delete->execute(); + + $select = db_select('node', 'n'); + $select->condition('status', 1); + $select->addExpression("'node'", 'entity_type'); + $select->addField('n', 'nid', 'entity_id'); + $select->addField('n', 'type', 'bundle'); + $select->addField('n', 'status', 'status'); + $select->addExpression(REQUEST_TIME, 'changed'); + + if ($bundle) { + // Mark all nodes of the specified content type for reindexing. + $select->condition('n.type', $bundle); + } + elseif (!empty($indexable_bundles)) { + // Restrict reindex to content types in the indexable bundles list. + $select->condition('n.type', $indexable_bundles, 'IN'); + } + + $insert = db_insert($indexer_table) + ->fields(array('entity_id', 'bundle', 'status', 'entity_type', 'changed')) + ->from($select) + ->execute(); + } + catch (Exception $e) { + $transaction->rollback(); + throw $e; + } +} + +/** + * Status callback for solrsearch, for nodes. + * after indexing a certain amount of nodes + * + * @param $entity_id + * @param $entity_type + * + * @return int + * The status of the node + */ +function solrsearch_index_node_status_callback($entity_id, $entity_type) { + // Make sure we have a boolean value. + // Anything different from 1 becomes zero + $entity = entity_load($entity_type, array($entity_id)); + $entity = $entity ? reset($entity) : FALSE; + + if (empty($entity)) { + // If the object failed to load, just stop. + return FALSE; + } + $status = ($entity->status == 1 ? 1 : 0); + return $status; +} + +/** + * Callback that converts term_reference field into an array + * + * @param object $node + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + * fields that will be indexed for this term reference + */ +function solrsearch_term_reference_indexing_callback($node, $field_name, $index_key, array $field_info) { + // Keep ancestors cached + $ancestors = &drupal_static(__FUNCTION__, array()); + + $fields = array(); + $vocab_names = array(); + if (!empty($node->{$field_name}) && function_exists('taxonomy_get_parents_all')) { + $field = $node->$field_name; + list($lang, $items) = each($field); + foreach ($items as $item) { + // Triple indexing of tids lets us do efficient searches (on tid) + // and do accurate per field or per-vocabulary faceting. + + // By including the ancestors to a term in the index we make + // sure that searches for general categories match specific + // categories, e.g. Fruit -> apple, a search for fruit will find + // content categorized with apple. + if (!isset($ancestors[$item['tid']])) { + $ancestors[$item['tid']] = taxonomy_get_parents_all($item['tid']); + } + foreach ($ancestors[$item['tid']] as $ancestor) { + // Index parent term against the field. Note that this happens + // regardless of whether the facet is set to show as a hierarchy or not. + // We would need a separate field if we were to index terms without any + // hierarchy at all. + // If the term is singular, then we cannot add another value to the + // document as the field is single + if ($field_info['multiple'] == true) { + $fields[] = array( + 'key' => $index_key, + 'value' => $ancestor->tid, + ); + } + $fields[] = array( + 'key' => 'tid', + 'value' => $ancestor->tid, + ); + $fields[] = array( + 'key' => 'im_vid_' . $ancestor->vid, + 'value' => $ancestor->tid, + ); + $name = solrsearch_clean_text($ancestor->name); + $vocab_names[$ancestor->vid][] = $name; + // We index each name as a string for cross-site faceting + // using the vocab name rather than vid in field construction . + $fields[] = array( + 'key' => 'sm_vid_' . solrsearch_vocab_name($ancestor->vid), + 'value' => $name, + ); + } + } + // Index the term names into a text field for MLT queries and keyword searching. + foreach ($vocab_names as $vid => $names) { + $fields[] = array( + 'key' => 'tm_vid_' . $vid . '_names', + 'value' => implode(' ', $names), + ); + } + } + return $fields; +} + +/** + * Helper function - return a safe (PHP identifier) vocabulary name. + * + * @param integer $vid + * @return string + */ +function solrsearch_vocab_name($vid) { + $names = &drupal_static(__FUNCTION__, array()); + + if (!isset($names[$vid])) { + $vocab_name = db_query('SELECT v.name FROM {taxonomy_vocabulary} v WHERE v.vid = :vid', array(':vid' => $vid))->fetchField(); + $names[$vid] = preg_replace('/[^a-zA-Z0-9_\x7f-\xff]/', '_', $vocab_name); + // Fallback for names ending up all as '_'. + $check = rtrim($names[$vid], '_'); + if (!$check) { + $names[$vid] = '_' . $vid . '_'; + } + } + return $names[$vid]; +} + +/** + * Callback that converts list module field into an array + * For every multivalued value we also add a single value to be able to + * use the stats + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + */ +function solrsearch_fields_default_indexing_callback($entity, $field_name, $index_key, array $field_info) { + $fields = array(); + $numeric = TRUE; + if (!empty($entity->{$field_name})) { + $field = $entity->$field_name; + list($lang, $values) = each($field); + switch ($field_info['index_type']) { + case 'integer': + case 'half-int': + case 'sint': + case 'tint': + case 'thalf-int': + case 'boolean': + $function = 'intval'; + break; + case 'float': + case 'double': + case 'sfloat': + case 'sdouble': + case 'tfloat': + case 'tdouble': + $function = 'solrsearch_floatval'; + break; + default: + $numeric = FALSE; + $function = 'solrsearch_clean_text'; + } + for ($i = 0; $i < count($values); $i++) { + $fields[] = array( + 'key' => $index_key, + 'value' => $function($values[$i]['value']), + ); + } + // Also store the first value of the field in a singular index for multi value fields + if ($field_info['multiple'] && $numeric && !empty($values[0])) { + $singular_field_info = $field_info; + $singular_field_info['multiple'] = FALSE; + $single_key = solrsearch_index_key($singular_field_info); + $fields[] = array( + 'key' => $single_key, + 'value' => $function($values[0]['value']), + ); + } + } + return $fields; +} + +/** + * This function is used during indexing to normalize the DATE and DATETIME + * fields into the appropriate format for Apache Solr. + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + */ +function solrsearch_date_default_indexing_callback($entity, $field_name, $index_key, array $field_info) { + $fields = array(); + if (!empty($entity->{$field_name})) { + $field = $entity->$field_name; + list($lang, $values) = each($field); + // Construct a Solr-ready date string in UTC time zone based on the field's date string and time zone. + $tz = new DateTimeZone(isset($field['timezone']) ? $field['timezone'] : 'UTC'); + + // $fields may end up having two values; one for the start date + // and one for the end date. + foreach ($values as $value) { + if ($date = date_create($value['value'], $tz)) { + $index_value = solrsearch_date_iso($date->format('U')); + $fields[] = array( + 'key' => $index_key, + 'value' => $index_value, + ); + } + + if (isset($value['value2'])) { + if ($date = date_create($value['value2'], $tz)) { + $index_value = solrsearch_date_iso($date->format('U')); + $fields[] = array( + // The value2 element is the end date. Therefore it gets indexed + // into its own Solr field. + 'key' => $index_key . '_end', + 'value' => $index_value, + ); + } + } + } + } + return $fields; +} + +/** + * This function is used during indexing to normalize the DATESTAMP fields + * into the appropriate format for Apache Solr. + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + */ +function solrsearch_datestamp_default_indexing_callback($entity, $field_name, $index_key, array $field_info) { + $fields = array(); + if (!empty($entity->{$field_name})) { + // $fields may end up having two values; one for the start date + // and one for the end date. + $field = $entity->$field_name; + list($lang, $values) = each($field); + + foreach ($values as $value) { + if (isset($value['value']) && $value['value'] != 0) { + $index_value = solrsearch_date_iso($value['value']); + $fields[] = array( + 'key' => $index_key, + 'value' => $index_value, + ); + } + if (isset($value['value2']) && $value['value'] != 0) { + $index_value = solrsearch_date_iso($value['value2']); + $fields[] = array( + // The value2 element is the end date. Therefore it gets indexed + // into its own Solr field. + 'key' => $index_key . '_end', + 'value' => $index_value, + ); + } + } + } + return $fields; +} + +function solrsearch_floatval($value) { + return sprintf('%0.20f', $value); +} + +/** + * Indexing callback for the node_reference module + * by the references module + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + */ +function solrsearch_nodereference_indexing_callback($entity, $field_name, $index_key, array $field_info) { + $fields = array(); + if (!empty($entity->{$field_name})) { + $index_key = solrsearch_index_key($field_info); + foreach ($entity->$field_name as $field_references) { + foreach ($field_references as $reference) { + if ($index_value = (!empty($reference['nid'])) ? $reference['nid'] : FALSE) { + $fields[] = array( + 'key' => $index_key, + 'value' => $index_value, + ); + } + } + } + } + return $fields; +} + +/** + * Indexing callback for the user_reference module + * by the references module + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + */ +function solrsearch_userreference_indexing_callback($entity, $field_name, $index_key, array $field_info) { + $fields = array(); + if (!empty($entity->$field_name)) { + $index_key = solrsearch_index_key($field_info); + foreach ($entity->$field_name as $field_references) { + foreach ($field_references as $reference) { + if ($index_value = (isset($reference['uid']) && strlen($reference['uid'])) ? $reference['uid'] : FALSE) { + $fields[] = array( + 'key' => $index_key, + 'value' => $index_value, + ); + } + } + } + } + return $fields; +} + +/** + * Indexing callback for entityreference fields. + * + * @param object $entity + * @param string $field_name + * @param string $index_key + * @param array $field_info + * @return array $fields + * + */ +function solrsearch_entityreference_indexing_callback($entity, $field_name, $index_key, $field_info) { + $fields = array(); + if (!empty($entity->{$field_name})) { + + // Gets entity type and index key. We need to prefix the ID with the entity + // type so we know what entity we are dealing with in the mapping callback. + $entity_type = $field_info['field']['settings']['target_type']; + $index_key = solrsearch_index_key($field_info); + + // Iterates over all references and adds them to the fields. + foreach ($entity->$field_name as $entity_references) { + foreach ($entity_references as $reference) { + if ($id = (!empty($reference['target_id'])) ? $reference['target_id'] : FALSE) { + $fields[] = array( + 'key' => $index_key, + 'value' => $entity_type . ':' . $id, + ); + } + } + } + } + return $fields; +} + +/** + * Extract HTML tag contents from $text and add to boost fields. + * + * $text must be stripped of control characters before hand. + * + * @param solrsearchDocument $document + * @param type $text + */ +function solrsearch_add_tags_to_document(solrsearchDocument $document, $text) { + $tags_to_index = variable_get('solrsearch_tags_to_index', array( + 'h1' => 'tags_h1', + 'h2' => 'tags_h2_h3', + 'h3' => 'tags_h2_h3', + 'h4' => 'tags_h4_h5_h6', + 'h5' => 'tags_h4_h5_h6', + 'h6' => 'tags_h4_h5_h6', + 'u' => 'tags_inline', + 'b' => 'tags_inline', + 'i' => 'tags_inline', + 'strong' => 'tags_inline', + 'em' => 'tags_inline', + 'a' => 'tags_a' + )); + + // Strip off all ignored tags. + $text = strip_tags($text, '<' . implode('><', array_keys($tags_to_index)) . '>'); + + preg_match_all('@<(' . implode('|', array_keys($tags_to_index)) . ')[^>]*>(.*)@Ui', $text, $matches); + foreach ($matches[1] as $key => $tag) { + $tag = strtolower($tag); + // We don't want to index links auto-generated by the url filter. + if ($tag != 'a' || !preg_match('@(?:http://|https://|ftp://|mailto:|smb://|afp://|file://|gopher://|news://|ssl://|sslv2://|sslv3://|tls://|tcp://|udp://|www\.)[a-zA-Z0-9]+@', $matches[2][$key])) { + if (!isset($document->{$tags_to_index[$tag]})) { + $document->{$tags_to_index[$tag]} = ''; + } + $document->{$tags_to_index[$tag]} .= ' ' . solrsearch_clean_text($matches[2][$key]); + } + } +} + +/** + * hook_cron() helper to try to make the index table consistent with their + * respective entity table. + */ +function solrsearch_index_node_check_table() { + // Check for unpublished content that wasn't deleted from the index. + $table = solrsearch_get_indexer_table('node'); + // We do not check more nodes than double the cron limit per time + // Update or delete at most this many in each Solr query. + $limit = variable_get('solrsearch_cron_mass_limit', 500); + $query = db_select($table, 'aien') + ->fields('n', array('nid', 'status')) + ->where('aien.status <> n.status') + ->range(0, ($limit * 2)) + ->addTag('solrsearch_index_node'); + $query->innerJoin('node', 'n', 'n.nid = aien.entity_id'); + $nodes = $query->execute()->fetchAllAssoc('nid'); + + $node_lists = array_chunk($nodes, $limit, TRUE); + foreach ($node_lists as $nodes) { + watchdog('Apache Solr', 'On cron running solrsearch_nodeapi_mass_update() on nids @nids', array('@nids' => implode(',', array_keys($nodes))), WATCHDOG_NOTICE); + if (!solrsearch_index_nodeapi_mass_update($nodes, $table)) { + // Solr query failed - so stop trying. + break; + } + } + + // Check for deleted content that wasn't deleted from the index. + $query = db_select($table, 'aien') + ->isNull('n.nid') + ->range(0, ($limit*2)); + $query->addExpression('aien.entity_id', 'nid'); + $query->leftJoin('node', 'n', 'n.nid = aien.entity_id'); + $nodes = $query->execute()->fetchAllAssoc('nid'); + $node_lists = array_chunk($nodes, $limit, TRUE); + + foreach ($node_lists as $nodes) { + watchdog('Apache Solr', 'On cron running solrsearch_nodeapi_mass_delete() on nids @nids', array('@nids' => implode(',', array_keys($nodes))), WATCHDOG_NOTICE); + if (!solrsearch_index_nodeapi_mass_delete($nodes, $table)) { + // Solr query failed - so stop trying. + break; + } + } +} + +/** + * Mass Update nodes from the solr indexer table + * + * @param array $nodes + * @param string $table + * @return boolean + * true if we mass updated, false if failed + */ +function solrsearch_index_nodeapi_mass_update(array $nodes, $table = NULL) { + if (empty($nodes)) { + return TRUE; + } + if (empty($table)) { + $table = solrsearch_get_indexer_table('node'); + } + + if (solrsearch_environment_variable_get(solrsearch_default_environment(), 'solrsearch_read_only', solrsearch_READ_WRITE) == solrsearch_READ_ONLY) { + return TRUE; + } + + $published_ids = array(); + $unpublished_ids = array(); + foreach ($nodes as $node) { + if ($node->status) { + $published_ids[$node->nid] = solrsearch_document_id($node->nid); + } + else { + $unpublished_ids[$node->nid] = solrsearch_document_id($node->nid); + } + } + try { + $env_id = solrsearch_default_environment(); + $solr = solrsearch_get_solr($env_id); + $solr->deleteByMultipleIds($unpublished_ids); + solrsearch_set_last_index_updated($env_id, REQUEST_TIME); + + // There was no exception, so update the table. + if ($published_ids) { + db_update($table) + ->fields(array('changed' => REQUEST_TIME, 'status' => 1)) + ->condition('entity_id', array_keys($published_ids), 'IN') + ->execute(); + } + if ($unpublished_ids) { + db_update($table) + ->fields(array('changed' => REQUEST_TIME, 'status' => 0)) + ->condition('entity_id', array_keys($unpublished_ids), 'IN') + ->execute(); + } + return TRUE; + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + return FALSE; + } +} + +/** + * Mass delete nodes from the solr indexer tables. + * + * @param array $nodes + * @param string $table + * @return boolean + * true if we mass updated, false if failed + */ +function solrsearch_index_nodeapi_mass_delete(array $nodes, $table = NULL) { + if (empty($nodes)) { + return TRUE; + } + if (empty($table)) { + $table = solrsearch_get_indexer_table('node'); + } + + if (solrsearch_environment_variable_get(solrsearch_default_environment(), 'solrsearch_read_only', solrsearch_READ_WRITE) == solrsearch_READ_ONLY) { + return TRUE; + } + + $ids = array(); + $nids = array(); + foreach ($nodes as $node) { + $ids[] = solrsearch_document_id($node->nid); + $nids[] = $node->nid; + } + try { + $env_id = solrsearch_default_environment(); + $solr = solrsearch_get_solr($env_id); + $solr->deleteByMultipleIds($ids); + solrsearch_set_last_index_updated($env_id, REQUEST_TIME); + // There was no exception, so update the table. + db_delete($table) + ->condition('entity_id', $nids, 'IN') + ->execute(); + return TRUE; + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + return FALSE; + } +} diff -r 000000000000 -r a2b4f67e73dc solrsearch.info --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.info Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,25 @@ +name = solrsearch +description = Frontend for searching Solr +core = 7.x +version = 0.5 +stylesheets[all][] = solrsearch.css +package = SolrSearch +dependencies[] = digitalobjects + + + +configure = admin/config/search/searchsolr/settings +files[] = solrsearch.install +files[] = solrsearch.module +files[] = solrsearch.admin.inc +files[] = solrsearch.interface.inc +files[] = Drupal_Apache_Solr_Service.php +files[] = Apache_Solr_Document.php +files[] = Solr_Base_Query.php +files[] = plugins/facetapi/adapter.inc +files[] = plugins/facetapi/query_type_date.inc +files[] = plugins/facetapi/query_type_term.inc +files[] = plugins/facetapi/query_type_numeric_range.inc +files[] = plugins/facetapi/query_type_integer.inc +files[] = plugins/facetapi/query_type_geo.inc + diff -r 000000000000 -r a2b4f67e73dc solrsearch.install --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.install Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,899 @@ + $t('Solr search'), + 'value' => $t('Missing environment configuration'), + 'description' => $t('Missing or invalid Solr environment record for the default environment ID %id.', array('%id' => $id)), + 'severity' => REQUIREMENT_ERROR, + ); + } + else { + $has_settings = TRUE; + } + + if ($has_settings) { + $ping = FALSE; + try { + $solr = solrsearch_get_solr($id); + $ping = @$solr->ping(variable_get('solrsearch_ping_timeout', 4)); + // If there is no $solr object, there is no instance available, so don't continue. + if (!$ping) { + throw new Exception(t('No Solr instance available when checking requirements.')); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + $value = $ping ? $t('Your site has contacted the Apache Solr server.') : $t('Your site was unable to contact the Apache Solr server.'); + $severity = $ping ? REQUIREMENT_OK : REQUIREMENT_ERROR; + $requirements['solrsearch'] = array( + 'title' => $t('Apache Solr'), + 'value' => $value, + 'description' => $t('Default environment url:
%url', array('%url' => $environment['url'])), + 'severity' => $severity, + ); + } + + return $requirements; +} + +/** + * Implements hook_install(). + */ +function solrsearch_install() { + module_load_include('inc', 'solrsearch', 'solrsearch_search.admin'); + /*module_load_include('inc', 'solrsearch', 'solrsearch.index');*/ + // Create one MLT block. + solrsearch_search_mlt_save_block(array('name' => st('More like this'))); + db_insert('solrsearch_environment')->fields(array('env_id' => 'echosearch', 'name' => 'localhost server', 'url' => 'http://localhost:8983/solr'))->execute(); + + // Initialize the entities to index. We enable all node types by default + $info = entity_get_info('node'); + $bundles = array_keys($info['bundles']); + $env_id = solrsearch_default_environment(); + /*solrsearch_index_set_bundles($env_id, 'node', $bundles);*/ + + drupal_set_message(st('Apache Solr is enabled. Visit the settings page.', array('@settings_link' => url('admin/config/search/solrsearch')))); +} + +/** + * Implements hook_enable(). + */ +function solrsearch_enable() { + // Completely build the index table. + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $env_id = solrsearch_default_environment(); + /*solrsearch_index_mark_for_reindex($env_id);*/ +} + +/** + * Implements hook_schema(). + */ +function solrsearch_schema() { + + $table = drupal_get_schema_unprocessed('system', 'cache'); + $table['description'] = 'Cache table for solrsearch to store Luke data and indexing information.'; + $schema['cache_solrsearch'] = $table; + + $schema['solrsearch_environment'] = array( + 'description' => 'The Solr search environment table.', + // Enable CTools exportables based on this table. + 'export' => array( + // Environment machine name. + 'key' => 'env_id', + // Description of key. + 'key name' => 'Environment machine name', + // Apache Solr doesn't allow disabling environments. + 'can disable' => FALSE, + // Variable name to use in exported code. + 'identifier' => 'environment', + // Thin wrapper for the environment save callback. + 'save callback' => 'solrsearch_ctools_environment_save', + // Thin wrapper for the environment delete callback. + 'delete callback' => 'solrsearch_ctools_environment_delete', + // Includes the environment variables in 'conf' as well as the fields in this table. + 'export callback' => 'solrsearch_ctools_environment_export', + // Use the same hook as the API name below. + 'default hook' => 'solrsearch_environments', + // CTools API implementation. + 'api' => array( + 'owner' => 'solrsearch', + 'api' => 'solrsearch_environments', + 'minimum_version' => 1, + 'current_version' => 1, + ), + ), + 'fields' => array( + 'env_id' => array( + 'description' => 'Unique identifier for the environment', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'Human-readable name for the server', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + 'url' => array( + 'description' => 'Full url for the server', + 'type' => 'varchar', + 'length' => 1000, + 'not null' => TRUE, + ), + 'service_class' => array( + 'description' => 'Optional class name to use for connection', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + ), + 'primary key' => array('env_id'), + ); + $schema['solrsearch_environment_variable'] = array( + 'description' => 'Variable values for each Solr search environment.', + 'fields' => array( + 'env_id' => array( + 'description' => 'Unique identifier for the environment', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The name of the variable.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value of the variable.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + ), + 'primary key' => array('env_id', 'name'), + ); + + // Technically the entity system does not require an integer ID. + // However, documentation mentions : + // id: The name of the property that contains the primary id of the + // entity. Every entity object passed to the Field API must have this + // property and its value must be numeric. + + //Predefine an amount of types that get their own table + $types = array( + 'other' => 'solrsearch_index_entities', + 'node' => 'solrsearch_index_entities_node', + ); + foreach ($types as $type => $table) { + $schema[$table] = array( + 'description' => 'Stores a record of when an entity changed to determine if it needs indexing by Solr.', + 'fields' => array( + 'entity_type' => array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'entity_id' => array( + 'description' => 'The primary identifier for an entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'bundle' => array( + 'description' => 'The bundle to which this entity belongs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + 'status' => array( + 'description' => 'Boolean indicating whether the entity should be in the index.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'changed' => array( + 'description' => 'The Unix timestamp when an entity was changed.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'bundle_changed' => array('bundle', 'changed'), + ), + 'primary key' => array('entity_id'), + ); + if ($type == 'other') { + // Need the entity type also in the pkey for multiple entities in one table. + $schema[$table]['primary key'][] = 'entity_type'; + } + } + + $schema['solrsearch_index_bundles'] = array( + 'description' => 'Records what bundles we should be indexing for a given environment.', + 'fields' => array( + 'env_id' => array( + 'description' => 'The name of the environment.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'entity_type' => array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'bundle' => array( + 'description' => 'The bundle to index.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + ), + 'primary key' => array('env_id', 'entity_type', 'bundle'), + ); + return $schema; +} + +/** + * Implements hook_uninstall(). + */ +function solrsearch_uninstall() { + // Remove variables. + variable_del('solrsearch_default_environment'); + variable_del('solrsearch_rows'); + variable_del('solrsearch_site_hash'); + variable_del('solrsearch_index_last'); + variable_del('solrsearch_search_mlt_blocks'); + variable_del('solrsearch_cron_limit'); + variable_del('solrsearch_exclude_nodeapi_types'); + variable_del('solrsearch_failure'); + variable_del('solrsearch_index_updated'); + variable_del('solrsearch_read_only'); + variable_del('solrsearch_set_nodeapi_messages'); + variable_del('solrsearch_last_optimize'); + variable_del('solrsearch_update_from_6303'); + // Remove blocks. + db_delete('block')->condition('module', 'solrsearch')->execute(); +} + +/** + * Add a table to track Solr servers. + */ +function solrsearch_update_7000() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + $schema['solrsearch_server'] = array( + 'description' => 'The Solr server table.', + 'fields' => array( + 'server_id' => array( + 'description' => 'Unique identifier for the server', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'Human-readable name for the server', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + 'scheme' => array( + 'description' => 'Preferred scheme for the registered server', + 'type' => 'varchar', + 'length' => 10, + 'not null' => TRUE, + 'default' => 'http' + ), + 'host' => array( + 'description' => 'Host name for the registered server', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + 'port' => array( + 'description' => 'Port number for the registered server', + 'type' => 'int', + 'not null' => TRUE, + ), + 'path' => array( + 'description' => 'Path to the registered server', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + 'service_class' => array( + 'description' => 'Optional class name to use for connection', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '' + ), + ), + 'primary key' => array('server_id'), + ); + db_create_table('solrsearch_server', $schema['solrsearch_server']); + // Insert into the table the current single server record. + $host = variable_get('solrsearch_host', 'localhost'); + $port = variable_get('solrsearch_port', '8983'); + $path = variable_get('solrsearch_path', '/solr'); + db_insert('solrsearch_server')->fields(array('server_id' => 'solr', 'name' => 'Apache Solr server', 'host' => $host, 'port' => $port, 'path' => $path))->execute(); + variable_set('solrsearch_default_server', 'solr'); + variable_del('solrsearch_host'); + variable_del('solrsearch_port'); + variable_del('solrsearch_path'); + $value = variable_get('solrsearch_service_class', NULL); + if (is_array($value)) { + list($module, $filepath, $class) = $value; + variable_set('solrsearch_service_class', $class); + } + variable_del('solrsearch_logging'); +} + + +/** + * Re-jigger the schema to use fewer, shorter keys. + */ +function solrsearch_update_7001() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + if (db_field_exists('solrsearch_server', 'asid')) { + // You installed the beta1 and need to be fixed up. + db_drop_field('solrsearch_server', 'asid'); + db_drop_unique_key('solrsearch_server', 'server_id'); + db_add_primary_key('solrsearch_server', array('server_id')); + db_drop_unique_key('solrsearch_server', 'host_post_path'); + db_change_field('solrsearch_server', 'port', 'port', + array( + 'description' => 'Port number for the registered server', + 'type' => 'int', + 'not null' => TRUE, + ) + ); + } +} + +/** + * Create the per-server variable table. + */ +function solrsearch_update_7002() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + $schema['solrsearch_server_variable'] = array( + 'description' => 'Variable values for each Solr server.', + 'fields' => array( + 'server_id' => array( + 'description' => 'Unique identifier for the server', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The name of the variable.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value of the variable.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + ), + 'primary key' => array('server_id', 'name'), + ); + db_create_table('solrsearch_server_variable', $schema['solrsearch_server_variable']); + $server_id = variable_get('solrsearch_default_server', 'solr'); + // Variables to be migrated: + $conf['solrsearch_enabled_facets'] = variable_get('solrsearch_enabled_facets', NULL); + $conf['solrsearch_search_query_fields'] = variable_get('solrsearch_search_query_fields', NULL); + $conf['solrsearch_search_type_boosts'] = variable_get('solrsearch_search_type_boosts', NULL); + $conf['solrsearch_search_comment_boost'] = variable_get('solrsearch_search_comment_boost', NULL); + $conf['solrsearch_search_changed_boost'] = variable_get('solrsearch_search_changed_boost', NULL); + $conf['solrsearch_search_sticky_boost'] = variable_get('solrsearch_search_sticky_boost', NULL); + $conf['solrsearch_search_promote_boost'] = variable_get('solrsearch_search_promote_boost', NULL); + $conf['solrsearch_search_excluded_types'] = variable_get('solrsearch_search_excluded_types', NULL); + foreach ($conf as $name => $value) { + if ($value !== NULL) { + db_merge('solrsearch_server_variable') + ->key(array('server_id' => $server_id, 'name' => $name)) + ->fields(array('value' => serialize($value))) + ->execute(); + } + variable_del($name); + } +} + +/** + * Move excluded comment types into a new variable. + */ +function solrsearch_update_7003() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + // Same as solrsearch_update_6006() + $exclude_comment_types = variable_get('solrsearch_exclude_comments_types', NULL); + if (is_array($exclude_comment_types)) { + $exclude = array(); + foreach ($exclude_comment_types as $type) { + $exclude[$type]['comment'] = TRUE; + } + variable_set('solrsearch_exclude_nodeapi_types', $exclude); + } + variable_del('solrsearch_exclude_comments_types'); +} + +/** + * Update solrsearch_failure variable. + */ +function solrsearch_update_7004() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + $failure = variable_get('solrsearch_failure', NULL); + switch ($failure) { + case 'show_error': + variable_set('solrsearch_failure', 'solrsearch:show_error'); + break; + case 'show_drupal_results': + variable_set('solrsearch_failure', 'node'); + break; + case 'show_no_results': + variable_set('solrsearch_failure', 'solrsearch:show_no_results'); + break; + } +} + +/** + * Re-jigger the schema to use just a url column. + */ +function solrsearch_update_7005() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + if (db_field_exists('solrsearch_server', 'port')) { + // You installed the beta3 and need to be fixed up. + $servers = db_query('SELECT * FROM {solrsearch_server}')->fetchAllAssoc('server_id', PDO::FETCH_ASSOC); + db_drop_field('solrsearch_server', 'scheme'); + db_drop_field('solrsearch_server', 'port'); + db_drop_field('solrsearch_server', 'path'); + db_change_field('solrsearch_server', 'host', 'url', + array( + 'description' => 'Full url for the server', + 'type' => 'varchar', + 'length' => 1000, + 'not null' => TRUE, + ) + ); + foreach ($servers as $id => $server) { + $port = $server['port'] ? ':' . $server['port'] : ''; + $url = $server['scheme'] . '://' . $server['host'] . $port . $server['path']; + db_update('solrsearch_server') + ->fields(array('url' => $url)) + ->condition('server_id', $id) + ->execute(); + } + } +} + +/** + * Remove facet-related variable deprecated by the Facet API integration. + */ +function solrsearch_update_7006() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + variable_del('solrsearch_facetstyle'); + variable_del('solrsearch_facet_show_children'); + variable_del('solrsearch_facet_query_limits'); + variable_del('solrsearch_facet_query_limit_default'); +} + +/** + * Rename tables to make them more generic. + */ +function solrsearch_update_7007() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + db_drop_primary_key('solrsearch_server'); + db_drop_primary_key('solrsearch_server_variable'); + db_rename_table('solrsearch_server', 'solrsearch_environment'); + db_rename_table('solrsearch_server_variable', 'solrsearch_environment_variable'); + db_change_field('solrsearch_environment', 'server_id', 'env_id', array( + 'description' => 'Unique identifier for the environment', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE) + ); + db_change_field('solrsearch_environment_variable', 'server_id', 'env_id', array( + 'description' => 'Unique identifier for the environment', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE) + ); + db_add_primary_key('solrsearch_environment', array('env_id')); + db_add_primary_key('solrsearch_environment_variable', array('env_id', 'name')); + $id = variable_get('solrsearch_default_server', NULL); + if (isset($id)) { + variable_set('solrsearch_default_environment', $id); + } + variable_del('solrsearch_default_server'); +} + +/** + * Remove more facet-related variable deprecated by the Facet API integration. + */ +function solrsearch_update_7008() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + variable_del('solrsearch_facet_missing'); + variable_del('solrsearch_facet_query_initial_limits'); + variable_del('solrsearch_facet_query_sorts'); + variable_del('solrsearch_facet_sort_active'); + variable_del('solrsearch_operator'); +} + +/** + * Update Facet API block deltas to account for removal of numeric ID from field names. + */ +function solrsearch_update_7009() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + // Only run when facetapi is available and/or installed + if (module_exists('facetapi')) { + module_load_include('inc', 'facetapi', 'facetapi.block'); + // Get all searchers + $searchers = facetapi_get_searcher_info(); + $realms = facetapi_get_realm_info(); + foreach ($searchers as $searcher_id => $searcher) { + foreach ($realms as $realm_id => $realm) { + foreach (field_info_fields() as $field_name => $field) { + // Generate the old delta + $facet_name_old = $field['id'] . '_' . $field['field_name']; + $delta_old = facetapi_build_delta($searcher['name'], $realm['name'], $facet_name_old); + $delta_old = substr(drupal_hash_base64($delta_old), 0, 32); + // Generate the new delta + $facet_name = $field['field_name']; + $delta = facetapi_build_delta($searcher['name'], $realm['name'], $facet_name); + $delta = substr(drupal_hash_base64($delta), 0, 32); + db_update('block') + ->fields(array('delta' => $delta)) + ->condition('module', 'facetapi') + ->condition('delta', $delta_old) + ->execute(); + } + } + } + } +} + +/** + * Update cache table schema for Drupal 7. + */ +function solrsearch_update_7010() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + db_drop_field('cache_solrsearch', 'headers'); + return 'Updated cache table schema for Drupal 7.'; +} + +/** + * Change the namespace for the indexer from solrsearch_search to solrsearch + */ +function solrsearch_update_7011() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + $stored = variable_get('solrsearch_index_last', array()); + if (isset($stored['solrsearch_search'])) { + $stored['solrsearch'] = $stored['solrsearch_search']; + unset($stored['solrsearch_search']); + variable_set('solrsearch_index_last', $stored); + } + return 'Updated the namespace variable for the index process.'; +} + +/** + * Rename some variables and update the database tables + */ +function solrsearch_update_7012() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + // @see: drupal_load() + if (!function_exists('solrsearch_default_environment')) { + include_once dirname(__FILE__) . '/solrsearch.module'; + } + + $env_id = solrsearch_default_environment(); + + // Variable changed from integer to array with environment integers + $stored = variable_get('solrsearch_index_last', array()); + if (isset($stored['solrsearch'])) { + $stored[$env_id]['node']['last_entity_id'] = $stored['solrsearch']['last_nid']; + $stored[$env_id]['node']['last_changed'] = $stored['solrsearch']['last_change']; + unset($stored['solrsearch']); + variable_set('solrsearch_index_last', $stored); + } + $last = variable_get('solrsearch_index_updated', NULL); + if (isset($last)) { + variable_set('solrsearch_index_updated', array($env_id => (int) $last)); + } + + // Change namespace to environment id + $excluded_types = solrsearch_environment_variable_get('solrsearch', 'solrsearch_search_excluded_types', array()); + if (!empty($excluded_types)) { + solrsearch_environment_variable_set($env_id, 'solrsearch_search_excluded_types', $excluded_types); + solrsearch_environment_variable_del('solrsearch', 'solrsearch_search_excluded_types'); + } + + // Install the new schema + //Predefine an amount of types that get their own table + $types = array( + 'other' => 'solrsearch_index_entities', + 'node' => 'solrsearch_index_entities_node', + ); + foreach ($types as $type => $table) { + $schema[$table] = array( + 'description' => 'Stores a record of when an entity changed to determine if it needs indexing by Solr.', + 'fields' => array( + 'entity_type' => array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'entity_id' => array( + 'description' => 'The primary identifier for an entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'bundle' => array( + 'description' => 'The bundle to which this entity belongs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + 'status' => array( + 'description' => 'Boolean indicating whether the entity is visible to non-administrators (eg, published for nodes).', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'changed' => array( + 'description' => 'The Unix timestamp when an entity was changed.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'changed' => array('bundle', 'status', 'changed'), + ), + 'primary key' => array('entity_id'), + ); + if ($type == 'other') { + // Need the entity type also in the pkey for multiple entities in one table. + $schema[$table]['primary key'][] = 'entity_type'; + } + // Create the table + db_create_table($table, $schema[$table]); + } + + $schema['solrsearch_index_bundles'] = array( + 'description' => 'Records what bundles we should be indexing for a given environment.', + 'fields' => array( + 'env_id' => array( + 'description' => 'The name of the environment.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'entity_type' => array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'bundle' => array( + 'description' => 'The bundle to index.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + ), + 'primary key' => array('env_id', 'entity_type', 'bundle'), + ); + db_create_table('solrsearch_index_bundles', $schema['solrsearch_index_bundles']); + + + // Move the data from solrsearch_search_node to solrsearch_index_entities_node + $select = db_select('solrsearch_search_node', 'asn'); + $select->join('node', 'n', 'asn.nid = n.nid'); + $select->addField('n', 'nid', 'entity_id'); + $select->addField('n', 'type', 'bundle'); + $select->addField('asn', 'status', 'status'); + $select->addField('asn', 'changed', 'changed'); + $select->addExpression("'node'", 'entity_type'); + $return_value = db_insert('solrsearch_index_entities_node') + ->fields(array('entity_id', 'bundle', 'status', 'changed', 'entity_type')) + ->from($select) + ->execute(); + // Drop the table solrsearch_search_node + db_drop_table('solrsearch_search_node'); + + $environments = solrsearch_load_all_environments(); + foreach ($environments as $env_id => $environment) { + $excluded_types = solrsearch_environment_variable_get($env_id, 'solrsearch_search_excluded_types', array()); + // Get indexable entity types + $options = array(); + foreach (entity_get_info() as $entity_type => $entity_info) { + if ($entity_type == 'node') { + foreach ($entity_info['bundles'] as $key => $info) { + // See if it was excluded & only of entity node. We will not enable + // other entity types by default + if (empty($excluded_types[$key])) { + $options[$entity_type][$key] = $key; + } + } + } + } + // Set all except the excluded types + // @see solrsearch_index_set_bundles() + foreach ($options as $entity_type => $bundles) { + db_delete('solrsearch_index_bundles') + ->condition('env_id', $env_id) + ->condition('entity_type', $entity_type) + ->execute(); + + if ($bundles) { + $insert = db_insert('solrsearch_index_bundles') + ->fields(array('env_id', 'entity_type', 'bundle')); + + foreach ($bundles as $bundle) { + $insert->values(array( + 'env_id' => $env_id, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + )); + } + $insert->execute(); + } + } + // Remove the excluded types + solrsearch_environment_variable_del($env_id, 'solrsearch_search_excluded_types'); + } +} + +/** + * Make consistent (and reduce) field lengths which cause excess pkey length. + */ +function solrsearch_update_7013() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + db_drop_primary_key('solrsearch_index_entities'); + db_change_field('solrsearch_index_entities', 'entity_type', 'entity_type', array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE) + ); + db_add_primary_key('solrsearch_index_entities', array('entity_id', 'entity_type')); + db_change_field('solrsearch_index_entities_node', 'entity_type', 'entity_type', array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE) + ); + db_drop_primary_key('solrsearch_index_bundles'); + db_change_field('solrsearch_index_bundles', 'env_id', 'env_id', array( + 'description' => 'Unique identifier for the environment', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE) + ); + db_change_field('solrsearch_index_bundles', 'entity_type', 'entity_type', array( + 'description' => 'The type of entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE) + ); + db_add_primary_key('solrsearch_index_bundles', array('env_id', 'entity_type', 'bundle')); +} + +/** + * Remove status from the key. + */ +function solrsearch_update_7014() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + $types = array( + 'other' => 'solrsearch_index_entities', + 'node' => 'solrsearch_index_entities_node', + ); + foreach ($types as $type => $table) { + db_drop_index($table, 'changed'); + db_add_index($table, 'bundle_changed', array('bundle', 'changed')); + } +} + + +/** + * Fix primary key schema mismatch for those who cleanly installed with beta16. + */ +function solrsearch_update_7015() { + if (variable_get('solrsearch_update_from_6303', FALSE)) { + return NULL; + } + + // Brand new installations since update_7013 have the wrong primary key. + db_drop_primary_key('solrsearch_index_entities'); + db_add_primary_key('solrsearch_index_entities', array('entity_id', 'entity_type')); +} + +/** + * Clean up solrsearch_update_from_6303. + * + * This variable had been used to bypass 7.x-1.x updates which are redundant + * with 6.x-3.x. + */ +function solrsearch_update_7016() { + variable_del('solrsearch_update_from_6303'); +} diff -r 000000000000 -r a2b4f67e73dc solrsearch.interface.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.interface.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,544 @@ +abort_search is TRUE. + * + * @param string $keys + * The search keys. + * + * @return + * A stdClass response object. + */ + function search($keys = NULL); + + /** + * Calls a method, without arguments, on the Solr object with which the query + * object was initialized. + * + * @param string $method + * The method to call on the Solr object. + * + * @return + * Any method return. + */ + function solr($method); +} + +/** + * The interface for all 'Service' objects. + */ +interface DrupalApacheSolrServiceInterface { + /** + * Call the /admin/ping servlet, to test the connection to the server. + * + * @param $timeout + * maximum time to wait for ping in seconds, -1 for unlimited (default 2). + * @return + * (float) seconds taken to ping the server, FALSE if timeout occurs. + */ + function ping($timeout = 2); + + /** + * Get information about the Solr Core. + * + * @return + * (string) system info encoded in json + */ + function getSystemInfo(); + + /** + * Get just the field meta-data about the index. + */ + function getFields($num_terms = 0); + + /** + * Get meta-data about the index. + */ + function getLuke($num_terms = 0); + + /** + * Get information about the Solr Core. + * + * Returns a Simple XMl document + */ + function getStats(); + + /** + * Get summary information about the Solr Core. + */ + function getStatsSummary(); + + /** + * Clear cached Solr data. + */ + function clearCache(); + + /** + * Constructor + * + * @param $url + * The URL to the Solr server, possibly including a core name. E.g. http://localhost:8983/solr/ + * or https://search.example.com/solr/core99/ + * @param $env_id + * The machine name of a corresponding saved configuration used for loading + * data like which facets are enabled. + */ + function __construct($url, $env_id = NULL); + + function getId(); + + /** + * Make a request to a servlet (a path) that's not a standard path. + * + * @param string $servlet + * A path to be added to the base Solr path. e.g. 'extract/tika' + * + * @param array $params + * Any request parameters when constructing the URL. + * + * @param array $options + * @see drupal_http_request() $options. + * + * @return + * response object + * + * @thows Exception + */ + function makeServletRequest($servlet, $params = array(), $options = array()); + + /** + * Get the Solr url + * + * @return string + */ + function getUrl(); + + /** + * Set the Solr url. + * + * @param $url + * + * @return $this + */ + function setUrl($url); + + /** + * Raw update Method. Takes a raw post body and sends it to the update service. Post body + * should be a complete and well formed xml document. + * + * @param string $rawPost + * @param float $timeout Maximum expected duration (in seconds) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function update($rawPost, $timeout = FALSE); + + /** + * Add an array of Solr Documents to the index all at once + * + * @param array $documents Should be an array of ApacheSolrDocument instances + * @param boolean $allowDups + * @param boolean $overwritePending + * @param boolean $overwriteCommitted + * + * @return response objecte + * + * @throws Exception If an error occurs during the service call + */ + function addDocuments($documents, $overwrite = NULL, $commitWithin = NULL); + + /** + * Send a commit command. Will be synchronous unless both wait parameters are set to false. + * + * @param boolean $optimize Defaults to true + * @param boolean $waitFlush Defaults to true + * @param boolean $waitSearcher Defaults to true + * @param float $timeout Maximum expected duration (in seconds) of the commit operation on the server (otherwise, will throw a communication exception). Defaults to 1 hour + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function commit($optimize = TRUE, $waitFlush = TRUE, $waitSearcher = TRUE, $timeout = 3600); + + /** + * Create a delete document based on document ID + * + * @param string $id Expected to be utf-8 encoded + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function deleteById($id, $timeout = 3600); + + /** + * Create and post a delete document based on multiple document IDs. + * + * @param array $ids Expected to be utf-8 encoded strings + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function deleteByMultipleIds($ids, $timeout = 3600); + + /** + * Create a delete document based on a query and submit it + * + * @param string $rawQuery Expected to be utf-8 encoded + * @param float $timeout Maximum expected duration of the delete operation on the server (otherwise, will throw a communication exception) + * @return stdClass response object + * + * @throws Exception If an error occurs during the service call + */ + function deleteByQuery($rawQuery, $timeout = 3600); + + /** + * Send an optimize command. Will be synchronous unless both wait parameters are set + * to false. + * + * @param boolean $waitFlush + * @param boolean $waitSearcher + * @param float $timeout Maximum expected duration of the commit operation on the server (otherwise, will throw a communication exception) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function optimize($waitFlush = TRUE, $waitSearcher = TRUE, $timeout = 3600); + + /** + * Simple Search interface + * + * @param string $query The raw query string + * @param array $params key / value pairs for other query parameters (see Solr documentation), use arrays for parameter keys used more than once (e.g. facet.field) + * + * @return response object + * + * @throws Exception If an error occurs during the service call + */ + function search($query = '', array $params = array(), $method = 'GET'); + + /** + * Get the current solr version. This could be 1, 3 or 4 + * + * @return int + * 1, 3 or 4. Does not give a more details version, for that you need + * to get the system info. + */ + function getSolrVersion(); + +} diff -r 000000000000 -r a2b4f67e73dc solrsearch.module --- /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 @@ + '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 facet missing 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)[^>]*>.*@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 '' . ($options['html'] ? $text : check_plain(html_entity_decode($text))) . ''; +} + +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 .= '

'; + $output .= t( + 'Settings for: @environment (Overview)', + array('@url' => $url, '@environment' => $environment['name']) + ); + $output .= "

\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.'), + ), + ); +} diff -r 000000000000 -r a2b4f67e73dc solrsearch.pages.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch.pages.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,349 @@ + $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys)); + + // Collect the search results. + $results = module_invoke('solrsearch_search', 'search_execute', $keys, $conditions); + + + } + //dpm($keys); + //dpm($conditions); + //dpm($results); + + foreach($results as $result){ + $data['digitalobjects_items_select'][]=$result['mpiwg-dri']; + } + + digitalobjects_digitalcollectionsManage($data); + return "TEST"; +} + + + + + + + +/** + * Menu callback; presents the search form and/or search results. + * + * @param $module + * Search module to use for the search. + * @param $keys + * Keywords to use for the search. + */ +function solrsearch_view($keys = '') { + $keys = trim($keys); + // Also try to pull search keywords out of the $_REQUEST variable to + // support old GET format of searches for existing links. + if (!$keys && !empty($_REQUEST['keys'])) { + $keys = trim($_REQUEST['keys']); + } + + + + + // Default results output is an empty string. + $results = array('#markup' => ''); + // Process the search form. Note that if there is $_POST data, + // search_form_submit() will cause a redirect to search/[module path]/[keys], + // which will get us back to this page callback. In other words, the search + // form submits with POST but redirects to GET. This way we can keep + // the search query URL clean as a whistle. + + if (empty($_POST['form_id']) || $_POST['form_id'] != 'solrsearch_form') { + + $conditions = NULL; + + $conditions = solrsearch_search_conditions($keys); + + // Only search if there are keywords or non-empty conditions. + if ($keys || !empty($conditions)) { + + // Log the search keys. + //watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys)); + + // Collect the search results. + $results = solrsearch_data($keys, 'solrsearch_search', $conditions); + + } + } + + // The form may be altered based on whether the search was run. + + $wrapper = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array('tool','box'), + ), + ); + + + + $wrapper['form'] = drupal_get_form('solrsearch_form', NULL, $keys); + + $build['search_form'] = $wrapper; + + + $build['search_results'] = $results; + + + + return $build; +} + +/** + * Process variables for search-results.tpl.php. + * + * The $variables array contains the following arguments: + * - $results: Search results array. + * - $module: Module the search results came from (module implementing + * hook_search_info()). + * + * @see search-results.tpl.php + */ +function template_preprocess_solrsearch_results(&$variables) { + + + $variables['search_results'] = ''; + if (!empty($variables['module'])) { + $variables['module'] = check_plain($variables['module']); + } + + + + + if (user_access("manage private collections")){ + $variables['digitalobjects_items_select']=base_path()."digitalcollections/manageCurrent"; + $variables['search_results'] .=''; + $variables['search_results'] .=''; + + + $sw = explode("/",current_path()); + $key= $sw[sizeof($sw)-1]; + + $variables['search_results'] .='Save all (max 500) to the current collection'; + } + + foreach ($variables['results'] as $result) { + + + + + $variables['search_results'] .= theme('solrsearch_result', array('result' => $result, 'module' => $variables['module'])); + + /* add checkboxes for the item */ + $variables['search_results'] .= '
'; + + if (user_access("manage private collections")){ + $variables['search_results'] .= ''; + } + + /* add tools for the item */ + $variables['search_results'] .= theme('digitalobjects_item_tools', array('objid' => $result['mpiwg-dri'])) . "
"; + + + + } + $variables['pager'] = theme('pager', array('tags' => array('<<','<','...','>','>>'),'quantity' => 5)); + + $variables['description'] = ''; + $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module']; +} + + +function check_array_plain($result,$field){ + if (!isset($result[$field])){ + return null; + } + + $string = $result[$field]; + + if (is_array($string)){ + return check_plain(implode($string)); + } else { + return check_plain($string); + } +} +/** + * Process variables for search-result.tpl.php. + * + * The $variables array contains the following arguments: + * - $result + * - $module + * + * @see search-result.tpl.php + */ +function template_preprocess_solrsearch_result(&$variables) { + + + $result = $variables['result']; + //dpm($variables); + $variables['url'] = create_url_from_dri($result['mpiwg-dri'],$result['access-type']); + $variables['access_type'] = $result['access-type']!="free" ? "restricted" :$result['access-type']; + $variables['title'] = check_plain($result['title']); + $variables['author'] = check_plain($result['author']); + $variables['date'] = check_array_plain($result,'date'); + $variables['signature'] = check_array_plain($result,'IM_call-number'); + $variables['doc_type'] = check_array_plain($result,'doc-type'); + $variables['author'] = check_plain($result['author']); + $variables['year'] = is_array($result['year']) ? implode($result['year']) :$result['year']; + $variables['thumburl'] = create_thumburl_from_dri($result['mpiwg-dri']); + // Check for existence. User search does not include snippets. + $variables['snippet'] = isset($result['snippet']) ? $result['snippet'] : ''; + + $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module']; + + $idstring = str_replace("INST:", "", $result['archive-path']); + $path = create_library_path($idstring); + + $variables['librarySearchPath'] = $path; //archive- path enthaelt die ID im Inst. Katalog + + $variables['collectionPath'] = $GLOBALS['base_url'] . str_replace("ECHOCOLL:", "/node/", $result['archive-path']); + $variables['content'] = $result['content']; + + $variables['external_url']=$result['url'][0]; + $variables['external_image']=$result['image'][0]; + $variables['provider']=$result['provider'][0]; + $variables['dataSource']=$result['dataSource']; + + +} + + + +/** + * As the search form collates keys from other modules hooked in via + * hook_form_alter, the validation takes place in _submit. + * search_form_validate() is used solely to set the 'processed_keys' form + * value for the basic search form. + */ +function solrsearch_form_validate($form, &$form_state) { + form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state); +} + +/** + * Process a search form submission. + */ +function solrsearch_form_submit($form, &$form_state) { + + + $keys = $form_state['values']['processed_keys']; + if ($keys == '') { + form_set_error('keys', t('Please enter some keywords.')); + // Fall through to the form redirect. + } + + $exact = $form_state['values']['exact']; + if ($exact==0) { #nicht genaue suche dann trunkiere das wort + $keys .="*"; + } + $form_state['redirect'] = $form_state['action'] . '/' . $keys; +} + + +/** + * Builds a search form. + * + * @param $action + * Form action. Defaults to "search/$path", where $path is the search path + * associated with the module in its hook_search_info(). This will be + * run through url(). + * @param $keys + * The search string entered by the user, containing keywords for the search. + * @param $module + * The search module to render the form for: a module that implements + * hook_search_info(). If not supplied, the default search module is used. + * @param $prompt + * Label for the keywords field. Defaults to t('Enter your keywords') if NULL. + * Supply '' to omit. + * + * @return + * A Form API array for the search form. + */ +function solrsearch_form($form, &$form_state, $action = '', $keys = '', $module = NULL, $prompt = NULL) { + $module_info = FALSE; + + if (!$action) { + $action = 'solrsearch'; + } + if (!isset($prompt)) { + $prompt = t('Enter your keywords'); + + } + + if ($keys == ''){ + $keys=$prompt; + } + + $form['#action'] = url($action); + + // Record the $action for later use in redirecting. + $form_state['action'] = $action; + $form['#attributes']['class'][] = 'search-form'; + $form['module'] = array('#type' => 'value', '#value' => $module); + $form['basic'] = array('#type' => 'container', '#attributes' => array('class' => array('container-inline','searchbox'))); + + $form['basic']['keys'] = array( + '#type' => 'textfield', + '#size' => $prompt ? 40 : 20, + '#attributes' =>array('placeholder' => t($keys),'class' => array('text')), + '#maxlength' => 255, + ); + // processed_keys is used to coordinate keyword passing between other forms + // that hook into the basic search form. + $form['basic']['processed_keys'] = array('#type' => 'value', '#value' => ''); + + + $form['basic']['submit'] = array( + '#type' => 'submit', + '#value' => t('>'), + '#attributes' =>array('class' => array('submit')), + + ); + + $form['basic']['exact'] = array( + '#type' => 'checkbox', + '#title' => 'whole word', + + + + ); + + + return $form; +} + + + diff -r 000000000000 -r a2b4f67e73dc solrsearch_search.admin.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search.admin.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1197 @@ +Facets will not be shown until you enable Facet API module.'); + } + else { + $description .= t('Remember to configure the facets on the search environment page and assign blocks to regions on the block settings page', array( + '!facetslink' => url('admin/config/search/solrsearch/settings/'), + '!blocklink' => url('admin/structure/block'), + )); + } + return array( + '#type' => 'radios', + '#title' => t('Behavior on empty search'), + '#options' => array( + 'none' => t("Show search box"), + 'browse' => t("Show enabled facets' blocks under the search box"), + 'blocks' => t("Show enabled facets' blocks in their configured regions"), + 'results' => t("Show enabled facets' blocks in their configured regions and first page of all available results"), + ), + '#default_value' => $default_value, + '#description' => $description, + ); +} + +/** + * Menu callback for the overview page showing custom search pages and blocks. + * @return array $build + */ +function solrsearch_search_page_list_all() { + + $build['pages'] = solrsearch_search_page_list_pages(); + $build['blocks'] = solrsearch_search_page_list_blocks(); + return $build; + +} + + +/** + * Listing of all the search pages + * @return array $build + */ +function solrsearch_search_page_list_pages() { + $build = array(); + $rows = array(); + $rows['core_solr_search'] = array(); + + // Build the sortable table header. + $header = array( + 'label' => array('data' => t('Name'), 'field' => 's.label'), + 'path' => array('data' => t('Path'), 'field' => 's.search_path'), + 'environment' => array('data' => t('Search environment')), + 'operations' => array('data' => t('Operations')), + ); + + $search_pages = solrsearch_search_load_all_search_pages(); + $default_search_page = solrsearch_search_default_search_page(); + foreach ($search_pages as $search_page) { + $row = array(); + + // Add the label + $label = check_plain($search_page['label']); + // Is this row our default environment? + if ($search_page['page_id'] == $default_search_page) { + $label = t('!search_page (Default)', array('!search_page' => $label)); + } + + $row[] = $label; + // Add the link + $row[] = array( + 'data' => array( + '#type' => 'link', + '#title' => $search_page['search_path'], + '#href' => $search_page['search_path'], + ), + ); + + // Add the search environment + $environment = solrsearch_environment_load($search_page['env_id']); + $row[] = $environment ? check_plain($environment['name']) : check_plain(t('')); + // Operations + $row[] = array('data' => l(t('Edit'), 'admin/config/search/solrsearch/search-pages/' . $search_page['page_id'] . '/edit')); + $row[] = array('data' => l(t('Clone'), 'admin/config/search/solrsearch/search-pages/' . $search_page['page_id'] . '/clone')); + + // Allow to revert a search page or to delete it + if (!isset($search_page['settings']['solrsearch_search_not_removable']) && !isset($search_page['in_code_only'])) { + if ((isset($search_page['type']) && $search_page['type'] == 'Overridden')) { + $row[] = array('data' => l(t('Revert'), 'admin/config/search/solrsearch/search-pages/' . $search_page['page_id'] . '/delete')); + } else { + $row[] = array('data' => l(t('Delete'), 'admin/config/search/solrsearch/search-pages/' . $search_page['page_id'] . '/delete')); + } + } + else { + $row[] = ''; + } + $rows[$search_page['page_id']] = $row; + } + + // Automatically enlarge our header with the operations size + $header['operations']['colspan'] = count(reset($rows)) - 3; + + $build['list'] = array( + '#prefix' => '

Pages

', + '#theme' => 'table', + '#header' => $header, + '#rows' => array_values($rows), + '#empty' => t('No available search pages.'), + ); + $build['pager'] = array( + '#theme' => 'pager', + '#quantity' => 20, + '#weight' => 10, + ); + + return $build; +} + +/** + * Listing of all the search blocks + * @return array $build + */ +function solrsearch_search_page_list_blocks() { + $build = array(); + $rows = array(); + + // Build the sortable table header. + $header = array( + 'label' => array('data' => t('Name'), 'field' => 's.label'), + 'environment' => array('data' => t('Search environment')), + 'operations' => array('data' => t('Operations')), + ); + + $search_blocks = variable_get('solrsearch_search_mlt_blocks', array()); + foreach ($search_blocks as $search_block_id => $search_block) { + $row = array(); + + // Add the label + $label = check_plain($search_block['name']); + $row[] = $label; + + // Add the search environment + $environment = solrsearch_environment_load($search_block['mlt_env_id']); + $row[] = $environment ? check_plain($environment['name']) : check_plain(t('')); + // Operations + if (module_exists('block')) { + $row[] = array('data' => l(t('Configure'), 'admin/structure/block/manage/solrsearch_search/' . $search_block_id . '/configure', array('query' => array('destination' => current_path())))); + } + $row[] = array('data' => l(t('Delete'), 'admin/config/search/solrsearch/search-pages/block/' . $search_block_id . '/delete')); + $rows[$search_block_id] = $row; + } + + // Automatically enlarge our header with the operations size + $header['operations']['colspan'] = count(reset($rows)) - 2; + + $build['list'] = array( + '#prefix' => '

Blocks "More Like This"

', + '#theme' => 'table', + '#header' => $header, + '#rows' => array_values($rows), + '#empty' => t('No available search blocks.'), + ); + $build['pager'] = array( + '#theme' => 'pager', + '#quantity' => 20, + '#weight' => 10, + ); + + return $build; +} + +/** + * Menu callback/form-builder for the form to create or edit a search page. + */ +function solrsearch_search_page_settings_form($form, &$form_state, $search_page = NULL) { + $environments = solrsearch_load_all_environments(); + $options = array('' => t('')); + foreach ($environments as $id => $environment) { + $options[$id] = $environment['name']; + } + // Validate the env_id. + if (!empty($search_page['env_id']) && !solrsearch_environment_load($search_page['env_id'])) { + $search_page['env_id'] = ''; + } + + // Initializes form with common settings. + $form['search_page'] = array( + '#type' => 'value', + '#value' => $search_page, + ); + + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#description' => '', + '#required' => TRUE, + '#size' => 30, + '#maxlength' => 32, + '#default_value' => !empty($search_page['label']) ? $search_page['label'] : '', + '#description' => t('The human-readable name of the search page configuration.'), + ); + + $form['page_id'] = array( + '#type' => 'machine_name', + '#maxlength' => 32, + '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'solrsearch_search_page_exists', + 'source' => array('label'), + ), + '#description' => '', + '#default_value' => !empty($search_page['page_id']) ? $search_page['page_id'] : '', + '#disabled' => !empty($search_page), + '#description' => t('A unique machine-readable identifier for the search page configuration. It must only contain lowercase letters, numbers, and underscores.'), + ); + + $form['description_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Description'), + '#default_value' => !empty($search_page['description']) ? TRUE : FALSE + ); + + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('Provide description'), + '#title_display' => 'invisible', + '#size' => 64, + '#default_value' => !empty($search_page['description']) ? $search_page['description'] : '', + '#dependency' => array( + 'edit-description-enable' => array(1), + ), + ); + + $is_default = FALSE; + if (!empty($search_page)) { + $is_default = $search_page['page_id'] == solrsearch_search_default_search_page(); + } + $form['make_default'] = array( + '#type' => 'checkbox', + '#title' => t('Make this Solr Search Page the default'), + '#description' => t('Useful for eg. making facets to link to this page when they are shown on non-search pages'), + '#default_value' => $is_default, + '#disabled' => $is_default, + ); + + $form['info'] = array( + '#title' => t('Search Page Information'), + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#prefix' => '
', + '#suffix' => '
', + ); + + $core_solr_search = FALSE; + if (!empty($search_page['page_id']) && ($search_page['page_id'] == 'core_solr_search')) { + $core_solr_search = TRUE; + } + if ($core_solr_search) { + $description = t('This page always uses the current default search environment'); + } + else { + $description = t('The environment that is used by this search page. If no environment is selected, this page will be disabled.'); + } + + $form['info']['env_id'] = array( + '#title' => t('Search environment'), + '#type' => 'select', + '#options' => $options, + '#default_value' => !empty($search_page['env_id']) ? $search_page['env_id'] : '', + '#disabled' => $core_solr_search, + '#description' => $description, + ); + + $form['info']['page_title'] = array( + '#title' => t('Title'), + '#type' => 'textfield', + '#required' => TRUE, + '#maxlength' => 255, + '#description' => 'You can use %value to place the search term in the title', + '#default_value' => !empty($search_page['page_title']) ? $search_page['page_title'] : '', + ); + + $search_types = solrsearch_search_load_all_search_types(); + $options = array('custom' => t('Custom Field')); + foreach ($search_types as $id => $search_type) { + $options[$id] = $search_type['name']; + } + + $form['info']['search_type'] = array( + '#title' => t('Search Type'), + '#type' => 'select', + '#options' => $options, + '#default_value' => !empty($search_page['settings']['solrsearch_search_search_type']) ? $search_page['settings']['solrsearch_search_search_type'] : '', + '#access' => !$core_solr_search, + '#description' => t('Use this only when filtering on a value from the search path. + For example, select Taxonomy Term to filter on a term ID (search/taxonomy/%).'), + '#ajax' => array( + 'callback' => 'solrsearch_search_ajax_search_page_default', + 'wrapper' => 'dynamic-search-page', + 'method' => 'replace', + ), + ); + + // Token element validate is added to validate the specific + // tokens that are allowed + $form['info']['search_path'] = array( + '#title' => t('Path'), + '#type' => 'textfield', + '#required' => TRUE, + '#maxlength' => 255, + '#description' => t('For example: search/my-search-page. Search keywords will appear at the end of the path.'), + '#default_value' => !empty($search_page['search_path']) ? $search_page['search_path'] : '', + ); + if (!$core_solr_search) { + $form['info']['search_path']['#description'] .= ' ' . t('You can use one % to make the search page dynamic.'); + } + + $form['info']['custom_filter_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Custom Filter'), + '#default_value' => !empty($search_page['settings']['solrsearch_search_custom_enable']) ? TRUE : FALSE + ); + + $form['info']['filters'] = array( + '#title' => t('Custom filters'), + '#type' => 'textfield', + '#required' => FALSE, + '#maxlength' => 255, + '#description' => t('A comma-separated list of lucene filter queries to apply by default.'), + '#default_value' => !empty($search_page['settings']['fq']) ? implode(', ', $search_page['settings']['fq']) : '', + '#dependency' => array( + 'edit-custom-filter-enable' => array(1), + 'edit-search-type' => array('custom'), + ), + ); + if (!$core_solr_search) { + $form['info']['filters']['#description'] .= ' ' . t('E.g. "bundle:blog, is_uid:(1 OR 2 OR %). % will be replaced by the value of % in the path"'); + } + + $form['advanced'] = array( + '#title' => t('Advanced Search Page Options'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + ); + + // Results per page per search page + $default_value = isset($search_page['settings']['solrsearch_search_per_page']) ? $search_page['settings']['solrsearch_search_per_page'] : '10'; + $form['advanced']['solrsearch_search_per_page'] = array( + '#type' => 'textfield', + '#size' => 3, + '#title' => t('Results per page'), + '#description' => t('How many items will be displayed on one page of the search result.'), + '#default_value' => $default_value, + ); + + // Enable/disable spellcheck on pages + $default_value = isset($search_page['settings']['solrsearch_search_spellcheck']) ? $search_page['settings']['solrsearch_search_spellcheck'] : TRUE; + $form['advanced']['solrsearch_search_spellcheck'] = array( + '#type' => 'checkbox', + '#title' => t('Enable spell check'), + '#description' => t('Display "Did you mean … ?" above search results.'), + '#default_value' => $default_value, + ); + + // Enable/disable search form on search page (replaced by a block perhaps) + $default_value = isset($search_page['settings']['solrsearch_search_search_box']) ? $search_page['settings']['solrsearch_search_search_box'] : TRUE; + $form['advanced']['solrsearch_search_search_box'] = array( + '#type' => 'checkbox', + '#title' => t('Enable the search box on the page'), + '#description' => t('Display a search box on the page.'), + '#default_value' => $default_value, + ); + + // Enable/disable search form on search page (replaced by a block perhaps) + $default_value = isset($search_page['settings']['solrsearch_search_allow_user_input']) ? $search_page['settings']['solrsearch_search_allow_user_input'] : FALSE; + $form['advanced']['solrsearch_search_allow_user_input'] = array( + '#type' => 'checkbox', + '#title' => t('Allow user input using the URL'), + '#description' => t('Allow users to use the URL for manual facetting via fq[] params (e.g. http://example.com/search/site/test?fq[]=uid:1&fq[]=tid:99). This will only work in combination with a keyword search. The recommended value is unchecked'), + '#default_value' => $default_value, + ); + + // Use the main search page setting as the default for new pages. + $default_value = isset($search_page['settings']['solrsearch_search_browse']) ? $search_page['settings']['solrsearch_search_browse'] : 'browse'; + $form['advanced']['solrsearch_search_browse'] = _solrsearch_search_browse_form($default_value); + + // Button for the corresponding actions + $form['actions'] = array( + '#type' => 'actions', + ); + + $form['actions']['submit'] = array( + '#type' => 'submit', + '#redirect' => 'admin/config/search/solrsearch/search-pages', + '#value' => t('Save'), + ); + $form['actions']['submit_edit'] = array( + '#type' => 'submit', + '#value' => t('Save and edit'), + ); + + $form['actions']['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#href' => 'admin/config/search/solrsearch/search-pages', + ); + + $form['#submit'][] = 'solrsearch_search_page_settings_form_submit'; + + return $form; +} + +/** + * Callback element needs only select the portion of the form to be updated. + * Since #ajax['callback'] return can be HTML or a renderable array (or an + * array of commands), we can just return a piece of the form. + */ +function solrsearch_search_ajax_search_page_default($form, $form_state, $search_page = NULL) { + + $search_page = $form_state['values']['search_page']; + $search_types = solrsearch_search_load_all_search_types(); + + // Helping with sensible defaults for the search path + $default_search_path = ''; + if (!empty($form_state['values']['search_type']) && $form_state['values']['search_type'] != 'custom') { + $default_search_path = $search_types[$form_state['values']['search_type']]['default menu']; + $form['info']['search_path']['#value'] = $default_search_path; + } + + // Helping with sensible defaults for the search title + $default_search_title = ''; + + if (empty($form_state['values']['page_title']) && $form_state['values']['search_type'] != 'custom') { + $default_search_title_callback = $search_types[$form_state['values']['search_type']]['title callback']; + $default_search_title = $default_search_title_callback(); + $form['info']['page_title']['#value'] = $default_search_title; + } + return $form['info']; +} + +function solrsearch_search_page_settings_form_validate($form, &$form_state) { + // Performs basic validation of the menu path. + if (url_is_external($form_state['values']['search_path'])) { + form_set_error('search_path', t('Path must be local.')); + } + $form_state['values']['search_path'] = trim($form_state['values']['search_path'], '/'); + if (empty($form_state['values']['search_path'])) { + form_set_error('search_path', t('Path required.')); + } + if (!is_numeric($form_state['values']['advanced']['solrsearch_search_per_page'])) { + form_set_error('advanced][solrsearch_search_per_page', t('The amount of search results must be an integer.')); + } + $form_state['values']['advanced']['solrsearch_search_per_page'] = (int) $form_state['values']['advanced']['solrsearch_search_per_page']; + if (empty($form_state['values']['advanced']['solrsearch_search_per_page'])) { + form_set_error('advanced][solrsearch_search_per_page', t('The amount of search results cannot be empty.')); + } + if (count(explode('%', $form_state['values']['search_path'])) > 2) { + form_set_error('search_path', t('Only one % placeholder is allowed.')); + } +} + +/** + * Processes solrsearch_search_page_settings_form form submissions. + */ +function solrsearch_search_page_settings_form_submit($form, &$form_state) { + $settings = array(); + $settings['fq'] = array(); + if ($form_state['values']['filters']) { + foreach (explode(',', $form_state['values']['filters']) as $string) { + $string = trim($string); + // Minimal validation. ':' must exist and can't be the 1st char.. + if (strpos($string, ':')) { + $settings['fq'][] = $string; + } + } + } + $settings['solrsearch_search_custom_enable'] = $form_state['values']['custom_filter_enable']; + $settings['solrsearch_search_search_type'] = $form_state['values']['search_type']; + // Add all advanced settings. + $settings += $form_state['values']['advanced']; + + // Set the default search page settings + if (!empty($form_state['values']['make_default']) && isset($form_state['values']['page_id'])) { + variable_set('solrsearch_search_default_search_page', $form_state['values']['page_id']); + } + + $search_page = array(); + $search_page['page_id'] = $form_state['values']['page_id']; + $search_page['label'] = $form_state['values']['label']; + $search_page['description'] = $form_state['values']['description']; + $search_page['env_id'] = $form_state['values']['env_id']; + $search_page['search_path'] = $form_state['values']['search_path']; + $search_page['page_title'] = $form_state['values']['page_title']; + $search_page['settings'] = $settings; + solrsearch_search_page_save($search_page); + + // Saves our values in the database, sets redirect path on success. + drupal_set_message(t('The configuration options have been saved for %page.', array('%page' => $form_state['values']['label']))); + if (isset($form_state['clicked_button']['#redirect'])) { + $form_state['redirect'] = $form_state['clicked_button']['#redirect']; + } + else { + $form_state['redirect'] = current_path(); + } + // Regardlessly of the destination parameter we want to go to another page + unset($_GET['destination']); + drupal_static_reset('drupal_get_destination'); + drupal_get_destination(); + // Menu rebuild needed to pick up search path. + menu_rebuild(); +} + +/** + * Deletes a single search page configuration. + */ +function solrsearch_search_delete_search_page_confirm($form, &$form_state, $search_page) { + + // Sets values required for deletion. + $form['page_id'] = array('#type' => 'value', '#value' => $search_page['page_id']); + $form['label'] = array('#type' => 'value', '#value' => $search_page['label']); + + if (isset($search_page['export_type']) && $search_page['export_type'] == '3') { + $verb = t('Revert'); + } + else { + $verb = t('Delete'); + } + + // Sets the message, or the title of the page. + $message = t( + 'Are you sure you want to !verb the %label search page configuration?', + array('%label' => $form['label']['#value'], '!verb' => strtolower($verb)) + ); + + + // Builds caption. + $caption = '

'; + $caption .= t( + 'The %label search page configuration will be deleted.', + array('%label' => $form['label']['#value']) + ); + $caption .= '

'; + $caption .= '

' . t('This action cannot be undone.') . '

'; + + // Finalizes and returns the confirmation form. + $return_path = 'admin/config/search/solrsearch/search-pages'; + $button_text = $verb; + if (!isset($search_page['settings']['solrsearch_search_not_removable'])) { + return confirm_form($form, filter_xss($message), $return_path, filter_xss($caption), check_plain($button_text)); + } + else { + // Maybe this should be solved somehow else + drupal_access_denied(); + } +} + +/** + * Process content type delete confirm submissions. + */ +function solrsearch_search_delete_search_page_confirm_submit($form, &$form_state) { + // Deletes the index configuration settings. + // @todo Invoke a hook that allows backends and indexers to delete their stuff. + db_delete('solrsearch_search_page') + ->condition('page_id', $form_state['values']['page_id']) + ->execute(); + + // Sets message, logs action. + drupal_set_message(t( + 'The %label search page configuration has been deleted.', + array('%label' => $form_state['values']['label']) + )); + watchdog('solrsearch_search', 'Deleted search page configuration "@page_id".', array('@page_id' => $form_state['values']['page_id']), WATCHDOG_NOTICE); + + // Rebuilds the menu cache. + menu_rebuild(); + + // Returns back to search page list page. + $form_state['redirect'] = 'admin/config/search/solrsearch/search-pages'; +} + +/** + * Clones a single search page configuration + * @param $search_page + * The search page that needs to be cloned + */ +function solrsearch_search_clone_search_page_confirm($form, &$form_state, $search_page) { + $form['page_id'] = array( + '#type' => 'value', + '#value' => $search_page['page_id'], + ); + return confirm_form( + $form, + t('Are you sure you want to clone search page %name?', array('%name' => $search_page['label'])), + 'admin/config/search/solrsearch', + '', + t('Clone'), + t('Cancel') + ); +} + +/** + * Submits the confirmations of the cloning of a search page + */ +function solrsearch_search_clone_search_page_confirm_submit($form, &$form_state) { + if (solrsearch_search_page_clone($form_state['values']['page_id'])) { + drupal_set_message(t('The search page was cloned')); + } + $form_state['redirect'] = 'admin/config/search/solrsearch/search-pages'; +} + +/** + * Menu callback - the settings form. + */ +function solrsearch_search_get_fields($environment = NULL) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + $environment = solrsearch_environment_load($env_id); + } + $env_id = $environment['env_id']; + + // Try to fetch the schema fields. + try { + $solr = solrsearch_get_solr($env_id); + $fields = $solr->getFields(); + return $fields; + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), 'warning'); + drupal_set_message(t('Cannot get information about the fields in the index.'), 'warning'); + } +} + +/** + * Menu callback - Bias settings form. + */ +function solrsearch_bias_settings_page($environment = NULL) { + if (empty($environment)) { + $env_id = solrsearch_default_environment(); + $environment = solrsearch_environment_load($env_id); + } + $env_id = $environment['env_id']; + + // Initializes output with information about which environment's setting we are + // editing, as it is otherwise not transparent to the end user. + $output = array( + 'solrsearch_environment' => array( + '#theme' => 'solrsearch_settings_title', + '#env_id' => $env_id, + ), + ); + + // Adds content bias and type boost forms. + $fields = solrsearch_search_get_fields($environment); + $form = array(); + $form = drupal_get_form('solrsearch_search_bias_form', $env_id, $fields); + $output['bias_forms'] = $form; + return $output; +} + +function solrsearch_search_bias_form($form, &$form_state, $env_id, $fields) { + $form['#env_id'] = $env_id; + $form['bias_tabs'] = array( + '#type' => 'vertical_tabs', + ); + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + '#submit' => array('solrsearch_search_bias_form_submit'), + ); + $form['actions']['reset'] = array( + '#type' => 'submit', + '#value' => t('Reset to defaults'), + '#submit' => array('solrsearch_search_bias_form_reset'), + ); + $form += solrsearch_search_result_bias_form($env_id); + $form += solrsearch_search_type_boost_form($env_id); + $form += solrsearch_search_field_bias_form($fields, $env_id); + return $form; +} + +function solrsearch_search_bias_form_submit(&$form, &$form_state) { + // Exclude unnecessary elements. + form_state_values_clean($form_state); + foreach ($form_state['values'] as $key => $value) { + if (is_array($value) && isset($form_state['values']['array_filter'])) { + $value = array_keys(array_filter($value)); + } + // There is no need to set default variable values. + if (!isset($form[$key]['#default_value']) || $form[$key]['#default_value'] != $value) { + switch ($key) { + case 'solrsearch_search_sticky_boost' : + case 'solrsearch_search_promote_boost' : + case 'solrsearch_search_date_boost' : + case 'solrsearch_search_comment_boost' : + case 'solrsearch_search_changed_boost' : + case 'solrsearch_search_type_boosts' : + case 'field_bias' : + solrsearch_environment_variable_set($form['#env_id'], $key, $value); + } + } + } + drupal_set_message(t('The configuration options have been saved.')); +} + +function solrsearch_search_bias_form_reset($form, &$form_state) { + // Exclude unnecessary elements. + form_state_values_clean($form_state); + + foreach ($form_state['values'] as $key => $value) { + solrsearch_environment_variable_del($form['#env_id'], $key); + } + drupal_set_message(t('The configuration options have been reset to their default values.')); +} + +/** + * Form builder function to set date, comment, etc biases. + */ +function solrsearch_search_result_bias_form($env_id) { + + $date_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_date_boost', '0:0'); + $comment_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_comment_boost', '0:0'); + $changed_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_changed_boost', '0:0'); + $sticky_boost = solrsearch_environment_variable_get($env_id, 'solrsearch_search_sticky_boost', '0'); + $promote_boost = solrsearch_environment_variable_get($env_id, 'solrsearch_search_promote_boost', '0'); + + $options = array( + '10:2000.0' => '10', + '8:1000.0' => '9', + '8:700.0' => '8', + '8:500.0' => '7', + '4:300.0' => '6', + '4:200.0' => '5', + '4:150.0' => '4', + '2:150.0' => '3', + '2:100.0' => '2', + '1:100.0' => '1', + '0:0' => t('Ignore'), + ); + + $weights = drupal_map_assoc(array('21.0', '13.0', '8.0', '5.0', '3.0', '2.0', '1.0', '0.8', '0.5', '0.3', '0.2', '0.1')); + $weights['0'] = t('Ignore'); + + $form = array(); + $form['result_bias'] = array( + '#type' => 'fieldset', + '#title' => t('Result biasing'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#description' => t('Give bias to certain properties when ordering the search results. Any value except Ignore will increase the score of the given type in search results. Choose Ignore to ignore any given property.'), + '#group' => 'bias_tabs', + ); + $form['result_bias']['solrsearch_search_sticky_boost'] = array( + '#type' => 'select', + '#options' => $weights, + '#title' => t("Sticky at top of lists"), + '#default_value' => $sticky_boost, + '#description' => t("Select additional bias to give to nodes that are set to be 'Sticky at top of lists'."), + ); + $form['result_bias']['solrsearch_search_promote_boost'] = array( + '#type' => 'select', + '#options' => $weights, + '#title' => t("Promoted to home page"), + '#default_value' => $promote_boost, + '#description' => t("Select additional bias to give to nodes that are set to be 'Promoted to home page'."), + ); + $form['result_bias']['solrsearch_search_date_boost'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t("More recently created"), + '#default_value' => $date_settings, + '#description' => t('This setting will change the result scoring so that nodes created more recently may appear before those with higher keyword matching.'), + ); + $form['result_bias']['solrsearch_search_comment_boost'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t("More comments"), + '#default_value' => $comment_settings, + '#description' => t('This setting will change the result scoring so that nodes with more comments may appear before those with higher keyword matching.'), + ); + $form['result_bias']['solrsearch_search_changed_boost'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t("More recent comments"), + '#default_value' => $changed_settings, + '#description' => t('This setting will change the result scoring so that nodes with the most recent comments (or most recent updates to the node itself) may appear before those with higher keyword matching.'), + ); + return $form; +} + +/** + * Form builder function to set query field weights. + */ +function solrsearch_search_field_bias_form($fields, $env_id) { + $form = array(); + // get the current weights + $defaults = array( + 'content' => '1.0', + 'ts_comments' => '0.5', + 'tos_content_extra' => '0.1', + 'label' => '5.0', + 'tos_name' => '3.0', + 'taxonomy_names' => '2.0', + 'tags_h1' => '5.0', + 'tags_h2_h3' => '3.0', + 'tags_h4_h5_h6' => '2.0', + 'tags_inline' => '1.0', + 'tags_a' => '0', + ); + $qf = solrsearch_environment_variable_get($env_id, 'field_bias', $defaults); + $weights = drupal_map_assoc(array('21.0', '13.0', '8.0', '5.0', '3.0', '2.0', '1.0', '0.8', '0.5', '0.3', '0.2', '0.1')); + $weights['0'] = t('Omit'); + if (!$qf) { + $qf = $defaults; + } + if ($fields) { + $form['field_bias'] = array( + '#type' => 'fieldset', + '#title' => t('Field biases'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#description' => t('Specify here which fields are more important when searching. Give a field a greater numeric value to make it more important. If you omit a field, it will not be searched.'), + '#group' => 'bias_tabs', + ); + foreach ($fields as $field_name => $field) { + // Only indexed feids are searchable. + if ($field->schema{0} == 'I') { + // By default we only show text fields. Use hook_form_alter to change. + // We use filter_xss to make sure links are allowed + $form['field_bias'][$field_name] = array( + '#access' => ($field->type == 'text' || $field->type == 'text_und'), + '#type' => 'select', + '#options' => $weights, + '#title' => filter_xss(solrsearch_field_name_map($field_name)), + '#default_value' => isset($qf[$field_name]) ? $qf[$field_name] : '0', + ); + } + } + + // Make sure all the default fields are included, even if they have + // no indexed content. + foreach ($defaults as $field_name => $weight) { + $form['field_bias'][$field_name] = array( + '#type' => 'select', + '#options' => $weights, + '#title' => check_plain(solrsearch_field_name_map($field_name)), + '#default_value' => isset($qf[$field_name]) ? $qf[$field_name] : $defaults[$field_name], + ); + } + + ksort($form['field_bias']); + } + return $form; +} + +/** + * Form builder function to set query type weights. + */ +function solrsearch_search_type_boost_form($env_id) { + + $form['type_boost'] = array( + '#type' => 'fieldset', + '#title' => t('Type biasing'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#group' => 'bias_tabs', + ); + $form['type_boost']['solrsearch_search_type_boosts'] = array( + '#type' => 'item', + '#description' => t("Specify here which node types should get a higher relevancy score in searches. Any value except Ignore will increase the score of the given type in search results."), + '#tree' => TRUE, + ); + + $weights = drupal_map_assoc(array('21.0', '13.0', '8.0', '5.0', '3.0', '2.0', '1.0', '0.8', '0.5', '0.3', '0.2', '0.1')); + $weights['0'] = t('Ignore'); + + // Get the current boost values. + $type_boosts = solrsearch_environment_variable_get($env_id, 'solrsearch_search_type_boosts', array()); + $names = array(); + foreach (entity_get_info() as $entity_type => $entity_info) { + if (!empty($entity_info['solrsearch']['indexable'])) { + foreach ($entity_info['bundles'] as $key => $info) { + $names[$key] = $info['label']; + } + } + } + asort($names); + + foreach ($names as $type => $name) { + $form['type_boost']['solrsearch_search_type_boosts'][$type] = array( + '#type' => 'select', + '#title' => t('%type type content bias', array('%type' => $name)), + '#options' => $weights, + '#default_value' => isset($type_boosts[$type]) ? $type_boosts[$type] : 0, + ); + } + + return $form; +} + +/** + * MoreLikeThis administration and utility functions. + */ +function solrsearch_search_mlt_add_block_form() { + $form = solrsearch_search_mlt_block_form(); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => '5', + ); + return $form; +} + +function solrsearch_search_mlt_add_block_form_submit($form, &$form_state) { + solrsearch_search_mlt_save_block($form_state['values']); + $block_message = t('New More like this block created. Configure this block in the Block administration', array('!configure' => url('admin/structure/block'))); + drupal_set_message($block_message); + $form_state['redirect'] = 'admin/config/search/solrsearch/search-pages'; +} + +/** + * Merge supplied settings with the standard defaults.. + */ +function solrsearch_search_mlt_block_defaults($block = array()) { + return $block + array( + 'name' => '', + 'num_results' => '5', + 'mlt_fl' => array( + 'label' => 'label', + 'taxonomy_names' => 'taxonomy_names', + ), + 'mlt_env_id' => 'solr', + 'mlt_mintf' => '1', + 'mlt_mindf' => '1', + 'mlt_minwl' => '3', + 'mlt_maxwl' => '15', + 'mlt_maxqt' => '20', + 'mlt_type_filters' => array(), + 'mlt_custom_filters' => '', + ); +} + +/** + * Constructs a list of field names used on the settings form. + * + * @return array An array containing a the fields in the solr instance. + */ +function solrsearch_search_mlt_get_fields() { + $rows = array(); + + try { + $solr = solrsearch_get_solr(); + $fields = $solr->getFields(); + foreach ($fields as $field_name => $field) { + if ($field->schema{4} == 'V') { + $rows[$field_name] = solrsearch_field_name_map($field_name); + } + } + ksort($rows); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + + return $rows; +} + +/** + * A helper function to save MLT block data. + * + * If passed a block delta, the function will update block settings. If it is + * not passed a block delta, the function will create a new block. + * + * @param array $block_settings An array containing the settings required to form + * a moreLikeThis request. + * + * @param int $delta The id of the block you wish to update. + */ +function solrsearch_search_mlt_save_block($block_settings = array(), $delta = NULL) { + $blocks = variable_get('solrsearch_search_mlt_blocks', array()); + if (is_null($delta)) { + $count = 0; + ksort($blocks); + // Construct a new array key. + if (end($blocks)) { + list(, $count) = explode('-', key($blocks)); + } + $delta = sprintf('mlt-%03d', 1 + $count); + } + $defaults = solrsearch_search_mlt_block_defaults(); + // Remove stray form values. + $blocks[$delta] = array_intersect_key($block_settings, $defaults) + $defaults; + // Eliminate non-selected fields. + $blocks[$delta]['mlt_fl'] = array_filter($blocks[$delta]['mlt_fl']); + $blocks[$delta]['delta'] = $delta; + $blocks[$delta]['mlt_type_filters'] = array_filter($blocks[$delta]['mlt_type_filters']); + $blocks[$delta]['mlt_custom_filters'] = trim($blocks[$delta]['mlt_custom_filters']); + variable_set('solrsearch_search_mlt_blocks', $blocks); +} + +function solrsearch_search_mlt_delete_block_form($form, &$form_state, $block) { + if ($block) { + // Backwards compatibility for the block deltas + if (isset($block['delta'])) { + $delta = $block['delta']; + } + else { + $delta = arg(6); + } + // Add our delta to the delete form + $form['delta'] = array( + '#type' => 'value', + '#value' => $delta, + ); + $question = t('Are you sure you want to delete the "More Like this" block %name?', array('%name' => $block['name'])); + $path = 'admin/structure/block'; + $description = t('The block will be deleted. This action cannot be undone.'); + $yes = t('Delete'); + $no = t('Cancel'); + return confirm_form($form, filter_xss($question), $path, $description, $yes, $no); + } +} + +function solrsearch_search_mlt_delete_block_form_submit($form, &$form_state) { + $blocks = solrsearch_search_load_all_mlt_blocks(); + + unset($blocks[$form_state['values']['delta']]); + variable_set('solrsearch_search_mlt_blocks', $blocks); + drupal_set_message(t('The block has been deleted.')); + $form_state['redirect'] = 'admin/config/search/solrsearch/search-pages'; +} + +/** + * Form to edit moreLikeThis block settings. + * + * @param int $delta If editing, the id of the block to edit. + * + * @return array The form used for editing. + * @todo Add term boost settings. + * @todo Enable the user to specify a query, rather then forcing suggestions + * based on the node id. + */ +function solrsearch_search_mlt_block_form($block_id = NULL) { + + if (!empty($block_id)) { + $block = solrsearch_search_mlt_block_load($block_id); + if (!$block) { + return array(); + } + } + else { + $block = solrsearch_search_mlt_block_defaults(); + } + + $form['delta'] = array( + '#type' => 'value', + '#default_value' => isset($block['delta']) ? $block['delta'] : '', + '#weight' => '-2', + ); + + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Block name'), + '#description' => t('The block name displayed to site users.'), + '#required' => TRUE, + '#default_value' => isset($block['name']) ? $block['name'] : '', + '#weight' => '-2', + ); + + $environments = solrsearch_load_all_environments(); + $options = array('' => t('')); + foreach ($environments as $id => $environment) { + $options[$id] = $environment['name']; + } + $form['mlt_env_id'] = array( + '#title' => t('Search environment'), + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($block['mlt_env_id']) ? $block['mlt_env_id'] : solrsearch_default_environment(), + ); + + $form['num_results'] = array( + '#type' => 'select', + '#title' => t('Maximum number of related items to display'), + '#default_value' => isset($block['num_results']) ? $block['num_results'] : '', + '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)), + '#weight' => -1, + + ); + /* + * + $form['mlt_fl'] = array( + '#type' => 'checkboxes', + '#title' => t('Fields for finding related content'), + '#description' => t('Choose the fields to be used in calculating similarity. The default combination of %taxonomy_names and %title will provide relevant results for typical sites.', array("%taxonomy_names" => solrsearch_field_name_map("taxonomy_names"), "%title" => solrsearch_field_name_map("label"))), + '#options' => solrsearch_search_mlt_get_fields(), + '#required' => TRUE, + '#default_value' => isset($block['mlt_fl']) ? $block['mlt_fl'] : '', + ); + */ + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced configuration'), + '#weight' => '1', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $options = drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7)); + /*$form['advanced']['mlt_mintf'] = array( + '#type' => 'select', + '#title' => t('Minimum term frequency'), + '#description' => t('A word must appear this many times in any given document before the document is considered relevant for comparison.'), + '#default_value' => isset($block['mlt_mintf']) ? $block['mlt_mintf'] : '', + '#options' => $options, + ); + $form['advanced']['mlt_mindf'] = array( + '#type' => 'select', + '#title' => t('Minimum document frequency'), + '#description' => t('A word must occur in at least this many documents before it will be used for similarity comparison.'), + '#default_value' => isset($block['mlt_mindf']) ? $block['mlt_mindf'] : '', + '#options' => $options, + ); + $form['advanced']['mlt_minwl'] = array( + '#type' => 'select', + '#title' => t('Minimum word length'), + '#description' => 'You can use this to eliminate short words such as "the" and "it" from similarity comparisons. Words must be at least this number of characters or they will be ignored.', + '#default_value' => isset($block['mlt_minwl']) ? $block['mlt_minwl'] : '', + '#options' => $options, + ); + $form['advanced']['mlt_maxwl'] = array( + '#type' => 'select', + '#title' => t('Maximum word length'), + '#description' => t('You can use this to eliminate very long words from similarity comparisons. Words of more than this number of characters will be ignored.'), + '#default_value' => isset($block['mlt_maxwl']) ? $block['mlt_maxwl'] : '', + '#options' => drupal_map_assoc(array(8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), + ); + $form['advanced']['mlt_maxqt'] = array( + '#type' => 'select', + '#title' => t('Maximum number of query terms'), + '#description' => t('The maximum number of query terms that will be included in any query. Lower numbers will result in fewer recommendations but will get results faster. If a content recommendation is not returning any recommendations, you can either check more "Comparison fields" checkboxes or increase the maximum number of query terms here.'), + '#options' => drupal_map_assoc(array(3, 5, 7, 10, 12, 15, 20, 25, 30, 35, 40)), + '#default_value' => isset($block['mlt_maxqt']) ? $block['mlt_maxqt'] : '', + ); +*/ + /* + $form['restrictions'] = array( + '#type' => 'fieldset', + '#title' => t('Filters'), + '#weight' => '1', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $type_options = array(); + foreach (node_type_get_types() as $key => $type) { + $type_options[$key] = $type->name; + } + + $form['restrictions']['mlt_type_filters'] = array( + '#type' => 'checkboxes', + '#title' => t('Content Types'), + '#default_value' => is_array($block['mlt_type_filters']) ? $block['mlt_type_filters'] : array(), + '#options' => $type_options, + '#description' => t('Select the content types that similarity suggestions should be restricted to. Multiple types are joined with an OR query, so selecting more types results in more recommendations. If none are selected, no filter will be applied.'), + '#weight' => '-2', + ); + + $form['restrictions']['mlt_custom_filters'] = array( + '#type' => 'textfield', + '#title' => t('Additional Query'), + '#description' => t("A query, in Lucene syntax, which will further filter the similarity suggestions. For example, 'label:strategy' will filter related content further to only those with strategy in the title. Here are some more examples:") . + '
    +
  • ss_language:fr
  • +
  • tid:(5 OR 7)
  • +
  • ds_created:[2009-05-01T23:59:59Z TO 2009-07-28T12:30:00Z]
  • +
  • -is_uid:0, -is_uid:1
  • +
', + '#required' => FALSE, + '#default_value' => isset($block['mlt_custom_filters']) ? $block['mlt_custom_filters'] : '', + '#weight' => '-1', + ); +*/ + return $form; +} diff -r 000000000000 -r a2b4f67e73dc solrsearch_search.info --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search.info Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,21 @@ +name = solrsearch_search +description = Frontend for solr II +dependencies[] = search +dependencies[] = solrsearch +package = SolrSearch +core = 7.x +configure = admin/config/search/solrsearch/search-pages + +files[] = solrsearch_search.install +files[] = solrsearch_search.module +files[] = solrsearch_search_blocks.inc +files[] = solrsearch_search.admin.inc +files[] = solrsearch_search.pages.inc + + +; Information added by drupal.org packaging script on 2013-03-15 +version = "7.x-1.1+34-dev" +core = "7.x" +project = "solrsearch" +datestamp = "1363307665" + diff -r 000000000000 -r a2b4f67e73dc solrsearch_search.install --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search.install Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,390 @@ + 'tid', + 'solrsearch_search_per_page' => 10, + 'solrsearch_search_browse' => 'results', + 'solrsearch_search_spellcheck' => FALSE, + 'solrsearch_search_search_box' => FALSE, + ); + $settings = serialize($settings); + + $fields = array( + 'page_id' => 'taxonomy_search', + 'label' => 'Taxonomy Search', + 'description' => 'Search all items with given term', + 'search_path' => 'taxonomy/term/%', + 'env_id' => '', + 'page_title' => '%value', + 'settings' => $settings, + ); + db_insert('solrsearch_search_page')->fields($fields)->execute(); +} + +/** + * Implements hook_enable(). + */ +function solrsearch_search_enable() { + // Make sure the default core search page is installed. + //dpm("ENABLE"); + $search_page = solrsearch_search_page_load('core_solr_search'); + //dpm($search_page); + if (empty($search_page)) { + // Add Default search page (core search) + // This is a duplication from update_7004 but it is intended + // so future changes are possible without breaking the update + $settings = array( + 'solrsearch_search_search_type' => 'custom', + 'solrsearch_search_per_page' => 10, + 'solrsearch_search_browse' => 'browse', + 'solrsearch_search_spellcheck' => TRUE, + 'solrsearch_search_not_removable' => TRUE, + 'solrsearch_search_search_box' => TRUE, + ); + $settings = serialize($settings); + + $fields = array( + 'page_id' => 'core_solr_search', + 'label' => 'Core Solr Search', + 'description' => 'Core SolrSearch', + 'search_path' => 'solrsearch/site', + 'env_id' => 'searchecho', + 'page_title' => 'echo', + 'settings' => $settings, + ); + db_insert('solrsearch_search_page')->fields($fields)->execute(); + } + + + #$active = variable_get('search_active_modules', array('node', 'user')); + #$active[] = 'solrsearch_search'; + #variable_set('search_active_modules', array_unique($active)); +} + +/** + * Implements hook_schema(). + */ +function solrsearch_search_schema() { + $schema = array(); + + $schema['solrsearch_search_page'] = array( + 'description' => 'Apache Solr Search search page settings.', + 'export' => array( + // Environment machine name. + 'key' => 'page_id', + // Description of key. + 'key name' => 'search page machine name', + // Variable name to use in exported code. + 'identifier' => 'searcher', + // Use the same hook as the API name below. + 'default hook' => 'solrsearch_search_default_searchers', + 'status' => 'solrsearch_search_page_status', + // CTools API implementation. + 'api' => array( + 'owner' => 'solrsearch_search', + 'api' => 'solrsearch_search_defaults', + 'minimum_version' => 3, + 'current_version' => 3, + ), + // Includes all search page specific configurations. + 'export callback' => 'solrsearch_search_page_settings_export', + ), + 'fields' => array( + 'page_id' => array( + 'description' => 'The machine readable name of the search page.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'label' => array( + 'description' => 'The human readable name of the search page.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'description' => array( + 'description' => 'The description of the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'search_path' => array( + 'description' => 'The path to the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'page_title' => array( + 'description' => 'The title of the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'env_id' => array( + 'description' => 'The machine name of the search environment.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + 'settings' => array( + 'description' => 'Serialized storage of general settings.', + 'type' => 'text', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('page_id'), + 'indexes' => array( + 'env_id' => array('env_id'), + ), + ); + + return $schema; +} + +/** + * Implements hook_uninstall(). + */ +function solrsearch_search_uninstall() { + $stored = variable_get('solrsearch_index_last', array()); + unset($stored['solrsearch_search']); + variable_set('solrsearch_index_last', $stored); + + $active = variable_get('search_active_modules', array('node', 'user')); + $idx = array_search('solrsearch_search', $active); + if ($idx !== FALSE) { + unset($active[$idx]); + variable_set('search_active_modules', $active); + } + // Remove variables. + variable_del('solrsearch_search_spellcheck'); + variable_del('solrsearch_search_mlt_blocks'); + variable_del('solrsearch_search_default_search_page'); + // Remove blocks. + db_delete('block')->condition('module', 'solrsearch_search')->execute(); +} + +/** + * Various updates for Drupal 7. + */ +function solrsearch_search_update_7000() { + $taxo_links = variable_get('solrsearch_search_taxonomy_links', 0); + // TODO - enable the new contrib module? + variable_del('solrsearch_search_taxonomy_links'); + // TODO - possibly rename block deltas, etc. + $active = variable_get('search_active_modules', array('node', 'user')); + $active[] = 'solrsearch_search'; + variable_set('search_active_modules', array_unique($active)); + if (variable_get('solrsearch_search_make_default', 0)) { + variable_set('search_default_module', 'solrsearch_search'); + } + variable_del('solrsearch_search_make_default'); +} + +/** + * Add solrsearch_search_page table. + */ +function solrsearch_search_update_7001() { + // Moved to 7002 +} + +/** + * Add solrsearch_search_page table for real. + */ +function solrsearch_search_update_7002() { + + $schema['solrsearch_search_page'] = array( + 'description' => 'Apache Solr Search search page settings.', + 'export' => array( + 'key' => 'page_id', + 'identifier' => 'searcher', + 'default hook' => 'solrsearch_search_default_searchers', + 'status' => 'solrsearch_search_page_status', + 'api' => array( + 'owner' => 'solrsearch_search', + 'api' => 'solrsearch_search_defaults', + 'minimum_version' => 3, + 'current_version' => 3, + ), + 'export callback' => 'solrsearch_search_page_settings_export', + ), + 'fields' => array( + 'page_id' => array( + 'description' => 'The machine readable name of the search page.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'label' => array( + 'description' => 'The human readable name of the search page.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'description' => array( + 'description' => 'The description of the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'search_path' => array( + 'description' => 'The path to the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'page_title' => array( + 'description' => 'The title of the search page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'env_id' => array( + 'description' => 'The machine name of the search environment.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'settings' => array( + 'description' => 'Serialized storage of general settings.', + 'type' => 'text', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('page_id'), + 'indexes' => array( + 'env_id' => array('env_id'), + ), + ); + if (db_table_exists('solrsearch_search_page')) { + // Just in case you are chasing HEAD. + db_drop_table('solrsearch_search_page'); + } + db_create_table('solrsearch_search_page', $schema['solrsearch_search_page']); +} + + +/** + * Delete all Apache Solr Search blocks - they moved to Facet API. + */ +function solrsearch_search_update_7003() { + // Remove blocks. + db_delete('block')->condition('module', 'solrsearch_search')->execute(); +} + +/** + * Add a default search page for core + * Add a taxonomy page if the taxonomy module was ever active + */ +function solrsearch_search_update_7004() { + // Add Default search page (core search) + $settings = array( + 'solrsearch_search_search_type' => 'custom', + 'solrsearch_search_per_page' => variable_get('solrsearch_rows', 10), + 'solrsearch_search_browse' => variable_get('solrsearch_search_browse', 'browse'), + 'solrsearch_search_spellcheck' => variable_get('solrsearch_search_spellcheck', TRUE), + 'solrsearch_search_not_removable' => TRUE, + 'solrsearch_search_search_box' => TRUE, + ); + $settings = serialize($settings); + + $fields = array( + + 'page_id' => 'core_solr_search', + 'label' => 'Core Solr Search', + 'description' => 'Core SolrSearch', + 'search_path' => 'solrsearch/site', + 'env_id' => 'searchecho', + 'page_title' => 'echo', + 'settings' => $settings, + ); + db_insert('solrsearch_search_page')->fields($fields)->execute(); + // Remove variables. + variable_del('solrsearch_search_spellcheck'); + variable_del('solrsearch_search_browse'); + + // Add this taxonomy search page to the database + $settings = array( + 'solrsearch_search_search_type' => 'tid', + 'solrsearch_search_per_page' => 10, + 'solrsearch_search_browse' => 'results', + 'solrsearch_search_spellcheck' => FALSE, + 'solrsearch_search_search_box' => FALSE, + ); + $settings = serialize($settings); + + $fields = array( + 'page_id' => 'taxonomy_search', + 'label' => 'Taxonomy Search', + 'description' => 'Search all items with given term', + 'search_path' => 'taxonomy/term/%', + 'env_id' => '', + 'page_title' => '%value', + 'settings' => $settings, + ); + db_insert('solrsearch_search_page')->fields($fields)->execute(); + + // Check if the taxonomy module was ever present + $status = db_query("SELECT 1 FROM {system} WHERE name = 'solrsearch_taxonomy'")->fetchField(); + if ($status) { + $message = t('If you had the solrsearch_taxonomy module enabled please go to the !link and enable the Taxonomy Term page', array('!link' => l('Apache Solr custom pages', 'admin/config/search/solrsearch/search-pages'))); + drupal_set_message($message, 'warning'); + } +} + +/** + * Make the env_id length on the solrsearch_search_page table 64 characters + * to match the length of the env_id on all other tables + */ +function solrsearch_search_update_7005(&$sandbox) { + db_drop_index('solrsearch_search_page', 'env_id'); + db_change_field('solrsearch_search_page', 'env_id', 'env_id', + array( + 'description' => 'The machine name of the search environment.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + array( + 'indexes' => array( + 'env_id' => array('env_id'), + ) + ) + ); +} + +/** + * Remove all solrsearch_search env variables for show_facets if it is zero + */ +function solrsearch_search_update_7006() { + module_load_include('module', 'solrsearch'); + $environments = solrsearch_load_all_environments(); + foreach ($environments as $environment) { + $show_facets = solrsearch_environment_variable_get($environment['env_id'], 'solrsearch_search_show_facets', 0); + if ($show_facets === 0) { + solrsearch_environment_variable_del($environment['env_id'], 'solrsearch_search_show_facets'); + } + } +} diff -r 000000000000 -r a2b4f67e73dc solrsearch_search.module --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search.module Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,1886 @@ +fetchField(); + + //dpm($query); + //dpm($count); + if ($count == 0) { + return NULL; + } + + // Load the default search page, we only support facets to link to this + // search page due to complexity and slow downs + //dpm("SEARCH PAGE I"); + + $search_page_id = solrsearch_search_default_search_page(); + //dpm("SP".$search_page_id); + $search_page = solrsearch_search_page_load($search_page_id); + // Do not continue if our search page is not valid + if (empty($search_page)) { + return NULL; + } + //dpm("SEARCH PAGE II"); + //dpm($search_page); + $show_facets = solrsearch_environment_variable_get($search_page['env_id'], 'solrsearch_search_show_facets', 0); + //dpm($show_facets); + if ($show_facets) { + + // Converts current path to lowercase for case insensitive matching. + $paths = array(); + $paths[] = drupal_strtolower(drupal_get_path_alias(current_path())); + $paths[] = drupal_strtolower(current_path()); + + $facet_pages = solrsearch_environment_variable_get($search_page['env_id'], 'solrsearch_search_facet_pages', ''); + //dpm("FACET_PAGES"); + //dpm($facet_pages); + //dpm($paths); + //dpm("FACET_PAGES_PATJS"); + // Iterates over each environment to check if an empty query should be run. + if (!empty($facet_pages)) { + // Compares path with settings, runs query if there is a match. + $patterns = drupal_strtolower($facet_pages); + foreach ($paths as $path) { + //dpm($path."===".$patterns); + if (drupal_match_path($path, $patterns)) { + //dpm("MATCH"); + try { + if (!empty($search_page['search_path'])) { + + $solr = solrsearch_get_solr($search_page['env_id']); + // Initializes params for empty query. + //dpm("solr"); + //dpm($solr); + $params = array( + 'spellcheck' => 'false', + 'fq' => array(), + 'rows' => 1, + ); + $context['page_id'] = $search_page_id; + $context['search_type'] = 'solrsearch_search_show_facets'; + solrsearch_search_run_empty('solrsearch', $params, $search_page['search_path'], $solr, $context); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + } + } + } + } +} + +/** + * Implements hook_menu(). + */ +function solrsearch_search_menu() { + $items['admin/config/search/solrsearch/search-pages'] = array( + 'title' => 'Pages/Blocks', + 'description' => 'Configure search pages', + 'page callback' => 'solrsearch_search_page_list_all', + 'access arguments' => array('administer search'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'solrsearch_search.admin.inc', + ); + $items['admin/config/search/solrsearch/search-pages/add'] = array( + 'title' => 'Add search page', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_search_page_settings_form'), + 'access arguments' => array('administer search'), + 'type' => MENU_LOCAL_ACTION, + 'weight' => 1, + 'file' => 'solrsearch_search.admin.inc', + ); + $items['admin/config/search/solrsearch/search-pages/%solrsearch_search_page/edit'] = array( + 'title' => 'Edit search page', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_search_page_settings_form', 5), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch_search.admin.inc', + ); + $items['admin/config/search/solrsearch/search-pages/%solrsearch_search_page/delete'] = array( + 'title' => 'Delete search page', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_search_delete_search_page_confirm', 5), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch_search.admin.inc', + ); + $items['admin/config/search/solrsearch/search-pages/%solrsearch_search_page/clone'] = array( + 'title' => 'Clone search page', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_search_clone_search_page_confirm', 5), + 'access arguments' => array('administer search'), + 'file' => 'solrsearch_search.admin.inc', + ); + $items['admin/config/search/solrsearch/search-pages/addblock'] = array( + 'title' => 'Add search block "More Like This"', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('solrsearch_search_mlt_add_block_form'), + 'access arguments' => array('administer search'), + 'type' => MENU_LOCAL_ACTION, + 'weight' => 2, + 'file' => 'solrsearch_search.admin.inc', + ); + + + return $items; +} + +function UNUSED_solrsearch_search_menu_alter(&$items) { + // Gets default search information. + $default_info = search_get_default_module_info(); + $search_types = solrsearch_search_load_all_search_types(); + $search_pages = solrsearch_search_load_all_search_pages(); + + // Iterates over search pages, builds menu items. + foreach ($search_pages as $search_page) { + // Validate the environemnt ID in case of import or missed deletion. + $environment = solrsearch_environment_load($search_page['env_id']); + if (!$environment) { + continue; + } + + // Parses search path into it's various parts, builds menu items dependent + // on whether %keys is in the path. + $parts = explode('/', $search_page['search_path']); + $keys_pos = count($parts); + // Tests whether we are simulating a core search tab. + $core_solr_search = ($parts[0] == 'search'); + $position = array_search('%', $parts); + $page_title = isset($search_page['page_title']) ? $search_page['page_title'] : 'Search Results'; + + + // Replace possible tokens [term:tid], [node:nid], [user:uid] with their + // menu-specific variant + $items[$search_page['search_path']] = array( + 'title' => $page_title, + 'page callback' => 'solrsearch_search_custom_page', + 'page arguments' => array($search_page['page_id'], '', $position), + 'access arguments' => array('search content'), + 'type' => ($core_solr_search) ? MENU_LOCAL_TASK : MENU_SUGGESTED_ITEM, + 'file' => 'solrsearch_search.pages.inc', + 'file path' => drupal_get_path('module', 'solrsearch_search'), + ); + + // Not using menu tail because of inflexibility with clean urls + $items[$search_page['search_path'] . '/%'] = array( + 'title' => $page_title, + 'page callback' => 'solrsearch_search_custom_page', + 'page arguments' => array($search_page['page_id'], $keys_pos, $position), + 'access arguments' => array('search content'), + 'type' => !($core_solr_search) ? MENU_CALLBACK : MENU_LOCAL_TASK, + 'file' => 'solrsearch_search.pages.inc', + 'file path' => drupal_get_path('module', 'solrsearch_search'), + ); + + // If title has a certain callback for the selected type we use it + $search_type_id = !empty($search_page['settings']['solrsearch_search_search_type']) ? $search_page['settings']['solrsearch_search_search_type'] : FALSE; + $search_type = !empty($search_types[$search_type_id]) ? $search_types[$search_type_id] : FALSE; + + if ($search_type && !empty($position)) { + $title_callback = $search_type['title callback']; + $items[$search_page['search_path']]['title callback'] = $title_callback; + $items[$search_page['search_path']]['title arguments'] = array($search_page['page_id'], $position); + $items[$search_page['search_path'] . '/%']['title callback'] = $title_callback; + $items[$search_page['search_path'] . '/%']['title arguments'] = array($search_page['page_id'], $position); + } + // If we have additional searches in the search/* path + if ($core_solr_search) { + $items[$search_page['search_path'] . '/%']['tab_root'] = 'search/' . $default_info['path'] . '/%'; + $items[$search_page['search_path'] . '/%']['tab_parent'] = 'search/' . $default_info['path']; + } + } +} + +/** + * Function that loads all the search types + * + * @return array $search_types + */ +function solrsearch_search_load_all_search_types() { + $search_types = &drupal_static(__FUNCTION__); + + if (isset($search_types)) { + return $search_types; + } + // Use cache_get to avoid DB when using memcache, etc. + $cache = cache_get('solrsearch_search:search_types', 'cache_solrsearch'); + if (isset($cache->data)) { + $search_types = $cache->data; + } + else { + $search_types = array( + 'bundle' => array( + 'name' => solrsearch_field_name_map('bundle'), + 'default menu' => 'search/type/%', + 'title callback' => 'solrsearch_search_get_value_title', + ), + ); + drupal_alter('solrsearch_search_types', $search_types); + cache_set('solrsearch_search:search_types', $search_types, 'cache_solrsearch'); + } + return $search_types; +} + + + +/** + * Used as a callback function to generate a title for a node/page depending + * on the input in the configuration screen + * @param integer $search_page_id + * @param integer $value + * @return String + */ +/* +function solrsearch_search_get_value_title($search_page_id = NULL, $value = NULL) { + $page_title = 'Search results for %value'; + if (isset($value) && isset($search_page_id)) { + $search_page = solrsearch_search_page_load($search_page_id); + $page_title = str_replace('%value', '!value', $search_page['page_title']); + $title = $value; + } + return t($page_title, array('!value' => $title)); +} +*/ + +/** + * Get or set the default search page id for the current page. + */ +function solrsearch_search_default_search_page($page_id = NULL) { + $default_page_id = &drupal_static(__FUNCTION__, NULL); + + if (isset($page_id)) { + $default_page_id = $page_id; + } + if (empty($default_page_id)) { + $default_page_id = variable_get('solrsearch_search_default_search_page', 'core_solr_search'); + } + return $default_page_id; +} + +/** + * Implements hook_solrsearch_default_environment() + * + * Make sure the core search page is using the default environment. + */ +function solrsearch_search_solrsearch_default_environment($env_id, $old_env_id) { + $page = solrsearch_search_page_load('core_solr_search'); + if ($page && $page['env_id'] != $env_id) { + $page['env_id'] = $env_id; + solrsearch_search_page_save($page); + } +} + +/** + * Load a search page + * @param string $page_id + * @return array + */ +function solrsearch_search_page_load($page_id) { + //dpm(" solrsearch_search_page_load"); + + $search_pages = solrsearch_search_load_all_search_pages(); + //dpm($search_pages); + if (!empty($search_pages[$page_id])) { + return $search_pages[$page_id]; + } + return FALSE; +} + +function solrsearch_search_page_save($search_page) { + if (!empty($search_page)) { + db_merge('solrsearch_search_page') + ->key(array('page_id' => $search_page['page_id'])) + ->fields(array( + 'label' => $search_page['label'], + 'page_id' => $search_page['page_id'], + 'description' => $search_page['description'], + 'env_id' => $search_page['env_id'], + 'search_path' => $search_page['search_path'], + 'page_title' => $search_page['page_title'], + 'settings' => serialize($search_page['settings']), + )) + ->execute(); + } +} + + /** + * Function that clones a search page + * + * @param $page_id + * The page identifier it needs to clone. + * + */ +function solrsearch_search_page_clone($page_id) { + $search_page = solrsearch_search_page_load($page_id); + // Get all search_pages + $search_pages = solrsearch_search_load_all_search_pages(); + // Create an unique ID + $new_search_page_id = solrsearch_create_unique_id($search_pages, $search_page['page_id']); + // Set this id to the new search page + $search_page['page_id'] = $new_search_page_id; + $search_page['label'] = $search_page['label'] . ' [cloned]'; + // All cloned search pages should be removable + if (isset($search_page['settings']['solrsearch_search_not_removable'])) { + unset($search_page['settings']['solrsearch_search_not_removable']); + } + // Save our new search page in the database + solrsearch_search_page_save($search_page); +} + +/** + * Implements hook_block_info(). + */ +function solrsearch_search_block_info() { + // Get all of the moreLikeThis blocks that the user has created + $blocks = solrsearch_search_load_all_mlt_blocks(); + foreach ($blocks as $delta => $settings) { + $blocks[$delta] += array('info' => t('Solr Search recommendations: !name', array('!name' => $settings['name'])) , 'cache' => DRUPAL_CACHE_PER_PAGE); + } + // Add the sort block. + $blocks['sort'] = array( + 'info' => t('Solr Search Core: Sorting'), + 'cache' => DRUPAL_CACHE_CUSTOM, + ); + + $blocks['solrsearch'] = array( + 'info' => t('Solr Search Core: Search'), + 'cache' => DRUPAL_CACHE_CUSTOM, + ); + + + $blocks['solrsearch_author'] = array( + 'info' => t('Solr Search Core: Search - author'), + 'cache' => DRUPAL_CACHE_CUSTOM, + ); + + $blocks['solrsearch_title'] = array( + 'info' => t('Solr Search Core: Search - title'), + 'cache' => DRUPAL_CACHE_CUSTOM, + ); + + + + return $blocks; +} + +/** + * Implements hook_block_view(). + */ +function solrsearch_search_block_view($delta = '') { + + if ($delta == 'sort') { + $environments = solrsearch_load_all_environments(); + foreach ($environments as $env_id => $environment) { + if (solrsearch_has_searched($env_id) && !solrsearch_suppress_blocks($env_id) && $delta == 'sort') { + $response = NULL; + $query = solrsearch_current_query($env_id); + $solrsort = NULL; + if ($query) { + // Get the query and response. Without these no blocks make sense. + $response = solrsearch_static_response_cache($query->getSearcher()); + } + + // If there are less than two results, do not return the sort block + if (empty($response) || ($response->response->numFound < 2)) { + return NULL; + } + + // Check if we have to return a cached version of this block + if ($query) { + // Get the URI without any query parameter. + $uri = parse_url(request_uri()); + // Get the current sort as an array. + $solrsort = $query->getSolrsort(); + $cache_id = $uri['path'] . ':' . implode(':', $solrsort); + // Do we have something in cache ? + if ($cache = cache_get($cache_id, 'cache_block')) { + $block = $cache->data; + return $block; + } + } + + $sorts = $query->getAvailableSorts(); + $sort_links = array(); + $path = $query->getPath(); + $new_query = clone $query; + $toggle = array('asc' => 'desc', 'desc' => 'asc'); + foreach ($sorts as $name => $sort) { + $active = $solrsort['#name'] == $name; + if ($name == 'score') { + $direction = ''; + $new_direction = 'desc'; // We only sort by descending score. + } + elseif ($active) { + $direction = $toggle[$solrsort['#direction']]; + $new_direction = $toggle[$solrsort['#direction']]; + } + else { + $direction = ''; + $new_direction = $sort['default']; + } + $new_query->setSolrsort($name, $new_direction); + $sort_links[$name] = array( + 'text' => $sort['title'], + 'path' => $path, + 'options' => array('query' => $new_query->getSolrsortUrlQuery()), + 'active' => $active, + 'direction' => $direction, + ); + } + foreach ($sort_links as $name => $link) { + $themed_links[$name] = theme('solrsearch_sort_link', $link); + } + $block = array( + 'subject' => t('Sort by'), + 'content' => theme('solrsearch_sort_list', array('items' => $themed_links)) + ); + // Cache the block + cache_set($cache_id, $block, 'cache_block'); + return $block; + } + } + } + elseif ($delta == 'solrsearch') { + + return solrsearch_search_block(); + } + + elseif($delta == 'solrsearch_author'){ + + return solrsearch_search_author_block(); + } + + elseif($delta == 'solrsearch_title'){ + + return solrsearch_search_title_block(); + } + + + + elseif (($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) { + $suggestions = array(); + // Determine whether the user can view the current node. Probably not necessary. + $block = solrsearch_search_mlt_block_load($delta); + if ($block && node_access('view', $node)) { + // Get our specific environment for the MLT block + $env_id = (!empty($block['mlt_env_id'])) ? $block['mlt_env_id'] : ''; + try { + $solr = solrsearch_get_solr($env_id); + $context['search_type'] = 'solrsearch_search_mlt'; + $context['block_id'] = $delta; + $docs = solrsearch_search_mlt_suggestions($block, solrsearch_document_id($node->nid), $solr, $context); + if (!empty($docs)) { + $suggestions['subject'] = check_plain($block['name']); + $suggestions['content'] = array( + '#theme' => 'solrsearch_search_mlt_recommendation_block', + '#docs' => $docs, + '#delta' => $delta + ); + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + } + return $suggestions; + } +} + +/** + * Implements hook_form_[form_id]_alter(). + */ +function solrsearch_search_form_block_admin_display_form_alter(&$form) { + foreach ($form['blocks'] as $key => $block) { + if ((strpos($key, "solrsearch_search_mlt-") === 0) && $block['module']['#value'] == 'solrsearch_search') { + $form['blocks'][$key]['delete'] = array( + '#type' => 'link', + '#title' => 'delete', + '#href' => 'admin/config/search/solrsearch/search-pages/block/' . $block['delta']['#value'] . '/delete', + ); + } + } +} + +/** + * Implements hook_block_configure(). + */ +function solrsearch_search_block_configure($delta = '') { + if ($delta != 'sort') { + require_once(drupal_get_path('module', 'solrsearch') . '/solrsearch_search.admin.inc'); + return solrsearch_search_mlt_block_form($delta); + } +} + +/** + * Implements hook_block_save(). + */ +function solrsearch_search_block_save($delta = '', $edit = array()) { + if ($delta != 'sort') { + require_once(drupal_get_path('module', 'solrsearch') . '/solrsearch_search.admin.inc'); + solrsearch_search_mlt_save_block($edit, $delta); + } +} + + +/** + * Return all the saved search pages + * @return array $search_pages + * Array of all search pages + */ +function solrsearch_search_load_all_search_pages() { + $search_pages = &drupal_static(__FUNCTION__, array()); + //dpm("solrsearch_search_load_all_search_pages"); + if (!empty($search_pages)) { + return $search_pages; + } + + // If ctools module is enabled, add search pages from code, e.g. from a + // feature module. + if (module_exists('ctools')) { + ctools_include('export'); + $defaults = ctools_export_load_object('solrsearch_search_page', 'all'); + foreach ($defaults as $page_id => $default) { + $search_pages[$page_id] = (array) $default; + } + } + + // Get all search_pages and their id + $search_pages_db = db_query('SELECT * FROM {solrsearch_search_page}')->fetchAllAssoc('page_id', PDO::FETCH_ASSOC); + //$search_pages_db = db_query('SELECT * FROM {apachesolr_search_page}')->fetchAllAssoc('page_id', PDO::FETCH_ASSOC); + //dpm($search_pages); + //dpm("QUERY"); + $search_pages = $search_pages + $search_pages_db; + + // Ensure that the core search page uses the default environment. In some + // instances, for example when unit testing, this search page isn't defined. + if (isset($search_pages['core_solr_search'])) { + $search_pages['core_solr_search']['env_id'] = solrsearch_default_environment(); + } + + // convert settings to an array + foreach ($search_pages as $id => $search_page) { + if (is_string($search_pages[$id]['settings'])) { + $search_pages[$id]['settings'] = unserialize($search_pages[$id]['settings']); + // Prevent false outcomes for the following search page + $settings = 0; + } + } + //dpm($search_pages); + //dpm("QUERY II"); + return $search_pages; +} + +function solrsearch_search_load_all_mlt_blocks() { + $search_blocks = variable_get('solrsearch_search_mlt_blocks', array()); + return $search_blocks; +} + +function solrsearch_search_mlt_block_load($block_id) { + $search_blocks = variable_get('solrsearch_search_mlt_blocks', array()); + return isset($search_blocks[$block_id]) ? $search_blocks[$block_id] : FALSE; +} + +/** + * Performs a moreLikeThis query using the settings and retrieves documents. + * + * @param $settings + * An array of settings. + * @param $id + * The Solr ID of the document for which you want related content. + * For a node that is solrsearch_document_id($node->nid) + * @param $solr + * The solr environment you want to query against + * + * @return An array of response documents, or NULL + */ +function solrsearch_search_mlt_suggestions($settings, $id, $solr = NULL, $context = array()) { + + try { + $fields = array( + 'mlt_mintf' => 'mlt.mintf', + 'mlt_mindf' => 'mlt.mindf', + 'mlt_minwl' => 'mlt.minwl', + 'mlt_maxwl' => 'mlt.maxwl', + 'mlt_maxqt' => 'mlt.maxqt', + 'mlt_boost' => 'mlt.boost', + 'mlt_qf' => 'mlt.qf', + ); + $params = array( + 'q' => 'id:' . $id, + 'qt' => 'mlt', + 'fl' => array('entity_id', 'entity_type', 'label', 'path', 'url'), + 'mlt.fl' => $settings['mlt_fl'], + 'start' => 0, + 'rows' => $settings['num_results'], + ); + // We can optionally specify a Solr object. + $query = solrsearch_drupal_query('solrsearch_mlt', $params, '', '', $solr, $context); + + foreach ($fields as $form_key => $name) { + if (!empty($settings[$form_key])) { + $query->addParam($name, $settings[$form_key]); + } + } + + $type_filters = array(); + if (is_array($settings['mlt_type_filters']) && !empty($settings['mlt_type_filters'])) { + $query->addFilter('bundle', '(' . implode(' OR ', $settings['mlt_type_filters']) . ') '); + } + + if ($custom_filters = $settings['mlt_custom_filters']) { + // @todo - fix the settings form to take a comma-delimited set of filters. + $query->addFilter('', $custom_filters); + } + + // This hook allows modules to modify the query object. + drupal_alter('solrsearch_query', $query); + if ($query->abort_search) { + return NULL; + } + + $response = $query->search(); + + if (isset($response->response->docs)) { + return (array) $response->response->docs; + } + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } +} + +function theme_solrsearch_search_mlt_recommendation_block($vars) { + $docs = $vars['docs']; + $links = array(); + foreach ($docs as $result) { + // Suitable for single-site mode. Label is already safe. + $links[] = l($result->label, $result->path, array('html' => TRUE)); + } + $links = array( + '#theme' => 'item_list', + '#items' => $links, + ); + return render($links); +} + +/** + * Implements hook_search_info(). + */ +function UNUSED_solrsearch_search_search_info() { + // Load our core search page + // This core search page is assumed to always be there. It cannot be deleted. + $search_page = solrsearch_search_page_load('core_solr_search'); + + // This can happen during install, or if the DB was manually changed. + if (empty($search_page)) { + $search_page = array(); + $search_page['page_title'] = 'echo'; + $search_page['search_path'] = 'solrsearch/site'; + } + + return array( + 'title' => $search_page['page_title'], + 'path' => str_replace('solrsearch/', '', $search_page['search_path']), + 'conditions_callback' => variable_get('solrsearch_search_conditions_callback', 'solrsearch_search_conditions'), + ); +} + +/** + * Implements hook_search_reset(). + */ +function UNUSED_solrsearch_search_search_reset() { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $env_id = solrsearch_default_environment(); + solrsearch_index_mark_for_reindex($env_id); +} + +/** + * Implements hook_search_status(). + */ +function UNUSED_solrsearch_search_search_status() { + module_load_include('inc', 'solrsearch', 'solrsearch.index'); + $env_id = solrsearch_default_environment(); + return solrsearch_index_status($env_id); +} + +/** + * Implements hook_search_execute(). + * @param $keys + * The keys that are available after the path that is defined in + * hook_search_info + * @param $conditions + * Conditions that are coming from solrsearch_search_conditions + */ +function solrsearch_search_search_execute($keys = NULL, $conditions = NULL) { + + $search_page = solrsearch_search_page_load('core_solr_search'); + $results = solrsearch_search_search_results($keys, $conditions, $search_page); + return $results; +} + +/** + * Implementation of a search_view() conditions callback. + */ +function solrsearch_search_conditions() { + //get default conditions from the core_solr_search + $search_page = solrsearch_search_page_load('core_solr_search'); + $conditions = solrsearch_search_conditions_default($search_page); + return $conditions; +} + +/** + * Implements hook_search_page(). + * @param $results + * The results that came from apache solr + */ +function solrsearch_search_search_page($results) { + $search_page = solrsearch_search_page_load('core_solr_search'); + $build = solrsearch_search_search_page_custom($results, $search_page); + return $build; +} + +/** + * Mimics solrsearch_search_search_page() but is used for custom search pages + * We prefer to keep them seperate so we are not dependent from core search + * when someone tries to disable the core search + * @param $results + * The results that came from apache solr + * @param $build + * the build array from where this function was called. Good to append output + * to the build array + * @param $search_page + * the search page that is requesting an output + */ +function solrsearch_search_search_page_custom($results, $search_page, $build = array()) { + if (!empty($search_page['settings']['solrsearch_search_spellcheck'])) { + + // Retrieve suggestion + $suggestions = solrsearch_search_get_search_suggestions($search_page['env_id']); + + if ($search_page && !empty($suggestions)) { + $build['suggestions'] = array( + '#theme' => 'solrsearch_search_suggestions', + '#links' => array(l($suggestions[0], $search_page['search_path'] . '/' . $suggestions[0])), + ); + } + } + // Retrieve expected results from searching + if (!empty($results['solrsearch_search_browse'])) { + // Show facet browsing blocks. + $build['search_results'] = solrsearch_search_page_browse($results['solrsearch_search_browse'], $search_page['env_id']); + + } + elseif ($results) { + + $build['search_results'] = array( + '#theme' => 'solrsearch_results', + '#results' => $results, + '#module' => 'solrsearch_search', + '#search_page' => $search_page, + ); + } + else { + // Give the user some custom help text. + $build['search_results'] = array('#markup' => theme('solrsearch_search_noresults')); + } + + // Allows modules to alter the render array before returning. + drupal_alter('solrsearch_search_page', $build, $search_page); + + return $build; +} + +/** + * Executes search depending on the conditions given. + * See solrsearch_search.pages.inc for another use of this function + */ +function solrsearch_search_search_results($keys = NULL, $conditions = NULL, $search_page = NULL) { + + $params = array(); + $results = array(); + // Process the search form. Note that if there is $_POST data, + // search_form_submit() will cause a redirect to search/[module path]/[keys], + // which will get us back to this page callback. In other words, the search + // form submits with POST but redirects to GET. This way we can keep + // the search query URL clean as a whistle. + if (empty($_POST['form_id']) + || ($_POST['form_id'] != 'solrsearch_search_custom_page_search_form') + && ($_POST['form_id'] != 'search_form') + && ($_POST['form_id'] != 'search_block_form') ) { + + // Check input variables + if (empty($search_page)) { + $search_page = solrsearch_search_page_load('core_solr_search'); + // Verify if it actually loaded + if (empty($search_page)) { + // Something must have been really messed up. + solrsearch_failure(t('Solr search'), $keys); + return array(); + } + } + if (empty($conditions)) { + $conditions = solrsearch_search_conditions_default($search_page); + } + + // Sort options from the conditions array. + // @see solrsearch_search_conditions_default() + // See This condition callback to find out how. + $solrsort = isset($conditions['solrsearch_search_sort']) ? $conditions['solrsearch_search_sort'] : ''; + // What to do when we have an initial empty search + $empty_search_behavior = isset($search_page['settings']['solrsearch_search_browse']) ? $search_page['settings']['solrsearch_search_browse'] : ''; + + try { + + $solr = solrsearch_get_solr($search_page['env_id']); + // Default parameters + $params['fq'] = isset($conditions['fq']) ? $conditions['fq'] : array(); + $params['rows'] = isset($conditions['rows']) ? $conditions['rows'] : $search_page['settings']['solrsearch_search_per_page']; + + if (empty($search_page['settings']['solrsearch_search_spellcheck'])) { + // Spellcheck needs to have a string as false/true + $params['spellcheck'] = 'false'; + } + else { + $params['spellcheck'] = 'true'; + } + + // Empty text Behavior + if (!$keys && !isset($conditions['f']) && ($empty_search_behavior == 'browse' || $empty_search_behavior == 'blocks')) { + // Pass empty search behavior as string on to solrsearch_search_search_page() + // Hardcoded solrsearch name since we rely on this for the facets + + $context['page_id'] = $search_page['page_id']; + $context['search_type'] = 'solrsearch_search_browse'; + solrsearch_search_run_empty('solrsearch', $params, $search_page['search_path'], $solr, $context); + $results['solrsearch_search_browse'] = $empty_search_behavior; + + if ($empty_search_behavior == 'browse') { + // Hide sidebar blocks for content-area browsing instead. + solrsearch_suppress_blocks($search_page['env_id'], TRUE); + } + } + // Full text behavior. Triggers with a text search or a facet + elseif (($keys || isset($conditions['f'])) || ($empty_search_behavior == 'results')) { + + $params['q'] = $keys; + // Hardcoded solrsearch name since we rely on this for the facets + $context['page_id'] = $search_page['page_id']; + $context['search_type'] = 'solrsearch_search_results'; + + $results = solrsearch_search_run('solrsearch', $params, $solrsort, $search_page['search_path'], pager_find_page(), $solr, $context); + + } + } + catch (Exception $e) { + + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + solrsearch_failure(t('Solr search'), $keys); + } + } + return $results; +} + +function solrsearch_search_conditions_default($search_page) { + $conditions = array(); + $search_type = isset($search_page['settings']['solrsearch_search_search_type']) ? $search_page['settings']['solrsearch_search_search_type'] : ''; + $allow_user_input = isset($search_page['settings']['solrsearch_search_allow_user_input']) ? $search_page['settings']['solrsearch_search_allow_user_input'] : FALSE; + $path_replacer = isset($search_page['settings']['solrsearch_search_path_replacer']) ? $search_page['settings']['solrsearch_search_path_replacer'] : ''; + $set_custom_filter = isset($search_page['settings']['solrsearch_search_custom_enable']) ? $search_page['settings']['solrsearch_search_custom_enable'] : ''; + $search_page_fq = !empty($search_page['settings']['fq']) ? $search_page['settings']['fq'] : ''; + + + + + + $conditions['fq'] = array(); + // We only allow this to happen if the search page explicitely allows it + if ($allow_user_input) { + // Get the filterQueries from the url + if (!empty($_GET['fq']) && is_array($_GET['fq'])) { + // Reset the array so that we have one level lower to go through + $conditions['fq'] = $_GET['fq']; + } + foreach($conditions['fq'] as $condition_id => $condition) { + // If the user input does not pass our validation we do not allow + // it to query solr + $test_query = solrsearch_drupal_subquery('Test'); + if (!$test_query->validFilterValue($condition)) { + unset($conditions['fq'][$condition_id]); + } + } + } + + // Custom filters added in search pages + if (!empty($search_page_fq) && !empty($set_custom_filter)) { + if (!empty($path_replacer)) { + // If the manual filter has a % in it, replace it with $value + $conditions['fq'][] = str_replace('%', $path_replacer, $search_page_fq); + } + else { + // Put the complete filter in the filter query + $conditions['fq'][] = $search_page_fq; + } + } + + // Search type filters (such as taxonomy) + if (!empty($path_replacer) && !empty($search_type) && $search_type != 'custom') { + $conditions['fq'][] = $search_type . ':' . $path_replacer; + } + + // We may also have filters added by facet API module. The 'f' + // is determined by variable FacetapiUrlProcessor::$filterKey. Hard + // coded here to avoid extra class loading. + if (!empty($_GET['f']) && is_array($_GET['f'])) { + if (module_exists('facetapi')) { + $conditions['f'] = $_GET['f']; + } + } + // Add the sort from the page to our conditions + $sort = isset($_GET['solrsort']) ? $_GET['solrsort'] : ''; + $conditions['solrsearch_search_sort'] = $sort; + + + //$conditions['fq'][] = "doc-type:indexMeta"; + //$conditions['fq'][] = "doc-type:institutesLibrary"; + $conditions['fq'][] = "doc-type:*"; + + return $conditions; +} + +/** + * Handle browse results for empty searches. + */ +function solrsearch_search_page_browse($empty_search_behavior, $env_id) { + $build = array(); + //dpm("solrsearch_search_page_browse"); + //dpm($empty_search_behavior); + + // Switch in case we come up with new flags. + switch ($empty_search_behavior) { + case 'browse': + drupal_add_js(drupal_get_path('module', 'solrsearch') .'/solrsearch-mpiwg.js'); + + if (module_exists('facetapi') && $query = solrsearch_current_query($env_id)) { + module_load_include('inc', 'facetapi', 'facetapi.block'); + // Get facet render elements. + $searcher = $query->getSearcher(); + $elements = facetapi_build_realm($searcher, 'block'); + $build = array(); + //dpm($searcher); + //dpm($elements); + foreach (element_children($elements) as $key) { + $delta = "facetapi_{$key}"; + // @todo: order/filter these pseudo-blocks according to block.module weight, visibility (see 7.x-1beta4) + $block = new stdClass(); + $block->visibility = TRUE; + $block->enabled = TRUE; + $block->module = 'facetapi'; + $block->subject = theme('facetapi_title', array('title' => $elements[$key]['#title'])); + $build[$delta] = $elements[$key]; + $block->region = NULL; + $block->delta = 'solrsearch-' . $key; + // @todo: the final themed block's div id attribute does not coincide with "real" block's id (see facetapi_get_delta_map()) + $build[$delta]['#block'] = $block; + $build[$delta]['#theme_wrappers'][] = 'block'; + $build['#sorted'] = TRUE; + } + //$build['#theme_wrappers'][] = 'solrsearch_search_browse_blocks'; + } + break; + } + return $build; +} + +/** + * Shows a groups of blocks for starting a search from a filter. + */ +function theme_solrsearch_search_browse_blocks($vars) { + $result = ''; + if ($vars['content']['#children']) { + $result .= "
\n

" . t('Browse available categories') . "

\n"; + $result .= '

' . t('Pick a category to launch a search.') . "

\n"; + $result .= $vars['content']['#children'] . "\n
\n"; + } + + return $result; +} + +/** + * Execute a search with zero results rows so as to populate facets. + */ +function solrsearch_search_run_empty($name, array $params = array(), $base_path = '', $solr = NULL, $context = array()) { + $query = solrsearch_drupal_query($name, $params, '', $base_path, $solr, $context); + $query->addParam('rows', '0'); + $solr_id = $query->solr('getId'); + list($final_query, $response) = solrsearch_do_query($query); + solrsearch_has_searched($solr_id, TRUE); +} + +/** + * Execute a search results based on keyword, filter, and sort strings. + * + * @param $name + * @param $params + * Array - 'q' is the keywords to search. + * @param $solrsort + * @param $base_path + * For constructing filter and sort links. Leave empty unless the links need to point somewhere + * other than the base path of the current request. + * @param integer $page + * For pagination. + * @param DrupalApacheSolrServiceInterface $solr + * The solr server resource to execute the search on. + * + * @return stdClass $response + * + * @throws Exception + */ +function solrsearch_search_run($name, array $params = array(), $solrsort = '', $base_path = '', $page = 0, DrupalApacheSolrServiceInterface $solr = NULL, $context = array()) { + // Merge the default params into the params sent in. + + + $params += solrsearch_search_basic_params(); + // This is the object that knows about the query coming from the user. + $query = solrsearch_drupal_query($name, $params, $solrsort, $base_path, $solr, $context); + + + + if ($query->getParam('q')) { + solrsearch_search_add_spellcheck_params($query); + } + + // Add the paging parameters + $query->page = $page; + + + //solrsearch_search_add_boost_params($query); + if ($query->getParam('q')) { + solrsearch_search_highlighting_params($query); + if (!$query->getParam('hl.fl')) { + $qf = array(); + //foreach ($query->getParam('qf') as $field) { + // Truncate off any boost so we get the simple field name. + //$parts = explode('^', $field, 2); + //$qf[$parts[0]] = TRUE; + //} + //foreach (array('content', 'ts_comments') as $field) { + //if (isset($qf[$field])) { + //$query->addParam('hl.fl', $field); + //} + //} + } + } + else { + // No highlighting, use the teaser as a snippet. + $query->addParam('fl', 'teaser'); + } + + + + list($final_query, $response) = solrsearch_do_query($query); + + + $env_id = $query->solr('getId'); + solrsearch_has_searched($env_id, TRUE); + $process_response_callback = solrsearch_environment_variable_get($env_id, 'process_response_callback', 'solrsearch_search_process_response'); + + + if (function_exists($process_response_callback)) { + return call_user_func($process_response_callback, $response, $final_query); + } + else { + return solrsearch_search_process_response($response, $final_query); + } +} + +function solrsearch_search_basic_params(DrupalSolrQueryInterface $query = NULL) { + $params = array( + 'fl' => array( + 'id', + 'entity_type', + 'author_c', + 'author', + 'title', + 'title_s', + 'keyword', + 'year', + 'IM_date', + 'IM_signature', + 'IM_call-number', + 'archive-path', + 'doc-type', + 'mpiwg-dri', + 'access-type', + 'content', + ), + 'mm' => 1, + 'rows' => 10, + 'pf' => 'title^2.0 author^1.0', + 'ps' => 15, + 'hl' => 'true', + 'hl.fl' => 'title', + 'hl.snippets' => 3, + 'hl.mergeContigious' => 'true', + 'f.content.hl.alternateField' => 'teaser', + 'f.content.hl.maxAlternateFieldLength' => 256, + ); + if ($query) { + $query->addParams($params); + } + return $params; +} + +/** + * Add highlighting settings to the search params. + * + * These settings are set in solrconfig.xml. + * See the defaults there. + * If you wish to override them, you can via settings.php or drush + */ +function solrsearch_search_highlighting_params(DrupalSolrQueryInterface $query = NULL) { + $params['hl'] = variable_get('solrsearch_hl_active', NULL); + $params['hl.fragsize']= variable_get('solrsearch_hl_textsnippetlength', NULL); + $params['hl.simple.pre'] = variable_get('solrsearch_hl_pretag', NULL); + $params['hl.simple.post'] = variable_get('solrsearch_hl_posttag', NULL); + $params['hl.snippets'] = variable_get('solrsearch_hl_numsnippets', NULL); + // This should be an array of possible field names. + $params['hl.fl'] = variable_get('solrsearch_hl_fieldtohighlight', NULL); + $params = array_filter($params); + if ($query) { + $query->addParams($params); + } + return $params; +} + +function solrsearch_search_add_spellcheck_params(DrupalSolrQueryInterface $query) { + $params = array(); + + // Add new parameter to the search request + //$params['spellcheck.q'] = $query->getParam('q'); + //$params['spellcheck'] = 'true'; + //$query->addParams($params); +} + +function solrsearch_search_add_boost_params(DrupalSolrQueryInterface $query) { + $env_id = $query->solr('getId'); + $params = array(); + + $defaults = array( + 'content' => '1.0', + 'ts_comments' => '0.5', + 'tos_content_extra' => '0.1', + 'label' => '5.0', + 'tos_name' => '3.0', + 'taxonomy_names' => '2.0', + 'tags_h1' => '5.0', + 'tags_h2_h3' => '3.0', + 'tags_h4_h5_h6' => '2.0', + 'tags_inline' => '1.0', + 'tags_a' => '0', + ); + $qf = solrsearch_environment_variable_get($env_id, 'field_bias', $defaults); + $fields = $query->solr('getFields'); + if ($qf && $fields) { + foreach ($fields as $field_name => $field) { + if (!empty($qf[$field_name])) { + $prefix = substr($field_name, 0, 3); + if ($field_name == 'content' || $prefix == 'IM_' || $prefix == 'TT_') { + // Normed fields tend to have a lower score. Multiplying by 40 is + // a rough attempt to bring the score in line with fields that are + // not normed. + $qf[$field_name] *= 40.0; + } + $params['qf'][$field_name] = $field_name . '^' . $qf[$field_name]; + } + } + } + + $date_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_date_boost', '0:0'); + $comment_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_comment_boost', '0:0'); + $changed_settings = solrsearch_environment_variable_get($env_id, 'solrsearch_search_changed_boost', '0:0'); + $sticky_boost = solrsearch_environment_variable_get($env_id, 'solrsearch_search_sticky_boost', '0'); + $promote_boost = solrsearch_environment_variable_get($env_id, 'solrsearch_search_promote_boost', '0'); + // For the boost functions for the created timestamp, etc we use the + // standard date-biasing function, as suggested (but steeper) at + // http://wiki.apache.org/solr/SolrRelevancyFAQ#How_can_I_boost_the_score_of_newer_documents + // ms() returns the time difference in ms between now and the date + // The function is thus: $ab/(ms(NOW,date)*$steepness + $ab). + list($date_steepness, $date_boost) = explode(':', $date_settings); + if ($date_boost) { + $ab = 4 / $date_steepness; + $params['bf'][] = "recip(ms(NOW,ds_created),3.16e-11,$ab,$ab)^$date_boost"; + } + // Boost on comment count. + list($comment_steepness, $comment_boost) = explode(':', $comment_settings); + if ($comment_boost) { + $params['bf'][] = "recip(div(1,max(is_comment_count,1)),$comment_steepness,10,10)^$comment_boost"; + } + // Boost for a more recent comment or node edit. + list($changed_steepness, $changed_boost) = explode(':', $changed_settings); + if ($changed_boost) { + $ab = 4 / $changed_steepness; + $params['bf'][] = "recip(ms(NOW,ds_created),3.16e-11,$ab,$ab)^$changed_boost"; + } + // Boost for nodes with sticky bit set. + if ($sticky_boost) { + $params['bq'][] = "bs_sticky:true^$sticky_boost"; + } + // Boost for nodes with promoted bit set. + if ($promote_boost) { + $params['bq'][] = "bs_promote:true^$promote_boost"; + } + // Modify the weight of results according to the node types. + $type_boosts = solrsearch_environment_variable_get($env_id, 'solrsearch_search_type_boosts', array()); + if (!empty($type_boosts)) { + foreach ($type_boosts as $type => $boost) { + // Only add a param if the boost is != 0 (i.e. > "Normal"). + if ($boost) { + $params['bq'][] = "bundle:$type^$boost"; + } + } + } + $query->addParams($params); + +} + +function solrsearch_search_process_response($response, DrupalSolrQueryInterface $query) { + $results = array(); + // We default to getting snippets from the body content and comments. + $hl_fl = $query->getParam('hl.fl'); + if (!$hl_fl) { + //TODO: make highlighitn configurabel + $hl_fl = array('title','author'); + } + $total = $response->response->numFound; + + + pager_default_initialize($total, $query->getParam('rows')); + if ($total > 0) { + $fl = $query->getParam('fl'); + // 'id' and 'entity_type' are the only required fields in the schema, and + // 'score' is generated by solr. + + + foreach ($response->response->docs as $doc) { + $extra = array(); + // Allow modules to alter each document and its extra information. + drupal_alter('solrsearch_search_result', $doc, $extra, $query); + + // Start with an empty snippets array. + $snippets = array(); + + //TODO mappe das irgendwo allgemein? + $doc->id=$doc->{'archive-path'}; + + // Find the nicest available snippet. + foreach ($hl_fl as $hl_param) { + if (isset($response->highlighting->{$doc->id}->$hl_param)) { + // Merge arrays preserving keys. + foreach ($response->highlighting->{$doc->id}->$hl_param as $value) { + $snippets[$hl_param][] = $value; + } + } + } + // If there's no snippet at this point, add the teaser. + if (!$snippets) { + if (isset($doc->teaser)) { + $snippets[] = truncate_utf8($doc->teaser, 256, TRUE); + } + } + + + $hook = 'solrsearch_search_snippets__' . $doc->{'doc-type'}[0]; + /*$bundle = !empty($doc->bundle) ? $doc->bundle : NULL; + if ($bundle) { + $hook .= '__' . $bundle; + }*/ + $snippet = theme($hook, array('doc' => $doc, 'snippets' => $snippets)); + + if (!isset($doc->content)) { + $doc->content = $snippet; + } + + /* + // Normalize common dates so that we can use Drupal's normal date and + // time handling. + if (isset($doc->ds_created)) { + $doc->created = strtotime($doc->ds_created); + } + else { + $doc->created = NULL; + } + + if (isset($doc->ds_changed)) { + $doc->changed = strtotime($doc->ds_changed); + } + else { + $doc->changed = NULL; + } + + if (isset($doc->tos_name)) { + $doc->name = $doc->tos_name; + } + else { + $doc->name = NULL; + } + */ + // Set all expected fields from fl to NULL if they are missing so + // as to prevent Notice: Undefined property. + $fl = array_merge($fl, array('path', 'label', 'score')); + foreach ($fl as $field) { + if (!isset($doc->{$field})) { + $doc->{$field} = NULL; + } + } + + $fields = (array) $doc; + + // a path is not a requirement of entity (see entity_uri() ), so we check if we + // can show it and fallback to the main page of the site if we don't + // have it. + if (!isset($doc->url)) { + $path = ''; + } + else { + $path = $doc->url; + } + + $result = array( + + // template_preprocess_search_result() runs check_plain() on the title + // again. Decode to correct the display. + 'title' => htmlspecialchars_decode($doc->title[0], ENT_QUOTES), + 'author' => htmlspecialchars_decode($doc->author[0], ENT_QUOTES), + // These values are not required by the search module but are provided + // to give entity callbacks and themers more flexibility. + 'score' => $doc->score, + 'snippets' => $snippets, + 'snippet' => $snippet, + 'fields' => $fields, + 'doc-type' => $doc->{'doc-type'}, + 'mpiwg-dri' => $doc->{'mpiwg-dri'}, + 'access-type'=> $doc->{'access-type'}, + 'year' => $doc->{'year'}, + 'archive-path' => $doc->{'archive-path'}, + + ); + + if (isset($doc->{'url'})){ + $result['url']=$doc->{'url'}; + } else { + $result['url']=array(""); + } + + if (isset($doc->{'image'})){ + $result['image']=$doc->{'image'}; + } else { + $result['image']=array(""); + } + + if (isset($doc->{'provider'})){ + $result['provider']=$doc->{'provider'}; + } else { + $result['provider']=array(""); + } + + if (isset($doc->{'dataSource'})){ + $result['dataSource']=$doc->{'dataSource'}; + } else { + $result['dataSource']="mpiwg"; + } + + + + if (is_array($doc->{'content'})){ + $result['content'] = check_plain(implode(" ",$doc->{'content'})); + } else { + $result['content'] = check_plain($doc->{'content'}); + } + + if (isset($doc->{'IM_date'})){ + $result['date']=$doc->{'IM_date'}; + } + + if (isset($doc->{'IM_call-number'})){ + $result['IM_call-number']=$doc->{'IM_call-number'}; + } + + if (isset($doc->{'IM_signature'})){ + $result['signature']=$doc->{'IM_signature'}; + } + + + // Call entity-type-specific callbacks for extra handling. + /*$function = solrsearch_entity_get_callback($doc->{'doc-type'}, 'result callback', ''); + if (is_callable($function)) { + $function($doc, $result, $extra); + } + */ + $result['extra'] = $extra; + + $results[] = $result; + } + } + // Hook to allow modifications of the retrieved results + foreach (module_implements('solrsearch_process_results') as $module) { + $function = $module . '_solrsearch_process_results'; + $function($results, $query); + } + return $results; +} + +/** + * Used as a callback function to generate a title for a node/page depending + * on the input in the configuration screen + * @param integer $search_page_id + * @param integer $value + * @return String + */ +function searchsolr_search_get_value_title($search_page_id = NULL, $value = NULL) { + $page_title = 'Search results for %value'; + if (isset($value) && isset($search_page_id)) { + $search_page = apachesolr_search_page_load($search_page_id); + $page_title = str_replace('%value', '!value', $search_page['page_title']); + $title = $value; + } + return t($page_title, array('!value' => $title)); +} + +/** + * Retrieve all of the suggestions that were given after a certain search + * @return array() + */ +function solrsearch_search_get_search_suggestions($env_id) { + $suggestions_output = array(); + if (solrsearch_has_searched($env_id)) { + $query = solrsearch_current_query($env_id); + $keyword = $query->getParam('q'); + $searcher = $query->getSearcher(); + $response = solrsearch_static_response_cache($searcher); + // Get spellchecker suggestions into an array. + if (!empty($response->spellcheck->suggestions)) { + $suggestions = get_object_vars($response->spellcheck->suggestions); + if ($suggestions) { + $replacements = array(); + // Get the original query and retrieve all words with suggestions. + foreach ($suggestions as $word => $value) { + $replacements[$word] = $value->suggestion[0]; + } + // Replace the keyword with the suggested keyword. + $suggested_keyword = strtr($keyword, $replacements); + // Show only if suggestion is different than current query. + if ($keyword != $suggested_keyword) { + $suggestions_output[] = $suggested_keyword; + } + } + } + } + return $suggestions_output; +} + +/** + * Implements hook_solrsearch_entity_info_alter(). + */ +function solrsearch_search_solrsearch_entity_info_alter(&$entity_info) { + // First set defaults so that we don't need to worry about NULL keys. + foreach (array_keys($entity_info) as $type) { + $entity_info[$type]['result callback'] = ''; + } + // Now set those values that we know. Other modules can do so + // for their own entities if they want. + $entity_info['node']['result callback'] = 'solrsearch_search_node_result'; +} + +/** + * Callback function for node search results. + * + * @param stdClass $doc + * The result document from Apache Solr. + * @param array $result + * The result array for this record to which to add. + */ +function solrsearch_search_node_result($doc, &$result, &$extra) { + $doc->uid = $doc->is_uid; + $result += array( + 'type' => node_type_get_name($doc->bundle), + 'user' => theme('username', array('account' => $doc)), + 'date' => isset($doc->changed) ? $doc->changed : 0, + 'node' => $doc, + 'uid' => $doc->is_uid, + ); + + if (isset($doc->is_comment_count)) { + $extra['comments'] = format_plural($doc->is_comment_count, '1 comment', '@count comments'); + } +} + +/** + * Returns whether a search page exists. + */ +function solrsearch_search_page_exists($search_page_id) { + return db_query('SELECT 1 FROM {solrsearch_search_page} WHERE page_id = :page_id', array(':page_id' => $search_page_id))->fetchField(); +} + +/** + * Template preprocess for solrsearch search results. + * + * We need to add additional entity/bundle-based templates + */ +function solrsearch_search_preprocess_search_result(&$variables) { + // If this search result is coming from our module, we want to improve the + // template potential to make life easier for themers. + + + if ($variables['module'] == 'solrsearch_search') { + $result = $variables['result']; + + //info in display should display the author. + $variables['info']=$result['author']; + if (!empty($result['entity_type'])) { + $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module'] . '__' . $result['entity_type']; + if (!empty($result['bundle'])) { + $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module'] . '__' . $result['entity_type'] . '__' . $result['bundle']; + } + } + } +} + +function solrsearch_search_preprocess_search_results(&$variables) { + // Initialize variables + $env_id = NULL; + + // If this is a solr search, expose more data to themes to play with. + if ($variables['module'] == 'solrsearch_search') { + // Fetch our current query + if (!empty($variables['search_page']['env_id'])) { + $env_id = $variables['search_page']['env_id']; + } + $query = solrsearch_current_query($env_id); + + if ($query) { + $variables['query'] = $query; + $variables['response'] = solrsearch_static_response_cache($query->getSearcher()); + } + if (empty($variables['response'])) { + $variables['description'] = ''; + return NULL; + } + $total = $variables['response']->response->numFound; + $params = $variables['query']->getParams(); + + $variables['description'] = t('Showing items @start through @end of @total.', array( + '@start' => $params['start'] + 1, + '@end' => $params['start'] + $params['rows'] - 1, + '@total' => $total, + )); + // Redefine the pager if it was missing + pager_default_initialize($total, $params['rows']); + $variables['pager'] = theme('pager', array('tags' => NULL)); + + // Add template hints for environments + if (!empty($env_id)) { + $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'] . '__' . $env_id; + // Add template hints for search pages + if (!empty($variables['search_page']['page_id'])) { + $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'] . '__' . $variables['search_page']['page_id']; + // Add template hints for both + $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'] . '__' . $env_id . '__' . $variables['search_page']['page_id']; + } + } + } +} + +/** + * Implements hook_solrsearch_environment_delete(). + */ +function solrsearch_search_solrsearch_environment_delete($server) { + db_update('solrsearch_search_page') + ->fields(array( + 'env_id' => '', + )) + ->condition('env_id', $server['env_id']) + ->execute(); + solrsearch_environment_variable_del($server['env_id'], 'solrsearch_search_show_facets'); + solrsearch_environment_variable_del($server['env_id'], 'solrsearch_search_facet_pages'); + menu_rebuild(); +} + +/*function solrsearch_search_form_search_block_form_alter(&$form, $form_state) { + if (variable_get('search_default_module') == 'solrsearch_search') { + $form['#submit'][] = 'solrsearch_search_form_search_submit'; + } +} +*/ + +/** + * Default theme function for spelling suggestions. + */ +function theme_solrsearch_search_suggestions($variables) { + $output = '
'; + $output .= '
' . t('Did you mean') . '
'; + foreach ((array) $variables['links'] as $link) { + $output .= '
' . $link . '
'; + } + $output .= '
'; + return $output; +} + +/** + * Added form submit function to retain filters. + * + * @see solrsearch_search_form_search_form_alter() + */ +function solrsearch_search_form_search_submit($form, &$form_state) { + $fv = $form_state['values']; + // Replace keys with their rawurlencoded value + if (isset($fv['search_block_form'])) { + $raw_keys = str_replace("/","%2f",$fv['search_block_form']); + $form_state['redirect'] = str_replace($fv['search_block_form'], $raw_keys, $form_state['redirect']); + } +} + + +/** + * submit function for the delete_index form. + * + */ +function solrsearch_search_build_spellcheck($form, &$form_state) { + try { + $solr = solrsearch_get_solr(); + $params['spellcheck'] = 'true'; + $params['spellcheck.build'] = 'true'; + $response = $solr->search('solr', 0, 0, $params); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } +} + +/** + * Implements hook_form_[form_id]_alter(). + * + * Adds settings to show facet blocks on non-search pages. + */ +function solrsearch_search_form_facetapi_realm_settings_form_alter(&$form, &$form_state) { + + if ('solrsearch' == $form['#facetapi']['adapter']->getId() && 'block' == $form['#facetapi']['realm']['name']) { + // Gets the environment ID from the searcher, stores in #facetapi property. + $env_id = ltrim(strstr($form['#facetapi']['adapter']->getSearcher(), '@'), '@'); + + $show_facets = solrsearch_environment_variable_get($env_id, 'solrsearch_search_show_facets', 0); + $facet_pages = solrsearch_environment_variable_get($env_id, 'solrsearch_search_facet_pages', ''); + + $form['#facetapi']['env_id'] = $env_id; + + $form['solrsearch_search_show_facets'] = array( + '#type' => 'checkbox', + '#title' => t('Show facets on non-search pages.'), + '#default_value' => $show_facets, + '#weight' => '-10', + ); + + $form['solrsearch_search_facet_pages'] = array( + '#title' => t('Non-search paths'), + '#type' => 'textarea', + '#default_value' => $facet_pages, + '#weight' => '-10', + '#dependency' => array( + 'edit-solrsearch-search-show-facets' => array(1), + ), + ); + + $form['#submit'][] = 'solrsearch_search_facetapi_realm_settings_form_submit'; + } +} + +/** + * Form submission handler for facetapi_realm_settings_form(). + */ +function solrsearch_search_facetapi_realm_settings_form_submit(&$form, &$form_state) { + $env_id = $form['#facetapi']['env_id']; + + // Adds the settings to the array keyed by environment ID, saves variables. + $show_facets = $form_state['values']['solrsearch_search_show_facets']; + $facet_pages = $form_state['values']['solrsearch_search_facet_pages']; + if ($show_facets) { + solrsearch_environment_variable_set($env_id, 'solrsearch_search_show_facets', $show_facets); + } + else { + // Due to performance reasons, we delete it from the vars so that our init + // process can react on environments that hae it set and not unset. + // See solrsearch_search_init(). + solrsearch_environment_variable_del($env_id, 'solrsearch_search_show_facets'); + } + solrsearch_environment_variable_set($env_id, 'solrsearch_search_facet_pages', $facet_pages); +} + +/** + * Implements hook_theme(). + */ +function solrsearch_search_theme() { + return array( + /** + * Shows the facets in blocks in the search result area + */ + 'solrsearch_search_browse_blocks' => array( + 'render element' => 'content', + ), + /** + * Shows the search snippet + */ + 'solrsearch_search_snippets' => array( + 'variables' => array('doc' => NULL, 'snippets' => array()), + ), + /** + * Shows a message when the search does not return any result + */ + 'solrsearch_search_noresults' => array( + 'variables' => array(), + ), + /** + * Shows a list of suggestions + */ + 'solrsearch_search_suggestions' => array( + 'variables' => array('links' => NULL), + ), + /** + * Shows a list of results (docs) in content recommendation block + */ + 'solrsearch_search_mlt_recommendation_block' => array( + 'variables' => array('docs' => NULL, 'delta' => NULL), + ), + 'solrsearch_search_block_form' => array( + 'render element' => 'form', + 'template' => 'solrsearch-block-form', + ), + + 'solrsearch_search_author_block_form' => array( + 'render element' => 'form', + 'template' => 'solrsearch-author-block-form', + ), + + 'solrsearch_search_title_block_form' => array( + 'render element' => 'form', + 'template' => 'solrsearch-title-block-form', + ), + ); +} + + + +/** + * Implements hook_theme_registry_alter(). + */ +function solrsearch_search_theme_registry_alter(&$theme_registry) { + + if (isset($theme_registry['search_results'])) { + $theme_registry['search_results']['variables']['search_page'] = NULL; + } +} + +/** + * Theme the highlighted snippet text for a search entry. + * + * @param array $vars + * + */ +function theme_solrsearch_search_snippets($vars) { + $result = ''; + if (is_array($vars['snippets'])) { + $snippets = $vars['snippets']; + if (isset($snippets['content'])) { + $result .= implode(' ... ', $snippets['content']); + unset($snippets['content']); + } + if (isset($snippets['teaser'])) { + $result .= (strlen($result) > 0) ? ' ... ' : ''; + $result .= implode(' ... ', $snippets['teaser']); + unset($snippets['teaser']); + } + if (count($snippets)) { + $result .= (strlen($result) > 0) ? ' ... ' : ''; + foreach ($snippets as $snippet) { + $result .= implode(' ... ', $snippet); + } + } + } + return $result . ' ...'; +} + +/** + * Brief message to display when no results match the query. + * + * @see search_help() + */ +function theme_solrsearch_search_noresults() { + return t('
    +
  • Check if your spelling is correct, or try removing filters.
  • +
  • Remove quotes around phrases to match each word individually: "blue drop" will match less than blue drop.
  • +
  • You can require or exclude terms using + and -: big +blue drop will require a match on blue while big blue -drop will exclude results that contain drop.
  • +
'); +} + + + + +/** + * Implements hook_forms(). + */ +function solrsearch_search_forms() { + $forms['solrsearch_search_block_form']= array( + 'callback' => 'solrsearch_search_box', + 'callback arguments' => array('solrsearch_search_block_form'), + ); + + $forms['solrsearch_search_author_block_form']= array( + 'callback' => 'solrsearch_search_author_box', + 'callback arguments' => array('solrsearch_search_author_block_form'), + ); + + $forms['solrsearch_search_title_block_form']= array( + 'callback' => 'solrsearch_search_title_box', + 'callback arguments' => array('solrsearch_search_title_block_form'), + ); + return $forms; +} + + + + + + + diff -r 000000000000 -r a2b4f67e73dc solrsearch_search.pages.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search.pages.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,129 @@ + 'container', + '#attributes' => array('class' => array('container-inline')), + ); + $form['basic']['keys'] = array( + '#type' => 'textfield', + '#title' => t('Enter terms'), + '#default_value' => $keys, + '#size' => 20, + '#maxlength' => 255, + ); + $form['basic']['submit'] = array( + '#type' => 'submit', + '#value' => t('Search'), + ); + + $form['basic']['get'] = array( + '#type' => 'hidden', + '#default_value' => json_encode(array_diff_key($_GET, array('q' => 1, 'page' => 1, 'solrsort' => 1, 'retain-filters' => 1))), + ); + + $fq = NULL; + + if (solrsearch_has_searched($search_page['env_id'])) { + $query = solrsearch_current_query($search_page['env_id']); + // We use the presence of filter query params as a flag for the retain filters checkbox. + $fq = $query->getParam('fq'); + } + + if ($fq || isset($form_state['input']['retain-filters'])) { + $form['basic']['retain-filters'] = array( + '#type' => 'checkbox', + '#title' => t('Retain current filters'), + '#default_value' => (int) !empty($_GET['retain-filters']), + ); + } + + return $form; +} + +/** + * Processes solrsearch_search_custom_page_search_form submissions. + */ +function UNUSED_solrsearch_search_custom_page_search_form_submit(&$form, &$form_state) { + $search_page = $form['#search_page']; + $redirect = $search_page['search_path']; + + // Also encode slashes so we don't get akward situations when obtaining the + // search key. We can't use drupal_encode_path because for "aestetic" reasons + // they don't encode slashes... + $redirect_value = rawurlencode($form_state['values']['keys']); + + if (strlen($form_state['values']['keys'])) { + $redirect .= '/' . $redirect_value; + } + + $get = array(); + if (isset($form_state['values']['get'])) { + $get = json_decode($form_state['values']['get'], TRUE); + } + if (!empty($form_state['values']['retain-filters'])) { + // Add our saved values + $get['retain-filters'] = '1'; + } + else { + // Remove all filters + if (!empty($search_page['settings']['solrsearch_search_allow_user_input'])) { + unset($get['fq']); + } + if (module_exists('facetapi')) { + unset($get['f']); + } + } + + // Add the query values into the redirect. + $form_state['redirect'] = array($redirect, array('query' => $get)); +} diff -r 000000000000 -r a2b4f67e73dc solrsearch_search_author_block.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search_author_block.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,93 @@ + 'textfield', + '#title' => t('Search'), + '#title_display' => 'invisible', + '#size' => 15, + '#default_value' => '', + '#attributes' => array('title' => t('Enter the terms you wish to search for.')), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search')); + $form['#submit'][] = 'solrsearch_search_author_box_form_submit'; + + return $form; +} + +/** + * Process a block search form submission. + */ +function solrsearch_search_author_box_form_submit($form, &$form_state) { + // The search form relies on control of the redirect destination for its + // functionality, so we override any static destination set in the request, + // for example by drupal_access_denied() or drupal_not_found() + // (see http://drupal.org/node/292565). + if (isset($_GET['destination'])) { + unset($_GET['destination']); + } + + // Check to see if the form was submitted empty. + // If it is empty, display an error message. + // (This method is used instead of setting #required to TRUE for this field + // because that results in a confusing error message. It would say a plain + // "field is required" because the search keywords field has no title. + // The error message would also complain about a missing #title field.) + if ($form_state['values']['solrsearch_search_author_block_form'] == '') { + form_set_error('keys', t('Please enter some keywords.')); + } + + $form_id = $form['form_id']['#value']; + $info = search_get_default_module_info(); + if ($info) { + $form_state['redirect'] = 'solrsearch/' . $info['path'] . '/IM_author:' . trim($form_state['values'][$form_id]); + } + else { + form_set_error(NULL, t('Search is currently disabled.'), 'error'); + } +} + +/** + * Process variables for search-block-form.tpl.php. + * + * The $variables array contains the following arguments: + * - $form + * + * @see search-block-form.tpl.php + */ +function template_preprocess_solrsearch_search_author_block_form(&$variables) { + $variables['search'] = array(); + $hidden = array(); + // Provide variables named after form keys so themers can print each element independently. + foreach (element_children($variables['form']) as $key) { + $type = $variables['form'][$key]['#type']; + if ($type == 'hidden' || $type == 'token') { + $hidden[] = drupal_render($variables['form'][$key]); + } + else { + $variables['search'][$key] = drupal_render($variables['form'][$key]); + } + } + // Hidden form elements have no value to themers. No need for separation. + $variables['search']['hidden'] = implode($hidden); + // Collect all form elements to make it easier to print the whole form. + $variables['solrsearch_search_form'] = implode($variables['search']); +} + + + +function solrsearch_search_author_block(){ + + $block['content'] = drupal_get_form('solrsearch_search_author_block_form'); + return $block; + + +} \ No newline at end of file diff -r 000000000000 -r a2b4f67e73dc solrsearch_search_blocks.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search_blocks.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,94 @@ + 'textfield', + '#title' => t('Search'), + '#title_display' => 'invisible', + '#size' => 15, + '#default_value' => '', + '#attributes' => array('title' => t('Enter the terms you wish to search for.')), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search')); + $form['#submit'][] = 'solrsearch_search_box_form_submit'; + + return $form; +} + +/** + * Process a block search form submission. + */ +function solrsearch_search_box_form_submit($form, &$form_state) { + // The search form relies on control of the redirect destination for its + // functionality, so we override any static destination set in the request, + // for example by drupal_access_denied() or drupal_not_found() + // (see http://drupal.org/node/292565). + if (isset($_GET['destination'])) { + unset($_GET['destination']); + } + + // Check to see if the form was submitted empty. + // If it is empty, display an error message. + // (This method is used instead of setting #required to TRUE for this field + // because that results in a confusing error message. It would say a plain + // "field is required" because the search keywords field has no title. + // The error message would also complain about a missing #title field.) + if ($form_state['values']['solrsearch_search_block_form'] == '') { + form_set_error('keys', t('Please enter some keywords.')); + } + + $form_id = $form['form_id']['#value']; + $info = search_get_default_module_info(); + if ($info) { + $form_state['redirect'] = 'solrsearch/' . $info['path'] . '/' . trim($form_state['values'][$form_id]); + } + else { + form_set_error(NULL, t('Search is currently disabled.'), 'error'); + } +} + + + + +function solrsearch_search_block(){ + + $block['content'] = drupal_get_form('solrsearch_search_block_form'); + return $block; + + +} diff -r 000000000000 -r a2b4f67e73dc solrsearch_search_title_block.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_search_title_block.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,93 @@ + 'textfield', + '#title' => t('Search'), + '#title_display' => 'invisible', + '#size' => 15, + '#default_value' => '', + '#attributes' => array('title' => t('Enter the terms you wish to search for.')), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search')); + $form['#submit'][] = 'solrsearch_search_title_box_form_submit'; + + return $form; +} + +/** + * Process a block search form submission. + */ +function solrsearch_search_title_box_form_submit($form, &$form_state) { + // The search form relies on control of the redirect destination for its + // functionality, so we override any static destination set in the request, + // for example by drupal_access_denied() or drupal_not_found() + // (see http://drupal.org/node/292565). + if (isset($_GET['destination'])) { + unset($_GET['destination']); + } + + // Check to see if the form was submitted empty. + // If it is empty, display an error message. + // (This method is used instead of setting #required to TRUE for this field + // because that results in a confusing error message. It would say a plain + // "field is required" because the search keywords field has no title. + // The error message would also complain about a missing #title field.) + if ($form_state['values']['solrsearch_search_title_block_form'] == '') { + form_set_error('keys', t('Please enter some keywords.')); + } + + $form_id = $form['form_id']['#value']; + $info = search_get_default_module_info(); + if ($info) { + $form_state['redirect'] = 'solrsearch/' . $info['path'] . '/IM_title:' . trim($form_state['values'][$form_id]); + } + else { + form_set_error(NULL, t('Search is currently disabled.'), 'error'); + } +} + +/** + * Process variables for search-block-form.tpl.php. + * + * The $variables array contains the following arguments: + * - $form + * + * @see search-block-form.tpl.php + */ +function template_preprocess_solrsearch_search_title_block_form(&$variables) { + $variables['search'] = array(); + $hidden = array(); + // Provide variables named after form keys so themers can print each element independently. + foreach (element_children($variables['form']) as $key) { + $type = $variables['form'][$key]['#type']; + if ($type == 'hidden' || $type == 'token') { + $hidden[] = drupal_render($variables['form'][$key]); + } + else { + $variables['search'][$key] = drupal_render($variables['form'][$key]); + } + } + // Hidden form elements have no value to themers. No need for separation. + $variables['search']['hidden'] = implode($hidden); + // Collect all form elements to make it easier to print the whole form. + $variables['solrsearch_search_form'] = implode($variables['search']); +} + + + +function solrsearch_search_title_block(){ + + $block['content'] = drupal_get_form('solrsearch_search_title_block_form'); + return $block; + + +} \ No newline at end of file diff -r 000000000000 -r a2b4f67e73dc solrsearch_terms.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/solrsearch_terms.inc Mon Jun 08 10:21:54 2015 +0200 @@ -0,0 +1,111 @@ +data); + $authorsAll=$data['terms'][$field]; + return $authorsAll; +} + +function solrsearch_load_data($field,$letter,$accessType="free"){ + #todo cache this! + $env = solrsearch_environment_load('echosearch'); + // $searchStr = "/select?q=". $field. ":". $letter . "*%20and%20doc-type:indexMeta%20&json.nl=map&facet.sort=index&facet.mincount=1&wt=json&facet=true&facet.field=" . $field . "&facet.limit=100000&start=0&rows=9"; + $searchStr = "/select?q=". $field. ":". $letter . "*&json.nl=map&facet.sort=index&facet.mincount=1&wt=json&facet=true&facet.field=" . $field . "&facet.limit=100000&start=0&rows=9"; + + + if ($accessType=="free") + { + $searchStr .="&fq=access-type:free"; + } + + $url = $env['url']; + $result = drupal_http_request($url . $searchStr); + $data = drupal_json_decode($result->data); + #return $data; + + $resultsAll=$data['facet_counts']['facet_fields'][$field]; + + #result list ist zu lang, da hier alle werke gefunden werden, bei denene ein Autor mit dem Buchstaben beginnt. + $res=array(); + foreach ($resultsAll as $author => $val){ + if (drupal_substr($author, 0,1) == $letter){ + $res[$author]=$val; + } + + } + + return $res; +} + +# + +function solrsearch_term_select_field(){ + $query = $_GET; + if (isset($query['browseField'])){ + drupal_goto("solrsearch-terms/" .$query['browseField'] ); + } + +} +function solrsearch_term_list($field="",$letter="A",$numperpage=20){ + + if ($field==""){ + return ''; + } + + + + #field z.b. author_c + + if (!user_access("view restricted content")){ + $accessType="free"; + } else { + $accessType=""; + } + + $authorsAll=solrsearch_load_data($field,$letter,$accessType); + $cnt=count($authorsAll); + + $page = pager_find_page(); + $offset = $numperpage * $page; + $authors = array_slice($authorsAll, $offset,$slice_lenght=$numperpage); + + pager_default_initialize($cnt, 10); + + $rs = theme('solrsearch_term_selection_form',array('field' => $field)); + $rs .=theme('pager', array('tags' => array('<<','<','..','>','>>'),'quantity' => 3)); + + if ($field=='author_c'){ + $rs .= theme('solrsearch_term_list_author',array('authors' => $authors,'cnt' => $cnt,'letter' => $letter)); + } else { + $rs .= theme('solrsearch_term_list_title',array('titles' => $authors,'cnt' => $cnt,'letter' => $letter)); + } + + + $rs .=theme('pager', array('tags' => array('<<','<','..','>','>>'),'quantity' => 3)); + + return $rs; + } + + + function solrsearch_alphapager($field) { + $attributes = array( 'class' => 'alpha-page' ); + $output = ""; + + foreach(range('A', 'Z') as $letter) { + $output .= " +
  • " . l($letter, "solrsearch-terms/" . $field . "/" . $letter, $attributes). "
  • "; + } + + + + return $output; + }