Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / views / src / Plugin / views / PluginBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views;
4
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Utility\Xss;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10 use Drupal\Core\Plugin\PluginBase as ComponentPluginBase;
11 use Drupal\Core\Render\Element;
12 use Drupal\Core\StringTranslation\TranslatableMarkup;
13 use Drupal\views\Plugin\views\display\DisplayPluginBase;
14 use Drupal\views\ViewExecutable;
15 use Symfony\Component\DependencyInjection\ContainerInterface;
16
17 /**
18  * Base class for any views plugin types.
19  *
20  * Via the @Plugin definition the plugin may specify a theme function or
21  * template to be used for the plugin. It also can auto-register the theme
22  * implementation for that file or function.
23  * - theme: the theme implementation to use in the plugin. This may be the name
24  *   of the function (without theme_ prefix) or the template file (without
25  *   template engine extension).
26  *   If a template file should be used, the file has to be placed in the
27  *   module's templates folder.
28  *   Example: theme = "mymodule_row" of module "mymodule" will implement
29  *   mymodule-row.html.twig in the [..]/modules/mymodule/templates folder.
30  * - register_theme: (optional) When set to TRUE (default) the theme is
31  *   registered automatically. When set to FALSE the plugin reuses an existing
32  *   theme implementation, defined by another module or views plugin.
33  * - theme_file: (optional) the location of an include file that may hold the
34  *   theme or preprocess function. The location has to be relative to module's
35  *   root directory.
36  * - module: machine name of the module. It must be present for any plugin that
37  *   wants to register a theme.
38  *
39  * @ingroup views_plugins
40  */
41 abstract class PluginBase extends ComponentPluginBase implements ContainerFactoryPluginInterface, ViewsPluginInterface, DependentPluginInterface {
42
43   /**
44    * Include negotiated languages when listing languages.
45    *
46    * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
47    */
48   const INCLUDE_NEGOTIATED = 16;
49
50   /**
51    * Include entity row languages when listing languages.
52    *
53    * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
54    */
55   const INCLUDE_ENTITY = 32;
56
57   /**
58    * Query string to indicate the site default language.
59    *
60    * @see \Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT
61    */
62   const VIEWS_QUERY_LANGUAGE_SITE_DEFAULT = '***LANGUAGE_site_default***';
63
64   /**
65    * Options for this plugin will be held here.
66    *
67    * @var array
68    */
69   public $options = [];
70
71   /**
72    * The top object of a view.
73    *
74    * @var \Drupal\views\ViewExecutable
75    */
76   public $view = NULL;
77
78   /**
79    * The display object this plugin is for.
80    *
81    * For display plugins this is empty.
82    *
83    * @todo find a better description
84    *
85    * @var \Drupal\views\Plugin\views\display\DisplayPluginBase
86    */
87   public $displayHandler;
88
89   /**
90    * Plugins's definition
91    *
92    * @var array
93    */
94   public $definition;
95
96   /**
97    * Denotes whether the plugin has an additional options form.
98    *
99    * @var bool
100    */
101   protected $usesOptions = FALSE;
102
103   /**
104    * Stores the render API renderer.
105    *
106    * @var \Drupal\Core\Render\RendererInterface
107    */
108   protected $renderer;
109
110   /**
111    * Constructs a PluginBase object.
112    *
113    * @param array $configuration
114    *   A configuration array containing information about the plugin instance.
115    * @param string $plugin_id
116    *   The plugin_id for the plugin instance.
117    * @param mixed $plugin_definition
118    *   The plugin implementation definition.
119    */
120   public function __construct(array $configuration, $plugin_id, $plugin_definition) {
121     parent::__construct($configuration, $plugin_id, $plugin_definition);
122
123     $this->definition = $plugin_definition + $configuration;
124   }
125
126   /**
127    * {@inheritdoc}
128    */
129   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
130     return new static($configuration, $plugin_id, $plugin_definition);
131   }
132
133   /**
134    * {@inheritdoc}
135    */
136   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
137     $this->view = $view;
138     $this->setOptionDefaults($this->options, $this->defineOptions());
139     $this->displayHandler = $display;
140
141     $this->unpackOptions($this->options, $options);
142   }
143
144   /**
145    * Information about options for all kinds of purposes will be held here.
146    * @code
147    * 'option_name' => array(
148    *  - 'default' => default value,
149    *  - 'contains' => (optional) array of items this contains, with its own
150    *      defaults, etc. If contains is set, the default will be ignored and
151    *      assumed to be array().
152    *  ),
153    * @endcode
154    *
155    * @return array
156    *   Returns the options of this handler/plugin.
157    */
158   protected function defineOptions() {
159     return [];
160   }
161
162   /**
163    * Fills up the options of the plugin with defaults.
164    *
165    * @param array $storage
166    *   An array which stores the actual option values of the plugin.
167    * @param array $options
168    *   An array which describes the options of a plugin. Each element is an
169    *   associative array containing:
170    *   - default: The default value of one option. Should be translated to the
171    *     interface text language selected for page if translatable.
172    *   - (optional) contains: An array which describes the available options
173    *     under the key. If contains is set, the default will be ignored and
174    *     assumed to be an empty array.
175    *   - (optional) 'bool': TRUE if the value is boolean, else FALSE.
176    */
177   protected function setOptionDefaults(array &$storage, array $options) {
178     foreach ($options as $option => $definition) {
179       if (isset($definition['contains'])) {
180         $storage[$option] = [];
181         $this->setOptionDefaults($storage[$option], $definition['contains']);
182       }
183       else {
184         $storage[$option] = $definition['default'];
185       }
186     }
187   }
188
189   /**
190    * {@inheritdoc}
191    */
192   public function filterByDefinedOptions(array &$storage) {
193     $this->doFilterByDefinedOptions($storage, $this->defineOptions());
194   }
195
196   /**
197    * Do the work to filter out stored options depending on the defined options.
198    *
199    * @param array $storage
200    *   The stored options.
201    * @param array $options
202    *   The defined options.
203    */
204   protected function doFilterByDefinedOptions(array &$storage, array $options) {
205     foreach ($storage as $key => $sub_storage) {
206       if (!isset($options[$key])) {
207         unset($storage[$key]);
208       }
209
210       if (isset($options[$key]['contains'])) {
211         $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']);
212       }
213     }
214   }
215
216   /**
217    * {@inheritdoc}
218    */
219   public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) {
220     if ($check && !is_array($options)) {
221       return;
222     }
223
224     if (!isset($definition)) {
225       $definition = $this->defineOptions();
226     }
227
228     foreach ($options as $key => $value) {
229       if (is_array($value)) {
230         // Ignore arrays with no definition.
231         if (!$all && empty($definition[$key])) {
232           continue;
233         }
234
235         if (!isset($storage[$key]) || !is_array($storage[$key])) {
236           $storage[$key] = [];
237         }
238
239         // If we're just unpacking our known options, and we're dropping an
240         // unknown array (as might happen for a dependent plugin fields) go
241         // ahead and drop that in.
242         if (!$all && isset($definition[$key]) && !isset($definition[$key]['contains'])) {
243           $storage[$key] = $value;
244           continue;
245         }
246
247         $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : [], $all, FALSE);
248       }
249       elseif ($all || !empty($definition[$key])) {
250         $storage[$key] = $value;
251       }
252     }
253   }
254
255   /**
256    * {@inheritdoc}
257    */
258   public function destroy() {
259     unset($this->view, $this->display, $this->query);
260   }
261
262   /**
263    * {@inheritdoc}
264    */
265   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
266     // Some form elements belong in a fieldset for presentation, but can't
267     // be moved into one because of the $form_state->getValues() hierarchy. Those
268     // elements can add a #fieldset => 'fieldset_name' property, and they'll
269     // be moved to their fieldset during pre_render.
270     $form['#pre_render'][] = [get_class($this), 'preRenderAddFieldsetMarkup'];
271   }
272
273   /**
274    * {@inheritdoc}
275    */
276   public function validateOptionsForm(&$form, FormStateInterface $form_state) {}
277
278   /**
279    * {@inheritdoc}
280    */
281   public function submitOptionsForm(&$form, FormStateInterface $form_state) {}
282
283   /**
284    * {@inheritdoc}
285    */
286   public function query() {}
287
288   /**
289    * {@inheritdoc}
290    */
291   public function themeFunctions() {
292     return $this->view->buildThemeFunctions($this->definition['theme']);
293   }
294
295   /**
296    * {@inheritdoc}
297    */
298   public function validate() {
299     return [];
300   }
301
302   /**
303    * {@inheritdoc}
304    */
305   public function summaryTitle() {
306     return $this->t('Settings');
307   }
308
309   /**
310    * {@inheritdoc}
311    */
312   public function pluginTitle() {
313     // Short_title is optional so its defaults to an empty string.
314     if (!empty($this->definition['short_title'])) {
315       return $this->definition['short_title'];
316     }
317     return $this->definition['title'];
318   }
319
320   /**
321    * {@inheritdoc}
322    */
323   public function usesOptions() {
324     return $this->usesOptions;
325   }
326
327   /**
328    * {@inheritdoc}
329    */
330   public function globalTokenReplace($string = '', array $options = []) {
331     return \Drupal::token()->replace($string, ['view' => $this->view], $options);
332   }
333
334   /**
335    * Replaces Views' tokens in a given string. The resulting string will be
336    * sanitized with Xss::filterAdmin.
337    *
338    * @param $text
339    *   Unsanitized string with possible tokens.
340    * @param $tokens
341    *   Array of token => replacement_value items.
342    *
343    * @return string
344    */
345   protected function viewsTokenReplace($text, $tokens) {
346     if (!strlen($text)) {
347       // No need to run filterAdmin on an empty string.
348       return '';
349     }
350     if (empty($tokens)) {
351       return Xss::filterAdmin($text);
352     }
353
354     $twig_tokens = [];
355     foreach ($tokens as $token => $replacement) {
356       // Twig wants a token replacement array stripped of curly-brackets.
357       // Some Views tokens come with curly-braces, others do not.
358       // @todo: https://www.drupal.org/node/2544392
359       if (strpos($token, '{{') !== FALSE) {
360         // Twig wants a token replacement array stripped of curly-brackets.
361         $token = trim(str_replace(['{{', '}}'], '', $token));
362       }
363
364       // Check for arrays in Twig tokens. Internally these are passed as
365       // dot-delimited strings, but need to be turned into associative arrays
366       // for parsing.
367       if (strpos($token, '.') === FALSE) {
368         // We need to validate tokens are valid Twig variables. Twig uses the
369         // same variable naming rules as PHP.
370         // @see http://php.net/manual/language.variables.basics.php
371         assert(preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $token) === 1, 'Tokens need to be valid Twig variables.');
372         $twig_tokens[$token] = $replacement;
373       }
374       else {
375         $parts = explode('.', $token);
376         $top = array_shift($parts);
377         assert(preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $top) === 1, 'Tokens need to be valid Twig variables.');
378         $token_array = [array_pop($parts) => $replacement];
379         foreach (array_reverse($parts) as $key) {
380           // The key could also be numeric (array index) so allow that.
381           assert(is_numeric($key) || preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $key) === 1, 'Tokens need to be valid Twig variables.');
382           $token_array = [$key => $token_array];
383         }
384         if (!isset($twig_tokens[$top])) {
385           $twig_tokens[$top] = [];
386         }
387         $twig_tokens[$top] += $token_array;
388       }
389     }
390
391     if ($twig_tokens) {
392       // Use the unfiltered text for the Twig template, then filter the output.
393       // Otherwise, Xss::filterAdmin could remove valid Twig syntax before the
394       // template is parsed.
395
396       $build = [
397         '#type' => 'inline_template',
398         '#template' => $text,
399         '#context' => $twig_tokens,
400         '#post_render' => [
401           function ($children, $elements) {
402             return Xss::filterAdmin($children);
403           },
404         ],
405       ];
406
407       // Currently you cannot attach assets to tokens with
408       // Renderer::renderPlain(). This may be unnecessarily limiting. Consider
409       // using Renderer::executeInRenderContext() instead.
410       // @todo: https://www.drupal.org/node/2566621
411       return (string) $this->getRenderer()->renderPlain($build);
412     }
413     else {
414       return Xss::filterAdmin($text);
415     }
416   }
417
418   /**
419    * {@inheritdoc}
420    */
421   public function getAvailableGlobalTokens($prepared = FALSE, array $types = []) {
422     $info = \Drupal::token()->getInfo();
423     // Site and view tokens should always be available.
424     $types += ['site', 'view'];
425     $available = array_intersect_key($info['tokens'], array_flip($types));
426
427     // Construct the token string for each token.
428     if ($prepared) {
429       $prepared = [];
430       foreach ($available as $type => $tokens) {
431         foreach (array_keys($tokens) as $token) {
432           $prepared[$type][] = "[$type:$token]";
433         }
434       }
435
436       return $prepared;
437     }
438
439     return $available;
440   }
441
442   /**
443    * {@inheritdoc}
444    */
445   public function globalTokenForm(&$form, FormStateInterface $form_state) {
446     $token_items = [];
447
448     foreach ($this->getAvailableGlobalTokens() as $type => $tokens) {
449       $item = [
450         '#markup' => $type,
451         'children' => [],
452       ];
453       foreach ($tokens as $name => $info) {
454         $item['children'][$name] = "[$type:$name]" . ' - ' . $info['name'] . ': ' . $info['description'];
455       }
456
457       $token_items[$type] = $item;
458     }
459
460     $form['global_tokens'] = [
461       '#type' => 'details',
462       '#title' => $this->t('Available global token replacements'),
463     ];
464     $form['global_tokens']['list'] = [
465       '#theme' => 'item_list',
466       '#items' => $token_items,
467       '#attributes' => [
468         'class' => ['global-tokens'],
469       ],
470     ];
471   }
472
473   /**
474    * {@inheritdoc}
475    */
476   public static function preRenderAddFieldsetMarkup(array $form) {
477     foreach (Element::children($form) as $key) {
478       $element = $form[$key];
479       // In our form builder functions, we added an arbitrary #fieldset property
480       // to any element that belongs in a fieldset. If this form element has
481       // that property, move it into its fieldset.
482       if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) {
483         $form[$element['#fieldset']][$key] = $element;
484         // Remove the original element this duplicates.
485         unset($form[$key]);
486       }
487     }
488     return $form;
489   }
490
491   /**
492    * {@inheritdoc}
493    */
494   public static function preRenderFlattenData($form) {
495     foreach (Element::children($form) as $key) {
496       $element = $form[$key];
497       if (!empty($element['#flatten'])) {
498         foreach (Element::children($element) as $child_key) {
499           $form[$child_key] = $form[$key][$child_key];
500         }
501         // All done, remove the now-empty parent.
502         unset($form[$key]);
503       }
504     }
505
506     return $form;
507   }
508
509   /**
510    * {@inheritdoc}
511    */
512   public function calculateDependencies() {
513     return [];
514   }
515
516   /**
517    * {@inheritdoc}
518    */
519   public function getProvider() {
520     $definition = $this->getPluginDefinition();
521     return $definition['provider'];
522   }
523
524   /**
525    * Makes an array of languages, optionally including special languages.
526    *
527    * @param int $flags
528    *   (optional) Flags for which languages to return (additive). Options:
529    *   - \Drupal\Core\Language::STATE_ALL (default): All languages
530    *     (configurable and default).
531    *   - \Drupal\Core\Language::STATE_CONFIGURABLE: Configurable languages.
532    *   - \Drupal\Core\Language::STATE_LOCKED: Locked languages.
533    *   - \Drupal\Core\Language::STATE_SITE_DEFAULT: Add site default language;
534    *     note that this is not included in STATE_ALL.
535    *   - \Drupal\views\Plugin\views\PluginBase::INCLUDE_NEGOTIATED: Add
536    *     negotiated language types.
537    *   - \Drupal\views\Plugin\views\PluginBase::INCLUDE_ENTITY: Add
538    *     entity row language types. Note that these are only supported for
539    *     display options, not substituted in queries.
540    * @param array|null $current_values
541    *   The currently-selected options in the list, if available.
542    *
543    * @return array
544    *   An array of language names, keyed by the language code. Negotiated and
545    *   special languages have special codes that are substituted in queries by
546    *   PluginBase::queryLanguageSubstitutions().
547    *   Only configurable languages and languages that are in $current_values are
548    *   included in the list.
549    */
550   protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $current_values = NULL) {
551     $manager = \Drupal::languageManager();
552     $languages = $manager->getLanguages($flags);
553     $list = [];
554
555     // The entity languages should come first, if requested.
556     if ($flags & PluginBase::INCLUDE_ENTITY) {
557       $list['***LANGUAGE_entity_translation***'] = $this->t('Content language of view row');
558       $list['***LANGUAGE_entity_default***'] = $this->t('Original language of content in view row');
559     }
560
561     // STATE_SITE_DEFAULT comes in with ID set
562     // to LanguageInterface::LANGCODE_SITE_DEFAULT.
563     // Since this is not a real language, surround it by '***LANGUAGE_...***',
564     // like the negotiated languages below.
565     if ($flags & LanguageInterface::STATE_SITE_DEFAULT) {
566       $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName();
567       // The language name may have already been translated, no need to
568       // translate it again.
569       // @see Drupal\Core\Language::filterLanguages().
570       if (!$name instanceof TranslatableMarkup) {
571         $name = $this->t($name);
572       }
573       $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name;
574       // Remove site default language from $languages so it's not added
575       // twice with the real languages below.
576       unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]);
577     }
578
579     // Add in negotiated languages, if requested.
580     if ($flags & PluginBase::INCLUDE_NEGOTIATED) {
581       $types_info = $manager->getDefinedLanguageTypesInfo();
582       $types = $manager->getLanguageTypes();
583       // We only go through the configured types.
584       foreach ($types as $id) {
585         if (isset($types_info[$id]['name'])) {
586           $name = $types_info[$id]['name'];
587           // Surround IDs by '***LANGUAGE_...***', to avoid query collisions.
588           $id = '***LANGUAGE_' . $id . '***';
589           $list[$id] = $this->t('@type language selected for page', ['@type' => $name]);
590         }
591       }
592       if (!empty($current_values)) {
593         foreach ($types_info as $id => $type) {
594           $id = '***LANGUAGE_' . $id . '***';
595           // If this (non-configurable) type is among the current values,
596           // add that option too, so it is not lost. If not among the current
597           // values, skip displaying it to avoid user confusion.
598           if (isset($type['name']) && !isset($list[$id]) && in_array($id, $current_values)) {
599             $list[$id] = $this->t('@type language selected for page', ['@type' => $type['name']]);
600           }
601         }
602       }
603     }
604
605     // Add real languages.
606     foreach ($languages as $id => $language) {
607       $list[$id] = $language->getName();
608     }
609
610     return $list;
611   }
612
613   /**
614    * Returns substitutions for Views queries for languages.
615    *
616    * This is needed so that the language options returned by
617    * PluginBase::listLanguages() are able to be used in queries. It is called
618    * by the Views module implementation of hook_views_query_substitutions()
619    * to get the language-related substitutions.
620    *
621    * @return array
622    *   An array in the format of hook_views_query_substitutions() that gives
623    *   the query substitutions needed for the special language types.
624    */
625   public static function queryLanguageSubstitutions() {
626     $changes = [];
627     $manager = \Drupal::languageManager();
628
629     // Handle default language.
630     $default = $manager->getDefaultLanguage()->getId();
631     $changes[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $default;
632
633     // Handle negotiated languages.
634     $types = $manager->getDefinedLanguageTypesInfo();
635     foreach ($types as $id => $type) {
636       if (isset($type['name'])) {
637         $changes['***LANGUAGE_' . $id . '***'] = $manager->getCurrentLanguage($id)->getId();
638       }
639     }
640
641     return $changes;
642   }
643
644   /**
645    * Returns the render API renderer.
646    *
647    * @return \Drupal\Core\Render\RendererInterface
648    */
649   protected function getRenderer() {
650     if (!isset($this->renderer)) {
651       $this->renderer = \Drupal::service('renderer');
652     }
653
654     return $this->renderer;
655   }
656
657 }