Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / views / src / ManyToOneHelper.php
1 <?php
2
3 namespace Drupal\views;
4
5 use Drupal\Core\Database\Query\Condition;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\views\Plugin\views\HandlerBase;
8
9 /**
10  * This many to one helper object is used on both arguments and filters.
11  *
12  * @todo This requires extensive documentation on how this class is to
13  * be used. For now, look at the arguments and filters that use it. Lots
14  * of stuff is just pass-through but there are definitely some interesting
15  * areas where they interact.
16  *
17  * Any handler that uses this can have the following possibly additional
18  * definition terms:
19  * - numeric: If true, treat this field as numeric, using %d instead of %s in
20  *            queries.
21  */
22 class ManyToOneHelper {
23
24   public function __construct($handler) {
25     $this->handler = $handler;
26   }
27
28   public static function defineOptions(&$options) {
29     $options['reduce_duplicates'] = ['default' => FALSE];
30   }
31
32   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
33     $form['reduce_duplicates'] = [
34       '#type' => 'checkbox',
35       '#title' => t('Reduce duplicates'),
36       '#description' => t("This filter can cause items that have more than one of the selected options to appear as duplicate results. If this filter causes duplicate results to occur, this checkbox can reduce those duplicates; however, the more terms it has to search for, the less performant the query will be, so use this with caution. Shouldn't be set on single-value fields, as it may cause values to disappear from display, if used on an incompatible field."),
37       '#default_value' => !empty($this->handler->options['reduce_duplicates']),
38       '#weight' => 4,
39     ];
40   }
41
42   /**
43    * Sometimes the handler might want us to use some kind of formula, so give
44    * it that option. If it wants us to do this, it must set $helper->formula = TRUE
45    * and implement handler->getFormula();
46    */
47   public function getField() {
48     if (!empty($this->formula)) {
49       return $this->handler->getFormula();
50     }
51     else {
52       return $this->handler->tableAlias . '.' . $this->handler->realField;
53     }
54   }
55
56   /**
57    * Add a table to the query.
58    *
59    * This is an advanced concept; not only does it add a new instance of the table,
60    * but it follows the relationship path all the way down to the relationship
61    * link point and adds *that* as a new relationship and then adds the table to
62    * the relationship, if necessary.
63    */
64   public function addTable($join = NULL, $alias = NULL) {
65     // This is used for lookups in the many_to_one table.
66     $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
67
68     if (empty($join)) {
69       $join = $this->getJoin();
70     }
71
72     // See if there's a chain between us and the base relationship. If so, we need
73     // to create a new relationship to use.
74     $relationship = $this->handler->relationship;
75
76     // Determine the primary table to seek
77     if (empty($this->handler->query->relationships[$relationship])) {
78       $base_table = $this->handler->view->storage->get('base_table');
79     }
80     else {
81       $base_table = $this->handler->query->relationships[$relationship]['base'];
82     }
83
84     // Cycle through the joins. This isn't as error-safe as the normal
85     // ensurePath logic. Perhaps it should be.
86     $r_join = clone $join;
87     while ($r_join->leftTable != $base_table) {
88       $r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
89     }
90     // If we found that there are tables in between, add the relationship.
91     if ($r_join->table != $join->table) {
92       $relationship = $this->handler->query->addRelationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
93     }
94
95     // And now add our table, using the new relationship if one was used.
96     $alias = $this->handler->query->addTable($this->handler->table, $relationship, $join, $alias);
97
98     // Store what values are used by this table chain so that other chains can
99     // automatically discard those values.
100     if (empty($this->handler->view->many_to_one_tables[$field])) {
101       $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
102     }
103     else {
104       $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
105     }
106
107     return $alias;
108   }
109
110   public function getJoin() {
111     return $this->handler->getJoin();
112   }
113
114   /**
115    * Provide the proper join for summary queries. This is important in part because
116    * it will cooperate with other arguments if possible.
117    */
118   public function summaryJoin() {
119     $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
120     $join = $this->getJoin();
121
122     // shortcuts
123     $options = $this->handler->options;
124     $view = $this->handler->view;
125     $query = $this->handler->query;
126
127     if (!empty($options['require_value'])) {
128       $join->type = 'INNER';
129     }
130
131     if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
132       return $query->ensureTable($this->handler->table, $this->handler->relationship, $join);
133     }
134     else {
135       if (!empty($view->many_to_one_tables[$field])) {
136         foreach ($view->many_to_one_tables[$field] as $value) {
137           $join->extra = [
138             [
139               'field' => $this->handler->realField,
140               'operator' => '!=',
141               'value' => $value,
142               'numeric' => !empty($this->definition['numeric']),
143             ],
144           ];
145         }
146       }
147       return $this->addTable($join);
148     }
149   }
150
151   /**
152    * Override ensureMyTable so we can control how this joins in.
153    * The operator actually has influence over joining.
154    */
155   public function ensureMyTable() {
156     if (!isset($this->handler->tableAlias)) {
157       // Case 1: Operator is an 'or' and we're not reducing duplicates.
158       // We hence get the absolute simplest:
159       $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
160       if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
161         if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
162           // query optimization, INNER joins are slightly faster, so use them
163           // when we know we can.
164           $join = $this->getJoin();
165           if (isset($join)) {
166             $join->type = 'INNER';
167           }
168           $this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
169           $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
170         }
171         else {
172           $join = $this->getJoin();
173           $join->type = 'LEFT';
174           if (!empty($this->handler->view->many_to_one_tables[$field])) {
175             foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
176               $join->extra = [
177                 [
178                   'field' => $this->handler->realField,
179                   'operator' => '!=',
180                   'value' => $value,
181                   'numeric' => !empty($this->handler->definition['numeric']),
182                 ],
183               ];
184             }
185           }
186
187           $this->handler->tableAlias = $this->addTable($join);
188         }
189
190         return $this->handler->tableAlias;
191       }
192
193       // Case 2: it's an 'and' or an 'or'.
194       // We do one join per selected value.
195       if ($this->handler->operator != 'not') {
196         // Clone the join for each table:
197         $this->handler->tableAliases = [];
198         foreach ($this->handler->value as $value) {
199           $join = $this->getJoin();
200           if ($this->handler->operator == 'and') {
201             $join->type = 'INNER';
202           }
203           $join->extra = [
204             [
205               'field' => $this->handler->realField,
206               'value' => $value,
207               'numeric' => !empty($this->handler->definition['numeric']),
208             ],
209           ];
210
211           // The table alias needs to be unique to this value across the
212           // multiple times the filter or argument is called by the view.
213           if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
214             if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
215               $this->handler->view->many_to_one_count[$this->handler->table] = 0;
216             }
217             $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
218           }
219
220           $this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
221           // Set tableAlias to the first of these.
222           if (empty($this->handler->tableAlias)) {
223             $this->handler->tableAlias = $this->handler->tableAliases[$value];
224           }
225         }
226       }
227       // Case 3: it's a 'not'.
228       // We just do one join. We'll add a where clause during
229       // the query phase to ensure that $table.$field IS NULL.
230       else {
231         $join = $this->getJoin();
232         $join->type = 'LEFT';
233         $join->extra = [];
234         $join->extraOperator = 'OR';
235         foreach ($this->handler->value as $value) {
236           $join->extra[] = [
237             'field' => $this->handler->realField,
238             'value' => $value,
239             'numeric' => !empty($this->handler->definition['numeric']),
240           ];
241         }
242
243         $this->handler->tableAlias = $this->addTable($join);
244       }
245     }
246     return $this->handler->tableAlias;
247   }
248
249   /**
250    * Provides a unique placeholders for handlers.
251    */
252   protected function placeholder() {
253     return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
254   }
255
256   public function addFilter() {
257     if (empty($this->handler->value)) {
258       return;
259     }
260     $this->handler->ensureMyTable();
261
262     // Shorten some variables:
263     $field = $this->getField();
264     $options = $this->handler->options;
265     $operator = $this->handler->operator;
266     $formula = !empty($this->formula);
267     $value = $this->handler->value;
268     if (empty($options['group'])) {
269       $options['group'] = 0;
270     }
271
272     // If $add_condition is set to FALSE, a single expression is enough. If it
273     // is set to TRUE, conditions will be added.
274     $add_condition = TRUE;
275     if ($operator == 'not') {
276       $value = NULL;
277       $operator = 'IS NULL';
278       $add_condition = FALSE;
279     }
280     elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
281       if (count($value) > 1) {
282         $operator = 'IN';
283       }
284       else {
285         $value = is_array($value) ? array_pop($value) : $value;
286         $operator = '=';
287       }
288       $add_condition = FALSE;
289     }
290
291     if (!$add_condition) {
292       if ($formula) {
293         $placeholder = $this->placeholder();
294         if ($operator == 'IN') {
295           $operator = "$operator IN($placeholder)";
296         }
297         else {
298           $operator = "$operator $placeholder";
299         }
300         $placeholders = [
301           $placeholder => $value,
302         ];
303         $this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
304       }
305       else {
306         $placeholder = $this->placeholder();
307         if (count($this->handler->value) > 1) {
308           $placeholder .= '[]';
309
310           if ($operator == 'IS NULL') {
311             $this->handler->query->addWhereExpression(0, "$field $operator");
312           }
313           else {
314             $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", [$placeholder => $value]);
315           }
316         }
317         else {
318           if ($operator == 'IS NULL') {
319             $this->handler->query->addWhereExpression(0, "$field $operator");
320           }
321           else {
322             $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", [$placeholder => $value]);
323           }
324         }
325       }
326     }
327
328     if ($add_condition) {
329       $field = $this->handler->realField;
330       $clause = $operator == 'or' ? new Condition('OR') : new Condition('AND');
331       foreach ($this->handler->tableAliases as $value => $alias) {
332         $clause->condition("$alias.$field", $value);
333       }
334
335       // implode on either AND or OR.
336       $this->handler->query->addWhere($options['group'], $clause);
337     }
338   }
339
340 }