Commit 18209994 authored by catch's avatar catch

Issue #2029509 by ekes, dawehner: Add a generic entity argument validation plugin.

parent 98353809
......@@ -9,19 +9,12 @@
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Annotation\ViewsArgumentValidator;
use Drupal\Core\Annotation\Translation;
use Drupal\views\Plugin\views\argument_validator\ArgumentValidatorPluginBase;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Validate whether an argument is an acceptable node.
*
* @ViewsArgumentValidator(
* id = "taxonomy_term",
* title = @Translation("Taxonomy term")
* )
* Adds legacy vocabulary handling to standard Entity Argument validation..
*/
class Term extends ArgumentValidatorPluginBase {
class Term extends Entity {
/**
* Overrides \Drupal\views\Plugin\views\Plugin\views\PluginBase::init().
......@@ -40,166 +33,4 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o
}
}
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['vids'] = array('default' => array());
$options['type'] = array('default' => 'tid');
$options['transform'] = array('default' => FALSE, 'bool' => TRUE);
return $options;
}
public function buildOptionsForm(&$form, &$form_state) {
$vocabularies = entity_load_multiple('taxonomy_vocabulary');
$options = array();
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = array(
'#type' => 'checkboxes',
'#prefix' => '<div id="edit-options-validate-argument-vocabulary-wrapper">',
'#suffix' => '</div>',
'#title' => t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#description' => t('If you wish to validate for specific vocabularies, check them; if none are checked, all terms will pass.'),
);
$form['type'] = array(
'#type' => 'select',
'#title' => t('Filter value type'),
'#options' => array(
'tid' => t('Term ID'),
'tids' => t('Term IDs separated by , or +'),
'name' => t('Term name'),
'convert' => t('Term name converted to Term ID'),
),
'#default_value' => $this->options['type'],
'#description' => t('Select the form of this filter value; if using term name, it is generally more efficient to convert it to a term ID and use Taxonomy: Term ID rather than Taxonomy: Term Name" as the filter.'),
);
$form['transform'] = array(
'#type' => 'checkbox',
'#title' => t('Transform dashes in URL to spaces in term name filter values'),
'#default_value' => $this->options['transform'],
);
}
public function submitOptionsForm(&$form, &$form_state, &$options = array()) {
// Filter unselected items so we don't unnecessarily store giant arrays.
$options['vids'] = array_filter($options['vids']);
}
public function validateArgument($argument) {
$vocabularies = array_filter($this->options['vids']);
$type = $this->options['type'];
$transform = $this->options['transform'];
switch ($type) {
case 'tid':
if (!is_numeric($argument)) {
return FALSE;
}
// @todo Deal with missing addTag('term access') that was removed when
// the db_select that was replaced by the entity_load.
$term = entity_load('taxonomy_term', $argument);
if (!$term) {
return FALSE;
}
$this->argument->validated_title = check_plain($term->label());
return empty($vocabularies) || !empty($vocabularies[$term->bundle()]);
case 'tids':
// An empty argument is not a term so doesn't pass.
if (empty($argument)) {
return FALSE;
}
$tids = new stdClass();
$tids->value = $argument;
$tids = $this->breakPhrase($argument, $tids);
if ($tids->value == array(-1)) {
return FALSE;
}
$test = drupal_map_assoc($tids->value);
$titles = array();
// check, if some tids already verified
static $validated_cache = array();
foreach ($test as $tid) {
if (isset($validated_cache[$tid])) {
if ($validated_cache[$tid] === FALSE) {
return FALSE;
}
else {
$titles[] = $validated_cache[$tid];
unset($test[$tid]);
}
}
}
// if unverified tids left - verify them and cache results
if (count($test)) {
$result = entity_load_multiple('taxonomy_term', $test);
foreach ($result as $term) {
if ($vocabularies && empty($vocabularies[$term->bundle()])) {
$validated_cache[$term->id()] = FALSE;
return FALSE;
}
$titles[] = $validated_cache[$term->id()] = check_plain($term->label());
unset($test[$term->id()]);
}
}
// Remove duplicate titles
$titles = array_unique($titles);
$this->argument->validated_title = implode($tids->operator == 'or' ? ' + ' : ', ', $titles);
// If this is not empty, we did not find a tid.
return empty($test);
case 'name':
case 'convert':
$terms = entity_load_multiple_by_properties('taxonomy_term', array('name' => $argument));
$term = reset($terms);
if ($transform) {
$term->name = str_replace(' ', '-', $term->name);
}
if ($term && (empty($vocabularies) || !empty($vocabularies[$term->bundle()]))) {
if ($type == 'convert') {
$this->argument->argument = $term->id();
}
$this->argument->validated_title = check_plain($term->label());
return TRUE;
}
return FALSE;
}
}
public function processSummaryArguments(&$args) {
$type = $this->options['type'];
$transform = $this->options['transform'];
if ($type == 'convert') {
$arg_keys = array_flip($args);
$result = entity_load_multiple('taxonomy_term', $args);
if ($transform) {
foreach ($result as $term) {
$term->name = str_replace(' ', '-', $term->name);
}
}
foreach ($result as $tid => $term) {
$args[$arg_keys[$tid]] = $term;
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\taxonomy\Plugin\views\argument_validator\TermName.
*/
namespace Drupal\taxonomy\Plugin\views\argument_validator;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\Core\Entity\EntityManager;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Validates whether a term name is a valid term argument.
*
* @ViewsArgumentValidator(
* id = "taxonomy_term_name",
* title = @Translation("Taxonomy term name"),
* entity_type = "taxonomy_term"
* )
*/
class TermName extends Entity {
/**
* The taxonomy term storage controller.
*
* @var \Drupal\taxonomy\TermStorageControllerInterface
*/
protected $termStorageController;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_manager);
// Not handling exploding term names.
$this->multipleCapable = FALSE;
$this->termStorageController = $entity_manager->getStorageController('taxonomy_term');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['transform'] = array('default' => FALSE, 'bool' => TRUE);
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, &$form_state) {
parent::buildOptionsForm($form, $form_state);
$form['transform'] = array(
'#type' => 'checkbox',
'#title' => t('Transform dashes in URL to spaces in term name filter values'),
'#default_value' => $this->options['transform'],
);
}
/**
* {@inheritdoc}
*/
public function validateArgument($argument) {
if ($this->options['transform']) {
$argument = str_replace('-', ' ', $argument);
}
$terms = $this->termStorageController->loadByProperties(array('name' => $argument));
if (!$terms) {
// Returned empty array no terms with the name.
return FALSE;
}
// Not knowing which term will be used if more than one is returned check
// each one.
foreach ($terms as $term) {
if (!$this->validateEntity($term)) {
return FALSE;
}
}
return TRUE;
}
}
......@@ -489,3 +489,16 @@ function views_taxonomy_set_breadcrumb(&$breadcrumb, &$argument) {
$breadcrumb[$path] = check_plain($parent->label());
}
}
/**
* Implements hook_views_plugins_argument_validator_alter().
*
* Extend the generic entity argument validator.
*
* @see \Drupal\views\Plugin\views\argument_validator\Entity
*/
function taxonomy_views_plugins_argument_validator_alter(array &$plugins) {
$plugins['entity:taxonomy_term']['title'] = t('Taxonomy term ID');
$plugins['entity:taxonomy_term']['class'] = 'Drupal\taxonomy\Plugin\views\argument_validator\Term';
$plugins['entity:taxonomy_term']['provider'] = 'taxonomy';
}
......@@ -87,7 +87,9 @@ display:
field: term_node_tid_depth
default_action: 'not found'
exception:
value: all
title_enable: true
title: All
title_enable: true
title: '%1'
default_argument_type: fixed
......@@ -95,17 +97,27 @@ display:
format: default_summary
specify_validation: true
validate:
type: taxonomy_term
type: 'entity:taxonomy_term'
fail: 'not found'
validate_options:
access: '1'
operation: view
multiple: '1'
bundles: { }
depth: '0'
break_phrase: true
plugin_id: taxonomy_index_tid_depth
relationship: none
group_type: group
admin_label: ''
default_argument_options: { }
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options: { }
validate_options: { }
summary_options:
base_path: ''
count: '1'
items_per_page: '25'
override: false
provider: taxonomy
term_node_tid_depth_modifier:
id: term_node_tid_depth_modifier
......
<?php
/**
* @file
* Contains \Drupal\views\Plugin\Derivative\ViewsEntityArgumentValidator.
*/
namespace Drupal\views\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DerivativeBase;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides views argument validator plugin definitions for all entity types.
*
* @ingroup views_argument_validator_plugins
*
* @see \Drupal\views\Plugin\views\argument_validator\Entity
*/
class ViewsEntityArgumentValidator extends DerivativeBase implements ContainerDerivativeInterface {
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* The string translation.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translationManager;
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = array();
/**
* Constructs an ViewsEntityArgumentValidator object.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
* The string translation.
*/
public function __construct($base_plugin_id, EntityManager $entity_manager, TranslationInterface $translation_manager) {
$this->basePluginId = $base_plugin_id;
$this->entityManager = $entity_manager;
$this->translationManager = $translation_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.entity'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions(array $base_plugin_definition) {
$entity_info = $this->entityManager->getDefinitions();
$this->derivatives = array();
foreach ($entity_info as $entity_type => $entity_info) {
$this->derivatives[$entity_type] = array(
'id' => 'entity:' . $entity_type,
'provider' => 'views',
'title' => $entity_info['label'],
'help' => $this->t('Validate @label', array('@label' => $entity_info['label'])),
'entity_type' => $entity_type,
'class' => $base_plugin_definition['class'],
);
}
return $this->derivatives;
}
/**
* Translates a string to the current language or to a given language.
*
* See the t() documentation for details.
*/
protected function t($string, array $args = array(), array $options = array()) {
return $this->translationManager->translate($string, $args, $options);
}
}
......@@ -252,10 +252,12 @@ public function buildOptionsForm(&$form, &$form_state) {
'#type' => 'container',
'#fieldset' => 'argument_present',
);
// Validator options include derivatives with :. These are sanitized for js
// and reverted on submission.
$form['validate']['type'] = array(
'#type' => 'select',
'#title' => t('Validator'),
'#default_value' => $this->options['validate']['type'],
'#default_value' => static::encodeValidatorId($this->options['validate']['type']),
'#states' => array(
'visible' => array(
':input[name="options[specify_validation]"]' => array('checked' => TRUE),
......@@ -288,8 +290,10 @@ public function buildOptionsForm(&$form, &$form_state) {
$plugin = $this->getPlugin('argument_validator', $id);
if ($plugin) {
if ($plugin->access() || $this->options['validate']['type'] == $id) {
$form['validate']['options'][$id] = array(
'#prefix' => '<div id="edit-options-validate-options-' . $id . '-wrapper">',
// Sanitize ID for js.
$sanitized_id = static::encodeValidatorId($id);
$form['validate']['options'][$sanitized_id] = array(
'#prefix' => '<div id="edit-options-validate-options-' . $sanitized_id . '-wrapper">',
'#suffix' => '</div>',
'#type' => 'item',
// Even if the plugin has no options add the key to the form_state.
......@@ -297,14 +301,14 @@ public function buildOptionsForm(&$form, &$form_state) {
'#states' => array(
'visible' => array(
':input[name="options[specify_validation]"]' => array('checked' => TRUE),
':input[name="options[validate][type]"]' => array('value' => $id),
':input[name="options[validate][type]"]' => array('value' => $sanitized_id),
),
),
'#id' => 'edit-options-validate-options-' . $id,
'#id' => 'edit-options-validate-options-' . $sanitized_id,
'#default_value' => array(),
);
$plugin->buildOptionsForm($form['validate']['options'][$id], $form_state);
$validate_types[$id] = $info['title'];
$plugin->buildOptionsForm($form['validate']['options'][$sanitized_id], $form_state);
$validate_types[$sanitized_id] = $info['title'];
}
}
}
......@@ -346,10 +350,12 @@ public function validateOptionsForm(&$form, &$form_state) {
$plugin->validateOptionsForm($form['summary']['options'][$summary_id], $form_state, $form_state['values']['options']['summary']['options'][$summary_id]);
}
$validate_id = $form_state['values']['options']['validate']['type'];
$sanitized_id = $form_state['values']['options']['validate']['type'];
// Correct ID for js sanitized version.
$validate_id = static::decodeValidatorId($sanitized_id);
$plugin = $this->getPlugin('argument_validator', $validate_id);
if ($plugin) {
$plugin->validateOptionsForm($form['validate']['options'][$default_id], $form_state, $form_state['values']['options']['validate']['options'][$validate_id]);
$plugin->validateOptionsForm($form['validate']['options'][$default_id], $form_state, $form_state['values']['options']['validate']['options'][$sanitized_id]);
}
}
......@@ -379,11 +385,13 @@ public function submitOptionsForm(&$form, &$form_state) {
$form_state['values']['options']['summary_options'] = $options;
}
$validate_id = $form_state['values']['options']['validate']['type'];
$sanitized_id = $form_state['values']['options']['validate']['type'];
// Correct ID for js sanitized version.
$form_state['values']['options']['validate']['type'] = $validate_id = static::decodeValidatorId($sanitized_id);
$plugin = $this->getPlugin('argument_validator', $validate_id);
if ($plugin) {
$options = &$form_state['values']['options']['validate']['options'][$validate_id];
$plugin->submitOptionsForm($form['validate']['options'][$validate_id], $form_state, $options);
$options = &$form_state['values']['options']['validate']['options'][$sanitized_id];
$plugin->submitOptionsForm($form['validate']['options'][$sanitized_id], $form_state, $options);
// Copy the now submitted options to their final resting place so they get saved.
$form_state['values']['options']['validate_options'] = $options;
}
......@@ -840,7 +848,7 @@ public function summarySort($order, $by = NULL) {
* @param $data
* The query results for the row.
*/
public function summaryArgument($data) {
public function summaryArgument($data) {
return $data->{$this->base_alias};
}
......@@ -1100,6 +1108,35 @@ public static function preRenderMoveArgumentOptions($form) {
return $form;
}
/**
* Sanitize validator options including derivatives with : for js.
*
* Reason and alternative: http://drupal.org/node/2035345
*
* @param string $id
* The identifier to be sanitized.
*
* @return string
* The sanitized identifier.
*
* @see decodeValidatorId().
*/
public static function encodeValidatorId($id) {
return str_replace(':', '---', $id);
}
/**
* Revert sanititized validator options.
*
* @param string $id
* The santitized identifier to be reverted.
*
* @return string
* The original identifier.
*/
public static function decodeValidatorId($id) {
return str_replace('---', ':', $id);
}
}
/**
......
<?php
/**
* @file
* Contains \Drupal\views\Plugin\views\argument_validator\Entity.
*/
namespace Drupal\views\Plugin\views\argument_validator;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a argument validator plugin for each entity type.
*
* @ViewsArgumentValidator(
* id = "entity",
* derivative = "Drupal\views\Plugin\Derivative\ViewsEntityArgumentValidator"
* )
*
* @see \Drupal\views\Plugin\Derivative\ViewsEntityArgumentValidator
*/
class Entity extends ArgumentValidatorPluginBase {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Boolean if this validator can handle multiple arguments.
*/
protected $multipleCapable;
/**
* Constructs an \Drupal\views\Plugin\views\argument_validator\Entity object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
$this->multipleCapable = TRUE;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.entity')
);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['bundles'] = array('default' => array());
$options['access'] = array('default' => FALSE, 'bool' => TRUE);
$options['operation'] = array('default' => 'view');
$options['multiple'] = array('default' => FALSE, 'bool' => TRUE);
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, &$form_state) {
parent::buildOptionsForm($form, $form_state);
$entity_type = $this->definition['entity_type'];
// Derivative IDs are all entity:entity_type. Sanitized for js.
// The ID is converted back on submission.
$sanitized_id = ArgumentPluginBase::encodeValidatorId($this->definition['id']);
$entity_definitions = $this->entityManager->getDefinitions();
$bundle_type = $entity_definitions[$entity_type]['entity_keys']['bundle'];
// If the entity has bundles, allow option to restrict to bundle(s).
if ($bundle_type) {
$bundles = entity_get_bundles($entity_type);
$bundle_options = array();
foreach ($bundles as $bundle_id => $bundle_info) {
$bundle_options[$bundle_id] = $bundle_info['label'];
}
$bundles_title = empty($entity_definitions[$entity_type]['bundle_label']) ? t('Bundles') : $entity_definitions[$entity_type]['bundle_label'];
if (in_array('Drupal\Core\Entity\ContentEntityInterface', class_implements($entity_definitions[$entity_type]['class']))) {
$fields = $this->entityManager->getFieldDefinitions($entity_type);
}
$bundle_name = (empty($fields) || empty($fields[$bundle_type]['label'])) ? t('bundles') : $fields[$bundle_type]['label'];
$form['bundles'] = array(
'#title' => $bundles_title,
'#default_value' => $this->options['bundles'],
'#type' => 'checkboxes',
'#options' => $bundle_options,
'#description' => t('Restrict to one or more %bundle_name. If none selected all are allowed.', array('%bundle_name' => $bundle_name)),