Issues (69)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/SearchApiElasticsearchAbstractService.inc (19 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @file
5
 * Provides a Elasticsearch-based service class for the Search API.
6
 */
7
8
/**
9
 * Elasticsearch service abstract class.
10
 */
11
abstract class SearchApiElasticsearchAbstractService extends SearchApiAbstractService {
12
13
  abstract protected function getClusterHealth();
14
  abstract protected function getClusterState();
15
  abstract protected function getTransportOptions();
16
  abstract protected function getSettings(SearchApiIndex $index);
17
  abstract protected function updateSettings(SearchApiIndex $index, $data);
18
19
  /**
20
   * {@inheritdoc}
21
   */
22
  public function supportsFeature($feature) {
23
    $this->_supportedFeatures = drupal_map_assoc(array('search_api_service_extra'));
24
  }
25
26
  /**
27
   * Overrides configurationForm().
28
   */
29
  public function configurationForm(array $form, array &$form_state) {
30
    $options = $this->options + array(
31
      'host' => '127.0.0.1',
32
      'port' => 9200,
33
      'path' => '',
34
      'url' => NULL,
35
      'transport' => 'Http',
36
      'persistent' => TRUE,
37
      'timeout' => 300,
38
      'log' => FALSE,
39
      'retryOnConflict' => 0,
40
    );
41
42
    // Daemon settings.
43
    $form['daemon_settings'] = array(
44
      '#type' => 'fieldset',
45
      '#title' => t('Elasticsearch client settings'),
46
      '#tree' => TRUE,
47
      '#prefix' => '<div id="elasticsearch-ajax-wrapper">',
48
      '#suffix' => '</div>',
49
    );
50
51
    $delta = 1;
52
    $i = 1;
53
    if (isset($form_state['values']['options']['form']) && !empty($form_state['values']['options']['form']) && !isset($form_state['values']['remove_delta'])) {
54
      unset($form_state['values']['options']['form']['add_more']);
55 View Code Duplication
      if (isset($form_state['values']['options']['form']['facet_limit'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
56
        unset($form_state['values']['options']['form']['facet_settings']);
57
        unset($form_state['values']['options']['form']['facet_limit']);
58
      }
59
60
      $delta = count($form_state['values']['options']['form']) + 1;
61
    }
62
    elseif (isset($form_state['values']['remove_delta'])) {
63
      unset($form_state['values']['options']['form']['add_more']);
64 View Code Duplication
      if (isset($form_state['values']['options']['form']['facet_limit'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
65
        unset($form_state['values']['options']['form']['facet_settings']);
66
        unset($form_state['values']['options']['form']['facet_limit']);
67
      }
68
69
      $delta = count($form_state['values']['options']['form']);
70
    }
71
    elseif (isset($this->options) && !empty($this->options)) {
72
      if (isset($this->options['facet_limit'])) {
73
        unset($this->options['facet_limit']);
74
      }
75
      $delta = count($this->options);
76
    }
77
78
    for ($c = 0; $c < $delta; $c++) {
79
      if (isset($form_state['values']['remove_delta']) && $c == $form_state['values']['remove_delta']) {
80
        unset($form_state['values']['options']['form'][$form_state['values']['remove_delta']]);
81
        continue;
82
      }
83
      else {
84
        // Daemon settings.
85
        $form['daemon_settings']['fieldset'][$c] = array(
86
          '#type' => 'fieldset',
87
          '#title' => t('Node :id', array(':id' => $i)),
88
          '#tree' => TRUE,
89
          '#collapsible' => TRUE,
90
          '#collapsed' => TRUE,
91
        );
92
93
        // Elasticsearch daemon host.
94
        $form['daemon_settings']['fieldset'][$c]['host'] = array(
95
          '#type' => 'textfield',
96
          '#title' => t('Host'),
97
          '#description' => t('Host to which the elasticsearch daemon will listen for this server. Default is %default.', array(
98
            '%default' => $options['host'],
99
          )),
100
          '#required' => TRUE,
101
          '#default_value' => isset($options[$c]['host']) ? $options[$c]['host'] : $options['host'],
102
          '#parents' => array('options', 'form', $c, 'host'),
103
        );
104
105
        // Elasticsearch daemon port.
106
        $form['daemon_settings']['fieldset'][$c]['port'] = array(
107
          '#type' => 'textfield',
108
          '#title' => t('Port'),
109
          '#description' => t('Port to which the elasticsearch daemon will listen for this server. Default is %default.', array(
110
            '%default' => $options['port'],
111
          )),
112
          '#required' => TRUE,
113
          '#default_value' => isset($options[$c]['port']) ? $options[$c]['port'] : $options['port'],
114
          '#parents' => array('options', 'form', $c, 'port'),
115
        );
116
117
        //Elasticsearch basic authentication.
118
        $form['daemon_settings']['fieldset'][$c]['headers'] = array(
119
          '#type' => 'fieldset',
120
          '#tree' => TRUE,
121
          '#title' => t('HTTP Basic Authentication'),
122
          '#description' => t('If your Elasticsearch server is protected by basic HTTP authentication, enter the login data here.'),
123
        );
124
125
        //Elasticsearch basic authentication username.
126
        $form['daemon_settings']['fieldset'][$c]['headers']['http_user'] = array(
127
          '#type' => 'textfield',
128
          '#title' => t('Username'),
129
          '#default_value' => (!empty($options[$c]['headers']) && !empty($options[$c]['headers']['http_user'])) ? $options[$c]['headers']['http_user'] : '',
130
          '#parents' => array('options', 'form', $c, 'headers', 'http_user'),
131
        );
132
133
        //Elasticsearch basic password.
134
        $form['daemon_settings']['fieldset'][$c]['headers']['http_pass'] = array(
135
          '#type' => 'password',
136
          '#title' => t('Password'),
137
          '#parents' => array('options', 'form', $c, 'headers', 'http_pass'),
138
        );
139
140
        //Elasticsearch basic authentication.
141
        $form['daemon_settings']['fieldset'][$c]['aws'] = array(
142
          '#type' => 'fieldset',
143
          '#tree' => TRUE,
144
          '#title' => t('AWS IAM Authentication'),
145
          '#description' => t('If your Elasticsearch server is protected by basic AWS authentication, enter the IAM credentials here, you shoul use the AwsAuthV4 transporter.'),
146
        );
147
148
        //Elasticsearch AWS Access Key.
149
        $form['daemon_settings']['fieldset'][$c]['aws']['aws_access_key_id'] = array(
150
          '#type' => 'textfield',
151
          '#title' => t('Access Key'),
152
          '#default_value' => (!empty($options[$c]['aws']) && !empty($options[$c]['aws']['aws_access_key_id'])) ? $options[$c]['aws']['aws_access_key_id'] : '',
153
          '#description' => t('IAM Access Key ID.'),
154
          '#parents' => array('options', 'form', $c, 'aws', 'aws_access_key_id'),
155
        );
156
157
        //Elasticsearch AWS Secret Key.
158
        $form['daemon_settings']['fieldset'][$c]['aws']['aws_secret_access_key'] = array(
159
          '#type' => 'textfield',
160
          '#title' => t('Secret Key'),
161
          '#default_value' => (!empty($options[$c]['aws']) && !empty($options[$c]['aws']['aws_secret_access_key'])) ? $options[$c]['aws']['aws_secret_access_key'] : '',
162
          '#description' => t('IAM Secret Access Key.'),
163
          '#parents' => array('options', 'form', $c, 'aws', 'aws_secret_access_key'),
164
        );
165
166
        //Elasticsearch AWS Region.
167
        $form['daemon_settings']['fieldset'][$c]['aws']['aws_region'] = array(
168
          '#type' => 'textfield',
169
          '#title' => t('AWS Region'),
170
          '#default_value' => (!empty($options[$c]['aws']) && !empty($options[$c]['aws']['aws_region'])) ? $options[$c]['aws']['aws_region'] : '',
171
          '#description' => t('Region where is installed your ElasticSearch Service Instance.'),
172
          '#parents' => array('options', 'form', $c, 'aws', 'aws_region'),
173
        );
174
175
        // Elasticsearch daemon path.
176
        $form['daemon_settings']['fieldset'][$c]['path'] = array(
177
          '#type' => 'textfield',
178
          '#title' => t('Elasticsearch path prefix'),
179
          '#description' => t('Normally empty. Use when you have remapped the Elasticsearch server API path.'),
180
          '#required' => FALSE,
181
          '#default_value' => isset($options[$c]['path']) ? $options[$c]['path'] : $options['path'],
182
          '#parents' => array('options', 'form', $c, 'path'),
183
        );
184
185
        // Elasticsearch daemon URL.
186
        $form['daemon_settings']['fieldset'][$c]['url'] = array(
187
          '#type' => 'textfield',
188
          '#title' => t('Elasticsearch url'),
189
          '#description' => t('Normally empty. Use instead of host/port when you have remapped the Elasticsearch server API url.'),
190
          '#required' => FALSE,
191
          '#default_value' => isset($options[$c]['url']) ? $options[$c]['url'] : $options['url'],
192
          '#parents' => array('options', 'form', $c, 'url'),
193
        );
194
195
        // Elasticsearch daemon transport.
196
        $form['daemon_settings']['fieldset'][$c]['transport'] = array(
197
          '#type' => 'select',
198
          '#title' => t('Select transport'),
199
          '#description' => t('Transport to connect to this elasticsearch server.'),
200
          '#options' => $this->getTransportOptions(),
201
          '#default_value' => isset($options[$c]['transport']) ? $options[$c]['transport'] : $options['transport'],
202
          '#parents' => array('options', 'form', $c, 'transport'),
203
        );
204
205
        // Elasticsearch daemon persistent.
206
        $form['daemon_settings']['fieldset'][$c]['persistent'] = array(
207
          '#type' => 'checkbox',
208
          '#title' => t('Persistent connection'),
209
          '#description' => t('Use persistent connection when connecting to this node.'),
210
          '#default_value' => isset($options[$c]['persistent']) ? $options[$c]['persistent'] : $options['persistent'],
211
          '#parents' => array('options', 'form', $c, 'persistent'),
212
        );
213
214
        // Elasticsearch daemon timeout.
215
        $form['daemon_settings']['fieldset'][$c]['timeout'] = array(
216
          '#type' => 'textfield',
217
          '#title' => t('Timeout in ms'),
218
          '#description' => t('Timeout in ms for waiting this elastic server to respond'),
219
          '#default_value' => isset($options[$c]['timeout']) ? $options[$c]['timeout'] : $options['timeout'],
220
          '#parents' => array('options', 'form', $c, 'timeout'),
221
        );
222
223
        // Elasticsearch daemon log.
224
        $form['daemon_settings']['fieldset'][$c]['log'] = array(
225
          '#type' => 'checkbox',
226
          '#title' => t('Log'),
227
          '#description' => t('Log this elasticsearch server queries to the default log.'),
228
          '#default_value' => isset($options[$c]['log']) ? $options[$c]['log'] : $options['log'],
229
          '#parents' => array('options', 'form', $c, 'log'),
230
        );
231
232
        if (!class_exists('\Psr\Log\AbstractLogger')) {
233
          $form['daemon_settings']['fieldset'][$c]['log']['#disabled'] = TRUE;
234
          $form['daemon_settings']['fieldset'][$c]['log']['#description'] = t('Logging Elasticsearch queries requires the <a href="@psr3_url">PSR-3 logger</a> to be installed and available. It is recommended to install the <a href="@psr3_watchdog_url">PSR-3 Watchdog module</a> or <a href="@monolog_url">Monolog module</a>.', array('@psr_url' => url('https://packagist.org/packages/psr/log'), '@psr3_watchdog_url' => url('https://drupal.org/project/psr3_watchdog'), '@monolog_url' => url('https://www.drupal.org/project/monolog')));
235
        }
236
237
        // Elasticsearch daemon retryOnConflict.
238
        $form['daemon_settings']['fieldset'][$c]['retryOnConflict'] = array(
239
          '#type' => 'textfield',
240
          '#title' => t('retryOnConflict'),
241
          '#description' => t('Sets the number of retries of a version conflict occurs because the document was updated between getting it and updating it.'),
242
          '#default_value' => isset($options[$c]['retryOnConflict']) ? $options[$c]['retryOnConflict'] : $options['retryOnConflict'],
243
          '#parents' => array('options', 'form', $c, 'retryOnConflict'),
244
        );
245
246
        if ((!isset($form_state['values']['remove_delta']) && $delta > 1) || (isset($form_state['values']['remove_delta']) && $delta > 2)) {
247
          // Elasticsearch daemon retryOnConflict.
248
          $form['daemon_settings']['fieldset'][$c]['remove_node'] = array(
249
            '#type' => 'submit',
250
            '#value' => t('Remove node') . ' ' . $i,
251
            '#submit' => array('_search_api_elasticsearch_configuration_form_remove_custom'),
252
            '#ajax' => array(
253
              'callback' => '_search_api_elasticsearch_configuration_form_remove_ajax',
254
              'wrapper' => 'elasticsearch-ajax-wrapper',
255
              'method' => 'replace',
256
              'effect' => 'fade',
257
            ),
258
            '#remove_delta' => $c,
259
            '#parents' => array('options', 'form', $c, 'remove_node'),
260
          );
261
        }
262
      }
263
      $i++;
264
    }
265
266
    // Elasticsearch daemon retryOnConflict.
267
    $form['add_more'] = array(
268
      '#type' => 'submit',
269
      '#value' => t('+'),
270
      '#submit' => array('_search_api_elasticsearch_configuration_form_submit_custom'),
271
      '#ajax' => array(
272
        'callback' => '_search_api_elasticsearch_configuration_form_ajax',
273
        'wrapper' => 'elasticsearch-ajax-wrapper',
274
        'method' => 'replace',
275
        'effect' => 'fade',
276
      ),
277
    );
278
279
    if (module_exists('search_api_facetapi')) {
280
      // Facet settings.
281
      $form['facet_settings'] = array(
282
        '#type' => 'fieldset',
283
        '#title' => t('Elasticsearch facet settings'),
284
        '#tree' => FALSE,
285
      );
286
287
      // Elasticsearch facet limit.
288
      $default = 10;
289
      $form['facet_settings']['facet_limit'] = array(
290
        '#type' => 'textfield',
291
        '#title' => t('Facet limit'),
292
        '#description' => t("Maximum number of facet elements to be returned by the server if 'no limit' is selected as hard limit is the facet option. Default is %default.", array(
293
          '%default' => $default,
294
        )),
295
        '#required' => TRUE,
296
        '#default_value' => (isset($options['facet_limit'])) ? $options['facet_limit'] : $default,
297
        '#parents' => array('options', 'form', 'facet_limit'),
298
      );
299
    }
300
301
    return $form;
302
  }
303
304
  /**
305
   * Overrides configurationFormValidate().
306
   */
307
  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
308
    unset($values['add_more']);
309
    $count_nodes = count($values);
0 ignored issues
show
$count_nodes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
310
311
    if (module_exists('search_api_facetapi')) {
312
      // Facet limit.
313
      if (filter_var($values['facet_limit'], FILTER_VALIDATE_INT, array('options' => array('min_range' => 0))) === FALSE) {
314
        form_set_error('options][form][facet_limit', t('You must enter a positive integer for the elasticsearch facet limit.'));
315
      }
316
    }
317
318
    $options = $this->getOptions();
319
    foreach ($values as $i => $setting) {
320
      if ($i != 'facet_limit') {
321
        // Daemon IP address.
322
        if (filter_var($values[$i]['host'], FILTER_VALIDATE_IP) === FALSE) {
323
          form_set_error('options][form]' . $i . '[host', t('You must enter a valid IP address for the elasticsearch daemon.'));
324
        }
325
326
        // Daemon Port.
327
        if (filter_var($values[$i]['port'], FILTER_VALIDATE_INT, array('options' => array('min_range' => 0, 'max_range' => 65535))) === FALSE) {
328
          form_set_error('options][form]' . $i . '[port', t('You must enter a valid Port (between 0 and 65535) for the elasticsearch daemon.'));
329
        }
330
331
        $values[$i]['path'] = $this->setPath($values[$i]['path']);
332
      }
333
334
      // Put http_user and http_password in correct form.
335
      if (!empty($setting['headers']['http_user'])) {
336
        // If username matches the old value and password is empty, then use the old Authentication.
337
        if (empty($setting['headers']['http_pass'])) {
338
          if ($setting['headers']['http_user'] == $options[$i]['headers']['http_user']) {
339
            $values[$i]['headers']['Authorization'] = $options[$i]['headers']['Authorization'];
340
          }
341
          // If username does not match and password is empty, then give a validation error.
342
          else {
343
            form_set_error('http_pass', t('If you are changing the username, you need to supply the password.'));
344
            return;
345
          }
346
        }
347
        else {
348
          $values[$i]['headers']['Authorization'] = 'Basic ' . base64_encode($setting['headers']['http_user'] . ':' . $setting['headers']['http_pass']);
349
        }
350
      }
351
      if (isset($values[$i]['headers']['http_pass'])) {
352
        unset($values[$i]['headers']['http_pass']);
353
      }
354
355
      // Put aws_access_key_id and aws_secret_access_keyword in correct form.
356
      if (!empty($setting['aws']['aws_access_key_id'])) {
357
        if (empty($setting['aws']['aws_secret_access_key'])) {
358
            form_set_error('aws_secret_access_key', t('Access Secret Key is required to generate the Access token.'));
359
            return;
360
        }
361
        if (empty($setting['aws']['aws_region'])) {
362
            form_set_error('aws_region', t('Region of the Elasticsearch Service must be specified.'));
363
            return;
364
        }
365
      }
366 View Code Duplication
      if (isset($values[$i]['aws']['aws_access_key_id'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
        $values[$i]['aws_access_key_id'] = $values[$i]['aws']['aws_access_key_id'];
368
      }
369 View Code Duplication
      if (isset($values[$i]['aws']['aws_secret_access_key'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
        $values[$i]['aws_secret_access_key'] = $values[$i]['aws']['aws_secret_access_key'];
371
      }
372 View Code Duplication
      if (isset($values[$i]['aws']['aws_region'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
373
        $values[$i]['aws_region'] = $values[$i]['aws']['aws_region'];
374
      }
375
    }
376
  }
377
378
  /**
379
   * Overrides configurationFormSubmit().
380
   */
381
  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
382
    $aggregation_limit = '';
383
384
    if (isset($values['facet_limit'])) {
385
      $aggregation_limit = $values['facet_limit'];
386
      unset($values['facet_limit']);
387
    }
388
389
    $values = array_values($values);
390
    $values['facet_limit'] = $aggregation_limit;
391
    $this->options = $values;
392
  }
393
394
  /**
395
   * Helper function. Parse an option form element.
396
   */
397
  protected function parseOptionFormElement($element, $key) {
398
    $children_keys = element_children($element);
399
400
    if (!empty($children_keys)) {
401
      $children = array();
402
      foreach ($children_keys as $child_key) {
403
        $child = $this->parseOptionFormElement($element[$child_key], $child_key);
404
        if (!empty($child)) {
405
          $children[] = $child;
406
        }
407
      }
408
      if (!empty($children)) {
409
        return array(
410
          'label' => isset($element['#title']) ? $element['#title'] : $key,
411
          'option' => $children,
412
        );
413
      }
414
    }
415
    elseif (isset($this->options[$key])) {
416
      return array(
417
        'label' => isset($element['#title']) ? $element['#title'] : $key,
418
        'option' => $key,
419
      );
420
    }
421
422
    return array();
423
  }
424
425
  /**
426
   * Helper function. Display a setting element.
427
   */
428
  protected function viewSettingElement($element) {
429
    $output = '';
430
431
    if (is_array($element['option'])) {
432
      $value = '';
433
      foreach ($element['option'] as $sub_element) {
434
        $value .= $this->viewSettingElement($sub_element);
435
      }
436
    }
437
    else {
438
      $value = $this->getOption($element['option']);
439
      $value = nl2br(check_plain(print_r($value, TRUE)));
440
    }
441
    $output .= '<dt><em>' . check_plain($element['label']) . '</em></dt>' . "\n";
442
    $output .= '<dd>' . $value . '</dd>' . "\n";
443
444
    return "<dl>\n{$output}</dl>";
445
  }
446
447
  /**
448
   * Helper function. Get the Elasticsearch mapping for a field.
449
   */
450
  protected function getFieldMapping($field) {
451
    $field_type = (isset($field['real_type'])) ? $field['real_type'] : $field['type'];
452
    $type = search_api_extract_inner_type($field_type);
453
454
    switch ($type) {
455
      case 'text':
456
        return array(
457
          'type' => 'string',
458
          'boost' => $field['boost'],
459
        );
460
461
      case 'uri':
462
      case 'string':
463
      case 'token':
464
        return array(
465
          'type' => 'string',
466
          'index' => 'not_analyzed',
467
        );
468
469
      case 'integer':
470
      case 'duration':
471
        return array(
472
          'type' => 'integer',
473
        );
474
475
      case 'boolean':
476
        return array(
477
          'type' => 'boolean',
478
        );
479
480
      case 'decimal':
481
        return array(
482
          'type' => 'float',
483
        );
484
485
      case 'date':
486
        return array(
487
          'type' => 'date',
488
          'format' => 'date_time',
489
        );
490
491
      case 'location':
492
        return array(
493
          'type' => 'geo_point',
494
          'lat_lon' => TRUE,
495
        );
496
497
      default:
498
        return NULL;
499
    }
500
  }
501
  /**
502
   * Helper function. Return date gap from two dates or timestamps.
503
   *
504
   * @see facetapi_get_timestamp_gap()
505
   */
506
  protected static function getDateGap($min, $max, $timestamp = TRUE) {
507
    if ($timestamp !== TRUE) {
508
      $min = strtotime($min);
509
      $max = strtotime($max);
510
    }
511
512
    if (empty($min) || empty($max)) {
513
      return 'DAY';
514
    }
515
516
    $diff = $max - $min;
517
518
    switch (TRUE) {
519
      case ($diff > 86400 * 365):
520
        return 'NONE';
521
522
      case ($diff > 86400 * gmdate('t', $min)):
523
        return 'YEAR';
524
525
      case ($diff > 86400):
526
        return 'MONTH';
527
528
      default:
529
        return 'DAY';
530
    }
531
  }
532
533
  /**
534
   * Helper function. Return server options.
535
   */
536
  protected function getOptions() {
537
    return $this->options;
538
  }
539
540
  /**
541
   * Helper function. Return a server option.
542
   */
543
  protected function getOption($option, $default = NULL) {
544
    $options = $this->getOptions();
545
    return isset($options[$option]) ? $options[$option] : $default;
546
  }
547
548
  /**
549
   * Helper function. Return index fields.
550
   */
551
  protected function getIndexFields(SearchApiQueryInterface $query) {
552
    $index = $query->getIndex();
553
    $index_fields = $index->getFields();
554
    return $index_fields;
555
  }
556
557
  /**
558
   * Helper function that return Sort for query in search.
559
   */
560
  protected function getSortSearchQuery(SearchApiQueryInterface $query) {
561
562
    $index_fields = $this->getIndexFields($query);
563
    $sort = array();
564
    foreach ($query->getSort() as $field_id => $direction) {
565
      $direction = drupal_strtolower($direction);
566
567
      if ($field_id === 'search_api_relevance') {
568
        $sort['_score'] = $direction;
569
      }
570
      elseif (isset($index_fields[$field_id])) {
571
        $sort[$field_id] = $direction;
572
      }
573
      else {
574
        throw new Exception(t('Incorrect sorting!.'));
575
      }
576
    }
577
    return $sort;
578
  }
579
580
  /**
581
   * Helper function return Facet filter.
582
   */
583
  protected function getAggregationSearchFilter(SearchApiQueryInterface $query, $aggregation_info) {
584
    $index_fields = $this->getIndexFields($query);
585
    $aggregation_search_filter = '';
0 ignored issues
show
$aggregation_search_filter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
586
587
    if (isset($aggregation_info['operator']) && drupal_strtolower($aggregation_info['operator']) == 'or') {
588
      $aggregation_search_filter = $this->parseFilter($query->getFilter(), $index_fields, $aggregation_info['field']);
589
      if (!empty($aggregation_search_filter)) {
590
        $aggregation_search_filter = $aggregation_search_filter[0];
591
      }
592
    }
593
    // Normal facet, we just use the main query filters.
594
    else {
595
      $aggregation_search_filter = $this->parseFilter($query->getFilter(), $index_fields);
596
      if (!empty($aggregation_search_filter)) {
597
        $aggregation_search_filter = $aggregation_search_filter[0];
598
      }
599
    }
600
601
    return $aggregation_search_filter;
602
  }
603
604
  /**
605
   * Helper function that return facet limits.
606
   */
607
  protected function getAggregationLimit(array $aggregation_info) {
608
    // If no limit (-1) is selected, use the server facet limit option.
609
    $aggregation_limit = !empty($aggregation_info['limit']) ? $aggregation_info['limit'] : -1;
610
    if ($aggregation_limit < 0) {
611
      $aggregation_limit = $this->getOption('facet_limit', 10);
612
    }
613
    return $aggregation_limit;
614
  }
615
616
  /**
617
   * Helper function which add params to date facets.
618
   */
619
  protected function getDateAggregationInterval($aggregation_id) {
620
    // Active search corresponding to this index.
621
    $searcher = key(facetapi_get_active_searchers());
622
623
    // Get the FacetApiAdpater for this searcher.
624
    $adapter = isset($searcher) ? facetapi_adapter_load($searcher) : NULL;
625
626
    // Get the date granularity.
627
    $date_gap = $this->getDateGranularity($adapter, $aggregation_id);
628
629
    switch ($date_gap) {
630
      // Already a selected YEAR, we want the months.
631
      case 'YEAR':
632
        $date_interval = 'month';
633
        break;
634
635
      // Already a selected MONTH, we want the days.
636
      case 'MONTH':
637
        $date_interval = 'day';
638
        break;
639
640
      // Already a selected DAY, we want the hours and so on.
641
      case 'DAY':
642
        $date_interval = 'hour';
643
        break;
644
645
      // By default we return result counts by year.
646
      default:
647
        $date_interval = 'year';
648
    }
649
650
    return $date_interval;
651
  }
652
653
  /**
654
   * Helper function to return date gap.
655
   */
656
  protected function getDateGranularity($adapter, $aggregation_id) {
657
    // Date gaps.
658
    $gap_weight = array('YEAR' => 2, 'MONTH' => 1, 'DAY' => 0);
659
    $gaps = array();
660
    $date_gap = 'YEAR';
661
662
    // Get the date granularity.
663
    if (isset($adapter)) {
664
      // Get the current date gap from the active date filters.
665
      $active_items = $adapter->getActiveItems(array('name' => $aggregation_id));
666
      if (!empty($active_items)) {
667
        foreach ($active_items as $active_item) {
668
          $value = $active_item['value'];
669
          if (strpos($value, ' TO ') > 0) {
670
            list($date_min, $date_max) = explode(' TO ', str_replace(array('[', ']'), '', $value), 2);
671
            $gap = self::getDateGap($date_min, $date_max, FALSE);
672
            if (isset($gap_weight[$gap])) {
673
              $gaps[] = $gap_weight[$gap];
674
            }
675
          }
676
        }
677
        if (!empty($gaps)) {
678
          // Minimum gap.
679
          $date_gap = array_search(min($gaps), $gap_weight);
680
        }
681
      }
682
    }
683
684
    return $date_gap;
685
  }
686
687
  /**
688
   * Helper function that parse facets.
689
   */
690
  protected function parseSearchAggregation($response, SearchApiQueryInterface $query) {
691
692
    $result = array();
693
    $index_fields = $this->getIndexFields($query);
694
    $aggregations = $query->getOption('search_api_facets');
695
696
    if (!empty($aggregations) && $response->hasAggregations()) {
697
      foreach ($response->getAggregations() as $aggregation_id => $aggregation_data) {
698
        if (isset($aggregations[$aggregation_id])) {
699
          $aggregation_info = $aggregations[$aggregation_id];
700
          $aggregation_min_count = $aggregation_info['min_count'];
701
702
          $field_id = $aggregation_info['field'];
703
          $field_type = search_api_extract_inner_type($index_fields[$field_id]['type']);
704
705
          // TODO: handle different types (GeoDistance and so on).
706
          if ($field_type === 'date') {
707
            foreach ($aggregation_data['buckets'] as $entry) {
708
              if ($entry['count'] >= $aggregation_min_count) {
709
                // Divide time by 1000 as we want seconds from epoch
710
                // not milliseconds.
711
                $result[$aggregation_id][] = array(
712
                  'count' => $entry['count'],
713
                  'filter' => '"' . ($entry['time'] / 1000) . '"',
714
                );
715
              }
716
            }
717
          }
718
          else {
719
            foreach ($aggregation_data['buckets'] as $term) {
720
              if ($term['doc_count'] >= $aggregation_min_count) {
721
                $result[$aggregation_id][] = array(
722
                  'count' => $term['doc_count'],
723
                  'filter' => '"' . $term['key'] . '"',
724
                );
725
              }
726
            }
727
          }
728
        }
729
      }
730
    }
731
732
    return $result;
733
  }
734
735
  /**
736
   * Helper function. Return the path in the correct format.
737
   */
738
  protected function setPath($path) {
739
    if (isset($path) && !empty($path)) {
740
      $trimmed_path = trim($path, '/');
741
      $path = $trimmed_path . '/';
742
    }
743
744
    return $path;
745
  }
746
747
  /**
748
   * Helper function. Escape a field or index name.
749
   *
750
   * Force names to be strictly alphanumeric-plus-underscore.
751
   */
752
  protected static function escapeName($name) {
753
    return preg_replace('/[^A-Za-z0-9_]+/', '', $name);
754
  }
755
756
  /**
757
   * Helper function. Get Autocomplete suggestions.
758
   *
759
   * @param SearchApiQueryInterface $query
760
   * @param SearchApiAutocompleteSearch $search
761
   * @param string $incomplete_key
762
   * @param string $user_input
763
   */
764
  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) {
765
    $suggestions = array();
766
    // Turn inputs to lower case, otherwise we get case sensivity problems.
767
    $incomp = drupal_strtolower($incomplete_key);
0 ignored issues
show
$incomp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
768
769
    $index = $query->getIndex();
770
    $index_fields = $this->getIndexFields($query);
0 ignored issues
show
$index_fields is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
771
772
    $complete = $query->getOriginalKeys();
0 ignored issues
show
$complete is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
773
    $query->keys($user_input);
774
775
    try {
776
      $response = $this->search($query);
777
    }
778
    catch (Exception $e) {
779
      watchdog('Elasticsearch', check_plain($e->getMessage()), array(), WATCHDOG_ERROR);
780
      return array();
781
    }
782
783
    $matches = array();
784
    if (isset($response['results'])) {
785
      $items = $index->loadItems(array_keys($response['results']));
786
      foreach ($items as $id => $item) {
787
        $node_title = $index->datasource()->getItemLabel($item);
788
        $matches[$node_title] = $node_title;
789
      }
790
791
      if ($matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
792
        // Eliminate suggestions that are too short or already in the query.
793
        foreach ($matches as $name => $node_title) {
794
          if (drupal_strlen($name) < 3 || isset($keys_array[$name])) {
795
            unset($matches[$name]);
796
          }
797
        }
798
799
        // The $count in this array is actually a score. We want the
800
        // highest ones first.
801
        arsort($matches);
802
803
        // Shorten the array to the right ones.
804
        $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE);
0 ignored issues
show
The variable $limit does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
$additional_matches is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
805
        $matches = array_slice($matches, 0, $limit, TRUE);
806
807
        foreach ($matches as $node => $name) {
808
          $suggestions[] = $name;
809
        }
810
      }
811
      $keys = trim($keys . ' ' . $incomplete_key);
0 ignored issues
show
The variable $keys seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
$keys is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
812
      return $suggestions;
813
    }
814
  }
815
816
  /**
817
   * Helper function: Recursively parse Search API filters.
818
   */
819 View Code Duplication
  protected function parseFilter(SearchApiQueryFilter $query_filter, $index_fields, $ignored_field_id = '') {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
820
821
    if (empty($query_filter)) {
822
      return NULL;
823
    }
824
    else {
825
      $conjunction = $query_filter->getConjunction();
826
827
      $filters = array();
828
829
      try {
830
        foreach ($query_filter->getFilters() as $filter_info) {
831
          $filter = NULL;
832
833
          // Simple filter [field_id, value, operator].
834
          if (is_array($filter_info)) {
835
            $filter_assoc = $this->getAssociativeFilter($filter_info);
836
            $this->correctFilter($filter_assoc, $index_fields, $ignored_field_id);
837
            // Check field.
838
            $filter = $this->getFilter($filter_assoc);
839
840
            if (!empty($filter)) {
841
              $filters[] = $filter;
842
            }
843
          }
844
          // Nested filters.
845
          elseif ($filter_info instanceof SearchApiQueryFilter) {
0 ignored issues
show
The class SearchApiQueryFilter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
846
            $nested_filters = $this->parseFilter($filter_info, $index_fields, $ignored_field_id);
847
            // TODO: handle error. - here is unnecessary cause in if we thow exceptions and this is still in try{}  .
848
            if (!empty($nested_filters)) {
849
              $filters = array_merge($filters, $nested_filters);
850
            }
851
          }
852
        }
853
        $filters = $this->setFiltersConjunction($filters, $conjunction);
854
      }
855
      catch (Exception $e) {
856
        watchdog('Elasticsearch', check_plain($e->getMessage()), array(), WATCHDOG_ERROR);
857
        drupal_set_message(check_plain($e->getMessage()), 'error');
858
      }
859
860
      return $filters;
861
    }
862
  }
863
864
  /**
865
   * Helper function that return associative array  of filters info.
866
   */
867 View Code Duplication
  protected function getAssociativeFilter(array $filter_info) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
868
869
    $filter_operator = str_replace('!=', '<>', $filter_info[2]);
870
    return array(
871
      'field_id' => $filter_info[0],
872
      'filter_value' => $filter_info[1],
873
      'filter_operator' => $filter_operator,
874
    );
875
  }
876
877
  /**
878
   * Helper function that check if filter is set correct.
879
   */
880
  protected function correctFilter($filter_assoc, $index_fields, $ignored_field_id = '') {
881
    if (!isset($filter_assoc['field_id']) || !isset($filter_assoc['filter_value'])
882
        || !isset($filter_assoc['filter_operator'])) {
883
      throw new Exception(t('Incorrect filter criteria is using for searching!'));
884
    }
885
886
    $field_id = $filter_assoc['field_id'];
887 View Code Duplication
    if (!isset($index_fields[$field_id])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
888
      throw new Exception(t(':field_id Undefined field ! Incorrect filter criteria is using for searching!', array(':field_id' => $field_id)));
889
    }
890
891
    // Check operator.
892
    if (empty($filter_assoc['filter_operator'])) {
893
      throw new Exception(t('Empty filter operator for :field_id field! Incorrect filter criteria is using for searching!', array(':field_id' => $field_id)));
894
    }
895
896
    // If field should be ignored, we skip.
897
    if ($field_id === $ignored_field_id) {
898
      return TRUE;
899
    }
900
901
    return TRUE;
902
  }
903
904
  /**
905
   * Return a full text search query.
906
   *
907
   * TODO: better handling of parse modes.
908
   */
909
  protected function flattenKeys($keys, $parse_mode = '', $full_text_fields = array()) {
910
    $conjunction = isset($keys['#conjunction']) ? $keys['#conjunction'] : 'AND';
911
    $negation = !empty($keys['#negation']);
912
    $values = array();
913
914
    foreach (element_children($keys) as $key) {
915
      $value = $keys[$key];
916
917
      if (empty($value)) {
918
        continue;
919
      }
920
921
      if (is_array($value)) {
922
        $values[] = $this->flattenKeys($value);
923
      }
924
      elseif (is_string($value)) {
925
        // If parse mode is not "direct": quote the keyword.
926
        if ($parse_mode !== 'direct') {
927
          $value = '"' . $value . '"';
928
        }
929
930
        $values[] = $value;
931
      }
932
    }
933
    if (!empty($values)) {
934
      return ($negation === TRUE ? 'NOT ' : '') . '(' . implode(" {$conjunction} ", $values) . ')';
935
    }
936
    else {
937
      return '';
938
    }
939
  }
940
941
  /**
942
   * Helper function. Returns the elasticsearch name of an index.
943
   */
944
  protected function getIndexName(SearchApiIndex $index) {
945
    global $databases;
946
947
    $site_database = $databases['default']['default']['database'];
948
949
    $index_machine_name = is_string($index) ? $index : $index->machine_name;
950
951
    return self::escapeName('elasticsearch_index_' . $site_database . '_' . $index_machine_name);
952
  }
953
954
  /**
955
   * Overrides fieldsUpdated().
956
   *
957
   * We only do the grunt work of building the array of properties. This allows
958
   * submodules who just need an array of properties to simply call this as a
959
   * parent method to have the array built for them, thus implementing the DRY
960
   * principle. The submodule is responsible for the return value.
961
   *
962
   * @param SearchApiIndex $index
963
   *   The Search API index.
964
   */
965
  public function fieldsUpdated(SearchApiIndex $index) {
966
    $this->fieldsUpdatedProperties = array(
967
      'id' => array('type' => 'string', 'include_in_all' => FALSE),
968
    );
969
    foreach ($index->getFields() as $field_id => $field_data) {
970
      $this->fieldsUpdatedProperties[$field_id] = $this->getFieldMapping($field_data);
971
    }
972
973
    // Allow other modules to alter properties.
974
    drupal_alter('search_api_elasticsearch_fields_updated', $index, $this->fieldsUpdatedProperties);
975
  }
976
977
  /**
978
   * Get analyzers for an Elasticsearch index.
979
   *
980
   * @param SearchApiIndex $index
981
   *   A Search API index object.
982
   *
983
   * @return array | bool
984
   *   An array of available analyzers. FALSE if none.
985
   */
986
  public function getAnalysisSettings(SearchApiIndex $index) {
987
    $settings = $this->getSettings($index);
988
    return isset($settings['analysis']) ? $settings['analysis'] : FALSE;
989
  }
990
991
  /**
992
   * {@inheritdoc}
993
   */
994
  public function getExtraInformation() {
995
    $info = array();
996
997
    $cluster_health = $this->getClusterHealth();
998
    if (!empty($cluster_health)) {
999
      $info[] = array(
1000
        'label' => t('Cluster Name'),
1001
        'info' => $cluster_health['cluster_name'],
1002
      );
1003
      $info[] = array(
1004
        'label' => t('Cluster Status'),
1005
        'info' => $cluster_health['status'],
1006
      );
1007
      $info[] = array(
1008
        'label' => t('Number of Nodes'),
1009
        'info' => $cluster_health['number_of_nodes'],
1010
      );
1011
      $info[] = array(
1012
        'label' => t('Number of Data Nodes'),
1013
        'info' => $cluster_health['number_of_data_nodes'],
1014
      );
1015
      $info[] = array(
1016
        'label' => t('Active Primary Shards'),
1017
        'info' => $cluster_health['active_primary_shards'],
1018
      );
1019
      $info[] = array(
1020
        'label' => t('Active Shards'),
1021
        'info' => $cluster_health['active_shards'],
1022
      );
1023
      $info[] = array(
1024
        'label' => t('Relocating Shards'),
1025
        'info' => $cluster_health['relocating_shards'],
1026
      );
1027
      $info[] = array(
1028
        'label' => t('Initializing Shards'),
1029
        'info' => $cluster_health['initializing_shards'],
1030
      );
1031
      $info[] = array(
1032
        'label' => t('Unassigned Shards'),
1033
        'info' => $cluster_health['unassigned_shards'],
1034
      );
1035
    }
1036
1037
    return $info;
1038
  }
1039
}
1040