definition = $plugin_definition + $configuration; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static($configuration, $plugin_id, $plugin_definition); } /** * {@inheritdoc} */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { $this->view = $view; $this->setOptionDefaults($this->options, $this->defineOptions()); $this->displayHandler = $display; $this->unpackOptions($this->options, $options); } /** * Information about options for all kinds of purposes will be held here. * @code * 'option_name' => array( * - 'default' => default value, * - 'contains' => (optional) array of items this contains, with its own * defaults, etc. If contains is set, the default will be ignored and * assumed to be array(). * ), * @endcode * * @return array * Returns the options of this handler/plugin. */ protected function defineOptions() { return []; } /** * Fills up the options of the plugin with defaults. * * @param array $storage * An array which stores the actual option values of the plugin. * @param array $options * An array which describes the options of a plugin. Each element is an * associative array containing: * - default: The default value of one option. Should be translated to the * interface text language selected for page if translatable. * - (optional) contains: An array which describes the available options * under the key. If contains is set, the default will be ignored and * assumed to be an empty array. * - (optional) 'bool': TRUE if the value is boolean, else FALSE. */ protected function setOptionDefaults(array &$storage, array $options) { foreach ($options as $option => $definition) { if (isset($definition['contains'])) { $storage[$option] = []; $this->setOptionDefaults($storage[$option], $definition['contains']); } else { $storage[$option] = $definition['default']; } } } /** * {@inheritdoc} */ public function filterByDefinedOptions(array &$storage) { $this->doFilterByDefinedOptions($storage, $this->defineOptions()); } /** * Do the work to filter out stored options depending on the defined options. * * @param array $storage * The stored options. * @param array $options * The defined options. */ protected function doFilterByDefinedOptions(array &$storage, array $options) { foreach ($storage as $key => $sub_storage) { if (!isset($options[$key])) { unset($storage[$key]); } if (isset($options[$key]['contains'])) { $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']); } } } /** * {@inheritdoc} */ public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) { if ($check && !is_array($options)) { return; } if (!isset($definition)) { $definition = $this->defineOptions(); } foreach ($options as $key => $value) { if (is_array($value)) { // Ignore arrays with no definition. if (!$all && empty($definition[$key])) { continue; } if (!isset($storage[$key]) || !is_array($storage[$key])) { $storage[$key] = []; } // If we're just unpacking our known options, and we're dropping an // unknown array (as might happen for a dependent plugin fields) go // ahead and drop that in. if (!$all && isset($definition[$key]) && !isset($definition[$key]['contains'])) { $storage[$key] = $value; continue; } $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : [], $all, FALSE); } elseif ($all || !empty($definition[$key])) { $storage[$key] = $value; } } } /** * {@inheritdoc} */ public function destroy() { unset($this->view, $this->display, $this->query); } /** * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { // Some form elements belong in a fieldset for presentation, but can't // be moved into one because of the $form_state->getValues() hierarchy. Those // elements can add a #fieldset => 'fieldset_name' property, and they'll // be moved to their fieldset during pre_render. $form['#pre_render'][] = [get_class($this), 'preRenderAddFieldsetMarkup']; } /** * {@inheritdoc} */ public function validateOptionsForm(&$form, FormStateInterface $form_state) {} /** * {@inheritdoc} */ public function submitOptionsForm(&$form, FormStateInterface $form_state) {} /** * {@inheritdoc} */ public function query() {} /** * {@inheritdoc} */ public function themeFunctions() { return $this->view->buildThemeFunctions($this->definition['theme']); } /** * {@inheritdoc} */ public function validate() { return []; } /** * {@inheritdoc} */ public function summaryTitle() { return $this->t('Settings'); } /** * {@inheritdoc} */ public function pluginTitle() { // Short_title is optional so its defaults to an empty string. if (!empty($this->definition['short_title'])) { return $this->definition['short_title']; } return $this->definition['title']; } /** * {@inheritdoc} */ public function usesOptions() { return $this->usesOptions; } /** * {@inheritdoc} */ public function globalTokenReplace($string = '', array $options = []) { return \Drupal::token()->replace($string, ['view' => $this->view], $options); } /** * Replaces Views' tokens in a given string. The resulting string will be * sanitized with Xss::filterAdmin. * * @param $text * Unsanitized string with possible tokens. * @param $tokens * Array of token => replacement_value items. * * @return string */ protected function viewsTokenReplace($text, $tokens) { if (!strlen($text)) { // No need to run filterAdmin on an empty string. return ''; } if (empty($tokens)) { return Xss::filterAdmin($text); } $twig_tokens = []; foreach ($tokens as $token => $replacement) { // Twig wants a token replacement array stripped of curly-brackets. // Some Views tokens come with curly-braces, others do not. // @todo: https://www.drupal.org/node/2544392 if (strpos($token, '{{') !== FALSE) { // Twig wants a token replacement array stripped of curly-brackets. $token = trim(str_replace(['{{', '}}'], '', $token)); } // Check for arrays in Twig tokens. Internally these are passed as // dot-delimited strings, but need to be turned into associative arrays // for parsing. if (strpos($token, '.') === FALSE) { // We need to validate tokens are valid Twig variables. Twig uses the // same variable naming rules as PHP. // @see http://php.net/manual/language.variables.basics.php assert(preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $token) === 1, 'Tokens need to be valid Twig variables.'); $twig_tokens[$token] = $replacement; } else { $parts = explode('.', $token); $top = array_shift($parts); assert(preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $top) === 1, 'Tokens need to be valid Twig variables.'); $token_array = [array_pop($parts) => $replacement]; foreach (array_reverse($parts) as $key) { // The key could also be numeric (array index) so allow that. 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.'); $token_array = [$key => $token_array]; } if (!isset($twig_tokens[$top])) { $twig_tokens[$top] = []; } $twig_tokens[$top] += $token_array; } } if ($twig_tokens) { // Use the unfiltered text for the Twig template, then filter the output. // Otherwise, Xss::filterAdmin could remove valid Twig syntax before the // template is parsed. $build = [ '#type' => 'inline_template', '#template' => $text, '#context' => $twig_tokens, '#post_render' => [ function ($children, $elements) { return Xss::filterAdmin($children); }, ], ]; // Currently you cannot attach assets to tokens with // Renderer::renderPlain(). This may be unnecessarily limiting. Consider // using Renderer::executeInRenderContext() instead. // @todo: https://www.drupal.org/node/2566621 return (string) $this->getRenderer()->renderPlain($build); } else { return Xss::filterAdmin($text); } } /** * {@inheritdoc} */ public function getAvailableGlobalTokens($prepared = FALSE, array $types = []) { $info = \Drupal::token()->getInfo(); // Site and view tokens should always be available. $types += ['site', 'view']; $available = array_intersect_key($info['tokens'], array_flip($types)); // Construct the token string for each token. if ($prepared) { $prepared = []; foreach ($available as $type => $tokens) { foreach (array_keys($tokens) as $token) { $prepared[$type][] = "[$type:$token]"; } } return $prepared; } return $available; } /** * {@inheritdoc} */ public function globalTokenForm(&$form, FormStateInterface $form_state) { $token_items = []; foreach ($this->getAvailableGlobalTokens() as $type => $tokens) { $item = [ '#markup' => $type, 'children' => [], ]; foreach ($tokens as $name => $info) { $item['children'][$name] = "[$type:$name]" . ' - ' . $info['name'] . ': ' . $info['description']; } $token_items[$type] = $item; } $form['global_tokens'] = [ '#type' => 'details', '#title' => $this->t('Available global token replacements'), ]; $form['global_tokens']['list'] = [ '#theme' => 'item_list', '#items' => $token_items, '#attributes' => [ 'class' => ['global-tokens'], ], ]; } /** * {@inheritdoc} */ public static function preRenderAddFieldsetMarkup(array $form) { foreach (Element::children($form) as $key) { $element = $form[$key]; // In our form builder functions, we added an arbitrary #fieldset property // to any element that belongs in a fieldset. If this form element has // that property, move it into its fieldset. if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) { $form[$element['#fieldset']][$key] = $element; // Remove the original element this duplicates. unset($form[$key]); } } return $form; } /** * {@inheritdoc} */ public static function preRenderFlattenData($form) { foreach (Element::children($form) as $key) { $element = $form[$key]; if (!empty($element['#flatten'])) { foreach (Element::children($element) as $child_key) { $form[$child_key] = $form[$key][$child_key]; } // All done, remove the now-empty parent. unset($form[$key]); } } return $form; } /** * {@inheritdoc} */ public function calculateDependencies() { return []; } /** * {@inheritdoc} */ public function getProvider() { $definition = $this->getPluginDefinition(); return $definition['provider']; } /** * Makes an array of languages, optionally including special languages. * * @param int $flags * (optional) Flags for which languages to return (additive). Options: * - \Drupal\Core\Language::STATE_ALL (default): All languages * (configurable and default). * - \Drupal\Core\Language::STATE_CONFIGURABLE: Configurable languages. * - \Drupal\Core\Language::STATE_LOCKED: Locked languages. * - \Drupal\Core\Language::STATE_SITE_DEFAULT: Add site default language; * note that this is not included in STATE_ALL. * - \Drupal\views\Plugin\views\PluginBase::INCLUDE_NEGOTIATED: Add * negotiated language types. * - \Drupal\views\Plugin\views\PluginBase::INCLUDE_ENTITY: Add * entity row language types. Note that these are only supported for * display options, not substituted in queries. * @param array|null $current_values * The currently-selected options in the list, if available. * * @return array * An array of language names, keyed by the language code. Negotiated and * special languages have special codes that are substituted in queries by * PluginBase::queryLanguageSubstitutions(). * Only configurable languages and languages that are in $current_values are * included in the list. */ protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $current_values = NULL) { $manager = \Drupal::languageManager(); $languages = $manager->getLanguages($flags); $list = []; // The entity languages should come first, if requested. if ($flags & PluginBase::INCLUDE_ENTITY) { $list['***LANGUAGE_entity_translation***'] = $this->t('Content language of view row'); $list['***LANGUAGE_entity_default***'] = $this->t('Original language of content in view row'); } // STATE_SITE_DEFAULT comes in with ID set // to LanguageInterface::LANGCODE_SITE_DEFAULT. // Since this is not a real language, surround it by '***LANGUAGE_...***', // like the negotiated languages below. if ($flags & LanguageInterface::STATE_SITE_DEFAULT) { $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName(); // The language name may have already been translated, no need to // translate it again. // @see Drupal\Core\Language::filterLanguages(). if (!$name instanceof TranslatableMarkup) { $name = $this->t($name); } $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name; // Remove site default language from $languages so it's not added // twice with the real languages below. unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]); } // Add in negotiated languages, if requested. if ($flags & PluginBase::INCLUDE_NEGOTIATED) { $types_info = $manager->getDefinedLanguageTypesInfo(); $types = $manager->getLanguageTypes(); // We only go through the configured types. foreach ($types as $id) { if (isset($types_info[$id]['name'])) { $name = $types_info[$id]['name']; // Surround IDs by '***LANGUAGE_...***', to avoid query collisions. $id = '***LANGUAGE_' . $id . '***'; $list[$id] = $this->t('@type language selected for page', ['@type' => $name]); } } if (!empty($current_values)) { foreach ($types_info as $id => $type) { $id = '***LANGUAGE_' . $id . '***'; // If this (non-configurable) type is among the current values, // add that option too, so it is not lost. If not among the current // values, skip displaying it to avoid user confusion. if (isset($type['name']) && !isset($list[$id]) && in_array($id, $current_values)) { $list[$id] = $this->t('@type language selected for page', ['@type' => $type['name']]); } } } } // Add real languages. foreach ($languages as $id => $language) { $list[$id] = $language->getName(); } return $list; } /** * Returns substitutions for Views queries for languages. * * This is needed so that the language options returned by * PluginBase::listLanguages() are able to be used in queries. It is called * by the Views module implementation of hook_views_query_substitutions() * to get the language-related substitutions. * * @return array * An array in the format of hook_views_query_substitutions() that gives * the query substitutions needed for the special language types. */ public static function queryLanguageSubstitutions() { $changes = []; $manager = \Drupal::languageManager(); // Handle default language. $default = $manager->getDefaultLanguage()->getId(); $changes[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $default; // Handle negotiated languages. $types = $manager->getDefinedLanguageTypesInfo(); foreach ($types as $id => $type) { if (isset($type['name'])) { $changes['***LANGUAGE_' . $id . '***'] = $manager->getCurrentLanguage($id)->getId(); } } return $changes; } /** * Returns the render API renderer. * * @return \Drupal\Core\Render\RendererInterface */ protected function getRenderer() { if (!isset($this->renderer)) { $this->renderer = \Drupal::service('renderer'); } return $this->renderer; } }