Version 1
[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() { return []; }
159
160   /**
161    * Fills up the options of the plugin with defaults.
162    *
163    * @param array $storage
164    *   An array which stores the actual option values of the plugin.
165    * @param array $options
166    *   An array which describes the options of a plugin. Each element is an
167    *   associative array containing:
168    *   - default: The default value of one option. Should be translated to the
169    *     interface text language selected for page if translatable.
170    *   - (optional) contains: An array which describes the available options
171    *     under the key. If contains is set, the default will be ignored and
172    *     assumed to be an empty array.
173    *   - (optional) 'bool': TRUE if the value is boolean, else FALSE.
174    */
175   protected function setOptionDefaults(array &$storage, array $options) {
176     foreach ($options as $option => $definition) {
177       if (isset($definition['contains'])) {
178         $storage[$option] = [];
179         $this->setOptionDefaults($storage[$option], $definition['contains']);
180       }
181       else {
182         $storage[$option] = $definition['default'];
183       }
184     }
185   }
186
187   /**
188    * {@inheritdoc}
189    */
190   public function filterByDefinedOptions(array &$storage) {
191     $this->doFilterByDefinedOptions($storage, $this->defineOptions());
192   }
193
194   /**
195    * Do the work to filter out stored options depending on the defined options.
196    *
197    * @param array $storage
198    *   The stored options.
199    * @param array $options
200    *   The defined options.
201    */
202   protected function doFilterByDefinedOptions(array &$storage, array $options) {
203     foreach ($storage as $key => $sub_storage) {
204       if (!isset($options[$key])) {
205         unset($storage[$key]);
206       }
207
208       if (isset($options[$key]['contains'])) {
209         $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']);
210       }
211     }
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) {
218     if ($check && !is_array($options)) {
219       return;
220     }
221
222     if (!isset($definition)) {
223       $definition = $this->defineOptions();
224     }
225
226     foreach ($options as $key => $value) {
227       if (is_array($value)) {
228         // Ignore arrays with no definition.
229         if (!$all && empty($definition[$key])) {
230           continue;
231         }
232
233         if (!isset($storage[$key]) || !is_array($storage[$key])) {
234           $storage[$key] = [];
235         }
236
237         // If we're just unpacking our known options, and we're dropping an
238         // unknown array (as might happen for a dependent plugin fields) go
239         // ahead and drop that in.
240         if (!$all && isset($definition[$key]) && !isset($definition[$key]['contains'])) {
241           $storage[$key] = $value;
242           continue;
243         }
244
245         $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : [], $all, FALSE);
246       }
247       elseif ($all || !empty($definition[$key])) {
248         $storage[$key] = $value;
249       }
250     }
251   }
252
253   /**
254    * {@inheritdoc}
255    */
256   public function destroy() {
257     unset($this->view, $this->display, $this->query);
258   }
259
260   /**
261    * {@inheritdoc}
262    */
263   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
264     // Some form elements belong in a fieldset for presentation, but can't
265     // be moved into one because of the $form_state->getValues() hierarchy. Those
266     // elements can add a #fieldset => 'fieldset_name' property, and they'll
267     // be moved to their fieldset during pre_render.
268     $form['#pre_render'][] = [get_class($this), 'preRenderAddFieldsetMarkup'];
269   }
270
271   /**
272    * {@inheritdoc}
273    */
274   public function validateOptionsForm(&$form, FormStateInterface $form_state) { }
275
276   /**
277    * {@inheritdoc}
278    */
279   public function submitOptionsForm(&$form, FormStateInterface $form_state) { }
280
281   /**
282    * {@inheritdoc}
283    */
284   public function query() { }
285
286   /**
287    * {@inheritdoc}
288    */
289   public function themeFunctions() {
290     return $this->view->buildThemeFunctions($this->definition['theme']);
291   }
292
293   /**
294    * {@inheritdoc}
295    */
296   public function validate() { return []; }
297
298   /**
299    * {@inheritdoc}
300    */
301   public function summaryTitle() {
302     return $this->t('Settings');
303   }
304
305   /**
306    * {@inheritdoc}
307    */
308   public function pluginTitle() {
309     // Short_title is optional so its defaults to an empty string.
310     if (!empty($this->definition['short_title'])) {
311       return $this->definition['short_title'];
312     }
313     return $this->definition['title'];
314   }
315
316   /**
317    * {@inheritdoc}
318    */
319   public function usesOptions() {
320     return $this->usesOptions;
321   }
322
323   /**
324    * {@inheritdoc}
325    */
326   public function globalTokenReplace($string = '', array $options = []) {
327     return \Drupal::token()->replace($string, ['view' => $this->view], $options);
328   }
329
330   /**
331    * Replaces Views' tokens in a given string. The resulting string will be
332    * sanitized with Xss::filterAdmin.
333    *
334    * @param $text
335    *   Unsanitized string with possible tokens.
336    * @param $tokens
337    *   Array of token => replacement_value items.
338    *
339    * @return string
340    */
341   protected function viewsTokenReplace($text, $tokens) {
342     if (!strlen($text)) {
343       // No need to run filterAdmin on an empty string.
344       return '';
345     }
346     if (empty($tokens)) {
347       return Xss::filterAdmin($text);
348     }
349
350     $twig_tokens = [];
351     foreach ($tokens as $token => $replacement) {
352       // Twig wants a token replacement array stripped of curly-brackets.
353       // Some Views tokens come with curly-braces, others do not.
354       //@todo: https://www.drupal.org/node/2544392
355       if (strpos($token, '{{') !== FALSE) {
356         // Twig wants a token replacement array stripped of curly-brackets.
357         $token = trim(str_replace(['{{', '}}'], '', $token));
358       }
359
360       // Check for arrays in Twig tokens. Internally these are passed as
361       // dot-delimited strings, but need to be turned into associative arrays
362       // for parsing.
363       if (strpos($token, '.') === FALSE) {
364         // We need to validate tokens are valid Twig variables. Twig uses the
365         // same variable naming rules as PHP.
366         // @see http://php.net/manual/language.variables.basics.php
367         assert('preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $token) === 1', 'Tokens need to be valid Twig variables.');
368         $twig_tokens[$token] = $replacement;
369       }
370       else {
371         $parts = explode('.', $token);
372         $top = array_shift($parts);
373         assert('preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $top) === 1', 'Tokens need to be valid Twig variables.');
374         $token_array = [array_pop($parts) => $replacement];
375         foreach (array_reverse($parts) as $key) {
376           // The key could also be numeric (array index) so allow that.
377           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.');
378           $token_array = [$key => $token_array];
379         }
380         if (!isset($twig_tokens[$top])) {
381           $twig_tokens[$top] = [];
382         }
383         $twig_tokens[$top] += $token_array;
384       }
385     }
386
387     if ($twig_tokens) {
388       // Use the unfiltered text for the Twig template, then filter the output.
389       // Otherwise, Xss::filterAdmin could remove valid Twig syntax before the
390       // template is parsed.
391
392       $build = [
393         '#type' => 'inline_template',
394         '#template' => $text,
395         '#context' => $twig_tokens,
396         '#post_render' => [
397           function ($children, $elements) {
398             return Xss::filterAdmin($children);
399           }
400         ],
401       ];
402
403       // Currently you cannot attach assets to tokens with
404       // Renderer::renderPlain(). This may be unnecessarily limiting. Consider
405       // using Renderer::executeInRenderContext() instead.
406       // @todo: https://www.drupal.org/node/2566621
407       return (string) $this->getRenderer()->renderPlain($build);
408     }
409     else {
410       return Xss::filterAdmin($text);
411     }
412   }
413
414   /**
415    * {@inheritdoc}
416    */
417   public function getAvailableGlobalTokens($prepared = FALSE, array $types = []) {
418     $info = \Drupal::token()->getInfo();
419     // Site and view tokens should always be available.
420     $types += ['site', 'view'];
421     $available = array_intersect_key($info['tokens'], array_flip($types));
422
423     // Construct the token string for each token.
424     if ($prepared) {
425       $prepared = [];
426       foreach ($available as $type => $tokens) {
427         foreach (array_keys($tokens) as $token) {
428           $prepared[$type][] = "[$type:$token]";
429         }
430       }
431
432       return $prepared;
433     }
434
435     return $available;
436   }
437
438   /**
439    * {@inheritdoc}
440    */
441   public function globalTokenForm(&$form, FormStateInterface $form_state) {
442     $token_items = [];
443
444     foreach ($this->getAvailableGlobalTokens() as $type => $tokens) {
445       $item = [
446         '#markup' => $type,
447         'children' => [],
448       ];
449       foreach ($tokens as $name => $info) {
450         $item['children'][$name] = "[$type:$name]" . ' - ' . $info['name'] . ': ' . $info['description'];
451       }
452
453       $token_items[$type] = $item;
454     }
455
456     $form['global_tokens'] = [
457       '#type' => 'details',
458       '#title' => $this->t('Available global token replacements'),
459     ];
460     $form['global_tokens']['list'] = [
461       '#theme' => 'item_list',
462       '#items' => $token_items,
463       '#attributes' => [
464         'class' => ['global-tokens'],
465       ],
466     ];
467   }
468
469   /**
470    * {@inheritdoc}
471    */
472   public static function preRenderAddFieldsetMarkup(array $form) {
473     foreach (Element::children($form) as $key) {
474       $element = $form[$key];
475       // In our form builder functions, we added an arbitrary #fieldset property
476       // to any element that belongs in a fieldset. If this form element has
477       // that property, move it into its fieldset.
478       if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) {
479         $form[$element['#fieldset']][$key] = $element;
480         // Remove the original element this duplicates.
481         unset($form[$key]);
482       }
483     }
484     return $form;
485   }
486
487   /**
488    * {@inheritdoc}
489    */
490   public static function preRenderFlattenData($form) {
491     foreach (Element::children($form) as $key) {
492       $element = $form[$key];
493       if (!empty($element['#flatten'])) {
494         foreach (Element::children($element) as $child_key) {
495           $form[$child_key] = $form[$key][$child_key];
496         }
497         // All done, remove the now-empty parent.
498         unset($form[$key]);
499       }
500     }
501
502     return $form;
503   }
504
505   /**
506    * {@inheritdoc}
507    */
508   public function calculateDependencies() {
509     return [];
510   }
511
512   /**
513    * {@inheritdoc}
514    */
515   public function getProvider() {
516     $definition = $this->getPluginDefinition();
517     return $definition['provider'];
518   }
519
520   /**
521    * Makes an array of languages, optionally including special languages.
522    *
523    * @param int $flags
524    *   (optional) Flags for which languages to return (additive). Options:
525    *   - \Drupal\Core\Language::STATE_ALL (default): All languages
526    *     (configurable and default).
527    *   - \Drupal\Core\Language::STATE_CONFIGURABLE: Configurable languages.
528    *   - \Drupal\Core\Language::STATE_LOCKED: Locked languages.
529    *   - \Drupal\Core\Language::STATE_SITE_DEFAULT: Add site default language;
530    *     note that this is not included in STATE_ALL.
531    *   - \Drupal\views\Plugin\views\PluginBase::INCLUDE_NEGOTIATED: Add
532    *     negotiated language types.
533    *   - \Drupal\views\Plugin\views\PluginBase::INCLUDE_ENTITY: Add
534    *     entity row language types. Note that these are only supported for
535    *     display options, not substituted in queries.
536    * @param array|null $current_values
537    *   The currently-selected options in the list, if available.
538    *
539    * @return array
540    *   An array of language names, keyed by the language code. Negotiated and
541    *   special languages have special codes that are substituted in queries by
542    *   PluginBase::queryLanguageSubstitutions().
543    *   Only configurable languages and languages that are in $current_values are
544    *   included in the list.
545    */
546   protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $current_values = NULL) {
547     $manager = \Drupal::languageManager();
548     $languages = $manager->getLanguages($flags);
549     $list = [];
550
551     // The entity languages should come first, if requested.
552     if ($flags & PluginBase::INCLUDE_ENTITY) {
553       $list['***LANGUAGE_entity_translation***'] = $this->t('Content language of view row');
554       $list['***LANGUAGE_entity_default***'] = $this->t('Original language of content in view row');
555     }
556
557     // STATE_SITE_DEFAULT comes in with ID set
558     // to LanguageInterface::LANGCODE_SITE_DEFAULT.
559     // Since this is not a real language, surround it by '***LANGUAGE_...***',
560     // like the negotiated languages below.
561     if ($flags & LanguageInterface::STATE_SITE_DEFAULT) {
562       $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName();
563       // The language name may have already been translated, no need to
564       // translate it again.
565       // @see Drupal\Core\Language::filterLanguages().
566       if (!$name instanceof TranslatableMarkup) {
567         $name = $this->t($name);
568       }
569       $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name;
570       // Remove site default language from $languages so it's not added
571       // twice with the real languages below.
572       unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]);
573     }
574
575     // Add in negotiated languages, if requested.
576     if ($flags & PluginBase::INCLUDE_NEGOTIATED) {
577       $types_info = $manager->getDefinedLanguageTypesInfo();
578       $types = $manager->getLanguageTypes();
579       // We only go through the configured types.
580       foreach ($types as $id) {
581         if (isset($types_info[$id]['name'])) {
582           $name = $types_info[$id]['name'];
583           // Surround IDs by '***LANGUAGE_...***', to avoid query collisions.
584           $id = '***LANGUAGE_' . $id . '***';
585           $list[$id] = $this->t('@type language selected for page', ['@type' => $name]);
586         }
587       }
588       if (!empty($current_values)) {
589         foreach ($types_info as $id => $type) {
590           $id = '***LANGUAGE_' . $id . '***';
591           // If this (non-configurable) type is among the current values,
592           // add that option too, so it is not lost. If not among the current
593           // values, skip displaying it to avoid user confusion.
594           if (isset($type['name']) && !isset($list[$id]) && in_array($id, $current_values)) {
595             $list[$id] = $this->t('@type language selected for page', ['@type' => $type['name']]);
596           }
597         }
598       }
599     }
600
601     // Add real languages.
602     foreach ($languages as $id => $language) {
603       $list[$id] = $language->getName();
604     }
605
606     return $list;
607   }
608
609   /**
610    * Returns substitutions for Views queries for languages.
611    *
612    * This is needed so that the language options returned by
613    * PluginBase::listLanguages() are able to be used in queries. It is called
614    * by the Views module implementation of hook_views_query_substitutions()
615    * to get the language-related substitutions.
616    *
617    * @return array
618    *   An array in the format of hook_views_query_substitutions() that gives
619    *   the query substitutions needed for the special language types.
620    */
621   public static function queryLanguageSubstitutions() {
622     $changes = [];
623     $manager = \Drupal::languageManager();
624
625     // Handle default language.
626     $default = $manager->getDefaultLanguage()->getId();
627     $changes[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $default;
628
629     // Handle negotiated languages.
630     $types = $manager->getDefinedLanguageTypesInfo();
631     foreach ($types as $id => $type) {
632       if (isset($type['name'])) {
633         $changes['***LANGUAGE_' . $id . '***'] = $manager->getCurrentLanguage($id)->getId();
634       }
635     }
636
637     return $changes;
638   }
639
640   /**
641    * Returns the render API renderer.
642    *
643    * @return \Drupal\Core\Render\RendererInterface
644    */
645   protected function getRenderer() {
646     if (!isset($this->renderer)) {
647       $this->renderer = \Drupal::service('renderer');
648     }
649
650     return $this->renderer;
651   }
652
653 }