From 182099941fba47a0d1e6aae76e8da8e4070292cd Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Wed, 4 Dec 2013 12:18:02 +0000 Subject: [PATCH] Issue #2029509 by ekes, dawehner: Add a generic entity argument validation plugin. --- .../Plugin/views/argument_validator/Term.php | 175 +------------- .../views/argument_validator/TermName.php | 91 +++++++ core/modules/taxonomy/taxonomy.views.inc | 13 + .../views/config/views.view.taxonomy_term.yml | 20 +- .../ViewsEntityArgumentValidator.php | 108 +++++++++ .../views/argument/ArgumentPluginBase.php | 63 ++++- .../views/argument_validator/Entity.php | 214 +++++++++++++++++ .../Plugin/argument_validator/EntityTest.php | 222 ++++++++++++++++++ 8 files changed, 717 insertions(+), 189 deletions(-) create mode 100644 core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/TermName.php create mode 100644 core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsEntityArgumentValidator.php create mode 100644 core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Entity.php create mode 100644 core/modules/views/tests/Drupal/views/Tests/Plugin/argument_validator/EntityTest.php diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php index ea1fa3a1495d..56a4701fd13f 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php @@ -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; - } - } - } - } diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/TermName.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/TermName.php new file mode 100644 index 000000000000..56475f9d7a91 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/TermName.php @@ -0,0 +1,91 @@ +<?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; + } + +} diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc index 5074ee90cb37..713d3243b836 100644 --- a/core/modules/taxonomy/taxonomy.views.inc +++ b/core/modules/taxonomy/taxonomy.views.inc @@ -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'; +} diff --git a/core/modules/views/config/views.view.taxonomy_term.yml b/core/modules/views/config/views.view.taxonomy_term.yml index 78b5078d3dc5..40ca1427934a 100644 --- a/core/modules/views/config/views.view.taxonomy_term.yml +++ b/core/modules/views/config/views.view.taxonomy_term.yml @@ -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 diff --git a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsEntityArgumentValidator.php b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsEntityArgumentValidator.php new file mode 100644 index 000000000000..15b2d448b679 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsEntityArgumentValidator.php @@ -0,0 +1,108 @@ +<?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); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php index 36c8ab358474..de8a22f42a89 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php @@ -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); + } } /** diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Entity.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Entity.php new file mode 100644 index 000000000000..b647691bbab9 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Entity.php @@ -0,0 +1,214 @@ +<?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)), + ); + } + + // Offer the option to filter by access to the entity in the argument. + $form['access'] = array( + '#type' => 'checkbox', + '#title' => t('Validate user has access to the %name', array('%name' => $entity_definitions[$entity_type]['label'])), + '#default_value' => $this->options['access'], + ); + $form['operation'] = array( + '#type' => 'radios', + '#title' => t('Access operation to check'), + '#options' => array('view' => t('View'), 'update' => t('Edit'), 'delete' => t('Delete')), + '#default_value' => $this->options['operation'], + '#states' => array( + 'visible' => array( + ':input[name="options[validate][options][' . $sanitized_id . '][access]"]' => array('checked' => TRUE), + ), + ), + ); + + // If class is multiple capable give the option to validate single/multiple. + if ($this->multipleCapable) { + $form['multiple'] = array( + '#type' => 'radios', + '#title' => t('Multiple arguments'), + '#options' => array( + 0 => t('Single ID', array('%type' => $entity_definitions[$entity_type]['label'])), + 1 => t('One or more IDs separated by , or +', array('%type' => $entity_definitions[$entity_type]['label'])), + ), + '#default_value' => (string) $this->options['multiple'], + ); + } + } + + /** + * {@inheritdoc} + */ + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { + // Filter out unused options so we don't store giant unnecessary arrays. + $options['bundles'] = array_filter($options['bundles']); + } + + /** + * {@inheritdoc} + */ + public function validateArgument($argument) { + $entity_type = $this->definition['entity_type']; + + if ($this->options['multiple']) { + // At this point only interested in individual IDs no matter what type, + // just splitting by the allowed delimiters. + $ids = array_filter(preg_split('/[,+ ]/', $argument)); + } + elseif ($argument) { + $ids = array($argument); + } + // No specified argument should be invalid. + else { + $ids = array(); + return FALSE; + } + + $entities = $this->entityManager->getStorageController($entity_type)->loadMultiple($ids); + // Validate each id => entity. If any fails break out and return false. + foreach ($ids as $id) { + // There is no entity for this ID. + if (!isset($entities[$id])) { + return FALSE; + } + if (!$this->validateEntity($entities[$id])) { + return FALSE; + } + } + + return TRUE; + } + + /** + * Validates an individual entity against class access settings. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * + * @return bool + * True if validated. + */ + protected function validateEntity(EntityInterface $entity) { + // If access restricted by entity operation. + if ($this->options['access'] && ! $entity->access($this->options['operation'])) { + return FALSE; + } + // If restricted by bundle. + $bundles = $this->options['bundles']; + if (count($bundles) && empty($bundles[$entity->bundle()])) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/core/modules/views/tests/Drupal/views/Tests/Plugin/argument_validator/EntityTest.php b/core/modules/views/tests/Drupal/views/Tests/Plugin/argument_validator/EntityTest.php new file mode 100644 index 000000000000..15d44f610aca --- /dev/null +++ b/core/modules/views/tests/Drupal/views/Tests/Plugin/argument_validator/EntityTest.php @@ -0,0 +1,222 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Tests\Plugin\argument_validator\EntityTest. + */ + +namespace Drupal\views\Tests\Plugin\argument_validator; + +use Drupal\Tests\UnitTestCase; +use Drupal\views\Plugin\views\argument_validator\Entity; + +/** + * Tests the generic entity argument validator. + * + * @group Drupal + * @group Views + * + * @see \Drupal\views\Plugin\views\argument_validator\Entity + */ +class EntityTest extends UnitTestCase { + + /** + * The view executable. + * + * @var \Drupal\views\ViewExecutable + */ + protected $executable; + + /** + * The view display. + * + * @var \Drupal\views\Plugin\views\display\DisplayPluginBase + */ + protected $display; + + /** + * The entity manager. + * + * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** + * The tested argument validator. + * + * @var \Drupal\views\Plugin\views\argument_validator\Entity + */ + protected $argumentValidator; + + public static function getInfo() { + return array( + 'name' => 'Argument validator: Entity', + 'description' => 'Tests the generic entity argument validator.', + 'group' => 'Views Plugin', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityManager = $this->getMockBuilder('Drupal\Core\Entity\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $mock_entity = $this->getMockBuilder('Drupal\Core\Entity\Entity') + ->disableOriginalConstructor() + ->setMethods(array('bundle', 'access')) + ->getMock(); + $mock_entity->expects($this->any()) + ->method('bundle') + ->will($this->returnValue('test_bundle')); + $mock_entity->expects($this->any()) + ->method('access') + ->will($this->returnValueMap(array( + array('test_op', NULL, TRUE), + array('test_op_2', NULL, FALSE), + array('test_op_3', NULL, TRUE), + ))); + + $mock_entity_bundle_2 = $this->getMockBuilder('Drupal\Core\Entity\Entity') + ->disableOriginalConstructor() + ->setMethods(array('bundle', 'access')) + ->getMock(); + $mock_entity_bundle_2->expects($this->any()) + ->method('bundle') + ->will($this->returnValue('test_bundle_2')); + $mock_entity_bundle_2->expects($this->any()) + ->method('access') + ->will($this->returnValueMap(array( + array('test_op_3', NULL, TRUE), + ))); + + + $storage_controller = $this->getMock('Drupal\Core\Entity\EntityStorageControllerInterface'); + + // Setup values for IDs passed as strings or numbers. + $value_map = array( + array(array(), array()), + array(array(1), array(1 => $mock_entity)), + array(array('1'), array(1 => $mock_entity)), + array(array(1, 2), array(1 => $mock_entity, 2 => $mock_entity_bundle_2)), + array(array('1', '2'), array(1 => $mock_entity, 2 => $mock_entity_bundle_2)), + array(array(2), array(2 => $mock_entity_bundle_2)), + array(array('2'), array(2 => $mock_entity_bundle_2)), + ); + $storage_controller->expects($this->any()) + ->method('loadMultiple') + ->will($this->returnValueMap($value_map)); + + $this->entityManager->expects($this->any()) + ->method('getStorageController') + ->will($this->returnValue($storage_controller)); + + $this->executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $this->display = $this->getMockBuilder('Drupal\views\Plugin\views\display\DisplayPluginBase') + ->disableOriginalConstructor() + ->getMock(); + + $definition = array( + 'entity_type' => 'entity_test', + ); + + $this->argumentValidator = new Entity(array(), 'entity_test', $definition, $this->entityManager); + } + + /** + * Tests the validate argument method with no access and bundles. + * + * @see \Drupal\views\Plugin\views\argument_validator\Entity::validateArgument() + */ + public function testValidateArgumentNoAccess() { + $options = array(); + $options['access'] = FALSE; + $options['bundles'] = array(); + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertFalse($this->argumentValidator->validateArgument(3)); + $this->assertFalse($this->argumentValidator->validateArgument('')); + + $this->assertTrue($this->argumentValidator->validateArgument(1)); + $this->assertTrue($this->argumentValidator->validateArgument(2)); + $this->assertFalse($this->argumentValidator->validateArgument('1,2')); + } + + /** + * Tests the validate argument method with access and no bundles. + * + * @see \Drupal\views\Plugin\views\argument_validator\Entity::validateArgument() + */ + public function testValidateArgumentAccess() { + $options = array(); + $options['access'] = TRUE; + $options['bundles'] = array(); + $options['operation'] = 'test_op'; + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertFalse($this->argumentValidator->validateArgument(3)); + $this->assertFalse($this->argumentValidator->validateArgument('')); + + $this->assertTrue($this->argumentValidator->validateArgument(1)); + + $options = array(); + $options['access'] = TRUE; + $options['bundles'] = array(); + $options['operation'] = 'test_op_2'; + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertFalse($this->argumentValidator->validateArgument(3)); + $this->assertFalse($this->argumentValidator->validateArgument('')); + + $this->assertFalse($this->argumentValidator->validateArgument(1)); + $this->assertFalse($this->argumentValidator->validateArgument(2)); + } + + /** + * Tests the validate argument method with bundle checking. + */ + public function testValidateArgumentBundle() { + $options = array(); + $options['access'] = FALSE; + $options['bundles'] = array('test_bundle' => 1); + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertTrue($this->argumentValidator->validateArgument(1)); + $this->assertFalse($this->argumentValidator->validateArgument(2)); + } + + /** + * Tests the validate argument method with multiple argument splitting. + */ + public function testValidateArgumentMultiple() { + $options = array(); + $options['access'] = TRUE; + $options['bundles'] = array(); + $options['operation'] = 'test_op'; + $options['multiple'] = TRUE; + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertTrue($this->argumentValidator->validateArgument('1')); + $this->assertFalse($this->argumentValidator->validateArgument('2')); + + $this->assertFalse($this->argumentValidator->validateArgument('1,2')); + $this->assertFalse($this->argumentValidator->validateArgument('1+2')); + + $options = array(); + $options['access'] = TRUE; + $options['bundles'] = array(); + $options['operation'] = 'test_op_3'; + $options['multiple'] = TRUE; + $this->argumentValidator->init($this->executable, $this->display, $options); + + $this->assertTrue($this->argumentValidator->validateArgument('1,2')); + $this->assertTrue($this->argumentValidator->validateArgument('1+2')); + } + +} -- GitLab