View source
<?php
namespace Drupal\search_api_solr\SolrConnector;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Url;
use Drupal\search_api\Plugin\ConfigurablePluginBase;
use Drupal\search_api\Plugin\PluginFormTrait;
use Drupal\search_api_solr\SearchApiSolrException;
use Drupal\search_api_solr\Solarium\Autocomplete\Query as AutocompleteQuery;
use Drupal\search_api_solr\Solarium\EventDispatcher\Psr14Bridge;
use Drupal\search_api_solr\SolrConnectorInterface;
use Solarium\Client;
use Solarium\Core\Client\Adapter\Curl;
use Solarium\Core\Client\Adapter\Http;
use Solarium\Core\Client\Adapter\TimeoutAwareInterface;
use Solarium\Core\Client\Endpoint;
use Solarium\Core\Client\Request;
use Solarium\Core\Client\Response;
use Solarium\Core\Query\Helper;
use Solarium\Core\Query\QueryInterface;
use Solarium\Exception\HttpException;
use Solarium\QueryType\Extract\Result as ExtractResult;
use Solarium\QueryType\Update\Query\Query as UpdateQuery;
use Solarium\QueryType\Select\Query\Query;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class SolrConnectorPluginBase extends ConfigurablePluginBase implements SolrConnectorInterface, PluginFormInterface {
use PluginFormTrait {
submitConfigurationForm as traitSubmitConfigurationForm;
}
protected $eventDispatcher;
protected $solr;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$plugin->eventDispatcher = new Psr14Bridge();
return $plugin;
}
public function defaultConfiguration() {
return [
'scheme' => 'http',
'host' => 'localhost',
'port' => '8983',
'path' => '/solr',
'core' => '',
'timeout' => 5,
'index_timeout' => 5,
'optimize_timeout' => 10,
'solr_version' => '',
'http_method' => 'AUTO',
'commit_within' => 1000,
];
}
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['scheme'] = array(
'#type' => 'select',
'#title' => $this
->t('HTTP protocol'),
'#description' => $this
->t('The HTTP protocol to use for sending queries.'),
'#default_value' => isset($this->configuration['scheme']) ? $this->configuration['scheme'] : 'http',
'#options' => array(
'http' => 'http',
'https' => 'https',
),
);
$form['host'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Solr host'),
'#description' => $this
->t('The host name or IP of your Solr server, e.g. <code>localhost</code> or <code>www.example.com</code>.'),
'#default_value' => isset($this->configuration['host']) ? $this->configuration['host'] : '',
'#required' => TRUE,
);
$form['port'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Solr port'),
'#description' => $this
->t('The Jetty example server is at port 8983, while Tomcat uses 8080 by default.'),
'#default_value' => isset($this->configuration['port']) ? $this->configuration['port'] : '',
'#required' => TRUE,
);
$form['path'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Solr path'),
'#description' => $this
->t('The path that identifies the Solr instance to use on the server.'),
'#default_value' => isset($this->configuration['path']) ? $this->configuration['path'] : '',
);
$form['core'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Solr core'),
'#description' => $this
->t('The name that identifies the Solr core to use on the server.'),
'#default_value' => isset($this->configuration['core']) ? $this->configuration['core'] : '',
);
$form['timeout'] = array(
'#type' => 'number',
'#min' => 1,
'#max' => 180,
'#title' => $this
->t('Query timeout'),
'#description' => $this
->t('The timeout in seconds for search queries sent to the Solr server.'),
'#default_value' => isset($this->configuration['timeout']) ? $this->configuration['timeout'] : 5,
'#required' => TRUE,
);
$form['index_timeout'] = array(
'#type' => 'number',
'#min' => 1,
'#max' => 180,
'#title' => $this
->t('Index timeout'),
'#description' => $this
->t('The timeout in seconds for indexing requests to the Solr server.'),
'#default_value' => isset($this->configuration['index_timeout']) ? $this->configuration['index_timeout'] : 5,
'#required' => TRUE,
);
$form['optimize_timeout'] = array(
'#type' => 'number',
'#min' => 1,
'#max' => 180,
'#title' => $this
->t('Optimize timeout'),
'#description' => $this
->t('The timeout in seconds for background index optimization queries on a Solr server.'),
'#default_value' => isset($this->configuration['optimize_timeout']) ? $this->configuration['optimize_timeout'] : 10,
'#required' => TRUE,
);
$form['commit_within'] = array(
'#type' => 'number',
'#min' => 0,
'#title' => $this
->t('Commit within'),
'#description' => $this
->t('The limit in milliseconds within a (soft) commit on Solr is forced after any updating the index in any way. Setting the value to "0" turns off this dynamic enforcement and lets Solr behave like configured solrconf.xml.'),
'#default_value' => isset($this->configuration['commit_within']) ? $this->configuration['commit_within'] : 1000,
'#required' => TRUE,
);
$form['workarounds'] = array(
'#type' => 'fieldset',
'#title' => $this
->t('Connector Workarounds'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['workarounds']['solr_version'] = array(
'#type' => 'select',
'#title' => $this
->t('Solr version override'),
'#description' => $this
->t('Specify the Solr version manually in case it cannot be retrived automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec"'),
'#options' => array(
'' => $this
->t('Determine automatically'),
'4' => '4.x',
'5' => '5.x',
'6' => '6.x',
),
'#default_value' => isset($this->configuration['solr_version']) ? $this->configuration['solr_version'] : '',
);
$form['workarounds']['http_method'] = array(
'#type' => 'select',
'#title' => $this
->t('HTTP method'),
'#description' => $this
->t('The HTTP method to use for sending queries. GET will often fail with larger queries, while POST should not be cached. AUTO will use GET when possible, and POST for queries that are too large.'),
'#default_value' => isset($this->configuration['http_method']) ? $this->configuration['http_method'] : 'AUTO',
'#options' => array(
'AUTO' => $this
->t('AUTO'),
'POST' => 'POST',
'GET' => 'GET',
),
);
return $form;
}
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state
->getValues();
if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) {
$form_state
->setError($form['port'], $this
->t('The port has to be an integer between 0 and 65535.'));
}
if (!empty($values['path']) && strpos($values['path'], '/') !== 0) {
$form_state
->setError($form['path'], $this
->t('If provided the path has to start with "/".'));
}
if (!empty($values['core']) && strpos($values['core'], '/') === 0) {
$form_state
->setError($form['core'], $this
->t('The core must not start with "/".'));
}
if (!$form_state
->hasAnyErrors()) {
$values_copied = $values;
if (!$values_copied['core']) {
$values_copied['core'] = '.';
}
$solr = $this
->createClient($values_copied);
$solr
->createEndpoint($values + [
'key' => 'core',
], TRUE);
try {
$this
->getServerLink();
} catch (\InvalidArgumentException $e) {
foreach ([
'scheme',
'host',
'port',
'path',
'core',
] as $part) {
$form_state
->setError($form[$part], $this
->t('The server link generated from the form values is illegal.'));
}
}
}
}
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state
->getValues();
foreach ($values['workarounds'] as $key => $value) {
$form_state
->setValue($key, $value);
}
$form_state
->unsetValue('workarounds');
$this
->traitSubmitConfigurationForm($form, $form_state);
}
protected function connect() {
if (!$this->solr) {
$configuration = $this->configuration;
if (!$configuration['core']) {
$configuration['core'] = '.';
}
$this->solr = $this
->createClient($configuration);
$this->solr
->createEndpoint($this->configuration + [
'key' => 'core',
], TRUE);
$this
->attachServerEndpoint();
}
}
protected function attachServerEndpoint() {
$this
->connect();
$configuration = $this->configuration;
$configuration['core'] = '.';
$configuration['key'] = 'server';
$this->solr
->createEndpoint($configuration);
}
protected function createClient(array &$configuration) {
$configuration[self::QUERY_TIMEOUT] = $configuration['timeout'] ?? 5;
$adapter = NULL;
if (extension_loaded('curl')) {
$adapter = new Curl($configuration);
}
else {
$adapter = new Http();
$adapter
->setTimeout($configuration[self::QUERY_TIMEOUT]);
}
unset($configuration['timeout']);
return new Client($adapter, $this->eventDispatcher);
}
protected function getServerUri() {
$this
->connect();
$url_path = $this->solr
->getEndpoint()
->getServerUri();
if ($this->configuration['host'] === 'localhost' && !empty($_SERVER['SERVER_NAME'])) {
$url_path = str_replace('localhost', $_SERVER['SERVER_NAME'], $url_path);
}
return $url_path;
}
public function getServerLink() {
$url_path = $this
->getServerUri();
$url = Url::fromUri($url_path);
return Link::fromTextAndUrl($url_path, $url);
}
public function getCoreLink() {
$url_path = $this
->getServerUri() . '#/' . $this->configuration['core'];
$url = Url::fromUri($url_path);
return Link::fromTextAndUrl($url_path, $url);
}
public function getSolrVersion($force_auto_detect = FALSE) {
if (!$force_auto_detect && !empty($this->configuration['solr_version'])) {
$min_version = [
'0',
'0',
'0',
];
$version = implode('.', explode('.', $this->configuration['solr_version']) + $min_version);
return '4.0.0' === $version ? '4.5.0' : $version;
}
$info = [];
try {
$info = $this
->getCoreInfo();
} catch (SearchApiSolrException $e) {
try {
$info = $this
->getServerInfo();
} catch (SearchApiSolrException $e) {
}
}
if (isset($info['lucene']['solr-spec-version'])) {
return $info['lucene']['solr-spec-version'];
}
return '0.0.0';
}
public function getSolrMajorVersion($version = '') {
[
$major,
,
] = explode('.', $version ?: $this
->getSolrVersion());
return $major;
}
public function getSolrBranch($version = '') {
return $this
->getSolrMajorVersion($version) . '.x';
}
public function getLuceneMatchVersion($version = '') {
[
$major,
$minor,
] = explode('.', $version ?: $this
->getSolrVersion());
return $major . '.' . $minor;
}
public function getServerInfo($reset = FALSE) {
$this
->useTimeout();
return $this
->getDataFromHandler('admin/info/system', $reset);
}
public function getCoreInfo($reset = FALSE) {
$this
->useTimeout();
return $this
->getDataFromHandler($this->configuration['core'] . '/admin/system', $reset);
}
public function getLuke() {
$this
->useTimeout();
return $this
->getDataFromHandler($this->configuration['core'] . '/admin/luke', TRUE);
}
public function getSchemaVersionString($reset = FALSE) {
return $this
->getCoreInfo($reset)['core']['schema'];
}
public function getSchemaVersion($reset = FALSE) {
$parts = explode('-', $this
->getSchemaVersionString($reset));
return $parts[1];
}
protected function getDataFromHandler($handler, $reset = FALSE) {
static $previous_calls = [];
$this
->connect();
$state_key = 'search_api_solr.endpoint.data';
$state = \Drupal::state();
$endpoint_data = $state
->get($state_key);
$server_uri = $this
->getServerUri();
if (!isset($previous_calls[$server_uri][$handler]) || !isset($endpoint_data[$server_uri][$handler]) || $reset) {
$previous_calls[$server_uri][$handler] = TRUE;
if (!is_array($endpoint_data) || !isset($endpoint_data[$server_uri][$handler]) || $reset) {
$query = $this->solr
->createApi([
'handler' => $handler,
'version' => Request::API_V1,
]);
$endpoint_data[$server_uri][$handler] = $this
->execute($query)
->getData();
$state
->set($state_key, $endpoint_data);
}
}
return $endpoint_data[$server_uri][$handler];
}
public function pingCore() {
return $this
->doPing();
}
public function pingServer() {
return $this
->doPing([
'handler' => 'admin/info/system',
], 'server');
}
protected function doPing($options = [], $endpoint_name = 'core') {
$this
->connect();
$query = $this->solr
->createPing($options);
try {
$start = microtime(TRUE);
$result = $this->solr
->execute($query, $endpoint_name);
if ($result
->getResponse()
->getStatusCode() == 200) {
return microtime(TRUE) - $start + 1.0E-6;
}
} catch (HttpException $e) {
}
return FALSE;
}
public function getStatsSummary() {
$this
->connect();
$this
->useTimeout();
$summary = array(
'@pending_docs' => '',
'@autocommit_time_seconds' => '',
'@autocommit_time' => '',
'@deletes_by_id' => '',
'@deletes_by_query' => '',
'@deletes_total' => '',
'@schema_version' => '',
'@core_name' => '',
'@index_size' => '',
);
$query = $this->solr
->createPing();
$query
->setResponseWriter(Query::WT_PHPS);
$query
->setHandler('admin/mbeans?stats=true');
$stats = $this
->execute($query)
->getData();
if (!empty($stats)) {
$update_handler_stats = $stats['solr-mbeans']['UPDATEHANDLER']['updateHandler']['stats'];
$summary['@pending_docs'] = (int) $update_handler_stats['docsPending'];
$max_time = (int) $update_handler_stats['autocommit maxTime'];
$summary['@autocommit_time_seconds'] = $max_time / 1000;
$summary['@autocommit_time'] = \Drupal::service('date.formatter')
->formatInterval($max_time / 1000);
$summary['@deletes_by_id'] = (int) $update_handler_stats['deletesById'];
$summary['@deletes_by_query'] = (int) $update_handler_stats['deletesByQuery'];
$summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query'];
$summary['@schema_version'] = $this
->getSchemaVersionString(TRUE);
$summary['@core_name'] = $stats['solr-mbeans']['CORE']['core']['stats']['coreName'];
if (version_compare($this
->getSolrVersion(TRUE), '6.4', '>=')) {
$summary['@index_size'] = $stats['solr-mbeans']['CORE']['core']['stats']['size'];
}
else {
$summary['@index_size'] = $stats['solr-mbeans']['QUERYHANDLER']['/replication']['stats']['indexSize'];
}
}
return $summary;
}
public function coreRestGet($path) {
$this
->useTimeout();
return $this
->restRequest('core', $path);
}
public function coreRestPost($path, $command_json = '') {
$this
->useTimeout(self::INDEX_TIMEOUT);
return $this
->restRequest('core', $path, Request::METHOD_POST, $command_json);
}
public function serverRestGet($path) {
$this
->useTimeout();
return $this
->restRequest('server', $path);
}
public function serverRestPost($path, $command_json = '') {
$this
->useTimeout(self::INDEX_TIMEOUT);
return $this
->restRequest('server', $path, Request::METHOD_POST, $command_json);
}
protected function restRequest($endpoint_key, $path, $method = Request::METHOD_GET, $command_json = '') {
$this
->connect();
$request = new Request();
$request
->setMethod($method);
$request
->addHeader('Accept: application/json');
if (Request::METHOD_POST == $method) {
$request
->addHeader('Content-type: application/json');
$request
->setRawData($command_json);
}
$request
->setHandler($path);
$endpoint = $this->solr
->getEndpoint($endpoint_key);
$response = $this
->executeRequest($request, $endpoint);
$output = Json::decode($response
->getBody());
if (!empty($output['errors'])) {
throw new SearchApiSolrException('Error trying to send a REST request.' . "\nError message(s):" . print_r($output['errors'], TRUE));
}
return $output;
}
public function getUpdateQuery() {
$this
->connect();
return $this->solr
->createUpdate();
}
public function getSelectQuery() {
$this
->connect();
return $this->solr
->createSelect();
}
public function getMoreLikeThisQuery() {
$this
->connect();
return $this->solr
->createMoreLikeThis();
}
public function getTermsQuery() {
$this
->connect();
return $this->solr
->createTerms();
}
public function getAutocompleteQuery() {
$this
->connect();
$this->solr
->registerQueryType('autocomplete', AutocompleteQuery::class);
return $this->solr
->createQuery('autocomplete');
}
public function getQueryHelper(QueryInterface $query = NULL) {
if ($query) {
return $query
->getHelper();
}
return new Helper();
}
public function getExtractQuery() {
$this
->connect();
return $this->solr
->createExtract();
}
protected function customizeRequest() {
$this
->connect();
return $this->solr
->getPlugin('customizerequest');
}
public function search(Query $query, Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
$this
->useTimeout(self::QUERY_TIMEOUT, $endpoint);
if ($this->configuration['http_method'] == 'AUTO') {
$this->solr
->getPlugin('postbigrequest');
}
$request = $this->solr
->createRequest($query);
if ($this->configuration['http_method'] == 'POST') {
$request
->setMethod(Request::METHOD_POST);
}
elseif ($this->configuration['http_method'] == 'GET') {
$request
->setMethod(Request::METHOD_GET);
}
return $this
->executeRequest($request, $endpoint);
}
public function createSearchResult(Query $query, Response $response) {
return $this->solr
->createResult($query, $response);
}
public function update(UpdateQuery $query, Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
$this
->useTimeout(self::INDEX_TIMEOUT, $endpoint);
if ($this->configuration['commit_within']) {
$request = $this
->customizeRequest();
$request
->createCustomization('id')
->setType('param')
->setName('commitWithin')
->setValue($this->configuration['commit_within']);
}
$result = $this
->execute($query, $endpoint);
return $result;
}
public function autocomplete(AutocompleteQuery $query, ?Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint();
}
$this
->useTimeout(self::QUERY_TIMEOUT, $endpoint);
if ($this->configuration['http_method'] === 'AUTO') {
$this->solr
->getPlugin('postbigrequest');
}
return $this
->execute($query, $endpoint);
}
public function execute(QueryInterface $query, Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
try {
return $this->solr
->execute($query, $endpoint);
} catch (HttpException $e) {
$this
->handleHttpException($e, $endpoint);
}
}
public function executeRequest(Request $request, Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
try {
return $this->solr
->executeRequest($request, $endpoint);
} catch (HttpException $e) {
$this
->handleHttpException($e, $endpoint);
}
}
protected function handleHttpException(HttpException $e, Endpoint $endpoint) {
$response_code = $e
->getCode();
switch ($response_code) {
case 404:
$description = $this
->t('not found');
break;
case 401:
case 403:
$description = $this
->t('access denied');
break;
default:
$description = $this
->t('unreachable');
}
throw new SearchApiSolrException($this
->t('Solr endpoint @endpoint @description.', [
'@endpoint' => $endpoint
->getBaseUri(),
'@description' => $description,
]), $response_code, $e);
}
public function optimize(Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
$this
->useTimeout(self::OPTIMIZE_TIMEOUT, $endpoint);
$update_query = $this->solr
->createUpdate();
$update_query
->addOptimize(TRUE, FALSE);
$this
->execute($update_query, $endpoint);
}
public function useTimeout(string $timeout = self::QUERY_TIMEOUT, ?Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint();
}
$adpater = $this->solr
->getAdapter();
if ($adpater instanceof TimeoutAwareInterface && ($seconds = $endpoint
->getOption($timeout))) {
$adpater
->setTimeout($seconds);
}
else {
\Drupal::logger('search_api')
->warning('The function SolrConnectorPluginBase::useTimeout() has no affect because you use a HTTP adapter that is not implementing TimeoutAwareInterface. You need to adjust your SolrConnector accordingly.');
}
}
public function extract(QueryInterface $query) {
$this
->useTimeout(self::INDEX_TIMEOUT);
return $this
->execute($query);
}
public function getContentFromExtractResult(ExtractResult $result, $filepath) {
$response = $result
->getResponse();
$json_data = $response
->getBody();
$array_data = Json::decode($json_data);
return $array_data[$filepath];
}
public function getEndpoint($key = 'core') {
$this
->connect();
return $this->solr
->getEndpoint($key);
}
public function getFile($file = NULL) {
$this
->connect();
$query = $this->solr
->createPing();
$query
->setHandler('admin/file');
$query
->addParam('contentType', 'text/xml;charset=utf-8');
if ($file) {
$query
->addParam('file', $file);
}
return $this
->execute($query)
->getResponse();
}
public function viewSettings() {
return [];
}
public function __sleep() {
unset($this->solr);
return parent::__sleep();
}
}