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\SolrConnectorInterface;
use Solarium\Client;
use Solarium\Core\Client\Endpoint;
use Solarium\Core\Client\Request;
use Solarium\Core\Client\Response;
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 = $container
->get('event_dispatcher');
return $plugin;
}
public function defaultConfiguration() {
return [
'scheme' => 'http',
'host' => 'localhost',
'port' => '8983',
'path' => '/solr',
'core' => '',
'timeout' => 5,
'index_timeout' => 5,
'optimize_timeout' => 10,
'finalize_timeout' => 30,
'solr_version' => '',
'http_method' => 'AUTO',
'commit_within' => 1000,
'jmx' => FALSE,
];
}
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['scheme'] = [
'#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' => [
'http' => 'http',
'https' => 'https',
],
];
$form['host'] = [
'#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'] = [
'#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'] = [
'#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'] = [
'#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'] = [
'#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'] = [
'#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'] = [
'#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['finalize_timeout'] = [
'#type' => 'number',
'#min' => 1,
'#max' => 180,
'#title' => $this
->t('Finalize timeout'),
'#description' => $this
->t('The timeout in seconds for index finalization queries on a Solr server.'),
'#default_value' => isset($this->configuration['finalize_timeout']) ? $this->configuration['finalize_timeout'] : 30,
'#required' => TRUE,
];
$form['commit_within'] = [
'#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'] = [
'#type' => 'details',
'#title' => $this
->t('Connector Workarounds'),
];
$form['workarounds']['solr_version'] = [
'#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' => [
'' => $this
->t('Determine automatically'),
'6' => '6.x',
'7' => '7.x',
'8' => '8.x',
],
'#default_value' => isset($this->configuration['solr_version']) ? $this->configuration['solr_version'] : '',
];
$form['workarounds']['http_method'] = [
'#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' => [
'AUTO' => $this
->t('AUTO'),
'POST' => 'POST',
'GET' => 'GET',
],
];
$form['advanced'] = [
'#type' => 'details',
'#title' => $this
->t('Advanced server configuration'),
];
$form['advanced']['jmx'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Enable JMX'),
'#description' => $this
->t('Enable JMX based monitoring.'),
'#default_value' => isset($this->configuration['jmx']) ? $this->configuration['jmx'] : FALSE,
];
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('Core or collection must not start with "/".'));
}
if (!$form_state
->hasAnyErrors()) {
$solr = new Client(NULL, $this->eventDispatcher);
$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);
}
foreach ($values['advanced'] as $key => $value) {
$form_state
->setValue($key, $value);
}
$form_state
->unsetValue('workarounds');
$form_state
->unsetValue('advanced');
$this
->traitSubmitConfigurationForm($form, $form_state);
}
protected function connect() {
if (!$this->solr) {
$this->solr = new Client(NULL, $this->eventDispatcher);
$this->solr
->createEndpoint($this->configuration + [
'key' => 'core',
], TRUE);
$this
->attachServerEndpoint();
}
}
protected function attachServerEndpoint() {
$this
->connect();
$configuration = $this->configuration;
$configuration['core'] = NULL;
$configuration['key'] = 'server';
$this->solr
->createEndpoint($configuration);
}
protected function getServerUri() {
$this
->connect();
$url_path = $this->solr
->getEndpoint('server')
->getBaseUri();
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 = explode('.', $this->configuration['solr_version']) + $min_version;
return implode('.', $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 = '') {
list($major, , ) = explode('.', $version ?: $this
->getSolrVersion());
return $major;
}
public function getSolrBranch($version = '') {
return $this
->getSolrMajorVersion($version) . '.x';
}
public function getLuceneMatchVersion($version = '') {
list($major, $minor, ) = explode('.', $version ?: $this
->getSolrVersion());
return $major . '.' . $minor;
}
public function getServerInfo($reset = FALSE) {
return $this
->getDataFromHandler('server', 'admin/info/system', $reset);
}
public function getCoreInfo($reset = FALSE) {
return $this
->getDataFromHandler('core', 'admin/system', $reset);
}
public function getLuke() {
return $this
->getDataFromHandler('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($endpoint_name, $handler, $reset = FALSE) {
static $previous_calls = [];
$this
->connect();
$endpoint = $this->solr
->getEndpoint($endpoint_name);
$endpoint_uri = $endpoint
->getBaseUri();
$state_key = 'search_api_solr.endpoint.data';
$state = \Drupal::state();
$endpoint_data = $state
->get($state_key);
if (!isset($previous_calls[$endpoint_uri][$handler]) || $reset) {
$previous_calls[$endpoint_name] = TRUE;
if (!is_array($endpoint_data) || !isset($endpoint_data[$endpoint_uri][$handler]) || $reset) {
$query = $this->solr
->createPing([
'handler' => $handler,
]);
$endpoint_data[$endpoint_uri][$handler] = $this
->execute($query, $endpoint)
->getData();
$state
->set($state_key, $endpoint_data);
}
}
return $endpoint_data[$endpoint_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();
$summary = [
'@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)) {
$solr_version = $this
->getSolrVersion(TRUE);
$max_time = -1;
if (version_compare($solr_version, '7.0', '>=')) {
$update_handler_stats = $stats['solr-mbeans']['UPDATE']['updateHandler']['stats'];
$summary['@pending_docs'] = (int) $update_handler_stats['UPDATE.updateHandler.docsPending'];
if (isset($update_handler_stats['UPDATE.updateHandler.softAutoCommitMaxTime'])) {
$max_time = (int) $update_handler_stats['UPDATE.updateHandler.softAutoCommitMaxTime'];
}
$summary['@deletes_by_id'] = (int) $update_handler_stats['UPDATE.updateHandler.deletesById'];
$summary['@deletes_by_query'] = (int) $update_handler_stats['UPDATE.updateHandler.deletesByQuery'];
$summary['@core_name'] = $stats['solr-mbeans']['CORE']['core']['stats']['CORE.coreName'];
$summary['@index_size'] = $stats['solr-mbeans']['CORE']['core']['stats']['INDEX.size'];
}
else {
$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['@deletes_by_id'] = (int) $update_handler_stats['deletesById'];
$summary['@deletes_by_query'] = (int) $update_handler_stats['deletesByQuery'];
$summary['@core_name'] = $stats['solr-mbeans']['CORE']['core']['stats']['coreName'];
if (version_compare($solr_version, '6.4', '>=')) {
$summary['@index_size'] = $stats['solr-mbeans']['CORE']['core']['stats']['size'];
}
else {
$summary['@index_size'] = $stats['solr-mbeans']['QUERYHANDLER']['/replication']['stats']['indexSize'];
}
}
$summary['@autocommit_time_seconds'] = $max_time / 1000;
$summary['@autocommit_time'] = \Drupal::service('date.formatter')
->formatInterval($max_time / 1000);
$summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query'];
$summary['@schema_version'] = $this
->getSchemaVersionString(TRUE);
}
return $summary;
}
public function coreRestGet($path) {
return $this
->restRequest('core', $path);
}
public function coreRestPost($path, $command_json = '') {
return $this
->restRequest('core', $path, Request::METHOD_POST, $command_json);
}
public function serverRestGet($path) {
return $this
->restRequest('server', $path);
}
public function serverRestPost($path, $command_json = '') {
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);
$timeout = $endpoint
->getTimeout();
$endpoint
->setTimeout($this->configuration['optimize_timeout']);
$response = $this
->executeRequest($request, $endpoint);
$endpoint
->setTimeout($timeout);
$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 getSpellcheckQuery() {
$this
->connect();
return $this->solr
->createSpellcheck();
}
public function getSuggesterQuery() {
$this
->connect();
return $this->solr
->createSuggester();
}
public function getAutocompleteQuery() {
$this
->connect();
$this->solr
->registerQueryType('autocomplete', '\\Drupal\\search_api_solr\\Solarium\\Autocomplete\\Query');
return $this->solr
->createQuery('autocomplete');
}
public function getQueryHelper(QueryInterface $query = NULL) {
if ($query) {
return $query
->getHelper();
}
return \Drupal::service('solarium.query_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');
}
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(QueryInterface $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');
}
$timeout = $endpoint
->getTimeout();
$endpoint
->setTimeout($this->configuration['index_timeout']);
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);
$endpoint
->setTimeout($timeout);
return $result;
}
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 = 'not found';
break;
case 401:
case 403:
$description = 'access denied';
break;
default:
$description = 'unreachable';
}
throw new SearchApiSolrException('Solr endpoint ' . $endpoint
->getBaseUri() . " {$description}.", $response_code, $e);
}
public function optimize(Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
$timeout = $endpoint
->getTimeout();
$endpoint
->setTimeout($this->configuration['optimize_timeout']);
$update_query = $this->solr
->createUpdate();
$update_query
->addOptimize(TRUE, FALSE);
$this
->execute($update_query, $endpoint);
$endpoint
->setTimeout($timeout);
}
public function adjustTimeout(int $timeout, Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
$previous_timeout = $this
->getTimeout($endpoint);
$endpoint
->setTimeout($timeout);
return $previous_timeout;
}
public function getTimeout(Endpoint $endpoint = NULL) {
$this
->connect();
if (!$endpoint) {
$endpoint = $this->solr
->getEndpoint('core');
}
return $endpoint
->getTimeout();
}
public function getIndexTimeout() {
return $this->configuration['index_timeout'];
}
public function getOptimizeTimeout() {
return $this->configuration['optimize_timeout'];
}
public function getFinalizeTimeout() {
return $this->configuration['finalize_timeout'];
}
public function extract(QueryInterface $query, Endpoint $endpoint = NULL) {
return $this
->execute($query, $endpoint);
}
public function getContentFromExtractResult(ExtractResult $result, $filepath) {
$response = $result
->getResponse();
$json_data = $response
->getBody();
$array_data = Json::decode($json_data);
if (isset($array_data[basename($filepath)])) {
return $array_data[basename($filepath)];
}
elseif (isset($array_data[$filepath])) {
return $array_data[$filepath];
}
throw new SearchApiSolrException('Unable to find extracted files within the Solr response body.');
}
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();
}
public function reloadCore() {
$this
->connect();
try {
$core = $this->configuration['core'];
$core_admin_query = $this->solr
->createCoreAdmin();
$reload_action = $core_admin_query
->createReload();
$reload_action
->setCore($core);
$core_admin_query
->setAction($reload_action);
$response = $this->solr
->coreAdmin($core_admin_query);
$was_successful = $response
->getWasSuccessful();
return $was_successful;
} catch (HttpException $e) {
throw new SearchApiSolrException("Reloading core {$core} failed with error code " . $e
->getCode() . '.', $e
->getCode(), $e);
}
}
public function alterConfigFiles(array &$files, string $lucene_match_version, string $server_id = '') {
if ($this->configuration['jmx']) {
$files['solrconfig_extra.xml'] .= "<jmx />\n";
}
}
}