"Fossies" - the Fresh Open Source Software Archive

Member "drupal-8.9.10/core/modules/views/src/Plugin/views/PluginBase.php" (26 Nov 2020, 21280 Bytes) of package /linux/www/drupal-8.9.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "PluginBase.php" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 9.0.8_vs_9.1.0-rc1.

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