diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/views/display/RestExport.php b/core/modules/rest/lib/Drupal/rest/Plugin/views/display/RestExport.php index b28b4770854c4d9f26e4f4380826b37fadebee80..c6055e2c1407dee03ad396cc0baa7e6bc95870a9 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/views/display/RestExport.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/views/display/RestExport.php @@ -7,7 +7,7 @@ namespace Drupal\rest\Plugin\views\display; - +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\ContentNegotiation; @@ -99,13 +99,15 @@ class RestExport extends PathPluginBase { * The route provider * @param \Drupal\Core\State\StateInterface $state * The state key value store. + * @param \Drupal\Core\Form\FormBuilderInterface $form_error + * The form builder. * @param \Drupal\Core\ContentNegotiation $content_negotiation * The content negotiation library. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, ContentNegotiation $content_negotiation, Request $request) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state); + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, FormBuilderInterface $form_error, ContentNegotiation $content_negotiation, Request $request) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state, $form_error); $this->contentNegotiation = $content_negotiation; $this->request = $request; } @@ -120,6 +122,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('router.route_provider'), $container->get('state'), + $container->get('form_builder'), $container->get('content_negotiation'), $container->get('request') ); diff --git a/core/modules/rest/tests/Drupal/rest/Tests/CollectRoutesTest.php b/core/modules/rest/tests/Drupal/rest/Tests/CollectRoutesTest.php index 5b78e769e3a82c21e0585cc3bad3015efa2f3a2e..db0e724fdce70a97f4284a4e7a2ebfd84c6ae761 100644 --- a/core/modules/rest/tests/Drupal/rest/Tests/CollectRoutesTest.php +++ b/core/modules/rest/tests/Drupal/rest/Tests/CollectRoutesTest.php @@ -90,6 +90,9 @@ protected function setUp() { ->getMock(); $container->set('plugin.manager.views.style', $style_manager); + $form_builder = $this->getMock('Drupal\Core\Form\FormBuilderInterface'); + $container->set('form_builder', $form_builder); + \Drupal::setContainer($container); $this->restExport = RestExport::create($container, array(), "test_routes", array()); diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php index bf4e008c3916f6783dfeaef9da369d1e3d9ff6d5..baa777307d98963230d9c009e20d16ffc8d8d3b1 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php @@ -7,6 +7,7 @@ namespace Drupal\views\Plugin\views\display; +use Drupal\Core\Form\FormErrorInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Routing\RouteCompiler; use Drupal\Core\Routing\RouteProviderInterface; @@ -39,6 +40,13 @@ abstract class PathPluginBase extends DisplayPluginBase implements DisplayRouter */ protected $state; + /** + * The form error helper. + * + * @var \Drupal\Core\Form\FormErrorInterface + */ + protected $formError; + /** * Constructs a PathPluginBase object. * @@ -52,12 +60,15 @@ abstract class PathPluginBase extends DisplayPluginBase implements DisplayRouter * The route provider. * @param \Drupal\Core\State\StateInterface $state * The state key value store. + * @param \Drupal\Core\Form\FormErrorInterface $form_error + * The form error helper. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, FormErrorInterface $form_error) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->routeProvider = $route_provider; $this->state = $state; + $this->formError = $form_error; } /** @@ -69,7 +80,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('router.route_provider'), - $container->get('state') + $container->get('state'), + $container->get('form_builder') ); } @@ -395,8 +407,9 @@ public function validateOptionsForm(&$form, &$form_state) { parent::validateOptionsForm($form, $form_state); if ($form_state['section'] == 'path') { - if (strpos($form_state['values']['path'], '%') === 0) { - form_error($form['path'], $form_state, t('"%" may not be used for the first segment of a path.')); + $errors = $this->validatePath($form_state['values']['path']); + foreach ($errors as $error) { + $this->formError->setError($form['path'], $form_state, $error); } // Automatically remove '/' and trailing whitespace from path. @@ -415,4 +428,44 @@ public function submitOptionsForm(&$form, &$form_state) { } } + /** + * Validates the path of the display. + * + * @param string $path + * The path to validate. + * + * @return array + * A list of error strings. + */ + protected function validatePath($path) { + $errors = array(); + if (strpos($path, '%') === 0) { + $errors[] = $this->t('"%" may not be used for the first segment of a path.'); + } + + $path_sections = explode('/', $path); + // Symfony routing does not allow to use numeric placeholders. + // @see \Symfony\Component\Routing\RouteCompiler + $numeric_placeholders = array_filter($path_sections, function ($section) { + return (preg_match('/^%(.*)/', $section, $matches) + && is_numeric($matches[1])); + }); + if (!empty($numeric_placeholders)) { + $errors[] = $this->t("Numeric placeholders may not be used. Please use plain placeholders (%)."); + } + return $errors; + } + + /** + * {@inheritdoc} + */ + public function validate() { + $errors = parent::validate(); + + $errors += $this->validatePath($this->getOption('path')); + + return $errors; + } + + } diff --git a/core/modules/views/tests/Drupal/views/Tests/Plugin/display/PathPluginBaseTest.php b/core/modules/views/tests/Drupal/views/Tests/Plugin/display/PathPluginBaseTest.php index 95f140ebfe8c45c5731ee65cb03c6b5ec4ee0d27..596b80545cbfe7c9da437527d1c501c16a629d43 100644 --- a/core/modules/views/tests/Drupal/views/Tests/Plugin/display/PathPluginBaseTest.php +++ b/core/modules/views/tests/Drupal/views/Tests/Plugin/display/PathPluginBaseTest.php @@ -47,6 +47,13 @@ class PathPluginBaseTest extends UnitTestCase { */ protected $state; + /** + * The mocked form error. + * + * @var \Drupal\Core\Form\FormErrorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $formError; + public static function getInfo() { return array( 'name' => 'Display: Path plugin base.', @@ -63,8 +70,9 @@ protected function setUp() { $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); $this->state = $this->getMock('\Drupal\Core\State\StateInterface'); + $this->formError = $this->getMock('Drupal\Core\Form\FormErrorInterface'); $this->pathPlugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') - ->setConstructorArgs(array(array(), 'path_base', array(), $this->routeProvider, $this->state)) + ->setConstructorArgs(array(array(), 'path_base', array(), $this->routeProvider, $this->state, $this->formError)) ->setMethods(NULL) ->getMock(); $this->setupContainer(); diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayPath.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayPath.php index 8435589cfe30ba720d45974c71a700e6f6c676fe..8eb3e6da2acf6438881c9f06b4bbd945717ff6d2 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayPath.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayPath.php @@ -30,6 +30,14 @@ public static function getInfo() { } public function testPathUI() { + $this->doBasicPathUITest(); + $this->doAdvancedPathsValidationTest(); + } + + /** + * Tests basic functionality in configuring a view. + */ + protected function doBasicPathUITest() { $this->drupalGet('admin/structure/views/view/test_view'); // Add a new page display and check the appearing text. @@ -44,6 +52,21 @@ public function testPathUI() { $this->assertLink(t('View @display', array('@display' => 'Page')), 0, 'view page link found on the page.'); } + /** + * Tests a couple of invalid path patterns. + */ + protected function doAdvancedPathsValidationTest() { + $url = 'admin/structure/views/nojs/display/test_view/page_1/path'; + + $this->drupalPostForm($url, array('path' => '%/magrathea'), t('Apply')); + $this->assertUrl($url); + $this->assertText('"%" may not be used for the first segment of a path.'); + + $this->drupalPostForm($url, array('path' => 'user/%1/example'), t('Apply')); + $this->assertUrl($url); + $this->assertText("Numeric placeholders may not be used. Please use plain placeholders (%)."); + } + /** * Tests deleting a page display that has no path. */ diff --git a/core/modules/views_ui/tests/Drupal/views_ui/Tests/ViewListBuilderTest.php b/core/modules/views_ui/tests/Drupal/views_ui/Tests/ViewListBuilderTest.php index 902b615ad7c68ff769e4eeebc19c90eacc9c1954..73f5013e7c89dbd29b80a9c9c99efbd2ea93a992 100644 --- a/core/modules/views_ui/tests/Drupal/views_ui/Tests/ViewListBuilderTest.php +++ b/core/modules/views_ui/tests/Drupal/views_ui/Tests/ViewListBuilderTest.php @@ -79,9 +79,10 @@ public function testBuildRowEntityList() { ); $route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); $state = $this->getMock('\Drupal\Core\State\StateInterface'); + $form_builder = $this->getMock('Drupal\Core\Form\FormBuilderInterface'); $page_display = $this->getMock('Drupal\views\Plugin\views\display\Page', array('initDisplay', 'getPath'), - array(array(), 'default', $display_manager->getDefinition('page'), $route_provider, $state) + array(array(), 'default', $display_manager->getDefinition('page'), $route_provider, $state, $form_builder) ); $page_display->expects($this->any()) ->method('getPath')