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