Upgraded drupal core with security updates
[yaffs-website] / web / core / modules / views / src / Plugin / views / HandlerBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\Unicode;
7 use Drupal\Component\Utility\UrlHelper;
8 use Drupal\Component\Utility\Xss;
9 use Drupal\Core\Extension\ModuleHandlerInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Render\Element;
12 use Drupal\Core\Session\AccountInterface;
13 use Drupal\views\Plugin\views\display\DisplayPluginBase;
14 use Drupal\views\Render\ViewsRenderPipelineMarkup;
15 use Drupal\views\ViewExecutable;
16 use Drupal\views\Views;
17 use Drupal\views\ViewsData;
18
19 /**
20  * Base class for Views handler plugins.
21  *
22  * @ingroup views_plugins
23  */
24 abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface {
25
26   /**
27    * Where the $query object will reside:
28    *
29    * @var \Drupal\views\Plugin\views\query\QueryPluginBase
30    */
31   public $query = NULL;
32
33   /**
34    * The table this handler is attached to.
35    *
36    * @var string
37    */
38   public $table;
39
40   /**
41    * The alias of the table of this handler which is used in the query.
42    *
43    * @var string
44    */
45   public $tableAlias;
46
47   /**
48    * The actual field in the database table, maybe different
49    * on other kind of query plugins/special handlers.
50    *
51    * @var string
52    */
53   public $realField;
54
55   /**
56    * With field you can override the realField if the real field is not set.
57    *
58    * @var string
59    */
60   public $field;
61
62   /**
63    * The relationship used for this field.
64    *
65    * @var string
66    */
67   public $relationship = NULL;
68
69   /**
70    * The module handler.
71    *
72    * @var \Drupal\Core\Extension\ModuleHandlerInterface
73    */
74   protected $moduleHandler;
75
76   /**
77    * The views data service.
78    *
79    * @var \Drupal\views\ViewsData
80    */
81   protected $viewsData;
82
83   /**
84    * Constructs a Handler object.
85    *
86    * @param array $configuration
87    *   A configuration array containing information about the plugin instance.
88    * @param string $plugin_id
89    *   The plugin_id for the plugin instance.
90    * @param mixed $plugin_definition
91    *   The plugin implementation definition.
92    */
93   public function __construct(array $configuration, $plugin_id, $plugin_definition) {
94     parent::__construct($configuration, $plugin_id, $plugin_definition);
95     $this->is_handler = TRUE;
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
102     parent::init($view, $display, $options);
103
104     // Check to see if this handler type is defaulted. Note that
105     // we have to do a lookup because the type is singular but the
106     // option is stored as the plural.
107
108     $this->unpackOptions($this->options, $options);
109
110     // This exist on most handlers, but not all. So they are still optional.
111     if (isset($options['table'])) {
112       $this->table = $options['table'];
113     }
114
115     // Allow aliases on both fields and tables.
116     if (isset($this->definition['real table'])) {
117       $this->table = $this->definition['real table'];
118     }
119
120     if (isset($this->definition['real field'])) {
121       $this->realField = $this->definition['real field'];
122     }
123
124     if (isset($this->definition['field'])) {
125       $this->realField = $this->definition['field'];
126     }
127
128     if (isset($options['field'])) {
129       $this->field = $options['field'];
130       if (!isset($this->realField)) {
131         $this->realField = $options['field'];
132       }
133     }
134
135     $this->query = &$view->query;
136   }
137
138   protected function defineOptions() {
139     $options = parent::defineOptions();
140
141     $options['id'] = ['default' => ''];
142     $options['table'] = ['default' => ''];
143     $options['field'] = ['default' => ''];
144     $options['relationship'] = ['default' => 'none'];
145     $options['group_type'] = ['default' => 'group'];
146     $options['admin_label'] = ['default' => ''];
147
148     return $options;
149   }
150
151   /**
152    * {@inheritdoc}
153    */
154   public function adminLabel($short = FALSE) {
155     if (!empty($this->options['admin_label'])) {
156       return $this->options['admin_label'];
157     }
158     $title = ($short && isset($this->definition['title short'])) ? $this->definition['title short'] : $this->definition['title'];
159     return $this->t('@group: @title', ['@group' => $this->definition['group'], '@title' => $title]);
160   }
161
162   /**
163    * {@inheritdoc}
164    */
165   public function getField($field = NULL) {
166     if (!isset($field)) {
167       if (!empty($this->formula)) {
168         $field = $this->getFormula();
169       }
170       else {
171         $field = $this->tableAlias . '.' . $this->realField;
172       }
173     }
174
175     // If grouping, check to see if the aggregation method needs to modify the field.
176     if ($this->view->display_handler->useGroupBy()) {
177       $this->view->initQuery();
178       if ($this->query) {
179         $info = $this->query->getAggregationInfo();
180         if (!empty($info[$this->options['group_type']]['method'])) {
181           $method = $info[$this->options['group_type']]['method'];
182           if (method_exists($this->query, $method)) {
183             return $this->query->$method($this->options['group_type'], $field);
184           }
185         }
186       }
187     }
188
189     return $field;
190   }
191
192   /**
193    * {@inheritdoc}
194    */
195   public function sanitizeValue($value, $type = NULL) {
196     switch ($type) {
197       case 'xss':
198         $value = Xss::filter($value);
199         break;
200       case 'xss_admin':
201         $value = Xss::filterAdmin($value);
202         break;
203       case 'url':
204         $value = Html::escape(UrlHelper::stripDangerousProtocols($value));
205         break;
206       default:
207         $value = Html::escape($value);
208         break;
209     }
210     return ViewsRenderPipelineMarkup::create($value);
211   }
212
213   /**
214    * Transform a string by a certain method.
215    *
216    * @param $string
217    *    The input you want to transform.
218    * @param $option
219    *    How do you want to transform it, possible values:
220    *      - upper: Uppercase the string.
221    *      - lower: lowercase the string.
222    *      - ucfirst: Make the first char uppercase.
223    *      - ucwords: Make each word in the string uppercase.
224    *
225    * @return string
226    *    The transformed string.
227    */
228   protected function caseTransform($string, $option) {
229     switch ($option) {
230       default:
231         return $string;
232       case 'upper':
233         return Unicode::strtoupper($string);
234       case 'lower':
235         return Unicode::strtolower($string);
236       case 'ucfirst':
237         return Unicode::ucfirst($string);
238       case 'ucwords':
239         return Unicode::ucwords($string);
240     }
241   }
242
243   /**
244    * {@inheritdoc}
245    */
246   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
247     // Some form elements belong in a fieldset for presentation, but can't
248     // be moved into one because of the $form_state->getValues() hierarchy. Those
249     // elements can add a #fieldset => 'fieldset_name' property, and they'll
250     // be moved to their fieldset during pre_render.
251     $form['#pre_render'][] = [get_class($this), 'preRenderAddFieldsetMarkup'];
252
253     parent::buildOptionsForm($form, $form_state);
254
255     $form['fieldsets'] = [
256       '#type' => 'value',
257       '#value' => ['more', 'admin_label'],
258     ];
259
260     $form['admin_label'] = [
261       '#type' => 'details',
262       '#title' => $this->t('Administrative title'),
263       '#weight' => 150,
264     ];
265     $form['admin_label']['admin_label'] = [
266       '#type' => 'textfield',
267       '#title' => $this->t('Administrative title'),
268       '#description' => $this->t('This title will be displayed on the views edit page instead of the default one. This might be useful if you have the same item twice.'),
269       '#default_value' => $this->options['admin_label'],
270       '#parents' => ['options', 'admin_label'],
271     ];
272
273     // This form is long and messy enough that the "Administrative title" option
274     // belongs in "Administrative title" fieldset at the bottom of the form.
275     $form['more'] = [
276       '#type' => 'details',
277       '#title' => $this->t('More'),
278       '#weight' => 200,
279       '#optional' => TRUE,
280     ];
281
282     // Allow to alter the default values brought into the form.
283     // @todo Do we really want to keep this hook.
284     $this->getModuleHandler()->alter('views_handler_options', $this->options, $this->view);
285   }
286
287   /**
288    * Gets the module handler.
289    *
290    * @return \Drupal\Core\Extension\ModuleHandlerInterface
291    */
292   protected function getModuleHandler() {
293     if (!$this->moduleHandler) {
294       $this->moduleHandler = \Drupal::moduleHandler();
295     }
296
297     return $this->moduleHandler;
298   }
299
300   /**
301    * Sets the module handler.
302    *
303    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
304    *   The module handler.
305    */
306   public function setModuleHandler(ModuleHandlerInterface $module_handler) {
307     $this->moduleHandler = $module_handler;
308   }
309
310   /**
311    * Provides the handler some groupby.
312    */
313   public function usesGroupBy() {
314     return TRUE;
315   }
316   /**
317    * Provide a form for aggregation settings.
318    */
319   public function buildGroupByForm(&$form, FormStateInterface $form_state) {
320     $display_id = $form_state->get('display_id');
321     $type = $form_state->get('type');
322     $id = $form_state->get('id');
323
324     $form['#section'] = $display_id . '-' . $type . '-' . $id;
325
326     $this->view->initQuery();
327     $info = $this->view->query->getAggregationInfo();
328     foreach ($info as $id => $aggregate) {
329       $group_types[$id] = $aggregate['title'];
330     }
331
332     $form['group_type'] = [
333       '#type' => 'select',
334       '#title' => $this->t('Aggregation type'),
335       '#default_value' => $this->options['group_type'],
336       '#description' => $this->t('Select the aggregation function to use on this field.'),
337       '#options' => $group_types,
338     ];
339   }
340
341   /**
342    * Perform any necessary changes to the form values prior to storage.
343    * There is no need for this function to actually store the data.
344    */
345   public function submitGroupByForm(&$form, FormStateInterface $form_state) {
346     $form_state->get('handler')->options['group_type'] = $form_state->getValue(['options', 'group_type']);
347   }
348
349   /**
350    * If a handler has 'extra options' it will get a little settings widget and
351    * another form called extra_options.
352    */
353   public function hasExtraOptions() { return FALSE; }
354
355   /**
356    * Provide defaults for the handler.
357    */
358   public function defineExtraOptions(&$option) { }
359
360   /**
361    * Provide a form for setting options.
362    */
363   public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) { }
364
365   /**
366    * Validate the options form.
367    */
368   public function validateExtraOptionsForm($form, FormStateInterface $form_state) { }
369
370   /**
371    * Perform any necessary changes to the form values prior to storage.
372    * There is no need for this function to actually store the data.
373    */
374   public function submitExtraOptionsForm($form, FormStateInterface $form_state) { }
375
376   /**
377    * Determine if a handler can be exposed.
378    */
379   public function canExpose() { return FALSE; }
380
381   /**
382    * Set new exposed option defaults when exposed setting is flipped
383    * on.
384    */
385   public function defaultExposeOptions() { }
386
387   /**
388    * Get information about the exposed form for the form renderer.
389    */
390   public function exposedInfo() { }
391
392   /**
393    * Render our chunk of the exposed handler form when selecting
394    */
395   public function buildExposedForm(&$form, FormStateInterface $form_state) { }
396
397   /**
398    * Validate the exposed handler form
399    */
400   public function validateExposed(&$form, FormStateInterface $form_state) { }
401
402   /**
403    * Submit the exposed handler form
404    */
405   public function submitExposed(&$form, FormStateInterface $form_state) { }
406
407   /**
408    * Form for exposed handler options.
409    */
410   public function buildExposeForm(&$form, FormStateInterface $form_state) { }
411
412   /**
413    * Validate the options form.
414    */
415   public function validateExposeForm($form, FormStateInterface $form_state) { }
416
417   /**
418    * Perform any necessary changes to the form exposes prior to storage.
419    * There is no need for this function to actually store the data.
420    */
421   public function submitExposeForm($form, FormStateInterface $form_state) { }
422
423   /**
424    * Shortcut to display the expose/hide button.
425    */
426   public function showExposeButton(&$form, FormStateInterface $form_state) { }
427
428   /**
429    * Shortcut to display the exposed options form.
430    */
431   public function showExposeForm(&$form, FormStateInterface $form_state) {
432     if (empty($this->options['exposed'])) {
433       return;
434     }
435
436     $this->buildExposeForm($form, $form_state);
437
438     // When we click the expose button, we add new gadgets to the form but they
439     // have no data in POST so their defaults get wiped out. This prevents
440     // these defaults from getting wiped out. This setting will only be TRUE
441     // during a 2nd pass rerender.
442     if ($form_state->get('force_expose_options')) {
443       foreach (Element::children($form['expose']) as $id) {
444         if (isset($form['expose'][$id]['#default_value']) && !isset($form['expose'][$id]['#value'])) {
445           $form['expose'][$id]['#value'] = $form['expose'][$id]['#default_value'];
446         }
447       }
448     }
449   }
450
451   /**
452    * {@inheritdoc}
453    */
454   public function access(AccountInterface $account) {
455     if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) {
456       if (isset($this->definition['access arguments']) && is_array($this->definition['access arguments'])) {
457         return call_user_func_array($this->definition['access callback'], [$account] + $this->definition['access arguments']);
458       }
459       return $this->definition['access callback']($account);
460     }
461
462     return TRUE;
463   }
464
465   /**
466    * {@inheritdoc}
467    */
468   public function preQuery() {
469   }
470
471   /**
472    * {@inheritdoc}
473    */
474   public function query() {
475   }
476
477   /**
478    * {@inheritdoc}
479    */
480   public function postExecute(&$values) { }
481
482   /**
483    * Provides a unique placeholders for handlers.
484    *
485    * @return string
486    *   A placeholder which contains the table and the fieldname.
487    */
488   protected function placeholder() {
489     return $this->query->placeholder($this->table . '_' . $this->field);
490   }
491
492   /**
493    * {@inheritdoc}
494    */
495   public function setRelationship() {
496     // Ensure this gets set to something.
497     $this->relationship = NULL;
498
499     // Don't process non-existent relationships.
500     if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') {
501       return;
502     }
503
504     $relationship = $this->options['relationship'];
505
506     // Ignore missing/broken relationships.
507     if (empty($this->view->relationship[$relationship])) {
508       return;
509     }
510
511     // Check to see if the relationship has already processed. If not, then we
512     // cannot process it.
513     if (empty($this->view->relationship[$relationship]->alias)) {
514       return;
515     }
516
517     // Finally!
518     $this->relationship = $this->view->relationship[$relationship]->alias;
519   }
520
521   /**
522    * {@inheritdoc}
523    */
524   public function ensureMyTable() {
525     if (!isset($this->tableAlias)) {
526       $this->tableAlias = $this->query->ensureTable($this->table, $this->relationship);
527     }
528     return $this->tableAlias;
529   }
530
531   /**
532    * {@inheritdoc}
533    */
534   public function adminSummary() { }
535
536   /**
537    * Determine if this item is 'exposed', meaning it provides form elements
538    * to let users modify the view.
539    *
540    * @return bool
541    */
542   public function isExposed() {
543     return !empty($this->options['exposed']);
544   }
545
546   /**
547    * Returns TRUE if the exposed filter works like a grouped filter.
548    */
549   public function isAGroup() { return FALSE; }
550
551   /**
552    * Define if the exposed input has to be submitted multiple times.
553    * This is TRUE when exposed filters grouped are using checkboxes as
554    * widgets.
555    */
556   public function multipleExposedInput() { return FALSE; }
557
558   /**
559    * Take input from exposed handlers and assign to this handler, if necessary.
560    */
561   public function acceptExposedInput($input) { return TRUE; }
562
563   /**
564    * If set to remember exposed input in the session, store it there.
565    */
566   public function storeExposedInput($input, $status) { return TRUE; }
567
568   /**
569    * {@inheritdoc}
570    */
571   public function getJoin() {
572     // get the join from this table that links back to the base table.
573     // Determine the primary table to seek
574     if (empty($this->query->relationships[$this->relationship])) {
575       $base_table = $this->view->storage->get('base_table');
576     }
577     else {
578       $base_table = $this->query->relationships[$this->relationship]['base'];
579     }
580
581     $join = $this->getTableJoin($this->table, $base_table);
582     if ($join) {
583       return clone $join;
584     }
585   }
586
587   /**
588    * {@inheritdoc}
589    */
590   public function validate() { return []; }
591
592   /**
593    * {@inheritdoc}
594    */
595   public function broken() {
596     return FALSE;
597   }
598
599   /**
600    * Creates cross-database SQL date formatting.
601    *
602    * @param string $format
603    *   A format string for the result, like 'Y-m-d H:i:s'.
604    *
605    * @return string
606    *   An appropriate SQL string for the DB type and field type.
607    */
608   public function getDateFormat($format) {
609     return $this->query->getDateFormat($this->getDateField(), $format);
610   }
611
612   /**
613    * Creates cross-database SQL dates.
614    *
615    * @return string
616    *   An appropriate SQL string for the db type and field type.
617    */
618   public function getDateField() {
619     return $this->query->getDateField("$this->tableAlias.$this->realField");
620   }
621
622   /**
623    * Gets views data service.
624    *
625    * @return \Drupal\views\ViewsData
626    */
627   protected function getViewsData() {
628     if (!$this->viewsData) {
629       $this->viewsData = Views::viewsData();
630     }
631
632     return $this->viewsData;
633   }
634
635   /**
636    * {@inheritdoc}
637    */
638   public function setViewsData(ViewsData $views_data) {
639     $this->viewsData = $views_data;
640   }
641
642   /**
643    * {@inheritdoc}
644    */
645   public static function getTableJoin($table, $base_table) {
646     $data = Views::viewsData()->get($table);
647     if (isset($data['table']['join'][$base_table])) {
648       $join_info = $data['table']['join'][$base_table];
649       if (!empty($join_info['join_id'])) {
650         $id = $join_info['join_id'];
651       }
652       else {
653         $id = 'standard';
654       }
655
656       $configuration = $join_info;
657       // Fill in some easy defaults.
658       if (empty($configuration['table'])) {
659         $configuration['table'] = $table;
660       }
661       // If this is empty, it's a direct link.
662       if (empty($configuration['left_table'])) {
663         $configuration['left_table'] = $base_table;
664       }
665
666       if (isset($join_info['arguments'])) {
667         foreach ($join_info['arguments'] as $key => $argument) {
668           $configuration[$key] = $argument;
669         }
670       }
671
672       $join = Views::pluginManager('join')->createInstance($id, $configuration);
673
674       return $join;
675     }
676   }
677
678   /**
679    * {@inheritdoc}
680    */
681   public function getEntityType() {
682     // If the user has configured a relationship on the handler take that into
683     // account.
684     if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') {
685       $relationship = $this->displayHandler->getOption('relationships')[$this->options['relationship']];
686       $table_data = $this->getViewsData()->get($relationship['table']);
687       $views_data = $this->getViewsData()->get($table_data[$relationship['field']]['relationship']['base']);
688     }
689     else {
690       $views_data = $this->getViewsData()->get($this->view->storage->get('base_table'));
691     }
692
693     if (isset($views_data['table']['entity type'])) {
694       return $views_data['table']['entity type'];
695     }
696     else {
697       throw new \Exception("No entity type for field {$this->options['id']} on view {$this->view->storage->id()}");
698     }
699   }
700
701   /**
702    * {@inheritdoc}
703    */
704   public static function breakString($str, $force_int = FALSE) {
705     $operator = NULL;
706     $value = [];
707
708     // Determine if the string has 'or' operators (plus signs) or 'and'
709     // operators (commas) and split the string accordingly.
710     if (preg_match('/^([\w0-9-_\.]+[+ ]+)+[\w0-9-_\.]+$/u', $str)) {
711       // The '+' character in a query string may be parsed as ' '.
712       $operator = 'or';
713       $value = preg_split('/[+ ]/', $str);
714     }
715     elseif (preg_match('/^([\w0-9-_\.]+[, ]+)*[\w0-9-_\.]+$/u', $str)) {
716       $operator = 'and';
717       $value = explode(',', $str);
718     }
719
720     // Filter any empty matches (Like from '++' in a string) and reset the
721     // array keys. 'strlen' is used as the filter callback so we do not lose
722     // 0 values (would otherwise evaluate == FALSE).
723     $value = array_values(array_filter($value, 'strlen'));
724
725     if ($force_int) {
726       $value = array_map('intval', $value);
727     }
728
729     return (object) ['value' => $value, 'operator' => $operator];
730   }
731
732   /**
733    * Displays the Expose form.
734    */
735   public function displayExposedForm($form, FormStateInterface $form_state) {
736     $item = &$this->options;
737     // flip
738     $item['exposed'] = empty($item['exposed']);
739
740     // If necessary, set new defaults:
741     if ($item['exposed']) {
742       $this->defaultExposeOptions();
743     }
744
745     $view = $form_state->get('view');
746     $display_id = $form_state->get('display_id');
747     $type = $form_state->get('type');
748     $id = $form_state->get('id');
749     $view->getExecutable()->setHandler($display_id, $type, $id, $item);
750
751     $view->addFormToStack($form_state->get('form_key'), $display_id, $type, $id, TRUE, TRUE);
752
753     $view->cacheSet();
754     $form_state->set('rerender', TRUE);
755     $form_state->setRebuild();
756     $form_state->set('force_expose_options', TRUE);
757   }
758
759   /**
760    * A submit handler that is used for storing temporary items when using
761    * multi-step changes, such as ajax requests.
762    */
763   public function submitTemporaryForm($form, FormStateInterface $form_state) {
764     // Run it through the handler's submit function.
765     $this->submitOptionsForm($form['options'], $form_state);
766     $item = $this->options;
767     $types = ViewExecutable::getHandlerTypes();
768
769     // For footer/header $handler_type is area but $type is footer/header.
770     // For all other handle types it's the same.
771     $handler_type = $type = $form_state->get('type');
772     if (!empty($types[$type]['type'])) {
773       $handler_type = $types[$type]['type'];
774     }
775
776     $override = NULL;
777     $view = $form_state->get('view');
778     $executable = $view->getExecutable();
779     if ($executable->display_handler->useGroupBy() && !empty($item['group_type'])) {
780       if (empty($executable->query)) {
781         $executable->initQuery();
782       }
783       $aggregate = $executable->query->getAggregationInfo();
784       if (!empty($aggregate[$item['group_type']]['handler'][$type])) {
785         $override = $aggregate[$item['group_type']]['handler'][$type];
786       }
787     }
788
789     // Create a new handler and unpack the options from the form onto it. We
790     // can use that for storage.
791     $handler = Views::handlerManager($handler_type)->getHandler($item, $override);
792     $handler->init($executable, $executable->display_handler, $item);
793
794     // Add the incoming options to existing options because items using
795     // the extra form may not have everything in the form here.
796     $options = $form_state->getValue('options') + $this->options;
797
798     // This unpacks only options that are in the definition, ensuring random
799     // extra stuff on the form is not sent through.
800     $handler->unpackOptions($handler->options, $options, NULL, FALSE);
801
802     // Store the item back on the view.
803     $executable = $view->getExecutable();
804     $executable->temporary_options[$type][$form_state->get('id')] = $handler->options;
805
806     // @todo Decide if \Drupal\views_ui\Form\Ajax\ViewsFormBase::getForm() is
807     //   perhaps the better place to fix the issue.
808     // \Drupal\views_ui\Form\Ajax\ViewsFormBase::getForm() drops the current
809     // form from the stack, even if it's an #ajax. So add the item back to the top
810     // of the stack.
811     $view->addFormToStack($form_state->get('form_key'), $form_state->get('display_id'), $type, $item['id'], TRUE);
812
813     $form_state->get('rerender', TRUE);
814     $form_state->setRebuild();
815     // Write to cache
816     $view->cacheSet();
817   }
818
819   /**
820    * Calculates options stored on the handler
821    *
822    * @param array $options
823    *   The options stored in the handler
824    * @param array $form_state_options
825    *   The newly submitted form state options.
826    *
827    * @return array
828    *   The new options
829    */
830   public function submitFormCalculateOptions(array $options, array $form_state_options) {
831     return $form_state_options + $options;
832   }
833
834 }