Upgraded drupal core with security updates
[yaffs-website] / web / core / modules / views / src / Plugin / views / field / FieldPluginBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\field;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\MarkupInterface;
7 use Drupal\Component\Utility\Unicode;
8 use Drupal\Component\Utility\UrlHelper;
9 use Drupal\Component\Utility\Xss;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Url as CoreUrl;
12 use Drupal\views\Plugin\views\HandlerBase;
13 use Drupal\views\Plugin\views\display\DisplayPluginBase;
14 use Drupal\views\Render\ViewsRenderPipelineMarkup;
15 use Drupal\views\ResultRow;
16 use Drupal\views\ViewExecutable;
17
18 /**
19  * @defgroup views_field_handlers Views field handler plugins
20  * @{
21  * Handler plugins for Views fields.
22  *
23  * Field handlers handle both querying and display of fields in views.
24  *
25  * Field handler plugins extend
26  * \Drupal\views\Plugin\views\field\FieldPluginBase. They must be
27  * annotated with \Drupal\views\Annotation\ViewsField annotation, and they
28  * must be in namespace directory Plugin\views\field.
29  *
30  * The following items can go into a hook_views_data() implementation in a
31  * field section to affect how the field handler will behave:
32  * - additional fields: An array of fields that should be added to the query.
33  *   The array is in one of these forms:
34  *   @code
35  *   // Simple form, for fields within the same table.
36  *   array('identifier' => fieldname)
37  *   // Form for fields in a different table.
38  *   array('identifier' => array('table' => tablename, 'field' => fieldname))
39  *   @endcode
40  *   As many fields as are necessary may be in this array.
41  * - click sortable: If TRUE (default), this field may be click sorted.
42  *
43  * @ingroup views_plugins
44  * @see plugin_api
45  */
46
47 /**
48  * Base class for views fields.
49  *
50  * @ingroup views_field_handlers
51  */
52 abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterface {
53
54   /**
55    * Indicator of the renderText() method for rendering a single item.
56    * (If no render_item() is present).
57    */
58   const RENDER_TEXT_PHASE_SINGLE_ITEM = 0;
59
60   /**
61    * Indicator of the renderText() method for rendering the whole element.
62    * (if no render_item() method is available).
63    */
64   const RENDER_TEXT_PHASE_COMPLETELY = 1;
65
66   /**
67    * Indicator of the renderText() method for rendering the empty text.
68    */
69   const RENDER_TEXT_PHASE_EMPTY = 2;
70
71   public $field_alias = 'unknown';
72   public $aliases = [];
73
74   /**
75    * The field value prior to any rewriting.
76    *
77    * @var mixed
78    */
79   public $original_value = NULL;
80
81   /**
82    * Stores additional fields which get added to the query.
83    *
84    * The generated aliases are stored in $aliases.
85    *
86    * @var array
87    */
88   public $additional_fields = [];
89
90   /**
91    * The link generator.
92    *
93    * @var \Drupal\Core\Utility\LinkGeneratorInterface
94    */
95   protected $linkGenerator;
96
97   /**
98    * Stores the render API renderer.
99    *
100    * @var \Drupal\Core\Render\RendererInterface
101    */
102   protected $renderer;
103
104   /**
105    * Keeps track of the last render index.
106    *
107    * @var int|NULL
108    */
109   protected $lastRenderIndex;
110
111   /**
112    * {@inheritdoc}
113    */
114   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
115     parent::init($view, $display, $options);
116
117     $this->additional_fields = [];
118     if (!empty($this->definition['additional fields'])) {
119       $this->additional_fields = $this->definition['additional fields'];
120     }
121
122     if (!isset($this->options['exclude'])) {
123       $this->options['exclude'] = '';
124     }
125   }
126
127   /**
128    * Determine if this field can allow advanced rendering.
129    *
130    * Fields can set this to FALSE if they do not wish to allow
131    * token based rewriting or link-making.
132    */
133   protected function allowAdvancedRender() {
134     return TRUE;
135   }
136
137   /**
138    * Called to add the field to a query.
139    */
140   public function query() {
141     $this->ensureMyTable();
142     // Add the field.
143     $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
144     $this->field_alias = $this->query->addField($this->tableAlias, $this->realField, NULL, $params);
145
146     $this->addAdditionalFields();
147   }
148
149   /**
150    * Add 'additional' fields to the query.
151    *
152    * @param $fields
153    *   An array of fields. The key is an identifier used to later find the
154    *   field alias used. The value is either a string in which case it's
155    *   assumed to be a field on this handler's table; or it's an array in the
156    *   form of
157    *   @code array('table' => $tablename, 'field' => $fieldname) @endcode
158    */
159   protected function addAdditionalFields($fields = NULL) {
160     if (!isset($fields)) {
161       // notice check
162       if (empty($this->additional_fields)) {
163         return;
164       }
165       $fields = $this->additional_fields;
166     }
167
168     $group_params = [];
169     if ($this->options['group_type'] != 'group') {
170       $group_params = [
171         'function' => $this->options['group_type'],
172       ];
173     }
174
175     if (!empty($fields) && is_array($fields)) {
176       foreach ($fields as $identifier => $info) {
177         if (is_array($info)) {
178           if (isset($info['table'])) {
179             $table_alias = $this->query->ensureTable($info['table'], $this->relationship);
180           }
181           else {
182             $table_alias = $this->tableAlias;
183           }
184
185           if (empty($table_alias)) {
186             debug(t('Handler @handler tried to add additional_field @identifier but @table could not be added!', ['@handler' => $this->definition['id'], '@identifier' => $identifier, '@table' => $info['table']]));
187             $this->aliases[$identifier] = 'broken';
188             continue;
189           }
190
191           $params = [];
192           if (!empty($info['params'])) {
193             $params = $info['params'];
194           }
195
196           $params += $group_params;
197           $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params);
198         }
199         else {
200           $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params);
201         }
202       }
203     }
204   }
205
206   /**
207    * {@inheritdoc}
208    */
209   public function clickSort($order) {
210     if (isset($this->field_alias)) {
211       // Since fields should always have themselves already added, just
212       // add a sort on the field.
213       $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
214       $this->query->addOrderBy(NULL, NULL, $order, $this->field_alias, $params);
215     }
216   }
217
218   /**
219    * {@inheritdoc}
220    */
221   public function clickSortable() {
222     return isset($this->definition['click sortable']) ? $this->definition['click sortable'] : TRUE;
223   }
224
225   /**
226    * {@inheritdoc}
227    */
228   public function label() {
229     if (!isset($this->options['label'])) {
230       return '';
231     }
232     return $this->options['label'];
233   }
234
235   /**
236    * {@inheritdoc}
237    */
238   public function elementType($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) {
239     if ($none_supported) {
240       if ($this->options['element_type'] === '0') {
241         return '';
242       }
243     }
244     if ($this->options['element_type']) {
245       return $this->options['element_type'];
246     }
247
248     if ($default_empty) {
249       return '';
250     }
251
252     if ($inline) {
253       return 'span';
254     }
255
256     if (isset($this->definition['element type'])) {
257       return $this->definition['element type'];
258     }
259
260     return 'span';
261   }
262
263   /**
264    * {@inheritdoc}
265    */
266   public function elementLabelType($none_supported = FALSE, $default_empty = FALSE) {
267     if ($none_supported) {
268       if ($this->options['element_label_type'] === '0') {
269         return '';
270       }
271     }
272     if ($this->options['element_label_type']) {
273       return $this->options['element_label_type'];
274     }
275
276     if ($default_empty) {
277       return '';
278     }
279
280     return 'span';
281   }
282
283   /**
284    * {@inheritdoc}
285    */
286   public function elementWrapperType($none_supported = FALSE, $default_empty = FALSE) {
287     if ($none_supported) {
288       if ($this->options['element_wrapper_type'] === '0') {
289         return 0;
290       }
291     }
292     if ($this->options['element_wrapper_type']) {
293       return $this->options['element_wrapper_type'];
294     }
295
296     if ($default_empty) {
297       return '';
298     }
299
300     return 'div';
301   }
302
303   /**
304    * {@inheritdoc}
305    */
306   public function getElements() {
307     static $elements = NULL;
308     if (!isset($elements)) {
309       // @todo Add possible html5 elements.
310       $elements = [
311         '' => $this->t('- Use default -'),
312         '0' => $this->t('- None -')
313       ];
314       $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
315     }
316
317     return $elements;
318   }
319
320   /**
321    * {@inheritdoc}
322    */
323   public function elementClasses($row_index = NULL) {
324     $classes = $this->tokenizeValue($this->options['element_class'], $row_index);
325     $classes = explode(' ', $classes);
326     foreach ($classes as &$class) {
327       $class = Html::cleanCssIdentifier($class);
328     }
329     return implode(' ', $classes);
330   }
331
332   /**
333    * {@inheritdoc}
334    */
335   public function tokenizeValue($value, $row_index = NULL) {
336     if (strpos($value, '{{') !== FALSE) {
337       $fake_item = [
338         'alter_text' => TRUE,
339         'text' => $value,
340       ];
341
342       // Use isset() because empty() will trigger on 0 and 0 is
343       // the first row.
344       if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) {
345         $tokens = $this->view->style_plugin->render_tokens[$row_index];
346       }
347       else {
348         // Get tokens from the last field.
349         $last_field = end($this->view->field);
350         if (isset($last_field->last_tokens)) {
351           $tokens = $last_field->last_tokens;
352         }
353         else {
354           $tokens = $last_field->getRenderTokens($fake_item);
355         }
356       }
357
358       $value = strip_tags($this->renderAltered($fake_item, $tokens));
359       if (!empty($this->options['alter']['trim_whitespace'])) {
360         $value = trim($value);
361       }
362     }
363
364     return $value;
365   }
366
367   /**
368    * {@inheritdoc}
369    */
370   public function elementLabelClasses($row_index = NULL) {
371     $classes = $this->tokenizeValue($this->options['element_label_class'], $row_index);
372     $classes = explode(' ', $classes);
373     foreach ($classes as &$class) {
374       $class = Html::cleanCssIdentifier($class);
375     }
376     return implode(' ', $classes);
377   }
378
379   /**
380    * {@inheritdoc}
381    */
382   public function elementWrapperClasses($row_index = NULL) {
383     $classes = $this->tokenizeValue($this->options['element_wrapper_class'], $row_index);
384     $classes = explode(' ', $classes);
385     foreach ($classes as &$class) {
386       $class = Html::cleanCssIdentifier($class);
387     }
388     return implode(' ', $classes);
389   }
390
391   /**
392    * {@inheritdoc}
393    */
394   public function getEntity(ResultRow $values) {
395     $relationship_id = $this->options['relationship'];
396     if ($relationship_id == 'none') {
397       return $values->_entity;
398     }
399     elseif (isset($values->_relationship_entities[$relationship_id])) {
400       return $values->_relationship_entities[$relationship_id];
401     }
402   }
403
404   /**
405    * {@inheritdoc}
406    */
407   public function getValue(ResultRow $values, $field = NULL) {
408     $alias = isset($field) ? $this->aliases[$field] : $this->field_alias;
409     if (isset($values->{$alias})) {
410       return $values->{$alias};
411     }
412   }
413
414   /**
415    * {@inheritdoc}
416    */
417   public function useStringGroupBy() {
418     return TRUE;
419   }
420
421   protected function defineOptions() {
422     $options = parent::defineOptions();
423
424     $options['label'] = ['default' => ''];
425     // Some styles (for example table) should have labels enabled by default.
426     $style = $this->view->getStyle();
427     if (isset($style) && $style->defaultFieldLabels()) {
428       $options['label']['default'] = $this->definition['title'];
429     }
430
431     $options['exclude'] = ['default' => FALSE];
432     $options['alter'] = [
433       'contains' => [
434         'alter_text' => ['default' => FALSE],
435         'text' => ['default' => ''],
436         'make_link' => ['default' => FALSE],
437         'path' => ['default' => ''],
438         'absolute' => ['default' => FALSE],
439         'external' => ['default' => FALSE],
440         'replace_spaces' => ['default' => FALSE],
441         'path_case' => ['default' => 'none'],
442         'trim_whitespace' => ['default' => FALSE],
443         'alt' => ['default' => ''],
444         'rel' => ['default' => ''],
445         'link_class' => ['default' => ''],
446         'prefix' => ['default' => ''],
447         'suffix' => ['default' => ''],
448         'target' => ['default' => ''],
449         'nl2br' => ['default' => FALSE],
450         'max_length' => ['default' => 0],
451         'word_boundary' => ['default' => TRUE],
452         'ellipsis' => ['default' => TRUE],
453         'more_link' => ['default' => FALSE],
454         'more_link_text' => ['default' => ''],
455         'more_link_path' => ['default' => ''],
456         'strip_tags' => ['default' => FALSE],
457         'trim' => ['default' => FALSE],
458         'preserve_tags' => ['default' => ''],
459         'html' => ['default' => FALSE],
460       ],
461     ];
462     $options['element_type'] = ['default' => ''];
463     $options['element_class'] = ['default' => ''];
464
465     $options['element_label_type'] = ['default' => ''];
466     $options['element_label_class'] = ['default' => ''];
467     $options['element_label_colon'] = ['default' => TRUE];
468
469     $options['element_wrapper_type'] = ['default' => ''];
470     $options['element_wrapper_class'] = ['default' => ''];
471
472     $options['element_default_classes'] = ['default' => TRUE];
473
474     $options['empty'] = ['default' => ''];
475     $options['hide_empty'] = ['default' => FALSE];
476     $options['empty_zero'] = ['default' => FALSE];
477     $options['hide_alter_empty'] = ['default' => TRUE];
478
479     return $options;
480   }
481
482   /**
483    * Performs some cleanup tasks on the options array before saving it.
484    */
485   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
486     $options = &$form_state->getValue('options');
487     $types = ['element_type', 'element_label_type', 'element_wrapper_type'];
488     $classes = array_combine(['element_class', 'element_label_class', 'element_wrapper_class'], $types);
489
490     foreach ($types as $type) {
491       if (!$options[$type . '_enable']) {
492         $options[$type] = '';
493       }
494     }
495
496     foreach ($classes as $class => $type) {
497       if (!$options[$class . '_enable'] || !$options[$type . '_enable']) {
498         $options[$class] = '';
499       }
500     }
501
502     if (empty($options['custom_label'])) {
503       $options['label'] = '';
504       $options['element_label_colon'] = FALSE;
505     }
506   }
507
508   /**
509    * Default options form that provides the label widget that all fields
510    * should have.
511    */
512   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
513     parent::buildOptionsForm($form, $form_state);
514
515     $label = $this->label();
516     $form['custom_label'] = [
517       '#type' => 'checkbox',
518       '#title' => $this->t('Create a label'),
519       '#default_value' => $label !== '',
520       '#weight' => -103,
521     ];
522     $form['label'] = [
523       '#type' => 'textfield',
524       '#title' => $this->t('Label'),
525       '#default_value' => $label,
526       '#states' => [
527         'visible' => [
528           ':input[name="options[custom_label]"]' => ['checked' => TRUE],
529         ],
530       ],
531       '#weight' => -102,
532     ];
533     $form['element_label_colon'] = [
534       '#type' => 'checkbox',
535       '#title' => $this->t('Place a colon after the label'),
536       '#default_value' => $this->options['element_label_colon'],
537       '#states' => [
538         'visible' => [
539           ':input[name="options[custom_label]"]' => ['checked' => TRUE],
540         ],
541       ],
542       '#weight' => -101,
543     ];
544
545     $form['exclude'] = [
546       '#type' => 'checkbox',
547       '#title' => $this->t('Exclude from display'),
548       '#default_value' => $this->options['exclude'],
549       '#description' => $this->t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'),
550       '#weight' => -100,
551     ];
552
553     $form['style_settings'] = [
554       '#type' => 'details',
555       '#title' => $this->t('Style settings'),
556       '#weight' => 99,
557     ];
558
559     $form['element_type_enable'] = [
560       '#type' => 'checkbox',
561       '#title' => $this->t('Customize field HTML'),
562       '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
563       '#fieldset' => 'style_settings',
564     ];
565     $form['element_type'] = [
566       '#title' => $this->t('HTML element'),
567       '#options' => $this->getElements(),
568       '#type' => 'select',
569       '#default_value' => $this->options['element_type'],
570       '#description' => $this->t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'),
571       '#states' => [
572         'visible' => [
573           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
574         ],
575       ],
576       '#fieldset' => 'style_settings',
577     ];
578
579     $form['element_class_enable'] = [
580       '#type' => 'checkbox',
581       '#title' => $this->t('Create a CSS class'),
582       '#states' => [
583         'visible' => [
584           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
585         ],
586       ],
587       '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
588       '#fieldset' => 'style_settings',
589     ];
590     $form['element_class'] = [
591       '#title' => $this->t('CSS class'),
592       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
593       '#type' => 'textfield',
594       '#default_value' => $this->options['element_class'],
595       '#states' => [
596         'visible' => [
597           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
598           ':input[name="options[element_class_enable]"]' => ['checked' => TRUE],
599         ],
600       ],
601       '#fieldset' => 'style_settings',
602     ];
603
604     $form['element_label_type_enable'] = [
605       '#type' => 'checkbox',
606       '#title' => $this->t('Customize label HTML'),
607       '#default_value' => !empty($this->options['element_label_type']) || (string) $this->options['element_label_type'] == '0' || !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
608       '#fieldset' => 'style_settings',
609     ];
610     $form['element_label_type'] = [
611       '#title' => $this->t('Label HTML element'),
612       '#options' => $this->getElements(FALSE),
613       '#type' => 'select',
614       '#default_value' => $this->options['element_label_type'],
615       '#description' => $this->t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'),
616       '#states' => [
617         'visible' => [
618           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
619         ],
620       ],
621       '#fieldset' => 'style_settings',
622     ];
623     $form['element_label_class_enable'] = [
624       '#type' => 'checkbox',
625       '#title' => $this->t('Create a CSS class'),
626       '#states' => [
627         'visible' => [
628           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
629         ],
630       ],
631       '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
632       '#fieldset' => 'style_settings',
633     ];
634     $form['element_label_class'] = [
635       '#title' => $this->t('CSS class'),
636       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
637       '#type' => 'textfield',
638       '#default_value' => $this->options['element_label_class'],
639       '#states' => [
640         'visible' => [
641           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
642           ':input[name="options[element_label_class_enable]"]' => ['checked' => TRUE],
643         ],
644       ],
645       '#fieldset' => 'style_settings',
646     ];
647
648     $form['element_wrapper_type_enable'] = [
649       '#type' => 'checkbox',
650       '#title' => $this->t('Customize field and label wrapper HTML'),
651       '#default_value' => !empty($this->options['element_wrapper_type']) || (string) $this->options['element_wrapper_type'] == '0' || !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
652       '#fieldset' => 'style_settings',
653     ];
654     $form['element_wrapper_type'] = [
655       '#title' => $this->t('Wrapper HTML element'),
656       '#options' => $this->getElements(FALSE),
657       '#type' => 'select',
658       '#default_value' => $this->options['element_wrapper_type'],
659       '#description' => $this->t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'),
660       '#states' => [
661         'visible' => [
662           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
663         ],
664       ],
665       '#fieldset' => 'style_settings',
666     ];
667
668     $form['element_wrapper_class_enable'] = [
669       '#type' => 'checkbox',
670       '#title' => $this->t('Create a CSS class'),
671       '#states' => [
672         'visible' => [
673           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
674         ],
675       ],
676       '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
677       '#fieldset' => 'style_settings',
678     ];
679     $form['element_wrapper_class'] = [
680       '#title' => $this->t('CSS class'),
681       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
682       '#type' => 'textfield',
683       '#default_value' => $this->options['element_wrapper_class'],
684       '#states' => [
685         'visible' => [
686           ':input[name="options[element_wrapper_class_enable]"]' => ['checked' => TRUE],
687           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
688         ],
689       ],
690       '#fieldset' => 'style_settings',
691     ];
692
693     $form['element_default_classes'] = [
694       '#type' => 'checkbox',
695       '#title' => $this->t('Add default classes'),
696       '#default_value' => $this->options['element_default_classes'],
697       '#description' => $this->t('Use default Views classes to identify the field, field label and field content.'),
698       '#fieldset' => 'style_settings',
699     ];
700
701     $form['alter'] = [
702       '#title' => $this->t('Rewrite results'),
703       '#type' => 'details',
704       '#weight' => 100,
705     ];
706
707     if ($this->allowAdvancedRender()) {
708       $form['alter']['#tree'] = TRUE;
709       $form['alter']['alter_text'] = [
710         '#type' => 'checkbox',
711         '#title' => $this->t('Override the output of this field with custom text'),
712         '#default_value' => $this->options['alter']['alter_text'],
713       ];
714
715       $form['alter']['text'] = [
716         '#title' => $this->t('Text'),
717         '#type' => 'textarea',
718         '#default_value' => $this->options['alter']['text'],
719         '#description' => $this->t('The text to display for this field. You may include HTML or <a href=":url">Twig</a>. You may enter data from this view as per the "Replacement patterns" below.', [':url' => CoreUrl::fromUri('http://twig.sensiolabs.org/documentation')->toString()]),
720         '#states' => [
721           'visible' => [
722             ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
723           ],
724         ],
725       ];
726
727       $form['alter']['make_link'] = [
728         '#type' => 'checkbox',
729         '#title' => $this->t('Output this field as a custom link'),
730         '#default_value' => $this->options['alter']['make_link'],
731       ];
732       $form['alter']['path'] = [
733         '#title' => $this->t('Link path'),
734         '#type' => 'textfield',
735         '#default_value' => $this->options['alter']['path'],
736         '#description' => $this->t('The Drupal path or absolute URL for this link. You may enter data from this view as per the "Replacement patterns" below.'),
737         '#states' => [
738           'visible' => [
739             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
740           ],
741         ],
742         '#maxlength' => 255,
743       ];
744       $form['alter']['absolute'] = [
745         '#type' => 'checkbox',
746         '#title' => $this->t('Use absolute path'),
747         '#default_value' => $this->options['alter']['absolute'],
748         '#states' => [
749           'visible' => [
750             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
751           ],
752         ],
753       ];
754       $form['alter']['replace_spaces'] = [
755         '#type' => 'checkbox',
756         '#title' => $this->t('Replace spaces with dashes'),
757         '#default_value' => $this->options['alter']['replace_spaces'],
758         '#states' => [
759           'visible' => [
760             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
761           ],
762         ],
763       ];
764       $form['alter']['external'] = [
765         '#type' => 'checkbox',
766         '#title' => $this->t('External server URL'),
767         '#default_value' => $this->options['alter']['external'],
768         '#description' => $this->t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."),
769         '#states' => [
770           'visible' => [
771             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
772           ],
773         ],
774       ];
775       $form['alter']['path_case'] = [
776         '#type' => 'select',
777         '#title' => $this->t('Transform the case'),
778         '#description' => $this->t('When printing URL paths, how to transform the case of the filter value.'),
779         '#states' => [
780           'visible' => [
781             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
782           ],
783         ],
784         '#options' => [
785           'none' => $this->t('No transform'),
786           'upper' => $this->t('Upper case'),
787           'lower' => $this->t('Lower case'),
788           'ucfirst' => $this->t('Capitalize first letter'),
789           'ucwords' => $this->t('Capitalize each word'),
790         ],
791         '#default_value' => $this->options['alter']['path_case'],
792       ];
793       $form['alter']['link_class'] = [
794         '#title' => $this->t('Link class'),
795         '#type' => 'textfield',
796         '#default_value' => $this->options['alter']['link_class'],
797         '#description' => $this->t('The CSS class to apply to the link.'),
798         '#states' => [
799           'visible' => [
800             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
801           ],
802         ],
803       ];
804       $form['alter']['alt'] = [
805         '#title' => $this->t('Title text'),
806         '#type' => 'textfield',
807         '#default_value' => $this->options['alter']['alt'],
808         '#description' => $this->t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'),
809         '#states' => [
810           'visible' => [
811             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
812           ],
813         ],
814       ];
815       $form['alter']['rel'] = [
816         '#title' => $this->t('Rel Text'),
817         '#type' => 'textfield',
818         '#default_value' => $this->options['alter']['rel'],
819         '#description' => $this->t('Include Rel attribute for use in lightbox2 or other javascript utility.'),
820         '#states' => [
821           'visible' => [
822             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
823           ],
824         ],
825       ];
826       $form['alter']['prefix'] = [
827         '#title' => $this->t('Prefix text'),
828         '#type' => 'textfield',
829         '#default_value' => $this->options['alter']['prefix'],
830         '#description' => $this->t('Any text to display before this link. You may include HTML.'),
831         '#states' => [
832           'visible' => [
833             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
834           ],
835         ],
836       ];
837       $form['alter']['suffix'] = [
838         '#title' => $this->t('Suffix text'),
839         '#type' => 'textfield',
840         '#default_value' => $this->options['alter']['suffix'],
841         '#description' => $this->t('Any text to display after this link. You may include HTML.'),
842         '#states' => [
843           'visible' => [
844             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
845           ],
846         ],
847       ];
848       $form['alter']['target'] = [
849         '#title' => $this->t('Target'),
850         '#type' => 'textfield',
851         '#default_value' => $this->options['alter']['target'],
852         '#description' => $this->t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."),
853         '#states' => [
854           'visible' => [
855             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
856           ],
857         ],
858       ];
859
860
861       // Get a list of the available fields and arguments for token replacement.
862
863       // Setup the tokens for fields.
864       $previous = $this->getPreviousFieldLabels();
865       $optgroup_arguments = (string) t('Arguments');
866       $optgroup_fields = (string) t('Fields');
867       foreach ($previous as $id => $label) {
868         $options[$optgroup_fields]["{{ $id }}"] = substr(strrchr($label, ":"), 2 );
869       }
870       // Add the field to the list of options.
871       $options[$optgroup_fields]["{{ {$this->options['id']} }}"] = substr(strrchr($this->adminLabel(), ":"), 2 );
872
873       foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
874         $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
875         $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
876       }
877
878       $this->documentSelfTokens($options[$optgroup_fields]);
879
880       // Default text.
881
882       $output = [];
883       $output[] = [
884         '#markup' => '<p>' . $this->t('You must add some additional fields to this display before using this field. These fields may be marked as <em>Exclude from display</em> if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.') . '</p>',
885       ];
886       // We have some options, so make a list.
887       if (!empty($options)) {
888         $output[] = [
889           '#markup' => '<p>' . $this->t("The following replacement tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.") . '</p>',
890         ];
891         foreach (array_keys($options) as $type) {
892           if (!empty($options[$type])) {
893             $items = [];
894             foreach ($options[$type] as $key => $value) {
895               $items[] = $key . ' == ' . $value;
896             }
897             $item_list = [
898               '#theme' => 'item_list',
899               '#items' => $items,
900             ];
901             $output[] = $item_list;
902           }
903         }
904       }
905       // This construct uses 'hidden' and not markup because process doesn't
906       // run. It also has an extra div because the dependency wants to hide
907       // the parent in situations like this, so we need a second div to
908       // make this work.
909       $form['alter']['help'] = [
910         '#type' => 'details',
911         '#title' => $this->t('Replacement patterns'),
912         '#value' => $output,
913         '#states' => [
914           'visible' => [
915             [
916               ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
917             ],
918             [
919               ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
920             ],
921             [
922               ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
923             ],
924           ],
925         ],
926       ];
927
928       $form['alter']['trim'] = [
929         '#type' => 'checkbox',
930         '#title' => $this->t('Trim this field to a maximum number of characters'),
931         '#default_value' => $this->options['alter']['trim'],
932       ];
933
934       $form['alter']['max_length'] = [
935         '#title' => $this->t('Maximum number of characters'),
936         '#type' => 'textfield',
937         '#default_value' => $this->options['alter']['max_length'],
938         '#states' => [
939           'visible' => [
940             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
941           ],
942         ],
943       ];
944
945       $form['alter']['word_boundary'] = [
946         '#type' => 'checkbox',
947         '#title' => $this->t('Trim only on a word boundary'),
948         '#description' => $this->t('If checked, this field be trimmed only on a word boundary. This is guaranteed to be the maximum characters stated or less. If there are no word boundaries this could trim a field to nothing.'),
949         '#default_value' => $this->options['alter']['word_boundary'],
950         '#states' => [
951           'visible' => [
952             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
953           ],
954         ],
955       ];
956
957       $form['alter']['ellipsis'] = [
958         '#type' => 'checkbox',
959         '#title' => $this->t('Add "…" at the end of trimmed text'),
960         '#default_value' => $this->options['alter']['ellipsis'],
961         '#states' => [
962           'visible' => [
963             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
964           ],
965         ],
966       ];
967
968       $form['alter']['more_link'] = [
969         '#type' => 'checkbox',
970         '#title' => $this->t('Add a read-more link if output is trimmed'),
971         '#default_value' => $this->options['alter']['more_link'],
972         '#states' => [
973           'visible' => [
974             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
975           ],
976         ],
977       ];
978
979       $form['alter']['more_link_text'] = [
980         '#type' => 'textfield',
981         '#title' => $this->t('More link label'),
982         '#default_value' => $this->options['alter']['more_link_text'],
983         '#description' => $this->t('You may use the "Replacement patterns" above.'),
984         '#states' => [
985           'visible' => [
986             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
987             ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
988           ],
989         ],
990       ];
991       $form['alter']['more_link_path'] = [
992         '#type' => 'textfield',
993         '#title' => $this->t('More link path'),
994         '#default_value' => $this->options['alter']['more_link_path'],
995         '#description' => $this->t('This can be an internal Drupal path such as node/add or an external URL such as "https://www.drupal.org". You may use the "Replacement patterns" above.'),
996         '#states' => [
997           'visible' => [
998             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
999             ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
1000           ],
1001         ],
1002       ];
1003
1004       $form['alter']['html'] = [
1005         '#type' => 'checkbox',
1006         '#title' => $this->t('Field can contain HTML'),
1007         '#description' => $this->t('An HTML corrector will be run to ensure HTML tags are properly closed after trimming.'),
1008         '#default_value' => $this->options['alter']['html'],
1009         '#states' => [
1010           'visible' => [
1011             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1012           ],
1013         ],
1014       ];
1015
1016       $form['alter']['strip_tags'] = [
1017         '#type' => 'checkbox',
1018         '#title' => $this->t('Strip HTML tags'),
1019         '#default_value' => $this->options['alter']['strip_tags'],
1020       ];
1021
1022       $form['alter']['preserve_tags'] = [
1023         '#type' => 'textfield',
1024         '#title' => $this->t('Preserve certain tags'),
1025         '#description' => $this->t('List the tags that need to be preserved during the stripping process. example &quot;&lt;p&gt; &lt;br&gt;&quot; which will preserve all p and br elements'),
1026         '#default_value' => $this->options['alter']['preserve_tags'],
1027         '#states' => [
1028           'visible' => [
1029             ':input[name="options[alter][strip_tags]"]' => ['checked' => TRUE],
1030           ],
1031         ],
1032       ];
1033
1034       $form['alter']['trim_whitespace'] = [
1035         '#type' => 'checkbox',
1036         '#title' => $this->t('Remove whitespace'),
1037         '#default_value' => $this->options['alter']['trim_whitespace'],
1038       ];
1039
1040       $form['alter']['nl2br'] = [
1041         '#type' => 'checkbox',
1042         '#title' => $this->t('Convert newlines to HTML &lt;br&gt; tags'),
1043         '#default_value' => $this->options['alter']['nl2br'],
1044       ];
1045     }
1046
1047     $form['empty_field_behavior'] = [
1048       '#type' => 'details',
1049       '#title' => $this->t('No results behavior'),
1050       '#weight' => 100,
1051     ];
1052
1053     $form['empty'] = [
1054       '#type' => 'textarea',
1055       '#title' => $this->t('No results text'),
1056       '#default_value' => $this->options['empty'],
1057       '#description' => $this->t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section below.'),
1058       '#fieldset' => 'empty_field_behavior',
1059     ];
1060
1061     $form['empty_zero'] = [
1062       '#type' => 'checkbox',
1063       '#title' => $this->t('Count the number 0 as empty'),
1064       '#default_value' => $this->options['empty_zero'],
1065       '#description' => $this->t('Enable to display the "no results text" if the field contains the number 0.'),
1066       '#fieldset' => 'empty_field_behavior',
1067     ];
1068
1069     $form['hide_empty'] = [
1070       '#type' => 'checkbox',
1071       '#title' => $this->t('Hide if empty'),
1072       '#default_value' => $this->options['hide_empty'],
1073       '#description' => $this->t('Enable to hide this field if it is empty. Note that the field label or rewritten output may still be displayed. To hide labels, check the style or row style settings for empty fields. To hide rewritten content, check the "Hide rewriting if empty" checkbox.'),
1074       '#fieldset' => 'empty_field_behavior',
1075     ];
1076
1077     $form['hide_alter_empty'] = [
1078       '#type' => 'checkbox',
1079       '#title' => $this->t('Hide rewriting if empty'),
1080       '#default_value' => $this->options['hide_alter_empty'],
1081       '#description' => $this->t('Do not display rewritten content if this field is empty.'),
1082       '#fieldset' => 'empty_field_behavior',
1083     ];
1084   }
1085
1086   /**
1087    * Returns all field labels of fields before this field.
1088    *
1089    * @return array
1090    *   An array of field labels keyed by their field IDs.
1091    */
1092   protected function getPreviousFieldLabels() {
1093     $all_fields = $this->view->display_handler->getFieldLabels();
1094     $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields)));
1095     return $field_options;
1096   }
1097
1098   /**
1099    * Provide extra data to the administration form
1100    */
1101   public function adminSummary() {
1102     return $this->label();
1103   }
1104
1105   /**
1106    * {@inheritdoc}
1107    */
1108   public function preRender(&$values) { }
1109
1110   /**
1111    * {@inheritdoc}
1112    */
1113   public function render(ResultRow $values) {
1114     $value = $this->getValue($values);
1115     return $this->sanitizeValue($value);
1116   }
1117
1118   /**
1119    * {@inheritdoc}
1120    */
1121   public function postRender(ResultRow $row, $output) {
1122     // Make sure the last rendered value is available also when this is
1123     // retrieved from cache.
1124     $this->last_render = $output;
1125     return [];
1126   }
1127
1128   /**
1129    * {@inheritdoc}
1130    */
1131   public function advancedRender(ResultRow $values) {
1132     // Clean up values from previous render calls.
1133     if ($this->lastRenderIndex != $values->index) {
1134       $this->last_render_text = '';
1135     }
1136     if ($this->allowAdvancedRender() && $this instanceof MultiItemsFieldHandlerInterface) {
1137       $raw_items = $this->getItems($values);
1138       // If there are no items, set the original value to NULL.
1139       if (empty($raw_items)) {
1140         $this->original_value = NULL;
1141       }
1142     }
1143     else {
1144       $value = $this->render($values);
1145       if (is_array($value)) {
1146         $value = $this->getRenderer()->render($value);
1147       }
1148       $this->last_render = $value;
1149       $this->original_value = $value;
1150     }
1151
1152     if ($this->allowAdvancedRender()) {
1153       $tokens = NULL;
1154       if ($this instanceof MultiItemsFieldHandlerInterface) {
1155         $items = [];
1156         foreach ($raw_items as $count => $item) {
1157           $value = $this->render_item($count, $item);
1158           if (is_array($value)) {
1159             $value = (string) $this->getRenderer()->render($value);
1160           }
1161           $this->last_render = $value;
1162           $this->original_value = $this->last_render;
1163
1164           $alter = $item + $this->options['alter'];
1165           $alter['phase'] = static::RENDER_TEXT_PHASE_SINGLE_ITEM;
1166           $items[] = $this->renderText($alter);
1167         }
1168
1169         $value = $this->renderItems($items);
1170       }
1171       else {
1172         $alter = ['phase' => static::RENDER_TEXT_PHASE_COMPLETELY] + $this->options['alter'];
1173         $value = $this->renderText($alter);
1174       }
1175
1176       if (is_array($value)) {
1177         $value = $this->getRenderer()->render($value);
1178       }
1179       // This happens here so that renderAsLink can get the unaltered value of
1180       // this field as a token rather than the altered value.
1181       $this->last_render = $value;
1182     }
1183
1184     // String cast is necessary to test emptiness of MarkupInterface
1185     // objects.
1186     if (empty((string) $this->last_render)) {
1187       if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) {
1188         $alter = $this->options['alter'];
1189         $alter['alter_text'] = 1;
1190         $alter['text'] = $this->options['empty'];
1191         $alter['phase'] = static::RENDER_TEXT_PHASE_EMPTY;
1192         $this->last_render = $this->renderText($alter);
1193       }
1194     }
1195     // If we rendered something, update the last render index.
1196     if ((string) $this->last_render !== '') {
1197       $this->lastRenderIndex = $values->index;
1198     }
1199     return $this->last_render;
1200   }
1201
1202   /**
1203    * {@inheritdoc}
1204    */
1205   public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
1206     // Convert MarkupInterface to a string for checking.
1207     if ($value instanceof MarkupInterface) {
1208       $value = (string) $value;
1209     }
1210     if (!isset($value)) {
1211       $empty = TRUE;
1212     }
1213     else {
1214       $empty = ($empty_zero || ($value !== 0 && $value !== '0'));
1215     }
1216
1217     if ($no_skip_empty) {
1218       $empty = empty($value) && $empty;
1219     }
1220     return $empty;
1221   }
1222
1223   /**
1224    * {@inheritdoc}
1225    */
1226   public function renderText($alter) {
1227     // We need to preserve the safeness of the value regardless of the
1228     // alterations made by this method. Any alterations or replacements made
1229     // within this method need to ensure that at the minimum the result is
1230     // XSS admin filtered. See self::renderAltered() as an example that does.
1231     $value_is_safe = $this->last_render instanceof MarkupInterface;
1232     // Cast to a string so that empty checks and string functions work as
1233     // expected.
1234     $value = (string) $this->last_render;
1235
1236     if (!empty($alter['alter_text']) && $alter['text'] !== '') {
1237       $tokens = $this->getRenderTokens($alter);
1238       $value = $this->renderAltered($alter, $tokens);
1239       // $alter['text'] is entered through the views admin UI and will be safe
1240       // because the output of $this->renderAltered() is run through
1241       // Xss::filterAdmin().
1242       // @see \Drupal\views\Plugin\views\PluginBase::viewsTokenReplace()
1243       // @see \Drupal\Component\Utility\Xss::filterAdmin()
1244       $value_is_safe = TRUE;
1245     }
1246
1247     if (!empty($this->options['alter']['trim_whitespace'])) {
1248       $value = trim($value);
1249     }
1250
1251     // Check if there should be no further rewrite for empty values.
1252     $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']);
1253
1254     // Check whether the value is empty and return nothing, so the field isn't rendered.
1255     // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE).
1256     // For numeric values you can specify whether "0"/0 should be empty.
1257     if ((($this->options['hide_empty'] && empty($value))
1258         || ($alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty))
1259       && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) {
1260       return '';
1261     }
1262     // Only in empty phase.
1263     if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) {
1264       // If we got here then $alter contains the value of "No results text"
1265       // and so there is nothing left to do.
1266       return ViewsRenderPipelineMarkup::create($value);
1267     }
1268
1269     if (!empty($alter['strip_tags'])) {
1270       $value = strip_tags($value, $alter['preserve_tags']);
1271     }
1272
1273     $more_link = '';
1274     if (!empty($alter['trim']) && !empty($alter['max_length'])) {
1275       $length = strlen($value);
1276       $value = $this->renderTrimText($alter, $value);
1277       if ($this->options['alter']['more_link'] && strlen($value) < $length) {
1278         $tokens = $this->getRenderTokens($alter);
1279         $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more');
1280         $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens);
1281         $more_link_path = $this->options['alter']['more_link_path'];
1282         $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens)));
1283
1284         // Make sure that paths which were run through URL generation work as
1285         // well.
1286         $base_path = base_path();
1287         // Checks whether the path starts with the base_path.
1288         if (strpos($more_link_path, $base_path) === 0) {
1289           $more_link_path = Unicode::substr($more_link_path, Unicode::strlen($base_path));
1290         }
1291
1292         // @todo Views should expect and store a leading /. See
1293         //   https://www.drupal.org/node/2423913.
1294         $options = [
1295           'attributes' => [
1296             'class' => [
1297               'views-more-link',
1298             ],
1299           ],
1300         ];
1301         if (UrlHelper::isExternal($more_link_path)) {
1302           $more_link_url = CoreUrl::fromUri($more_link_path, $options);
1303         }
1304         else {
1305           $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options);
1306         }
1307         $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url);
1308       }
1309     }
1310
1311     if (!empty($alter['nl2br'])) {
1312       $value = nl2br($value);
1313     }
1314
1315     if ($value_is_safe) {
1316       $value = ViewsRenderPipelineMarkup::create($value);
1317     }
1318     $this->last_render_text = $value;
1319
1320     if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
1321       if (!isset($tokens)) {
1322         $tokens = $this->getRenderTokens($alter);
1323       }
1324       $value = $this->renderAsLink($alter, $value, $tokens);
1325     }
1326
1327     // Preserve whether or not the string is safe. Since $more_link comes from
1328     // \Drupal::l(), it is safe to append. Check if the value is an instance of
1329     // \Drupal\Component\Render\MarkupInterface here because renderAsLink()
1330     // can return both safe and unsafe values.
1331     if ($value instanceof MarkupInterface) {
1332       return ViewsRenderPipelineMarkup::create($value . $more_link);
1333     }
1334     else {
1335       // If the string is not already marked safe, it is still OK to return it
1336       // because it will be sanitized by Twig.
1337       return $value . $more_link;
1338     }
1339   }
1340
1341   /**
1342    * Render this field as user-defined altered text.
1343    */
1344   protected function renderAltered($alter, $tokens) {
1345     return $this->viewsTokenReplace($alter['text'], $tokens);
1346   }
1347
1348   /**
1349    * Trims the field down to the specified length.
1350    *
1351    * @param array $alter
1352    *   The alter array of options to use.
1353    *     - max_length: Maximum length of the string, the rest gets truncated.
1354    *     - word_boundary: Trim only on a word boundary.
1355    *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1356    *     - html: Make sure that the html is correct.
1357    *
1358    * @param string $value
1359    *   The string which should be trimmed.
1360    *
1361    * @return string
1362    *   The rendered trimmed string.
1363    */
1364   protected function renderTrimText($alter, $value) {
1365     if (!empty($alter['strip_tags'])) {
1366       // NOTE: It's possible that some external fields might override the
1367       // element type.
1368       $this->definition['element type'] = 'span';
1369     }
1370     return static::trimText($alter, $value);
1371   }
1372
1373   /**
1374    * Render this field as a link, with the info from a fieldset set by
1375    * the user.
1376    */
1377   protected function renderAsLink($alter, $text, $tokens) {
1378     $options = [
1379       'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
1380       'alias' => FALSE,
1381       'entity' => NULL,
1382       'entity_type' => NULL,
1383       'fragment' => NULL,
1384       'language' => NULL,
1385       'query' => [],
1386     ];
1387
1388     $alter += [
1389       'path' => NULL
1390     ];
1391
1392     $path = $alter['path'];
1393     // strip_tags() and viewsTokenReplace remove <front>, so check whether it's
1394     // different to front.
1395     if ($path != '<front>') {
1396       // Use strip_tags as there should never be HTML in the path.
1397       // However, we need to preserve special characters like " that were
1398       // removed by Html::escape().
1399       $path = Html::decodeEntities($this->viewsTokenReplace($alter['path'], $tokens));
1400
1401       // Tokens might contain <front>, so check for <front> again.
1402       if ($path != '<front>') {
1403         $path = strip_tags($path);
1404       }
1405
1406       // Tokens might have resolved URL's, as is the case for tokens provided by
1407       // Link fields, so all internal paths will be prefixed by base_path(). For
1408       // proper further handling reset this to internal:/.
1409       if (strpos($path, base_path()) === 0) {
1410         $path = 'internal:/' . substr($path, strlen(base_path()));
1411       }
1412
1413       // If we have no $path and no $alter['url'], we have nothing to work with,
1414       // so we just return the text.
1415       if (empty($path) && empty($alter['url'])) {
1416         return $text;
1417       }
1418
1419       // If no scheme is provided in the $path, assign the default 'http://'.
1420       // This allows a url of 'www.example.com' to be converted to
1421       // 'http://www.example.com'.
1422       // Only do this when flag for external has been set, $path doesn't contain
1423       // a scheme and $path doesn't have a leading /.
1424       if ($alter['external'] && !parse_url($path, PHP_URL_SCHEME) && strpos($path, '/') !== 0) {
1425         // There is no scheme, add the default 'http://' to the $path.
1426         $path = "http://" . $path;
1427       }
1428     }
1429
1430     if (empty($alter['url'])) {
1431       if (!parse_url($path, PHP_URL_SCHEME)) {
1432         // @todo Views should expect and store a leading /. See
1433         //   https://www.drupal.org/node/2423913.
1434         $alter['url'] = CoreUrl::fromUserInput('/' . ltrim($path, '/'));
1435       }
1436       else {
1437         $alter['url'] = CoreUrl::fromUri($path);
1438       }
1439     }
1440
1441     $options = $alter['url']->getOptions() + $options;
1442
1443     $path = $alter['url']->setOptions($options)->toUriString();
1444
1445     if (!empty($alter['path_case']) && $alter['path_case'] != 'none' && !$alter['url']->isRouted()) {
1446       $path = str_replace($alter['path'], $this->caseTransform($alter['path'], $this->options['alter']['path_case']), $path);
1447     }
1448
1449     if (!empty($alter['replace_spaces'])) {
1450       $path = str_replace(' ', '-', $path);
1451     }
1452
1453     // Parse the URL and move any query and fragment parameters out of the path.
1454     $url = UrlHelper::parse($path);
1455
1456     // Seriously malformed URLs may return FALSE or empty arrays.
1457     if (empty($url)) {
1458       return $text;
1459     }
1460
1461     // If the path is empty do not build a link around the given text and return
1462     // it as is.
1463     // http://www.example.com URLs will not have a $url['path'], so check host as well.
1464     if (empty($url['path']) && empty($url['host']) && empty($url['fragment']) && empty($url['url'])) {
1465       return $text;
1466     }
1467
1468     // If we get to here we have a path from the url parsing. So assign that to
1469     // $path now so we don't get query strings or fragments in the path.
1470     $path = $url['path'];
1471
1472     if (isset($url['query'])) {
1473       // Remove query parameters that were assigned a query string replacement
1474       // token for which there is no value available.
1475       foreach ($url['query'] as $param => $val) {
1476         if ($val == '%' . $param) {
1477           unset($url['query'][$param]);
1478         }
1479         // Replace any empty query params from URL parsing with NULL. So the
1480         // query will get built correctly with only the param key.
1481         // @see \Drupal\Component\Utility\UrlHelper::buildQuery().
1482         if ($val === '') {
1483           $url['query'][$param] = NULL;
1484         }
1485       }
1486
1487       $options['query'] = $url['query'];
1488     }
1489
1490     if (isset($url['fragment'])) {
1491       $path = strtr($path, ['#' . $url['fragment'] => '']);
1492       // If the path is empty we want to have a fragment for the current site.
1493       if ($path == '') {
1494         $options['external'] = TRUE;
1495       }
1496       $options['fragment'] = $url['fragment'];
1497     }
1498
1499     $alt = $this->viewsTokenReplace($alter['alt'], $tokens);
1500     // Set the title attribute of the link only if it improves accessibility
1501     if ($alt && $alt != $text) {
1502       $options['attributes']['title'] = Html::decodeEntities($alt);
1503     }
1504
1505     $class = $this->viewsTokenReplace($alter['link_class'], $tokens);
1506     if ($class) {
1507       $options['attributes']['class'] = [$class];
1508     }
1509
1510     if (!empty($alter['rel']) && $rel = $this->viewsTokenReplace($alter['rel'], $tokens)) {
1511       $options['attributes']['rel'] = $rel;
1512     }
1513
1514     $target = trim($this->viewsTokenReplace($alter['target'], $tokens));
1515     if (!empty($target)) {
1516       $options['attributes']['target'] = $target;
1517     }
1518
1519     // Allow the addition of arbitrary attributes to links. Additional attributes
1520     // currently can only be altered in preprocessors and not within the UI.
1521     if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) {
1522       foreach ($alter['link_attributes'] as $key => $attribute) {
1523         if (!isset($options['attributes'][$key])) {
1524           $options['attributes'][$key] = $this->viewsTokenReplace($attribute, $tokens);
1525         }
1526       }
1527     }
1528
1529     // If the query and fragment were programmatically assigned overwrite any
1530     // parsed values.
1531     if (isset($alter['query'])) {
1532       // Convert the query to a string, perform token replacement, and then
1533       // convert back to an array form for
1534       // \Drupal\Core\Utility\LinkGeneratorInterface::generate().
1535       $options['query'] = UrlHelper::buildQuery($alter['query']);
1536       $options['query'] = $this->viewsTokenReplace($options['query'], $tokens);
1537       $query = [];
1538       parse_str($options['query'], $query);
1539       $options['query'] = $query;
1540     }
1541     if (isset($alter['alias'])) {
1542       // Alias is a boolean field, so no token.
1543       $options['alias'] = $alter['alias'];
1544     }
1545     if (isset($alter['fragment'])) {
1546       $options['fragment'] = $this->viewsTokenReplace($alter['fragment'], $tokens);
1547     }
1548     if (isset($alter['language'])) {
1549       $options['language'] = $alter['language'];
1550     }
1551
1552     // If the url came from entity_uri(), pass along the required options.
1553     if (isset($alter['entity'])) {
1554       $options['entity'] = $alter['entity'];
1555     }
1556     if (isset($alter['entity_type'])) {
1557       $options['entity_type'] = $alter['entity_type'];
1558     }
1559
1560     // The path has been heavily processed above, so it should be used as-is.
1561     $final_url = CoreUrl::fromUri($path, $options);
1562
1563     // Build the link based on our altered Url object, adding on the optional
1564     // prefix and suffix
1565     $render = [
1566       '#type' => 'link',
1567       '#title' => $text,
1568       '#url' => $final_url,
1569     ];
1570
1571     if (!empty($alter['prefix'])) {
1572       $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
1573     }
1574     if (!empty($alter['suffix'])) {
1575       $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
1576     }
1577     return $this->getRenderer()->render($render);
1578
1579   }
1580
1581   /**
1582    * {@inheritdoc}
1583    */
1584   public function getRenderTokens($item) {
1585     $tokens = [];
1586     if (!empty($this->view->build_info['substitutions'])) {
1587       $tokens = $this->view->build_info['substitutions'];
1588     }
1589     $count = 0;
1590     foreach ($this->displayHandler->getHandlers('argument') as $arg => $handler) {
1591       $token = "{{ arguments.$arg }}";
1592       if (!isset($tokens[$token])) {
1593         $tokens[$token] = '';
1594       }
1595
1596       // Use strip tags as there should never be HTML in the path.
1597       // However, we need to preserve special characters like " that
1598       // were removed by Html::escape().
1599       $tokens["{{ raw_arguments.$arg }}"] = isset($this->view->args[$count]) ? strip_tags(Html::decodeEntities($this->view->args[$count])) : '';
1600       $count++;
1601     }
1602
1603     // Get flattened set of tokens for any array depth in query parameters.
1604     if ($request = $this->view->getRequest()) {
1605       $tokens += $this->getTokenValuesRecursive($request->query->all());
1606     }
1607
1608     // Now add replacements for our fields.
1609     foreach ($this->displayHandler->getHandlers('field') as $field => $handler) {
1610       /** @var static $handler */
1611       $placeholder = $handler->getFieldTokenPlaceholder();
1612
1613       if (isset($handler->last_render)) {
1614         $tokens[$placeholder] = $handler->last_render;
1615       }
1616       else {
1617         $tokens[$placeholder] = '';
1618       }
1619
1620       // We only use fields up to (and including) this one.
1621       if ($field == $this->options['id']) {
1622         break;
1623       }
1624     }
1625
1626     // Store the tokens for the row so we can reference them later if necessary.
1627     $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens;
1628     $this->last_tokens = $tokens;
1629     if (!empty($item)) {
1630       $this->addSelfTokens($tokens, $item);
1631     }
1632
1633     return $tokens;
1634   }
1635
1636   /**
1637    * Returns a token placeholder for the current field.
1638    *
1639    * @return string
1640    *   A token placeholder.
1641    */
1642   protected function getFieldTokenPlaceholder() {
1643     return '{{ ' . $this->options['id'] . ' }}';
1644   }
1645
1646   /**
1647    * Recursive function to add replacements for nested query string parameters.
1648    *
1649    * E.g. if you pass in the following array:
1650    *   array(
1651    *     'foo' => array(
1652    *       'a' => 'value',
1653    *       'b' => 'value',
1654    *     ),
1655    *     'bar' => array(
1656    *       'a' => 'value',
1657    *       'b' => array(
1658    *         'c' => value,
1659    *       ),
1660    *     ),
1661    *   );
1662    *
1663    * Would yield the following array of tokens:
1664    *   array(
1665    *     '%foo_a' => 'value'
1666    *     '%foo_b' => 'value'
1667    *     '%bar_a' => 'value'
1668    *     '%bar_b_c' => 'value'
1669    *   );
1670    *
1671    * @param $array
1672    *   An array of values.
1673    *
1674    * @param $parent_keys
1675    *   An array of parent keys. This will represent the array depth.
1676    *
1677    * @return
1678    *   An array of available tokens, with nested keys representative of the array structure.
1679    */
1680   protected function getTokenValuesRecursive(array $array, array $parent_keys = []) {
1681     $tokens = [];
1682
1683     foreach ($array as $param => $val) {
1684       if (is_array($val)) {
1685         // Copy parent_keys array, so we don't affect other elements of this
1686         // iteration.
1687         $child_parent_keys = $parent_keys;
1688         $child_parent_keys[] = $param;
1689         // Get the child tokens.
1690         $child_tokens = $this->getTokenValuesRecursive($val, $child_parent_keys);
1691         // Add them to the current tokens array.
1692         $tokens += $child_tokens;
1693       }
1694       else {
1695         // Create a token key based on array element structure.
1696         $token_string = !empty($parent_keys) ? implode('.', $parent_keys) . '.' . $param : $param;
1697         $tokens['{{ arguments.' . $token_string . ' }}'] = strip_tags(Html::decodeEntities($val));
1698       }
1699     }
1700
1701     return $tokens;
1702   }
1703
1704   /**
1705    * Add any special tokens this field might use for itself.
1706    *
1707    * This method is intended to be overridden by items that generate
1708    * fields as a list. For example, the field that displays all terms
1709    * on a node might have tokens for the tid and the term.
1710    *
1711    * By convention, tokens should follow the format of {{ token__subtoken }}
1712    * where token is the field ID and subtoken is the field. If the
1713    * field ID is terms, then the tokens might be {{ terms__tid }} and
1714    * {{ terms__name }}.
1715    */
1716   protected function addSelfTokens(&$tokens, $item) { }
1717
1718   /**
1719    * Document any special tokens this field might use for itself.
1720    *
1721    * @see addSelfTokens()
1722    */
1723   protected function documentSelfTokens(&$tokens) { }
1724
1725   /**
1726    * {@inheritdoc}
1727    */
1728   public function theme(ResultRow $values) {
1729     $renderer = $this->getRenderer();
1730     $build = [
1731       '#theme' => $this->themeFunctions(),
1732       '#view' => $this->view,
1733       '#field' => $this,
1734       '#row' => $values,
1735     ];
1736     $output = $renderer->render($build);
1737
1738     // Set the bubbleable rendering metadata on $view->element. This ensures the
1739     // bubbleable rendering metadata of individual rendered fields is not lost.
1740     // @see \Drupal\Core\Render\Renderer::updateStack()
1741     $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build);
1742
1743     return $output;
1744   }
1745
1746   public function themeFunctions() {
1747     $themes = [];
1748     $hook = 'views_view_field';
1749
1750     $display = $this->view->display_handler->display;
1751
1752     if (!empty($display)) {
1753       $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'] . '__' . $this->options['id'];
1754       $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'];
1755       $themes[] = $hook . '__' . $display['id'] . '__' . $this->options['id'];
1756       $themes[] = $hook . '__' . $display['id'];
1757       if ($display['id'] != $display['display_plugin']) {
1758         $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1759         $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'];
1760         $themes[] = $hook . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1761         $themes[] = $hook . '__' . $display['display_plugin'];
1762       }
1763     }
1764     $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $this->options['id'];
1765     $themes[] = $hook . '__' . $this->view->storage->id();
1766     $themes[] = $hook . '__' . $this->options['id'];
1767     $themes[] = $hook;
1768
1769     return $themes;
1770   }
1771
1772   public function adminLabel($short = FALSE) {
1773     return $this->getField(parent::adminLabel($short));
1774   }
1775
1776   /**
1777    * Trims the field down to the specified length.
1778    *
1779    * @param array $alter
1780    *   The alter array of options to use.
1781    *     - max_length: Maximum length of the string, the rest gets truncated.
1782    *     - word_boundary: Trim only on a word boundary.
1783    *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1784    *     - html: Make sure that the html is correct.
1785    *
1786    * @param string $value
1787    *   The string which should be trimmed.
1788    *
1789    * @return string
1790    *   The trimmed string.
1791    */
1792   public static function trimText($alter, $value) {
1793     if (Unicode::strlen($value) > $alter['max_length']) {
1794       $value = Unicode::substr($value, 0, $alter['max_length']);
1795       if (!empty($alter['word_boundary'])) {
1796         $regex = "(.*)\b.+";
1797         if (function_exists('mb_ereg')) {
1798           mb_regex_encoding('UTF-8');
1799           $found = mb_ereg($regex, $value, $matches);
1800         }
1801         else {
1802           $found = preg_match("/$regex/us", $value, $matches);
1803         }
1804         if ($found) {
1805           $value = $matches[1];
1806         }
1807       }
1808       // Remove scraps of HTML entities from the end of a strings
1809       $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
1810
1811       if (!empty($alter['ellipsis'])) {
1812         $value .= t('…');
1813       }
1814     }
1815     if (!empty($alter['html'])) {
1816       $value = Html::normalize($value);
1817     }
1818
1819     return $value;
1820   }
1821
1822   /**
1823    * Gets the link generator.
1824    *
1825    * @return \Drupal\Core\Utility\LinkGeneratorInterface
1826    */
1827   protected function linkGenerator() {
1828     if (!isset($this->linkGenerator)) {
1829       $this->linkGenerator = \Drupal::linkGenerator();
1830     }
1831     return $this->linkGenerator;
1832   }
1833
1834   /**
1835    * Returns the render API renderer.
1836    *
1837    * @return \Drupal\Core\Render\RendererInterface
1838    */
1839   protected function getRenderer() {
1840     if (!isset($this->renderer)) {
1841       $this->renderer = \Drupal::service('renderer');
1842     }
1843
1844     return $this->renderer;
1845   }
1846
1847 }
1848
1849 /**
1850  * @} End of "defgroup views_field_handlers".
1851  */