3 namespace Drupal\views\Plugin\views;
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;
18 * Base class for any views plugin types.
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
36 * - module: machine name of the module. It must be present for any plugin that
37 * wants to register a theme.
39 * @ingroup views_plugins
41 abstract class PluginBase extends ComponentPluginBase implements ContainerFactoryPluginInterface, ViewsPluginInterface, DependentPluginInterface {
44 * Include negotiated languages when listing languages.
46 * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
48 const INCLUDE_NEGOTIATED = 16;
51 * Include entity row languages when listing languages.
53 * @see \Drupal\views\Plugin\views\PluginBase::listLanguages()
55 const INCLUDE_ENTITY = 32;
58 * Query string to indicate the site default language.
60 * @see \Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT
62 const VIEWS_QUERY_LANGUAGE_SITE_DEFAULT = '***LANGUAGE_site_default***';
65 * Options for this plugin will be held here.
72 * The top object of a view.
74 * @var \Drupal\views\ViewExecutable
79 * The display object this plugin is for.
81 * For display plugins this is empty.
83 * @todo find a better description
85 * @var \Drupal\views\Plugin\views\display\DisplayPluginBase
87 public $displayHandler;
90 * Plugins's definition
97 * Denotes whether the plugin has an additional options form.
101 protected $usesOptions = FALSE;
104 * Stores the render API renderer.
106 * @var \Drupal\Core\Render\RendererInterface
111 * Constructs a PluginBase object.
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.
120 public function __construct(array $configuration, $plugin_id, $plugin_definition) {
121 parent::__construct($configuration, $plugin_id, $plugin_definition);
123 $this->definition = $plugin_definition + $configuration;
129 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
130 return new static($configuration, $plugin_id, $plugin_definition);
136 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
138 $this->setOptionDefaults($this->options, $this->defineOptions());
139 $this->displayHandler = $display;
141 $this->unpackOptions($this->options, $options);
145 * Information about options for all kinds of purposes will be held here.
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().
156 * Returns the options of this handler/plugin.
158 protected function defineOptions() {
163 * Fills up the options of the plugin with defaults.
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.
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']);
184 $storage[$option] = $definition['default'];
192 public function filterByDefinedOptions(array &$storage) {
193 $this->doFilterByDefinedOptions($storage, $this->defineOptions());
197 * Do the work to filter out stored options depending on the defined options.
199 * @param array $storage
200 * The stored options.
201 * @param array $options
202 * The defined options.
204 protected function doFilterByDefinedOptions(array &$storage, array $options) {
205 foreach ($storage as $key => $sub_storage) {
206 if (!isset($options[$key])) {
207 unset($storage[$key]);
210 if (isset($options[$key]['contains'])) {
211 $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']);
219 public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) {
220 if ($check && !is_array($options)) {
224 if (!isset($definition)) {
225 $definition = $this->defineOptions();
228 foreach ($options as $key => $value) {
229 if (is_array($value)) {
230 // Ignore arrays with no definition.
231 if (!$all && empty($definition[$key])) {
235 if (!isset($storage[$key]) || !is_array($storage[$key])) {
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;
247 $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : [], $all, FALSE);
249 elseif ($all || !empty($definition[$key])) {
250 $storage[$key] = $value;
258 public function destroy() {
259 unset($this->view, $this->display, $this->query);
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'];
276 public function validateOptionsForm(&$form, FormStateInterface $form_state) {}
281 public function submitOptionsForm(&$form, FormStateInterface $form_state) {}
286 public function query() {}
291 public function themeFunctions() {
292 return $this->view->buildThemeFunctions($this->definition['theme']);
298 public function validate() {
305 public function summaryTitle() {
306 return $this->t('Settings');
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'];
317 return $this->definition['title'];
323 public function usesOptions() {
324 return $this->usesOptions;
330 public function globalTokenReplace($string = '', array $options = []) {
331 return \Drupal::token()->replace($string, ['view' => $this->view], $options);
335 * Replaces Views' tokens in a given string. The resulting string will be
336 * sanitized with Xss::filterAdmin.
339 * Unsanitized string with possible tokens.
341 * Array of token => replacement_value items.
345 protected function viewsTokenReplace($text, $tokens) {
346 if (!strlen($text)) {
347 // No need to run filterAdmin on an empty string.
350 if (empty($tokens)) {
351 return Xss::filterAdmin($text);
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));
364 // Check for arrays in Twig tokens. Internally these are passed as
365 // dot-delimited strings, but need to be turned into associative arrays
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;
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];
384 if (!isset($twig_tokens[$top])) {
385 $twig_tokens[$top] = [];
387 $twig_tokens[$top] += $token_array;
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.
397 '#type' => 'inline_template',
398 '#template' => $text,
399 '#context' => $twig_tokens,
401 function ($children, $elements) {
402 return Xss::filterAdmin($children);
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);
414 return Xss::filterAdmin($text);
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));
427 // Construct the token string for each token.
430 foreach ($available as $type => $tokens) {
431 foreach (array_keys($tokens) as $token) {
432 $prepared[$type][] = "[$type:$token]";
445 public function globalTokenForm(&$form, FormStateInterface $form_state) {
448 foreach ($this->getAvailableGlobalTokens() as $type => $tokens) {
453 foreach ($tokens as $name => $info) {
454 $item['children'][$name] = "[$type:$name]" . ' - ' . $info['name'] . ': ' . $info['description'];
457 $token_items[$type] = $item;
460 $form['global_tokens'] = [
461 '#type' => 'details',
462 '#title' => $this->t('Available global token replacements'),
464 $form['global_tokens']['list'] = [
465 '#theme' => 'item_list',
466 '#items' => $token_items,
468 'class' => ['global-tokens'],
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.
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];
501 // All done, remove the now-empty parent.
512 public function calculateDependencies() {
519 public function getProvider() {
520 $definition = $this->getPluginDefinition();
521 return $definition['provider'];
525 * Makes an array of languages, optionally including special languages.
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.
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.
550 protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $current_values = NULL) {
551 $manager = \Drupal::languageManager();
552 $languages = $manager->getLanguages($flags);
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');
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);
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]);
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]);
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']]);
605 // Add real languages.
606 foreach ($languages as $id => $language) {
607 $list[$id] = $language->getName();
614 * Returns substitutions for Views queries for languages.
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.
622 * An array in the format of hook_views_query_substitutions() that gives
623 * the query substitutions needed for the special language types.
625 public static function queryLanguageSubstitutions() {
627 $manager = \Drupal::languageManager();
629 // Handle default language.
630 $default = $manager->getDefaultLanguage()->getId();
631 $changes[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $default;
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();
645 * Returns the render API renderer.
647 * @return \Drupal\Core\Render\RendererInterface
649 protected function getRenderer() {
650 if (!isset($this->renderer)) {
651 $this->renderer = \Drupal::service('renderer');
654 return $this->renderer;