Security update to Drupal 8.4.6
[yaffs-website] / web / core / modules / search / src / Plugin / views / filter / Search.php
1 <?php
2
3 namespace Drupal\search\Plugin\views\filter;
4
5 use Drupal\Core\Database\Query\Condition;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\views\Plugin\views\filter\FilterPluginBase;
8 use Drupal\views\Plugin\views\display\DisplayPluginBase;
9 use Drupal\views\ViewExecutable;
10 use Drupal\views\Views;
11
12 /**
13  * Filter handler for search keywords.
14  *
15  * @ingroup views_filter_handlers
16  *
17  * @ViewsFilter("search_keywords")
18  */
19 class Search extends FilterPluginBase {
20
21   /**
22    * This filter is always considered multiple-valued.
23    *
24    * @var bool
25    */
26   protected $alwaysMultiple = TRUE;
27
28   /**
29    * A search query to use for parsing search keywords.
30     *
31     * @var \Drupal\search\ViewsSearchQuery
32     */
33   protected $searchQuery = NULL;
34
35   /**
36    * TRUE if the search query has been parsed.
37    *
38    * @var bool
39    */
40   protected $parsed = FALSE;
41
42   /**
43    * The search type name (value of {search_index}.type in the database).
44    *
45    * @var string
46    */
47   protected $searchType;
48
49   /**
50    * {@inheritdoc}
51    */
52   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
53     parent::init($view, $display, $options);
54
55     $this->searchType = $this->definition['search_type'];
56   }
57
58   /**
59    * {@inheritdoc}
60    */
61   protected function defineOptions() {
62     $options = parent::defineOptions();
63
64     $options['operator']['default'] = 'optional';
65
66     return $options;
67   }
68
69   /**
70    * {@inheritdoc}
71    */
72   protected function operatorForm(&$form, FormStateInterface $form_state) {
73     $form['operator'] = [
74       '#type' => 'radios',
75       '#title' => $this->t('On empty input'),
76       '#default_value' => $this->operator,
77       '#options' => [
78         'optional' => $this->t('Show All'),
79         'required' => $this->t('Show None'),
80       ],
81     ];
82   }
83
84   /**
85    * {@inheritdoc}
86    */
87   protected function valueForm(&$form, FormStateInterface $form_state) {
88     $form['value'] = [
89       '#type' => 'textfield',
90       '#size' => 15,
91       '#default_value' => $this->value,
92       '#attributes' => ['title' => $this->t('Search keywords')],
93       '#title' => !$form_state->get('exposed') ? $this->t('Keywords') : '',
94     ];
95   }
96
97   /**
98    * {@inheritdoc}
99    */
100   public function validateExposed(&$form, FormStateInterface $form_state) {
101     if (!isset($this->options['expose']['identifier'])) {
102       return;
103     }
104
105     $key = $this->options['expose']['identifier'];
106     if (!$form_state->isValueEmpty($key)) {
107       $this->queryParseSearchExpression($form_state->getValue($key));
108       if (count($this->searchQuery->words()) == 0) {
109         $form_state->setErrorByName($key, $this->formatPlural(\Drupal::config('search.settings')->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
110       }
111     }
112   }
113
114   /**
115    * Sets up and parses the search query.
116    *
117    * @param string $input
118    *   The search keywords entered by the user.
119    */
120   protected function queryParseSearchExpression($input) {
121     if (!isset($this->searchQuery)) {
122       $this->parsed = TRUE;
123       $this->searchQuery = db_select('search_index', 'i', ['target' => 'replica'])->extend('Drupal\search\ViewsSearchQuery');
124       $this->searchQuery->searchExpression($input, $this->searchType);
125       $this->searchQuery->publicParseSearchExpression();
126     }
127   }
128
129   /**
130    * {@inheritdoc}
131    */
132   public function query() {
133     // Since attachment views don't validate the exposed input, parse the search
134     // expression if required.
135     if (!$this->parsed) {
136       $this->queryParseSearchExpression($this->value);
137     }
138     $required = FALSE;
139     if (!isset($this->searchQuery)) {
140       $required = TRUE;
141     }
142     else {
143       $words = $this->searchQuery->words();
144       if (empty($words)) {
145         $required = TRUE;
146       }
147     }
148     if ($required) {
149       if ($this->operator == 'required') {
150         $this->query->addWhere($this->options['group'], 'FALSE');
151       }
152     }
153     else {
154       $search_index = $this->ensureMyTable();
155
156       $search_condition = new Condition('AND');
157
158       // Create a new join to relate the 'search_total' table to our current
159       // 'search_index' table.
160       $definition = [
161         'table' => 'search_total',
162         'field' => 'word',
163         'left_table' => $search_index,
164         'left_field' => 'word',
165       ];
166       $join = Views::pluginManager('join')->createInstance('standard', $definition);
167       $search_total = $this->query->addRelationship('search_total', $join, $search_index);
168
169       // Add the search score field to the query.
170       $this->search_score = $this->query->addField('', "$search_index.score * $search_total.count", 'score', ['function' => 'sum']);
171
172       // Add the conditions set up by the search query to the views query.
173       $search_condition->condition("$search_index.type", $this->searchType);
174       $search_dataset = $this->query->addTable('node_search_dataset');
175       $conditions = $this->searchQuery->conditions();
176       $condition_conditions =& $conditions->conditions();
177       foreach ($condition_conditions as $key => &$condition) {
178         // Make sure we just look at real conditions.
179         if (is_numeric($key)) {
180           // Replace the conditions with the table alias of views.
181           $this->searchQuery->conditionReplaceString('d.', "$search_dataset.", $condition);
182         }
183       }
184       $search_conditions =& $search_condition->conditions();
185       $search_conditions = array_merge($search_conditions, $condition_conditions);
186
187       // Add the keyword conditions, as is done in
188       // SearchQuery::prepareAndNormalize(), but simplified because we are
189       // only concerned with relevance ranking so we do not need to normalize.
190       $or = new Condition('OR');
191       foreach ($words as $word) {
192         $or->condition("$search_index.word", $word);
193       }
194       $search_condition->condition($or);
195
196       $this->query->addWhere($this->options['group'], $search_condition);
197
198       // Add the GROUP BY and HAVING expressions to the query.
199       $this->query->addGroupBy("$search_index.sid");
200       $matches = $this->searchQuery->matches();
201       $placeholder = $this->placeholder();
202       $this->query->addHavingExpression($this->options['group'], "COUNT(*) >= $placeholder", [$placeholder => $matches]);
203     }
204     // Set to NULL to prevent PDO exception when views object is cached.
205     $this->searchQuery = NULL;
206   }
207
208 }