Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / views / src / Plugin / views / filter / StringFilter.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\filter;
4
5 use Drupal\Core\Database\Query\Condition;
6 use Drupal\Core\Form\FormStateInterface;
7
8 /**
9  * Basic textfield filter to handle string filtering commands
10  * including equality, like, not like, etc.
11  *
12  * @ingroup views_filter_handlers
13  *
14  * @ViewsFilter("string")
15  */
16 class StringFilter extends FilterPluginBase {
17
18   /**
19    * All words separated by spaces or sentences encapsulated by double quotes.
20    */
21   const WORDS_PATTERN = '/ (-?)("[^"]+"|[^" ]+)/i';
22
23   // exposed filter options
24   protected $alwaysMultiple = TRUE;
25
26   protected function defineOptions() {
27     $options = parent::defineOptions();
28
29     $options['expose']['contains']['required'] = ['default' => FALSE];
30
31     return $options;
32   }
33
34   /**
35    * This kind of construct makes it relatively easy for a child class
36    * to add or remove functionality by overriding this function and
37    * adding/removing items from this array.
38    */
39   public function operators() {
40     $operators = [
41       '=' => [
42         'title' => $this->t('Is equal to'),
43         'short' => $this->t('='),
44         'method' => 'opEqual',
45         'values' => 1,
46       ],
47       '!=' => [
48         'title' => $this->t('Is not equal to'),
49         'short' => $this->t('!='),
50         'method' => 'opEqual',
51         'values' => 1,
52       ],
53       'contains' => [
54         'title' => $this->t('Contains'),
55         'short' => $this->t('contains'),
56         'method' => 'opContains',
57         'values' => 1,
58       ],
59       'word' => [
60         'title' => $this->t('Contains any word'),
61         'short' => $this->t('has word'),
62         'method' => 'opContainsWord',
63         'values' => 1,
64       ],
65       'allwords' => [
66         'title' => $this->t('Contains all words'),
67         'short' => $this->t('has all'),
68         'method' => 'opContainsWord',
69         'values' => 1,
70       ],
71       'starts' => [
72         'title' => $this->t('Starts with'),
73         'short' => $this->t('begins'),
74         'method' => 'opStartsWith',
75         'values' => 1,
76       ],
77       'not_starts' => [
78         'title' => $this->t('Does not start with'),
79         'short' => $this->t('not_begins'),
80         'method' => 'opNotStartsWith',
81         'values' => 1,
82       ],
83       'ends' => [
84         'title' => $this->t('Ends with'),
85         'short' => $this->t('ends'),
86         'method' => 'opEndsWith',
87         'values' => 1,
88       ],
89       'not_ends' => [
90         'title' => $this->t('Does not end with'),
91         'short' => $this->t('not_ends'),
92         'method' => 'opNotEndsWith',
93         'values' => 1,
94       ],
95       'not' => [
96         'title' => $this->t('Does not contain'),
97         'short' => $this->t('!has'),
98         'method' => 'opNotLike',
99         'values' => 1,
100       ],
101       'shorterthan' => [
102         'title' => $this->t('Length is shorter than'),
103         'short' => $this->t('shorter than'),
104         'method' => 'opShorterThan',
105         'values' => 1,
106       ],
107       'longerthan' => [
108         'title' => $this->t('Length is longer than'),
109         'short' => $this->t('longer than'),
110         'method' => 'opLongerThan',
111         'values' => 1,
112       ],
113       'regular_expression' => [
114         'title' => $this->t('Regular expression'),
115         'short' => $this->t('regex'),
116         'method' => 'opRegex',
117         'values' => 1,
118       ],
119     ];
120     // if the definition allows for the empty operator, add it.
121     if (!empty($this->definition['allow empty'])) {
122       $operators += [
123         'empty' => [
124           'title' => $this->t('Is empty (NULL)'),
125           'method' => 'opEmpty',
126           'short' => $this->t('empty'),
127           'values' => 0,
128         ],
129         'not empty' => [
130           'title' => $this->t('Is not empty (NOT NULL)'),
131           'method' => 'opEmpty',
132           'short' => $this->t('not empty'),
133           'values' => 0,
134         ],
135       ];
136     }
137
138     return $operators;
139   }
140
141   /**
142    * Build strings from the operators() for 'select' options
143    */
144   public function operatorOptions($which = 'title') {
145     $options = [];
146     foreach ($this->operators() as $id => $info) {
147       $options[$id] = $info[$which];
148     }
149
150     return $options;
151   }
152
153   public function adminSummary() {
154     if ($this->isAGroup()) {
155       return $this->t('grouped');
156     }
157     if (!empty($this->options['exposed'])) {
158       return $this->t('exposed');
159     }
160
161     $options = $this->operatorOptions('short');
162     $output = '';
163     if (!empty($options[$this->operator])) {
164       $output = $options[$this->operator];
165     }
166     if (in_array($this->operator, $this->operatorValues(1))) {
167       $output .= ' ' . $this->value;
168     }
169     return $output;
170   }
171
172   protected function operatorValues($values = 1) {
173     $options = [];
174     foreach ($this->operators() as $id => $info) {
175       if (isset($info['values']) && $info['values'] == $values) {
176         $options[] = $id;
177       }
178     }
179
180     return $options;
181   }
182
183   /**
184    * Provide a simple textfield for equality
185    */
186   protected function valueForm(&$form, FormStateInterface $form_state) {
187     // We have to make some choices when creating this as an exposed
188     // filter form. For example, if the operator is locked and thus
189     // not rendered, we can't render dependencies; instead we only
190     // render the form items we need.
191     $which = 'all';
192     if (!empty($form['operator'])) {
193       $source = ':input[name="options[operator]"]';
194     }
195     if ($exposed = $form_state->get('exposed')) {
196       $identifier = $this->options['expose']['identifier'];
197
198       if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
199         // exposed and locked.
200         $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
201       }
202       else {
203         $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
204       }
205     }
206
207     if ($which == 'all' || $which == 'value') {
208       $form['value'] = [
209         '#type' => 'textfield',
210         '#title' => $this->t('Value'),
211         '#size' => 30,
212         '#default_value' => $this->value,
213       ];
214       $user_input = $form_state->getUserInput();
215       if ($exposed && !isset($user_input[$identifier])) {
216         $user_input[$identifier] = $this->value;
217         $form_state->setUserInput($user_input);
218       }
219
220       if ($which == 'all') {
221         // Setup #states for all operators with one value.
222         foreach ($this->operatorValues(1) as $operator) {
223           $form['value']['#states']['visible'][] = [
224             $source => ['value' => $operator],
225           ];
226         }
227       }
228     }
229
230     if (!isset($form['value'])) {
231       // Ensure there is something in the 'value'.
232       $form['value'] = [
233         '#type' => 'value',
234         '#value' => NULL
235       ];
236     }
237   }
238
239   public function operator() {
240     return $this->operator == '=' ? 'LIKE' : 'NOT LIKE';
241   }
242
243   /**
244    * Add this filter to the query.
245    *
246    * Due to the nature of fapi, the value and the operator have an unintended
247    * level of indirection. You will find them in $this->operator
248    * and $this->value respectively.
249    */
250   public function query() {
251     $this->ensureMyTable();
252     $field = "$this->tableAlias.$this->realField";
253
254     $info = $this->operators();
255     if (!empty($info[$this->operator]['method'])) {
256       $this->{$info[$this->operator]['method']}($field);
257     }
258   }
259
260   public function opEqual($field) {
261     $this->query->addWhere($this->options['group'], $field, $this->value, $this->operator());
262   }
263
264   protected function opContains($field) {
265     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value) . '%', 'LIKE');
266   }
267
268   protected function opContainsWord($field) {
269     $where = $this->operator == 'word' ? new Condition('OR') : new Condition('AND');
270
271     // Don't filter on empty strings.
272     if (empty($this->value)) {
273       return;
274     }
275
276     preg_match_all(static::WORDS_PATTERN, ' ' . $this->value, $matches, PREG_SET_ORDER);
277     foreach ($matches as $match) {
278       $phrase = FALSE;
279       // Strip off phrase quotes
280       if ($match[2]{0} == '"') {
281         $match[2] = substr($match[2], 1, -1);
282         $phrase = TRUE;
283       }
284       $words = trim($match[2], ',?!();:-');
285       $words = $phrase ? [$words] : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
286       foreach ($words as $word) {
287         $where->condition($field, '%' . db_like(trim($word, " ,!?")) . '%', 'LIKE');
288       }
289     }
290
291     if ($where->count() === 0) {
292       return;
293     }
294
295     // previously this was a call_user_func_array but that's unnecessary
296     // as views will unpack an array that is a single arg.
297     $this->query->addWhere($this->options['group'], $where);
298   }
299
300   protected function opStartsWith($field) {
301     $this->query->addWhere($this->options['group'], $field, db_like($this->value) . '%', 'LIKE');
302   }
303
304   protected function opNotStartsWith($field) {
305     $this->query->addWhere($this->options['group'], $field, db_like($this->value) . '%', 'NOT LIKE');
306   }
307
308   protected function opEndsWith($field) {
309     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value), 'LIKE');
310   }
311
312   protected function opNotEndsWith($field) {
313     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value), 'NOT LIKE');
314   }
315
316   protected function opNotLike($field) {
317     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value) . '%', 'NOT LIKE');
318   }
319
320   protected function opShorterThan($field) {
321     $placeholder = $this->placeholder();
322     // Type cast the argument to an integer because the SQLite database driver
323     // has to do some specific alterations to the query base on that data type.
324     $this->query->addWhereExpression($this->options['group'], "LENGTH($field) < $placeholder", [$placeholder => (int) $this->value]);
325   }
326
327   protected function opLongerThan($field) {
328     $placeholder = $this->placeholder();
329     // Type cast the argument to an integer because the SQLite database driver
330     // has to do some specific alterations to the query base on that data type.
331     $this->query->addWhereExpression($this->options['group'], "LENGTH($field) > $placeholder", [$placeholder => (int) $this->value]);
332   }
333
334   /**
335    * Filters by a regular expression.
336    *
337    * @param string $field
338    *   The expression pointing to the queries field, for example "foo.bar".
339    */
340   protected function opRegex($field) {
341     $this->query->addWhere($this->options['group'], $field, $this->value, 'REGEXP');
342   }
343
344   protected function opEmpty($field) {
345     if ($this->operator == 'empty') {
346       $operator = "IS NULL";
347     }
348     else {
349       $operator = "IS NOT NULL";
350     }
351
352     $this->query->addWhere($this->options['group'], $field, NULL, $operator);
353   }
354
355 }