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