Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / views / src / Plugin / views / filter / InOperator.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\filter;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\views\Plugin\views\display\DisplayPluginBase;
8 use Drupal\views\ViewExecutable;
9 use Drupal\Core\Form\OptGroup;
10
11 /**
12  * Simple filter to handle matching of multiple options selectable via checkboxes
13  *
14  * Definition items:
15  * - options callback: The function to call in order to generate the value options. If omitted, the options 'Yes' and 'No' will be used.
16  * - options arguments: An array of arguments to pass to the options callback.
17  *
18  * @ingroup views_filter_handlers
19  *
20  * @ViewsFilter("in_operator")
21  */
22 class InOperator extends FilterPluginBase {
23
24   protected $valueFormType = 'checkboxes';
25
26   /**
27    * @var array
28    * Stores all operations which are available on the form.
29    */
30   protected $valueOptions = NULL;
31
32   /**
33    * The filter title.
34    *
35    * @var string
36    */
37   protected $valueTitle;
38
39   /**
40    * {@inheritdoc}
41    */
42   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
43     parent::init($view, $display, $options);
44
45     $this->valueTitle = $this->t('Options');
46     $this->valueOptions = NULL;
47   }
48
49   /**
50    * Child classes should be used to override this function and set the
51    * 'value options', unless 'options callback' is defined as a valid function
52    * or static public method to generate these values.
53    *
54    * This can use a guard to be used to reduce database hits as much as
55    * possible.
56    *
57    * @return array|null
58    *   The stored values from $this->valueOptions.
59    */
60   public function getValueOptions() {
61     if (isset($this->valueOptions)) {
62       return $this->valueOptions;
63     }
64
65     if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) {
66       if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) {
67         $this->valueOptions = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']);
68       }
69       else {
70         $this->valueOptions = call_user_func($this->definition['options callback']);
71       }
72     }
73     else {
74       $this->valueOptions = [t('Yes'), $this->t('No')];
75     }
76
77     return $this->valueOptions;
78   }
79
80   public function defaultExposeOptions() {
81     parent::defaultExposeOptions();
82     $this->options['expose']['reduce'] = FALSE;
83   }
84
85   public function buildExposeForm(&$form, FormStateInterface $form_state) {
86     parent::buildExposeForm($form, $form_state);
87     $form['expose']['reduce'] = [
88       '#type' => 'checkbox',
89       '#title' => $this->t('Limit list to selected items'),
90       '#description' => $this->t('If checked, the only items presented to the user will be the ones selected here.'),
91       // Safety.
92       '#default_value' => !empty($this->options['expose']['reduce']),
93     ];
94   }
95
96   protected function defineOptions() {
97     $options = parent::defineOptions();
98
99     $options['operator']['default'] = 'in';
100     $options['value']['default'] = [];
101     $options['expose']['contains']['reduce'] = ['default' => FALSE];
102
103     return $options;
104   }
105
106   /**
107    * This kind of construct makes it relatively easy for a child class
108    * to add or remove functionality by overriding this function and
109    * adding/removing items from this array.
110    */
111   public function operators() {
112     $operators = [
113       'in' => [
114         'title' => $this->t('Is one of'),
115         'short' => $this->t('in'),
116         'short_single' => $this->t('='),
117         'method' => 'opSimple',
118         'values' => 1,
119       ],
120       'not in' => [
121         'title' => $this->t('Is not one of'),
122         'short' => $this->t('not in'),
123         'short_single' => $this->t('<>'),
124         'method' => 'opSimple',
125         'values' => 1,
126       ],
127     ];
128     // if the definition allows for the empty operator, add it.
129     if (!empty($this->definition['allow empty'])) {
130       $operators += [
131         'empty' => [
132           'title' => $this->t('Is empty (NULL)'),
133           'method' => 'opEmpty',
134           'short' => $this->t('empty'),
135           'values' => 0,
136         ],
137         'not empty' => [
138           'title' => $this->t('Is not empty (NOT NULL)'),
139           'method' => 'opEmpty',
140           'short' => $this->t('not empty'),
141           'values' => 0,
142         ],
143       ];
144     }
145
146     return $operators;
147   }
148
149   /**
150    * Build strings from the operators() for 'select' options
151    */
152   public function operatorOptions($which = 'title') {
153     $options = [];
154     foreach ($this->operators() as $id => $info) {
155       $options[$id] = $info[$which];
156     }
157
158     return $options;
159   }
160
161   protected function operatorValues($values = 1) {
162     $options = [];
163     foreach ($this->operators() as $id => $info) {
164       if (isset($info['values']) && $info['values'] == $values) {
165         $options[] = $id;
166       }
167     }
168
169     return $options;
170   }
171
172   protected function valueForm(&$form, FormStateInterface $form_state) {
173     $form['value'] = [];
174     $options = [];
175
176     $exposed = $form_state->get('exposed');
177     if (!$exposed) {
178       // Add a select all option to the value form.
179       $options = ['all' => $this->t('Select all')];
180     }
181
182     $this->getValueOptions();
183     $options += $this->valueOptions;
184     $default_value = (array) $this->value;
185
186     $which = 'all';
187     if (!empty($form['operator'])) {
188       $source = ':input[name="options[operator]"]';
189     }
190     if ($exposed) {
191       $identifier = $this->options['expose']['identifier'];
192
193       if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
194         // exposed and locked.
195         $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
196       }
197       else {
198         $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
199       }
200
201       if (!empty($this->options['expose']['reduce'])) {
202         $options = $this->reduceValueOptions();
203
204         if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
205           $default_value = [];
206         }
207       }
208
209       if (empty($this->options['expose']['multiple'])) {
210         if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce'])) || isset($this->options['value']['all'])) {
211           $default_value = 'All';
212         }
213         elseif (empty($default_value)) {
214           $keys = array_keys($options);
215           $default_value = array_shift($keys);
216         }
217         else {
218           $copy = $default_value;
219           $default_value = array_shift($copy);
220         }
221       }
222     }
223
224     if ($which == 'all' || $which == 'value') {
225       $form['value'] = [
226         '#type' => $this->valueFormType,
227         '#title' => $this->valueTitle,
228         '#options' => $options,
229         '#default_value' => $default_value,
230         // These are only valid for 'select' type, but do no harm to checkboxes.
231         '#multiple' => TRUE,
232         '#size' => count($options) > 8 ? 8 : count($options),
233       ];
234       $user_input = $form_state->getUserInput();
235       if ($exposed && !isset($user_input[$identifier])) {
236         $user_input[$identifier] = $default_value;
237         $form_state->setUserInput($user_input);
238       }
239
240       if ($which == 'all') {
241         if (!$exposed && (in_array($this->valueFormType, ['checkbox', 'checkboxes', 'radios', 'select']))) {
242           $form['value']['#prefix'] = '<div id="edit-options-value-wrapper">';
243           $form['value']['#suffix'] = '</div>';
244         }
245         // Setup #states for all operators with one value.
246         foreach ($this->operatorValues(1) as $operator) {
247           $form['value']['#states']['visible'][] = [
248             $source => ['value' => $operator],
249           ];
250         }
251       }
252     }
253   }
254
255   /**
256    * When using exposed filters, we may be required to reduce the set.
257    */
258   public function reduceValueOptions($input = NULL) {
259     if (!isset($input)) {
260       $input = $this->valueOptions;
261     }
262
263     // Because options may be an array of strings, or an array of mixed arrays
264     // and strings (optgroups) or an array of objects, we have to
265     // step through and handle each one individually.
266     $options = [];
267     foreach ($input as $id => $option) {
268       if (is_array($option)) {
269         $options[$id] = $this->reduceValueOptions($option);
270         continue;
271       }
272       elseif (is_object($option)) {
273         $keys = array_keys($option->option);
274         $key = array_shift($keys);
275         if (isset($this->options['value'][$key])) {
276           $options[$id] = $option;
277         }
278       }
279       elseif (isset($this->options['value'][$id])) {
280         $options[$id] = $option;
281       }
282     }
283     return $options;
284   }
285
286   /**
287    * {@inheritdoc}
288    */
289   public function acceptExposedInput($input) {
290     if (empty($this->options['exposed'])) {
291       return TRUE;
292     }
293
294     // The "All" state for this type of filter could have a default value. If
295     // this is a non-multiple and non-required option, then this filter will
296     // participate by using the default settings *if* 'limit' is true.
297     if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) {
298       $identifier = $this->options['expose']['identifier'];
299       if ($input[$identifier] == 'All') {
300         return TRUE;
301       }
302     }
303
304     return parent::acceptExposedInput($input);
305   }
306
307   protected function valueSubmit($form, FormStateInterface $form_state) {
308     // Drupal's FAPI system automatically puts '0' in for any checkbox that
309     // was not set, and the key to the checkbox if it is set.
310     // Unfortunately, this means that if the key to that checkbox is 0,
311     // we are unable to tell if that checkbox was set or not.
312
313     // Luckily, the '#value' on the checkboxes form actually contains
314     // *only* a list of checkboxes that were set, and we can use that
315     // instead.
316
317     $form_state->setValue(['options', 'value'], $form['value']['#value']);
318   }
319
320   public function adminSummary() {
321     if ($this->isAGroup()) {
322       return $this->t('grouped');
323     }
324     if (!empty($this->options['exposed'])) {
325       return $this->t('exposed');
326     }
327     $info = $this->operators();
328
329     $this->getValueOptions();
330     // Some filter_in_operator usage uses optgroups forms, so flatten it.
331     $flat_options = OptGroup::flattenOptions($this->valueOptions);
332
333     if (!is_array($this->value)) {
334       return;
335     }
336
337     $operator = $info[$this->operator]['short'];
338     $values = '';
339     if (in_array($this->operator, $this->operatorValues(1))) {
340       // Remove every element which is not known.
341       foreach ($this->value as $value) {
342         if (!isset($flat_options[$value])) {
343           unset($this->value[$value]);
344         }
345       }
346       // Choose different kind of output for 0, a single and multiple values.
347       if (count($this->value) == 0) {
348         $values = $this->t('Unknown');
349       }
350       elseif (count($this->value) == 1) {
351         // If any, use the 'single' short name of the operator instead.
352         if (isset($info[$this->operator]['short_single'])) {
353           $operator = $info[$this->operator]['short_single'];
354         }
355
356         $keys = $this->value;
357         $value = array_shift($keys);
358         if (isset($flat_options[$value])) {
359           $values = $flat_options[$value];
360         }
361         else {
362           $values = '';
363         }
364       }
365       else {
366         foreach ($this->value as $value) {
367           if ($values !== '') {
368             $values .= ', ';
369           }
370           if (Unicode::strlen($values) > 8) {
371             $values = Unicode::truncate($values, 8, FALSE, TRUE);
372             break;
373           }
374           if (isset($flat_options[$value])) {
375             $values .= $flat_options[$value];
376           }
377         }
378       }
379     }
380
381     return $operator . (($values !== '') ? ' ' . $values : '');
382   }
383
384   public function query() {
385     $info = $this->operators();
386     if (!empty($info[$this->operator]['method'])) {
387       $this->{$info[$this->operator]['method']}();
388     }
389   }
390
391   protected function opSimple() {
392     if (empty($this->value)) {
393       return;
394     }
395     $this->ensureMyTable();
396
397     // We use array_values() because the checkboxes keep keys and that can cause
398     // array addition problems.
399     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", array_values($this->value), $this->operator);
400   }
401
402   protected function opEmpty() {
403     $this->ensureMyTable();
404     if ($this->operator == 'empty') {
405       $operator = "IS NULL";
406     }
407     else {
408       $operator = "IS NOT NULL";
409     }
410
411     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", NULL, $operator);
412   }
413
414   public function validate() {
415     $this->getValueOptions();
416     $errors = parent::validate();
417
418     // If the operator is an operator which doesn't require a value, there is
419     // no need for additional validation.
420     if (in_array($this->operator, $this->operatorValues(0))) {
421       return [];
422     }
423
424     if (!in_array($this->operator, $this->operatorValues(1))) {
425       $errors[] = $this->t('The operator is invalid on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
426     }
427     if (is_array($this->value)) {
428       if (!isset($this->valueOptions)) {
429         // Don't validate if there are none value options provided, for example for special handlers.
430         return $errors;
431       }
432       if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) {
433         // Don't validate if the field is exposed and no default value is provided.
434         return $errors;
435       }
436
437       // Some filter_in_operator usage uses optgroups forms, so flatten it.
438       $flat_options = OptGroup::flattenOptions($this->valueOptions);
439
440       // Remove every element which is not known.
441       foreach ($this->value as $value) {
442         if (!isset($flat_options[$value])) {
443           unset($this->value[$value]);
444         }
445       }
446       // Choose different kind of output for 0, a single and multiple values.
447       if (count($this->value) == 0) {
448         $errors[] = $this->t('No valid values found on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
449       }
450     }
451     elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) {
452       $errors[] = $this->t('The value @value is not an array for @operator on filter: @filter', ['@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE)]);
453     }
454     return $errors;
455   }
456
457 }