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